Comment nous faisons travailler plusieurs agents de programmation sur un même dépôt avec les worktrees git
La plupart des jours, plusieurs agents de programmation travaillent en même temps dans notre dépôt. L’un construit une fonctionnalité, un autre relit une branche différente, un troisième corrige un test qui échoue. Une copie de travail ordinaire ne contient qu’une branche à la fois, et changer de branche sous un agent en plein build casse son travail. Les worktrees git résolvent cela : chaque agent reçoit son propre dossier avec sa propre branche, et tous les dossiers partagent un seul dépôt.
Cet article explique ce qu’un worktree partage et ce qu’il garde pour lui, les commandes que nous utilisons chaque jour, la façon dont nous confions des worktrees aux agents, et les quatre pièges que l’état partagé nous a tendus, chacun avec la règle qui l’a supprimé.
Ce qu’est un worktree
Un worktree est un dossier contenant un commit extrait, qui appartient à un dépôt existant. Le dossier créé par git clone ou git init est le worktree principal ; git worktree add crée des worktrees liés à côté de lui. Un worktree lié partage tout avec le dépôt, sauf quelques fichiers qui lui sont propres : son HEAD, son index et les fichiers sur le disque.
En pratique :
- Partagé : chaque objet, chaque référence sous
refs/(les branches, les tags, les branches de suivi distantes et le stash, qui est la référencerefs/stash), le fichier de configuration et les hooks. - Propre à chaque worktree : le HEAD, l’index, les fichiers extraits, et quelques références que git garde à part volontairement (
refs/bisect,refs/worktree,refs/rewritten). - Une branche, un worktree : git refuse d’extraire une branche qu’un autre worktree a déjà extraite, sauf si vous le forcez. Un commit peut être extrait dans autant de worktrees que vous voulez avec un HEAD détaché.
Un commit fait dans un worktree est donc visible aussitôt dans tous les autres, sans fetch. Une branche créée dans l’un existe dans tous. C’est ce qui rend les worktrees peu coûteux : seuls les fichiers sur lesquels vous travaillez sont copiés.
Les commandes que nous utilisons
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
La première ligne crée une nouvelle branche à partir de la dernière branche principale distante et l’extrait dans un nouveau dossier. La deuxième extrait un commit sans branche, pour une relecture ou une exécution de tests qui ne doit rien déplacer. list affiche chaque worktree avec son commit et sa branche. remove supprime le dossier et les traces que git en garde, et refuse tant que le dossier contient des modifications ou des fichiers non suivis, ou des sous-modules ; --force passe outre. prune efface les traces des worktrees dont les dossiers ont été supprimés à la main.
Comment nous confions des worktrees aux agents
Chaque tâche reçoit un worktree, créé par l’agent qui fera le travail :
- Une tâche, un worktree, une branche. La branche porte le nom du ticket de la tâche, et le worktree part de la dernière branche principale distante : chaque agent commence donc du même point, quoi qu’il y ait d’extrait ailleurs.
- Dans le projet, dans un dossier ignoré par git. Les worktrees vivent dans un dossier de la copie principale que git ignore : leurs fichiers et leurs journaux sont à un clic dans l’éditeur et n’apparaissent jamais comme des modifications dans la copie principale.
- Un relecteur a son propre worktree détaché sur le commit exact examiné. L’agent qui écrit le code peut continuer à travailler sur la branche pendant la relecture, et la relecture lit un commit qui ne peut pas bouger sous ses pieds.
- Des chemins absolus, pas de
cd. Un agent peut lancer plusieurs commandes shell en parallèle dans une même session, et elles partagent un seul répertoire de travail : uncddans l’une déplace les autres. Nos agents écrivent chaque chemin en entier, ou utilisentgit -C <folder>, de sorte que chaque commande agit sur le worktree visé. - Rebaser avant chaque push. La branche est rebasée sur la dernière branche principale distante avant chaque push, puis ramenée à un seul commit avant l’ouverture de sa pull request, de sorte qu’une fusion ajoute exactement un commit à l’historique.
- Nettoyer après la fusion. Une fois la modification fusionnée et vérifiée, l’agent supprime le worktree, supprime la branche en local et sur le dépôt distant, exécute
git worktree pruneet vérifie que ni l’un ni l’autre n’apparaît encore dans la liste.
Quatre pièges, et la règle qui a supprimé chacun
Tout ce qu’un worktree partage est partagé avec tous les agents en même temps. Chacun des pièges ci-dessous nous a coûté une exécution cassée avant que nous le trouvions.
Le stash est une pile unique pour tous
git stash empile sur refs/stash, et cette référence est partagée. Un agent a mis son travail en cours de côté avec un stash ; un autre agent, dans un autre worktree, a lancé git stash pop et pris cette entrée. Les modifications du premier agent avaient disparu de son dossier et ne sont revenues que grâce à git fsck --unreachable.
La règle : les agents n’utilisent jamais git stash. Le travail inachevé est mis de côté dans un commit sur la branche propre de l’agent, qu’aucun autre worktree ne peut prendre.
La configuration est partagée elle aussi
Les réglages des sous-modules vivent dans la configuration du dépôt, que tous les worktrees partagent. Un agent qui a exécuté git submodule deinit en supprimant son worktree a désenregistré le sous-module pour la copie principale et pour tous les autres worktrees, et le build a échoué partout.
Les règles : un worktree qui contient des sous-modules est supprimé avec git worktree remove --force seul, jamais après un deinit ; et un nouveau worktree exécute git submodule update --init avant son premier build, car un nouveau worktree démarre avec ses dossiers de sous-modules vides.
Les dépendances ne doivent pas être partagées par un lien symbolique
Installer les dépendances dans chaque nouveau worktree prend du temps, et un lien symbolique vers le node_modules d’une autre copie ressemble à un raccourci. Ce n’en est pas un : npm installe à travers le lien et supprime les paquets qu’il n’attend pas, dans la copie vers laquelle le lien pointe, et il a interrompu l’exécution des tests d’un autre agent en plein milieu.
Les règles : chaque worktree installe ses propres dépendances. Quand une copie fait gagner du temps, nous clonons le dossier en copie sur écriture (cp -Rc sur l’APFS de macOS, qui partage les blocs du disque jusqu’à ce qu’un fichier change) et nous vidons tout cache qui stocke des chemins absolus, comme le cache de transformation de Playwright, car il pointerait encore vers l’autre copie.
Les outils qui enregistrent le dossier où ils ont tourné
Certains outils écrivent dans leur sortie le chemin d’où ils ont été lancés. Nos plans d’infrastructure, exécutés depuis un worktree, affichaient des changements qui n’étaient que ce chemin, pas de vrais changements.
La règle : les changements d’infrastructure ne sont appliqués que depuis la copie principale, une fois la modification fusionnée, jamais depuis un worktree ; un plan lancé dans un worktree sert à lire.
Ce que cela coûte
Les worktrees sont peu coûteux en historique, pas en disque ni en processeur. Chacun contient un jeu complet de fichiers extraits et, pour une application web, ses propres dépendances : une douzaine de worktrees occupent donc un espace réel. Les builds se partagent les mêmes processeurs, c’est pourquoi nous lançons un seul build lourd à la fois. Et un worktree qui n’est jamais supprimé garde sa branche extraite et la bloque ailleurs : c’est pourquoi le nettoyage fait partie de la tâche, et non une réflexion après coup.
Questions fréquentes
Que partage un worktree git avec la copie de travail principale ?
Presque tout. Les objets, les branches et les tags, le stash et la configuration sont partagés. Chaque worktree a son propre HEAD, son propre index et ses propres fichiers sur le disque : une branche différente peut donc être extraite dans chaque dossier en même temps.
Deux worktrees peuvent-ils extraire la même branche ?
Non. Git refuse d’extraire une branche qu’un autre worktree a déjà extraite, sauf si vous le forcez. Un worktree peut en revanche extraire un commit avec un HEAD détaché, ce qui nous permet de donner à un relecteur le commit exact examiné.
Pourquoi les agents ne doivent-ils pas utiliser git stash dans un worktree ?
Le stash est une référence unique, refs/stash, partagée par tous les worktrees du dépôt. Un agent qui récupère le stash avec pop peut prendre l’entrée d’un autre agent. Nous mettons plutôt le travail inachevé de côté dans un commit sur la propre branche de l’agent.
Comment nettoyer un worktree après une fusion ?
Supprimez-le avec git worktree remove, supprimez la branche en local et sur le dépôt distant, exécutez git worktree prune, puis vérifiez que ni la branche ni le worktree n’apparaît encore dans la liste.