Introdução

No processo de desenvolvimento de software, é comum e até recomendado fazer commits pequenos e frequentes. No entanto, nem todos esses commits são “perfeitos” para serem enviados diretamente para a branch principal (main ou master). Muitas vezes, criamos commits intermediários com código quebrado, funcionalidades incompletas, ou mensagens de commit provisórias (“WIP - Work In Progress”).

O desafio surge quando queremos um histórico de commits limpo, coeso e fácil de entender para a main branch, mas também precisamos da flexibilidade de “salvar” nosso trabalho provisoriamente para testar novas abordagens ou voltar a um ponto específico.

Este post abordará como conciliar essas duas necessidades. Veremos técnicas para gerenciar o desenvolvimento caótico e, em seguida, como “polir” seu histórico de commits usando git rebase interativo e outras ferramentas, garantindo que apenas commits de alta qualidade cheguem ao seu repositório compartilhado.

O Cenário de Desenvolvimento Comum (e Caótico)

Imagine que você está implementando uma nova funcionalidade. Você começa com um commit base e, à medida que avança, faz commits como:

  • feat: inicio da funcionalidade X
  • fix: ajusta bug na funcionalidade X (WIP)
  • chore: tenta nova abordagem para X (build quebrado)
  • refactor: refatora parte de X
  • feat: X quase pronta
  • fix: corrige testes (mas ainda não está perfeito)

Seu histórico de commits local pode se parecer com isto:

A -- B -- C -- D -- E -- F (feature-branch)

Onde C, D, E, F são seus commits intermediários.

O problema é que C, D, E podem conter estados ruins do código ou serem muito granulares. Queremos que eles se tornem um ou dois commits lógicos e funcionais antes de ir para a main.

Técnicas para Gerenciar o Desenvolvimento Provisório

Antes de pensar em organizar, precisamos de formas de “salvar” nosso progresso sem nos comprometer imediatamente com um commit final.

1. git stash - Guardando alterações temporariamente

git stash é seu amigo quando você precisa trocar de branch ou lidar com uma interrupção, mas não quer fazer um commit incompleto. Ele guarda o estado atual do seu diretório de trabalho e do staging area.

# Salva as alterações não commitadas
git stash push -m "Mensagem para o stash"

# Lista os stashes
git stash list

# Aplica o stash mais recente e o remove da lista
git stash pop

# Aplica um stash específico (ex: stash@{1}) e o mantém na lista
git stash apply stash@{1}

Isso permite que você mantenha seu branch limpo enquanto experimenta, sem poluir seu histórico de commits.

2. Commits Provisórios em Branches de Desenvolvimento

É perfeitamente aceitável fazer commits provisórios em um branch de desenvolvimento (ex: feature/minha-nova-funcionalidade). A chave é não enviar esses commits para branches compartilhadas (como main) antes de limpá-los.

Organizando o Histórico de Commits com git rebase Interativo

Uma vez que você alcançou o resultado desejado e seu código está funcional, é hora de “limpar” o histórico de commits. git rebase -i (interactive rebase) é a ferramenta mais poderosa para isso.

Ele permite que você reescreva uma sequência de commits, podendo:

  • squash / fixup: Combinar múltiplos commits em um único.
  • reword: Alterar a mensagem de um commit.
  • edit: Parar em um commit para fazer alterações adicionais (ex: corrigir um bug, adicionar um arquivo esquecido).
  • drop: Remover um commit do histórico.
  • reorder: Mudar a ordem dos commits.

Como usar git rebase -i

Você precisa especificar o commit anterior ao primeiro commit que você deseja reescrever.

git rebase -i <SHA_DO_COMMIT_ANTERIOR>
# Ou, para os últimos N commits
git rebase -i HEAD~N

Exemplo: Seu histórico:

A -- B -- C (fix: bug) -- D (feat: add X) -- E (fix: X) -- F (chore: adjust) (feature-branch)

Você quer limpar os commits C, D, E, F. O commit anterior é B.

git rebase -i B
# Ou se B for 4 commits atrás do HEAD:
git rebase -i HEAD~4

O Git abrirá seu editor de texto padrão com uma lista dos commits que você selecionou para reescrever, de cima para baixo (os mais antigos primeiro):

pick C fix: bug
pick D feat: add X
pick E fix: X
pick F chore: adjust

# Rebase 27170a4..9d070b4 onto 27170a4 (4 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 (e.g. `git commit --amend`) for each commit
# 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 | -c <commit>| --no-ff <commit>]
# .       create a merge commit and reset HEAD to it (use 'git merge <commit>' normally)
#
# 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.
#

Cenário de Limpeza:

Vamos transformar os commits C, D, E, F em um único commit lógico feat: implementa funcionalidade X.

Você editará o arquivo de rebase da seguinte forma:

pick C fix: bug
squash D feat: add X
squash E fix: X
fixup F chore: adjust
  • pick C: Escolhemos o primeiro commit como base.
  • squash D: Combina D com C. O Git abrirá um editor para você combinar as mensagens de C e D.
  • squash E: Combina E com o resultado anterior. O Git abrirá um editor para combinar a mensagem.
  • fixup F: Combina F com o resultado anterior, mas descarta a mensagem de commit de F. Isso é útil para pequenos ajustes de formatação ou correções rápidas que não precisam de uma mensagem própria.

Após salvar e fechar o editor, o Git pode abrir outro editor para que você combine as mensagens dos commits “squashed”. Você pode escrever uma mensagem de commit limpa, por exemplo: feat: implementa funcionalidade X completa.

Seu histórico final na feature-branch será:

A -- B -- C' (feat: implementa funcionalidade X completa) (feature-branch)

Onde C' é o novo commit que contém todas as alterações de C, D, E e F.

Outras Opções Úteis do Rebase

  • edit: Use edit se precisar parar em um commit específico para fazer correções ou adicionar algo que foi esquecido.

    pick C fix: bug
    edit D feat: add X
    pick E fix: X
    pick F chore: adjust
    

    O Git pausará no commit D. Você pode fazer suas alterações, git add ., e depois git commit --amend para incorporar as novas alterações ao commit D. Em seguida, git rebase --continue para seguir com o rebase.

  • reword: Para apenas alterar a mensagem de um commit:

    reword C fix: bug
    pick D feat: add X
    ...
    

    O Git pausará para você editar a mensagem de C.

Outras Ferramentas para Limpeza Rápida

1. git commit --amend - Ajustando o último commit

Se você acabou de fazer um commit e percebeu que esqueceu um arquivo, ou que a mensagem está errada, git commit --amend é a solução. Ele permite que você “edite” o último commit.

# Faça suas alterações
# git add <ARQUIVOS_ESQUECIDOS>

# Re-comita as alterações e/ou edita a mensagem do commit anterior
git commit --amend --no-edit # Mantém a mensagem de commit existente
git commit --amend # Abre o editor para você mudar a mensagem

2. git reset - Desfazendo commits localmente

git reset é usado para mover o HEAD e, opcionalmente, o staging area e o diretório de trabalho. É útil para desfazer commits que você não quer mais no seu histórico local.

  • git reset --soft <COMMIT_ID>: Move o HEAD para <COMMIT_ID>, mas mantém as alterações dos commits desfeitos no seu staging area. Útil para “desfazer” commits e depois “re-commitá-los” de forma diferente.
  • git reset --mixed <COMMIT_ID> (padrão): Move o HEAD e o staging area para <COMMIT_ID>, mantendo as alterações dos commits desfeitos no seu diretório de trabalho (como arquivos não staged).
  • git reset --hard <COMMIT_ID>: PERIGOSO! Move o HEAD, o staging area e o diretório de trabalho para <COMMIT_ID>, descartando todas as alterações dos commits desfeitos. Use com extrema cautela, pois você perderá seu trabalho.
# Desfaz o último commit, mantendo as alterações no staging area
git reset --soft HEAD~1

# Desfaz o último commit, movendo as alterações para o diretório de trabalho (unstaged)
git reset HEAD~1

# Desfaz o último commit e descarta as alterações (CUIDADO!)
git reset --hard HEAD~1

Considerações Importantes

  • Nunca faça rebase em branches que já foram publicadas! Reescrever o histórico de commits de um branch compartilhado pode causar problemas sérios para seus colaboradores. O rebase é para uso em branches locais e não compartilhadas.
  • Faça commits frequentes e pequenos: Isso facilita muito a tarefa de rebase e permite que você “salve” seu trabalho sem medo.
  • Mensagens de commit: Embora a ideia seja ter commits provisórios com mensagens ruins, esforce-se para que as mensagens dos commits finais (após o rebase) sejam claras, concisas e sigam as convenções do projeto.

Conclusão

A flexibilidade do Git permite que você adote um fluxo de trabalho onde a experimentação e os commits provisórios são parte natural do desenvolvimento. Com ferramentas como git stash, git commit --amend, git reset e, principalmente, o git rebase interativo, você pode transformar um histórico de commits bagunçado em uma narrativa limpa e profissional antes de integrar suas mudanças à main branch. Dominar essas técnicas é crucial para manter um repositório organizado e facilitar a colaboração em equipe.