---
id: 773766eac9e6ff3c46bd686d78539aed
author: Lemon Mint
title: 'Regândirea monorepo-ului Git: o arhitectură CAS-Native pentru oameni și Coding Agents'
description: Cum să construiești un monorepo zero-daemon, CAS-native, pe Git standard, folosind parentless trunk snapshots, isolated workspace references, in-memory merge trees, hierarchical OWNERS și O(delta) mirror synchronization.
language: ro
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
---

Fiecare organizație de inginerie în creștere alege, în cele din urmă, între multe depozite și unul singur. Începi cu zeci de depozite mici, iar viața pare simplă la început. Fiecare bibliotecă are propriul issue tracker, propriul pipeline CI și un `git clone` care se termină în câteva secunde. Apoi modifici o structură dintr-un pachet de rețea de bază și îți petreci restul după-amiezii deschizând pull request-uri de actualizare a versiunii în șase depozite dependente, așteptând ca cozile CI să se golească în ordine topologică.

Muți totul într-un monorepo Git tradițional și schimbi coordonarea dintre depozite pe dimensiunea depozitului. Pe măsură ce istoricul commit-urilor se adâncește și activele binare se acumulează în reviziile anterioare, `git fetch` încetinește. Directoarele de lucru ajung la gigabytes. Curând, cineva propune instalarea unui daemon personalizat de sistem de fișiere virtual, a unui server dedicat de code review sau a unei flote de workeri de build la distanță, doar pentru a face dezvoltarea zilnică suportabilă.

Când agenți de programare autonomi se alătură echipei, ambele moduri de defectare se agravează. Polirepo-urile orbesc agentul față de apelanții din alte proiecte și îl obligă să jongleze cu credențialele între depozite. Monorepo-urile tradiționale epuizează discul containerului agentului și lățimea de bandă a rețelei la fiecare inițializare a unei sarcini, iar agenții concurenți care calcă pe aceeași ramură creează curse nesfârșite de rebase.

Inspirați de arhitectura `google3`, am construit modificări atomice pe un singur trunk, onboarding sparse în sub o secundă, guvernanță ierarhică prin `OWNERS` și oglinzi open-source independente, folosind plumbing Git standard și stocare de obiecte de larg consum.

Designul nu necesită nici un daemon personalizat de sistem de fișiere, nici un aparat terț de review, deoarece tratează Git ca pe un Content-Addressable Store (CAS) bazat pe arbore Merkle.

## Concepția greșită din centrul monorepo-urilor Git lente

Când dezvoltatorii se plâng că Git nu scalează la monorepo-uri mari, rareori ating o limită a bazei de date de obiecte a Git. În `.git/objects`, Git stochează trei primitive imuabile, indexate după hash-ul de conținut SHA-1 (sau SHA-256):

1. **`blob`**: Conținutul brut în octeți al unui singur fișier.
2. **`tree`**: O listare de director care asociază nume de fișiere și permisiuni cu hash-uri de `blob` sau de `tree` copil.
3. **`commit`**: Un antet text mic (de obicei între 200 și 300 de octeți) care indică un singur hash de `tree` rădăcină, zero sau mai multe hash-uri de `commit` părinte și metadate despre autor.

Dacă un monorepo conține 50.000 de fișiere și editezi o singură linie dintr-un fișier, noul commit reutilizează hash-urile exacte ale `blob`-urilor și `tree`-urilor pentru celelalte 49.999 de fișiere.

Un monorepo Git tradițional pare greoi deoarece convenția încarcă o singură referință, `refs/heads/main`, cu trei sarcini conflictuale:

- Deține **întregul arbore de directoare unificat** al fiecărui proiect.
- Târăște după sine **întregul lanț de commit-uri istorice legate de părinți** până în prima zi.
- Se află în `refs/heads/*`, spațiul de nume pe care serverele Git îl anunță și îl negociază implicit la fiecare `git clone` și `git fetch`.

De fiecare dată când un dezvoltator sau un runner CI face fetch la `refs/heads/main`, clientul și serverul negociază accesibilitatea în graful de commit-uri peste mii de revizii istorice. Dacă cineva a comis accidental o arhivă de 100 MB acum trei ani și a șters-o a doua zi, `blob`-ul ei încă atârnă de un `tree` istoric accesibil din `refs/heads/main`.

## Separarea spațiului de nume al referințelor

Protocolul de rețea al Git anunță doar `refs/heads/*` și `refs/tags/*` în timpul unui clone standard. Referințele din spații de nume personalizate, precum `refs/trunk/*` sau `refs/ws/*`, trăiesc în exact același depozit de obiecte de pe server, beneficiază de aceeași compresie delta din packfile și pot fi descărcate sau trimise explicit, dar nu impun nicio suprapunere asupra clonărilor implicite.

Separarea acestor sarcini între referințe distincte dintr-un singur depozit elimină blocajul:

```
refs/heads/main               Doar plan de control (~20 KB: reguli, .gitconfig, configurație de sincronizare, scripturi)
refs/trunk/main               Snapshot de commit fără părinte, care indică arborele rădăcină unificat cel mai recent
refs/trunk/history            Lanț liniar de commit-uri legate de părinți, care partajează exact SHA-1-ul arborelui rădăcină
refs/ws/<actor>/<id>          Snapshot-uri izolate de spațiu de lucru (changelist-uri) pentru fiecare dezvoltator sau agent
refs/ws/cl/<change-id>/<tip>  Patchset-uri imuabile: fiecare vârf al unei modificări trimise la review
refs/heads/land/<id>          Declanșator efemer al cozii de merge, șters imediat după integrare
```

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

Ramura implicită nu conține cod sursă al aplicației. Ea urmărește doar regulile de funcționare ale depozitului (`AGENTS.md`, `README.md`), un `.gitconfig` partajat cu alias-uri de workflow, configurația Starlark de sincronizare (`sync.star`) și scripturile de merge de pe server. Clonarea ramurii implicite transferă aproximativ 20 KB și se termină în milisecunde.

### 2. Snapshot-ul trunk fără părinte (`refs/trunk/main`)

`refs/trunk/main` indică spre arborele sursă complet și unificat al întregii organizații (`//web/*`, `//net/*`, `//ipc/*`, `//storage/*`, `//crypto/*`, `//ai/*`, `//devtools/*`, `//corp/*`, `//third_party/*`, `//experimental/*`), dar obiectul său de commit este creat **fără flag-ul de părinte `-p`**.

Deoarece `refs/trunk/main` are zero părinți (`parents = []`), descărcarea sa nu necesită nicio negociere a grafului de commit-uri. Serverul Git nu parcurge istoricul; trimite singurul commit de vârf și obiectele `tree` ale directoarelor sale curente. Binarele istorice sau fișierele șterse în luna precedentă nu sunt accesibile din `refs/trunk/main` și nu sunt niciodată transferate.

### 3. Istoricul liniar de audit (`refs/trunk/history`)

Dezvoltatorii au totuși nevoie de `git blame` și `git log` atunci când investighează regresii. De fiecare dată când o modificare este integrată, scriptul de merge creează două obiecte commit care indică exact același hash SHA-1 `$NEW_TREE` al arborelui rădăcină:

```bash
# Commit snapshot fără părinte, pentru checkout-uri instantanee de workspace la adâncimea 1
SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")

# Commit legat de părinte, pentru git log liniar și 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"
```

Păstrarea atât a lui `refs/trunk/main`, cât și a lui `refs/trunk/history` nu dublează amprenta de stocare. Deoarece ambele commit-uri referă același hash `$NEW_TREE`, Content-Addressable Store al Git deduplică 100% din obiectele `tree` și `blob`. Costul suplimentar de stocare al snapshot-ului fără părinte este un singur antet de commit de 250 de octeți.

### 4. Spații de lucru izolate (`refs/ws/<actor>/<id>`)

Ramurile de lucru în curs sunt trimise către `refs/ws/<actor>/<id>`, nu către `refs/heads/*`. Sute de agenți de programare pot salva checkpoint-uri la fiecare minut fără a polua meniurile de ramuri sau a încetini `git fetch` pentru inginerii umani.

## Onboarding în patru pași: cum funcționează checkout-ul sparse și blobless

Cu această structură de referințe, un dezvoltator nou sau un container efemer al unui agent de programare nu instalează un CLI personalizat sau un driver FUSE. Onboarding-ul durează patru pași, cu Git standard și alias-urile depozitului:

```bash
# 1. Clonează planul de control de 20 KB, cu filtrare de blob-uri și modul sparse activat
git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
cd monorepo

# 2. Încarcă alias-urile depozitului și atașează refs/trunk/*
git config include.path ../.gitconfig
git trunk-init

# 3. Declară directoarele de domeniu necesare pentru această sarcină
git sparse-checkout set net/relay ipc/codec

# 4. Creează un spațiu de lucru izolat, ancorat la cel mai recent snapshot trunk
git ws-new alice/optimize-framing
```

Fiecare pas descarcă doar ceea ce are nevoie:

1. `git clone --filter=blob:none --sparse` descarcă planul de control de 20 KB de pe `refs/heads/main`.
2. `git trunk-init` configurează `+refs/trunk/*:refs/remotes/origin/trunk/*` și rulează un fetch fără blob-uri. Git descarcă metadatele `tree` pentru `refs/trunk/main` (de obicei câteva zeci de kilobytes) și **zero blob-uri de fișiere**.
3. `git sparse-checkout set net/relay ipc/codec` îi indică filtrului de index în mod cone al Git să materializeze doar directorul rădăcină și cei doi subarbori.
4. `git ws-new` comută la o ramură nouă ancorată la `origin/trunk/main`. Apoi Git cere la cerere doar obiectele `blob` din `net/relay` și `ipc/codec`. Restul de 95% din monorepo nu este niciodată descărcat pe disc.

### Perspectiva viitoare: montarea `refs/trunk/main` ca sistem de fișiere virtual (VFS)

Checkout-ul sparse cu clonări fără blob-uri acoperă fluxurile zilnice fără unelte suplimentare pe client. Deoarece `refs/trunk/main` este deja un arbore Merkle pur într-un Content-Addressable Store, arhitectura oferă și o bază directă pentru un sistem de fișiere virtual (VFS) opțional, în spațiul utilizatorului, prin FUSE pe Linux, FileProvider pe macOS sau ProjFS pe Windows:

1. **Montare instantanee a arborelui complet**: Un daemon local ușor de montare citește obiectul `tree` rădăcină al lui `refs/trunk/main` și proiectează întreaga ierarhie de directoare a monorepo-ului în directorul de lucru în milisecunde, eliminând necesitatea rulării `git sparse-checkout set`.
2. **Încărcarea blob-urilor la cerere**: Conținutul fișierelor rămâne virtual până când un compilator, un IDE sau un agent emite un apel de sistem `open(2)` sau `read(2)`. Daemonul preia atunci `blob`-ul SHA-1 corespunzător de la endpoint-ul CAS și îl memorează în cache pe stocarea NVMe locală.
3. **Overlay copy-on-write pentru spații de lucru**: Modificările locale se scriu într-un strat overlay superior ușor. Rularea `git ws-save` calculează hash-ul doar pentru căile modificate din acel overlay, scrie obiectele `blob` și `tree` actualizate și trimite commit-ul rezultat către `refs/ws/<actor>/<id>`, fără a scana directoarele nemodificate.

## Modelul changelist pe Git pur: integrare pe server, în memorie

Într-un flux Git tradițional, integrarea unei ramuri de funcționalitate fie creează un commit de merge cu doi părinți, care face istoricul nelinear, fie obligă dezvoltatorii să facă rebase manual și să concureze cu push-uri concurente către `main`.

Inspirat de modelul Changelist (CL) al `google3`, fiecare spațiu de lucru din arhitectura noastră reprezintă o **modificare atomică unică** față de snapshot-ul trunk. În timp ce lucrează local în `ws/alice/optimize-framing`, un dezvoltator sau un agent poate face zece commit-uri incrementale (`wip`, `fix tests`, `lint`). Când modificarea este gata de integrare, serverul comprimă delta spațiului de lucru într-o singură revizie liniară de trunk, fără a face niciodată checkout unui director de lucru pe runner-ul CI.

Iată cum se execută în memorie scriptul de integrare (`land.sh`), folosind plumbing-ul Git 2.40+:

```bash
# 1. Identifică commit-ul snapshot rădăcină din care a fost ramificat spațiul de lucru
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. Calculează arborele rădăcină combinat în memorie, fără checkout al unui director de lucru
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
```

În pasul 2:

- Indiferent câte commit-uri locale au fost făcute pe ramura spațiului de lucru, scriptul compară doar trei hash-uri de arbore: snapshot-ul de bază original (`$BASE_TREE`), snapshot-ul trunk curent (`$MAIN_TREE`) și arborele final al spațiului de lucru (`$WORK_TREE`).
- Dacă `refs/trunk/main` a avansat în `//web/blog` în timp ce Alice edita `//net/relay`, `git merge-tree --write-tree` calculează direct în interiorul depozitului de obiecte Git arborele rădăcină combinat `$NEW_TREE`, în milisecunde, fără a scrie fișiere pe sistemul de fișiere al runner-ului.
- După ce verificările de politică de mai jos trec, un singur `git push --atomic` cu `--force-with-lease` actualizează `refs/trunk/main` și adaugă un singur commit liniar la `refs/trunk/history`, iar declanșatorul de integrare `refs/heads/land/<id>` este șters.

## Aplicarea granițelor arhitecturale: vizibilitate pe patru niveluri și `OWNERS` ierarhic

Consolidarea codului într-un singur depozit funcționează doar dacă granițele dintre domenii sunt aplicate mecanic înainte ca un commit să poată fi integrat. Imediat după ce `git merge-tree --write-tree` produce `$NEW_TREE` (și înainte ca `git push --atomic` să actualizeze trunk-ul), motorul nostru de politici rulează două verificări O(delta) asupra `git diff-tree "$MAIN_TREE" "$NEW_TREE"`.

### 1. Vizibilitatea domeniilor pe patru niveluri

Arborele rădăcină este partiționat în patru niveluri de vizibilitate:

| Prefix de director                                                                     | Nivel               | Importuri permise                                | Importuri interzise            | Poartă `OWNERS`        |
| :------------------------------------------------------------------------------------- | :------------------ | :----------------------------------------------- | :----------------------------- | :--------------------- |
| `//web/*`, `//net/*`, `//ipc/*`, `//storage/*`, `//crypto/*`, `//ai/*`, `//devtools/*` | Producție publică   | Producție publică, `//third_party/*`             | `//corp/*`, `//experimental/*` | Aplicată               |
| `//third_party/*`                                                                      | Furnizor extern     | Autonome sau `//third_party/*`                   | Toate domeniile non-`third_party` | Aplicată            |
| `//corp/*`                                                                             | Privat intern       | Producție publică, `//corp/*`, `//third_party/*` | `//experimental/*`             | Aplicată               |
| `//experimental/<actor>/*`                                                             | Sandbox             | Orice pachet non-`corp`                          | `//corp/*`                     | Exceptată (aprobată automat) |

Dacă un pachet public din `//web/*` încearcă să importe un pachet intern din `//corp/*` sau un prototip neabsolvit din `//experimental/*`, job-ul de integrare se oprește înainte de a atinge `refs/trunk/main`.

### 2. Verificarea ierarhică a `OWNERS` fără un server Gerrit

Verificatorul aplică fișiere `OWNERS` la nivel de director, cu reguli de moștenire:

- Pentru fiecare cale modificată între `$MAIN_TREE` și `$NEW_TREE`, verificatorul urcă din directorul fișierului până la `//OWNERS`.
- Orice fișier `OWNERS` poate declara `set noparent`, pentru a bloca aprobările moștenite din directoarele părinte, sau `per-file *.sql=@dba-team`, pentru a limita aprobările după un glob de nume de fișier.
- **Invariantul anti-manipulare**: Fișierele `OWNERS` existente sunt întotdeauna citite din `$MAIN_TREE` (arborele trunk curent), niciodată din `$NEW_TREE`. Un autor neaprobat nu își poate acorda singur permisiunea editând `OWNERS` în același commit de spațiu de lucru.
- **Exceptarea sandbox**: Orice cale sub `//experimental/<actor>/*` ocolește verificările `OWNERS`, astfel încât inginerii și agenții să poată prototipa liber cu `git ws-exp <actor> <project>` și să promoveze ulterior pachetul într-un domeniu de producție cu `git ws-graduate`.

## Review fără fricțiune: PR-uri efemere și diff-uri de snapshot

Deoarece `refs/heads/main` deține doar planul de control, iar `refs/trunk/main` este un commit fără părinte, deschiderea directă a unui Pull Request standard GitHub dintr-o ramură de spațiu de lucru către `main` ar încerca să compare istorii fără legătură. Am rezolvat această problemă prin trei fluxuri de review adaptate diferitelor tipuri de modificări:

1. **Diff-uri instantanee two-dot pentru spațiul de lucru (`git ws-save` + Edge Diff Router)**
   Trimiterea unui spațiu de lucru cu `git ws-save` expune `refs/ws/<actor>/<id>` printr-un worker edge ușor. Acest worker rezolvă `refs/trunk/main` și referința spațiului de lucru la SHA-urile lor de commit și redirecționează către vizualizarea de comparare two-dot a GitHub (`/compare/<trunk-sha>..<ws-sha>`). Pentru patch-uri mici sau modificări generate de agenți, reviewerii pot inspecta diff-ul și pot da click pe **Land** direct în interfața web.
2. **Pull Request-uri GitHub efemere (`git ws-pr`)**
   Pentru review linie cu linie pe GitHub, `git ws-pr` trimite commit-ul snapshot rădăcină (`$(git rev-list --max-parents=0 HEAD)`) către `refs/heads/base/<id>` și vârful spațiului de lucru către `refs/heads/review/<id>`. Apoi deschide un Pull Request GitHub care compară `review/<id>` cu `base/<id>`. Deoarece ramura de bază este snapshot-ul din care a fost ramificat spațiul de lucru, tab-ul **Files changed** al GitHub arată doar delta spațiului de lucru. Când reviewerul dă click pe **Merge pull request**, un webhook promovează commit-ul head la `refs/heads/land/<id>`, rulează pipeline-ul de integrare în memorie și șterge automat atât `refs/heads/base/<id>`, cât și `refs/heads/review/<id>`.
3. **Pull Request-uri pe oglinzi externe**
   Contribuitorii open-source nu trebuie să învețe niciodată fluxul de lucru al monorepo-ului. Ei fac fork și deschid pull request-uri către depozitele oglindă independente, ca de obicei.

### Modificări, patchset-uri și transferul aprobărilor

Un nume de ramură nu poate identifica o modificare. Un agent își poate redenumi ramura, modifica vârful sau face rebase pe un trunk mai nou de mai multe ori înainte de integrare. Ca în Gerrit, fluxul identifică o modificare printr-un trailer `Change-Id: I<40 hex>` din mesajul commit-ului de vârf. `git ws-save`, `git ws-pr` și `git ws-land` adaugă trailer-ul atunci când vârful nu îl are și îl păstrează în caz contrar, astfel încât identificatorul supraviețuiește amend-urilor, rebase-urilor și redenumirilor. Commit-ul trunk integrat păstrează trailer-ul și înregistrează numărul pull request-ului său de review, astfel încât `git trunk-cl <number|Change-Id>` îl găsește în `refs/trunk/history`.

Fiecare vârf trimis la review devine un patchset. `git ws-pr` îl trimite către referința imuabilă `refs/ws/cl/<change-id>/<tip>`, lângă ramurile de review. Rularea din nou a `git ws-pr` după un amend sau un `git ws-sync` adaugă un patchset la același pull request. `git ws-ps` listează patchset-urile modificării curente, iar `git ws-psdiff <a> <b>` afișează interdiff-ul dintre două dintre ele, astfel încât reviewerul citește doar ce s-a schimbat de la ultima rundă.

GitHub leagă o aprobare de un singur commit, deci fiecare rebase ar necesita în mod normal un review nou. Pipeline-ul de integrare acceptă și o aprobare acordată altui patchset al aceleiași modificări, atunci când ambele patchset-uri aduc același patch față de propriul snapshot de bază. Compară `git patch-id --verbatim` peste întregul diff binar. Un rebase curat pe un trunk mai nou se integrează fără un nou review, în timp ce orice modificare de conținut, inclusiv o modificare de spații sau de mod al fișierului, necesită o nouă aprobare.

## Sincronizare bidirecțională O(delta) și ramuri de producție separate

Multe organizații publică pachete individuale ca depozite GitHub independente, astfel încât utilizatorii externi să poată rula `go get`, `cargo install` sau să facă fork unui singur proiect fără a atinge monorepo-ul. Unele proiecte, precum blogurile statice sau aplicațiile web cu porți de lansare, mențin și o ramură separată `production` alături de `main`.

Motorul nostru de sincronizare Starlark gestionează atât exporturile, cât și contribuțiile venite din exterior, în timp **O(delta)**, proporțional cu fișierele modificate într-un commit și nu cu dimensiunea monorepo-ului:

- **Splicing de index fără blob-uri**: Atunci când inserează sau extrage subarborele unui depozit oglindă din arborele rădăcină al monorepo-ului, motorul setează `GIT_NO_LAZY_FETCH=1` și rulează `git update-index --index-info`, urmat de `git write-tree --missing-ok`. Subarborii și blob-urile nemodificate rămân referite prin SHA-1 și nu sunt niciodată descărcate pe runner-ul CI.
- **Prefetch de blob-uri doar pentru delta**: Înainte de a trimite un commit exportat către o oglindă independentă, scriptul rulează `git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree"` și preia, prin `git cat-file --batch`, doar blob-urile vechi și noi ale fișierelor modificate. Git poate apoi calcula un pachet delta subțire și îl poate trimite către depozitul oglindă în câteva secunde.
- **Păstrarea workflow-urilor și a ramurilor de lansare**: În timpul exportului, sincronizatorul păstrează directorul `.github/workflows/` propriu al fiecărui depozit independent, astfel încât runner-ele CI publice să ruleze testele proiectului pe oglindă. Fiecare workflow din configurația Starlark vizează o ramură explicită, precum `main`, ceea ce păstrează porțile de lansare pentru depozitele care se implementează dintr-o ramură `production` separată. Integrările în monorepo se sincronizează cu ramura `main` a oglinzii, iar commit-urile automate de metadate sau de traducere din `main` se sincronizează înapoi în `refs/trunk/main` prin webhook. Promovarea `main` către `production` rămâne un pas explicit de lansare.

## Gânduri finale

Această arhitectură tratează depozitul de obiecte Git ca pe un Content-Addressable Store, separă `refs/heads/main` de `refs/trunk/main` și `refs/trunk/history`, identifică fiecare modificare printr-un `Change-Id` cu patchset-uri imuabile și îmbină arborii în memorie cu `git merge-tree --write-tree`. Costul onboarding-ului crește odată cu directoarele de care are nevoie o sarcină, iar costul integrării și al sincronizării oglinzii crește cu dimensiunea unei modificări, nu cu dimensiunea depozitului.
