A Magia do git rebase

Traduções disponíveis:

Uma das ferramentas que eu acho das mais úteis no git, e da qual eu mais amo é o git-rebase(1). Ela se baseia na premissa que trouxe o git para o mundo dos sistemas de controle de versão: de que você pode e deve alterar a história do teu repositório de forma a tornar ele mais legível e organizado.

Ao redor do mundo, cada projeto estabelece sua convenção de como trabalhar com o git, alguns utilizam técnicas como o git-flow, outros, apenas uma branch e tags, outros dividem em duas branchs principais, master/main e production/release. A convenção no meu trabalho, é de sempre criar uma branch específica para trabalhar em uma nova funcionalidade, correção de algum erro ou melhoria num geral. Feito isso, eu adoto a disciplina de (1) manter ela atualizada com a branch pai usando git pull --rebase origin/branch_pai e (2) conforme for trabalhando nela, procuro manter ela organizada usando o git rebase -i HEAD~N.

Rebase

O rebase tem um funcionamento bastante simples, em essência, ao contrário de executar um merge (ou seja, criar um commit que puxa as mudanças para a tua branch), o rebase: temporariamente remove os teus patches, puxa os commits da branch pai e aplica os teus patches em sequência em cima dessa branch atualizada. O benefício aqui é claro: a branch contém apenas uma lista de commits com o seu trabalho novo, de forma legível e coerente.

Utilizando o parâmetro -i no comando, inicia um rebase interativo, iniciando a partir do commit que eu pedi. 1 Por exemplo, git rebase -i HEAD~5, vai iniciar um rebase até os meus últimos 5 commits. O rebase vai mostrar o seguinte:

pick 9fdd140 hooks: Flesh out the Hook::Destroy service test
pick ecb296e resource: Flesh out the resource test
pick bd115a0 hooks: Generate token when subscribing
pick fcaba9d hooks: Flesh out the handler test
pick 3926d4b doc: Add document about testing

# Rebase bed2ff9..3926d4b onto bed2ff9 (5 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.
#

Ou seja, vai me mostrar meus últimos 5 commits, e me pedir para eu escolher (pick ou break), remover (drop), editar (edit ou reword) ou juntar eles (fixup e squash).

Disciplina

Meu plano de trabalho, em geral é o seguinte: conforme eu avanço no trabalho que eu tenho que fazer, commito localmente de forma que eu vagamente consiga explicar o que eu fiz. No momento que eu sinto que terminei e já vale de apresentar esse trabalho, utilizo o rebase para reescrever, quebrar/juntar esses commits em uma narrativa de passos sequenciais mais legíveis e coerente.

Buscando escrever essa narrativa, procuro criar mudanças atômicas que por si só sejam responsáveis por responder a pergunta de o que foi feito? e isso não quebra os testes? 2.

Uma experiência que me ajudou muito nesse propósito, foi o de contribuir com projetos que utilizam uma lista de e-mail pública 3, enviando patches ao invés de criar Pull Requests. Em especial, porque a lógica de trabalho muda. A forma de trabalho apresentada por ferramentas como Github, GitLab e outras derivações similares é de criar um PR e infinitamente seguir adicionando commits nele até que o trabalho esteja pronto, a interface que se vire para mostrar o que realmente é importante. Contudo, a lógica de trabalho do git por email é de tentar ser o mais assertivo possível, evitando patchsets 4 gigantes e com mudanças em lugares diferentes. Uma das vantagens de lidar com patchsets, é que é possível aplicar uma seleção de patches de um patchset, e pedir revisão dos outros que não façam sentido ou necessitem de um trabalho maior naquele momento.

Aplicando essa lógica, procuro sempre quebrar um patchset muito grande em vários patchsets diferentes 5 e com patches atômicos, descritivos, geralmente organizado em módulos ou partes 6.

Em resumo, o git é fantástico. Ter disciplina para utilizá-lo e aprender como ele funciona é uma obrigação de qualquer programador. Uma das principais diferenças dele para os outros sistemas de controle de versão que vieram antes é justamente que a história de trabalho daquele repositório não é escrita em pedra, não é apenas leitura. Aliado a isso o git tem uma extensiva documentação offline, disponível para você estudá-lo a respeito nos seus manuais. Recomendo fortemente a leitura dessas man pages: git-rebase(1), giteveryday(7) e gitrevisions(7).


  1. HEAD~N, significa essencialmente a partir do ultimo commit disponível nessa branch até N commits anteriormente. ↩︎

  2. Ou seja, sem fix tests, fix typo↩︎

  3. Que é o formato de contribuição do qual o git foi feito para funcionar originalmente, exemplos notáveis são a lista de email do Kernel, Alpine Linux, git, cgit, dwm e outras ferramentas da suite suckless, coreutils, musl-libc, freedesktop, vim, emacs, sistemas BSD e por aí vai. ↩︎

  4. Um patch é um commit, e um patchset é uma coleção de patches. ↩︎

  5. Um exemplo aqui poderia ser organizar includes, remover espaços em branco, ou qualquer mudança estética no código que não tenha relação direta com a funcionalidade/correção que seja necessário originalmente. ↩︎

  6. Geralmente adoto o seguinte formato para meus commits: módulo: sub-módulo, descrição do que esse commit faz e descrevo o porque essa mudança foi feita e qual abordagem eu tomei no corpo do commit. ↩︎