GoSuda

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

By Lemon Mint views ...

Toda organização de engenharia em crescimento acaba chegando à mesma bifurcação no caminho. Começa-se com dezenas de pequenos repositórios, e a vida parece 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 antes que você dê o primeiro gole no café. Então chega o custo. 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 downstream, aguardando as filas de CI serem liberadas em ordem topológica.

Mover tudo para um monorepo Git tradicional significa trocar a dor de coordenação pela dor física. À medida que o histórico de commits se aprofunda e ativos binários se acumulam em revisões passadas, o git fetch fica 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. Polyrepos cegam o agente para os chamadores em outros projetos e o obrigam a manipular credenciais em vários repositórios. Monorepos tradicionais esgotam o disco do contêiner do agente e a largura de banda da rede a cada inicialização de tarefa, enquanto agentes concorrentes que atuam sobre o mesmo branch criam corridas de rebase intermináveis.

Queríamos uma terceira opção. Inspirados na arquitetura google3, fizemos uma pergunta simples: Podemos obter 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 apenas a infraestrutura padrão do Git e armazenamento de objetos commodity?

A resposta foi sim, e ela não exigiu um daemon de sistema de arquivos personalizado nem um dispositivo de revisão de terceiros. Exigiu apenas usar o Git para aquilo que ele realmente é por baixo dos panos: um Content-Addressable Store (CAS) baseado em árvores de Merkle.

O Equívoco Central 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 primitivos imutáveis, indexados por seu hash de conteúdo SHA-1 (ou SHA-256):

  1. blob: O conteúdo bruto, em bytes, de um único arquivo.
  2. tree: Uma listagem de diretório que mapeia nomes de arquivos e permissões para hashes de blob ou de tree filhas.
  3. commit: Um pequeno cabeçalho de texto (normalmente de 200 a 300 bytes) que aponta para um único hash de tree raiz, zero ou mais hashes de commit pais e metadados do autor.

Como content-addressable store, esse design é notavelmente eficiente. Se um monorepo contém 50.000 arquivos e você edita uma única linha em um arquivo, o novo commit reutiliza exatamente os mesmos hashes de blob e tree dos outros 49.999 arquivos.

Por que, então, um monorepo Git tradicional parece pesado? Porque a convenção sobrecarrega uma única referência, refs/heads/main, com três funções conflitantes:

  • Ela contém a árvore de diretórios unificada inteira de todos os projetos.
  • Ela arrasta consigo a cadeia inteira de commits históricos ligados por pais até o primeiro dia.
  • Ela reside em refs/heads/*, o namespace que servidores Git anunciam e negociam por padrão em cada git clone e git fetch.

Toda vez que um desenvolvedor ou runner de CI busca refs/heads/main, cliente e servidor negociam a alcançabilidade no grafo de commits através de milhares de revisões históricas. Se alguém commitou acidentalmente um arquivo de 100 MB há três anos e o removeu no dia seguinte, seu blob ainda está preso a uma tree histórica alcançável a partir de refs/heads/main.

Separando o Namespace de Referências em Quatro Camadas

O protocolo de rede do Git anuncia apenas refs/heads/* e refs/tags/* durante um clone padrão. Referências em namespaces personalizados, como refs/trunk/* ou refs/ws/*, residem exatamente no mesmo armazenamento de objetos no servidor, beneficiam-se da mesma compressão delta de packfiles e podem ser buscadas ou enviadas explicitamente, sem impor qualquer sobrecarga aos clones padrão.

Ao dividir responsabilidades entre quatro namespaces de referência dentro de um único repositório, o gargalo desaparece:

1refs/heads/main          Apenas plano de controle (~20 KB: regras, .gitconfig, configuração 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 exatamente o SHA-1 da árvore raiz
4refs/ws/<actor>/<id>     Snapshots de workspace isolados (changelists) por desenvolvedor ou agente
5refs/heads/land/<id>     Gatilho efêmero de merge queue, excluído imediatamente após o land

1. O Plano de Controle (refs/heads/main)

O branch padrão não contém código-fonte de aplicações. Ele rastreia 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 scripts de merge do lado do servidor. Clonar o branch padrão transfere cerca de 20 KB e termina em milissegundos.

2. O Snapshot Sem Pais do Trunk (refs/trunk/main)

refs/trunk/main aponta para a árvore-fonte completa e unificada de toda a organização (//web/*, //net/*, //ipc/*, //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á-lo não exige nenhuma negociação do grafo de commits. O servidor Git não percorre o histórico; ele envia o único commit de topo e seus objetos tree de diretório atuais. Binários históricos ou arquivos removidos 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)

Desenvolvedores ainda precisam de git blame e git log ao investigar regressões. Sempre que uma mudança é integrada, o script de merge cria dois objetos de commit apontando para o mesmo hash SHA-1 de $NEW_TREE:

 1# Commit de snapshot sem pais para checkouts instantâneos de workspace com profundidade 1
 2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
 3
 4# Commit com pais para git log e git blame lineares
 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 dobra a pegada de armazenamento do repositório? De forma alguma. 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 enviados para refs/ws/<actor>/<id> em vez de refs/heads/*. Uma centena de agentes de codificação pode salvar checkpoints a cada minuto sem poluir os menus de branches nem tornar o git fetch mais lento para os engenheiros humanos.

Onboarding em Cinco Comandos: Como Funciona o Checkout Esparso + Blobless

Com esse 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 cinco comandos Git padrão:

 1# 1. Clona 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. Carrega os aliases do repositório e anexa refs/trunk/*
 6git config include.path ../.gitconfig
 7git trunk-init
 8
 9# 3. Declara os diretórios de domínio necessários para esta tarefa
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Cria um workspace isolado enraizado no snapshot mais recente do trunk
13git ws-new alice/optimize-framing

Por baixo dos panos, cada etapa realiza apenas o trabalho mínimo necessário:

  1. git clone --filter=blob:none --sparse baixa o plano de controle de 20 KB em refs/heads/main.
  2. git trunk-init configura +refs/trunk/*:refs/remotes/origin/trunk/* e executa um fetch sem blobs. O Git baixa os metadados de tree de refs/trunk/main (normalmente algumas dezenas de quilobytes) e zero blobs de arquivos.
  3. git sparse-checkout set net/relay ipc/codec instrui o filtro de índice em modo cone do Git a materializar apenas o diretório raiz e essas duas subárvores.
  4. git ws-new muda para um novo branch enraizado em origin/trunk/main. Nesse exato momento, o Git solicita sob demanda apenas os objetos blob dentro de net/relay e ipc/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 em espaço de usuário, via FUSE no Linux, FileProvider no macOS ou ProjFS no Windows:

  1. Montagem Instantânea da Árvore Completa: Um daemon local leve de montagem lê o objeto tree raiz de refs/trunk/main e projeta toda a hierarquia de diretórios do monorepo no diretório de trabalho em milissegundos, eliminando a necessidade de executar git sparse-checkout set.
  2. Carregamento de Blobs sob Demanda: O conteúdo dos arquivos permanece virtual até que um compilador, IDE ou agente emita uma chamada de sistema open(2) ou read(2). O daemon então busca o blob SHA-1 correspondente no endpoint do CAS e o armazena em cache no armazenamento NVMe local.
  3. Overlay Copy-on-Write para Workspaces: Edições locais são gravadas em uma camada leve de overlay superior. Executar git ws-save calcula o hash apenas dos caminhos modificados nesse overlay, grava os objetos blob e tree atualizados e envia o commit resultante para refs/ws/<actor>/<id>, sem varrer diretórios não modificados.

O Modelo de Changelist em Git Puro: Landing em Memória no Servidor

Em um fluxo de trabalho Git tradicional, a integração de um branch de funcionalidade cria um commit de merge com dois pais (transformando o histórico em um pátio ferroviário emaranhado) ou exige que os desenvolvedores façam rebase manual e disputem a corrida com outros engenheiros que enviam alterações para main.

Inspirado no modelo de Changelist (CL) do google3, cada workspace em nossa arquitetura representa uma mudança atômica única em relação ao snapshot do 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 do 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 plumbing do Git 2.40+:

 1# 1. Identifica o commit de snapshot raiz do qual o workspace se ramificou
 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. Calcula 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

Observe atentamente a etapa 2:

  • Independentemente de quantos commits locais foram feitos no branch do workspace, o script compara apenas três hashes de árvore: o snapshot base original ($BASE_TREE), o snapshot atual do trunk ($MAIN_TREE) e a árvore final do workspace ($WORK_TREE).
  • Se refs/trunk/main avançou em //web/blog enquanto Alice editava //net/relay, o git merge-tree --write-tree calcula a árvore raiz combinada $NEW_TREE diretamente dentro do armazenamento de objetos do Git, em milissegundos. Nenhum arquivo é gravado no sistema de arquivos do runner.
  • Por fim, git push --atomic com --force-with-lease atualiza refs/trunk/main, acrescenta um commit linear a refs/trunk/history e exclui o gatilho de landing refs/heads/land/<id> em uma única transação.

Impondo Limites de Arquitetura: Visibilidade em Quatro Níveis e OWNERS Hierárquico

A consolidação de código em um único repositório só funciona se os limites entre domínios forem impostos mecanicamente antes que um commit possa ser integrado. Logo após o git merge-tree --write-tree produzir $NEW_TREE (e antes que o 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órioNívelImportações PermitidasImportações ProibidasGate de OWNERS
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Produção PúblicaProdução Pública, //third_party/*//corp/*, //experimental/*Aplicado
//third_party/*Fornecedor ExternoAutocontido ou //third_party/*Todos os domínios não third_partyAplicado
//corp/*Privado InternoProdução Pública, //corp/*, //third_party/*//experimental/*Aplicado
//experimental/<actor>/*SandboxQualquer pacote não corp//corp/*Isento (aprovado automaticamente)

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

Você não precisa de uma instância dedicada do Gerrit para impor arquivos OWNERS no nível de diretório com regras de herança:

  • Para cada caminho modificado entre $MAIN_TREE e $NEW_TREE, o verificador percorre o caminho ascendente, a partir do diretório do arquivo, até //OWNERS.
  • Qualquer arquivo OWNERS pode declarar set noparent para bloquear aprovações herdadas de diretórios pais, ou per-file *.sql=@dba-team para limitar aprovações por padrão de nome de arquivo (glob).
  • Invariante Anti-Adulteração: Os arquivos OWNERS existentes são sempre lidos a partir de $MAIN_TREE (a árvore atual do trunk), nunca de $NEW_TREE. Um autor não aprovado não pode conceder a si mesmo permissão editando OWNERS no mesmo commit do workspace.
  • Isenção para Sandbox: Qualquer caminho sob //experimental/<actor>/* ignora as verificações de OWNERS, para que engenheiros e agentes possam prototipar livremente com git ws-exp <actor> <project> e, posteriormente, graduar o pacote para um domínio de produção com git 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 um Pull Request padrão do GitHub diretamente de um branch de workspace para main tentaria comparar históricos não relacionados. Resolvemos isso com três fluxos de revisão adaptados a diferentes tipos de mudança:

  1. Diffs de Workspace de Duplo Ponto Instantâneos (git ws-save + Edge Diff Router) Enviar um workspace com git ws-save expõe refs/ws/<actor>/<id> por meio de um worker leve de borda. Esse worker resolve refs/trunk/main e 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 pequenos patches ou mudanças geradas por agentes, os revisores podem inspecionar o diff e clicar em Land diretamente na interface web.
  2. Pull Requests Efêmeros no GitHub (git ws-pr) Para revisão linha a linha no GitHub, git ws-pr envia o commit de snapshot raiz ($(git rev-list --max-parents=0 HEAD)) para refs/heads/base/<id> e a ponta do workspace para refs/heads/review/<id>. Em seguida, abre um Pull Request no GitHub comparando review/<id> com base/<id>. Como o branch base é exatamente o snapshot do qual o workspace se ramificou, a aba Files changed do GitHub mostra apenas as linhas exatas modificadas no workspace. Quando o revisor clica em Merge pull request, um webhook promove o commit de topo para refs/heads/land/<id>, executa o pipeline de landing em memória e exclui automaticamente refs/heads/base/<id> e refs/heads/review/<id>.
  3. Pull Requests em Espelhos Externos Contribuidores open-source nunca precisam aprender o fluxo de trabalho do monorepo. Eles fazem fork e abrem pull requests nos repositórios espelho independentes, como de costume.

Sincronização Bidirecional O(delta) e Branches de Produção Separados

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 um branch production separado 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:

  • Splicing de Índice Sem Blobs: Ao inserir ou retirar a subárvore de um repositório espelho na árvore raiz do monorepo, o motor define GIT_NO_LAZY_FETCH=1 e executa git update-index --index-info seguido de git write-tree --missing-ok. Subárvores e blobs não modificados são referenciados apenas por seus hashes SHA-1, sem nunca serem 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, por meio de git cat-file --batch. Assim, o Git pode calcular um thin pack delta e enviá-lo ao repositório espelho em segundos.
  • Preservação de Fluxos de Trabalho e de Branches de Release: Durante a exportação, o sincronizador preserva o diretório .github/workflows/ 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 em Starlark tem como alvo um branch explícito, como main, o que preserva os gates de liberação de repositórios que fazem deploy a partir de um branch production separado. As integrações no monorepo sincronizam com o branch main do espelho, e commits automatizados de metadados ou tradução em main são sincronizados de volta para refs/trunk/main via webhook. Promover main para production continua sendo uma etapa explícita de release.

Considerações Finais

Construir um monorepo de alta velocidade não exige abandonar o Git padrão nem operar uma frota complexa de servidores de infraestrutura sob medida. 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, e mescla árvores em memória com git merge-tree --write-tree. Como resultado, tanto engenheiros humanos quanto agentes de codificação autônomos podem clonar um workspace em menos de um segundo e escalar com clareza por centenas de projetos.