Programa
"Commit early, commit often" é um mantra popular no desenvolvimento de software quando você usa o Git. Dessa forma, você garante que cada alteração seja bem documentada, melhora a colaboração e facilita o acompanhamento da evolução do projeto. No entanto, isso também pode levar a uma superabundância de commits.
É aqui que entra em jogo a importância de eliminar os commits. Squashing commits é o processo de combinar várias entradas de commit em um único commit coeso.
Torne-se um engenheiro de dados
Por exemplo, digamos que trabalhamos em um recurso que implementa um formulário de login e criamos os quatro commits a seguir:
Depois que o recurso é concluído, para o projeto geral, esses commits são muito detalhados. Não precisamos saber no futuro que encontramos um bug que foi corrigido durante o desenvolvimento. Para garantir um histórico limpo na ramificação principal, juntamos esses commits em um único commit:
Como reduzir os commits no Git: Rebase interativo
O método mais comum para reduzir os commits é usar um rebase interativo. Nós o iniciamos usando o comando:
git rebase -i HEAD~<number_of_commits>
Substitua pelo número de commits que você deseja esmagar.
No nosso caso, temos quatro commits, portanto, o comando é:
git rebase -i HEAD~4
Ao executar esse comando, você abrirá um editor de linha de comando interativo:
A seção superior exibe os commits, enquanto a parte inferior contém comentários sobre como esmagar os commits.
Temos quatro compromissos. Para cada um deles, precisamos decidir qual comando executar. O que nos interessa são os comandos pick
(p
) e squash
(s
). Para esmagar esses quatro commits em um único commit, podemos escolher o primeiro e esmagar os três restantes.
Aplicamos os comandos modificando o texto que precede cada commit, alterando especificamente pick
para s
ou squash
para o segundo, terceiro e quarto commits. Para fazer essas edições, precisamos entrar no modo "INSERT" no editor de texto de linha de comando pressionando a tecla i
no teclado:
Depois de pressionar i
, o texto -- INSERT --
aparecerá na parte inferior, indicando que você entrou no modo de inserção. Agora, podemos mover o cursor com as teclas de seta, excluir caracteres e digitar como faríamos em um editor de texto padrão:
Quando estivermos satisfeitos com as alterações, precisaremos sair do modo de inserção pressionando a tecla Esc
no teclado. A próxima etapa é salvar nossas alterações e sair do editor. Para fazer isso, primeiro pressionamos a tecla :
para sinalizar ao editor que pretendemos executar um comando:
Na parte inferior do editor, agora vemos um ponto e vírgula :
solicitando que você insira um comando. Para salvar as alterações, usamos o comando w
, que significa "write" (escrever). Para fechar o editor, use q
, que significa "quit" (sair). Esses comandos podem ser combinados e digitados juntos wq
:
Para executar o comando, pressionamos a tecla Enter
. Essa ação fechará o editor atual e abrirá um novo, permitindo que você insira a mensagem de commit para o commit recém-esmagado. O editor exibirá uma mensagem padrão que inclui as mensagens dos quatro commits que estamos eliminando:
Recomendo que você modifique a mensagem para refletir com precisão as alterações implementadas por esses commits combinados - afinal, o objetivo do squashing é manter um histórico limpo e de fácil leitura.
Para interagir com o editor e editar a mensagem, pressione i
novamente para entrar no modo de edição e editar a mensagem de acordo com sua preferência.
Nesse caso, substituímos a mensagem de confirmação por "Implementar formulário de login". Para sair do modo de edição, pressione Esc
. Em seguida, salve as alterações pressionando :
, inserindo o comando wq
e pressionando Enter
.
Como visualizar o histórico de confirmações
Em geral, recuperar todo o histórico do commit pode ser um desafio. Para visualizar o histórico de confirmações, você pode usar o comando git log
. No exemplo mencionado, antes de realizar o squash, a execução do comando git log
exibiria o seguinte:
Para navegar na lista de commits, use as teclas de seta para cima e para baixo. Para sair, pressione q
.
Podemos usar o site git log
para confirmar o sucesso do squash. Ao executá-lo após o squash, você verá um único commit com a nova mensagem:
Empurrando o commit esmagado
O comando acima atuará no repositório local. Para atualizar o repositório remoto, precisamos enviar nossas alterações. No entanto, como alteramos o histórico do commit, precisamos forçar o push usando a opção --force
:
git push --force origin feature/login-form
Forçar o push substituirá o histórico de commits no ramo remoto e, potencialmente, interromperá outras pessoas que estejam trabalhando nesse ramo. É uma boa prática comunicar-se com a equipe antes de fazer isso
Uma maneira mais segura de forçar o envio, que reduz o risco de interromper os colaboradores, é usar a opção --force-with-lease
:
git push --force-with-lease origin feature/login-form
Essa opção garante que só forçaremos o envio se a ramificação remota não tiver sido atualizada desde a última busca ou extração.
Eliminação de commits específicos
Imagine que temos cinco compromissos:
Suponhamos que você queira manter os commits 1, 2 e 5 e eliminar os commits 3 e 4.
Ao usar o rebase interativo, os commits marcados para squashing serão combinados com o commit diretamente anterior. Nesse caso, isso significa que queremos esmagar o Commit4
para que ele se funda com o Commit3
.
Para fazer isso, precisamos iniciar um rebase interativo que inclua esses dois commits. Nesse caso, três commits são suficientes, portanto, usamos o comando:
git rebase -i HEAD~3
Em seguida, definimos Commit4
como s
para que ele seja esmagado com Commit3
:
Depois de executar esse comando e listar os commits, observamos que os commits 3 e 4 foram esmagados juntos, enquanto os demais permanecem inalterados.
Squashing de um commit específico
No comando git rebase -i HEAD~3
, a parte HEAD
é uma abreviação do commit mais recente. A sintaxe ~3
é usada para especificar um ancestral de um commit. Por exemplo, HEAD~1
refere-se à origem do commit HEAD
.
Em um rebase interativo, os commits considerados incluem todos os commits ancestrais que levam ao commit especificado no comando. Observe que o commit especificado não está incluído:
Em vez de usar o HEAD, podemos especificar um hash de confirmação diretamente. Por exemplo, Commit2
tem um hash de dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
, portanto, o comando:
git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf
iniciaria um rebase considerando todos os commits feitos após Commit2
. Portanto, se quisermos iniciar um rebase em um commit específico e incluir esse commit, podemos usar o comando:
git rebase -i <commit-hash>~1
Resolução de conflitos ao esmagar commits
Quando fazemos squash de commits, combinamos várias alterações de commit em um único commit, o que pode gerar conflitos se as alterações se sobrepuserem ou divergirem significativamente. Aqui estão alguns cenários comuns em que podem surgir conflitos:
- Mudanças sobrepostas: Se dois ou mais commits que estão sendo esmagados modificaram as mesmas linhas de um arquivo ou linhas estreitamente relacionadas, o Git pode não ser capaz de reconciliar automaticamente essas alterações.
- Estado de alterações diferentes: Se um commit adiciona um determinado trecho de código e outro commit modifica ou exclui esse mesmo trecho de código, esmagar esses commits pode levar a conflitos que precisam ser resolvidos.
- Renomeando e modificando: Se um commit renomear um arquivo e os commits subsequentes fizerem alterações no nome antigo, esmagar esses commits pode confundir o Git, causando um conflito.
- Alterações em arquivos binários: Os arquivos binários não se fundem bem usando ferramentas de comparação baseadas em texto. Se vários commits alterarem o mesmo arquivo binário e tentarmos esmagá-los, poderá ocorrer um conflito porque o Git não pode reconciliar automaticamente essas alterações.
- História complexa: Se os commits tiverem um histórico complexo com várias mesclagens, ramificações ou rebases entre eles, esmagá-los pode resultar em conflitos devido à natureza não linear das alterações.
Ao esmagar, o Git tentará aplicar cada alteração uma a uma. Se encontrar conflitos durante o processo, ele fará uma pausa e nos permitirá resolvê-los.
O conflito será marcado com os marcadores de conflito <<<<<<
e >>>>>>
. Para lidar com os conflitos, precisamos abrir os arquivos e resolver cada um manualmente, selecionando a parte do código que queremos manter.
Depois de resolver os conflitos, precisamos preparar os arquivos resolvidos usando o comando git add
. Em seguida, podemos continuar o rebase usando o seguinte comando:
git rebase --continue
Para saber mais sobre conflitos no Git, confira este tutorial sobre como resolver conflitos de mesclagem no Git.
Alternativas ao Squashing com Rebase
O comando git merge --squash
é um método alternativo ao git rebase -i
para combinar vários commits em um único commit. Esse comando é particularmente útil quando você deseja mesclar as alterações de uma ramificação na ramificação principal e, ao mesmo tempo, esmagar todos os commits individuais em um só. Aqui está uma visão geral de como você pode fazer squash usando git merge
:
- Navegamos até o ramo de destino no qual queremos incorporar as alterações.
- Executamos o comando
git merge --squash
substituindopelo nome da filial.
- Confirmamos as alterações com
git commit
para criar uma única confirmação que represente todas as alterações do ramo de recursos.
Por exemplo, digamos que você queira incorporar as alterações da ramificação feature/login-form
em main
como um único commit:
git checkout main
git merge --squash feature-branch
git commit -m "Implement login form"
Essas são as limitações dessa abordagem em comparação com git rebase -i
:
- Granularidade do controle: Menos controle sobre commits individuais. Com o rebase, podemos escolher quais commits serão mesclados, enquanto o merge força a combinação de todas as alterações em um único commit.
- Histórico intermediário: Ao usar a mesclagem, o histórico individual de commits do ramo de recursos é perdido no ramo principal. Isso pode dificultar o rastreamento das alterações incrementais feitas durante o desenvolvimento do recurso.
- Revisão pré-compromisso: Como ele prepara todas as alterações como um único conjunto de alterações, não podemos revisar ou testar cada commit individualmente antes de esmagá-lo, ao contrário do que ocorre durante um rebase interativo, em que cada commit pode ser revisado e testado em sequência.
Conclusão
A incorporação de commits frequentes e pequenos no fluxo de trabalho de desenvolvimento promove a colaboração e a documentação clara, mas também pode sobrecarregar o histórico do projeto. A redução de commits atinge um equilíbrio, preservando os marcos importantes e eliminando o ruído de pequenas alterações iterativas.
Para saber mais sobre o Git, recomendo estes recursos: