GoSuda

Przemyślenie monorepo Git: architektura natywna dla CAS dla ludzi i agentów kodujących

By Lemon Mint views ...

Każda rozwijająca się organizacja inżynierska prędzej czy później staje przed wyborem między wieloma repozytoriami a jednym. Na początku, gdy istnieje kilkadziesiąt małych repozytoriów, życie wydaje się proste. Każda biblioteka ma własny tracker zgłoszeń, własny potok CI oraz polecenie git clone, które kończy się w ciągu kilku sekund. Potem zmieniasz strukturę w kluczowym pakiecie sieciowym i spędzasz resztę popołudnia na otwieraniu pull requestów z podbiciem wersji w sześciu zależnych repozytoriach, czekając, aż kolejki CI opróżnią się w kolejności topologicznej.

Jeśli przeniesiesz wszystko do tradycyjnego monorepozytorium Git, wymienisz koordynację między repozytoriami na rozmiar repozytorium. Wraz z pogłębianiem się historii commitów i gromadzeniem się binarnych zasobów w poprzednich rewizjach git fetch zaczyna zwalniać. Drzewa robocze puchną do gigabajtów. Wkrótce ktoś zaproponuje uruchomienie niestandardowego demona wirtualnego systemu plików, dedykowanego serwera do przeglądu kodu lub floty zdalnych workerów budowania, wyłącznie po to, by codzienna praca pozostała znośna.

Gdy do zespołu dołączają autonomiczne agenty programistyczne, oba tryby awarii się nasilają. Polirepozytoria pozbawiają agenta wiedzy o wywołaniach z innych projektów i zmuszają go do jongllowania poświadczeniami między repozytoriami. Tradycyjne monorepozytoria z kolei pochłaniają dysk kontenera agenta i przepustowość sieci przy każdej inicjalizacji zadania, a współbieżni agenci pracujący na tej samej gałęzi tworzą niekończące się wyścigi przy rebase.

Inspirując się architekturą google3, zbudowaliśmy atomowe zmiany na jednym pniu, wdrażanie sparse trwające poniżej sekundy, hierarchiczne zarządzanie OWNERS oraz samodzielne otwarte lustra oparte na standardowej infrastrukturze Git i tanim magazynie obiektów.

Projekt nie wymaga ani niestandardowego demona systemu plików, ani zewnętrznego urządzenia do przeglądu kodu, ponieważ traktuje Git jako Content-Addressable Store (CAS) oparty na drzewie Merkle.

Błędne przekonanie u podstaw powolnych monorepozytoriów Git

Gdy programiści narzekają, że Git nie skaluje się do dużych monorepozytoriów, rzadko trafiają na ograniczenie bazy obiektów Git. W katalogu .git/objects Git przechowuje trzy niezmienne prymitywy indeksowane skrótem SHA-1 (lub SHA-256) zawartości:

  1. blob: surowa zawartość bajtowa pojedynczego pliku.
  2. tree: listing katalogu, który odwzorowuje nazwy plików i uprawnienia na skróty blob lub podrzędnych tree.
  3. commit: niewielki tekstowy nagłówek (zazwyczaj 200 do 300 bajtów) wskazujący na jeden korzeniowy tree, zero lub więcej rodzicielskich commit oraz metadane autora.

Jeśli monorepozytorium zawiera 50 000 plików, a ty edytujesz jedną linię w jednym pliku, nowy commit ponownie wykorzystuje dokładne skróty blob i tree pozostałych 49 999 plików.

Tradycyjne monorepozytorium Git wydaje się ciężkie, ponieważ konwencja przeciąża pojedyncze odniesienie, refs/heads/main, trzema sprzecznymi zadaniami:

  • Przechowuje całe zunifikowane drzewo katalogów wszystkich projektów.
  • Ciągnie za sobą cały łańcuch historycznych commitów powiązanych przez rodziców sięgający pierwszego dnia.
  • Znajduje się w refs/heads/*, przestrzeni nazw, którą serwery Git domyślnie ogłaszają i negocjują przy każdym git clone oraz git fetch.

Za każdym razem, gdy programista lub runner CI pobiera refs/heads/main, klient i serwer negocjują osiągalność w grafie commitów przez tysiące historycznych rewizji. Jeśli ktoś przypadkowo zacommitował archiwum o rozmiarze 100 MB trzy lata temu i usunął je następnego dnia, jego blob nadal wisi na historycznym tree, osiągalnym z refs/heads/main.

Rozdzielenie przestrzeni nazw referencji

Protokół sieciowy Git ogłasza podczas standardowego klonowania wyłącznie refs/heads/* i refs/tags/*. Referencje w niestandardowych przestrzeniach nazw, takich jak refs/trunk/* czy refs/ws/*, znajdują się w tej samej bazie obiektów na serwerze, korzystają z tej samej kompresji delta w packfile i mogą być pobierane lub wysyłane jawnie, a jednocześnie nie generują żadnego narzutu przy domyślnych klonowaniach.

Rozdzielenie tych zadań między osobne referencje w jednym repozytorium usuwa wąskie gardło:

1refs/heads/main               Tylko płaszczyzna sterowania (~20 KB: reguły, .gitconfig, konfiguracja synchronizacji, skrypty)
2refs/trunk/main               Bezrodzicielski commit migawki wskazujący na najnowsze zunifikowane korzeniowe drzewo
3refs/trunk/history            Liniowy łańcuch commitów powiązanych przez rodziców, współdzielący dokładnie ten sam SHA-1 korzeniowego drzewa
4refs/ws/<actor>/<id>          Izolowane migawki przestrzeni roboczych (listy zmian) dla każdego programisty lub agenta
5refs/ws/cl/<change-id>/<tip>  Niezmienne zestawy poprawek: każdy wierzchołek zmiany przesłany do przeglądu
6refs/heads/land/<id>          Efemeryczny wyzwalacz kolejki scalania, usuwany natychmiast po wylądowaniu

1. Płaszczyzna sterowania (refs/heads/main)

Domyślna gałąź nie zawiera kodu źródłowego aplikacji. Śledzi wyłącznie zasady działania repozytorium (AGENTS.md, README.md), wspólny .gitconfig z aliasami przepływu pracy, konfigurację synchronizacji w Starlark (sync.star) oraz skrypty scalania po stronie serwera. Klonowanie domyślnej gałęzi przesyła około 20 KB i kończy się w ciągu milisekund.

2. Bezrodzicielska migawka pnia (refs/trunk/main)

refs/trunk/main wskazuje na kompletne, zunifikowane drzewo źródeł całej organizacji (//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*, //corp/*, //third_party/*, //experimental/*), jednak jego obiekt commit jest tworzony bez flagi rodzica -p.

Ponieważ refs/trunk/main nie ma rodziców (parents = []), jego pobranie nie wymaga negocjacji grafu commitów. Serwer Git nie przechodzi po historii; wysyła pojedynczy commit wierzchołkowy i jego bieżące obiekty tree katalogów. Historyczne binaria lub pliki usunięte w zeszłym miesiącu nie są osiągalne z refs/trunk/main i nigdy nie są przesyłane.

3. Liniowa historia audytu (refs/trunk/history)

Programiści nadal potrzebują git blame i git log podczas badania regresji. Za każdym razem, gdy zmiana zostaje wylądowana, skrypt scalający tworzy dwa obiekty commit wskazujące na dokładnie ten sam skrót SHA-1 korzeniowego drzewa $NEW_TREE:

 1# Bezrodzicielski commit migawki dla natychmiastowych checkoutów przestrzeni roboczej o głębokości 1
 2SNAP=$(git commit-tree "$NEW_TREE" -m "$RAW_MSG")
 3
 4# Commit powiązany z rodzicem dla liniowego git log i 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"

Utrzymywanie zarówno refs/trunk/main, jak i refs/trunk/history nie podwaja zajmowanej pamięci. Ponieważ oba commity odwołują się do identycznego skrótu $NEW_TREE, magazyn obiektów Git deduplikuje w 100% obiekty tree i blob. Dodatkowym kosztem bezrodzicielskiej migawki jest jedynie nagłówek commitu o rozmiarze 250 bajtów.

4. Izolowane przestrzenie robocze (refs/ws/<actor>/<id>)

Gałęzie prac w toku są wysyłane do refs/ws/<actor>/<id>, a nie do refs/heads/*. Setka agentów programistycznych może zapisywać punkty kontrolne co minutę, nie zaśmiecając list gałęzi i nie spowalniając git fetch dla inżynierów.

Wdrożenie w czterech krokach: jak działa sparse i blobless checkout

Przy takim układzie referencji nowy programista lub efemeryczny kontener agenta programistycznego nie instaluje żadnego niestandardowego CLI ani sterownika FUSE. Wdrożenie składa się z czterech kroków wykonywanych standardowym Gitem i aliasami repozytorium:

 1# 1. Sklonuj 20-kilobajtową płaszczyznę sterowania z filtrowaniem blobów i włączonym trybem sparse
 2git clone --filter=blob:none --sparse https://github.com/example-org/monorepo.git
 3cd monorepo
 4
 5# 2. Załaduj aliasy repozytorium i podłącz refs/trunk/*
 6git config include.path ../.gitconfig
 7git trunk-init
 8
 9# 3. Zadeklaruj katalogi domenowe potrzebne do tego zadania
10git sparse-checkout set net/relay ipc/codec
11
12# 4. Utwórz izolowaną przestrzeń roboczą opartą na najnowszej migawce pnia
13git ws-new alice/optimize-framing

Każdy krok pobiera wyłącznie to, czego potrzebuje:

  1. git clone --filter=blob:none --sparse pobiera 20-kilobajtową płaszczyznę sterowania z refs/heads/main.
  2. git trunk-init konfiguruje +refs/trunk/*:refs/remotes/origin/trunk/* i wykonuje pobranie bez blobów. Git ściąga metadane tree dla refs/trunk/main (zazwyczaj kilkadziesiąt kilobajtów) i zero blobów plików.
  3. git sparse-checkout set net/relay ipc/codec nakazuje indeksowi Git w trybie cone zmaterializować wyłącznie katalog główny oraz te dwa poddrzewa.
  4. git ws-new przełącza się na nową gałąź opartą na origin/trunk/main. Następnie Git żąda na żądanie wyłącznie obiektów blob znajdujących się w net/relay i ipc/codec. Pozostałe 95% monorepozytorium nigdy nie jest pobierane na dysk.

Perspektywa na przyszłość: montowanie refs/trunk/main jako wirtualnego systemu plików (VFS)

Sparse checkout z klonowaniem bez blobów obejmuje codzienne przepływy pracy bez dodatkowych narzędzi klienckich. Ponieważ refs/trunk/main jest już czystym drzewem Merkle w Content-Addressable Store, architektura stanowi również bezpośrednią podstawę dla opcjonalnego wirtualnego systemu plików (VFS) w przestrzeni użytkownika, realizowanego przez FUSE w systemie Linux, FileProvider w systemie macOS lub ProjFS w systemie Windows:

  1. Natychmiastowe montowanie pełnego drzewa: lekki lokalny demon montowania odczytuje korzeniowy obiekt tree z refs/trunk/main i w ciągu milisekund rzutuje całą hierarchię katalogów monorepozytorium do katalogu roboczego, eliminując potrzebę uruchamiania git sparse-checkout set.
  2. Pobieranie blobów na żądanie: zawartość plików pozostaje wirtualna, dopóki kompilator, IDE lub agent nie wywoła wywołania systemowego open(2) lub read(2). Demon pobiera wtedy odpowiedni blob o skrócie SHA-1 z punktu końcowego CAS i zapisuje go w lokalnej pamięci NVMe.
  3. Nakładka copy-on-write dla przestrzeni roboczych: lokalne edycje zapisywane są w lekkiej górnej warstwie nakładki. Uruchomienie git ws-save haszuje wyłącznie zmienione ścieżki w tej nakładce, zapisuje zaktualizowane obiekty blob i tree oraz wysyła powstały commit do refs/ws/<actor>/<id>, bez skanowania niezmodyfikowanych katalogów.

Model listy zmian na czystym Gitcie: scalanie po stronie serwera w pamięci

W tradycyjnym przepływie pracy Git scalenie gałęzi funkcjonalnej albo tworzy merge commit z dwoma rodzicami, co czyni historię nieliniową, albo wymaga od programistów ręcznego rebase i wyścigu z równoległymi wysyłkami do main.

Inspirując się modelem listy zmian (Changelist, CL) z google3, każda przestrzeń robocza w naszej architekturze reprezentuje pojedynczą atomową zmianę względem migawki pnia. Podczas pracy lokalnej w ws/alice/optimize-framing programista lub agent może wykonać dziesięć przyrostowych commitów (wip, fix tests, lint). Gdy zmiana jest gotowa do wylądowania, serwer zwija deltę przestrzeni roboczej do jednej liniowej rewizji pnia, nigdy nie wykonując checkoutu katalogu roboczego na runnerze CI.

Oto sposób, w jaki skrypt lądowania (land.sh) wykonuje się w pamięci, przy użyciu plumbingu Git 2.40+:

 1# 1. Zidentyfikuj korzeniowy commit migawki, od którego odgałęziła się przestrzeń robocza
 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. Oblicz scalone korzeniowe drzewo w pamięci, bez checkoutu drzewa roboczego
 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

W kroku 2:

  • Niezależnie od liczby lokalnych commitów wykonanych na gałęzi przestrzeni roboczej, skrypt porównuje tylko trzy skróty drzew: pierwotną migawkę bazową ($BASE_TREE), bieżącą migawkę pnia ($MAIN_TREE) oraz końcowe drzewo przestrzeni roboczej ($WORK_TREE).
  • Jeśli refs/trunk/main zaktualizowało się w //web/blog, podczas gdy Alice edytowała //net/relay, git merge-tree --write-tree oblicza połączone korzeniowe drzewo $NEW_TREE bezpośrednio w magazynie obiektów Git w ciągu milisekund, bez zapisywania plików w systemie plików runnera.
  • Po pomyślnym przejściu poniższych kontroli polityki jedno polecenie git push --atomic z --force-with-lease aktualizuje refs/trunk/main i dopisuje jeden liniowy commit do refs/trunk/history, a wyzwalacz lądowania refs/heads/land/<id> zostaje usunięty.

Egzekwowanie granic architektury: czterostopniowa widoczność i hierarchiczne OWNERS

Konsolidacja kodu w jednym repozytorium działa wyłącznie wtedy, gdy granice między domenami są egzekwowane mechanicznie, zanim commit zostanie wylądowany. Zaraz po tym, jak git merge-tree --write-tree wygeneruje $NEW_TREE (a przed aktualizacją pnia przez git push --atomic), nasz silnik polityk uruchamia dwa sprawdzenia o złożoności O(delta) nad wynikiem git diff-tree "$MAIN_TREE" "$NEW_TREE".

1. Czterostopniowa widoczność domen

Korzeniowe drzewo jest podzielone na cztery poziomy widoczności:

Prefiks kataloguPoziomDozwolone importyZabronione importyBramka OWNERS
//web/*, //net/*, //ipc/*, //storage/*, //crypto/*, //ai/*, //devtools/*Publiczna produkcjaPubliczna produkcja, //third_party/*//corp/*, //experimental/*Egzekwowana
//third_party/*Zewnętrzny dostawcaSamodzielne lub //third_party/*Wszystkie domeny inne niż third_partyEgzekwowana
//corp/*Wewnętrzna prywatnaPubliczna produkcja, //corp/*, //third_party/*//experimental/*Egzekwowana
//experimental/<actor>/*PiaskownicaDowolny pakiet niebędący corp//corp/*Zwolniona (automatycznie zatwierdzona)

Jeśli publiczny pakiet w //web/* próbuje zaimportować wewnętrzny pakiet z //corp/* lub niedojrzały prototyp z //experimental/*, zadanie lądowania zostaje przerwane, zanim dotknie refs/trunk/main.

2. Hierarchiczna weryfikacja OWNERS bez serwera Gerrit

Weryfikator egzekwuje pliki OWNERS na poziomie katalogów z regułami dziedziczenia:

  • Dla każdej ścieżki zmodyfikowanej między $MAIN_TREE a $NEW_TREE weryfikator przechodzi w górę od katalogu pliku do //OWNERS.
  • Dowolny plik OWNERS może zadeklarować set noparent, aby zablokować dziedziczone zatwierdzenia z katalogów nadrzędnych, lub per-file *.sql=@dba-team, aby zawęzić zatwierdzenia do wzorca nazwy pliku.
  • Niezmiennik przeciwko manipulacji: istniejące pliki OWNERS są zawsze odczytywane z $MAIN_TREE (bieżącego drzewa pnia), nigdy z $NEW_TREE. Nieautoryzowany autor nie może przyznać sobie uprawnień, edytując OWNERS w tym samym commicie przestrzeni roboczej.
  • Wyłączenie dla piaskownicy: każda ścieżka w //experimental/<actor>/* omija kontrole OWNERS, dzięki czemu inżynierowie i agenty mogą swobodnie prototypować za pomocą git ws-exp <actor> <project>, a następnie przenieść pakiet do domeny produkcyjnej poleceniem git ws-graduate.

Przegląd kodu bez tarcia: efemeryczne PR-y i różnice migawek

Ponieważ refs/heads/main zawiera wyłącznie płaszczyznę sterowania, a refs/trunk/main jest bezrodzicielskim commitem, otwarcie standardowego pull requesta w GitHub bezpośrednio z gałęzi przestrzeni roboczej do main próbowałoby porównać niepowiązane historie. Rozwiązaliśmy to, wprowadzając trzy przepływy przeglądu dostosowane do różnych rodzajów zmian:

  1. Natychmiastowe dwukropkowe różnice przestrzeni roboczej (git ws-save + Edge Diff Router) Wypchnięcie przestrzeni roboczej poleceniem git ws-save udostępnia refs/ws/<actor>/<id> przez lekkiego workera brzegowego. Worker ten rozwiązuje refs/trunk/main i referencję przestrzeni roboczej do ich SHA commitów, a następnie przekierowuje do widoku porównania z dwiema kropkami w GitHub (/compare/<trunk-sha>..<ws-sha>). Przy małych poprawkach lub zmianach wygenerowanych przez agenta recenzenci mogą sprawdzić różnice i kliknąć Land bezpośrednio w interfejsie webowym.
  2. Efemeryczne pull requesty w GitHub (git ws-pr) Do przeglądu linia po linii w GitHub polecenie git ws-pr wysyła commit migawki bazowej ($(git rev-list --max-parents=0 HEAD)) do refs/heads/base/<id>, a wierzchołek przestrzeni roboczej do refs/heads/review/<id>. Następnie otwiera pull request w GitHub, porównujący review/<id> z base/<id>. Ponieważ gałąź bazowa jest migawką, z której odgałęziła się przestrzeń robocza, zakładka Files changed w GitHub pokazuje wyłącznie deltę przestrzeni roboczej. Gdy recenzent kliknie Merge pull request, webhook awansuje commit wierzchołkowy do refs/heads/land/<id>, uruchamia potok lądowania w pamięci i automatycznie usuwa zarówno refs/heads/base/<id>, jak i refs/heads/review/<id>.
  3. Pull requesty do zewnętrznych luster Współtwórcy open source nigdy nie muszą poznawać przepływu pracy monorepozytorium. Wykonują fork i otwierają pull requesty do samodzielnych repozytoriów-luster w zwykły sposób.

Zmiany, zestawy poprawek i przenoszenie zatwierdzeń

Nazwa gałęzi nie może identyfikować zmiany. Agent może kilkakrotnie zmienić nazwę gałęzi, poprawić wierzchołek lub wykonać rebase na nowszy pień przed lądowaniem. Podobnie jak w Gerrit, przepływ identyfikuje zmianę za pomocą nagłówka Change-Id: I<40 znaków szesnastkowych> w wiadomości wierzchołkowego commitu. Polecenia git ws-save, git ws-pr i git ws-land dodają ten nagłówek, gdy wierzchołek go nie ma, i zachowują go, gdy już istnieje, dzięki czemu identyfikator przetrwa poprawki, rebase i zmiany nazw. Wylądowany commit pnia zachowuje nagłówek i zapisuje numer swojego pull requesta do przeglądu, dzięki czemu git trunk-cl <number|Change-Id> odnajduje go w refs/trunk/history.

Każdy wierzchołek przesłany do przeglądu staje się zestawem poprawek. Polecenie git ws-pr wysyła go do niezmiennej referencji refs/ws/cl/<change-id>/<tip> obok gałęzi przeglądu. Ponowne uruchomienie git ws-pr po poprawce lub git ws-sync dodaje zestaw poprawek do tego samego pull requesta. Polecenie git ws-ps wypisuje zestawy poprawek bieżącej zmiany, a git ws-psdiff <a> <b> wyświetla interdiff między dwoma z nich, tak aby recenzent czytał wyłącznie to, co zmieniło się od ostatniej rundy.

GitHub wiąże zatwierdzenie z pojedynczym commitem, więc każdy rebase normalnie wymagałby nowego przeglądu. Potok lądowania akceptuje również zatwierdzenie udzielone innemu zestawowi poprawek tej samej zmiany, jeśli oba zestawy wprowadzają tę samą poprawkę względem własnej migawki bazowej. Porównuje on wynik git patch-id --verbatim dla pełnego binarnego diffu. Czysty rebase na nowszy pień zostaje wylądowany bez ponownego przeglądu, natomiast każda edycja treści, także zmiana białych znaków lub trybu pliku, wymaga nowego zatwierdzenia.

Dwukierunkowa synchronizacja O(delta) i oddzielne gałęzie produkcyjne

Wiele organizacji publikuje poszczególne pakiety jako samodzielne repozytoria GitHub, aby zewnętrzni użytkownicy mogli wykonać go get, cargo install lub sforkować pojedynczy projekt bez dotykania monorepozytorium. Niektóre projekty, takie jak statyczne blogi lub aplikacje webowe z bramkami wydań, utrzymują również osobną gałąź production obok main.

Nasz silnik synchronizacji w Starlark obsługuje zarówno eksport wychodzący, jak i wkłady przychodzące w czasie O(delta), proporcjonalnym do liczby plików zmienionych w commicie, a nie do rozmiaru monorepozytorium:

  • Wstawianie indeksu bez blobów: podczas wstawiania poddrzewa repozytorium-lustra do korzeniowego drzewa monorepozytorium lub wyjmowania go z niego silnik ustawia GIT_NO_LAZY_FETCH=1 i uruchamia git update-index --index-info, a następnie git write-tree --missing-ok. Niezmodyfikowane poddrzewa i bloby pozostają referencjonowane przez SHA-1 i nigdy nie są pobierane na runner CI.
  • Wstępne pobieranie blobów wyłącznie dla delty: przed wypchnięciem wyeksportowanego commitu do samodzielnego lustra skrypt uruchamia git diff-tree -r --diff-filter=AM "$ext_parent" "$export_tree" i pobiera wyłącznie stare oraz nowe bloby zmodyfikowanych plików poleceniem git cat-file --batch. Dzięki temu Git może obliczyć cienki pakiet delta i wypchnąć go do repozytorium-lustra w ciągu kilku sekund.
  • Zachowanie gałęzi przepływu pracy i wydań: podczas eksportu wychodzącego synchronizator zachowuje katalog .github/workflows/ każdego samodzielnego repozytorium, dzięki czemu publiczne runnery CI wykonują testy projektu na lustrze. Każdy workflow w konfiguracji Starlark odwołuje się do jawnej gałęzi, na przykład main, co zachowuje bramki wydań dla repozytoriów wdrażanych z osobnej gałęzi production. Lądowania w monorepozytorium synchronizują się z gałęzią main lustra, a zautomatyzowane commity metadanych lub tłumaczeń na main synchronizują się z powrotem do refs/trunk/main przez webhook. Awansowanie main do production pozostaje jawnym krokiem wydania.

Uwagi końcowe

Ta architektura traktuje magazyn obiektów Git jako Content-Addressable Store, oddziela refs/heads/main od refs/trunk/main i refs/trunk/history, identyfikuje każdą zmianę za pomocą Change-Id z niezmiennymi zestawami poprawek i scala drzewa w pamięci za pomocą git merge-tree --write-tree. Koszt wdrożenia rośnie wraz z katalogami, których potrzebuje zadanie, a koszt lądowania i synchronizacji lustra rośnie wraz z rozmiarem zmiany, a nie z rozmiarem repozytorium.