Repensando o Monorepo Git: Uma Arquitetura Nativa de CAS para Humanos e Agentes de Programação

Toda organização de engenharia em crescimento acaba escolhendo entre muitos repositórios e um só. Começar com dezenas de pequenos repositórios faz a vida parecer simples no início. Cada biblioteca tem seu próprio rastreador de issues, seu próprio pipeline de CI e um git clone que termina em segundos. Depois, você altera uma struct em um pacote central de rede e passa o resto da tarde abrindo pull requests de atualização de versão em seis repositórios dependentes, esperando as filas de CI serem liberadas em ordem topológica.
Coloque tudo em um monorepo Git tradicional e você troca a coordenação entre repositórios pelo tamanho do repositório. À medida que o histórico de commits se aprofunda e ativos binários se acumulam em revisões passadas, o git fetch fica mais lento. Os working trees chegam a gigabytes. Logo alguém sugere implantar um daemon de sistema de arquivos virtual personalizado, um servidor dedicado de revisão de código ou uma frota de workers de build remotos apenas para tornar o desenvolvimento diário suportável.
Quando agentes de codificação autônomos entram na equipe, ambos os modos de falha se agravam. Os polirepositórios cegam o agente para os chamadores em outros projetos e o obrigam a gerenciar credenciais em vários repositórios. Os monorepos tradicionais sobrecarregam o disco do contêiner do agente e a largura de banda da rede a cada inicialização de tarefa, enquanto agentes simultâneos pisando na mesma branch geram corridas de rebase intermináveis.
Inspirados na arquitetura google3, construímos mudanças atômicas em trunk único, onboarding esparso em menos de um segundo, governança hierárquica de OWNERS e espelhos open-source independentes, usando a infraestrutura padrão do Git e armazenamento de objetos de prateleira.
O projeto não necessita de um daemon de sistema de arquivos personalizado nem de um appliance de revisão de terceiros, pois trata o Git como um Content-Addressable Store (CAS) baseado em árvore de Merkle.
O Equívoco no Centro dos Monorepos Git Lentos
Quando desenvolvedores reclamam que o Git não escala para monorepos grandes, raramente estão atingindo um limite no banco de objetos do Git. Dentro de .git/objects, o Git armazena três primitivas imutáveis, indexadas pelo hash de conteúdo SHA-1 (ou SHA-256):
blob: O conteúdo bruto em bytes de um único arquivo.tree: Uma listagem de diretório que mapeia nomes de arquivos e permissões para hashes deblobou detreefilhos.commit: Um pequeno cabeçalho de texto (normalmente de 200 a 300 bytes) que aponta para um único hash detreeraiz, para zero ou mais hashes decommitpais e para metadados do autor.
Se um monorepo contém 50.000 arquivos e você edita uma única linha em um arquivo, o novo commit reutiliza exatamente os hashes de blob e tree dos outros 49.999 arquivos.
Um monorepo Git tradicional parece pesado porque a convenção sobrecarrega uma única referência, refs/heads/main, com três funções conflitantes:
- Ele contém a árvore de diretórios unificada inteira de todos os projetos.
- Ele arrasta consigo a cadeia inteira de commits históricos ligados por pais, até o primeiro dia.
- Ele fica dentro de
refs/heads/*, o namespace que os servidores Git anunciam e negociam por padrão em cadagit cloneegit fetch.
Toda vez que um desenvolvedor ou um runner de CI busca refs/heads/main, o cliente e o servidor negociam a alcançabilidade no grafo de commits entre milhares de revisões históricas. Se alguém commitou acidentalmente um arquivo de 100 MB três anos atrás e o removeu no dia seguinte, seu blob continua pendurado em uma tree histórica alcançável a partir de refs/heads/main.
Separando o Namespace de Referências
O protocolo de transporte do Git só anuncia refs/heads/* e refs/tags/* durante um clone padrão. Referências em namespaces personalizados, como refs/trunk/* ou refs/ws/*, vivem no mesmo armazenamento de objetos no servidor, se beneficiam da mesma compressão por deltas em packfiles e podem ser buscadas ou enviadas explicitamente, mas não impõem nenhuma sobrecarga aos clones padrão.
Separar essas funções em referências distintas dentro de um único repositório elimina o gargalo:
1refs/heads/main Apenas plano de controle (~20 KB: regras, .gitconfig, config de sincronização, scripts)
2refs/trunk/main Snapshot de commit sem pais apontando para a árvore raiz unificada mais recente
3refs/trunk/history Cadeia linear de commits ligados por pais, compartilhando o SHA-1 exato da árvore raiz
4refs/ws/<actor>/<id> Snapshots de workspace isolados (changelists) por desenvolvedor ou agente
5refs/ws/cl/<change-id>/<tip> Patchsets imutáveis: cada tip de uma mudança enviada para revisão
6refs/heads/land/<id> Gatilho efêmero da fila de merge, excluído imediatamente após o landing
1. O Plano de Controle (refs/heads/main)
A branch padrão não contém código-fonte de aplicações. Ela registra apenas as regras operacionais do repositório (AGENTS.md, README.md), um .gitconfig compartilhado com aliases de fluxo de trabalho, a configuração de sincronização em Starlark (sync.star) e os scripts de merge do lado do servidor. Clonar a branch padrão transfere cerca de 20 KB e termina em milissegundos.
2. O Snapshot Trunk sem Pais (refs/trunk/main)
refs/trunk/main aponta para a árvore-fonte unificada completa de toda a organização (//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), mas seu objeto de commit é criado sem a flag -p de pai.
Como refs/trunk/main não tem pais (parents = []), buscá-la não exige nenhuma negociação no grafo de commits. O servidor Git não percorre o histórico; ele envia o único commit de topo e os objetos tree de diretório atuais. Binários históricos ou arquivos excluídos no mês passado não são alcançáveis a partir de refs/trunk/main e nunca são transferidos.
3. O Histórico Linear de Auditoria (refs/trunk/history)
Os desenvolvedores ainda precisam de git blame e git log ao investigar regressões. Toda vez que uma mudança é integrada, o script de merge cria dois objetos de commit apontando para o mesmo hash SHA-1 exato da árvore raiz $NEW_TREE:
1# Commit snapshot sem pais para checkouts instantâneos de workspace com profundidade 1
2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
3
4# Commit ligado ao pai para git log linear e git blame
5HIST=$(git commit-tree "$NEW_TREE" -p "$OLD_HIST" -m "$RAW_MSG")
6
7git push --atomic origin \
8 --force-with-lease="refs/trunk/main:${OLD_MAIN}" \
9 --force-with-lease="refs/trunk/history:${OLD_HIST}" \
10 "${SNAP}:refs/trunk/main" \
11 "${HIST}:refs/trunk/history"
Manter tanto refs/trunk/main quanto refs/trunk/history não duplica a ocupação de armazenamento. Como ambos os commits referenciam o mesmo hash $NEW_TREE, o content-addressable store do Git deduplica 100% dos objetos tree e blob. O custo extra de armazenamento do snapshot sem pais é apenas um cabeçalho de commit de 250 bytes.
4. Workspaces Isolados (refs/ws/<actor>/<id>)
Branches de trabalho em andamento são enviadas para refs/ws/<actor>/<id> em vez de refs/heads/*. Cem agentes de codificação podem salvar checkpoints a cada minuto sem poluir os menus de branches nem desacelerar o git fetch dos engenheiros humanos.
Onboarding em Quatro Etapas: Como Funciona o Checkout Esparso e Sem Blobs
Com este layout de referências, um novo desenvolvedor ou um contêiner efêmero de agente de codificação não instala uma CLI personalizada nem um driver FUSE. O onboarding leva quatro etapas com Git padrão e os aliases do repositório:
1# 1. Clone o plano de controle de 20 KB com filtro de blobs e modo esparso habilitados
2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
3cd monorepo
4
5# 2. Carregue os aliases do repositório e anexe refs/trunk/*
6git config include.path ../.gitconfig
7git trunk-init
8
9# 3. Declare os diretórios de domínio necessários para esta tarefa
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Crie um workspace isolado a partir do snapshot trunk mais recente
13git ws-new alice/optimize-framing
Cada etapa busca apenas o que precisa:
git clone --filter=blob:none --sparsebaixa o plano de controle de 20 KB emrefs/heads/main.git trunk-initconfigura+refs/trunk/*:refs/remotes/origin/trunk/*e executa um fetch sem blobs. O Git baixa os metadados detreederefs/trunk/main(normalmente algumas dezenas de quilobytes) e zero blobs de arquivos.git sparse-checkout set net/relay ipc/codecinstrui o filtro de índice em modo cone do Git a materializar apenas o diretório raiz e essas duas subárvores.git ws-newmuda para uma nova branch baseada emorigin/trunk/main. Em seguida, o Git solicita sob demanda apenas os objetosblobdentro denet/relayeipc/codec. Os 95% restantes do monorepo nunca são baixados para o disco.
Horizonte Futuro: Montando refs/trunk/main como um Sistema de Arquivos Virtual (VFS)
O checkout esparso com clones sem blobs cobre os fluxos de trabalho diários sem ferramentas adicionais no cliente. Como refs/trunk/main já é uma árvore de Merkle pura em um Content-Addressable Store, a arquitetura também oferece uma base direta para um Sistema de Arquivos Virtual (VFS) opcional no espaço do usuário, via FUSE no Linux, FileProvider no macOS ou ProjFS no Windows:
- Montagem Instantânea da Árvore Completa: Um daemon local leve de montagem lê o objeto
treeraiz derefs/trunk/maine projeta toda a hierarquia de diretórios do monorepo no diretório de trabalho em milissegundos, eliminando a necessidade de executargit sparse-checkout set. - Carregamento de Blobs sob Demanda: O conteúdo dos arquivos permanece virtual até que um compilador, uma IDE ou um agente emita uma chamada de sistema
open(2)ouread(2). O daemon então busca oblobSHA-1 correspondente no endpoint do CAS e o armazena em cache no NVMe local. - Overlay Copy-on-Write para Workspaces: As edições locais são gravadas em uma camada superior leve de overlay. Executar
git ws-savecalcula o hash apenas dos caminhos modificados nesse overlay, grava os objetosblobetreeatualizados e envia o commit resultante pararefs/ws/<actor>/<id>sem examinar diretórios não modificados.
O Modelo de Changelist em Git Puro: Landing em Memória do Lado do Servidor
Em um fluxo de trabalho Git tradicional, o merge de uma branch de funcionalidade cria um commit de merge com dois pais, o que torna o histórico não linear, ou exige que os desenvolvedores façam rebase manualmente e disputem com pushes concorrentes para main.
Inspirado no modelo Changelist (CL) do google3, cada workspace em nossa arquitetura representa uma mudança atômica única em relação ao snapshot trunk. Enquanto trabalha localmente em ws/alice/optimize-framing, um desenvolvedor ou agente pode fazer dez commits incrementais (wip, fix tests, lint). Quando a mudança está pronta para ser integrada, o servidor colapsa o delta do workspace em uma única revisão linear no trunk, sem nunca fazer checkout de um diretório de trabalho no runner de CI.
Eis como o script de landing (land.sh) é executado em memória usando o encanamento (plumbing) do Git 2.40+:
1# 1. Identifique o commit snapshot raiz de onde o workspace foi ramificado
2BASE=$(git rev-list --max-parents=0 "$WORK_SHA")
3
4BASE_TREE=$(git rev-parse "${BASE}^{tree}")
5MAIN_TREE=$(git rev-parse "${OLD_MAIN}^{tree}")
6WORK_TREE=$(git rev-parse "${WORK_SHA}^{tree}")
7
8# 2. Calcule a árvore raiz mesclada em memória, sem checkout de working tree
9if [ "$MAIN_TREE" = "$BASE_TREE" ]; then
10 NEW_TREE="$WORK_TREE"
11elif OUT=$(git merge-tree --write-tree --merge-base="$BASE_TREE" "$MAIN_TREE" "$WORK_TREE"); then
12 NEW_TREE=$(head -n1 <<<"$OUT")
13else
14 echo "Merge conflict detected; run git ws-sync to rebase." >&2
15 exit 1
16fi
Na etapa 2:
- Independentemente de quantos commits locais tenham sido feitos na branch do workspace, o script compara apenas três hashes de árvore: o snapshot base original (
$BASE_TREE), o snapshot trunk atual ($MAIN_TREE) e a árvore final do workspace ($WORK_TREE). - Se
refs/trunk/mainavançou em//web/blogenquanto Alice editava//net/relay,git merge-tree --write-treecalcula a árvore raiz combinada$NEW_TREEdiretamente dentro do armazenamento de objetos do Git, em milissegundos, sem gravar arquivos no sistema de arquivos do runner. - Após a aprovação das verificações de política descritas abaixo, um único
git push --atomiccom--force-with-leaseatualizarefs/trunk/main, acrescenta um commit linear arefs/trunk/historye exclui o gatilho de landingrefs/heads/land/<id>.
Aplicando Limites de Arquitetura: Visibilidade em Quatro Níveis e OWNERS Hierárquico
Consolidar código em um único repositório só funciona se os limites entre domínios forem aplicados mecanicamente antes que um commit possa ser integrado. Logo após o git merge-tree --write-tree produzir $NEW_TREE (e antes que git push --atomic atualize o trunk), nosso motor de políticas executa duas verificações O(delta) sobre git diff-tree "$MAIN_TREE" "$NEW_TREE".
1. Visibilidade de Domínio em Quatro Níveis
A árvore raiz é particionada em quatro níveis de visibilidade:
| Prefixo de Diretório | Nível | Importações Permitidas | Importações Proibidas | Gate de OWNERS |
|---|---|---|---|---|
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/* | Produção Pública | Produção Pública, //third_party/* | //corp/*, //experimental/* | Aplicado |
//third_party/* | Fornecedor Externo | Autocontido ou //third_party/* | Todos os domínios não third_party | Aplicado |
//corp/* | Privado Interno | Produção Pública, //corp/*, //third_party/* | //experimental/* | Aplicado |
//experimental/<actor>/* | Sandbox | Qualquer pacote que não seja corp | //corp/* | Isento (aprovação automática) |
Se um pacote público em //web/* tentar importar um pacote interno de //corp/* ou um protótipo não graduado de //experimental/*, o job de landing é abortado antes de tocar em refs/trunk/main.
2. Verificação Hierárquica de OWNERS Sem um Servidor Gerrit
O verificador aplica arquivos OWNERS em nível de diretório, com regras de herança:
- Para cada caminho modificado entre
$MAIN_TREEe$NEW_TREE, o verificador sobe a partir do diretório do arquivo até//OWNERS. - Qualquer arquivo
OWNERSpode declararset noparentpara bloquear aprovações herdadas de diretórios pais, ouper-file *.sql=@dba-teampara restringir aprovações por padrão glob de nome de arquivo. - Invariante Anti-Adulteração: Os arquivos
OWNERSexistentes são sempre lidos a partir de$MAIN_TREE(a árvore trunk atual), nunca de$NEW_TREE. Um autor não aprovado não pode conceder a si mesmo permissão editandoOWNERSno mesmo commit do workspace. - Isenção do Sandbox: Qualquer caminho em
//experimental/<actor>/*ignora as verificações deOWNERS, para que engenheiros e agentes possam prototipar livremente comgit ws-exp <actor> <project>e, mais tarde, graduar o pacote para um domínio de produção comgit ws-graduate.
Revisão de Código Sem Atrito: PRs Efêmeros e Diffs de Snapshot
Como refs/heads/main contém apenas o plano de controle e refs/trunk/main é um commit sem pais, abrir diretamente a partir de uma branch de workspace um Pull Request padrão do GitHub para main tentaria comparar históricos não relacionados. Resolvemos isso com três fluxos de revisão adaptados a diferentes mudanças:
- Diffs de Workspace de Duplo Ponto Instantâneos (
git ws-save+ Roteador de Diff de Borda) Ao enviar um workspace comgit ws-save,refs/ws/<actor>/<id>fica exposto por meio de um worker leve de borda. Esse worker resolverefs/trunk/maine a referência do workspace para seus SHAs de commit e redireciona para a visualização de comparação de duplo ponto do GitHub (/compare/<trunk-sha>..<ws-sha>). Para patches pequenos ou mudanças geradas por agentes, os revisores podem inspecionar o diff e clicar em Land diretamente na interface web. - Pull Requests Efêmeros no GitHub (
git ws-pr) Para revisão linha a linha no GitHub,git ws-prenvia o commit snapshot raiz ($(git rev-list --max-parents=0 HEAD)) pararefs/heads/base/<id>e a ponta do workspace pararefs/heads/review/<id>. Em seguida, abre um Pull Request no GitHub comparandoreview/<id>combase/<id>. Como a branch base é o snapshot de onde o workspace foi ramificado, a aba Files changed do GitHub mostra apenas o delta do workspace. Quando o revisor clica em Merge pull request, um webhook promove o commit de topo pararefs/heads/land/<id>, executa o pipeline de landing em memória e exclui automaticamenterefs/heads/base/<id>erefs/heads/review/<id>. - Pull Requests em Espelhos Externos Contribuidores open-source nunca precisam aprender o fluxo de trabalho do monorepo. Eles fazem fork e abrem pull requests contra repositórios espelho independentes, como de costume.
Mudanças, Patchsets e Transferência de Aprovações
Um nome de branch não consegue identificar uma mudança. Um agente pode renomear sua branch, reescrever a ponta (amend) ou fazer rebase sobre um trunk mais recente várias vezes antes do landing. Como no Gerrit, o fluxo de trabalho identifica uma mudança por um trailer Change-Id: I<40 hex> na mensagem do commit de topo. git ws-save, git ws-pr e git ws-land adicionam o trailer quando a ponta não o possui e o mantêm quando já existe, de modo que o identificador sobrevive a amends, rebases e renomeações. O commit trunk integrado mantém o trailer e registra o número de seu pull request de revisão, de modo que git trunk-cl <number|Change-Id> o encontra em refs/trunk/history.
Cada ponta enviada para revisão torna-se um patchset. git ws-pr o envia para a referência imutável refs/ws/cl/<change-id>/<tip>, ao lado das branches de revisão. Executar git ws-pr novamente após um amend ou um git ws-sync acrescenta um patchset ao mesmo pull request. git ws-ps lista os patchsets da mudança atual, e git ws-psdiff <a> <b> imprime o interdiff entre dois deles, para que o revisor leia apenas o que mudou desde a última rodada.
O GitHub vincula uma aprovação a um único commit, de modo que todo rebase normalmente exigiria uma nova revisão. O pipeline de landing também aceita uma aprovação dada a outro patchset da mesma mudança quando ambos os patchsets fazem o mesmo patch sobre seus respectivos snapshots base. Ele compara git patch-id --verbatim sobre o diff binário completo. Um rebase limpo sobre um trunk mais recente é integrado sem outra revisão, enquanto qualquer edição de conteúdo, incluindo mudança de espaços em branco ou de modo de arquivo, exige uma nova aprovação.
Sincronização Bidirecional em O(delta) e Branches de Produção Separadas
Muitas organizações publicam pacotes individuais como repositórios independentes no GitHub, para que usuários externos possam executar go get, cargo install ou fazer fork de um único projeto sem tocar no monorepo. Alguns projetos, como blogs estáticos ou aplicações web com liberação controlada, também mantêm uma branch production separada ao lado de main.
Nosso motor de sincronização em Starlark trata tanto as exportações de saída quanto as contribuições de entrada em tempo O(delta), proporcional aos arquivos alterados em um commit, e não ao tamanho do monorepo:
- Splice de Índice Sem Blobs: Ao inserir ou remover a subárvore de um repositório espelho na árvore raiz do monorepo, o motor define
GIT_NO_LAZY_FETCH=1e executagit update-index --index-infoseguido degit write-tree --missing-ok. Subárvores e blobs não modificados permanecem referenciados por SHA-1 e nunca são baixados para o runner de CI. - Pré-busca de Blobs Apenas do Delta: Antes de enviar um commit exportado para um espelho independente, o script executa
git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"e pré-busca apenas os blobs antigos e novos dos arquivos modificados, viagit cat-file --batch. O Git pode então calcular um packfile delta enxuto e enviá-lo ao repositório espelho em segundos. - Preservação de Fluxos de Trabalho e Branches de Release: Durante a exportação, o sincronizador preserva o diretório
.github/workflows/próprio de cada repositório independente, para que os runners públicos de CI executem os testes do projeto no espelho. Cada workflow na configuração Starlark tem como alvo uma branch explícita, comomain, o que preserva os gates de release de repositórios que implantam a partir de uma branchproductionseparada. Os landings do monorepo sincronizam com a branchmaindo espelho, e commits automatizados de metadados ou tradução emmainsão sincronizados de volta pararefs/trunk/mainvia webhook. Promovermainparaproductioncontinua sendo uma etapa explícita de release.
Considerações Finais
Esta arquitetura trata o armazenamento de objetos do Git como um Content-Addressable Store, separa refs/heads/main de refs/trunk/main e refs/trunk/history, identifica cada mudança por um Change-Id com patchsets imutáveis e mescla árvores em memória com git merge-tree --write-tree. O custo do onboarding cresce com os diretórios de que uma tarefa precisa, e o custo do landing e da sincronização com espelhos cresce com o tamanho de uma mudança, não com o tamanho do repositório.