Artigos

Combine seus commits com squash

Depois de trabalhar em muitos projetos com diferentes fluxos do Git, passei a valorizar a importância de um histórico de branches limpo. Infelizmente, a forma habitual como as pessoas usam Git não favorece esse objetivo.

Muitas vezes, o fluxo de trabalho de um desenvolvedor se parece com isto:

  1. Escolher uma issue para trabalhar
  2. Criar uma nova branch de funcionalidade
    git checkout -b my-feature
  3. Trabalhar
    git commit -m "Did X"
  4. Trabalhar um pouco mais
    git commit -m "Did Y"
  5. Já está pronto?
    git commit -m "Did Z"
  6. …
  7. Fazer push
  8. Criar uma pull request

Depois, outras pessoas revisam a pull request e ela é integrada à master ou a outra branch. A branch de destino recebe um novo commit de merge acompanhado de vários commits de trabalho em andamento.

Nunca encontrei utilidade em deixar todos esses commits no repositório. Raramente oferecem informações úteis ou interessantes sobre a funcionalidade. Normalmente não compilam corretamente nem passam nas suítes de testes automatizados. Além disso, dificultam acompanhar o histórico do repositório, então qual é o sentido? Outro problema dessa abordagem é que desestimula os desenvolvedores a registrar em commits seu código em andamento, por medo de deixar commits inúteis.

A solução é combinar seus commits com squash e fazer rebase sobre suas branches de longa duração. Ao fazer squash, você modifica o histórico do Git e substitui todos os commits de trabalho em andamento por um único commit que contém todas as alterações; depois faz rebase desse commit sobre a branch de destino. Você terá um histórico contendo apenas commits úteis, com bons títulos e descrições. Esses commits também compilarão corretamente e passarão em todos os testes.

Veja um exemplo:

Tenho um repositório Git com um arquivo test.txt vazio e um commit:

commit 15e184a2ff855d833d6456cd1cd0745205b3152d (HEAD -> master)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:22:44 2020 -0300

  Add text.txt

Vamos criar uma nova branch de funcionalidade chamada my-feature e adicionar alguns commits.

commit 2d2a2e9ee5a055653c990ee1e0a32d2e85cbf45e (HEAD -> my-feature)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:25:37 2020 -0300

  Change test.txt

commit cd11a3ddf29aaeb85fd9d4aec7ca0c27eaa9346c
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:25:27 2020 -0300

  Change test.txt

commit 15e184a2ff855d833d6456cd1cd0745205b3152d (master)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:22:44 2020 -0300

  Add text.txt

Agora vamos combinar os dois commits em um. Precisamos anotar o hash do commit imediatamente anterior ao primeiro commit da nossa branch de funcionalidade. Neste caso, é:

15e184a2ff855d833d6456cd1cd0745205b3152d

$ git rebase --interact
ive 15e184a2ff855d833d6456cd1cd0745205b3152d

Git abrirá seu editor de texto com uma mensagem parecida com esta:

pick cd11a3d Change test.txt
pick 2d2a2e9 Change test.txt

# Rebase 15e184a..2d2a2e9 onto 15e184a (2 command(s))
#
# 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
# d, drop = remove commit
#
# 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

Vou deixar o primeiro como pick e alterar o segundo para squash, salvar o arquivo e sair do editor.

Git abrirá uma nova janela do editor com a nova mensagem do commit. Altere-a para obter um bom título e uma boa descrição, salve e saia.

Vamos ver o log novamente:

commit 6bacb3d302b7cf25e179e67e0c32aec1711a2d7c (HEAD -> my-feature)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:25:27 2020 -0300

  This is a great short message

  This is a great long message.

commit 15e184a2ff855d833d6456cd1cd0745205b3152d (master)
Author: Juan Cabrera <jcabrera@sophilabs.com>
Date:   Tue Apr 21 16:22:44 2020 -0300

  Add text.txt

Agora você pode fazer push da sua branch para o remoto. Talvez precise usar --force se não for seu primeiro push. Depois, pode fazer um merge fast-forward da funcionalidade na master. Se isso não for possível, precisará fazer rebase da funcionalidade sobre a master.

Todos os provedores conhecidos de Git (por exemplo, GitLab, Bitbucket e Github) permitem exigir esse comportamento nas PRs de todas as branches (ou de um subconjunto escolhido), o que facilita bastante o processo (consulte o manual do seu provedor para saber como ativar esse recurso). Basta criar uma nova branch de funcionalidade, fazer quantos commits quiser, fazer push, criar uma pull request e integrá-la. O provedor combinará todos os seus commits em um, usará a mensagem fornecida e fará um merge fast-forward na branch de destino.

Seu histórico do Git será muito mais limpo e útil se seguir este fluxo de trabalho. É também assim que a maioria dos grandes projetos de código aberto é gerenciada.

“Combine seus commits com squash” por Juan Cabrera está sob a licença CC BY SA. Os exemplos de código-fonte estão sob a licença MIT.

Foto de Viviana Rishe.

Classificado em Pesquisa e aprendizado.

Leituras relacionadas