Estrutura do Repositório Git: Um Guia Técnico Completo
Git é um sistema de controlo de versões distribuído que armazena o histórico do projeto como um grafo acíclico dirigido (DAG) de objetos de instantâneos imutáveis. Cada repositório Git é construído a partir de três zonas lógicas — o diretório de trabalho, o índice de staging e o armazenamento de objetos dentro de .git/ — mais um conjunto de ponteiros leves (branches, tags, remotos) que navegam nesse histórico. Compreender como estas camadas interagem é a diferença entre usar o Git mecanicamente e usá-lo com precisão cirúrgica.
Se aloja os seus repositórios num VPS, dominar esta estrutura interna permite-lhe recuperar de desastres, conceber pipelines CI/CD eficientes e auditar cada byte do histórico do seu projeto sem depender de uma plataforma de terceiros.
O Modelo das Três Zonas: Como o Git Move Dados
Antes de mergulhar nos componentes individuais, interiorize o modelo de fluxo de dados que governa cada operação Git:
Working Directory --> Staging Area (Index) --> .git/ Object Store
(edit) (git add) (git commit)As alterações viajam da esquerda para a direita quando constrói um commit, e da direita para a esquerda quando restaura ou repõe. Cada comando Git é essencialmente uma operação de leitura ou escrita em uma ou mais destas zonas.
Diretório de Trabalho
O diretório de trabalho (também chamado de árvore de trabalho) é a vista do sistema de ficheiros do seu projeto num estado de checkout específico. Quando executa git clone ou git checkout, o Git reconstrói ficheiros a partir de objetos comprimidos em .git/objects/ e escreve-os neste diretório.
Os ficheiros no diretório de trabalho existem num de quatro estados:
- Não rastreado — o Git nunca viu este ficheiro; existe apenas no disco.
- Rastreado, não modificado — o ficheiro corresponde exatamente ao último instantâneo confirmado.
- Rastreado, modificado — o ficheiro difere do último instantâneo confirmado mas não foi colocado em staging.
- Rastreado, eliminado — o ficheiro foi removido do disco mas a eliminação não foi colocada em staging.
Uma nuance crítica que confunde muitos programadores: o diretório de trabalho não é uma simples cópia do repositório. O Git reconstrói-o lendo objetos de árvore e descomprimindo objetos blob. Se .git/ estiver intacto, pode sempre regenerar o diretório de trabalho do zero — o inverso não é verdade.
Sparse Checkout para Grandes Monorepos
Em repositórios com dezenas de milhares de ficheiros (comum em arquiteturas monorepo), pode limitar quais os caminhos que o Git materializa no diretório de trabalho:
git sparse-checkout init --cone
git sparse-checkout set services/api services/authIsto é inestimável num VPS com I/O de disco limitado, porque o Git ignora a descompressão de blobs para caminhos fora do cone.
Área de Staging (Índice)
A área de staging, internamente chamada de índice, é um ficheiro binário localizado em .git/index. Atua como um próximo commit proposto — um instantâneo mutável que se situa entre o seu diretório de trabalho e o armazenamento de objetos permanente.
git add <file> # Stage a specific file
git add -p # Interactively stage hunks within a file
git add -u # Stage all tracked modifications and deletions
git status # Compare working directory and index against HEAD
git diff --cached # Show diff between index and HEADPor Que Existe o Índice
O índice resolve um problema que ferramentas VCS mais simples ignoram: commits parciais. Pode ter modificado cinco ficheiros mas apenas querer três deles no próximo commit. O índice permite-lhe compor exatamente o instantâneo que pretende registar, independentemente do que o seu editor tem aberto.
Caso extremo — corrupção do índice: Se uma falha do sistema interromper um git add, o ficheiro de índice pode ficar corrompido. Os sintomas incluem git status a bloquear ou a reportar resultados estranhos. Recuperação:
rm .git/index
git resetO Git reconstrói o índice a partir de HEAD sem tocar no seu diretório de trabalho.
O Índice como Registo de Conflitos de Merge
Durante um conflito de merge, o índice armazena três versões de cada ficheiro em conflito simultaneamente (estágios 1, 2 e 3 — base, nossa versão, versão deles). É por isso que git diff --cached não mostra nada útil durante um conflito; precisa de git diff --cc ou de uma ferramenta de merge para inspecionar os três estágios.
O Diretório .git/: Anatomia do Armazenamento de Objetos
O diretório .git/ é o repositório. Tudo o resto — o diretório de trabalho, clones remotos — é derivado dele. Eliminar .git/ transforma um repositório num diretório simples sem histórico.
.git/
├── HEAD
├── config
├── description
├── index
├── COMMIT_EDITMSG
├── hooks/
├── info/
├── logs/
│ ├── HEAD
│ └── refs/
├── objects/
│ ├── info/
│ └── pack/
└── refs/
├── heads/
├── remotes/
└── tags/HEAD
HEAD é um ficheiro de texto simples contendo uma ref simbólica (apontando para um branch) ou um hash SHA-1 bruto (estado HEAD desanexado).
cat .git/HEAD
# ref: refs/heads/main <-- on a branch
# a3f1c9d... <-- detached HEADO HEAD desanexado não é um estado de erro — é intencional quando faz checkout de uma tag ou de um commit específico para inspeção. O perigo está em fazer commits com HEAD desanexado: esses commits são acessíveis apenas via reflog até os anexar a um branch.
git checkout -b rescue-branch # Attach detached commits to a new branchconfig
O ficheiro de configuração do repositório local. Substitui as definições globais (~/.gitconfig) e do sistema (/etc/gitconfig). Entradas comuns:
[core]
repositoryformatversion = 0
filemode = true
bare = false
[remote "origin"]
url = git@github.com:user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/mainNum servidor auto-hospedado, irá frequentemente editar este ficheiro diretamente ao rodar URLs remotos ou ao configurar uploadpack.allowReachableSHA1InWant para clones parciais.
refs/
O diretório refs/ contém ficheiros de texto simples, cada um contendo um único hash SHA-1. São os ponteiros nomeados que tornam o DAG do Git navegável.
| Tipo de Ref | Caminho | Descrição |
|---|---|---|
| Branch local | refs/heads/<name> | Aponta para o commit mais recente de um branch |
| Branch de rastreamento remoto | refs/remotes/<remote>/<name> | Cache local do commit mais recente de um branch remoto |
| Tag leve | refs/tags/<name> | Aponta diretamente para um objeto commit |
| Tag anotada | refs/tags/<name> | Aponta para um objeto tag, que aponta para um commit |
| Stash | refs/stash | Aponta para o commit do stash |
Por desempenho, o Git empacota refs em .git/packed-refs assim que um repositório acumula muitas delas. Verifique sempre ambas as localizações ao criar scripts que trabalhem com refs.
Objetos Git: O Núcleo Imutável
Tudo armazenado em .git/objects/ é endereçado por conteúdo: o nome do ficheiro é o hash SHA-1 (ou SHA-256 em versões mais recentes do Git) do conteúdo do objeto. Isto torna o Git inerentemente resistente a adulterações — alterar qualquer byte altera o hash, quebrando a cadeia.
Os Quatro Tipos de Objetos
| Tipo de Objeto | O Que Armazena | Aponta Para |
|---|---|---|
| Blob | Conteúdo bruto do ficheiro (sem nome de ficheiro, sem permissões) | Nada |
| Tree | Listagem de diretório: nomes de ficheiros, permissões, SHAs de blob/tree | Blobs e outras trees |
| Commit | Autor, committer, timestamp, mensagem, SHA(s) do(s) pai(s) | Uma tree + zero ou mais commits pai |
| Tag | Identidade do tagger, timestamp, mensagem, assinatura GPG | Normalmente um commit |
Inspecionar Objetos Diretamente
# Show the type of any object
git cat-file -t a3f1c9d
# Show the content of any object
git cat-file -p a3f1c9d
# Show the tree of the current HEAD commit
git ls-tree HEAD
# Show a specific blob's content
git show HEAD:src/main.pyObjetos Soltos vs. Ficheiros Pack
Inicialmente, cada objeto é armazenado como um ficheiro comprimido individual em .git/objects/<2-char-prefix>/<38-char-suffix>. Estes são objetos soltos. Com o tempo, o Git executa git gc (recolha de lixo) para agrupar objetos soltos em ficheiros pack (.git/objects/pack/*.pack) com um índice correspondente (.pack.idx).
Os ficheiros pack utilizam compressão delta — armazenando a diferença entre objetos semelhantes em vez de cópias completas. Um repositório com milhares de ficheiros de texto semelhantes pode encolher dramaticamente após o empacotamento. Num VPS com capacidade NVMe limitada, executar git gc --aggressive em repositórios grandes antes de arquivar é prática padrão.
git count-objects -vH # Show loose object count and disk usage
git gc --aggressive # Repack aggressively (CPU-intensive)
git verify-pack -v .git/objects/pack/*.idx | sort -k3 -n | tail -20
# Find the 20 largest objects in the packHistórico de Commits: O Grafo Acíclico Dirigido
Cada objeto commit contém exatamente um ponteiro para um objeto tree (o instantâneo do diretório raiz) e zero ou mais ponteiros para commits pai. Isto forma um DAG onde:
- Zero pais = o commit inicial (commit raiz)
- Um pai = um commit normal
- Dois pais = um commit de merge
- Três ou mais pais = um merge octopus (raro, usado para integrar muitos branches de funcionalidades simultaneamente)
git log --oneline --graph --all # Visualize the full DAG
git log --format="%H %P" # Show each commit's SHA and parent SHA(s)Imutabilidade de Commits e Reescrita de Histórico
Como o SHA de um commit é derivado do seu conteúdo (incluindo SHAs dos pais), qualquer reescrita cria um novo commit com um novo SHA. Operações como git rebase, git commit --amend e git filter-repo não modificam o histórico — criam histórico paralelo. Os commits antigos permanecem no armazenamento de objetos até serem recolhidos pelo garbage collector.
É por isso que fazer force-push de histórico reescrito para um branch partilhado é destrutivo: os branches locais dos colaboradores ainda apontam para a cadeia de commits antiga.
Branches: Ponteiros Leves
Um branch não é mais do que um ficheiro de 41 bytes contendo um hash SHA-1. Criar um branch é instantâneo independentemente do tamanho do repositório porque o Git apenas escreve um pequeno ficheiro.
git branch feature/auth # Create branch at current HEAD
git checkout -b feature/auth # Create and switch in one step
git switch -c feature/auth # Modern equivalent (Git 2.23+)
git branch -d feature/auth # Delete (safe: refuses if unmerged)
git branch -D feature/auth # Delete (force: regardless of merge status)Internos dos Branches
cat .git/refs/heads/main
# a3f1c9d8e2b1f4c7d9e0a1b2c3d4e5f6a7b8c9d0Quando faz commit num branch, o Git escreve o novo SHA do commit neste ficheiro. Isso é a totalidade de “avançar um ponteiro de branch.”
Branches de Rastreamento e Configuração Upstream
Uma relação de rastreamento diz ao Git qual branch remoto um branch local deve comparar para relatórios de divergência git status e comportamento de git pull.
git branch --set-upstream-to=origin/main main
git branch -vv # Show tracking relationships and ahead/behind countsTags: Marcadores Permanentes no Histórico
As tags marcam commits específicos como significativos — tipicamente lançamentos de software. Ao contrário dos branches, as tags não são movidas por novos commits.
| Funcionalidade | Tag Leve | Tag Anotada |
|---|---|---|
| Armazenamento | Um ficheiro ref apontando para um commit | Um objeto tag no armazenamento de objetos |
| Metadados | Nenhum | Nome do tagger, email, data, mensagem |
| Assinatura GPG | Não possível | Suportado via git tag -s |
| Recomendado para lançamentos | Não | Sim |
Transferência com git push --tags | Sim | Sim |
git tag v2.1.0 # Lightweight tag at HEAD
git tag -a v2.1.0 -m "Release 2.1.0" # Annotated tag
git tag -s v2.1.0 -m "Signed release" # GPG-signed annotated tag
git push origin --tags # Push all tags to remote
git push origin v2.1.0 # Push a specific tagArmadilha crítica: git push não envia tags por padrão. As equipas frequentemente esquecem isto e publicam notas de lançamento referenciando uma tag que não existe no remoto.
Remotos: Colaboração Distribuída
Um remoto é um URL nomeado armazenado em .git/config. Os branches de rastreamento remoto (em refs/remotes/) são instantâneos locais de apenas leitura dos branches do remoto, atualizados apenas quando faz fetch explicitamente.
git remote add origin git@github.com:user/repo.git
git remote -v # List remotes with URLs
git remote set-url origin <new-url> # Change a remote URL
git fetch origin # Update remote-tracking branches
git fetch --prune # Remove stale remote-tracking branches
git push origin main # Push local main to remote
git push -u origin feature/auth # Push and set upstream trackingMúltiplos Remotos
Um único repositório pode rastrear múltiplos remotos — comum quando se mantém um fork juntamente com o upstream:
git remote add upstream git@github.com:original/repo.git
git fetch upstream
git merge upstream/mainQuando auto-hospeda repositórios bare num servidor dedicado para a sua equipa, cada programador adiciona o servidor como remoto e usa autenticação por chave SSH para acesso de push.
Hooks: Aplicação Automatizada em Cada Evento Git
Os hooks são scripts executáveis em .git/hooks/. O Git chama-os em pontos definidos do fluxo de trabalho. Não são transferidos por git clone ou git push — cada programador (ou servidor) deve instalá-los independentemente. Esta é uma fonte frequente de confusão em ambientes de equipa.
Hooks do Lado do Cliente
| Hook | Gatilho | Uso Comum |
|---|---|---|
pre-commit | Antes do prompt de mensagem de commit | Linting, deteção de segredos, execução de testes |
prepare-commit-msg | Após a criação da mensagem padrão | Injetar nome do branch na mensagem |
commit-msg | Após o utilizador escrever a mensagem | Impor formato de commit convencional |
post-commit | Após o commit ser registado | Notificações locais |
pre-push | Antes de git push ser executado | Executar suite de testes completa |
pre-rebase | Antes do rebase iniciar | Impedir rebase de branches publicados |
Hooks do Lado do Servidor
| Hook | Gatilho | Uso Comum |
|---|---|---|
pre-receive | Antes das refs serem atualizadas | Impor proteção de branch, rejeitar force-push |
update | Por ref durante a receção | Aplicação de política por branch |
post-receive | Após todas as refs atualizadas | Acionar CI/CD, enviar notificações |
Exemplo: Hook Pre-commit para Deteção de Segredos
#!/usr/bin/env bash
# .git/hooks/pre-commit
if git diff --cached --name-only | xargs grep -lE '(AKIA|passwords*=|api_keys*=)' 2>/dev/null; then
echo "ERROR: Potential secret detected in staged files. Commit aborted."
exit 1
fi
exit 0Torná-lo executável:
chmod +x .git/hooks/pre-commitPara distribuição de hooks em toda a equipa, use uma ferramenta como Husky (projetos Node.js) ou armazene hooks num diretório hooks/ na raiz do repositório e crie links simbólicos durante a configuração do projeto.
Reflog: A Rede de Segurança
O reflog regista cada movimento de HEAD e dos ponteiros de branch, incluindo operações que parecem destruir o histórico (reposições forçadas, rebases, commits alterados). É armazenado em .git/logs/.
git reflog # Show HEAD movement history
git reflog show main # Show movement history for a specific branch
git checkout HEAD@{3} # Check out the state HEAD was in 3 moves ago
git branch recovered HEAD@{5} # Recover commits by branching from a reflog entryAs entradas do reflog expiram após 90 dias por padrão (gc.reflogExpire). Num servidor de produção, considere estender este prazo:
git config gc.reflogExpire 180
git config gc.reflogExpireUnreachable 30Repositórios Bare: Hospedagem do Lado do Servidor
Um repositório bare não tem diretório de trabalho. Contém apenas o conteúdo de .git/ ao nível da raiz. Os repositórios bare são o formato correto para hospedagem centralizada — aceitam pushes sem as complicações de um branch com checkout.
git init --bare /srv/repos/myproject.gitQuando faz push para o GitHub, GitLab ou um servidor Git auto-hospedado, está a fazer push para um repositório bare. Se aloja o seu próprio servidor Git num VPS com cPanel ou num VPS Linux puro, repositórios bare em /srv/repos/ com acesso SSH são a arquitetura padrão.
Inicializar um Repositório Bare Partilhado
# On the server
git init --bare --shared=group /srv/repos/project.git
chown -R git:developers /srv/repos/project.git
# On a developer's machine
git remote add origin git@yourserver.com:/srv/repos/project.git
git push -u origin mainArmazenamento de Objetos Git: Tamanho, Integridade e Manutenção
Verificar a Saúde do Repositório
git fsck --full # Verify object integrity (finds dangling and corrupt objects)
git fsck --lost-found # Write dangling objects to .git/lost-found/Encontrar e Remover Objetos Grandes
Ficheiros binários grandes acidentalmente confirmados são uma causa comum de repositórios sobredimensionados. Identifique-os antes de usar git filter-repo para os excluir:
# Find the 10 largest objects by compressed size
git verify-pack -v .git/objects/pack/*.idx
| sort -k3 -rn
| head -10
| awk '{print $1}'
| xargs -I{} git cat-file -p {}# Remove a file from all history (requires git-filter-repo)
git filter-repo --path path/to/large-file.bin --invert-pathsApós a filtragem, todos os colaboradores devem re-clonar — os seus repositórios locais referenciam hashes SHA que já não existem no histórico reescrito.
Comparação: Conceitos Chave do Repositório Git
| Conceito | Tipo | Mutável | Armazenado Em | Transferido por Push/Fetch |
|---|---|---|---|---|
| Blob | Objeto | Não | .git/objects/ | Sim (quando acessível) |
| Tree | Objeto | Não | .git/objects/ | Sim (quando acessível) |
| Commit | Objeto | Não | .git/objects/ | Sim (quando acessível) |
| Tag Anotada | Objeto | Não | .git/objects/ | Apenas com --tags |
| Branch | Ref | Sim | .git/refs/heads/ | Sim |
| Branch de rastreamento remoto | Ref | Sim (no fetch) | .git/refs/remotes/ | Não (cache local) |
| Tag Leve | Ref | Não | .git/refs/tags/ | Apenas com --tags |
| HEAD | Symref/hash | Sim | .git/HEAD | Não |
| Índice | Ficheiro binário | Sim | .git/index | Não |
| Hooks | Scripts | Sim | .git/hooks/ | Não |
| Reflog | Log | Sim (expira automaticamente) | .git/logs/ | Não |
Matriz de Decisão Prática e Principais Conclusões
Use esta lista de verificação ao configurar ou auditar um repositório Git na sua infraestrutura:
Inicialização do repositório
- Use
git init --bare --shared=grouppara qualquer repositório que receberá pushes de múltiplos utilizadores. - Armazene repositórios bare fora de diretórios acessíveis pela web (nunca em
/var/www/).
Saúde do armazenamento de objetos
- Execute
git fsck --fullapós qualquer incidente de armazenamento ou erro do sistema de ficheiros. - Agende
git gcperiodicamente em repositórios de longa duração; automatize-o via cron no seu servidor. - Monitorize o tamanho dos ficheiros pack com
git count-objects -vH; investigue se a contagem de objetos soltos exceder 1.000.
Higiene de branches e refs
- Elimine branches mesclados prontamente; refs obsoletas acumulam-se e abrandam as operações
git fetch --prune. - Use
git fetch --pruneem pipelines CI para evitar agir sobre branches remotos eliminados.
Implementação de hooks
- Nunca confie em
.git/hooks/para política em toda a equipa — os hooks não são clonados. Use hookspre-receivedo lado do servidor ou uma barreira CI em vez disso. - Audite os hooks do lado do servidor após cada atualização do servidor Git; os caminhos do interpretador de hooks podem mudar.
Segurança em servidores auto-hospedados
- Restrinja o acesso SSH ao utilizador
gitcom comandos forçados (command=emauthorized_keys). - Use
git-shellcomo shell de login para o utilizadorgitpara impedir a execução de comandos arbitrários. - Associe o seu servidor de repositório a um certificado SSL válido se expuser qualquer interface web (Gitea, GitLab, cgit).
Reescrita de histórico
- Nunca reescreva o histórico em branches partilhados com outros sem um plano de migração coordenado.
- Após
git filter-repo, todos os colaboradores devem re-clonar; atualize imediatamente os URLs remotos do CI/CD.
Recuperação de desastres
- Estenda a expiração do reflog em servidores de produção (
gc.reflogExpire = 180). - Mantenha um clone bare secundário num host separado como backup; um simples
git fetchdo primário é suficiente.
FAQ
Qual é a diferença entre um repositório Git bare e um não-bare?
Um repositório não-bare tem um diretório de trabalho onde os ficheiros são verificados, mais um subdiretório .git/ contendo o armazenamento de objetos. Um repositório bare contém apenas o armazenamento de objetos na sua raiz (sem diretório de trabalho) e é o formato correto para um servidor partilhado que recebe pushes.
Posso recuperar commits após executar git reset --hard?
Sim, desde que os commits não tenham sido recolhidos pelo garbage collector. Execute git reflog para encontrar o SHA do commit que pretende recuperar, depois git checkout -b recovery-branch <SHA> para o anexar a um novo branch. As entradas do reflog são retidas por 90 dias por padrão.
Por que é que git push não transfere as minhas tags?
Por design, git push apenas transfere commits acessíveis a partir das refs que envia explicitamente. As tags são refs separadas e devem ser enviadas com git push origin --tags (todas as tags) ou git push origin <tagname> (uma tag específica).
O que acontece ao índice durante um conflito de merge?
O índice armazena as três versões de cada ficheiro em conflito simultaneamente: estágio 1 (ancestral comum/base), estágio 2 (a sua versão) e estágio 3 (a versão deles). O git add normal apenas escreve o estágio 0 (resolvido). Até todos os conflitos serem resolvidos e colocados em staging, git commit recusará prosseguir.
Como diferem os hooks Git entre implementações do lado do cliente e do lado do servidor?
Os hooks do lado do cliente são executados na máquina do programador e não são aplicados centralmente — qualquer programador pode contorná-los eliminando o ficheiro de hook. Os hooks do lado do servidor (pre-receive, update, post-receive) são executados no servidor de hospedagem e não podem ser contornados pelo cliente, tornando-os o ponto de aplicação correto para políticas de proteção de branch, requisitos de revisão de código e acionadores CI/CD.
