Wie wir mehrere Coding-Agenten mit Git-Worktrees an einem Repository arbeiten lassen
An den meisten Tagen arbeiten mehrere Coding-Agenten gleichzeitig in unserem Repository. Einer baut ein Feature, ein anderer prüft einen anderen Branch, ein dritter repariert einen fehlschlagenden Test. Ein normaler Checkout hat immer nur einen Branch, und wer einem Agenten mitten im Build den Branch wechselt, macht seine Arbeit kaputt. Git-Worktrees lösen das: Jeder Agent bekommt seinen eigenen Ordner mit seinem eigenen Branch, und alle Ordner teilen sich ein Repository.
Dieser Beitrag erklärt, was ein Worktree teilt und was er für sich behält, welche Befehle wir jeden Tag nutzen, wie wir Agenten Worktrees geben und welche vier Fallen uns der geteilte Zustand gestellt hat, jeweils mit der Regel, die sie beseitigt hat.
Was ein Worktree ist
Ein Worktree ist ein Ordner mit einem ausgecheckten Commit, der zu einem bestehenden Repository gehört. Der Ordner, den git clone oder git init anlegt, ist der Haupt-Worktree; git worktree add legt daneben verknüpfte Worktrees an. Ein verknüpfter Worktree teilt alles mit dem Repository bis auf ein paar eigene Dateien: seinen HEAD, seinen Index und die Dateien auf der Festplatte.
In der Praxis:
- Gemeinsam: jedes Objekt, jede Referenz unter
refs/(Branches, Tags, Remote-Tracking-Branches und der Stash, also die Referenzrefs/stash), die Konfigurationsdatei und die Hooks. - Pro Worktree: HEAD, der Index, die ausgecheckten Dateien und ein paar Referenzen, die Git absichtlich getrennt hält (
refs/bisect,refs/worktree,refs/rewritten). - Ein Branch, ein Worktree: Git weigert sich, einen Branch auszuchecken, den schon ein anderer Worktree hat, außer du erzwingst es. Ein Commit lässt sich mit Detached HEAD in beliebig vielen Worktrees auschecken.
Ein Commit, der in einem Worktree entsteht, ist also sofort in allen anderen sichtbar, ohne Fetch. Ein Branch, der in einem angelegt wird, ist in allen da. Das macht Worktrees billig: Kopiert werden nur die Dateien, an denen du arbeitest.
Die Befehle, die wir nutzen
git worktree add -b 123-export-csv ../work/123-export-csv origin/master
git worktree add --detach ../work/review-123 <commit>
git worktree list
git worktree remove ../work/123-export-csv
git worktree prune
Die erste Zeile legt einen neuen Branch vom neuesten Haupt-Branch auf dem Remote an und checkt ihn in einem neuen Ordner aus. Die zweite checkt einen Commit ohne Branch aus, für ein Review oder einen Testlauf, der nichts bewegen darf. list zeigt jeden Worktree mit seinem Commit und Branch. remove löscht den Ordner und die Einträge, die Git dazu führt, und verweigert das, solange der Ordner Änderungen oder unversionierte Dateien hat oder Submodule enthält; --force setzt sich darüber hinweg. prune räumt die Einträge zu Worktrees auf, deren Ordner von Hand gelöscht wurden.
Wie wir Agenten Worktrees geben
Jede Aufgabe bekommt einen Worktree, angelegt von dem Agenten, der die Arbeit macht:
- Eine Aufgabe, ein Worktree, ein Branch. Der Branch ist nach dem Issue der Aufgabe benannt, und der Worktree startet vom neuesten Haupt-Branch auf dem Remote, sodass jeder Agent am selben Punkt beginnt, egal was anderswo ausgecheckt ist.
- Im Projekt, in einem von Git ignorierten Ordner. Die Worktrees liegen in einem Ordner des Haupt-Checkouts, den Git ignoriert, sodass ihre Dateien und Logs im Editor einen Klick entfernt sind und im Haupt-Checkout nie als Änderungen auftauchen.
- Ein Reviewer bekommt einen eigenen Worktree mit Detached HEAD, genau am Commit, der geprüft wird. Wer implementiert, kann weiter auf dem Branch arbeiten, während das Review läuft, und das Review liest einen Commit, der sich unter ihm nicht bewegen kann.
- Absolute Pfade statt
cd. Ein Agent kann in einer Sitzung mehrere Shell-Befehle parallel ausführen, und sie teilen sich ein Arbeitsverzeichnis: Eincdin einem verschiebt auch die anderen. Unsere Agenten schreiben jeden Pfad vollständig aus oder nutzengit -C <folder>, damit jeder Befehl in dem Worktree wirkt, den er meint. - Rebase vor jedem Push. Der Branch wird vor jedem Push auf den neuesten Haupt-Branch auf dem Remote rebased und vor dem Öffnen seines Pull Requests per Squash zu einem Commit zusammengefasst, sodass ein Merge genau einen Commit zur Historie hinzufügt.
- Aufräumen nach dem Merge. Sobald die Änderung gemergt und geprüft ist, entfernt der Agent den Worktree, löscht den Branch lokal und auf dem Remote, führt
git worktree pruneaus und prüft, dass keins von beiden noch aufgeführt ist.
Vier Fallen und die Regel, die jede beseitigt hat
Alles, was ein Worktree teilt, teilt er gleichzeitig mit jedem Agenten. Jede Falle unten hat uns einen kaputten Lauf gekostet, bevor wir sie gefunden haben.
Der Stash ist ein Stapel für alle
git stash legt Einträge auf refs/stash ab, und diese Referenz ist gemeinsam. Ein Agent hat seine laufende Arbeit in den Stash gelegt; ein anderer Agent hat in einem anderen Worktree git stash pop ausgeführt und diesen Eintrag genommen. Die Änderungen des ersten Agenten waren aus seinem Ordner verschwunden und kamen nur über git fsck --unreachable zurück.
Die Regel: Agenten nutzen nie git stash. Unfertige Arbeit wird als Commit auf dem eigenen Branch des Agenten geparkt, den kein anderer Worktree nehmen kann.
Auch die Konfiguration ist gemeinsam
Die Einstellungen der Submodule stehen in der Konfiguration des Repositorys, die sich alle Worktrees teilen. Ein Agent, der beim Entfernen seines Worktrees git submodule deinit ausgeführt hat, hat das Submodul für den Haupt-Checkout und jeden anderen Worktree abgemeldet, und der Build ist in allen fehlgeschlagen.
Die Regeln: Ein Worktree mit Submodulen wird allein mit git worktree remove --force entfernt, nie nach einem deinit; und ein frischer Worktree führt vor seinem ersten Build git submodule update --init aus, weil ein neuer Worktree mit leeren Submodul-Ordnern startet.
Abhängigkeiten nicht über einen Symlink teilen
Abhängigkeiten in jedem neuen Worktree zu installieren kostet Zeit, und ein Symlink auf das node_modules eines anderen Checkouts sieht nach einer Abkürzung aus. Das ist er nicht: npm installiert durch den Symlink hindurch und entfernt die Pakete, die es nicht erwartet, aus dem Checkout, auf den der Link zeigt, und hat so den Testlauf eines anderen Agenten mittendrin kaputt gemacht.
Die Regeln: Jeder Worktree installiert seine eigenen Abhängigkeiten. Wenn eine Kopie Zeit spart, klonen wir den Ordner mit Copy-on-Write (cp -Rc auf APFS von macOS, das die Blöcke auf der Festplatte teilt, bis sich eine Datei ändert) und leeren jeden Cache, der absolute Pfade speichert, etwa den Transform-Cache von Playwright, weil er sonst weiter auf den anderen Checkout zeigt.
Werkzeuge, die sich den Ordner merken, in dem sie liefen
Manche Werkzeuge schreiben den Pfad, aus dem sie gestartet wurden, in ihre Ausgabe. Unsere Infrastruktur-Pläne zeigten, aus einem Worktree ausgeführt, Änderungen, die nur dieser Pfad waren, keine echten Änderungen an irgendetwas.
Die Regel: Infrastruktur-Änderungen werden nur aus dem Haupt-Checkout angewendet, nachdem die Änderung gemergt ist, nie aus einem Worktree; ein Plan, der in einem Worktree läuft, ist nur zum Lesen da.
Was es kostet
Worktrees sind billig in der Historie, nicht auf der Festplatte oder bei der CPU. Jeder hat einen vollständigen Satz ausgecheckter Dateien und, bei einer Web-App, seine eigenen Abhängigkeiten, sodass ein Dutzend Worktrees spürbar Platz braucht. Builds teilen sich dieselben Prozessoren, also lassen wir immer nur einen schweren Build gleichzeitig laufen. Und ein Worktree, der nie entfernt wird, hält seinen Branch ausgecheckt und blockiert ihn anderswo; deshalb gehört das Aufräumen zur Aufgabe und kommt nicht erst hinterher.
Häufige Fragen
Was teilt ein Git-Worktree mit dem Haupt-Checkout?
Fast alles. Die Objekte, die Branches und Tags, der Stash und die Konfiguration sind gemeinsam. Jeder Worktree hat seinen eigenen HEAD, seinen eigenen Index und seine eigenen Dateien auf der Festplatte, sodass in jedem Ordner gleichzeitig ein anderer Branch ausgecheckt sein kann.
Können zwei Worktrees denselben Branch auschecken?
Nein. Git weigert sich, einen Branch auszuchecken, den schon ein anderer Worktree ausgecheckt hat, es sei denn, du erzwingst es. Ein Worktree kann stattdessen einen Commit mit Detached HEAD auschecken, und so geben wir einem Reviewer genau den Commit, der geprüft wird.
Warum sollten Agenten in einem Worktree kein git stash nutzen?
Der Stash ist eine einzige Referenz, refs/stash, die sich alle Worktrees des Repositorys teilen. Ein Agent, der den Stash mit pop zurückholt, kann den Eintrag eines anderen Agenten nehmen. Wir parken unfertige Arbeit stattdessen als Commit auf dem eigenen Branch des Agenten.
Wie räumst du einen Worktree nach einem Merge auf?
Entferne ihn mit git worktree remove, lösche den Branch lokal und auf dem Remote, führe git worktree prune aus und prüfe, dass weder der Branch noch der Worktree noch aufgeführt ist.