Git internals: objects, trees, blobs e commits por dentro
1. Introdução ao modelo de objetos do Git
Diferente de outros sistemas de controle de versão que armazenam diferenças entre arquivos, o Git funciona como um sistema de arquivos imutável e endereçável por conteúdo. Cada objeto no repositório é identificado unicamente pelo hash SHA-1 do seu conteúdo, e uma vez criado, nunca é modificado.
O diretório .git/objects/ armazena todos esses objetos em subdiretórios nomeados pelos dois primeiros caracteres do hash. Por exemplo, um objeto com hash e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 será armazenado em .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391.
Existem quatro tipos fundamentais de objetos: blob, tree, commit e tag (anotada). Cada um tem uma estrutura específica e um propósito claro no ecossistema do Git.
2. Blobs: a unidade básica de armazenamento de arquivos
Um blob (Binary Large Object) armazena exclusivamente o conteúdo de um arquivo — sem nome, sem permissões, sem metadados. Se dois arquivos em locais diferentes tiverem o mesmo conteúdo, eles compartilharão o mesmo blob.
Vamos criar um blob manualmente:
$ echo "Hello, Git internals!" | git hash-object -w --stdin
5e1c309dae7f45e0f8b1f0c0d1c9b0e8c7f6a5b4
O parâmetro -w instrui o Git a escrever o objeto no banco de dados de objetos. O hash retornado é o SHA-1 do conteúdo, calculado sobre a string "blob <tamanho>\0<conteúdo>". O Git comprime o resultado com zlib antes de armazenar.
Podemos verificar o conteúdo armazenado:
$ git cat-file -p 5e1c309dae7f45e0f8b1f0c0d1c9b0e8c7f6a5b4
Hello, Git internals!
3. Trees: representando diretórios e hierarquias
Enquanto blobs armazenam conteúdo de arquivos, as trees representam diretórios. Uma tree contém uma lista de entradas, cada uma com:
- Modo: permissões do arquivo (100644 para arquivo comum, 100755 para executável, 040000 para subdiretório)
- Tipo: blob ou tree
- Hash: SHA-1 do objeto referenciado
- Nome: nome do arquivo ou diretório
Vamos criar uma tree manualmente. Primeiro, criamos dois blobs:
$ echo "Arquivo A" | git hash-object -w --stdin
aaa1112223334445556667778889990001112223
$ echo "Arquivo B" | git hash-object -w --stdin
bbb2223334445556667778889990001112223334
Agora, construímos uma tree usando git mktree:
$ printf "100644 blob aaa1112223334445556667778889990001112223\tarquivo_a.txt\n" | git mktree
ccc3334445556667778889990001112223334445556
Para adicionar um subdiretório, primeiro criamos uma tree para ele:
$ printf "100644 blob bbb2223334445556667778889990001112223334\tarquivo_b.txt\n" | git mktree
ddd4445556667778889990001112223334445556667
Depois, criamos a tree raiz incluindo o subdiretório:
$ printf "100644 blob aaa1112223334445556667778889990001112223\tarquivo_a.txt\n040000 tree ddd4445556667778889990001112223334445556667\tsubdir\n" | git mktree
eee5556667778889990001112223334445556667778
Podemos inspecionar a tree com:
$ git ls-tree eee5556667778889990001112223334445556667778
100644 blob aaa1112223334445556667778889990001112223 arquivo_a.txt
040000 tree ddd4445556667778889990001112223334445556667 subdir
4. Commits: pontos de referência no histórico
Um commit é um objeto que referencia uma tree raiz (snapshot completo do projeto) e contém metadados como autor, committer, mensagem e commits pais. É o commit que dá significado ao histórico.
Vamos criar um commit manualmente a partir da tree que construímos:
$ git commit-tree eee5556667778889990001112223334445556667778 -m "Primeiro commit manual"
fff6667778889990001112223334445556667778889
Para criar um commit com pai (segundo commit na história):
$ echo "Segundo commit" | git commit-tree eee5556667778889990001112223334445556667778 -p fff6667778889990001112223334445556667778889
ggg7778889990001112223334445556667778889990
Inspecionando o commit:
$ git cat-file -p fff6667778889990001112223334445556667778889
tree eee5556667778889990001112223334445556667778
author Seu Nome <email@exemplo.com> 1700000000 -0300
committer Seu Nome <email@exemplo.com> 1700000000 -0300
Primeiro commit manual
Cada commit aponta para uma tree que representa o estado completo do repositório naquele momento. Isso permite que o Git reconstrua qualquer versão instantaneamente.
5. Tags anotadas e objetos especiais
Tags leves (lightweight) são apenas referências a um commit, armazenadas em .git/refs/tags/. Já as tags anotadas são objetos completos no banco de dados.
Criando uma tag anotada:
$ git tag -a v1.0 -m "Versão 1.0" fff6667778889990001112223334445556667778889
O objeto tag criado pode ser inspecionado:
$ git cat-file -p refs/tags/v1.0
object fff6667778889990001112223334445556667778889
type commit
tag v1.0
tagger Seu Nome <email@exemplo.com> 1700000000 -0300
Versão 1.0
6. Navegando pelos objetos com ferramentas de baixo nível
O Git oferece ferramentas poderosas para explorar o grafo de objetos. Vamos rastrear um commit até o conteúdo de um arquivo:
$ git rev-parse HEAD
fff6667778889990001112223334445556667778889
$ git cat-file -p HEAD | head -1
tree eee5556667778889990001112223334445556667778
$ git ls-tree eee5556667778889990001112223334445556667778
100644 blob aaa1112223334445556667778889990001112223 arquivo_a.txt
040000 tree ddd4445556667778889990001112223334445556667 subdir
$ git cat-file -p aaa1112223334445556667778889990001112223
Arquivo A
Com git show, podemos ver o diff de um commit:
$ git show --stat fff6667778889990001112223334445556667778889
7. Ciclo de vida: como objetos são criados e referenciados
Quando executamos git add, o Git:
1. Cria blobs para cada arquivo modificado
2. Atualiza o index (staging area) com as novas entradas
Quando executamos git commit:
1. O Git cria uma tree a partir do conteúdo do index
2. Cria um objeto commit apontando para essa tree
3. Atualiza a referência do branch atual (ex: refs/heads/main)
Objetos que não são referenciados por nenhum commit, branch ou tag são chamados de dangling objects. Eles podem ser recuperados por até 30 dias antes do garbage collection:
$ git fsck --lost-found
dangling blob aaa1112223334445556667778889990001112223
8. Considerações sobre desempenho e integridade
A imutabilidade dos objetos garante integridade total: qualquer alteração no conteúdo de um arquivo produz um hash diferente, e qualquer tentativa de modificar um objeto existente invalidaria todas as referências a ele.
Para otimizar armazenamento, o Git utiliza packfiles, que comprimem múltiplos objetos juntos e armazenam deltas entre versões similares. Isso reduz drasticamente o espaço em disco para repositórios grandes.
Quanto às limitações, o Git está em transição do SHA-1 para SHA-256, oferecendo git init --object-format=sha256 para novos repositórios que exigem maior resistência a colisões.
Referências
- Pro Git Book - Chapter 10: Git Internals — Capítulo completo do livro oficial sobre os fundamentos internos do Git, incluindo objetos, trees e commits.
- Git Objects Documentation — Documentação oficial sobre a estrutura de objetos no Git.
- Git Internals - Objects, Blobs, Trees and Commits (Atlassian) — Tutorial prático da Atlassian explicando o modelo de objetos do Git.
- Understanding Git: Objects and References (GitHub Blog) — Artigo técnico do GitHub sobre objetos e referências no Git.
- Git from the Inside Out (Mary Rose Cook) — Explicação visual e interativa dos conceitos internos do Git, com diagramas e exemplos práticos.