DevOps, CI/CD e contêineres
Branches e fluxos de trabalho no Git: merge, rebase, Git Flow e trunk-based development
Intermediário10 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
No Git, branches não custam nada, então as decisões que importam são quanto tempo elas vivem e como são integradas. Este guia explica os merges fast-forward e de três vias, o rebase e suas regras, a resolução de conflitos e, em seguida, compara os fluxos de trabalho que os times realmente usam — feature branches com pull requests, Git Flow, GitHub Flow e trunk-based development — com orientações sobre qual combina com cada modelo de release.
O que você vai aprender
- Explicar o que um commit de merge registra e quando o Git faz um fast-forward no lugar dele
- Usar rebase para manter o histórico linear e saber exatamente quando não usá-lo
- Resolver conflitos de merge com método e evitar as formas mais comuns de estragá-los
- Comparar Git Flow, GitHub Flow e trunk-based development e escolher um para o time
- Aplicar cherry-pick, rebase interativo e branches protegidas a situações reais de release
Criar uma branch no Git não custa nada, o que desloca as perguntas interessantes de "devemos criar uma branch?" para "quanto tempo uma branch vive, como ela volta para a linha principal e como fica o histórico depois?". Respostas diferentes produzem um dia a dia muito diferente: um time que faz rebase de branches curtas sobre a main várias vezes por dia vive em outro mundo em relação a um time que mantém branches develop, release/* e hotfix/* com uma cerimônia de merge para cada uma. Este guia explica a mecânica — merge, rebase, conflitos — com precisão suficiente para você prever o que um comando vai fazer e, depois, compara os fluxos de trabalho construídos sobre ela.
Branches, merge base e fast-forward
Uma branch é um ponteiro para um commit. Quando duas branches compartilham um ancestral comum e uma delas avançou, o Git chama esse ancestral de merge base. O merge traz para a branch atual as mudanças que a outra branch acumulou desde a merge base.
Se a branch atual não se moveu desde a merge base — a main não tem commits novos e a feature tem três —, não há nada a combinar. O Git simplesmente avança o ponteiro main até a ponta de feature. Isso é um merge fast-forward: nenhum commit de merge, um histórico linear e nenhum vestígio de que a branch existiu.
Se as duas branches avançaram, o Git faz um merge de três vias: compara a merge base com cada uma das pontas, aplica os dois conjuntos de mudanças e registra o resultado em um commit de merge com dois pais. É esse commit de merge que torna a branch visível no histórico daí em diante.
git switch main
git merge feature # fast-forward if possible, merge commit otherwise
git merge --no-ff feature # always create a merge commit, even when a fast-forward was possible
git merge --ff-only feature # refuse to merge unless a fast-forward is possible
--no-ff é usado por times que querem que cada funcionalidade apareça como um único commit de merge em git log --first-parent, o que permite reverter uma funcionalidade inteira com git revert -m 1 <merge-commit>. --ff-only é usado por times que exigem histórico linear e fazem rebase das branches antes do merge.
Rebase
git rebase main, executado na feature, pega cada commit da feature desde a merge base e o reaplica em cima da ponta atual da main, criando commits novos com hashes novos. O efeito é como se você tivesse criado a branch hoje: suas mudanças ficam em cima das de todo mundo, o histórico é linear e um merge posterior na main vira fast-forward.
O rebase é a ferramenta certa para manter uma branch privada atualizada. É a ferramenta errada para qualquer coisa que já esteja compartilhada, porque os commits reescritos têm hashes diferentes dos que seus colegas baixaram, e o próximo pull deles gera commits duplicados e conflitos desconcertantes. A regra é curta: nunca faça rebase de commits que existem fora do seu repositório, a menos que todos os envolvidos saibam e concordem (uma branch pessoal de pull request em que só você faz push é a exceção comum).
O rebase interativo (git rebase -i main) abre a lista de commits prestes a serem reaplicados e permite reordenar, juntar (squash), editar, reescrever a mensagem ou descartar cada um. É assim que você transforma oito commits "wip" em dois commits coerentes antes de abrir um pull request.
pick 3f2a1c9 Add DiscountCode entity
squash 8b7d0e2 fix typo
squash c41e77a wip
pick 92ad5b1 Apply discount codes at checkout
reword d05f3a6 tests
Depois que você salva, o Git reaplica os commits segundo as instruções e pede as mensagens quando necessário. Combinado com git add -p, o rebase interativo é o que permite entregar aos revisores poucos commits que fazem, cada um, uma única coisa.
Resolvendo conflitos
Um conflito acontece quando os dois lados mudaram as mesmas linhas (ou um apagou um arquivo que o outro editou) e o Git não consegue decidir. Ele para, marca o arquivo e espera.
<<<<<<< HEAD
BigDecimal total = subtotal.add(shipping);
=======
BigDecimal total = subtotal.add(shippingCost).add(tax);
>>>>>>> feature/tax
A parte acima de ======= é a branch atual (HEAD); a de baixo é a branch que está sendo mesclada. O procedimento:
Veja o que está em conflito
git statuslista os caminhos em "Unmerged paths".git diffmostra os conflitos com os dois lados.Edite cada arquivo até o resultado pretendido
Remova os marcadores e escreva o código que deve existir — muitas vezes uma combinação dos dois lados, às vezes nenhum deles. Compile e rode os testes antes de seguir em frente.Marque como resolvido
git add <file>avisa o Git de que o conflito foi resolvido.Conclua a operação
git merge --continue(ougit commit) para um merge;git rebase --continuepara um rebase, que pode então apresentar o próximo commit em conflito.--abort, em qualquer um dos dois, volta ao estado anterior ao início.
Ferramentas de merge (git mergetool, ou a visão de três painéis do IntelliJ IDEA e do VS Code) mostram base, "ours" e "theirs" lado a lado, o que é muito mais fácil do que ler marcadores para qualquer conflito com mais de uma ou duas linhas. Habilitar o rerere ("reuse recorded resolution") faz o Git lembrar como você resolveu um conflito e aplicar a mesma resolução automaticamente na próxima vez que o encontrar — valiosíssimo quando você faz rebase de uma branch longa repetidas vezes.
Dois hábitos reduzem conflitos mais do que qualquer ferramenta: integrar a main diariamente para que a divergência continue pequena e evitar reformatações em massa ou movimentação de arquivos no mesmo commit que mudanças de lógica.
Fluxos de trabalho comparados
Com a mecânica dominada, os fluxos de trabalho são receitas de quando criar branches e como fazer o merge.
Feature branches com pull requests
Cada mudança ganha uma branch a partir da main, o trabalho acontece nela e um pull request (merge request no GitLab) reúne os comentários de revisão e os resultados de CI antes que a branch seja mesclada e apagada. Esse é o ponto de partida no GitHub, no GitLab e no Bitbucket. As variáveis são o tempo de vida da branch e o estilo de merge: branches que duram um ou dois dias e são mescladas com squash ou rebase dão uma main limpa e linear; branches que vivem semanas recriam justamente os problemas de integração que a integração contínua existe para evitar.
GitHub Flow
Um modelo deliberadamente mínimo: a main está sempre pronta para deploy; toda mudança é uma branch e um pull request; fazer merge na main faz o deploy. Não há branches de release, porque uma release é simplesmente o que a main contém. Combina com serviços com deploy contínuo e com times que têm testes automatizados sólidos.
Git Flow
O modelo elaborado, com duas branches permanentes e três tipos de branches temporárias:
| Branch | Criada a partir de | Mesclada em | Finalidade |
|---|---|---|---|
main | — | — | Apenas releases de produção, cada uma com uma tag de versão |
develop | main | main via release | Branch de integração da próxima release |
feature/* | develop | develop | Funcionalidades individuais |
release/* | develop | main e develop | Estabilizar uma release: incremento de versão, apenas correções de bugs |
hotfix/* | main | main e develop | Correção urgente em produção |
O Git Flow responde a perguntas reais para software com releases versionadas e várias versões com suporte em uso pelos clientes: para onde vão as correções da 2.3 enquanto a 2.4 está sendo estabilizada e a 2.5 está em desenvolvimento? O custo é a cerimônia e as branches de vida longa — a develop pode se afastar da main por semanas —, e para um serviço web com vários deploys por dia ele é pura sobrecarga. O próprio autor do modelo observou que ele foi concebido antes de a entrega contínua virar prática comum.
Trunk-based development
Todo mundo faz commit na main (o trunk) pelo menos uma vez por dia, diretamente ou por meio de pull requests que duram horas. Funcionalidades incompletas ficam escondidas com feature flags ou construindo o novo caminho ao lado do antigo ("branch by abstraction"), e não em uma branch. Branches de release, quando existem, são criadas a partir do trunk e recebem apenas correções via cherry-pick, nunca trabalho novo. A pesquisa da DORA associa esse modelo a um desempenho de entrega mais alto, e ele é o parceiro natural da integração contínua. Exige confiança na suíte de testes, mudanças pequenas e investimento em feature flags; times sem isso acham o modelo desconfortável.
Como escolher
| Situação | Escolha razoável |
|---|---|
| Serviço web com deploy contínuo, uma versão em produção | GitHub Flow ou trunk-based |
| Produto com releases numeradas, uma versão com suporte | Feature branches mais uma branch de release de vida curta por versão |
| Biblioteca ou produto on-premise com várias versões com suporte | Git Flow ou um modelo de "branches de release com cherry-picks" |
| Time grande, monorepo, automação robusta | Trunk-based com feature flags |
| Aprovação regulatória de releases | Qualquer um dos anteriores, com a aprovação no deploy, não no merge |
A lição recorrente é que o fluxo de trabalho deve seguir o modelo de release, e não o contrário.
Branches de release, cherry-picks e tags
Mesmo times trunk-based às vezes precisam de uma branch de release: uma versão já foi entregue aos clientes e precisa de uma correção enquanto a main seguiu em frente. O padrão é criar release/2.4 a partir do commit com tag, corrigir o bug primeiro na main (para que ele nunca se perca), depois fazer git cherry-pick <hash> na branch de release, criar ali a tag v2.4.1 e gerar o build a partir da tag. Corrigir primeiro na main evita a falha clássica em que uma correção vai só para a branch de release e reaparece como regressão na versão seguinte.
Proteja as branches importantes na plataforma de hospedagem: exija pull requests, exija que a verificação de CI passe e proíba force-pushes e exclusões na main e em release/*. Regras de proteção não custam nada e evitam os acidentes mais destrutivos.
Convenções de commit
Um modelo de branches funciona melhor com commits consistentes. Conventional Commits (feat:, fix:, refactor:, com um escopo opcional e um ! para breaking changes) é um formato amplamente usado que as ferramentas conseguem interpretar para gerar changelogs e calcular automaticamente a próxima versão semântica. Adotando ou não o formato de prefixos, mantenha os commits da branch significativos depois do rebase, nomeie as branches pela intenção (fix/null-shipping-cost, feat/discount-codes) e referencie o issue tracker no pull request, e não em cada commit.
Erros comuns
- Fazer rebase de uma branch compartilhada. Os colegas recebem commits duplicados e conflitos no próximo pull. Faça rebase só do que é seu; faça merge de todo o resto.
- Feature branches de vida longa. Três semanas de trabalho integradas de uma vez produzem uma maratona de conflitos e escondem bugs de integração. Faça merge de pedaços pequenos atrás de flags.
- Resolver um conflito mantendo "ours" em tudo. Escolher um lado às cegas descarta a mudança da outra pessoa e reintroduz o bug que ela corrigiu. Leia os dois lados.
- Fazer merge da
mainem uma feature branch repetidamente e depois fazer o merge de volta. O histórico vira uma malha de commits de merge que esconde o que a funcionalidade mudou. Em vez disso, faça rebase das branches privadas ou traga amaincom moderação. - Hotfix só na branch de release. A correção nunca chega à
maine o bug volta na próxima release. Corrija primeiro namaine depois leve a correção por cherry-pick para a branch de release. - Apagar a branch antes de o pull request ser mesclado. Na maioria das plataformas, o pull request é fechado junto com ela. Deixe a plataforma apagar a branch no merge.
- Fazer force-push sem
--force-with-lease. Sobrescreve o commit que um colega enviou um minuto antes.
Principais pontos
- Um merge faz fast-forward quando o destino não se moveu; caso contrário, cria um commit de merge com dois pais que registra a combinação.
- O rebase recria commits em cima de outra branch e gera histórico linear; use-o em branches privadas e nunca em commits que outras pessoas possam ter baixado.
- Resolva conflitos lendo os dois lados, editando até o resultado pretendido e testando, depois
adde--continue;rereree a integração diária tornam os conflitos raros e pequenos. - GitHub Flow e trunk-based development servem para deploy contínuo; Git Flow serve para produtos versionados com várias releases com suporte; alinhe o fluxo de trabalho ao modelo de release.
- Corrija primeiro na
maine faça cherry-pick para as branches de release, marque as releases com tags anotadas e proteja amaincontra force-pushes e merges sem revisão. - Commits e branches pequenos e bem nomeados, organizados com rebase interativo antes da revisão, deixam qualquer fluxo de trabalho mais fácil de levar no dia a dia.
Fontes e leituras complementares
Perguntas frequentes
- Devo fazer merge ou rebase da minha feature branch sobre a main?
- Faça rebase enquanto a branch for só sua: o histórico fica linear e seus commits passam para cima da main mais recente, então o merge final é trivial. Assim que a branch for enviada ao remoto e outras pessoas puderem ter baseado trabalho nela, pare de fazer rebase e faça merge. Muitos times combinam as duas coisas: rebase local para se atualizar e, depois, squash-merge ou merge com --no-ff por meio de um pull request.
- O Git Flow ainda é recomendado?
- O próprio autor disse que ele serve para software com releases versionadas explícitas e várias versões com suporte — aplicações desktop, bibliotecas, produtos on-premise — e que serviços web com deploy contínuo são mais bem atendidos por um modelo mais simples. Se você faz deploy a partir da main várias vezes por dia, as branches develop e release do Git Flow acrescentam cerimônia sem trazer benefício.
- Como levo um único commit de correção da main para uma branch de release antiga?
- Com a branch de release em checkout, use git cherry-pick com o hash do commit. O Git aplica a mesma mudança como um novo commit na branch de release. Cherry-picks funcionam melhor quando o commit original é pequeno e autocontido; se ele depender de outras mudanças, você vai esbarrar em conflitos, e muitas vezes é mais limpo corrigir primeiro na branch com suporte mais antiga e ir mesclando dali para as mais novas.
- O que é um squash merge e quando ele é uma má ideia?
- Um squash merge junta todos os commits de uma branch em um único commit no destino, o que dá um histórico limpo com um commit por funcionalidade. É um bom padrão para pull requests cheios de commits de trabalho em andamento. É uma má ideia quando os commits individuais da branch têm significado — um refactoring seguido de uma mudança de comportamento — ou quando você vai fazer merge da mesma branch de novo mais tarde, porque o Git não consegue saber que o commit squashed veio daquela branch e vai repetir os conflitos.