Publicações

Git rebase com exemplos

Oscar Esgalha
Oscar Esgalha Backend Developer

De acordo com uma pesquisa da fundação Eclipse, em 2014 Git se tornou a ferramenta mais usada para controle e versionamento de projetos de software. Desde então sua popularidade apenas cresceu: de acordo com as pesquisas anuais do Stack Overflow, seu uso por desenvolvedores chegou a quase 70% em 2017 e mais de 85% em 2018. Nas últimas pesquisas, feitas em 2021, tanto no relatório do State of Developer Ecosystem 2021 da JetBrains quanto no Developer Survey da Stack Overflow, o Git aparece como uma ferramenta usada por mais de 93% das pessoas que trabalham com desenvolvimento.

Como essa ferramenta praticamente domina os projetos de software, é muito provável que seja usada no projeto que você trabalha hoje ou em seu próximo projeto. Apesar da popularidade, uma crítica comum ao Git é a sua usabilidade, que deixa a desejar em alguns aspectos.

Vale lembrar que Git não foi desenhado, inicialmente, para ser usado por pessoas. O sistema deveria ser usado como base por outros softwares para construir ferramentas mais alto nível, com uma interface melhor. O programa, portanto, evoluiu ao longo dos anos para ter uma usabilidade melhor, mas mesmo nos comandos especificamente desenhados para usuários humanos é possível notar o vazamento de alguns detalhes de implementação.

Este é mais um artigo sobre o comando git rebase, contudo, ao invés de focar nos conceitos e detalhes de implementação necessários para entender o que o comando faz, serão apresentadas algumas situações em que o comando pode ser útil e como usá-lo. A apresentação de conceitos será econômica, o objetivo aqui é aprender como usar o rebase, não como ele funciona.

Este artigo se destina para quem chegou há pouco na carreira de desenvolvimento, ouviu falar sobre git rebase, leu alguns artigos e tutoriais, mas ainda não sente confiança em seu uso. Se você não sabe o que é um commit ou uma branch, ainda é cedo para ler o conteúdo abaixo. Se você já usa Git há mais tempo, talvez não veja nada de novo aqui.

Faça backup

Para todos os cenários abaixo é interessante fazer um backup da sua branch antes de usar o git rebase. O comando altera o histórico de sua branch trocando commits antigos por novos, é portanto, uma ação potencialmente destrutiva. Esse backup vai te dar mais liberdade para usar e experimentar o comando até adquirir familiaridade. Caso algo dê errado, basta restaurar o backup, sem risco de perder seu trabalho.

Suponha que você esteja trabalhando na branch feat/my-feature:

❯ git branch
* feat/my-feature
  main

Para fazer um backup dessa branch crie uma nova branch:

❯ git branch bkp/feat/my-feature

Alternativamente você pode criar a branch nova e mudar para ela:

❯ git checkout -b bkp/feat/my-feature

Para conferir que o conteúdo do projeto é o mesmo.

O comando git log também pode indicar que o conteúdo das duas branchs é igual:

❯ git log --oneline
f7d96e3 (HEAD -> feat/my-feature, bkp/feat/my-feature) test: add tests for the main feature
(...)

A branch feat/my-feature e bkp/feat/my-feature estão no mesmo commit f7d96e3.

Agora você pode fazer alterações e experimentar na branch feat/my-feature. Se precisar restaurar seu conteúdo execute:

❯ git reset --hard bkp/feat/my-feature
HEAD is now at f7d96e3 test: add tests for the main feature

Observe que a mensagem de saída indica que o repositório voltou para o mesmo commit de quando o backup foi criado.

Quando terminar de trabalhar na branch feat/my-feature e ela for integrada à branch principal do projeto, você pode apagar também (além dela) a branch de backup.

Agora que você sabe como fazer experimentos numa branch sem correr risco de perder seu trabalho veja em quais cenários o git rebase pode ser útil.

Atualizar minha branch

Imagine que você está trabalhando em sua branch feat/my-feature e alguém do seu time enviou novas alterações para a branch principal do projeto, a main.

Considere o cenário abaixo:

❯ git log --all --decorate --oneline --graph
* 56bc3c6 (main) docs: improve main module documentation
| * 2c72423 (HEAD -> feat/my-feature) feat: log main operations
| * f7d96e3 test: add tests for the main feature
|/
* 1532cd9 feat: add main package file
* a8c9dce feat: initialize repository

Nesse exemplo, a branch feat/my-feature foi criada quando o commit mais recente era o 1532cd9. O trabalho feito nessa branch feat/my-feature foi adicionar dois commits: f7d96e3 e 2c72423.

É possível também observar no grafo que um novo commit foi adicionado à branch main, o 56bc3c6. Esse commit não está presente na branch em que estamos trabalhando, a feat/my-feature.

Para atualizar a sua branch feat/my-feature com o novo commit que entrou no projeto, primeiro verifique que você está na branch correta, ou seja, a que terá o histórico modificado:

❯ git branch
* feat/my-feature
  main

O comando usado para o próximo passo é o mesmo, porém há dois cenários que podem ocorrer quando você tentar juntar os históricos com o rebase: com conflitos e sem conflitos.

Com conflitos

Agora, para atualizar a branch atual com o novo histórico da branch main execute o comando git rebase main:

❯ git rebase main
First, rewinding head to replay your work on top of it...
Applying: test: add tests for the main feature
Applying: feat: log main operations
Using index info to reconstruct a base tree...
M       index.js
Falling back to patching base and 3-way merge...
Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
error: Failed to merge in the changes.
Patch failed at 0002 feat: log main operations
hint: Use 'git am --show-current-patch' to see the failed patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

No exemplo acima ocorreu um conflito, ou seja, um mesmo arquivo foi alterado na branch atual e na branch main e em linhas próximas. Essa é uma situação que o Git não consegue resolver automaticamente conflitos, portanto cabe à você analisar quais mudanças devem e não devem estar presentes no resultado final.

Essas 3 linhas da saída acima:

Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
error: Failed to merge in the changes.

Indicam que o conflito ocorreu no arquivo index.js e que a tentativa de juntar as modificações dos dois "lados" (da branch atual e da branch principal) não foi bem-sucedida.

E algumas linhas abaixo:

Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

Explica como marcar que o conflito foi resolvido e seguir adiante com o processo. Ou seja, é possível editar o arquivo index.js para corrigí-lo e depois executar git add index.js seguido de git rebase --continue para seguir com o processo.

O comando git rebase --skip ignora a alteração que você fez e mantem apenas a da branch principal. Raramente você quer fazer isso enquanto junta o histórico de duas branchs e tem uma forma melhor de descartar commits desnecessários, que será apresentada mais abaixo.

Caso algo dê errado é possível desfazer tudo para tentar de novo depois. Rode o git rebase --abort para cancelar a operação e voltar a branch para como ela estava antes.

Para resolver o conflito no arquivo index.js você precisa abrir o mesmo no seu editor de texto favorito e identificar as seguintes linhas adicionadas pelo Git:

<<<<<<< HEAD
// código que já esta na branch main
=======
// código que está apenas na branch `feat/my-feature`
>>>>>>> mensagem do commit da branch feat/my-feature que causou o conflito

A quantidade de vezes que esse padrão aparece varia dependendo das alterações feitas por você e pelo seu time. Escolha as linhas adequadas para o projeto e remova as marcações adicionadas pelo Git. Editores de texto modernos ajudam bastante com isso. Quando o arquivo estiver sem nenhuma marcação de conflito, execute os comandos que foram apresentados anteriormente:

❯ git add index.js
❯ git rebase --continue
Applying: feat: log main operations

Agora é possível consultar novamente o histórico de commits do repositório:

❯ git log --all --decorate --oneline --graph
* 455455d (HEAD -> feat/my-feature) feat: log main operations
* e9499a2 test: add tests for the main feature
* 56bc3c6 (main) docs: improve main module documentation
* 1532cd9 feat: add main package file
* a8c9dce feat: initialize repository

Veja que a bifurcação que existia sumiu. A branch feat/my-feature agora surge a partir do último commit da branch main. Observe também que os commits da branch main continuam exatamente os mesmos: a8c9dce, 1532cd9 e 56bc3c6, mas os commits da branch que teve o rebase aplicado são diferentes de antes: e9499a2 e 455455d ao invés de f7d96e3 e 2c72423.

Sem conflitos

Quando não há conflitos, o processo é mais simples. Tome como exemplo o cenário abaixo:

❯ git log --all --decorate --oneline --graph
* 772dd4f (HEAD -> main) docs: improve main module documentation
| * 2c72423 (feat/my-feature) feat: log main operations
| * f7d96e3 test: add tests for the main feature
|/
* 1532cd9 feat: add main package file
* a8c9dce feat: initialize repository

Se esse commit 772dd4f que foi adicionado na branch main altera arquivos diferentes ou em linhas distantes de um mesmo arquivo a saída do comando é mais breve:

❯ git rebase main
First, rewinding head to replay your work on top of it...
Applying: test: add tests for the main feature
Applying: feat: log main operations

Veja como ficou o histórico:

❯ git log --all --decorate --oneline --graph
* 345ed13 (HEAD -> feat/my-feature) feat: log main operations
* 5bcd994 test: add tests for the main feature
* 772dd4f (main) docs: improve main module documentation
* 1532cd9 feat: add main package file
* a8c9dce feat: initialize repository

A bifurcação novamente foi eliminada. Observe, no entanto, que mesmo sem conflito os commits da branch foram novamente alterados! Antes a branch tinha e9499a2 e 455455d ao invés de 5bcd994 e 345ed13.

Como atualizar o repositório remoto

É importante saber que o histórico sempre é alterado, porque se você já empurrou commits dessa branch para algum repositório remoto, no GitHub ou GitLab por exemplo, ao tentar empurrar sua branch atualizada, encontrará um erro parecido com esse:

error: failed to push some refs to '<endereço do repositório remoto>'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Essa provavelmente é a parte mais confusa do processo. Se você seguir as instruções da saída entrará numa situação em que precisa resolver conflitos entre o histórico do repositório remoto (desatualizado) e o histório do repositório local, que você acabou de organizar. O resultado desse trabalho pode te levar a um terceiro histórico.

Por padrão você pode empurrar alterações para o repositório remoto apenas se não houver risco de perder o histórico que já existe no repositório remoto.

Como há intenção aqui de sobrescrever o histórico com o novo histórico organizado que você produziu, é preciso desativar essa proteção que vem ligada por padrão no Git. Para isso, você deve usar a flag --force:

git push --force

Essa flag vai ignorar o conteúdo que existe no repositório remoto e substituir à força o que existir lá pelo que você empurrar. Pelo poder destrutivo, essa flag deve ser usada com cuidado; este é um caso em que é aceitável usá-la.

Uma alternativa melhor, é usar a flag --force-with-lease, que também substitui o conteúdo do repositório remoto pelo que você tiver no repositório local, mas antes de fazer isso verifica se o conteúdo do repositório remoto ainda é o mesmo de quando você começou a trabalhar. Ou seja, se você estiver trabalhando com mais alguém numa branch feat/second-feature, fizer um rebase nessa branch e tentar executar um git push --force-with-lease, o comando será rejeitado se a outra pessoa trabalhando contigo subir uma alteração.

O Git tem essa proteção ligada por padrão para evitar que commits sejam perdidos acidentalmente num repositório. Quando detecta alguma divergência no histórico da branch entre o repositório local e remoto vai rejeitar as alterações. Para usar o git rebase efetivamente você precisa desativar essa proteção (através da flag --force-with-lease), e a própria execução do git rebase faz uma operação potencialmente destrutiva, porque reescreve o histórico da branch.

O comando é perigoso e partes do processo não são tão intuitivas. Uma boa forma de se familiarizar com o comando e ganhar mais confiança é criar um repositório de teste. Adicione alguns arquivos e commits para treinar o fluxo sem correr o risco de atrapalhar algum projeto.

Porque manter a branch atualizada

É interessante que você mantenha a sua branch sempre atualizada com os commits da branch principal. Eventualmente o conteúdo da sua branch deve ir para a branch principal, portanto manter sua branch atualizada pode facilitar essa integração no futuro.

Demorar para atualizar a sua branch ou integrar seu trabalho na branch principal do projeto pode te dar mais trabalho para solucionar conflitos. A atualização constante resulta em conflitos menores e mais fáceis de resolver. Além disso, você garante que seu código continua funcionando como deveria quando adicionado ao código mais atual do projeto.

Note que se você tem problemas frequentes com conflitos, pode ser que a divisão de tarefas do projeto esteja inadequada, o padrão de branches e fluxo de integração não seja o melhor para o seu cenário, ou até mesmo a organização do código do projeto tenha espaço para melhorar. Leve esses problemas para o seu time.

E o merge?

Outra forma de atualizar a sua branch com as alterações empurradas para a branch principal é através do merge. Vamos retomar o primeiro cenário usado no exemplo do rebase:

❯ git log --all --decorate --oneline --graph
* 56bc3c6 (main) docs: improve main module documentation
| * 2c72423 (HEAD -> feat/my-feature) feat: log main operations
| * f7d96e3 test: add tests for the main feature
|/
* 1532cd9 feat: add main package file
* a8c9dce feat: initialize repository

Para atualizar a branch feat/my-feature com o novo histórico da branch main execute o comando git merge main:

❯ git merge main
Auto-merging index.js
CONFLICT (content): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.

Aqui é necessário resolver conflitos da mesma forma que no caso do rebase. A diferença é que você precisa resolver apenas uma vez todos os conflitos causados por todos os commits. Lembre-se que no rebase os conflitos são resolvidos para cada commit.

Depois de resolver os conflitos dos arquivos é necessário criar o commit de merge, conforme instruído na última saída: fix conflicts and then commit the result. Basta adicionar os arquivos alterados e criar um commit regular:

❯ git commit
[feat/my-feature 70f42d7] Merge branch 'main' into feat/my-feature

Agora é possível olhar como fica o histórico de commits do repositório após o merge:

❯ git log --all --decorate --oneline --graph
*   70f42d7 (HEAD -> feat/my-feature) Merge branch 'main' into feat/my-feature
|\
| * 56bc3c6 (main) docs: improve main module documentation
* | 2c72423 feat: log main operations
* | f7d96e3 test: add tests for the main feature
|/
* 1532cd9 feat: add main package file
* a8c9dce feat: initialize repository

Observe que a bifurcação no histórico ainda existe, mas os caminhos são reunidos no commit de merge 70f42d7. Outra diferença é que nenhum commit antigo foi alterado. A única alteração é a adição de um commit de merge, que junta as duas branchs mantendo seus históricos separados.

Como o histórico de commits não é alterado, é possível empurrar as alterações para o repositório remoto sem precisar desabilitar nenhuma proteção padrão do Git.

Apesar de ser a opção mais fácil, vale a pena aprender a usar adequadamente o rebase para atualizar sua branch de trabalho. O histórico final do projeto vai ficar mais limpo e fácil de navegar. Além disso, dependendo do projeto isso pode ser uma exigência dos responsáveis. Quando você tenta colaborar num projeto open source é comum pedirem para que você faça um rebase para atualizar o seu Pull Request ou Merge Request com o código mais recente do projeto.

Organizar o histórico da minha branch

Além de juntar o trabalho de duas branchs, o rebase também pode ser usado para fazer alterações no histórico da branch em que você está trabalhando. Serão apresentados abaixo alguns exemplos mais comuns. Para fazer essas alterações o rebase interativo será usado.

Para ilustrar as operações, considere o seguinte histórico de commits:

❯ git log --all --decorate --oneline --graph -n 7
* 1b72906 (HEAD -> feat/my-feature) ci: configur laod testing
* 8e25aae chore: fix code style
* 8a3c66f chore: monitor application usage
* 3bb7e10 feat: add logging and auth
* 381bead junk: add some logs to debug
* 455455d feat: log main operations
* e9499a2 test: add tests for the main feature

Há espaço para melhorar esse histórico. O rebase interativo permite editar múltiplos commits de uma vez. Para trabalhar nesse range de commits execute o comando:

❯ git rebase -i HEAD~7

Observação: a flag -n 7 foi usada no comando git log acima para ver os último 7 commits. No rebase acima foi usado o range HEAD~7 para trabalhar em 7 commits, começando em HEAD que é o último commit da branch em que você está trabalhando.

O comando acima abre o editor de texto configurado para uso com o Git.

Dica: você pode alterar o editor de texto padrão para o VSCode, por exemplo, com o seguinte comando:

❯ git config --global core.editor code

De volta ao rebase interativo, seu editor de texto terá o seguinte conteúdo:

pick e9499a2 test: add tests for the main feature
pick 455455d feat: log main operations
pick 381bead junk: add some logs to debug
pick 3bb7e10 feat: add logging and auth
pick 8a3c66f chore: monitor application usage
pick 8e25aae chore: fix code style
pick 1b72906 ci: configur laod testing

# Rebase 56bc3c6..1b72906 onto 3bb7e10 (7 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>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# 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

Uma lista com os commits selecionados, seguido de algumas instruções comentadas. Cada linha começa com um comando (ex: pick) ou sua abreviação (ex: p) e termina com o commit, representado pelo SHA (esse conjunto de 7 letras e números). A mensagem do commit aparece apenas para auxiliar na visualização, seu conteúdo aí não interfere no resultado do rebase.

O que você deve fazer agora é especificar qual comando é executado em cada commit e depois salvar/fechar o arquivo. O Git irá executar essa sequência de comandos.

Se você não alterar nada, ou seja, deixar o pick para todos os commits, o resultado final do rebase será o mesmo histórico. Nada é alterado.

Descartar commits

No exemplo apresentado, um dos commits foi inserido para facilitar o desenvolvimento e pode ser descartado. Seu conteúdo não é útil para o futuro do projeto: 381bead junk: add some logs to debug. Pela mensagem é possível imaginar que alguns "prints" foram adicionados para ver o valor de algumas variáveis.

Para removê-lo, execute o git rebase:

❯ git rebase -i HEAD~7

Na lista de comandos, troque o pick do commit que deve ser removido por um drop ou d

pick e9499a2 test: add tests for the main feature
pick 455455d feat: log main operations
-pick 381bead junk: add some logs to debug
+d 381bead junk: add some logs to debug
pick 3bb7e10 feat: add logging and auth
pick 8a3c66f chore: monitor application usage
pick 8e25aae chore: fix code style
pick 1b72906 ci: configur laod testing

(...)

E salve/feche o arquivo editado para seguir com o rebase.

Uma mensagem de sucesso deve aparecer na saída:

Successfully rebased and updated refs/heads/feat/my-feature.

E o commit foi removido do histórico:

❯ git log --all --decorate --oneline --graph -n 7
* cd491d2 (HEAD -> feat/my-feature) ci: configur laod testing
* f12b86a chore: fix code style
* f30ed38 chore: monitor application usage
* cfc80fb feat: add logging and auth
* 455455d feat: log main operations
* e9499a2 test: add tests for the main feature
* 56bc3c6 (main) docs: improve main module documentation

Note que os commits após o commit removido foram alterados: 3bb7e10 tornou-se cfc80fb, por exemplo.

Juntar commits

Outra opção disponível é juntar um ou mais commits em um commit apenas. No exemplo trabalhado aqui, temos um commit que faz alterações no código f30ed38 chore: monitor application usage seguido de um commit que faz apenas alterações estéticas f12b86a chore: fix code style. Isso pode ter acontecido porque o código introduzido em f30ed38 não seguia os padrões do projeto, e o próximo commit f12b86a corrigiu o problema. Nesse caso, os dois commits deveriam ser apenas um. A mensagem fix code style é um ruído desnecessário no histórico do projeto.

Para juntar esses dois commits, execute o git rebase:

❯ git rebase -i HEAD~6

Na lista de comandos, troque o pick do commit que corrige o estilo de código por um fixup ou f:

pick e9499a2 test: add tests for the main feature
pick 455455d feat: log main operations
pick cfc80fb feat: add logging and auth
pick f30ed38 chore: monitor application usage
-pick f12b86a chore: fix code style
+f f12b86a chore: fix code style
pick cd491d2 ci: configur laod testing

(...)

O fixup ou o squash junta o commit marcado no commit anterior. A diferença entre os dois é que o fixup descarta a mensagem do commit, enquanto o squash abre o editor para que você defina a mensagem do commit novo (que junta os dois), inicialmente aparece a mensagem de todos os commits envolvidos.

Depois de salvar/fechar o arquivo editado para seguir com o rebase, a mensagem de sucesso deve aparecer novamente:

Successfully rebased and updated refs/heads/feat/my-feature.

Observe o novo histórico:

❯ git log --all --decorate --oneline --graph -n 6
* 3d20746 (HEAD -> feat/my-feature) ci: configur laod testing
* c9a673e chore: monitor application usage
* cfc80fb feat: add logging and auth
* 455455d feat: log main operations
* e9499a2 test: add tests for the main feature
* 56bc3c6 (main) docs: improve main module documentation

O commit isolado chore: fix code style foi removido e seu conteúdo foi incluído no commit chore: monitor application usage.

Editar mensagens de commit

Apesar do grande valor de escrever boas mensagens de commit, pode ser que você não queira interromper o fluxo de desenvolvimento no momento que cria o commit. É possível escrever uma mensagem breve, apenas para lembrar do que o commit se trata, e melhorar o texto da mensagem depois.

Ocasionalmente você também pode cometer erros de digitação e perceber mais tarde que uma mensagem de commit está incorreta. No exemplo trabalhado neste artigo, esse é o caso do commit 3d20746 ci: configur laod testing. Esses erros na mensagem podem ser ajustados com o rebase.

Para corrigir a mensagem de commit, execute o git rebase:

❯ git rebase -i HEAD~6

Na lista de comandos, troque o pick do commit com a mensagem incorreta por um reword ou r:

pick 56bc3c6 docs: improve main module documentation
pick e9499a2 test: add tests for the main feature
pick 455455d feat: log main operations
pick cfc80fb feat: add logging and auth
pick c9a673e chore: monitor application usage
-pick 3d20746 ci: configur laod testing
+r 3d20746 ci: configur laod testing

(...)

Salve/feche o arquivo editado para seguir com o rebase. O Git vai abrir o editor de texto configurado para que você edite a mensagem do commit marcado com o reword. Quando você terminar de editar a mensagem e salvar, deve encontrar a mensagem de sucesso.

Veja como ficou o histórico:

❯ git log --all --decorate --oneline --graph -n 6
* 96f9cdc (HEAD -> feat/my-feature) ci: configure load testing
* c9a673e chore: monitor application usage
* cfc80fb feat: add logging and auth
* 455455d feat: log main operations
* e9499a2 test: add tests for the main feature
* 56bc3c6 (main) docs: improve main module documentation

O commit ci: configur laod testing agora está com o texto correto ci: configure load testing.

Dividir um commit

O escopo de um commit é mais um aspecto que pode ser reconsiderado quando você revisa o histórico de sua branch. Daqui a 6 meses, se for necessário usar um git blame para visualizar o commit que alterou determinada linha de código, o conjunto de alterações do commit faz sentido? Ou o mesmo commit concentra muitas alterações não relacionadas?

Lembre-se, no entanto, que além da semântica do conjunto de alterações de um commit, é necessário se preocupar com o funcionamento do software. Se você dividir um commit em múltiplos, é importante que o projeto funcione também nos commits individuais.

No exemplo apresentado neste artigo, o commit cfc80fb feat: add logging and auth provavelmente concentra mais alterações do que deveria. Uma forma mais adequada de apresentar essas alterações seria um commit para a adição do logger e outro para a autenticação.

Para dividir esse commit, execute o git rebase:

❯ git rebase -i HEAD~6

Na lista de comandos, troque o pick do commit a ser dividido por um edit ou e:

pick 56bc3c6 docs: improve main module documentation
pick e9499a2 test: add tests for the main feature
pick 455455d feat: log main operations
-pick cfc80fb feat: add logging and auth
+e cfc80fb feat: add logging and auth
pick c9a673e chore: monitor application usage
pick 96f9cdc ci: configure load testing

(...)

Depois de salvar/fechar o arquivo editado você retornará para o terminal no commit que foi marcado para edição. É como se você tivesse acabado de fazer o commit.

Agora execute um git reset para desfazer esse commit:

git reset HEAD^

As alterações que antes estavam neste commit estão novamente disponíveis. Você se encontra na mesma situação em que estava antes de criar esse commit. Agora é possível criar múltiplos commits como de costume, usando os comandos git add e git commit. Quando terminar de criar os novos commits, use o comando git rebase --continue para continuar com o rebase.

Seguindo o cenário de exemplo, veja como fica o histórico após criar um commit para o logging e outro para o auth:

❯ git log --all --decorate --oneline --graph -n 6
* 3887396 (HEAD -> feat/my-feature) ci: configure load testing
* f19c227 chore: monitor application usage
* 2b9388a feat: add logging
* 2185144 feat: add auth
* 455455d feat: log main operations
* e9499a2 test: add tests for the main feature

Observe que no lugar do commit cfc80fb feat: add logging and auth agora tem dois commits: 2185144 feat: add auth e 2b9388a feat: add logging.

Quando não usar rebase?

Agora que você sabe alguns cenários em que o uso do git rebase pode ser útil, é importante saber quando não usá-lo. Lembre-se que o resultado de um rebase modifica o histórico, de modo que é necessário indicar para um repositório remoto que você está conscientemente sobrescrevendo o histórico de uma branch.

Da mesma forma que essa proteção de históricos divergentes existe quando você tenta empurrar trabalho de um repositório local para um repositório remoto, ela está presente quando você tenta puxar trabalho de um repositório remoto.

Portanto se você usar o rebase e empurrar o resultado em uma branch que tenha mais alguém trabalhando, você também empurrou uma preocupação extra para quem está trabalhando com você, pois será necessário cuidado ao puxar as alterações para não perder nenhum trabalho.

Todo repositório git tem uma organização mínima de branchs e quais os fluxos adequados para usá-las. Independente da organização escolhida, geralmente há branchs públicas, onde o trabalho de várias pessoas é integrado, e branchs privadas, que ainda não foi integrada com o restante do projeto.

Para evitar problemas, use o git rebase apenas nas branchs privadas, em que apenas você esteja trabalhando. Não modifique o histórico de uma branch que já está em uso por outras pessoas ou sistemas.

Para saber mais

O livro disponível gratuitamente na internet Pro Git tem um capítulo sobre Rebase, que se aprofunda em alguns conceitos. Você pode também ler no nosso blog um artigo sobre como escrever boas mensagens de commit. No Git Merge 2016 a Emily Xie fez uma palestra de 30 minutos que explica várias partes internas do Git, que pode te ajudar a entender melhor o que está acontencedo quando algo dá errado.

Referências

Solução de problemas, melhoria de gestão e aumento de produtividade

Com tecnologias renomadas, aplicamos práticas ágeis e capacitamos sua equipe.

Fale com um consultor