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
na branch consolidada, para pegar somente o que você modificou, e faz um Guarda temporariamente mudanças não commitadas do working directory e staging area. Não enxerga commits já feitos.git stashgit stash applyna 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:
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
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.
- Você cria uma branch temporaria só para mergear os 3 PR’s necessários (não tem problema pois será descartada)
- Utiliza o
apenas para simplificar a marcação do ponto de partida do seu trabalho atual (pode usar o hash também) Marcador 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.git tag - Faça a sua implementação normalmente
- 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
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 HEAD
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 --autosquash-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
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.