Git Avançado
Internals do Git: O Modelo de Objetos
O Git não é um sistema de controle de versão baseado em diffs — é um content-addressable filesystem. Cada objeto é identificado por um hash SHA-1 (ou SHA-256 no Git 2.42+) do seu conteúdo.
# Os 4 tipos de objetos fundamentais do Git:
# 1. BLOB — conteúdo puro de um arquivo (sem nome, sem permissões)
echo "hello" | git hash-object --stdin -w
# Gera: ce013625030ba8dba906f756967f9e9ca394464a
# Armazenado em .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
# 2. TREE — representação de um diretório (aponta para blobs e outras trees)
git cat-file -p HEAD^{tree}
# 100644 blob a1b2c3d4... README.md
# 040000 tree e5f6a7b8... src/
# 3. COMMIT — snapshot + metadados (autor, data, mensagem, parent(s))
git cat-file -p HEAD
# tree 4b825dc6...
# parent a1b2c3d4...
# author Lucas <lucas@email.com> 1704067200 -0300
# committer Lucas <lucas@email.com> 1704067200 -0300
#
# feat: adicionar autenticação JWT
# 4. TAG (annotated) — referência nomeada para um commit com metadados
git tag -a v1.0.0 -m "Release estável 1.0.0"
git cat-file -p v1.0.0
# object a1b2c3d4... (commit)
# type commit
# tagger Lucas <lucas@email.com>
# Explorar o object store:
git count-objects -vH # Contagem e tamanho dos objetos
git fsck --full # Verificar integridade do repositório
O Grafo Acíclico Direcionado (DAG)
# Todo commit aponta para seu(s) parent(s), formando um DAG:
# Histórico linear:
# A ← B ← C ← D (HEAD/main)
# Merge commit (dois parents):
# A ← B ← C ← F (merge commit, parents: C e E)
# ↑ ↑
# └─ D ← E (feature)
# Isso é crucial porque:
# 1. Commits são IMUTÁVEIS — rebase "reescreve" criando NOVOS objetos
# 2. Branches são apenas PONTEIROS (refs) para commits — mover branch é O(1)
# 3. O garbage collector remove objetos não referenciados
Refs: Branches, HEAD e Tags
# Branches são arquivos simples contendo um hash SHA-1:
cat .git/refs/heads/main
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# HEAD é uma referência simbólica (aponta para uma branch):
cat .git/HEAD
# ref: refs/heads/main
# Detached HEAD — HEAD aponta diretamente para um commit:
git checkout a1b2c3d
cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# Tags lightweight vs annotated:
git tag v1.0.0-rc1 # Lightweight: apenas um ponteiro
git tag -a v1.0.0 -m "Release" # Annotated: objeto tag completo
# Packed refs — otimização para repositórios com muitas refs:
git pack-refs --all
cat .git/packed-refs
# a1b2c3d4... refs/tags/v1.0.0
# e5f6a7b8... refs/heads/feature/auth
# Remote tracking branches:
git branch -vv # Mostra a relação local ↔ remote
# * main a1b2c3d [origin/main] feat: auth
# develop e5f6a7b [origin/develop: ahead 2] fix: login
Estratégias de Merge
# 1. FAST-FORWARD — quando não há divergência (linear)
# main: A ← B ← C
# feat: C ← D ← E
# Resultado: main simplesmente avança o ponteiro para E
git merge --ff-only feature-branch
# Falha se não for possível fast-forward
# 2. THREE-WAY MERGE — quando há divergência
# Usa o merge base (ancestral comum) + as duas pontas
# Base: B Ours: C Theirs: E
# A ← B ← C ← M (merge commit)
# ↑ ↑
# └─ D ← E
git merge --no-ff feature-branch
# Sempre cria merge commit (mesmo se fast-forward fosse possível)
# 3. OCTOPUS MERGE — merge de múltiplas branches simultaneamente
# Útil para integrar várias features de uma vez
git merge feature-a feature-b feature-c
# Não funciona se houver conflitos — é para merges triviais
# 4. RECURSIVE (padrão) vs ORT (novo padrão a partir do Git 2.34)
# ORT (Ostensibly Recursive's Twin) é significativamente mais rápido
# em repositórios grandes com muitos renames
git merge -s ort feature-branch
git merge -s recursive -X patience feature-branch # Opção patience diff
# Estratégias de resolução de conflitos:
git merge -X ours feature-branch # Em conflito, prefere nosso lado
git merge -X theirs feature-branch # Em conflito, prefere o lado deles
git merge -s ours feature-branch # Ignora TODAS as mudanças da outra branch
# Rerere — memorizar resoluções de conflitos para reutilizar:
git config rerere.enabled true
# Quando resolver um conflito, o Git memoriza a resolução
# Se o mesmo conflito aparecer novamente (ex: durante rebase), resolve automaticamente
Rebase: Reescrevendo o Histórico
# Rebase padrão — reaplica commits no topo de outra branch:
git checkout feature
git rebase main
# Antes: A ← B ← C (main)
# ↑
# └─ D ← E (feature)
# Depois: A ← B ← C (main) ← D' ← E' (feature)
# D' e E' são NOVOS commits (novos hashes SHA-1)
# REBASE INTERATIVO — a ferramenta mais poderosa do Git:
git rebase -i HEAD~5
# Abre editor com:
# pick a1b2c3d feat: adicionar modelo User
# pick e5f6a7b fix: corrigir validação de email
# pick c9d0e1f wip: debug
# pick f2a3b4c feat: adicionar endpoint de login
# pick 1234567 fix: typo no controller
# Comandos disponíveis:
# pick (p) = manter o commit como está
# reword (r) = manter mudanças, editar mensagem
# edit (e) = parar para editar o commit (amend, split)
# squash (s) = juntar com o commit anterior (mantém ambas as mensagens)
# fixup (f) = juntar com o anterior (descarta a mensagem deste)
# drop (d) = remover o commit completamente
# exec (x) = executar um comando shell entre commits
# break (b) = parar aqui (continuar com git rebase --continue)
# Reordenar commits: simplesmente mude a ordem das linhas
# Resultado limpo:
# pick a1b2c3d feat: adicionar modelo User
# fixup e5f6a7b fix: corrigir validação de email
# drop c9d0e1f wip: debug
# pick f2a3b4c feat: adicionar endpoint de login
# fixup 1234567 fix: typo no controller
# REBASE --ONTO — reaplicar commits em um ponto arbitrário:
# Cenário: você criou sub-feature a partir de feature, mas feature mudou
git rebase --onto main feature sub-feature
# Remove os commits de feature e aplica sub-feature direto na main
# Autosquash — fixup automático:
git commit --fixup=a1b2c3d # Cria commit com prefixo "fixup! ..."
git rebase -i --autosquash HEAD~5
# O Git automaticamente reordena e marca como fixup
# Executar testes durante rebase:
git rebase -i HEAD~10 --exec "npm test"
# Roda npm test após cada commit — garante que nenhum commit quebra os testes
Cherry-pick e Reflog
# CHERRY-PICK — aplicar um commit específico na branch atual:
git cherry-pick abc1234
# Cria um NOVO commit com as mesmas mudanças (novo hash)
# Cherry-pick de um range:
git cherry-pick A..B # Aplica commits depois de A até B (exclusive A)
git cherry-pick A^..B # Aplica commits de A até B (inclusive A)
# Cherry-pick sem commitar (para combinar múltiplos):
git cherry-pick --no-commit abc1234 def5678
git commit -m "feat: combinar funcionalidades X e Y"
# REFLOG — o "undo" universal do Git:
# Registra TODA movimentação de HEAD (mesmo rebases e resets)
git reflog
# a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/feature
# e5f6a7b HEAD@{1}: rebase (pick): feat: adicionar auth
# c9d0e1f HEAD@{2}: rebase (start): checkout main
# f2a3b4c HEAD@{3}: commit: wip: debug
# Recuperar estado anterior ao rebase que deu errado:
git reset --hard HEAD@{3}
# Volta para exatamente como estava antes do rebase
# Reflog com timestamps:
git reflog --date=iso
# Entradas expiram após 90 dias (30 para não referenciados)
git config gc.reflogExpire 180.days
git config gc.reflogExpireUnreachable 90.days
# Recuperar branch deletada:
git branch -D feature-importante # Ops!
git reflog | grep feature-importante
git checkout -b feature-importante HEAD@{5}
Git Bisect: Busca Binária de Bugs
# Encontrar o commit que introduziu um bug em O(log n) passos:
git bisect start
git bisect bad # Commit atual tem o bug
git bisect good v1.0.0 # Essa tag não tinha o bug
# O Git faz checkout no commit do meio. Teste e informe:
git bisect good # Se este commit está OK
git bisect bad # Se este commit tem o bug
# Repita até encontrar o commit exato
# BISECT AUTOMATIZADO — executa um script em cada commit:
git bisect start HEAD v1.0.0
git bisect run npm test
# O Git executa "npm test" em cada ponto da busca binária
# Exit code 0 = good, qualquer outro = bad
# Bisect com script personalizado:
git bisect run sh -c 'npm run build && npm test -- --grep "login"'
# Pular commits que não compilam:
git bisect skip
# Ver o log do bisect:
git bisect log # Histórico de boas/más decisões
git bisect reset # Voltar ao estado original
Worktrees: Múltiplas Working Directories
# Worktrees permitem ter múltiplas branches checked out simultaneamente,
# sem precisar de stash ou clone adicional:
# Criar worktree para review de PR sem perder o trabalho atual:
git worktree add ../review-pr-42 origin/pr-42
# Criar worktree para hotfix urgente:
git worktree add ../hotfix-auth main
cd ../hotfix-auth
# Trabalhar no hotfix enquanto a branch original continua intacta
# Listar worktrees:
git worktree list
# /home/dev/projeto a1b2c3d [feature/auth]
# /home/dev/review-pr-42 e5f6a7b [origin/pr-42]
# /home/dev/hotfix-auth c9d0e1f [main]
# Remover worktree:
git worktree remove ../review-pr-42
# Caso de uso real: rodar testes na main enquanto desenvolve na feature
# Cada worktree compartilha o mesmo .git (economiza espaço)
Git Hooks
# Hooks são scripts executados automaticamente em eventos do Git.
# Armazenados em .git/hooks/ (local) ou configurados via core.hooksPath.
# Configurar hooks compartilhados pelo time:
git config core.hooksPath .githooks
# Agora todos os hooks ficam versionados no repositório
# PRE-COMMIT — executar antes de cada commit:
# .githooks/pre-commit
#!/bin/sh
set -e
npm run lint-staged # Lint apenas nos arquivos staged
npm run type-check # Verificação de tipos
# COMMIT-MSG — validar formato da mensagem:
# .githooks/commit-msg
#!/bin/sh
commit_msg=$(cat "$1")
pattern="^(feat|fix|refactor|docs|test|chore|perf|ci|build|style)(\(.+\))?: .{1,72}"
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "ERRO: Mensagem não segue Conventional Commits"
echo "Formato: tipo(escopo): descrição"
exit 1
fi
# PRE-PUSH — executar antes de push:
# .githooks/pre-push
#!/bin/sh
set -e
npm test # Testes devem passar antes de push
npm run build # Build deve funcionar
# Ferramentas que gerenciam hooks: husky, lefthook, pre-commit
# Exemplo com lefthook (mais rápido que husky):
# lefthook.yml
# pre-commit:
# parallel: true
# commands:
# lint:
# glob: "*.{ts,tsx}"
# run: npx eslint {staged_files}
# types:
# run: npx tsc --noEmit
Submódulos vs Subtrees
# SUBMODULES — referência a um commit específico de outro repositório:
git submodule add https://github.com/org/shared-lib.git libs/shared
# Cria .gitmodules com a configuração
# O parent repo armazena apenas o hash do commit referenciado
git submodule update --init --recursive # Clonar com submódulos
git submodule update --remote # Atualizar para último commit
# Problemas com submodules:
# - Clone precisa de --recurse-submodules
# - Checkout de branch pode deixar submódulo em detached HEAD
# - Contribuidores esquecem de atualizar
# SUBTREES — copiar o código diretamente no repositório:
git subtree add --prefix=libs/shared https://github.com/org/shared-lib.git main --squash
git subtree pull --prefix=libs/shared https://github.com/org/shared-lib.git main --squash
git subtree push --prefix=libs/shared https://github.com/org/shared-lib.git main
# Subtree vs Submodule:
# Subtree: código fica no repo (mais simples, clone normal funciona)
# Submodule: referência externa (mais limpo, mas mais complexo)
# Recomendação: subtree para bibliotecas compartilhadas pequenas,
# submodule para repositórios grandes independentes
.gitattributes e Gerenciamento de Arquivos Grandes
# .gitattributes — controlar como o Git trata arquivos:
# Normalização de line endings (evitar diffs falsos):
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
*.png binary
*.jpg binary
# Diff personalizado para lock files:
package-lock.json -diff
yarn.lock -diff
# Merge strategy para arquivos específicos:
database/schema.sql merge=ours
# GIT LFS — Large File Storage para arquivos grandes:
git lfs install
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "assets/videos/**"
# Isso cria/atualiza .gitattributes:
# *.psd filter=lfs diff=lfs merge=lfs -text
# Verificar arquivos rastreados pelo LFS:
git lfs ls-files
git lfs status
# Migrar arquivos existentes para LFS:
git lfs migrate import --include="*.psd" --everything
# Reescreve todo o histórico — requer force push
Manutenção do Repositório: gc, prune e fsck
# GIT GC (garbage collection) — compactar e limpar o repositório:
git gc # Execução padrão
git gc --aggressive # Mais thorough (mais lento)
git gc --auto # Só executa se necessário
# O que gc faz:
# 1. Compacta objetos loose em packfiles
# 2. Remove objetos não referenciados (após período de expiração)
# 3. Compacta refs em packed-refs
# 4. Remove reflogs expirados
# PRUNE — remover objetos não referenciados:
git prune --dry-run # Ver o que seria removido
git prune # Remover efetivamente
# FSCK — verificar integridade do repositório:
git fsck --full --no-dangling
# Detecta objetos corrompidos, referências quebradas
# Verificar tamanho do repositório e objetos grandes:
git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
sort -k3 -n -r | head -20
# Lista os 20 maiores objetos no repositório
# Reescrever histórico para remover arquivo grande acidentalmente commitado:
git filter-repo --path secrets.env --invert-paths
# Requer git-filter-repo (substituto moderno do filter-branch)
# CUIDADO: reescreve todo o histórico — requer force push coordenado
Estratégias de Branching para Times
TRUNK-BASED DEVELOPMENT (recomendado para times experientes):
main ────────●────●────●────●────●────
\ / \ /
feature-A feature-B (vida curta: 1-2 dias máximo)
Vantagens:
- Integração contínua real (CI funciona de verdade)
- Menos conflitos de merge
- Deploy contínuo facilitado
Requisitos:
- Feature flags para funcionalidades incompletas
- Cobertura de testes sólida
- CI rápido (< 10 minutos)
- Code review ágil
GITHUB FLOW (simples e eficaz):
main ─────────────●───────────●───────
/ \ / \
feature-A PR feature-B PR
Uma branch main, feature branches, PRs para merge.
Ideal para deploy contínuo, SaaS.
GIT FLOW (releases estruturadas):
main ─────────────────●───────────●───
develop ──●──●──●──●──┘ │
\ / │
feature hotfix → main + develop
Vantagem: releases controladas com versionamento.
Desvantagem: branches de vida longa → merge hell.
Ideal para software com releases formais (mobile, libraries).
Conventional Commits e Automação
# Formato: tipo(escopo): descrição
# Exemplos:
# feat(auth): adicionar autenticação via OAuth2
# fix(api): corrigir N+1 query na listagem de pedidos
# refactor(core): extrair lógica de pagamento para service
# perf(cache): adicionar cache Redis ao catálogo de produtos
# docs(api): atualizar documentação da API v2
# test(auth): adicionar testes de integração para fluxo de login
# chore(deps): atualizar dependências
# ci(actions): adicionar job de security scanning
# build(docker): otimizar multi-stage build
# BREAKING CHANGE no footer:
# feat(api)!: remover endpoint v1 de autenticação
#
# BREAKING CHANGE: O endpoint /api/v1/auth foi removido.
# Migre para /api/v2/auth conforme documentação.
# Ferramentas de automação:
# - commitlint: valida formato da mensagem
# - semantic-release: versionamento automático baseado nos commits
# - conventional-changelog: gera CHANGELOG automaticamente
# .commitlintrc.json
# {
# "extends": ["@commitlint/config-conventional"],
# "rules": {
# "type-enum": [2, "always", ["feat", "fix", "refactor", "perf",
# "docs", "test", "chore", "ci", "build"]],
# "subject-max-length": [2, "always", 72]
# }
# }
# semantic-release analisa os commits desde a última release:
# fix → patch (1.0.0 → 1.0.1)
# feat → minor (1.0.0 → 1.1.0)
# BREAKING CHANGE → major (1.0.0 → 2.0.0)
Dicas Avançadas para o Dia a Dia
# Buscar uma string em todo o histórico do Git:
git log -S "senha_secreta" --all --oneline
# Encontra commits que adicionaram ou removeram essa string
# Blame com detecção de código movido entre arquivos:
git blame -C -C -C arquivo.ts
# -C detecta código copiado/movido de outros arquivos
# Diff com detecção de renames:
git diff --find-renames --find-copies HEAD~5
# Log visual do grafo:
git log --graph --oneline --all --decorate
# Aliases úteis no ~/.gitconfig:
# [alias]
# lg = log --graph --oneline --all --decorate
# st = status -sb
# unstage = reset HEAD --
# last = log -1 HEAD --stat
# amend = commit --amend --no-edit
# wip = !git add -A && git commit -m "wip: trabalho em progresso"
# Sparse checkout — clonar apenas parte do repositório:
git clone --filter=blob:none --sparse https://github.com/org/monorepo.git
cd monorepo
git sparse-checkout set packages/meu-servico
# Partial clone — clonar sem blobs (download sob demanda):
git clone --filter=blob:none https://github.com/org/large-repo.git
# Blobs são baixados apenas quando necessário (git checkout, git diff)
Referências
- “Pro Git” (Scott Chacon & Ben Straub) — o livro definitivo sobre Git, disponível gratuitamente em git-scm.com/book
- Git Reference Documentation — documentação oficial de todos os comandos em git-scm.com/docs