---
id: 773766eac9e6ff3c46bd686d78539aed
author: Lemon Mint
title: 'Repensando el Git Monorepo: una arquitectura CAS-Native para humanos y coding agents'
description: Cómo construir un monorepo sin demonios, nativo de CAS, sobre Git estándar, utilizando instantáneas de tronco sin padres, referencias de espacios de trabajo aislados, árboles de fusión en memoria, OWNERS jerárquicos y sincronización de espejos en O(delta).
language: es
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 organización de ingeniería en crecimiento acaba eligiendo entre muchos repositorios o uno solo. Al principio, con decenas de repositorios pequeños, la vida parece sencilla. Cada biblioteca tiene su propio gestor de incidencias, su propio pipeline de CI y un `git clone` que termina en segundos. Luego cambias una estructura en un paquete central de red y pasas el resto de la tarde abriendo pull requests de actualización de versión en seis repositorios dependientes, esperando a que las colas de CI se vacíen en orden topológico.

Si trasladas todo a un monorepo tradicional de Git, cambias la coordinación entre repositorios por el tamaño del repositorio. A medida que el historial de commits crece y los activos binarios se acumulan en revisiones pasadas, `git fetch` se vuelve lento. Los árboles de trabajo alcanzan los gigabytes. Muy pronto, alguien propone desplegar un demonio de sistema de archivos virtual a medida, un servidor dedicado de revisión de código o una flota de workers de compilación remotos, solo para que el desarrollo diario siga siendo soportable.

Cuando los agentes de programación autónomos se incorporan al equipo, ambos modos de fallo empeoran. Los polyrepos ciegan al agente frente a los llamadores de otros proyectos y lo obligan a gestionar credenciales en múltiples repositorios. Los monorepos tradicionales saturan el disco del contenedor del agente y el ancho de banda de red en cada inicialización de tarea, mientras que los agentes concurrentes que trabajan sobre la misma rama generan carreras de rebase interminables.

Inspirados en la arquitectura `google3`, construimos cambios atómicos sobre un único trunk, una incorporación dispersa (sparse) en menos de un segundo, una gobernanza jerárquica mediante `OWNERS` y réplicas de código abierto independientes, todo ello con la infraestructura estándar de Git y almacenamiento de objetos de bajo coste.

El diseño no necesita ni un demonio de sistema de archivos a medida ni un dispositivo de revisión de terceros, porque trata Git como un Content-Addressable Store (CAS) basado en árboles de Merkle.

## El malentendido en el origen de los monorepos lentos en Git

Cuando los desarrolladores se quejan de que Git no escala a monorepos grandes, rara vez están alcanzando un límite en la base de datos de objetos de Git. Dentro de `.git/objects`, Git almacena tres primitivas inmutables indexadas por su hash de contenido SHA-1 (o SHA-256):

1. **`blob`**: el contenido binario bruto de un único archivo.
2. **`tree`**: un listado de directorio que asocia nombres de archivo y permisos con hashes de `blob` o de `tree` hijos.
3. **`commit`**: una pequeña cabecera de texto (normalmente de 200 a 300 bytes) que apunta a un único hash de `tree` raíz, a cero o más hashes de `commit` padre y a metadatos del autor.

Si un monorepo contiene 50 000 archivos y editas una sola línea de un archivo, el nuevo commit reutiliza exactamente los hashes de `blob` y `tree` de los otros 49 999 archivos.

Un monorepo tradicional de Git resulta pesado porque la convención sobrecarga una única referencia, `refs/heads/main`, con tres funciones en conflicto:

- Contiene el **árbol de directorios unificado completo** de todos los proyectos.
- Arrastra consigo la **cadena completa de commits históricos enlazados por padres** hasta el primer día.
- Reside dentro de `refs/heads/*`, el espacio de nombres que los servidores Git anuncian y negocian por defecto en cada `git clone` y `git fetch`.

Cada vez que un desarrollador o un runner de CI obtiene `refs/heads/main`, el cliente y el servidor negocian la alcanzabilidad en el grafo de commits a través de miles de revisiones históricas. Si alguien subió por accidente un archivo comprimido de 100 MB hace tres años y lo borró al día siguiente, su `blob` sigue colgando de un `tree` histórico alcanzable desde `refs/heads/main`.

## Separar el espacio de nombres de referencias

El protocolo de red de Git solo anuncia `refs/heads/*` y `refs/tags/*` durante un clone estándar. Las referencias bajo espacios de nombres personalizados, como `refs/trunk/*` o `refs/ws/*`, viven en el mismo almacén de objetos del servidor, se benefician de la misma compresión delta de packfiles y pueden obtenerse o enviarse explícitamente, pero no imponen ninguna sobrecarga en los clones por defecto.

Repartir estas funciones entre referencias separadas dentro de un único repositorio elimina el cuello de botella:

```
refs/heads/main               Solo plano de control (~20 KB: reglas, .gitconfig, configuración de sincronización, scripts)
refs/trunk/main               Commit de instantánea sin padres que apunta al árbol raíz unificado más reciente
refs/trunk/history            Cadena lineal de commits enlazados por padres que comparte el SHA-1 exacto del árbol raíz
refs/ws/<actor>/<id>          Instantáneas de workspace aisladas (listas de cambios) por desarrollador o agente
refs/ws/cl/<change-id>/<tip>  Patchsets inmutables: cada punta de un cambio enviada a revisión
refs/heads/land/<id>          Disparador efímero de la cola de fusión, borrado inmediatamente tras el landing
```

### 1. El plano de control (`refs/heads/main`)

La rama por defecto no contiene código fuente de la aplicación. Solo registra las reglas operativas del repositorio (`AGENTS.md`, `README.md`), un `.gitconfig` compartido con alias de flujo de trabajo, la configuración de sincronización en Starlark (`sync.star`) y los scripts de fusión del lado del servidor. Clonar la rama por defecto transfiere unos 20 KB y termina en milisegundos.

### 2. La instantánea de trunk sin padres (`refs/trunk/main`)

`refs/trunk/main` apunta al árbol fuente completo y unificado de toda la organización (`//web/*`, `//net/*`, `//ipc/*`, `//storage/*`, `//crypto/*`, `//ai/*`, `//devtools/*`, `//corp/*`, `//third_party/*`, `//experimental/*`), pero su objeto commit se crea **sin el flag `-p`** de padre.

Como `refs/trunk/main` no tiene padres (`parents = []`), obtenerlo no requiere ninguna negociación del grafo de commits. El servidor Git no recorre el historial: envía el único commit de cabeza y sus objetos `tree` de directorio actuales. Los binarios históricos o los archivos borrados el mes pasado no son alcanzables desde `refs/trunk/main` y nunca se transfieren.

### 3. El historial lineal de auditoría (`refs/trunk/history`)

Los desarrolladores siguen necesitando `git blame` y `git log` al investigar regresiones. Cada vez que se aprueba un cambio, el script de fusión crea dos objetos commit que apuntan al **mismo hash SHA-1 exacto del árbol raíz `$NEW_TREE`**:

```bash
# Commit de instantánea sin padres para checkouts de workspace inmediatos de profundidad 1
SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")

# Commit enlazado con su padre para git log y git blame lineales
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"
```

Mantener tanto `refs/trunk/main` como `refs/trunk/history` no duplica la huella de almacenamiento. Como ambos commits referencian el mismo `$NEW_TREE`, el almacén direccionable por contenido de Git deduplica el 100 % de los objetos `tree` y `blob`. El coste adicional de la instantánea sin padres es una única cabecera de commit de 250 bytes.

### 4. Workspaces aislados (`refs/ws/<actor>/<id>`)

Las ramas de trabajo en curso se envían a `refs/ws/<actor>/<id>` en lugar de a `refs/heads/*`. Cien agentes de programación pueden guardar puntos de control cada minuto sin contaminar los desplegables de ramas ni ralentizar `git fetch` para los ingenieros humanos.

## Incorporación en cuatro pasos: cómo funciona el checkout disperso y sin blobs

Con esta disposición de referencias, un nuevo desarrollador o un contenedor efímero de agente de programación no instala ninguna CLI a medida ni ningún driver FUSE. La incorporación consta de cuatro pasos con Git estándar y los alias del repositorio:

```bash
# 1. Clonar el plano de control de 20 KB con filtrado de blobs y modo disperso activado
git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
cd monorepo

# 2. Cargar los alias del repositorio y adjuntar refs/trunk/*
git config include.path ../.gitconfig
git trunk-init

# 3. Declarar los directorios de dominio necesarios para esta tarea
git sparse-checkout set net/relay ipc/codec

# 4. Crear un workspace aislado basado en la instantánea de trunk más reciente
git ws-new alice/optimize-framing
```

Cada paso obtiene solo lo que necesita:

1. `git clone --filter=blob:none --sparse` descarga el plano de control de 20 KB de `refs/heads/main`.
2. `git trunk-init` configura `+refs/trunk/*:refs/remotes/origin/trunk/*` y ejecuta una obtención sin blobs. Git descarga los metadatos `tree` de `refs/trunk/main` (normalmente unas decenas de kilobytes) y **cero blobs de archivos**.
3. `git sparse-checkout set net/relay ipc/codec` indica al filtro de índice en modo cono de Git que materialice solo el directorio raíz y esos dos subárboles.
4. `git ws-new` cambia a una nueva rama basada en `origin/trunk/main`. Después, Git solicita bajo demanda únicamente los objetos `blob` dentro de `net/relay` e `ipc/codec`. El 95 % restante del monorepo nunca se descarga al disco.

### Horizonte futuro: montar `refs/trunk/main` como sistema de archivos virtual (VFS)

El checkout disperso con clones sin blobs cubre los flujos de trabajo diarios sin herramientas adicionales en el cliente. Como `refs/trunk/main` ya es un árbol de Merkle puro en un Content-Addressable Store, la arquitectura también proporciona una base directa para un sistema de archivos virtual (VFS) opcional en espacio de usuario mediante FUSE en Linux, FileProvider en macOS o ProjFS en Windows:

1. **Montaje instantáneo del árbol completo**: un demonio ligero de montaje local lee el objeto `tree` raíz de `refs/trunk/main` y proyecta toda la jerarquía de directorios del monorepo en el directorio de trabajo en milisegundos, eliminando la necesidad de ejecutar `git sparse-checkout set`.
2. **Carga de blobs bajo demanda**: el contenido de los archivos permanece virtual hasta que un compilador, un IDE o un agente invoca la llamada al sistema `open(2)` o `read(2)`. Entonces, el demonio obtiene el `blob` SHA-1 correspondiente desde el endpoint del CAS y lo almacena en caché en el almacenamiento NVMe local.
3. **Superposición copy-on-write para workspaces**: las ediciones locales se escriben en una capa superior ligera de superposición. Ejecutar `git ws-save` calcula el hash únicamente de las rutas modificadas en esa superposición, escribe los objetos `blob` y `tree` actualizados y envía el commit resultante a `refs/ws/<actor>/<id>` sin analizar los directorios no modificados.

## El modelo de listas de cambios sobre Git puro: landing en memoria en el servidor

En un flujo de trabajo tradicional de Git, fusionar una rama de funcionalidad crea un commit de fusión con dos padres, lo que hace que el historial no sea lineal, o obliga a los desarrolladores a hacer rebase manualmente y competir con los pushes concurrentes sobre `main`.

Inspirado en el modelo de Changelist (CL) de `google3`, cada workspace de nuestra arquitectura representa un **cambio atómico único** sobre la instantánea de trunk. Mientras trabaja localmente en `ws/alice/optimize-framing`, un desarrollador o agente puede hacer diez commits incrementales (`wip`, `fix tests`, `lint`). Cuando el cambio está listo para aterrizar, el servidor colapsa el delta del workspace en una única revisión lineal de trunk sin llegar a hacer checkout de un directorio de trabajo en el runner de CI.

Así se ejecuta en memoria el script de landing (`land.sh`) usando la capa de plumbing de Git 2.40+:

```bash
# 1. Identificar el commit de instantánea raíz desde el que se bifurcó el workspace
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. Calcular el árbol raíz fusionado en memoria, sin checkout del árbol de trabajo
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
```

En el paso 2:

- Independientemente del número de commits locales realizados en la rama del workspace, el script compara solo tres hashes de árbol: la instantánea base original (`$BASE_TREE`), la instantánea actual de trunk (`$MAIN_TREE`) y el árbol final del workspace (`$WORK_TREE`).
- Si `refs/trunk/main` avanzó en `//web/blog` mientras Alice editaba `//net/relay`, `git merge-tree --write-tree` calcula el árbol raíz combinado `$NEW_TREE` directamente dentro del almacén de objetos de Git en milisegundos, sin escribir archivos en el sistema de archivos del runner.
- Una vez superadas las comprobaciones de política descritas a continuación, un único `git push --atomic` con `--force-with-lease` actualiza `refs/trunk/main` y añade un commit lineal a `refs/trunk/history`, y el disparador de landing `refs/heads/land/<id>` se elimina.

## Aplicar los límites de arquitectura: visibilidad en cuatro niveles y `OWNERS` jerárquicos

Consolidar código en un único repositorio solo funciona si los límites entre dominios se aplican de forma mecánica antes de que un commit pueda aterrizar. Justo después de que `git merge-tree --write-tree` produzca `$NEW_TREE` (y antes de que `git push --atomic` actualice el trunk), nuestro motor de políticas ejecuta dos comprobaciones de coste O(delta) sobre `git diff-tree "$MAIN_TREE" "$NEW_TREE"`.

### 1. Visibilidad de dominios en cuatro niveles

El árbol raíz se divide en cuatro niveles de visibilidad:

| Prefijo de directorio                                                                  | Nivel               | Importaciones permitidas                         | Importaciones prohibidas       | Control `OWNERS`      |
| :------------------------------------------------------------------------------------- | :------------------ | :----------------------------------------------- | :----------------------------- | :-------------------- |
| `//web/*`, `//net/*`, `//ipc/*`, `//storage/*`, `//crypto/*`, `//ai/*`, `//devtools/*` | Producción pública  | Producción pública, `//third_party/*`            | `//corp/*`, `//experimental/*` | Aplicado              |
| `//third_party/*`                                                                      | Proveedor externo   | Autocontenido o `//third_party/*`                | Todos los dominios que no sean `third_party`   | Aplicado              |
| `//corp/*`                                                                             | Privado interno     | Producción pública, `//corp/*`, `//third_party/*` | `//experimental/*`             | Aplicado              |
| `//experimental/<actor>/*`                                                             | Sandbox             | Cualquier paquete que no sea `corp`              | `//corp/*`                     | Exento (aprobación automática) |

Si un paquete público de `//web/*` intenta importar un paquete interno de `//corp/*` o un prototipo no graduado de `//experimental/*`, el job de landing se aborta antes de tocar `refs/trunk/main`.

### 2. Verificación jerárquica de `OWNERS` sin servidor Gerrit

El verificador aplica archivos `OWNERS` a nivel de directorio con reglas de herencia:

- Para cada ruta modificada entre `$MAIN_TREE` y `$NEW_TREE`, el verificador recorre hacia arriba desde el directorio del archivo hasta `//OWNERS`.
- Cualquier archivo `OWNERS` puede declarar `set noparent` para bloquear las aprobaciones heredadas de los directorios padre, o `per-file *.sql=@dba-team` para acotar las aprobaciones mediante un glob de nombre de archivo.
- **Invariante anti-manipulación**: los archivos `OWNERS` existentes se leen siempre desde `$MAIN_TREE` (el árbol actual de trunk), nunca desde `$NEW_TREE`. Un autor no autorizado no puede concederse permisos editando `OWNERS` en el mismo commit del workspace.
- **Exención de sandbox**: cualquier ruta bajo `//experimental/<actor>/*` omite las comprobaciones de `OWNERS`, de modo que los ingenieros y agentes puedan prototipar libremente con `git ws-exp <actor> <project>` y, más tarde, graduar el paquete a un dominio de producción con `git ws-graduate`.

## Revisión de código sin fricción: pull requests efímeras y diffs de instantánea

Como `refs/heads/main` solo contiene el plano de control y `refs/trunk/main` es un commit sin padres, abrir directamente desde una rama de workspace una pull request estándar de GitHub hacia `main` intentaría comparar historiales no relacionados. Lo resolvimos con tres flujos de revisión adaptados a distintos tipos de cambios:

1. **Diffs de workspace de dos puntos instantáneos (`git ws-save` + Edge Diff Router)**
   Enviar un workspace con `git ws-save` expone `refs/ws/<actor>/<id>` a través de un worker ligero en el edge. Este worker resuelve `refs/trunk/main` y la referencia del workspace a sus SHAs de commit y redirige a la vista de comparación de dos puntos de GitHub (`/compare/<trunk-sha>..<ws-sha>`). Para parches pequeños o cambios generados por agentes, los revisores pueden inspeccionar el diff y hacer clic en **Land** directamente en la interfaz web.
2. **Pull requests efímeras de GitHub (`git ws-pr`)**
   Para revisión línea por línea en GitHub, `git ws-pr` envía el commit de instantánea raíz (`$(git rev-list --max-parents=0 HEAD)`) a `refs/heads/base/<id>` y la punta del workspace a `refs/heads/review/<id>`. Después abre una pull request de GitHub que compara `review/<id>` con `base/<id>`. Como la rama base es la instantánea desde la que se bifurcó el workspace, la pestaña **Files changed** de GitHub muestra solo el delta del workspace. Cuando el revisor hace clic en **Merge pull request**, un webhook promueve el commit de cabeza a `refs/heads/land/<id>`, ejecuta el pipeline de landing en memoria y elimina automáticamente `refs/heads/base/<id>` y `refs/heads/review/<id>`.
3. **Pull requests en réplicas externas**
   Los colaboradores de código abierto nunca tienen que aprender el flujo de trabajo del monorepo. Hacen fork y abren pull requests contra réplicas independientes como de costumbre.

### Cambios, patchsets y arrastre de aprobaciones

Un nombre de rama no puede identificar un cambio. Un agente puede renombrar su rama, enmendar la punta o hacer rebase sobre un trunk más reciente varias veces antes de aterrizar. Como en Gerrit, el flujo de trabajo identifica un cambio mediante un trailer `Change-Id: I<40 hex>` en el mensaje del commit de la punta. `git ws-save`, `git ws-pr` y `git ws-land` añaden el trailer cuando la punta no lo tiene y lo conservan cuando ya existe, de modo que el identificador sobrevive a enmiendas, rebases y renombrados. El commit de trunk aterrizado conserva el trailer y registra el número de su pull request de revisión, por lo que `git trunk-cl <number|Change-Id>` lo encuentra en `refs/trunk/history`.

Cada punta enviada a revisión se convierte en un patchset. `git ws-pr` la envía a la referencia inmutable `refs/ws/cl/<change-id>/<tip>` junto a las ramas de revisión. Ejecutar `git ws-pr` de nuevo tras una enmienda o un `git ws-sync` añade un patchset a la misma pull request. `git ws-ps` lista los patchsets del cambio actual, y `git ws-psdiff <a> <b>` imprime el interdiff entre dos de ellos, de modo que el revisor lea solo lo que cambió desde la última ronda.

GitHub vincula una aprobación a un único commit, por lo que cada rebase requeriría normalmente una nueva revisión. El pipeline de landing también acepta una aprobación concedida a otro patchset del mismo cambio cuando ambos patchsets aplican el mismo parche sobre su propia instantánea base. Para ello compara `git patch-id --verbatim` sobre el diff binario completo. Un rebase limpio sobre un trunk más reciente aterriza sin otra revisión, mientras que cualquier edición de contenido, incluido un cambio de espacios en blanco o de modo de archivo, requiere una nueva aprobación.

## Sincronización bidireccional de coste O(delta) y ramas de producción separadas

Muchas organizaciones publican paquetes individuales como repositorios independientes de GitHub para que los usuarios externos puedan hacer `go get`, `cargo install` o hacer fork de un único proyecto sin tocar el monorepo. Algunos proyectos, como blogs estáticos o aplicaciones web con puertas de liberación, también mantienen una rama `production` separada junto a `main`.

Nuestro motor de sincronización en Starlark gestiona tanto las exportaciones salientes como las contribuciones entrantes en tiempo **O(delta)**, proporcional a los archivos modificados en un commit y no al tamaño del monorepo:

- **Empalme de índice sin blobs**: al insertar o extraer el subárbol de un repositorio réplica en el árbol raíz del monorepo, el motor establece `GIT_NO_LAZY_FETCH=1` y ejecuta `git update-index --index-info` seguido de `git write-tree --missing-ok`. Los subárboles y blobs no modificados permanecen referenciados por SHA-1 y nunca se descargan al runner de CI.
- **Prefetch de blobs solo del delta**: antes de enviar un commit exportado a una réplica independiente, el script ejecuta `git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"` y precarga solo los blobs antiguos y nuevos de los archivos modificados mediante `git cat-file --batch`. Así Git puede calcular un packfile delta ligero y enviarlo al repositorio réplica en segundos.
- **Preservación de flujos de trabajo y ramas de release**: durante la exportación saliente, el sincronizador conserva el directorio `.github/workflows/` propio de cada repositorio independiente, de modo que los runners públicos de CI ejecuten las pruebas del proyecto en la réplica. Cada workflow de la configuración de Starlark apunta a una rama explícita como `main`, lo que preserva las puertas de liberación de los repositorios que despliegan desde una rama `production` separada. Los landings del monorepo se sincronizan con la rama `main` de la réplica, y los commits automáticos de metadatos o traducciones en `main` se sincronizan de vuelta a `refs/trunk/main` mediante webhook. Promover `main` a `production` sigue siendo un paso de liberación explícito.

## Reflexiones finales

Esta arquitectura trata el almacén de objetos de Git como un Content-Addressable Store, separa `refs/heads/main` de `refs/trunk/main` y `refs/trunk/history`, identifica cada cambio mediante un `Change-Id` con patchsets inmutables y fusiona árboles en memoria con `git merge-tree --write-tree`. El coste de incorporación crece con los directorios que necesita una tarea, y el coste del landing y de la sincronización con las réplicas crece con el tamaño de un cambio, no con el tamaño del repositorio.
