---
id: 773766eac9e6ff3c46bd686d78539aed
author: Lemon Mint
title: 'Repensando o Monorepo Git: Uma Arquitetura Nativa de CAS para Humanos e Agentes de Programação'
description: Como construir um monorepo sem daemon, nativo de CAS, sobre Git padrão, utilizando snapshots de trunk sem commits pais, referências de workspace isoladas, árvores de merge em memória, OWNERS hierárquico e sincronização de espelhos em O(delta).
language: pt
date: 2026-10-30T01:17:15.292299Z
path: /blog/posts/rethinking-the-git-monorepo-a-cas-native-architecture-for-humans-and-coding-agents-zfd3d7931
category: lowlevel
---

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):

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` filhos.
3. **`commit`**: Um pequeno cabeçalho de texto (normalmente de 200 a 300 bytes) que aponta para um único hash de `tree` raiz, para zero ou mais hashes de `commit` pais 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 cada `git clone` e `git 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:

```
refs/heads/main               Apenas plano de controle (~20 KB: regras, .gitconfig, config de sincronização, scripts)
refs/trunk/main               Snapshot de commit sem pais apontando para a árvore raiz unificada mais recente
refs/trunk/history            Cadeia linear de commits ligados por pais, compartilhando o SHA-1 exato da árvore raiz
refs/ws/<actor>/<id>          Snapshots de workspace isolados (changelists) por desenvolvedor ou agente
refs/ws/cl/<change-id>/<tip>  Patchsets imutáveis: cada tip de uma mudança enviada para revisão
refs/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`:

```bash
# Commit snapshot sem pais para checkouts instantâneos de workspace com profundidade 1
SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")

# Commit ligado ao pai para git log linear e git blame
HIST=$(git commit-tree "$NEW_TREE" -p "$OLD_HIST" -m "$RAW_MSG")

git push --atomic origin \
  --force-with-lease="refs/trunk/main:${OLD_MAIN}" \
  --force-with-lease="refs/trunk/history:${OLD_HIST}" \
  "${SNAP}:refs/trunk/main" \
  "${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:

```bash
# 1. Clone o plano de controle de 20 KB com filtro de blobs e modo esparso habilitados
git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
cd monorepo

# 2. Carregue os aliases do repositório e anexe refs/trunk/*
git config include.path ../.gitconfig
git trunk-init

# 3. Declare os diretórios de domínio necessários para esta tarefa
git sparse-checkout set net/relay ipc/codec

# 4. Crie um workspace isolado a partir do snapshot trunk mais recente
git ws-new alice/optimize-framing
```

Cada etapa busca apenas o que precisa:

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 uma nova branch baseada em `origin/trunk/main`. Em seguida, 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 no espaço do 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, uma IDE ou um 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 NVMe local.
3. **Overlay Copy-on-Write para Workspaces**: As edições locais são gravadas em uma camada superior leve de overlay. 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 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+:

```bash
# 1. Identifique o commit snapshot raiz de onde o workspace foi ramificado
BASE=$(git rev-list --max-parents=0 "$WORK_SHA")

BASE_TREE=$(git rev-parse "${BASE}^{tree}")
MAIN_TREE=$(git rev-parse "${OLD_MAIN}^{tree}")
WORK_TREE=$(git rev-parse "${WORK_SHA}^{tree}")

# 2. Calcule a árvore raiz mesclada em memória, sem checkout de working tree
if [ "$MAIN_TREE" = "$BASE_TREE" ]; then
  NEW_TREE="$WORK_TREE"
elif OUT=$(git merge-tree --write-tree --merge-base="$BASE_TREE" "$MAIN_TREE" "$WORK_TREE"); then
  NEW_TREE=$(head -n1 <<<"$OUT")
else
  echo "Merge conflict detected; run git ws-sync to rebase." >&2
  exit 1
fi
```

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/main` avançou em `//web/blog` enquanto Alice editava `//net/relay`, `git merge-tree --write-tree` calcula a árvore raiz combinada `$NEW_TREE` diretamente 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 --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>`.

## 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_TREE` e `$NEW_TREE`, o verificador sobe 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 restringir aprovações por padrão glob de nome de arquivo.
- **Invariante Anti-Adulteração**: Os arquivos `OWNERS` existentes 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 editando `OWNERS` no mesmo commit do workspace.
- **Isenção do Sandbox**: Qualquer caminho em `//experimental/<actor>/*` ignora as verificações de `OWNERS`, para que engenheiros e agentes possam prototipar livremente com `git ws-exp <actor> <project>` e, mais tarde, 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 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:

1. **Diffs de Workspace de Duplo Ponto Instantâneos (`git ws-save` + Roteador de Diff de Borda)**
   Ao enviar um workspace com `git ws-save`, `refs/ws/<actor>/<id>` fica exposto 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 patches pequenos 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 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 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 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 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=1` e executa `git update-index --index-info` seguido de `git 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, via `git 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, como `main`, o que preserva os gates de release de repositórios que implantam a partir de uma branch `production` separada. Os landings do monorepo sincronizam com a 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

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.
