Ga naar inhoud

Git Worktrees

NOTES

Doel van de les

  • Het probleem: één working directory per repo; uncommitted werk blokkeert git switch - en waarom je soms op meerdere branches tegelijk wil werken.
  • De tussenoplossingen (meerdere clones, git stash, WIP-commits) en waarom ze onhandig zijn.
  • Worktrees als nette oplossing: git worktree add/list/remove.
  • Kern van de les: worktrees zijn vandaag de standaardmanier om met meerdere coding agents tegelijk aan één project te werken.

Kernboodschap

  • Één repo, meerdere working directories: elke worktree heeft zijn eigen bestanden, maar deelt de Git-database (geschiedenis, branches, remotes).
  • Branches isoleren commits; worktrees isoleren bestanden.
  • Twee agents in dezelfde directory overschrijven elkaar zonder enige foutmelding. Één working directory per agent is dus geen luxe, maar noodzaak.

Voorbereiding

  • Demo-repo (hello) lokaal klaarzetten en de volledige demo op voorhand testen.
  • Twee terminaltabs voorbereiden; opencode geïnstalleerd en ingelogd.
  • Commando's testen op het OS van de klas (op Windows: git worktree add -b hotfix ..\hello-hotfix).
  • Worktrees bestaan sinds Git 2.5: check git --version (elke moderne installatie kan het).

Verloop van de les

  • Conceptueel lesgeven: studenten moeten worktrees vooral begrijpen en herkennen, niet blindelings kunnen opdreunen.
  • De oefeningen zijn kort; het actieve moment zit in de demo op het einde.
  • Laat de demo traag genoeg gaan om git worktree list en de afwezigheid van conflicten echt te tonen.

Het probleem: op meerdere branches tegelijk werken

Tot nu toe werkten we altijd in één directory. Je kiest een branch, en Git past de bestanden in die directory aan. Dat werkt prima voor één taak, maar zodra je aan meerdere dingen tegelijk wil werken, wringt het.

Wanneer wil je dat eigenlijk?

  • Een feature is half af, en plots is een dringende fix op main nodig (een hotfix).
  • Een pull request van iemand anders reviewen of uittesten zonder je eigen werk weg te leggen.
  • Experimenteren of in de geschiedenis speuren zonder je huidige branch te bevuilen.
  • Een (of meerdere!) coding agent een taak geven terwijl jij zelf verder werkt.

Waarom is dat zo lastig? Omdat een repo standaard één working directory heeft. Die directory hoort bij de branch die op dat moment open staat. Wil je naar een andere branch, dan moet die working directory mee... inclusief al je niet-gecommitte werk.

Je werkt bijvoorbeeld al een uur aan feature-search, als een collega om een dringende hotfix vraagt. Even snel naar main switchen? Git steekt daar een stokje voor:

git statusOn branch feature-search
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: hello.py

git switch mainerror: Your local changes to the following files would be overwritten by checkout:
hello.py
Please commit your changes or stash them before you switch branches.
Aborting

Git weigert omdat je aanpassing aan hello.py verloren zou gaan bij het switchen naar main.

Ook een geslaagde switch is riskant

Zelfs wanneer git switch wel lukt (omdat er geen overlap is), is het gedrag gevaarlijk: Git neemt je niet-gecommitte nieuwe bestanden dan stilletjes mee naar de andere branch.

Op het eerste gezicht lijkt dat handig, tot je daar iets commit: je half afgewerkte feature belandt dan in een commit op main (of omgekeerd). Je werk is dan niet verloren, maar je geschiedenis wordt er wel erg rommelig van.

De onhandige tussenoplossingen

Er bestaan een paar klassieke trucs om dit probleem te omzeilen. Ze werken, maar geen enkele is echt aangenaam.

Meerdere clones

  • git clone in een aparte directory per taak: elke directory werkt als een "eigen" repo.
  • Nadelen: elke clone heeft een eigen .git (schijfruimte, traag bij grote repos), branches en remotes lopen uit elkaar, en je werkt zo snel in de verkeerde clone.

git stash / git stash pop

  • Werk opzijzetten (git stash), switchen, later terugzetten (git stash pop).
  • Nadelen: pop botst als de andere branch intussen veranderde, de stash-stack wordt een rommel, stashes hangen aan geen enkele branch (makkelijk "verloren"), en je verliest alle context.
git stashSaved working directory and index state WIP on feature-search: 3d563fd Add initial hello module

git switch mainSwitched to branch 'main'

git stash popOn branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: hello.py

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (dbcc7816404d03e3070050169fc4c8b56992973f)

Merk op wat er in dit voorbeeld eigenlijk gebeurt: de aanpassing hoorde inhoudelijk bij feature-search, maar staat nu als losse wijziging op main. Je moet zelf goed bijhouden waar welk werk thuishoort.

"WIP"-commits

  • Een Work-in-Progress tussencommit maken zodat je kan switchen, later rechtzetten met amend/rebase.
  • Nadelen: vervuilde geschiedenis, risico dat een WIP-commit per ongeluk gepusht wordt of in een PR terechtkomt, en CI die op een halve feature draait.

Alle oplossingen op een rij:

Oplossing Voordeel Grootste nadeel
Meerdere clones Volledige isolatie: eigen working directory én eigen Git-repo. Dubbel beheer: aparte .git, aparte remotes, afwijkende branches - en je werkt snel in de verkeerde clone.
git stash Snel, werkt altijd lokaal. Stashes hangen aan geen enkele branch; vergeten stashes en conflicten bij pop.
WIP-commit Eenvoudig: gewoon committen en later opkuisen. Vervuilde geschiedenis; een WIP-commit glipt makkelijk mee in een push, PR of CI-run.
Worktree Eigen working directory, gedeelde geschiedenis - geen extra repo. Extra directory om te beheren; build-artefacten en tooling bestaan per worktree.

Worktrees: de betere oplossing

Worktree

Een worktree is een extra working directory (working tree) die aan dezelfde Git repo hangt. Elke worktree heeft zijn eigen bestanden en uitgecheckte branch; geschiedenis, branches en remotes worden gedeeld via dezelfde .git-directory.

flowchart BT
    G["Git database (.git)<br>geschiedenis · branches · remotes"]
    W1["working directory 1<br>hello · branch main"] --> G
    W2["working directory 2<br>hello-hotfix · branch hotfix"] --> G
    W3["working directory 3<br>hello-agent · branch feature-x"] --> G
Extra: Een worktree herkennen

In een gewone repo is .git een directory. In een worktree is .git een bestand dat verwijst naar de administratie van die worktree in de hoofdrepo:

$ cat .git
gitdir: /Users/maria/dev/hello/.git/worktrees/hello-hotfix

Handige regel dus: zie je een .git-bestand in plaats van een .git-directory, dan zit je in een worktree.

De belangrijkste commando's:

Commando Beschrijving
git worktree add -b <branch> <pad> Nieuwe worktree aanmaken met een nieuwe branch.
git worktree add <pad> <bestaande-branch> Nieuwe worktree voor een bestaande branch of commit.
git worktree list Overzicht van alle worktrees, met hun branch en laatste commit.
git worktree remove <pad> Worktree verwijderen (de branch blijft bestaan).
git worktree prune Administratie opkuisen van worktrees waarvan de directory al verdwenen is.

Paden op Windows

Gebruik in de commando's het pad naar de map voor de nieuwe worktree. Op Windows werkt zowel ..\hello-hotfix als ../hello-hotfix.

Een basisvoorbeeld:

git worktree add -b hotfix ../hello-hotfixPreparing worktree (new branch 'hotfix')
HEAD is now at 22b1b17 Add module docstring in hello.py

cd ../hello-hotfixgit statusOn branch hotfix
nothing to commit, working tree clean

Je kan nu gewoon werken in ../hello-hotfix: bestanden aanpassen, tests draaien, committen. Het is een volwaardige working directory met een eigen HEAD en staging area. Na een commit:

git log --oneline8e1c3a7 (HEAD -> hotfix) Fix urgent bug in hello.py
22b1b17 Add module docstring in hello.py
cd ../hellogit worktree list/Users/bob/dev/hello 22b1b17 [main]
/Users/bob/dev/hello-hotfix 8e1c3a7 [hotfix]

git worktree list toont duidelijk dat elke worktree zijn eigen branch en laatste commit heeft. Enkele eigenschappen:

  • Eén branch per worktree: een branch kan maar in één worktree tegelijk bestaan. Probeer je het toch, dan weigert Git:

    git switch hotfixfatal: 'hotfix' is already used by worktree at '/Users/bob/dev/hello-hotfix'

  • Gedeelde geschiedenis: de commit 8e1c3a7 staat onmiddellijk in de hele repo - in alle worktrees, zonder pushen of fetchen. Commandos als git log --all of git branch tonen alles van alle worktrees samen.

  • Eigen toestand: elke worktree heeft zijn eigen working directory, staging area en HEAD. Een commit in de ene worktree verandert niets aan de bestanden van de andere.

Vanuit de hoofdrepo kan je de hotfix nu mergen, net als elke andere branch:

git merge hotfixUpdating 22b1b17..8e1c3a7
Fast-forward
hello.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

En opruimen:

git worktree remove ../hello-hotfixgit branch -d hotfixDeleted branch hotfix (was 8e1c3a7).

Worktree verwijderen met niet-gecommitte aanpassingen

git worktree remove weigert als de worktree aangepaste of untracked bestanden bevat:

fatal: '../hello-hotfix' contains modified or untracked files, use --force to delete it
Extra: opruimen na handmatig verwijderen

Verwijder je een worktree-directory handmatig (rm -rf hello-hotfix) in plaats van met git worktree remove, dan blijft er administratie achter. git worktree list toont de verdwenen worktree dan als prunable, en git worktree prune kuis de restanten op.

rm -rf ../hello-hotfixgit worktree list/Users/maria/dev/hello 22b1b17 [main]
/Users/maria/dev/hello-hotfix 8e1c3a7 [hotfix] prunable
git worktree prunegit worktree list/Users/maria/dev/hello 22b1b17 [main]

Extra: een commit of pull request uittesten zonder branch

Je wil even rondkijken in een oude commit of de branch van een PR uittesten, zonder daar een eigen branch voor te maken. Dat kan met een detached HEAD worktree:

git worktree add --detach ../hello-review 22b1b17

Het resultaat is een gewone directory waarin je vrij kan experimenteren; je gooit ze achteraf gewoon weg met git worktree remove.

Beperkingen

Worktrees zijn geen wondermiddel. Hou rekening met:

  • Virtuele Python omgeving (uv) is lokaal in elke worktree en moet dus in nieuwe worktrees opnieuw worden aangemaakt (bvb uv sync)
  • .gitignore bestanden (bvb. .env) zitten niet in Git en dus ook niet in een nieuwe worktree.
  • Opkuis: worktrees en hun branches verdwijnen niet automatisch. Gebruik git worktree list om te zien wat er rondslingert en ruim op wanneer een taak klaar is.

Oefeningen

Oefening 1: het probleem voelen

Werk in je eigen hello repo.

  1. Maak een nieuwe branch feature-search en switch ernaar.
  2. Maak een aanpassing in hello.py, maar commit ze niet.
  3. Probeer naar main te switchen. Wat gebeurt er?
  4. Zet je werk opzij met git stash, switch naar main en haal het terug met git stash pop. Op welke branch staat je aanpassing nu?
  5. Bespreek: wat zou er gebeuren als je hier per ongeluk commit? En wat als je de stash was vergeten?
  6. Ruim op: git restore hello.py.
Oplossing

git switch -c feature-searchSwitched to a new branch 'feature-search'
# pas hello.py aangit switch mainerror: Your local changes to the following files would be overwritten by checkout:
hello.py
Please commit your changes or stash them before you switch branches.
Aborting

Git beschermt je werk. Met git stash geraak je alsnog op main:

git stashSaved working directory and index state WIP on feature-search: 3d563fd Add initial hello module

git switch mainSwitched to branch 'main'

git stash popOn branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: hello.py

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (dbcc7816404d03e3070050169fc4c8b56992973f)

De aanpassing die bij feature-search hoorde, staat nu op main. Precies daarom zijn stashes en WIP-commits zo foutgevoelig.

Oefening 2: een worktree in de praktijk

  1. Voer git worktree list uit in je hello repo. Hoeveel worktrees zijn er?
  2. Maak een hotfix-worktree aan: git worktree add -b hotfix ../hello-hotfix.
  3. Open ../hello-hotfix in VS Code en pas hello.py aan. Commit je wijziging.
  4. Keer terug naar je hoofdrepo. Vraag jezelf af: zonder te pushen of te pullen, is de commit van de hotfix hier al bekend? Controleer met git log --all --oneline en git worktree list.
  5. Merge de hotfix in main.
  6. Ruim op: verwijder de worktree en daarna de branch. Controleer met git worktree list en git branch wat er overblijft.
  7. Extra: probeer git switch hotfix terwijl de worktree nog bestaat. Wat gebeurt er?
Oplossing

git worktree list/Users/maria/dev/hello 22b1b17 [main]
git worktree add -b hotfix ../hello-hotfixPreparing worktree (new branch 'hotfix')
HEAD is now at 22b1b17 Add module docstring in hello.py

# open../hello-hotfix# pas hello.py aan en commitgit commit -a -m "Fix urgent bug in hello.py"[hotfix 8e1c3a7] Fix urgent bug in hello.py
1 file changed, 1 insertion(+), 1 deletion(-)

# open../hellogit log --all --oneline8e1c3a7 (hotfix) Fix urgent bug in hello.py
22b1b17 (HEAD -> main) Add module docstring in hello.py

De commit is onmiddellijk zichtbaar in de hoofdrepo: worktrees delen dezelfde Git-database. Mergen en opruimen:

git merge hotfixUpdating 22b1b17..8e1c3a7
Fast-forward
hello.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

git worktree remove ../hello-hotfixgit branch -d hotfixDeleted branch hotfix (was 8e1c3a7).

git worktree list/Users/maria/dev/hello 8e1c3a7 [main]

git branch* main

De extra opdracht geeft opnieuw fatal: 'hotfix' is already used by worktree at ...: zolang de worktree bestaat, is de branch bezet.

Worktrees en coding agents

Tot hier leek een worktree vooral een handigheidje voor wie vaak van branch wisselt. Maar er is één ontwikkeling die worktrees van handig naar noodzakelijk opwaarderen: coding agents.

Waarom één working directory niet werkt voor agents

Een coding agent (OpenCode, Claude Code, Codex, ...) werkt in de working directory. Hij leest en bewerkt bestanden, voert commando's uit, draait tests en maakt commits. Dat is precies wat een agent nuttig maakt, maar ook waar het misgaat zodra er twee dingen tegelijk in die directory gebeuren:

  • Twee agents, één directory: beide bewerken dezelfde bestanden. Er is geen enkele foutmelding wanneer agent B het werk van agent A overschrijft; de laatste schrijver wint.
  • Jij en een agent: terwijl de agent werkt, staat jouw directory op elk moment in beweging. Je eigen half afgewerkte wijzigingen en die van de agent belanden in dezelfde working tree en dus in dezelfde diff.
  • Agents en branches: een branch isoleert je commits, maar niet je bestanden. Twee agents op twee verschillende branches kunnen dus nog steeds niet tegelijk in dezelfde working directory werken: elke git switch verandert de bestanden onder de voeten van de andere agent.

De regel is simpel: één working directory per agent. Precies de isolatie die we krijgen met worktrees.

Branch Worktree
Isoleert commits commits en bestanden
Eigen working directory nee, gedeeld ja, één per worktree
Geschiedenis, remotes gedeeld gedeeld

Het resultaat:

  • Echte isolatie: agents kunnen elkaar niet overschrijven.
  • Jij blijft verder werken: je eigen working directory blijft gewoon op main staan, zonder stash of WIP-commits. Wat de agents doen, raakt jou niet.
  • Parallel werk: je kan twee, drie of meer agents tegelijk verschillende taken laten uitvoeren, elk op een eigen branch + worktree.
  • Controle op het einde: elke agent levert een eigen branch op. Je kan dan kiezen wat er in main komt, in welke volgorde.

Worktrees in de praktijk van agent-tools

Je zal worktrees steeds vaker tegenkomen, ook als je er zelf geen aanmaakt. Sommige agents maken zelf een worktree per sessie. Claude Code doet dat bijvoorbeeld met claude --worktree feature-x: je krijgt een geïsoleerde directory onder .claude/worktrees/ met een eigen branch.

Tools zoals wt (Worktrunk) beheren volledige worktree-workflows: aanmaken, switchen, opruimen en automatische stappen zoals dependencies installeren. Agent-orchestratoren gebruiken hetzelfde patroon: één taak = één werkdirectory = één branch.

Als developer hoef je dus niet elke worktree zelf aan te maken. Waar het op aankomt:

  1. Begrijpen dat een worktree een gewone working directory is, met een eigen branch, binnen dezelfde repo.
  2. Herkennen wanneer een tool of agent er een gebruikt: een .git-bestand, een pad buiten je gewone repo-directory, of git worktree list.
  3. Beoordelen wat er in die branch gebeurt voor je mergt: git log --all, de diff bekijken, tests draaien.
  4. Opruimen wanneer de taak klaar is: git worktree remove en daarna de branch verwijderen.

Een agent-instructie die worktrees respecteert

Werkt een agent in zijn eigen worktree, dan moet hij daar ook blijven. Dat leg je vast in de projectinstructies (AGENTS.md) of in de prompt:

AGENTS.md (fragment)
## Git-regels

- Je werkt in een eigen worktree: werk uitsluitend in de huidige directory.
- Voer geen `git switch`, `git checkout -b` of `git worktree`-commando's uit.
- Commit je wijzigingen op de branch die hier uitgecheckt is; raak `main` niet aan.
- Draai de tests lokaal voor je commit.

En bij het starten geef je de agent een concrete taak, in de worktree waar hij hoort:

$ cd ../hello-feature-logging
$ opencode run "Voeg logging toe aan hello.py en commit je wijziging op deze branch."

Demo: twee opencode-sessies in aparte worktrees

We sluiten af met de workflow waar het in deze les echt om draait: twee agents, één project, twee worktrees.

NOTES

Laat de demo traag genoeg gaan zodat cursisten dit expliciet zien:

  1. git worktree list is de hele tijd de "vloot" aan werkplekken.
  2. In de hoofdrepo verandert niets zolang de agents werken.
  3. Beide agents maken onafhankelijke commits; er is geen enkele fout of overschrijving, want ze delen geen bestanden.
  4. Jij beslist op het einde wat er in main komt (review + merge).
  5. Opruimen is twee commando's per worktree.

Variant om de controle te benadrukken: laat beide agents in hetzelfde bestand (hello.py) werken. De worktrees blijven perfect geïsoleerd; het conflict verschijnt pas bij de merge - waar jij het oplost.


Samenvatting

Samenvatting

  • Een Git repo heeft standaard één working directory. Uncommitted werk blokkeert git switch (of reist stiekem mee naar de andere branch), en twee agents in dezelfde directory overschrijven elkaars werk zonder foutmelding.
  • Klassieke tussenoplossingen zijn meerdere clones (dubbel beheer), git stash (hangt aan geen enkele branch, conflicten bij pop) en WIP-commits (vervuilde geschiedenis).

  • Een worktree is een extra working directory voor dezelfde repo: eigen bestanden en branch, gedeelde geschiedenis via dezelfde .git. Je herkent er een aan het .git-bestand; git worktree list toont ze allemaal.
  • Worktrees maak je met git worktree add -b <branch> <pad>, je verwijdert ze met git worktree remove <pad> - de branch blijft daarna bestaan. Eén branch kan maar in één worktree tegelijk uitgecheckt zijn.
  • Build-artefacten, virtuele omgevingen en genegeerde bestanden (.env, ...) bestaan per worktree: reken op een extra setup per worktree.

  • Coding agents maken worktrees noodzakelijk: één working directory per agent vermijdt overschreven werk, verrassende branch-switches en verloren wijzigingen.
  • Elke agent levert een eigen branch op; jij reviewt en mergt op het moment dat het jou past. Experimenten gooi je weg met één git worktree remove.
  • Sommige agents maken zelf worktrees aan (bv. Claude Code met claude --worktree); tools als wt beheren de volledige workflow. Als developer moet je worktrees vooral herkennen, begrijpen en controleren.

Belangrijke nieuwe commando's:

git worktree add -b <branch> <pad>
git worktree add <pad> <bestaande-branch>
git worktree list
git worktree remove <pad>
git worktree prune

Wat moet je kunnen?

  • Je kan uitleggen waarom één working directory beperkend is wanneer je op meerdere branches tegelijk wil werken.
  • Je kan de tussenoplossingen (meerdere clones, git stash, WIP-commits) benoemen met hun grootste nadeel.

  • Je kan uitleggen wat een worktree is: een extra working directory met een eigen branch, maar met een gedeelde Git-database.
  • Je herkent een worktree aan het .git-bestand of met git worktree list.
  • Je kan in eigen woorden uitleggen waarom worktrees cruciaal zijn bij parallelle coding agents: eigen bestanden, eigen branch, geen overschrijvingen, controle bij de merge.
  • Je weet hoe je een worktree opruimt (git worktree remove) en dat je de branch daarna zelf nog verwijdert.