Transferify
Înapoi la blog

Cum rulăm mai mulți agenți de programare pe același repository cu git worktrees

În cele mai multe zile, mai mulți agenți de programare lucrează în același timp în repository-ul nostru. Unul construiește o funcționalitate, altul verifică un alt branch, un al treilea repară un test care pică. Un checkout obișnuit are un singur branch odată, iar schimbarea branch-ului sub un agent aflat la jumătatea unui build îi strică munca. Git worktrees rezolvă asta: fiecare agent primește propriul folder cu propriul branch, iar toate folderele folosesc același repository.

Articolul acesta explică ce are un worktree în comun cu restul și ce păstrează doar pentru el, comenzile pe care le folosim zilnic, cum le dăm agenților worktree-uri și cele patru capcane pe care ni le-a întins starea comună, fiecare cu regula care a eliminat-o.

Un repository și trei worktree-uri. Repository-ul conține obiectele, branch-urile și tag-urile, stash-ul, configurația și hook-urile, comune tuturor. Fiecare worktree, adică checkout-ul principal, branch-ul de lucru al unui agent și commit-ul detașat al unui reviewer, are propriul HEAD, propriul index și propriile fișiere.Un repository și trei worktree-uri. Repository-ul conține obiectele, branch-urile și tag-urile, stash-ul, configurația și hook-urile, comune tuturor. Fiecare worktree, adică checkout-ul principal, branch-ul de lucru al unui agent și commit-ul detașat al unui reviewer, are propriul HEAD, propriul index și propriile fișiere.
Un repository, mai multe worktree-uri. Fiecare folder are propriul commit în checkout și propriile fișiere; istoricul, branch-urile, stash-ul și configurația sunt comune.

Ce este un worktree

Un worktree e un folder cu un commit în checkout care aparține unui repository existent. Folderul creat de git clone sau git init e worktree-ul principal; git worktree add creează lângă el worktree-uri legate. Un worktree legat are totul în comun cu repository-ul, în afară de câteva fișiere proprii: HEAD-ul lui, indexul lui și fișierele de pe disc.

În practică:

  • Comune: fiecare obiect, fiecare ref din refs/ (branch-urile, tag-urile, branch-urile care urmăresc remote-ul și stash-ul, care e ref-ul refs/stash), fișierul de configurare și hook-urile.
  • Pentru fiecare worktree: HEAD, indexul, fișierele din checkout și câteva ref-uri pe care git le ține separat intenționat (refs/bisect, refs/worktree, refs/rewritten).
  • Un branch, un worktree: git refuză să facă checkout la un branch pe care îl are deja un alt worktree, dacă nu e forțat. Un commit poate fi în checkout în oricâte worktree-uri vrei, cu HEAD detașat.

Așa că un commit făcut într-un worktree e vizibil imediat în toate celelalte, fără fetch. Un branch creat într-unul există în toate. De asta worktree-urile costă puțin: nu se copiază nimic în afară de fișierele la care lucrezi.

Comenzile pe care le folosim

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

Prima linie creează un branch nou din cel mai recent branch principal de pe remote și îl pune în checkout într-un folder nou. A doua face checkout la un commit fără branch, pentru un review sau o rulare de teste care nu are voie să miște nimic. list arată fiecare worktree cu commit-ul și branch-ul lui. remove șterge folderul și evidența pe care o ține git despre el și refuză cât timp folderul are modificări sau fișiere neurmărite ori conține submodule; --force trece peste asta. prune șterge evidența git pentru worktree-urile ale căror foldere au fost șterse manual.

Cum le dăm agenților worktree-uri

Fiecare sarcină primește un worktree, creat de agentul care va face munca:

  • O sarcină, un worktree, un branch. Branch-ul poartă numele issue-ului sarcinii, iar worktree-ul pornește din cel mai recent branch principal de pe remote, așa că fiecare agent începe din același punct, indiferent ce e în checkout în altă parte.
  • În proiect, într-un folder ignorat de git. Worktree-urile stau într-un folder al checkout-ului principal pe care git îl ignoră, așa că fișierele și jurnalele lor sunt la un clic distanță în editor și nu apar niciodată ca modificări în checkout-ul principal.
  • Un reviewer primește propriul worktree detașat exact la commit-ul pe care îl verifică. Cel care implementează poate lucra mai departe pe branch cât timp rulează review-ul, iar review-ul citește un commit care nu se poate mișca sub el.
  • Căi absolute, nu cd. Un agent poate rula mai multe comenzi de shell în paralel într-o sesiune, iar ele au același director de lucru: un cd într-una le mută și pe celelalte. Agenții noștri scriu fiecare cale complet sau folosesc git -C <folder>, ca fiecare comandă să lucreze în worktree-ul pe care îl vizează.
  • Rebase înainte de fiecare push. Branch-ul primește rebase pe cel mai recent branch principal de pe remote înainte de fiecare push și e redus prin squash la un singur commit înainte să se deschidă pull request-ul lui, așa că un merge adaugă exact un commit în istoric.
  • Curățenie după merge. După ce schimbarea a intrat prin merge și a fost verificată, agentul șterge worktree-ul, șterge branch-ul local și pe remote, rulează git worktree prune și verifică că niciunul dintre ele nu mai apare în listă.

Patru capcane și regula care a eliminat-o pe fiecare

Tot ce are un worktree în comun e comun cu fiecare agent, în același timp. Fiecare capcană de mai jos ne-a costat o rulare stricată până să o găsim.

Stash-ul e o singură stivă pentru toți

git stash adaugă intrări pe refs/stash, iar acel ref e comun. Un agent și-a pus în stash munca în curs; alt agent, într-un alt worktree, a rulat git stash pop și a luat acea intrare. Modificările primului agent au dispărut din folderul lui și au putut fi recuperate doar prin git fsck --unreachable.

Regula: agenții nu folosesc niciodată git stash. Munca neterminată e păstrată ca un commit pe branch-ul propriu al agentului, pe care niciun alt worktree nu îl poate lua.

Și configurația e comună

Setările submodulelor stau în configurația repository-ului, pe care o au în comun toate worktree-urile. Un agent care a rulat git submodule deinit când și-a șters worktree-ul a scos submodulul din evidență pentru checkout-ul principal și pentru toate celelalte worktree-uri, iar build-ul a picat în toate.

Regulile: un worktree cu submodule se șterge doar cu git worktree remove --force, niciodată după un deinit; iar un worktree nou rulează git submodule update --init înainte de primul build, pentru că un worktree nou pornește cu folderele submodulelor goale.

Dependențele nu se împart printr-un symlink

Instalarea dependențelor în fiecare worktree nou durează, iar un symlink către node_modules din alt checkout pare o scurtătură. Nu e: npm instalează prin symlink și șterge pachetele pe care nu le așteaptă din checkout-ul spre care arată linkul, și așa a stricat la jumătate rularea testelor unui alt agent.

Regulile: fiecare worktree își instalează propriile dependențe. Când o copie economisește timp, clonăm folderul cu copy-on-write (cp -Rc pe APFS din macOS, care împarte blocurile de pe disc până se schimbă un fișier) și ștergem orice cache care păstrează căi absolute, cum e cache-ul de transformări al Playwright, pentru că ar arăta în continuare spre celălalt checkout.

Unelte care rețin folderul în care au rulat

Unele unelte scriu în rezultatul lor calea din care au rulat. Planurile noastre de infrastructură, rulate dintr-un worktree, arătau modificări care erau doar acea cale, nu modificări reale ale vreunei resurse.

Regula: modificările de infrastructură se aplică doar din checkout-ul principal, după ce schimbarea a intrat prin merge, niciodată dintr-un worktree; un plan rulat într-un worktree e doar de citit.

Cât costă

Worktree-urile costă puțin în istoric, dar nu și pe disc sau la procesor. Fiecare are un set complet de fișiere în checkout și, pentru o aplicație web, propriile dependențe, așa că o duzină de worktree-uri ocupă mult spațiu. Build-urile folosesc aceleași procesoare, așa că rulăm un singur build greu odată. Iar un worktree care nu e șters niciodată își ține branch-ul în checkout și îl blochează în altă parte, motiv pentru care curățenia face parte din sarcină și nu e lăsată pe mai târziu.

Întrebări frecvente

Ce are un git worktree în comun cu checkout-ul principal?

Aproape totul. Obiectele, branch-urile și tag-urile, stash-ul și configurația sunt comune. Fiecare worktree are propriul HEAD, propriul index și propriile fișiere pe disc, așa că fiecare folder poate avea în același timp alt branch în checkout.

Pot două worktree-uri să facă checkout la același branch?

Nu. Git refuză să facă checkout la un branch pe care un alt worktree îl are deja în checkout, dacă nu îl forțezi. Un worktree poate în schimb să facă checkout la un commit cu HEAD detașat, și așa îi dăm unui reviewer exact commit-ul pe care îl verifică.

De ce nu ar trebui ca agenții să folosească git stash într-un worktree?

Stash-ul e un singur ref, refs/stash, comun tuturor worktree-urilor din repository. Un agent care face pop pe stash poate lua intrarea altui agent. În loc de asta, păstrăm munca neterminată ca un commit pe branch-ul propriu al agentului.

Cum ștergi un worktree după un merge?

Îl ștergi cu git worktree remove, ștergi branch-ul local și pe remote, rulezi git worktree prune și verifici că nici branch-ul, nici worktree-ul nu mai apar în listă.

Înapoi sus