Git Interactive Rebase, Squash, Amend e Outras Formas de Reescrever a História

Tute Costa
Traduzido por Neil Carvalho
Este artigo também está disponível em: English and français

“Por favor, faça um rebase em cima do main e nós iremos mesclar seu pull request”.

“Você pode agrupar seus commits para termos um histórico git limpo e reversível?”.

“Você pode reescrever a mensagem do seu commit para descrever melhor o problema que ele resolve e como o resolve?”.

Questões como essas são frequentemente feitas em pull requests. Vamos ver por que elas existem, como executá-las e seus possíveis problemas.

Altere a última mensagem de commit com git amend

Uma das reescritas de histórico mais simples que podemos fazer com o git é alterar a mensagem do commit mais recente. Digamos que logo após fazer um commit você encontre um erro de digitação na descrição ou encontre uma maneira melhor de descrever o conjunto de alterações.

Para modificar a mensagem do commit, execute o comando amend:

git commit --amend

Ele irá abrir um editor com a última mensagem de commit, para que você possa modificá-la. Depois de salvar, um novo commit será criado com as mesmas alterações e a nova mensagem, substituindo o commit com a mensagem anterior.

Modificar as últimas alterações de arquivos do commit com git amend

O git amend também pode ser útil para incluir arquivos que você esqueceu de rastrear ou modificações nos arquivos que você acabou de fazer. Para fazer isso, você pode adicionar as alterações e depois executar o amend:

git add README.md config/routes.rb
git rm notes.txt
git commit --amend

Além de editar a mensagem do commit, o novo commit conterá as alterações especificadas com os comandos git add e git rm.

Você também pode editar a autoria. Por exemplo:

git commit --amend --author="Tute Costa and Dan Croak <tute+dan@thoughtbot.com>"

Modifique os arquivos do commit mais recente sem editar a mensagem do commit

Para adicionar ou remover arquivos/alterações do último commit sem editar sua mensagem, adicione a flag --no-edit:

git commit -m "Update README with latest deploy changes"
git add README.md config/routes.rb
git rm notes.txt
git commit --amend --no-edit

O novo commit terá os arquivos alterados e manterá a mensagem anterior.

Conquista Desbloqueada! Agora você pode alterar o último commit do seu repositório para incluir novas alterações nos arquivos e/ou melhorar a mensagem do commit. Mas não comece a alterar tudo antes de entender a última seção deste post intitulado PERIGO.

Modificar outras mensagens de commit

Gostaria de falar sobre isso agora, mas precisamos entender uma ferramenta mais geral antes. Preste atenção!

Tudo será mais fácil assim que lermos sobre…

Rebase interativo

git rebase reaplica commits, um por vez, em ordem, de sua branch atual para outra. Ele aceita várias opções e parâmetros, então essa é uma explicação básica, suficiente para preencher o espaço entre os comentários do StackOverflow ou GitHub e as páginas de manual do git.

Uma opção interessante que ele aceita é --interactive (-i, abreviando), que irá abrir um editor com uma lista dos commits que estão prestes a serem alterados. Esta lista aceita comandos, permitindo que o usuário edite a lista antes de iniciar a ação de rebase.

Vamos ver um exemplo.

Modificar as últimas mensagens de commit

Vamos supor que eu queira reescrever qualquer um dos últimos 4 commits deste blog. Eu executo git rebase -i HEAD~4, e aqui está o que eu vejo:

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

# Rebase 8db7e8b..fa20af3 onto 8db7e8b
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Nós vemos os últimos quatro commits, do mais antigo ao mais recente. Vê o comentário abaixo da lista de commits? Bom trabalho explicando, git!

pick (p, abreviando) é a ação padrão. Neste caso, ele reaplicaria o commit como está, sem alterações em seu conteúdo ou mensagem. Salvar (e executar) este arquivo não fará alterações no repositório.

Se eu digo reword (r de forma abreviada) em um commit que quero editar:

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
r 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

Quando salvo e saio do editor, o git seguirá os comandos descritos, colocando-me novamente no editor, como se eu tivesse feito ammend no commit 3e7ee36. Eu edito aquela mensagem de commit, salvo e saio do editor, e aqui está a saída:

robots.thoughtbot.com tc-git-rebase % git rebase -i HEAD~4
[detached HEAD dd62a66] Stop all the highlighting
 Author: Caleb Hearth
 Date: Fri Oct 31 10:52:26 2014 -0500
 2 files changed, 39 insertions(+), 42 deletions(-)
Successfully rebased and updated refs/heads/tc-git-rebase.

Agora Caleb diz em sua mensagem de commit “Stop all the highlighting”, independente de você ser uma criança ou não.

Conquista Desbloqueada! Você agora pode alterar a mensagem de qualquer commit que desejar. Você pode fazê-lo, apenas certifique-se de entender a seção PERIGO.

Agrupar commits juntos

Outros dois comandos que o rebase interativo nos oferece são:

  • squash (s para abreviar), que combina o commit com o anterior (o da linha acima)
  • fixup (f para abreviar), que funciona como “squash”, mas descarta a mensagem deste commit

Vamos continuar trabalhando no exemplo de rebase que trabalhamos anteriormente. Tínhamos quatro commits, o meu para este post de blog e três outros do Caleb, relacionados ao seu post anterior sobre PGP:

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

Digamos que eu queira agrupar os commits do Caleb, porque eles pertencem ao mesmo conjunto lógico de alterações, e assim podemos usar git revert facilmente se decidirmos que preferimos não ter essas alterações neste repositório.

Vamos querer manter a mensagem do primeiro commit e agrupar os dois commits subsequentes no anterior. Altero pick para squash onde apropriado:

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
s 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

Salvo, e sou levado ao editor para decidir a mensagem do commit dos três commits agrupados (veja como eles são concatenados um após o outro):

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

Besides demystifying a relatively complex tool, protocol, and etiquette,
this post is intended to help with problems such as the one outlined in
this tweet:

> Emailed sensitive info to someone with PGP. They replied, with my
> original email, all in clear text. They didn't realize it.

# This is the 2nd commit message:

Fix PostChecker::Post#urls

# This is the 3rd commit message:

Hey kids, stop all the highlighting

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author:    Caleb Hearth
# Date:      Tue Sep 2 09:39:07 2014 -0500
#
# rebase in progress; onto 71d4789
# You are currently editing a commit while rebasing branch 'tc-git-rebase' on '71d4789'.

Decido remover a mensagem do terceiro commit e adicionar uma nota mais relevante à mensagem do segundo commit. Salvo no editor, e os quatro commits se transformaram em dois: o de Caleb e o meu em seguida. Ótimo!

Poderíamos ter usado o comando fixup, se tivéssemos percebido anteriormente que queríamos as mudanças, mas não a mensagem do commit, do terceiro commit. Nesse caso, os comandos seriam:

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
f 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

Ao salvar, o editor já teria incluído a mensagem do terceiro commit comentada para nós:

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

Besides demystifying a relatively complex tool, protocol, and etiquette,
this post is intended to help with problems such as the one outlined in
this tweet:

> Emailed sensitive info to someone with PGP. They replied, with my
> original email, all in clear text. They didn't realize it.

- https://twitter.com/csoghoian/status/505366816685060096

* Use examples that reasonably approximates `gpg2` output.
* Reject backslash from <abbr title="Uniform Resource Locator">URL</abbr>s
  before checking them

  Markdown allows backslashes in <abbr title="Uniform Resource
  Locator">URL</abbr>s to escape characters, which are passed directly to the
  <abbr title="Uniform Resource Locator">URL</abbr>, so `http://some\_url.com`
  becomes `http://some_url.com`.

  This mitigates issues with markdown syntax highlighting thinking that
  emphasized text has started, such as `_this text_`.

# This is the 2nd commit message:

Fix PostChecker::Post#urls

# The 3rd commit message will be skipped:

#     Hey kids, stop all the highlighting

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author:    Caleb Hearth
# Date:      Tue Sep 2 09:39:07 2014 -0500
#
# rebase in progress; onto 71d4789
# You are currently editing a commit while rebasing branch 'tc-git-rebase' on
'71d4789'.

Salve e obtenha a saída:

[detached HEAD 809241b] Introduce OpenPGP and teach basic usage
 Author: Caleb Hearth
 Date: Tue Sep 2 09:39:07 2014 -0500
 2 files changed, 1429 insertions(+), 1 deletion(-)
 create mode 100644 source/posts/2014/10-31-pgp-and-you.md
Successfully rebased and updated refs/heads/tc-git-rebase.

O resultado é o mesmo: 2 commits em vez de 4, cada um com um post de blog único e diferente.

Conquista Desbloqueada! Agora você pode agrupar commits. Como sempre, esteja ciente da seção PERIGO.

Rebase em cima da main

Nós fazemos um fork de uma biblioteca open source, começamos a trabalhar em uma feature branch, e novas mudanças são feitas no repositório da biblioteca (upstream). Nosso histórico se parece com:

       A---B---C feature
     /
D---E---F---G upstream/main

A pessoa mantenedora da biblioteca nos pede para “fazer rebase em cima da main”, então corrigimos quaisquer conflitos de merge que possam surgir entre ambas as branches e mantemos nosso conjunto de mudanças junto.

Ela gostaria de ver um histórico como:

               A'--B'--C' feature
             /
D---E---F---G upstream/main

Queremos reaplicar nossos commits, um a um, em ordem, em cima da main do upstream. Parece exatamente o que o comando rebase faz!

Vamos ver quais comandos nos levariam ao cenário desejado:

# Aponta nosso `upstream` remoto para o fork original
git remote add upstream https://github.com/thoughtbot/factory_bot.git

# Busca os commits mais recentes de `upstream` (o fork original)
git fetch upstream

# Faz o checkout da nossa feature branch
git checkout feature

# Reaplica-a em cima do main do upstream
git rebase upstream/main

# Corrige conflitos, depois `git rebase --continue`, repete até concluir
# Envia para o nosso fork
git push --force origin feature

O GitHub tem um botão Sync Fork na interface web para manter o repositório local sincronizado com um repositório upstream. É um recurso útil para contribuidores de código aberto.

Conquista Desbloqueada! Sua feature branch será aplicada em cima das últimas mudanças do fork original.

E assim chegamos a…

PERIGO: Você está reescrevendo a história do git

Viu o --force no último comando git push? Isso significa que estamos sobrescrevendo o histórico do repositório. Isso é sempre seguro de fazer em commits que não compartilhamos com outros membros da equipe, ou em branches que são nossas (veja minhas iniciais no exemplo deste post de blog).

Mas se você forçar o push de edições que já foram compartilhadas com a equipe (commits que existem fora do meu repositório, como as mudanças que fiz nos commits do PGP que já foram compartilhadas), então a branch de todos fica dessincronizada. É importante mantê-los atualizados sobre as mudanças.

Uma alternativa mais suave ao force push é usar git push force with lease quando se trabalha com outros em uma branch. Isso permite fazer um force push sem o risco de sobrescrever o trabalho deles inadvertidamente.

Reescrever o histórico significa abandonar commits existentes e criar novos, que podem ser muito semelhantes, mas são diferentes. Se outros basearem trabalho em seus commits anteriores e você reescrever e forçar o push de seus commits, os membros da sua equipe terão que refazer o merge do trabalho deles (se notarem a perda potencial).

Na thoughtbot, prefixamos nossas branches com nossas iniciais, sinalizando que esses commits podem ser reescritos e outros são recomendados a fazer rebase do seu repositório local frequentemente. Quando esses commits chegam ao main, nunca os reescrevemos novamente.

Então reescreva o histórico do git e comunique-se com seus colegas de equipe sobre como sua equipe quer manter o histórico do git limpo e reversível.

Conquista Desbloqueada! Agora você sabe como reescrever o histórico do git, exercendo uma boa cidadania.