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