Recorte Git Commits / Squashing Git History

Eu verifico meu código em uma ramificação do Git a cada poucos minutos, e os comentários acabam sendo coisas como “Tudo quebrado começando de novo” e outros absurdos.

Então, a cada poucos minutos / horas / dias eu faço um commit sério com um comentário real como: “Corrigido bug # 22.55, 3ª vez.” Como posso separar esses dois conceitos? Eu gostaria de poder remover todos os meus frequentes commits e deixar os sérios.

Resposta editada com agora (na segunda metade desta input) a nova correção do Git1.7! action e opção --autosquash para reordenação de consolidação rápida e edição de mensagens.


Primeiro, o clássico processo de esmagamento, como feito antes do Git1.7.
(Git1.7 tem o mesmo processo, apenas tornado mais rápido pela possibilidade de reordenação automática de commit ao invés de reordenação manual, e por mensagens de squash mais limpas)

Eu gostaria de poder remover todos os meus check-ins frequentes e deixar os sérios.

Isso é chamado de commits de esmagamento .
Você tem algum bom exemplo de “comit cleaning” neste artigo do Git Ready :
(Nota: o recurso interativo rebase surgiu desde setembro de 2007 , e permite esmagar ou dividir ou remover ou reordenar commits: veja também a página do GitPro )

Uma palavra de caucanvas : faça isso apenas em commits que não tenham sido enviados para um repository externo. Se outras pessoas tiverem baseado o trabalho fora dos commits que você irá excluir, muitos conflitos podem ocorrer. Apenas não reescreva sua história se ela tiver sido compartilhada com outras pessoas.

alt text http://sofpt.miximages.com/git/squash1.png

Os últimos 4 commits seriam muito mais felizes se eles estivessem envolvidos juntos

 $ git rebase -i HEAD~4 pick 01d1124 Adding license pick 6340aaa Moving license into its own file pick ebfd367 Jekyll has become self-aware. pick 30e0ccb Changed the tagline in the binary, too. # Rebase 60709da..30e0ccb onto 60709da # # Commands: # p, pick = use commit # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # # If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. # 

rebase usando os últimos quatro commits de onde o HEAD está com HEAD~4 .
Nós vamos apenas esmagar tudo em um commit.
Então, mudar as primeiras quatro linhas do arquivo para isso fará o truque:

 pick 01d1124 Adding license squash 6340aaa Moving license into its own file squash ebfd367 Jekyll has become self-aware. squash 30e0ccb Changed the tagline in the binary, too. 

Basicamente, isso diz ao Git para combinar todos os quatro commits no primeiro commit da lista. Depois que isso é feito e salvo, outro editor aparece com o seguinte:

 # This is a combination of 4 commits. # The first commit's message is: Adding license # This is the 2nd commit message: Moving license into its own file # This is the 3rd commit message: Jekyll has become self-aware. # This is the 4th commit message: Changed the tagline in the binary, too. # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # Explicit paths specified without -i nor -o; assuming --only paths... # Not currently on any branch. # Changes to be committed: # (use "git reset HEAD ..." to unstage) # # new file: LICENSE # modified: README.textile # modified: Rakefile # modified: bin/jekyll # 

Como estamos combinando muitos commits, o Git permite que você modifique a mensagem do novo commit com base nos demais commits envolvidos no processo. Edite a mensagem como quiser e salve e saia.
Uma vez feito isso, seus commits foram esmagados com sucesso!

 Created commit 0fc4eea: Creating license file, and making jekyll self-aware. 4 files changed, 27 insertions(+), 30 deletions(-) create mode 100644 LICENSE Successfully rebased and updated refs/heads/master. 

E se olharmos a história de novo …

alt text http://sofpt.miximages.com/git/squash2.png


Nota: para fins de “commit squashing”, o Git1.7 (fevereiro de 2010) introduziu 2 novos elementos (como mencionado por Dustin no comentário):

  • git rebase -i ” aprendeu uma nova ação ” fixup ” que atrapalha a mudança, mas não afeta a mensagem de log existente.
  • git rebase -i ” também aprendeu a opção --autosquash que é útil junto com a nova ação de “correção”.

Ambas (ação de correção e opção --autosquash ) são ilustradas nesta input de blog da Thechnosorcery Networks . Essas características vêm sendo cozinhadas desde junho de 2009 e foram debatidas em dezembro do ano passado .

A ação ou diretiva de fixup é para esmagar um commit que você teria reordenado manualmente na lista de edição de confirmação de um rebase --interactive , enquanto ignorava a segunda mensagem de confirmação, o que tornaria a etapa de edição da mensagem mais rápida (você pode salvá-la: commit squashed terá apenas a primeira mensagem de commit)
A mensagem de confirmação resultante será apenas a primeira confirmação.

  # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message 

A opção --autosquash trata de tornar o processo de reordenação de confirmação automaticamente para você:

Se você sabe com que commit quer esmagar algo, pode cometer uma mensagem de “ squash! $other_commit_subject squash! $other_commit_subject ”. Então, se você executar @git rebase --interactive --autosquash commitish@ , a linha será automaticamente definida como squash e colocada abaixo do commit com o assunto $ other_commit_subject.

(Na verdade, o squash! só pode usar o começo de outra mensagem de commit)

 $ vim Foo.txt $ git commit -am "Change all the 'Bar's to 'Foo's" [topic 8374d8e] Change all the 'Bar's to 'Foo's 1 files changed, 2 insertions(+), 2 deletions(-) $ vim Bar.txt $ git commit -am "Change all the 'Foo's to 'Bar's" [topic 2d12ce8] Change all the 'Foo's to 'Bar's 1 files changed, 1 insertions(+), 1 deletions(-) $ vim Foo.txt $ git commit -am "squash! Change all the 'Bar's" [topic 259a7e6] squash! Change all the 'Bar's 1 files changed, 2 insertions(+), 1 deletions(-) 

Vejo? Aqui o terceiro commit usa apenas o começo da primeira mensagem de commit.
Uma rebase --interactive --autosquash moverá o commit esmagado abaixo do relevante:

 pick 8374d8e Change all the 'Bar's to 'Foo's squash 259a7e6 squash! Change all the 'Bar's pick 2d12ce8 Change all the 'Foo's to 'Bar's 

A edição da mensagem seria:

 # This is a combination of 2 commits. # The first commit's message is: Change all the 'Bar's to 'Foo's # This is the 2nd commit message: squash! Change all the 'Bar's 

Por padrão, significa que você manteria a operação de compactação gravada na mensagem de confirmação.
Mas com o conserto! diretiva, você pode manter essa “invisível” --autosquash na mensagem de commit, enquanto ainda se beneficia do reordenamento automático de --autosquash com a opção --autosquash (e o fato de que sua segunda mensagem de commit é baseada no primeiro commit que você quer que seja esmagada com ).

 pick 8374d8e Change all the 'Bar's to 'Foo's fixup cfc6e54 fixup! Change all the 'Bar's pick 2d12ce8 Change all the 'Foo's to 'Bar's 

A mensagem por padrão será:

 # This is a combination of 2 commits. # The first commit's message is: Change all the 'Bar's to 'Foo's # The 2nd commit message will be skipped: # fixup! Change all the 'Bar's 

Observe que o fixup! A mensagem de commit já está comentada.
Você pode salvar a mensagem como está e sua mensagem de confirmação original será mantida .
Muito útil para include alterações quando você percebe que esqueceu de adicionar parte de uma confirmação anterior .

Agora, se você quiser corrigir ou squash com base no commit anterior que você acabou de fazer, Jacob Helwig (o autor da input do blog Technosorcery Networks) recomenda os seguintes aliases:

 [alias] fixup = !sh -c 'git commit -m \"fixup! $(git log -1 --format='\\''%s'\\'' $@)\"' - squash = !sh -c 'git commit -m \"squash! $(git log -1 --format='\\''%s'\\'' $@)\"' - 

E por fazer um rebase interativo, que sempre se beneficiará do reordenamento automático dos commits que devem ser eliminados:

 [alias] ri = rebase --interactive --autosquash 

Atualização para Git 2.18 (Q2 2018): ” git rebase -i ” às vezes deixada intermediária ” # This is a combination of N commits ” mensagem destinada ao consumo humano dentro de um editor no resultado final em certos casos de canto, que foi corrigido .

Veja commit 15ef693 , commit dc4b5bc , commit e12a7ef , commit d5bc6f2 (27 abr 2018) por Johannes Schindelin ( dscho ) .
(Mesclado por Junio ​​C Hamano – gitster – em commit 4a3bf32 , 23 de maio de 2018)

rebase --skip : limpe a mensagem de commit após uma falha na correção / squash

Durante uma série de comandos de ajuste / squash, o rebase interativo cria uma mensagem de confirmação com comentários. Isso será apresentado ao usuário no editor se pelo menos um desses comandos for um squash .

Em qualquer caso, a mensagem de commit será limpa eventualmente, removendo todos os comentários intermediários, na etapa final de tal cadeia de correção / squash.

No entanto, se o último comando fixup / squash nessa cadeia falhar com conflitos de mesclagem e se o usuário decidir ignorá-lo (ou resolvê-lo para uma área de trabalho limpa e continuar o rebase), o código atual falhará ao limpar o arquivo. enviar mensagem.

Essa confirmação corrige esse comportamento.

A correção é um pouco mais complicada do que aparenta, porque não se trata apenas da questão de saber se somos ou não git rebase --skip um conserto ou squash. Também é sobre como remover a mensagem de confirmação do fixup / squash ignorada da mensagem de confirmação acumulada. E também é sobre a questão de saber se devemos deixar o usuário editar a mensagem de commit final ou não (“Houve uma squash na cadeia que não foi pulada ?”).

Por exemplo, nesse caso, queremos corrigir a mensagem de confirmação, mas não abri-la em um editor:

 pick <- succeeds fixup <- succeeds squash <- fails, will be skipped 

É aqui que o arquivo de current-fixups introduzido recentemente é realmente útil. Uma olhada rápida e podemos determinar se houve uma abóbora não pulada. Nós só precisamos ter certeza de mantê-lo atualizado com relação aos comandos de correção / squash ignorados. Como um bônus, podemos até mesmo evitar cometer desnecessariamente, por exemplo, quando houve apenas uma correção, e ela falhou, e foi ignorada.

Para corrigir apenas o bug em que a mensagem de commit final não foi limpa corretamente, mas sem consertar o resto, teria sido mais complicado do que consertar tudo de uma só vez, portanto, esse commit agrupa mais do que uma única preocupação.


O Git 2.19 (Q3 2018) corrige um bug: Quando " git rebase -i " é instruído a esmagar dois ou mais commits em um, ele rotula a mensagem de log para cada commit com seu número.
Ele corretamente chamou o primeiro de "1º commit", mas o próximo foi " commit #1 ", que estava off-by-one (!).

Veja commit dd2e36e (15 de agosto de 2018) por Phillip Wood ( phillipwood ) .
(Mesclado por Junio ​​C Hamano - gitster - em commit 36fd1e8 , 20 de agosto de 2018)

rebase -i : corrige numeração na mensagem de squash

Commit e12a7ef (" rebase -i : Handle" combinação de commits "com GETTEXT_POISON ", 2018-04-27, Git 2.18) mudou o modo como mensagens de commit individuais são rotuladas quando esmagamos commits juntos.
Ao fazer isso, uma regressão foi introduzida, onde a numeração das mensagens é desativada em um. Essa confirmação corrige isso e adiciona um teste para a numeração.

Usando Soft Reset em vez de Rebase para Squash History do GIT

Eu acho que o tamanho das respostas do VonC fala volumes – literalmente – sobre o quão complicado é o git rebase . Esta é a minha extensão de outra resposta para uma questão minha .

  1. Você tem um ticket-201 filial ticket-201 que você ramificou do master . Você quer fingir que todos os commits do ticket-201 nunca aconteceram, mas que você fez todo o trabalho de uma só vez.
  2. Reboot suave para o ponto de ramificação usando git reset --soft hash que hash deve ser um hash de confirmação que está no log do ticket-201 .
  3. Confirme suas alterações usando add e depois commit. Agora o histórico da filial terá apenas o primeiro commit e o novo com o novo material.

Inventando Histórias de Compromissos Arbitrários em Diferentes Ramos

Usando redefinições, você pode rewrite o histórico como quiser, embora suas edições perdam o charme de ter o timestamp correto. Supondo que você não se importe com isso (as vezes / datas em seus arquivos serão suficientes, talvez?), Ou se você quiser mexer com os commits, você pode seguir estes passos:

  1. Check-out de uma nova ramificação em commit0 (finja que é um hash): git checkout -b new-history commit0
  2. Agora você pode obter os arquivos de commit5 : git reset --hard commit5
  3. Volte para o seu ponto de índice: git reset --soft commit0
  4. Commit e este será o segundo commit no branch.

Essa ideia é simples, eficaz e flexível.

Usando Squash

Recentemente, tenho trabalhado em outro ramo e usando squash . A outra ramificação é chamada temp, e então eu uso git merge temp --squash para trazê-la para a ramificação real que é enviada para o servidor.

O stream de trabalho é algo assim, supondo que eu esteja trabalhando no Ticket65252 :

 git branch -d temp #remove old temp bbranch git checkout -b temp # work work work, committing all the way git checkout Ticket65252 git merge temp --squash git commit -m "Some message here" 

Vantagens sobre o uso de rebase ? Muito menos complicado.

Vantagens sobre o uso de reset --hard e, em seguida, reset --soft ? Menos confuso e ligeiramente menos propenso a erros.

Use git rebase -i para escolher e esmagar seus commits juntos.