Comando `git history`
(lalitm.com)- O comando experimental
git history, introduzido entre o Git 2.54 e 2.55, automatiza a modificação de commits antigos e a reorganização de branches derivadas sem alterar o fluxo de trabalho tradicional do Git fixupincorpora alterações já preparadas em um commit antigo e depois faz rebase automaticamente de todos os branches locais derivados desse commit para o novo hashrewordaltera a mensagem de um commit antigo, esplitdivide as mudanças de um commit em dois commits no nível de hunk, reorganizando em seguida os commits e branches posteriores- Os três subcomandos usam processamento atômico, recusando operações com potencial de conflito para evitar que a árvore de trabalho fique quebrada no meio do processo, mas não funcionam em históricos com commits de merge
- Embora não ofereça o tratamento de conflitos como entidade de primeira classe no
jj, desfazer baseado em log de operações nem a modelagem da working copy como commit, ele permite usar vários recursos de edição de histórico no estilo dojjdiretamente na distribuição principal do Git, sem instalação separada
Por que editar histórico no Git tradicional é incômodo
- Ao lidar com várias mudanças em paralelo, é preciso alternar constantemente entre branches e commits, e durante um
rebase -ia árvore de trabalho pode acabar em um estado incompleto - O substituto do Git
jjé frequentemente citado como solução para esse problema, mas mesmo tentando usá-lo por alguns dias a cada três meses ao longo de um ano e meio, no fim a pessoa sempre acaba voltando ao fluxo tradicional do Git git historyé um comando experimental incluído na distribuição principal do Git, então pode ser testado sem instalar ferramentas extras- Os subcomandos são três:
fixup,rewordesplit
fixup: corrigir ao mesmo tempo um commit antigo e branches derivados
git history fixupcorrige partes erradas de um commit antigo e faz rebase automático dos branches derivados desse commit- Basta preparar as alterações com
git add, como em um processo comum de correção, e então executargit history fixup <commit>para incorporar as mudanças preparadas ao commit alvo - É parecido com combinar
git commit --fixupcom rebase autosquash, mas com a diferença de que também atualiza outros branches que contêm o commit alvo - Enquanto
git rebase --update-refsmove apenas referências dentro do escopo do rebase atual,git historyencontra e reescreve todos os branches locais derivados do commit alvo- Se necessário, é possível limitar o escopo da atualização apenas ao branch atual
- Não funciona em históricos com commits de merge, então pode ser difícil aplicá-lo a alguns modos de uso do Git
- Ao incorporar uma correção em
B, é criado um novoB*com hash diferente, e os commits acima dele,CeD, também são recriados comoC*eD*- As pontas dos branches
feat-1efeat-2também passam a apontar para os novos commits
- As pontas dos branches
Processamento atômico e restrição a conflitos
- Os três subcomandos são todos processados de forma atômica, sem deixar a árvore de trabalho em um estado parcialmente quebrado
- A atomicidade é garantida recusando a própria operação quando ela pode causar conflitos
- Isso é mais limitado funcionalmente do que o
jj, que trata conflitos como objetos de primeira classe, permitindo manter o estado de conflito durante o rebase e resolvê-lo depois - As limitações do
git historyforam aplicadas de forma intencional para que a reescrita de histórico não se transforme em uma operação de preservação de estado- Se o Git passar a oferecer suporte a conflitos de primeira classe no futuro, essa limitação poderá ser removida
reword: mudar a mensagem de commit sem mexer na árvore de trabalho
git history rewordaltera a mensagem de um commit antigo e reorganiza automaticamente os commits e branches acima dele- Ao executar
git history reword <commit>, o editor abre com a mensagem atual; depois de editar e salvar, a pilha de commits posteriores é reconstruída com os novos hashes - É útil para ajustar mensagens de commits anteriores à estrutura atual quando o design muda durante o desenvolvimento iterativo
- Funciona da mesma forma que
fixup, exceto pelo fato de alterar apenas a mensagem do commit, e não o conteúdo da árvore rewordnão mexe no índice nem na árvore de trabalho; ele altera apenas o grafo de commits- Até commits de branches que não estão em checkout podem ser modificados sem atrapalhar o trabalho atual
- Se a mensagem de
Bfor alterada, o hash muda, então o commit seguinteCé recriado comoC*efeat-1também passa a seguir o novo commit
split: dividir um commit em dois
git history splitpermite selecionar interativamente as mudanças de um commit para separá-las em dois commits- Ao executar
git history split <commit>, abre-se um prompt para escolher, no nível de hunk, quais mudanças do commit alvo serão mantidas no primeiro commit- Os hunks selecionados e mantidos formam o primeiro commit
- Os hunks restantes entram no segundo commit
- É parecido com
git add -p, mas sem exigir o procedimento complexo de combinar isso comgit rebase - Se um
Bque continha duas mudanças sem relação for dividido, surgemB1eB2, e o commit posteriorCé reconstruído comoC*sobre esses dois commits - Entre os três comandos, este é o mais específico em termos de uso, mas é útil quando realmente é preciso separar um commit
Diferenças de funcionalidade em relação ao jj
git historynão substitui todo o modelo de trabalho oferecido pelojj- Em contrapartida,
git historyoferece, dentro do Git tradicional, edição de commits antigos, mudança de mensagens, divisão de commits e atualização de branches derivados, permitindo trazer algumas vantagens dojjpara o fluxo de trabalho atual - Como ainda existe a possibilidade de afrouxar essas limitações no futuro, há espaço para melhorias adicionais em lançamentos futuros do Git
1 comentários
Comentários do Hacker News
O Git era incômodo, mas melhorou depois de ler os três primeiros capítulos do livro Pro Git, disponibilizado gratuitamente. Ele oferece um excelente modelo mental sobre o funcionamento interno do Git e, para o bem ou para o mal, a interface do usuário do Git reflete diretamente sua estrutura interna, então, depois de entender isso, tudo passou a fazer sentido
git checkoutnão perde alterações ainda não adicionadas ao Git ao trocar de branch, mas foi projetado para reverter mudanças ao restaurar outra versão de um arquivo; como isso refletiria diretamente a estrutura interna?Além disso, a inconsistência da CLI, que dificulta encontrar a sintaxe exata da linha de comando mesmo quando você sabe a operação e o comando desejados internamente, também torna o Git difícil de usar
git commit -am “Changes”sozinho não bastaExiste
git rebase --abort, e também dá para criar uma tag antes do rebase e depois voltar comgit reset --hard $set_tag. Como o estado anterior não desaparece, não há motivo para ter medo de rebase interativotempoubefore-rebaseexatamente para isso. Uso como se fossem tags, mas branches são mais fáceis de mover, então antes de umrebase -igrande normalmente executogit branch before-rebase/some-featureNa prática, quase nunca precisei de
git reset before-rebase, mas muitas vezes compareigit log -p before-rebasecom a branch depois do rebase para verificar alterações não intencionais no processo de resolução de conflitosgit reflog?jj undogit rebase, logo em seguida digito--abortEu não me vejo como coder ou programador, mas aprender Git me deu algo como um superpoder de organização na forma de pensar. Uso para uma enorme variedade de coisas: sites, projetos de design, eletrônica, composição musical, repositório pessoal de conhecimento, scripts de administração remota, gerenciamento de configuração, trechos de código etc. Não é perfeito, mas recomendo que todo mundo com quem trabalho aprenda
Em Getting Things Done, de David Allen, há a ideia de que você precisa de um sistema confiável para reunir e organizar seus pensamentos. Eu uso OmniFocus para tarefas, projetos e listas, mas confio no Git para organizar e preservar software e alguns dados que crio no computador
Em teoria eu gosto muito desses comandos e também uso sempre os equivalentes no
jj, mas não consegui descobrir como assinar os commits modificados porgit history. Mesmo olhando o manual e o código-fonte do Git, se eu modifico commits já assinados com GPG usandogit history reword HEAD~, todas as assinaturas desaparecemComo eu queria manter as assinaturas dos commits, no fim voltei ao tradicional
git rebase -igit historyé um recurso experimental, então vale a pena reportar como bugMesmo sem comandos novos, venho fazendo grandes alterações no histórico local de commits do Git há mais de 10 anos sem muita dificuldade. Tenho convicção de que um dos principais motivos de Git e rebase parecerem difíceis é um problema de interface do usuário: precisar descobrir por conta própria as opções exatas da linha de comando e como elas funcionam
No Windows, a caixa de diálogo de rebase do TortoiseGit era poderosa e ao mesmo tempo fácil de usar. Bastava clicar com o botão direito no commit de base no log de commits e escolher
"Rebase onto this", e, como em https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm..., ficava claro como reordenar, dividir, combinar e editar commits; se cancelasse no meio, tudo voltava ao estado originalPorém, como ele usa uma extensão de shell, há cliques demais; eu preferiria mais se fosse um app independente com abas, embora isso pareça vir dos princípios de design herdados do TortoiseSVN. Também testei o Git Extensions por um tempo, mas não encontrei recursos que usava com frequência, ou já estava acostumado ao TortoiseGit e acabei não migrando. A tela de log pode ser vista em https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht...
My Computer. Pelo que entendo, isso atingiu o auge no XP, e a partir do Vista a Microsoft mudou a direção para algo mais difícil de estenderAinda assim, até hoje não entendo por que algum menu do VC++6 criava um objeto de shell parecido com uma pasta em vez de uma lista comum, e o mostrava na caixa de diálogo de abrir arquivo. Era um assistente para adicionar componentes ao projeto, e eu às vezes ia procurar aquela pasta falsa em
My Computerenquanto a caixa de diálogo estava abertafixupjá era possível comgit commit --fixupou, antes disso, comgit rebase -i, erewordtambém dava para fazer comgit rebase -iParece que se acumulou feedback suficiente de que era incômodo editar um arquivo para decidir a ação de cada commit, então foi adicionado um comando de nível mais alto que encapsula a funcionalidade existente
Ferramentas de linha de comando dificultam o acesso à saída anterior quando você executa o próximo comando. Mesmo que eu queira fazer cherry-pick de um commit visto em
git log, se o hash já não estiver mais visível no terminal, não dá para digitá-lo só de memória; já uma interface com estado consegue manter várias interações em paralelo e preservar informações anterioresO Git tem muita informação de estado para lembrar: branch atual, branch anterior, arquivos modificados, commits ainda não enviados por push etc. Se você não dominar ferramentas ao redor, como exibição de branch em plugins de shell e autocompletar, várias abas e divisões de janela, cópia entre abas e busca no autocompletar, usar Git pode se tornar muito desagradável
Vejo a fraqueza prática do
jjbasicamente só na integração com Git. Ele formaliza linguagens específicas de domínio para conjuntos de arquivos, conjuntos de revisões e templates, usando isso de forma consistente em toda a configuração e na CLI, e fornece conceitos poderosos como interface sem expor diretamente a estrutura internaMas ele é flexível demais, então a interoperabilidade com Git depende bastante de convenções e disciplina, e como quase todo estado do
jjpode ser expresso em Git, é fácil bagunçar o repositório remoto. Para recuperar isso, é preciso entender Git em profundidadePara um novo projeto solo, vale a pena tentar
jj. Em condições favoráveis, ele passa uma sensação parecida com o Git inicial comparado ao Subversion, e, se você primeiro se acostumar com a forma de trabalhar do própriojj, talvez fique um pouco mais preparado para a difícil integração posterior com Git. O contexto relacionado está em https://news.ycombinator.com/item?id=48903054jjtambém tem backends que não são Git, mas no momento o único explicitamente indicado como adequado para ambiente operacional é o backend Git. Umjjsem integração com Git ainda está mais para conceito do que para realidadeGosto do
jje do seu modelo mental de controle de versão, então vou continuar usando, mas às vezes preciso descer para o Git para concluir o trabalho, então ele parece mais um wrapper conveniente sobre o Git do que um novo sistema de gerenciamento de configuração. Se o Git adicionar comandos que deem suporte ao fluxo de trabalho dojj, será difícil encontrar motivo para instalar ojjseparadamenteA forma como
git history fixupfaz rebase automático de todos os branches descendentes não combina com meu fluxo de trabalho. Eu preservo cada versão de um branch de funcionalidade comomyfeature.1,myfeature.2etc. para encontrar versões antigas quando o comportamento muda em uma versão nova, egit reflognão substitui issoPor exemplo, se o problema surgiu entre
myfeature.58emyfeature.59e cada funcionalidade tem 15 commits, comgit range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59dá para ver as diferenças entre as duas versões commit a commit, mesmo que os commits de base sejam diferentes. Por isso, não quero rebase automático de todos os branches que contenham um determinado commit, e a abordagem de usar tags para versões antigas também não funcionou bem para mimO Git recente implementou, como comandos separados com menos atrito, três tarefas comuns de
git rebase --interactive, e isso parece funcionar apenas quando não há conflitosgit historyé realmente útil. Eu prefiro mover commits visualmente com rebase interativo, e não gostaria de acabar apontando por engano um hash de commit órfão que sumiu do branch local atual como alvo degit historyNão entendo por que dedicar tanto esforço a deixar o histórico do Git perfeitamente arrumado. No fim, ninguém relê commits individuais, então basta fazer squash de tudo antes do merge
"oops"e"fix", eu rejeito na hora.Os commits contam a história do trabalho, então é preciso oferecer um histórico de commits legível para que o revisor não precise se esforçar ainda mais para entender a intenção
Se a base de código e as funcionalidades forem bem pequenas, talvez dê para fazer squash de tudo, mas eu prefiro uma forma que permita verificar o fluxo real das mudanças e fazer cherry-pick ou reverter commits seletivamente
Entendi que, com
Datualmente em checkout, ao mudarBparaB', ogit rebase --update-refsdeixariaEfora do intervalo do rebase como está e criaria apenasB' → C' → D', mantendo oB → Eexistente.Já o
git history fixupatualizaria atéE, gerandoE'? Se sim, queria saber se também existe alguma forma de fazer a atualização de todos os branches descendentes usando ogit rebase, ao qual estou acostumado há décadasEcomo está. Nunca usei--update-refsdiretamente, mas, se a documentação estiver correta, o resultado deve ser o terceiro grafo, gerando atéE', então o comportamento desejado já é possível comgit rebase