Para muitos desenvolvedores git se resume a pull, push, commit, branches, merge e as vezes um stash (eu era assim não muito tempo atrás), até surgir aquela coceirinha na nuca quando se depara com algumas rotinas do trabalho em equipe:

Imagine o seguinte, você criou três PR’s distintos para a branch develop, e necessita que os 3 fossem mergeados para iniciar uma nova tarefa apartir da develop, porém eles não foram mergeados e nem tem previsão de quando isso ocorra.

O que fazer nesse caso com o conhecimento básico de git?

  • Crio uma branch de consolidação, faço o merge das 3 outras branchs nessa e depois faço um novo PR para develop. Simples e resolvido, certo?

ERRADO.

Funcionar até funciona mas esse novo PR vai com todas as modificações das 3 branches mergeadas junto, o que faz com que tenha um PR “sujo” e grande para ser revisado, com todas as modificações que já estão em outras PR’s e que já seriam revisadas.

Mas como um desenvolvedor astuto, você já havia mapeado isso, então o que você faz:

  • Ao invés de fazer o que citei, você cria uma nova branch para o PR, faz um git stashGuarda temporariamente mudanças não commitadas do working directory e staging area. Não enxerga commits já feitos. na branch consolidada, para pegar somente o que você modificou, e faz um git stash apply na branch nova.

Pronto, PR limpo, só mergear após os outros 3.

Só tem um pequeno detalhe, o git stash só pega arquivos modificados e que não foram commitados, e agora? fazer tudo sem commitar mesmo?

Antes de responter essa questão, vamos “criar” uma nova branch nessa linha de racicínio para entender de fato alguns conceitos básicos de git.


Afinal, qual a real diferença em “Merge” e “Rebase”

Merge

git switch develop
git merge feature

Imagine o git como uma árvore que não para de crescer. Uma branch (galho em inglês), é criada quando você quer continuar trabalhando na árvore, mas sem que outros colegas te atrapalhem, imagine que você precise cortar as pontas de algumas folhas, e seu colega precise cortar a lateral dessas folhas, os dois fazendo isso no mesmo galho pode ser um problema, então você cria um galho nessa mesma árvore e segue cortando suas folhas.

Você terminou de cortar suas folhas, e precisa mostrar para equipe como ficou, o que você faz, você junta esse galho novamente a árvore da equipe, assim a equipe consegue ver cada folha cortada, e em qual ordem você foi cortando.

O ato de trazer seu galho para a árvore é o que seria o merge.

Agora de forma técnica, o merge demonstra através de um commit, todos os commits anteriores feito de uma branch para outra, no caso do nosso exemplo, tudo que foi commitado na branch feature, passa para a branch develop, mantendo o historico de commits, e adicionando um commit que “junta tudo”.

Todo commit tem um “pai”, que no caso é o commit anterior, no caso de uma branch, quando é criada, ela parte do ultimo commit da branch pai, o que acontece no merge é um commit com dois pais, assim preservando o histórico.

Rebase

Beleza, mas o rebase também não junta duas branchs?

git switch feature
git rebase develop

Sim e não, o rebase ao invés de criar um commit de merge com dois pais, o que ele faz é, como o nome ja diz, mudar a base, ele pega o primeiro commit dessa nova branch e muda o pai desse commit para ser o ultimo commit da branch que você indicar, e isso causa uma mudança no histório, ao invés de um braço divergindo da branch original e depois voltando para ela, o rebase mantém linear

Avance entre as opções abaixo, e veja a diferença que fica no histórico do git entre um git merge e um rebase seguido de fast-forward:

developdevelopdevelopdevelopfeaturefeature (rebased)ooXYABMA'B'A'B'

X e Y foram para a develop enquanto A e B foram feitos na feature — as duas linhas divergiram.

O quarto botão acima mostra o resultado final: como a branch já parte do topo da develop, o merge não precisa criar um commit — é um fast-forwardTipo de merge onde não é criado commit de merge — o ponteiro da branch simplesmente avança para o topo da outra, pois não há divergência entre elas..


Resolvendo o problema original

Entendendo essa diferença, podemos voltar ao problema anterior:

git rebase —onto

Para o caso de uma feature grande, você provavelmente faria vários commits, então o git stash fica fora de cogitação, uma das possibilidades seria o git rebase --onto. O que esse comendo faz é basicamente pegar só os seus commits e reaplicar em outra base, descartando a antiga.

  1. Você cria uma branch temporaria só para mergear os 3 PR’s necessários (não tem problema pois será descartada)
  2. Utiliza o git tagMarcador com nome colado em um commit específico. Diferente de uma branch, não se move quando você faz novos commits. Aqui serve de referência para o --onto saber onde seu trabalho começa. apenas para simplificar a marcação do ponto de partida do seu trabalho atual (pode usar o hash também)
  3. Faça a sua implementação normalmente
  4. Após o merge dos 3 PR’s faça um rebase --onto

A sintaxe do rebase --onto é a seguinte:

git rebase --onto <nova-base> <base-antiga> <branch>

Tradução: “replica em cima de origin/develop todos os commits de work/minha-atividade que vieram depois de base-integracao. Como os commits dos 3 PRs já estão na develop, eles somem — sobra só o meu trabalho, agora linear e limpo.

Pelos nossos exemplos o fluxo ficaria:

# 1. Criar branch de integração e mergear os 3 PRs
git switch -c integration/base origin/develop
git merge origin/feature/pr-1 origin/feature/pr-2 origin/feature/pr-3

# 2. Criar branch de trabalho e marcar o ponto de partida
git switch -c work/minha-atividade integration/base
git tag base-integracao

# 3. Implementar normalmente (vários commits)

# 4. Quando os 3 PRs forem mergeados na develop
git fetch origin
git rebase --onto origin/develop base-integracao work/minha-atividade
git tag -d base-integracao
git push -u origin work/minha-atividade
ooPR1PR2PR3[TAG]ABA'B'integration/basework/minha-atividadedevelop

integration/base tem os 3 PRs mergeados. work/minha-atividade parte daí — a tag marca onde seu trabalho começa.

git cherry-pick

Uma outra alternativa para o caso de ser apenas 1 commit, seria usar o cherry-pick.

Na branch onde foi mergeada os 3 PR’s, após implementar a nova funcionalidade, faça git rev-parse HEADRetorna o hash completo do commit apontado pela referência. git rev-parse HEAD imprime o hash do commit atual — útil para anotar antes de trocar de branch., e anote o hash, ex: abc123

Quando os 3 PR’s forem mergeados, crie uma nova branch apartir da develop e execute git cherry-pick abc123.

# na branch de integração, após seu commit
git rev-parse HEAD   # anote o hash, ex: abc123

# quando os 3 PRs forem mergeados na develop
git switch -c feature/minha-atividade origin/develop
git cherry-pick abc123
git push -u origin feature/minha-atividade

Uma ótima alternativa ao git stash quando a mudança já foi commitada, o que é feito é criar um commit novo com hash diferente, mas com as mesmas mudanças. O commit original permanece.


Mais ferramentas que você deveria conhecer

Aproveitando o embalo, segue mais comandos git que você deveria conhecer

git worktree

Essa era umas das ferramentas menos utilizadas do git, até a chegada dos agentes de IA.

Imagine o caso, você possui um agente investigando ou implementando uma tarefa demorada em uma determinada branch, e nesse meio tempo você queria fazer uma outra implementação que não tem nada haver com essa, mas você não pode trocar a branch se não o agente perde o contexto, a solução para isso é o git worktree

O que é feito é criar uma nova branch numa pasta separada (literalmente cria uma nova pasta com os arquivos da branch, criando uma nova área para trabalho)

git worktree add <caminho> <branch>

<caminho> seria onde a pasta será criada

<branch> é apartir de qual branch essa worktree será criada

# materializa a branch de integração numa pasta separada
git worktree add ../app-integracao integration/base

# você continua trabalhando na pasta atual em outra branch
# o agente trabalha em ../app-integracao sem interferir

Como o .git é compartilhado, depois é só você commitar e abrir seus PR’s normalmente

Atenção: A unica restrição é que a mesma branch não pode estar ativa em 2 worktrees.

Para remover um worktree use o seguinte comando:

git worktree remove <caminho>

—force-with-lease em vez de —force

Suponha o caso de você precisar fazer um rebase da develop na sua branch feature.

Sua branch feature esta assim:

Local:   o--o--A--B   ← feature
Remoto:  o--o--A--B   ← origin/feature (em sincronia)

Porém a develop avançou nos commits X e Y, então você o rebase:

git rebase origin/develop

Agora suas branchs locais e remotas divergiram:

Local:   o--o--X--Y--A'--B'   ← rebaseado, hashes novos
Remoto:  o--o--A--B            ← ainda tem a versão antiga

Porém quando você faz um rebase ou rebase --onto, como eu disse, os commits “mudam de pai”, logo seus hashes mudam (A vira A', B vira B', etc…), como o remoto ainda tem os hashes antigos, quando você tenta dar push o git recusa porquê houve uma divergência entre o repositório local e o remoto, e bloqueia pois parece que você está perdendo commits (já que os hashes são diferentes).

Nesse caso o que muitos fariam seria git push --force, para forçar o push mesmo assim.

O problema é que se alguém commitou na sua branch enquanto você tava rebaseando (normalmente a branch é individual, mas vai que né), o --force vai apagar o trabalho dessa pessoa sem avisar, o remoto vira:

Remoto:  o--o--A--B--C   ← colega commitou C enquanto você rebaseava

Com o seu --force fica:

Remoto vira: o--o--X--Y--A'--B'   ← C sumiu, perdido

A solução para isso é o --force-with-lease O git compara o que estava no remoto quando você fez o ultimo fetch (antes do rebase), com o remoto atualizado e se os hashes não baterem o git aborta e te avisa.

git push --force-with-lease origin feature

Commits fixup! + —autosquash

Imagina o seguinte, sexta-feira, 16h, você terminou a ultima feature e fez um commit, foi pegar um café e no caminho lembrou de algo que ficou faltando, o que você faz?

  • Um novo commit com a seguinte mensagem: ajustes

Depois você lembra de outra correção que é necessária:

  • Um novo commit com: agora vai

Os dois ultimos commits, são correções do primeiro, o que deixaria o histórico do git sujo, veja como usar:

Imagine os commits com os seguintes hashes e ordem:

abc1234  feat: implementa tela de VA
def5678  ajuste
ghi9012  corrige typo
jkl3456  agora vai

Ao invés de fazer os commits assim, use a flag --fixup apontando para o commit da feature:

git commit --fixup=abc1234

Depois antes de fazer um merge faça um rebase com --autosquashFlag do rebase interativo que detecta commits com prefixo fixup! e os reordena automaticamente, marcando-os para absorção pelo commit original — sem precisar editar o editor na mão., e utilize o -i para abrir a edição interativa

git rebase -i --autosquash origin/develop

O Git vai detectar os commits fixup! pelo nome, e reordenar logo abaixo do commit original os marcando para absorção automaticamente, aí você só confirma. Resultado:

ANTES                            DEPOIS
abc1234  feat: implementa...  →  abc1234' feat: implementa...
def5678  ajuste               →  (absorvido)
ghi9012  corrige typo         →  (absorvido)
jkl3456  agora vai            →  (absorvido)

Assim faz um commit limpo sem retrabalho manual

git reflog

Essa aqui salva vidas!!!

O reflog é como se fosse um diário interno do git, nele é registrado toda vez que o HEADPonteiro que indica onde você está no repositório — normalmente aponta para o último commit da branch atual. Quando você troca de branch, faz commit ou rebase, o HEAD se move. se moveu.

Cada vez que você faz um commit, checkout, rebase, reset, merge, o git anota no reflog

e o git reflog lista todo essse histórico.

a3f9c21 HEAD@{0}  rebase (finish): returning to refs/heads/feature/minha-tarefa
d7e1b40 HEAD@{1}  commit: fix: corrige validacao
f2e1b39 HEAD@{2}  commit: feat: implementa nova funcionalidade
abc1234 HEAD@{3}  checkout: moving from develop to feature/minha-tarefa
9f3c211 HEAD@{4}  checkout: moving from feature/minha-tarefa to develop

E por quê isso é importante? Para desfazer suas cagadas, imagine:

Quando você faz um rebase errado e parece que você perdeu commits (horas de trabalho), na verdade eles ainda existem, só ficaram sem branch apontando para eles.

Com git reflog você vê que antes do rebase o HEAD estava em abc1234

e faz um reset para ele:

git reset --hard HEAD@{3}

Existem inúmeros outros comandos interessantes do git que talvez sejam úteis para o seu caso, mas resolvi listar nesse post os que já foram uteis para mim e que quando comentava com outros devs eles não conheciam, futuramente me deparando com novos problemas, volto com uma parte 2 desse post.