image

Acesse bootcamps ilimitados e +650 cursos pra sempre

60
%OFF
Article image

LF

Luis Ferreira19/08/2025 17:32
Compartilhe

Git Merge, rebase, cherry-pick e squash

    O Git oferece um conjunto de ferramentas poderosas para gerenciar o histórico do seu projeto. Entender como git merge, git rebase, git cherry-pick e git squash funcionam é fundamental para um fluxo de trabalho colaborativo eficiente e para manter um histórico de commits limpo e compreensível.

    Vamos explorar cada um deles em detalhes.

    1. Definição e Propósito de Cada Comando

    git merge

    • Definição: O git merge é usado para integrar alterações de uma branch para outra.
    • Propósito: Combinar o trabalho de diferentes branches de forma a preservar o histórico exato de como as coisas aconteceram, incluindo a divergência das branches.
    • Como funciona: Ele cria um novo commit (conhecido como "merge commit") que tem dois ou mais pais. Este commit aponta para os estados finais das branches que estão sendo mescladas, unindo seus históricos. Se as branches não tiverem divergido (i.e., uma é um ancestral direto da outra), o Git pode fazer um "fast-forward merge", que simplesmente move o ponteiro da branch atual para o commit mais recente da branch a ser mesclada, sem criar um novo merge commit.

    git rebase

    • Definição: O git rebase é usado para re-aplicar uma série de commits de uma branch em cima de outra branch base.
    • Propósito: Manter um histórico de commits linear e limpo, fazendo parecer que todo o desenvolvimento ocorreu sequencialmente em uma única linha.
    • Como funciona: Ele move a base da sua branch de trabalho para o final de outra branch. Em essência, o Git "recorta" os commits da sua branch, aplica-os um a um no topo da branch de destino e, em seguida, move o ponteiro da sua branch para o commit final re-aplicado. Isso re-escreve o histórico de commits, gerando novos hashes para os commits re-aplicados.

    git cherry-pick

    • Definição: O git cherry-pick é usado para aplicar um commit específico (e apenas esse commit) de uma branch em outra.
    • Propósito: Portar um fix urgente ou uma funcionalidade específica de uma branch para outra, sem ter que mesclar branches inteiras.
    • Como funciona: Ele pega as alterações introduzidas por um único commit e as aplica como um novo commit na branch atual. O novo commit terá um novo hash, mas o mesmo conteúdo de alteração do commit original.

    git squash (geralmente via git rebase -i)

    • Definição: O git squash é uma operação que combina múltiplos commits em um único commit. Não é um comando Git direto como merge ou rebase, mas uma ação disponível dentro do git rebase -i (rebase interativo).
    • Propósito: Limpar um histórico de commits, consolidando commits intermediários (como "WIP" - Work In Progress, "fix typo", etc.) em um commit significativo e coeso antes de um merge final ou envio para revisão.
    • Como funciona: Durante um git rebase -i, você marca os commits que deseja "squashar" (ou s para abreviar). O Git então combina esses commits em um único, permitindo que você escreva uma nova mensagem de commit para o commit consolidado. Isso também re-escreve o histórico.

    2. Exemplos Práticos de Uso e Cenários

    Vamos configurar um cenário inicial para nossos exemplos:

    # Inicializa um novo repositório Git
    git init
    echo "initial content" > file.txt
    git add .
    git commit -m "C1: Initial commit"
    ​
    # Cria e alterna para uma nova feature branch
    git branch feature-a
    git checkout feature-a
    echo "feature-a line 1" >> file.txt
    git commit -m "C2: Add feature A part 1"
    echo "feature-a line 2" >> file.txt
    git commit -m "C3: Add feature A part 2"
    ​
    # Volta para a branch main e adiciona um commit independente
    git checkout main
    echo "main line 1" >> file.txt
    git commit -m "C4: Add main line"
    

    Histórico Inicial:

    C1 (Initial commit)
    ├── C2 (Add feature A part 1) - on feature-a
    └── C3 (Add feature A part 2) - on feature-a
    └── C4 (Add main line) - on main
    

    Representação visual do histórico:

    A -- B (main)
     \
    C -- D (feature-a)
    ​
    # Onde:
    # A = C1
    # B = C4
    # C = C2
    # D = C3
    

    Exemplo: git merge

    • Cenário: Você finalizou o desenvolvimento na feature-a e quer integrá-la à main, preservando o histórico de ambas as branches.
    • Comandos:
    git checkout main
    git merge feature-a
    
    • Impacto no Histórico: O Git cria um novo commit de merge que une os históricos.
    • Histórico Após Merge:
    A -- B -- M (Merge feature-a into main)
     \    /
    C -- D
    
    • M é o novo merge commit.
    • Ambos B (o último commit da main) e D (o último commit da feature-a) são pais de M.
    • Output Esperado:
    Merging branches/feature-a into main
    ... (detalhes dos arquivos alterados) ...
    Merge made by the 'recursive' strategy.
     file.txt | 2 ++
     1 file changed, 2 insertions(+)
    
    • Quando é Mais Adequado:
    • Para integrar branches de feature completas na main ou develop.
    • Em fluxos de trabalho colaborativos onde a preservação do histórico exato de merges é importante.
    • Quando você não quer reescrever o histórico de commits.

    Exemplo: git rebase

    • Cenário: Você está desenvolvendo em feature-a e quer incorporar as últimas alterações da main para manter sua branch atualizada, mas deseja um histórico linear antes de um futuro merge (talvez um fast-forward merge).
    • Comandos:
    git checkout feature-a
    git rebase main
    
    • Impacto no Histórico: O Git re-aplica os commits C2 e C3 da feature-a após C4 da main. Os commits re-aplicados (C2' e C3') terão novos hashes.
    • Histórico Após Rebase:
    A -- B -- C' -- D'
    
    • C' é C2 re-aplicado em cima de B.
    • D' é C3 re-aplicado em cima de C'.
    • feature-a agora aponta para D'.
    • main ainda aponta para B.
    • Output Esperado:
    Successfully rebased and updated refs/heads/feature-a.
    
    • Quando é Mais Adequado:
    • Para manter seu branch de trabalho (local) atualizado com a main antes de um push ou merge.
    • Para criar um histórico de commits linear e "limpo", facilitando a leitura e ferramentas como git bisect.
    • Importante: Nunca rebaseie commits que já foram compartilhados em um repositório remoto público, pois isso pode causar problemas graves para outros colaboradores.

    Exemplo: git cherry-pick

    • Cenário: Imagine que na feature-a (após os commits C2 e C3), você adicionou um commit C5 que corrige um bug crítico que também afeta a main. Você quer aplicar apenas esse fix na main sem trazer o resto da feature-a.
    • Vamos criar um C5 na feature-a:
    git checkout feature-a
    echo "bug fix for critical issue" >> bug_fix.txt
    git add bug_fix.txt
    git commit -m "C5: Critical bug fix"
    
    • Histórico da feature-a agora: C1 -- C2 -- C3 -- C5
    • Comandos:
    git checkout main
    git cherry-pick <hash_do_C5> # Substitua <hash_do_C5> pelo hash real do commit C5
    
    • Impacto no Histórico: Um novo commit (C5') é criado na main com as alterações de C5.
    • Histórico Após Cherry-pick:
    A -- B -- C5' (on main)
     \
    C -- D -- C5 (on feature-a)
    
    • Output Esperado:
    [main <novo_hash>] C5: Critical bug fix
     1 file changed, 1 insertion(+)
     create mode 100644 bug_fix.txt
    
    • Quando é Mais Adequado:
    • Para aplicar hotfixes em branches de produção.
    • Para portar commits específicos entre branches sem mesclar tudo.
    • Para integrar uma pequena funcionalidade isolada.

    Exemplo: git squash (via git rebase -i)

    • Cenário: Na sua feature-a você tem os commits C2 ("Add feature A part 1") e C3 ("Add feature A part 2"). Você decide que esses dois commits representam uma única unidade lógica de trabalho e deveriam ser um só commit antes de serem integrados à main.
    • Comandos:
    git checkout feature-a
    git rebase -i HEAD~2 # Isso abre o rebase interativo para os últimos 2 commits
    
    • Isso abrirá um editor de texto com algo assim:
    pick <hash_C2> C2: Add feature A part 1
    pick <hash_C3> C3: Add feature A part 2
    
    # Rebase <hash_C1>..<hash_C3> onto <hash_C1>
    #
    # 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> = discard 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
    #
    # 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.
    #
    # Note that empty commits are commented out
    
    • Para squashear C3 em C2, você muda a linha de C3 para squash (ou s):
    pick <hash_C2> C2: Add feature A part 1
    squash <hash_C3> C3: Add feature A part 2
    
    • Salve e feche o editor. O Git então abrirá um novo editor para você escrever a mensagem do novo commit combinado.
    • Impacto no Histórico: Os commits C2 e C3 são substituídos por um único novo commit (C2-C3-squashed).
    • Histórico Após Squash:
    A -- C2-C3-squashed (on feature-a)
    
    • Output Esperado:
    [feature-a <novo_hash>] Combined commit for Feature A
     2 files changed, 2 insertions(+)
     ...
    Successfully rebased and updated refs/heads/feature-a.
    
    • Quando é Mais Adequado:
    • Para limpar o histórico de uma branch de feature antes de um merge.
    • Para agrupar commits relacionados (e.g., um commit de funcionalidade e vários commits de correção para essa funcionalidade).
    • Para tornar o histórico mais legível e facilitar futuras revisões ou git bisect.

    3. Comparação Direta entre git merge e git rebase

    Essas duas são as ferramentas mais comuns para integrar branches, mas têm filosofias muito diferentes e impactos distintos no histórico.

    Como Eles Lidam com a Integração de Branches

    • git merge:
    • Realiza uma integração não-destrutiva.
    • Ele encontra o ancestral comum mais recente entre as duas branches e, em seguida, combina as alterações de cada branch a partir desse ponto.
    • O resultado é um novo "merge commit" que tem os dois branches como seus pais. Isso cria um histórico que mostra claramente quando e onde as branches divergiram e foram reunidas.
    • git rebase:
    • Realiza uma integração que re-escreve o histórico.
    • Ele pega os commits da sua branch atual e os "re-aplica" um a um em cima do commit mais recente da branch de destino.
    • Isso faz parecer que os seus commits foram criados diretamente no topo da branch de destino, resultando em um histórico linear. Os commits originais na sua branch são descartados e novos commits com novos hashes são criados.

    Impacto no Histórico de Commits

    Característicagit mergegit rebaseTipo de HistóricoNão-linear (branching history)Linear (cleaner history)PreservaçãoPreserva o histórico exato da ramificação e fusãoReescreve o histórico; commits antigos são perdidos, novos criadosMerge CommitsSempre cria um novo "merge commit" (exceto fast-forward)Não cria merge commits (exceto em rebase interativo com merge action)Hashes de CommitsCommits originais mantêm seus hashesCommits re-aplicados obtêm novos hashes

    Vantagens e Desvantagens de Cada Abordagem

    Característicagit mergegit rebaseVantagens- Preserva o contexto histórico exato. - Mais seguro para branches públicas/compartilhadas. - Simples de usar.- Histórico de commits limpo e linear, fácil de seguir. - Facilita ferramentas como git bisect e git blame. - Evita merge commits "poluídos".Desvantagens- O histórico pode se tornar "bagunçado" com muitos merge commits. - Dificulta a leitura da linha principal de desenvolvimento.- Reescreve o histórico: perigoso em branches que já foram pushadas e são compartilhadas com outros (requer git push --force). - Pode ser mais complexo para resolver conflitos, pois eles são apresentados sequencialmente. - Pode esconder a realidade de onde as divergências aconteceram.

    4. Discussão sobre Melhores Práticas

    A escolha entre merge e rebase depende muito do seu fluxo de trabalho e das políticas da sua equipe.

    • Desenvolvimento Colaborativo em Equipe:
    • Regra de Ouro: Nunca rebaseie commits que já foram pushados para um repositório remoto e são compartilhados com outros. Reescrever histórico público pode causar confusão e problemas para seus colegas, pois eles teriam que re-sincronizar seus próprios repositórios.
    • Para integração de features em branches principais (ex: main, develop): Geralmente, git merge é preferível para manter a integridade do histórico compartilhado. Ele mostra explicitamente quando uma feature foi integrada.
    • Para manter sua branch local atualizada: Use git pull --rebase em vez de git pull (que é um fetch seguido de merge). Isso re-aplicará seus commits locais em cima dos commits mais recentes do repositório remoto, mantendo seu histórico local linear antes de você pushar.
    • Para limpar sua branch de feature *antes* do push: git rebase -i é excelente para organizar e squashear seus commits em uma branch local antes de enviá-la para revisão ou merge na branch principal.
    • Manutenção de Branches de Longa Duração:
    • Se você tem branches de longa duração (ex: para uma versão futura), geralmente é melhor usar git merge para trazer as atualizações da main para dentro dela regularmente. Isso evita que a branch se desvie muito e acumule muitos conflitos, além de preservar o histórico de onde as atualizações vieram.
    • git rebase pode ser usado em branches de longa duração apenas se você for o único trabalhando nelas ou se a equipe for pequena e houver comunicação clara sobre o rebase.
    • Preparação de Histórico de Commits para Release:
    • O git rebase -i é uma ferramenta poderosa para "polir" o histórico de uma branch de feature antes que ela seja mesclada em uma branch de release ou main.
    • Você pode usar squash ou fixup para combinar commits intermediários em unidades lógicas. Isso resulta em um histórico de commits limpo, com cada commit representando uma alteração significativa e atômica, o que é ótimo para revisões de código, git log e reversões futuras.

    5. Exemplos de como git cherry-pick e git squash podem ser usados em conjunto

    Esses comandos são complementares e podem ser combinados para um controle granular do seu histórico.

    Cenário: Você está trabalhando em uma feature-branch e tem vários commits. Um dos commits é um bug fix crucial (C_fix) que a equipe de QA precisa testar na main imediatamente. Além disso, sua feature-branch tem vários commits de trabalho em progresso (WIP) que você gostaria de consolidar antes de mesclar a feature final.

    # Setup
    git checkout main
    # Suponha que main está em: A -- B -- C_main
    
    git checkout -b feature-branch
    # Commits na feature-branch:
    # D: Implement part 1
    # E: WIP - more changes
    # F: C_fix: Critical bug fix for feature
    # G: WIP - final adjustments
    # H: Refactor code
    
    # Histórico inicial da feature-branch: C_main -- D -- E -- F -- G -- H
    
    1. git cherry-pick** o hotfix para a main:**
    git checkout main
    git cherry-pick <hash_do_F> # Hash do commit 'F: C_fix: Critical bug fix'
    
    1. Histórico após cherry-pick:
    C_main -- C_fix' (on main)
     \
    D -- E -- F -- G -- H (on feature-branch)
    
    • C_fix' é o novo commit na main contendo apenas o bug fix.
    1. git squash** os commits da feature-branch (usando rebase -i):** Agora, você quer limpar a feature-branch antes de integrá-la completamente na main. Você decide que os commits D, E, F, G e H podem ser consolidados em um ou dois commits mais significativos.
    git checkout feature-branch
    git rebase -i <hash_de_C_main> # Ou HEAD~5 se forem os 5 últimos commits
    
    1. No editor interativo, você pode definir as ações:
    pick <hash_de_D> D: Implement part 1
    squash <hash_de_E> E: WIP - more changes
    squash <hash_de_F> F: C_fix: Critical bug fix for feature # Mesmo que foi cherry-picked, pode ser squashed aqui
    squash <hash_de_G> G: WIP - final adjustments
    squash <hash_de_H> H: Refactor code
    
    1. (Ou você poderia agrupar de forma diferente, como pick D, squash E, pick F, squash G, squash H para ter dois commits na feature-branch). Após salvar e fechar, você escreverá a nova mensagem para o(s) commit(s) consolidado(s).
    2. Histórico da feature-branch** após squash (exemplo: tudo em um único commit):**
    C_main -- Feature_Consolidated (on feature-branch)
    
    1. git rebase** a feature-branch limpa na main:** Agora que sua feature-branch está limpa e consolidada, você pode rebaseá-la na main para prepará-la para um merge linear (fast-forward, se possível) ou um merge normal.
    git rebase main
    
    1. Histórico após rebase:
    C_main -- C_fix' -- Feature_Consolidated' (on main, after rebase)
    
    • C_fix' é o commit do hotfix que já estava na main.
    • Feature_Consolidated' é a versão re-aplicada do seu commit consolidado da feature.
    1. Agora, você pode fazer um git merge feature-branch na main e, como a feature-branch está à frente da main, um fast-forward merge ocorrerá, resultando em um histórico linear:
    C_main -- C_fix' -- Feature_Consolidated' (main e feature-branch apontam para cá)
    

    Este exemplo demonstra como você pode usar cherry-pick para integração seletiva e rebase -i (com squash) para limpar e consolidar commits, criando um histórico limpo e gerenciável.

    6. Ilustrações Visuais (Descrições Textuais)

    Para ilustrar como cada comando altera o histórico de commits, usaremos uma notação simples:

    • A, B, C, ... representam commits.
    • -- representa uma conexão de pai para filho (um commit vem depois do outro).
    • -- M -- representa um merge commit.
    • X' representa uma versão re-aplicada ou nova de um commit X.

    Cenário Inicial:

    Temos uma main branch com commits A e B. Temos uma feature-branch que divergiu de A e tem commits C e D.

          C -- D  (feature-branch)
         /
    A -- B          (main)
    

    git merge (Exemplo: git checkout main; git merge feature-branch)

    O git merge cria um novo commit (M) que une as duas linhas de desenvolvimento. O histórico da feature-branch é preservado.

          C -- D
         /      \
    A -- B --------- M (Merge Commit)
    
    • M tem B e D como seus pais.

    git rebase (Exemplo: git checkout feature-branch; git rebase main)

    O git rebase "move" os commits da feature-branch para o topo da main. Os commits C e D são re-escritos como C' e D'.

    A -- B -- C' -- D'  (feature-branch)
    
    • main ainda está em B.
    • O histórico é linearizado.

    git cherry-pick (Exemplo: git checkout main; git cherry-pick D)

    O git cherry-pick aplica um commit específico (D) como um novo commit (D') na branch atual (main).

          C -- D  (feature-branch)
         /
    A -- B -- D'    (main)
    
    • D' é um novo commit na main com o mesmo conteúdo de D.

    git squash (Exemplo: git checkout feature-branch; git rebase -i HEAD~2 para squashear C e D)

    O git squash (através de rebase -i) combina múltiplos commits (C e D) em um único commit (CD_squashed).

    A -- CD_squashed (feature-branch)
    
    • CD_squashed é um novo commit que consolida as alterações de C e D.

    Em resumo, o Git oferece flexibilidade imensa para gerenciar seu histórico. A chave é entender as diferenças e escolher a ferramenta certa para o trabalho, sempre lembrando da regra de ouro: nunca reescreva o histórico de branches que já foram compartilhadas publicamente. Com um bom entendimento e prática, você pode manter um histórico de commits limpo, legível e útil para sua equipe e para o futuro do seu projeto.

    Compartilhe
    Recomendados para você
    Ri Happy - Front-end do Zero #2
    Avanade - Back-end com .NET e IA
    Akad - Fullstack Developer
    Comentários (0)