Skip to main content

Git do jeito fácil: altere o histórico com rebase — parte I

Escrito por
Headshot of Or Kamara

Or Kamara

9 de junho de 2020

0 minutos de leitura

Recentemente, trabalhei em um recurso grande, com várias mudanças em diferentes partes do código. Para deixar o PR desse recurso mais fácil de revisar, dividi toda a funcionalidade em vários commits. Com o tempo, logo percebi que essa divisão não bastava: havia muitas mudanças a fazer em cada um daqueles commits, seja por causa de problemas que encontrei, seja para atender aos ajustes da revisão de código.

Naquele momento, eu já conhecia o básico do mágico comando interativo git rebase, que você pode usar para “reescrever seu histórico”. Mas as mudanças que eu precisava fazer eram mais complexas do que meu conhecimento básico permitia, então comecei a explorar as opções desse comando.

Para quem ainda não conhece o comando, este é o resultado de executar git rebase -i BRANCH:

pick 271c66e feat: 1st commit
pick 51588eb feat: 2nd commit
pick 4b339f2 feat: 3rd commit

# Rebase 24aa86a..4b339f2 onto 24aa86a (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
...

Lembro perfeitamente da primeira vez que executei esse comando. Minha reação foi algo como: Tááá, o que está acontecendo aqui? Prometeram uma maneira interativa e fácil de alterar meus commits, mas tudo o que vejo é uma interface supercomplexa… Precisei literalmente dedicar um tempo para ler sobre cada uma daquelas opções, porque, depois de sair dessa tela, eu poderia perder todo o trabalho que tinha feito (#DesenvolvedorComMedo).

Até sair da tela e cancelar tudo foi uma experiência complicada… Precisei pesquisar no Google como fazer isso e descobri que era só apagar todas as linhas de commits para não haver mudanças. Tão fácil (e nada intuitivo). Meu primeiro pensamento foi: “Mas espera, não confio muito em tudo o que dizem no Google. E se eu fizer isso e todo o meu trabalho desaparecer?”

Então, decidi que não havia outra saída e comecei a investigar esses comandos.

Neste artigo, quero explicar alguns problemas que desenvolvedores precisam resolver ao trabalhar em PRs com vários commits. Não vou explicar cada detalhe desses comandos, pois há vários artigos que explicam o que acontece nos bastidores. Em vez disso, vou apresentar alguns cenários que todos nós podemos encontrar ao trabalhar com git e sugerir a solução adequada usando git rebase -i.

Como este é um assunto bastante extenso, vou dividir o artigo em duas partes. Nesta parte, vou explicar os fundamentos do Rebase e apresentar dois cenários comuns. Na segunda parte, vou apresentar mais quatro cenários comuns e algumas conclusões.

Sem mais delongas, vamos à primeira parte!

Rebase em poucas palavras

Vamos começar com o seguinte cenário: você e um amigo estão trabalhando em um novo projeto empolgante. Vocês acabaram de criar o repositório (e fazer o primeiro commit, chamado A) e agora é hora de programar. Para trabalhar com mais eficiência, vocês decidiram dividir as tarefas. Você ficou responsável pelo recurso principal, e seu amigo, pela estrutura do aplicativo.

O ponto de partida dos dois é o mesmo: o commit A. Depois de cinco horas de trabalho intenso, você quer terminar e integrar as mudanças à master. Seu amigo é o primeiro a integrar o trabalho e cria três novos commits a partir de A: B + C + D. Agora é sua vez de integrar na master todo o seu trabalho da FeatureBranch: os commits E + F + G. É assim que fica:

      E----F----G FeatureBranch
     /
    A----B----C----D master

Para testar a funcionalidade que você desenvolveu, precisa usar algumas funções da estrutura do aplicativo, mais especificamente as do commit D. Em outras palavras, você precisa alterar a base da sua branch do commit A para o commit D, como se tivesse criado a branch a partir da versão mais recente:

                     E----F----G FeatureBranch
                    /
    A----B----C----D master

É exatamente para isso que o git rebase serve: mover uma sequência de commits para uma nova base. Nos bastidores, o git cria novos commits e os aplica à base especificada.

Mais especificamente, neste artigo, vamos falar sobre o modo interativo.

Ao executar o rebase com a opção -i, você pode alterar commits individuais durante o processo básico de rebase — é aí que a mágica começa! No modo interativo, você escolhe o que fazer com cada commit: juntar, dividir, renomear, reordenar e muito mais.

Vamos entender cada uma dessas opções com alguns cenários comuns em que o git rebase pode ser útil.

Cenários comuns (e como lidar com eles!)

Vamos ver alguns cenários comuns em que você pode querer alterar o histórico do seu código e como fazer isso com o Git:

1. Alterar a mensagem de um commit antigo (reword)

Você fez várias mudanças no código e as dividiu perfeitamente em diferentes commits, mas depois percebeu um erro de digitação em uma das mensagens:

* 3d320d9 (HEAD -> feat/git-history) feat: 3rd commit
* 03d03e9 feat: 3nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

Como você provavelmente notou, a mensagem do segundo commit é 3nd commit (ops!) — vamos corrigir isso.

Se o erro estivesse no último commit, poderíamos usar o comando git commit --amend. Mas, como ele está no meio do nosso histórico do git, precisamos fazer algo mais complexo.

Neste caso, vamos usar o comando git rebase -i master (master pode ser outra branch) e escolher a opção reword para o segundo commit (a21bd1d):

pick 271c66e feat: 1st commit
reword 03d03e9 feat: 3nd commit
pick 3d320d9 feat: 3rd commit

Depois de salvar o arquivo, um novo editor será aberto para que você possa editar a mensagem do commit:

feat: 3nd commit

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sun May 17 16:28:13 2020 +0300
#
# interactive rebase in progress; onto 24aa86a
# Last commands done (2 commands done):
#    pick 271c66e feat: 1st commit
#    r 03d03e9 feat: 3nd commit
# Next command to do (1 remaining command):
#    pick 3d320d9 feat: 3rd commit
# You are currently editing a commit while rebasing branch 'feat/git-history' on '24aa86a'.
#
# Changes to be committed:
#       modified:   b.txt
#

Agora, basta alterar a primeira linha para a nova mensagem desejada, feat: 2nd commit, e salvar novamente.

Agora o histórico do git está corrigido:

* fdb5f71 (HEAD -> feat/git-history) feat: 3rd commit
* e2952e7 feat: 2nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

2. Alterar o conteúdo de um commit antigo (squash, fixup)

Você acabou de receber comentários da revisão de código de um colega e precisa fazer pequenas mudanças: adicionar um novo arquivo e alterar o conteúdo de outro que já existe.

Como você provavelmente sabe, cada commit deve representar um bloco de lógica, não apenas mudanças em arquivos. Uma coisa que devemos evitar é usar uma mensagem como CR fixes ou, no nosso caso, até mesmo really small CR fixes. O ideal é juntar todos os commits para que, no final, cada um contenha todas as mudanças relevantes, incluindo os ajustes da revisão de código.

Como fazer isso? É aí que as opções squash e fixup entram em cena.

Primeiro, vamos criar commits temporários com as mudanças necessárias:

* 17a73c1 (HEAD -> feat/git-history) CR fix: add new file to 2nd commit
* 5641a86 CR fix: content of file in 1st commit
* fdb5f71 feat: 3rd commit
* e2952e7 feat: 2nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

No exemplo acima, criamos dois novos commits:

  • 5641a86 — altera o conteúdo do arquivo criado no primeiro commit

  • 17a73c1 — adiciona um novo arquivo ao segundo commit

Mas agora não queremos manter o histórico do git desse jeito. Queremos juntar 5641a86 com 271c66e e 17a73c1 com e2952e7. Para isso, devemos executar o comando git rebase -i master. Como da última vez, esta é a primeira tela que veremos:

pick 271c66e feat: 1st commit
pick e2952e7 feat: 2nd commit
pick fdb5f71 feat: 3rd commit
pick 5641a86 cr fix: content of file in 1st commit
pick 17a73c1 cr fix: add new file to 2nd commit

Agora, vamos reordenar as linhas de acordo com a ordem desejada dos commits e usar a opção squash nos commits 5641a86 e 17a73c1:

pick 271c66e feat: 1st commit
squash 5641a86 cr fix: content of file in 1st commit
pick e2952e7 feat: 2nd commit
squash 17a73c1 cr fix: add new file to 2nd commit
pick fdb5f71 feat: 3rd commit

Com essas mudanças, o git vai processar os commits (de 271c66e a fdb5f71) e incorporar o conteúdo dos commits marcados com squash ao commit anterior. Agora o histórico do git está exatamente como queríamos!

* 1b2f5b1 (HEAD -> feat/git-history) feat: 3rd commit
* 6871683 feat: 2nd commit
* 682c27f feat: 1st commit
* 24aa86a (master) Init: Git History

Outra opção que podemos usar em vez de squash é fixup. As duas fazem praticamente a mesma coisa, mas fixup descarta a mensagem de log do commit.

Uma observação importante: às vezes, ao juntar commits, podemos gerar conflitos no código. Quando o git encontra conflitos, você precisa resolvê-los, preparar as mudanças e continuar o rebase executando git rebase --continue. Lembre-se de que você sempre pode executar git rebase --abort se não souber o que fazer. Esse comando interrompe o rebase e restaura o repositório ao estado original, antes de executar git rebase -i.

Para concluir

E é isso para a primeira parte sobre como alterar o histórico do git usando Rebase! Na segunda parte, vamos apresentar mais quatro cenários comuns e sugestões de como lidar com eles.

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.

Publicado em: