O Modelo Mental do Git para Equipes Rust
Os comandos do dia a dia do Git (branch, merge, rebase, worktree) são todas visões sobre uma única estrutura subjacente: um grafo direcionado acíclico (DAG) de commits. Cada conflito de merge confuso, cada momento de "espera, onde foram meus commits" e cada dúvida sobre worktree se resolvem claramente quando esse grafo se torna o modelo mental em vez de uma pasta de versões de arquivos.
Isso importa mais para equipes Rust do que pode parecer à primeira vista. Um workspace Cargo agrupa vários crates e um único Cargo.lock em um único repositório, então as mesmas operações de grafo que parecem inofensivas em um repositório de script de crate único começam a interagir com a instabilidade do lockfile, coordenação de release de múltiplos crates e longos ciclos de cargo build que tornam a troca de branches custosa. Entender o grafo, não apenas os comandos, é o que permite a uma equipe raciocinar sobre essas interações em vez de memorizar workarounds.
Resumo
- O histórico do Git é um DAG de commits imutáveis; branches, merges e rebases são todas operações que adicionam nós ou renomeiam ponteiros nesse grafo.
- Por que Importa: Tratar commits como "versões de arquivos" em vez de nós de grafo faz com que conflitos de merge, rebase e worktrees pareçam mágica em vez de álgebra de grafo previsível.
- Conceitos Chave: commit, branch (ref), HEAD, merge commit, rebase, worktree, object database.
- Quando Usar: Sempre que você raciocinar sobre "este merge será limpo?", "o que o rebase realmente faz com meus commits?" ou "por que dois worktrees podem compartilhar histórico sem duplicá-lo?".
- Limitações / Trade-offs: O modelo de grafo explica o que o Git faz, não o que uma equipe deve fazer com ele; a estratégia de branching ainda é uma escolha de política sobreposta.
- Tópicos Relacionados: Desenvolvimento baseado em trunk, trens de release, resolução de conflitos do
Cargo.lock, filas de merge de CI.
Fundamentos
Um commit é um snapshot imutável e endereçado por conteúdo: uma árvore de conteúdos de arquivos naquele momento, mais metadados (autor, mensagem, timestamp), mais um ponteiro para seu commit pai (ou dois pais, para um merge). Nada em um commit muda após sua criação; o hash SHA-1 (ou SHA-256) que o identifica é derivado de seu conteúdo, então qualquer edição produz um novo commit com um novo hash em vez de mutar o antigo.
Uma branch não é um contêiner que guarda commits. É um rótulo leve e móvel apontando para um commit. Criar feat/billing-tax não copia nada, apenas escreve um novo ponteiro no commit atual; commitar nessa branch move o ponteiro para frente um nó por vez. HEAD é um ponteiro adicional que geralmente aponta para uma branch, dizendo ao Git qual branch se move quando você commita.
Encadeie commits suficientes, cada um apontando de volta para seu pai, e o resultado é um grafo direcionado acíclico: direcionado porque os ponteiros de pai só vão em uma direção, acíclico porque um commit nunca pode ser seu próprio ancestral. Uma analogia útil é uma árvore genealógica de snapshots de sistema de arquivos, onde qualquer commit pode ser alcançado andando para trás pelos links de pai, e onde duas branches compartilhando um ancestral formam uma bifurcação na árvore que pode ser reconciliada mais tarde.
main: A---B---C
\
feat/x: D---E
Aqui A, B e C são commits em main; feat/x bifurcou de B e adicionou D e E. Ambos os rótulos de branch são apenas ponteiros para o mesmo grafo de commits, não armazenamento separado.
Mecânicas e Interações
Merging reconcilia duas linhas de histórico criando um novo commit com dois pais, um em cada ponta da branch, mais uma árvore combinada computada por um diff de três vias contra o ancestral comum. Continuando o diagrama acima, fazer merge de feat/x em main adiciona o commit F:
main: A---B---C-------F
\ /
feat/x: D-------E
F não reescreveu C, D ou E, ele simplesmente adicionou um novo nó com dois ponteiros de pai, que é por que o merge preserva os commits exatos que cada branch contém. Um merge fast-forward é o caso especial onde main não se moveu desde que as branches divergiram, então o Git pode simplesmente deslizar o ponteiro de main para E sem nenhum novo commit.
Rebase faz o oposto: em vez de adicionar um nó de reconciliação, ele reproduz cada commit em feat/x como um commit totalmente novo com um pai diferente, descartando os originais. Fazer rebase de feat/x em main produz D' e E', estruturalmente semelhantes a D e E, mas com novos hashes porque seu pai mudou:
main: A---B---C
\
feat/x': D'---E'
Esta é a fonte mais comum de confusão no Git: rebase não "move" commits, ele cria novos commits e abandona os antigos, que é exatamente por que fazer rebase de uma branch depois que outra pessoa já a puxou reescreve o histórico debaixo dela. É também por que "meus commits desapareceram" após um rebase quase sempre significa que os commits antigos ainda são alcançáveis, apenas via git reflog, não verdadeiramente perdidos, porque o Git não faz garbage collection de commits não referenciados imediatamente.
Um worktree não mexe no modelo de grafo, ele muda quantos diretórios de trabalho apontam para o mesmo banco de dados de objetos. git worktree add ../review origin/feat/x cria um segundo checkout com seu próprio HEAD e índice, mas ambos os worktrees leem e escrevem commits no exato mesmo store de objetos .git, então um commit feito em um worktree é imediatamente visível para o outro via o grafo compartilhado. É por isso que worktrees são baratos comparados a um segundo git clone: existe apenas uma cópia do grafo de commits, apenas múltiplos ponteiros para ele e múltiplos checkouts de sistema de arquivos de snapshots específicos.
Considerações Avançadas e Aplicações
Na escala de Cargo-workspace, o modelo de grafo tem uma consequência muito concreta: Cargo.lock é um único arquivo na raiz do workspace, então quaisquer duas branches que cada uma atualize uma versão de dependência produzirão um conflito de merge de três vias nesse arquivo, mesmo quando as mudanças reais de origem não se sobrepõem. A correção é ciente do grafo, não do lockfile: fazer rebase frequentemente mantém a divergência entre uma branch de feature e main pequena, o que mantém o diff de três vias pequeno, o que mantém os conflitos raros e mecânicos (cargo update -p <crate> para regenerar) em vez de grandes e manuais.
Trens de release complicam isso ainda mais. Uma branch release/2.6.0 bifurca o grafo em um ponto no tempo, e cada cherry-pick subsequente nela cria um novo commit (com um novo hash) que compartilha conteúdo, mas não identidade com sua contraparte em main. Equipes que entendem isso esperam que git log --oneline na branch de release se pareça com, mas nunca idêntico a, o histórico correspondente em main, o que evita a suposição equivocada de que fazer merge de main de volta em uma branch de release ativa é seguro apenas porque "os commits já existem em algum lugar".
Worktrees provam seu valor especificamente porque os tempos de build do Rust não são triviais: trocar branches em um único diretório de trabalho invalida o estado incremental de target/, enquanto um segundo worktree mantém um target/ separado ativo para um build de release de longa duração ou uma revisão de PR, sem pagar por um segundo clone completo do banco de dados de objetos.
| Abordagem | Força | Fraqueza | Melhor Encaixe |
|---|---|---|---|
| Merge commit | Preserva commits originais exatos e forma de histórico real | O grafo fica largo com muitos nós de merge ao longo do tempo | Branches compartilhadas de longa duração (trens de release) |
| Rebase | Histórico linear e fácil de ler; diffs pequenos contra main | Reescreve a identidade do commit; inseguro em branches compartilhadas | Branches de feature privadas antes da revisão |
| Squash merge | Um commit limpo por ticket em main | Perde o histórico em nível de commit intermediário | Pequenas equipes que querem um main simples e legível |
Equívocos Comuns
- "Branches contêm commits" - uma branch é um ponteiro para um commit; os commits que ela parece "conter" são apenas tudo o que é alcançável andando pelos links de pai a partir desse ponteiro.
- "Rebase perde meu trabalho" - rebase cria novos commits e abandona os antigos, mas os commits antigos permanecem alcançáveis via
reflogaté que o garbage collection seja executado, então o trabalho é recuperável, não destruído. - "Fazer merge de main em uma branch de release a mantém sincronizada com segurança" - puxa todos os commits alcançáveis de
main, incluindo trabalho não verificado, não apenas as correções que a branch de release deseja. - "Worktrees duplicam o repositório" - eles duplicam o diretório de trabalho e o índice, não o banco de dados de objetos, que é por que eles são muito mais baratos do que um segundo clone.
- "Um merge fast-forward e um merge real fazem a mesma coisa" - um fast-forward apenas move um ponteiro sem novo commit; um merge verdadeiro cria um commit de dois pais e um ponto de bifurcação visível no grafo.
FAQs
O que é realmente um commit, mecanicamente?
Um snapshot do diretório endereçado por conteúdo mais metadados mais um ou mais ponteiros de pai, identificado por um hash de seu próprio conteúdo. Nada sobre ele muda após a criação.
Se branches são apenas ponteiros, o que se move quando eu commito?
O rótulo da branch para o qual HEAD aponta atualmente avança para o novo commit; o commit antigo se torna o pai desse novo commit.
Por que o rebase muda os hashes dos commits?
Porque o hash de um commit é derivado de seu conteúdo, incluindo seu ponteiro pai, e o rebase dá a cada commit reproduzido um novo pai, o que muda seu hash mesmo que o diff do arquivo seja idêntico.
É seguro fazer rebase de uma branch que outros já puxaram?
Apenas com coordenação (force-push com lease e um aviso), pois todos os outros em suas branches locais ainda apontam para os commits antigos e agora abandonados e divergirão.
Por que workspaces Cargo veem mais conflitos de merge do que repositórios de crate único?
Porque Cargo.lock é um único arquivo compartilhado na raiz do workspace, então bumps de dependência não relacionados em branches diferentes colidem lá, mesmo quando as mudanças de origem não se sobrepõem.
Como um worktree evita duplicar todo o repo?
Todos os worktrees compartilham um banco de dados de objetos .git; apenas o diretório de trabalho e o índice são separados por worktree, então os commits são visíveis entre worktrees imediatamente.
O que "fast-forward" realmente pula?
Ele pula a criação de um merge commit inteiramente, já que o ponteiro de main pode simplesmente deslizar para a ponta da branch de feature quando não existem commits divergentes em main.
Por que commits "perdidos" após um rebase geralmente voltam?
Porque o Git não deleta commits não referenciados imediatamente; git reflog rastreia posições recentes do HEAD, então os commits abandonados ainda são alcançáveis até que o garbage collection seja executado.
O squash merge muda o modelo de grafo?
Não, ainda é apenas um commit com um ponteiro pai; ele simplesmente descarta as identidades individuais dos commits intermediários em favor de um snapshot combinado.
Por que o histórico de uma branch de release diverge do main mesmo após um cherry-pick?
Cherry-picking reproduz as mudanças de um commit como um commit totalmente novo com um pai diferente, o mesmo mecanismo do rebase, então ele recebe um novo hash distinto do original em main.
O modelo de grafo explica por que o bisect funciona?
Sim: git bisect é uma busca binária na cadeia de ancestralidade no DAG, o que só funciona porque essa cadeia é um grafo bem definido e percorrível de ponteiros pai.
Tags também se encaixam neste modelo de grafo?
Uma tag é outro ponteiro, como uma branch, exceto que ela não se move à medida que novos commits são adicionados; ela marca permanentemente um commit, que é por que tags de release permanecem referências estáveis.
Relacionados
- Branching & Merging - a receita do dia a dia construída sobre este modelo de grafo.
- Git Worktrees - a mecânica de banco de dados de objetos compartilhado na prática.
- Merge Conflict Resolution Checklist - lidando com conflitos de
Cargo.locke de origem. - Essential Git Commands - os comandos diários que operam neste grafo.
Versões da Stack: Esta página é conceitual e não está vinculada a uma versão específica da stack.