Pourquoi j'ai arrêté de recopier mes .env à la main entre worktrees Git, et pourquoi j'ai choisi WorkTrunk plutôt que d'écrire mon propre script Python.
J’utilise les worktrees Git depuis 2019 environ, découverts en contribuant à CPython, où il suffisait de relancer un build pour vérifier un correctif. Le mécanisme est excellent. C’est en m’en servant de plus en plus sur mes propres projets que chaque nouveau worktree s’est mis à coûter quatre corvées manuelles : recopier le .env, créer une base de données dédiée, réinstaller les dépendances, reconstruire une image Docker au nom de la branche. J’ai hésité à écrire mon propre script Python pour automatiser ça. J’ai finalement choisi WorkTrunk (wt), qui fait exactement ce travail via des hooks déclaratifs. Le gain le plus net est arrivé d’un usage que je n’avais pas en tête en l’installant : relire la PR d’un contributeur est devenu une opération sans conséquence sur mon environnement.
git worktree vs wtJ’ai découvert les worktrees Git vers 2019, en contribuant à CPython. C’est un core dev de CPython qui m’a montré la commande. Le besoin était très concret : tester la branche d’un contributeur, ou vérifier un correctif, sans perdre ce que j’avais en cours dans mon propre répertoire de travail.
À l’époque, la plomberie était minimale. CPython n’a ni base de données ni fichier d’environnement : un nouveau worktree, un ./configure && make, et je pouvais lancer la suite de tests. Le worktree suffisait à lui-même, ce qui explique sans doute pourquoi j’ai mis des années à en voir la limite.
J’ai trouvé ça brillant tout de suite, et pour une raison précise : un worktree n’est pas un clone du dépôt. Je ne duplique pas l’historique Git, je ne re-télécharge rien. C’est un seul dépôt .git, avec plusieurs répertoires de travail actifs en parallèle, chacun sur sa propre branche.
Le scénario que ça résout, tout le monde le connaît. Je suis en plein dans une fonctionnalité, rien n’est committé proprement, et on me tape sur l’épaule pour un bug urgent. Sans worktree, je stashe en croisant les doigts, ou je commite quelque chose de bancal juste pour changer de branche. Avec, j’ai ma feature dans un dossier, le hotfix dans un autre, la revue de PR dans un troisième, et je ne perds jamais le fil.
Ce qui a changé récemment, c’est la fréquence. Avec les agents de codage, Claude Code, Codex et les autres, lancer plusieurs corrections ou améliorations en parallèle est devenu une façon normale de travailler. Les worktrees sont ce qui rend la chose tenable : chaque agent dans son répertoire, sur sa branche, sans toucher à la branche principale ni au voisin. Je suis passé d’un worktree de temps en temps à plusieurs en même temps, presque tous les jours.
Sauf que mes projets ne ressemblent pas à CPython, et que cette montée en fréquence a fait apparaître une plomberie qui n’existait pas en 2019.
Mes projets ont presque tous une base de données et un fichier d’environnement. Et c’est là que le joli mécanisme des worktrees se grippe : git worktree add me donne un répertoire de travail propre, et rien d’autre.
Chaque nouveau worktree me coûtait donc la même séquence manuelle :
.env depuis le dépôt principal, parce qu’il est ignoré par Git et qu’il ne suit donc pas la branche..venv avec uv, parce qu’il est lui aussi ignoré par Git et absent du nouveau répertoire.Aucune de ces quatre étapes n’est difficile. Le problème n’est pas la difficulté, c’est la répétition et le moment où elle tombe. Je crée un worktree précisément quand je veux aller vite, quand quelqu’un m’a interrompu. Et à cet instant précis, l’outil me demande cinq minutes de plomberie.
🚧 Attention : les dépendances ne se traitent pas toutes de la même façon. Copier un
.venvPython est une mauvaise idée : il contient des chemins absolus qui ne survivent pas au changement de dossier. Il faut le reconstruire, pas le déplacer.
Le résultat, c’est que j’utilisais les worktrees moins souvent que je n’aurais dû. Un outil qui coûte cinq minutes à l’usage finit par ne plus être utilisé dans les situations pressées, c’est-à-dire exactement celles pour lesquelles il existe.
Une fois le problème identifié, j’avais quatre pistes.
Option 1 : continuer à la main. C’est ce que je faisais. Ça marche, et le coût par worktree reste faible. Écartée parce que le coût est faible mais permanent, et parce qu’il tombe au pire moment. Et à force de le payer, j’ai fini par me demander s’il n’existait pas déjà une solution : je ne dois pas être le seul développeur que ça agace.
Option 2 : écrire mon propre script. C’est la piste que j’ai le plus sérieusement envisagée, et honnêtement celle vers laquelle mon réflexe me poussait. Un script Python qui crée le worktree, va chercher les fichiers dont j’ai besoin, les dépose au bon endroit, démarre un conteneur Postgres et installe les dépendances. Une centaine de lignes, je sais faire.
Je l’ai écartée pour une raison que je connais bien pour m’être déjà fait avoir : ce genre de script ne reste jamais à cent lignes. Il faut gérer la suppression du worktree, le nettoyage du conteneur orphelin, les collisions de ports, les projets qui n’ont pas la même stack, la portabilité entre mes machines. Je serais devenu le mainteneur d’un outil interne pour un problème que d’autres avaient déjà résolu. Le coût de possession dépasse largement le coût d’écriture.
Option 3 : wtp (Worktree Plus). Il existe un projet du même genre, satococoa/wtp, écrit en Go, avec hooks post-create et gestion automatique des chemins. Sur le papier il répondait à mon besoin.
Ce qui m’a fait hésiter, c’est son rythme de développement. Au moment où j’écris, le dernier commit remonte à mars 2026, soit environ six mois. Le dépôt n’est pas archivé et compte plus de 600 stars (constaté le 20 septembre 2026 sur l’historique du dépôt), donc je ne vais pas le déclarer mort : un outil peut très bien être simplement fini. Mais je n’avais pas envie d’introduire dans mon workflow quotidien une dépendance dont je ne sais pas dire si elle sera encore suivie dans un an.
💡 Astuce : avant d’adopter un outil qui va s’insérer dans ton quotidien, regarde la date du dernier commit et si le dépôt est archivé. Ça prend dix secondes et ça évite des migrations subies.
Option 4 : WorkTrunk. C’est celle que j’ai retenue. Le projet est très actif : huit versions entre le 30 juillet et le 16 septembre 2026, soit plus d’une par semaine (constaté le 20 septembre 2026 sur la page des releases). Au moment où je choisis, c’est rassurant : le projet est suivi, les corrections arrivent.
WorkTrunk (la commande wt) prend exactement le créneau qui me manquait : rendre le travail avec les worktrees aussi naturel que le travail avec des branches classiques, et surtout laisser déclarer dans un fichier de configuration tout ce que je faisais à la main.
Pourquoi lui plutôt que mon script : parce qu’il traite les quatre corvées comme un seul problème, celui du cycle de vie d’un worktree. La copie des fichiers ignorés, le démarrage de la base, l’installation des dépendances, la construction de l’image et le nettoyage à la suppression sont tous des hooks sur le même cycle. C’est précisément la partie que j’aurais mal faite dans un script maison, parce que j’aurais commencé par la création et bâclé la destruction.
Les compromis que j’assume en le choisissant, et il y en a :
git worktree en dessous, mais c’est une couche de plus à comprendre le jour où quelque chose casse.wt switch crée ou bascule vers un worktree, en gérant le nommage et le chemin à ma place.
$ wt switch --create feature-auth
✓ Created branch feature-auth from main and worktree @ ~/repo.feature-auth
Une seule commande, un nouveau répertoire. Le flag -x exécute une commande juste après le switch, ce qui est pratique pour enchaîner sur un agent :
$ wt switch -c feature-auth -x claude -- "Implémente l'authentification OAuth2"
-c crée la branche et le worktree, -x claude lance Claude Code dedans, et ce qui suit -- est passé comme prompt initial.
wt list donne l’état de tous les worktrees d’un coup d’œil.
$ wt list
Branch Status HEAD± main↕ main…± Remote⇅ Commit Age Message
@ feature-auth + ↑1 +27 -8 ↑1 +31 4bc72dc 2h Add authentication module
^ main ^⇡ ⇡1 0e631ad 1d Initial commit
Le @ marque le worktree courant, le + des changements non commités, ↑1 un commit d’avance sur main. C’est ce que je faisais avec une série de cd et de git status, condensé dans un tableau.
wt merge enchaîne commit, squash, rebase, merge et nettoyage du worktree.
$ wt merge main
◎ Generating commit message and committing changes...
✓ Committed changes @ a1b2c3d
◎ Merging 1 commit to main (no rebase needed)
✓ Merged to main (1 commit, 2 files, +53)
◎ Removing feature-auth worktree & branch in background
C’est la commande qui m’a le plus surpris en bien, parce que c’est l’étape que je faisais le plus mal : je supprimais le worktree et j’oubliais la branche, ou l’inverse. Des options existent pour reprendre la main : --no-squash pour conserver l’historique, --no-remove pour garder le worktree, --no-rebase pour ne pas réécrire l’historique, à condition que main puisse encore avancer en fast-forward.
Par défaut, chaque worktree atterrit dans un dossier voisin du dépôt (~/code/myproject.feature-auth). L’emplacement est configurable via worktree-path dans ~/.config/worktrunk/config.toml, avec des variables de template :
# Dans un sous-dossier caché du dépôt
worktree-path = "{{ repo_path }}/.worktrees/{{ branch | sanitize }}"
# Dans un dossier centralisé, hors du dépôt
worktree-path = "~/worktrees/{{ repo }}/{{ branch | sanitize }}"
# Organisé par propriétaire du remote, utile en multi-orgas
worktree-path = "~/development/{{ owner }}/{{ repo }}/{{ branch }}"
Je l’ai découvert en lisant wt switch --help. C’est une ligne facile à rater, et pourtant : sans argument, wt switch n’échoue pas, il ouvre un sélecteur interactif, avec la liste des worktrees à gauche et un panneau de prévisualisation à droite.
Je tape pour filtrer, je navigue aux flèches, Entrée bascule. Par défaut la liste ne contient que mes worktrees, et trois options l’élargissent : --branches ajoute les branches locales qui n’en ont pas, --remotes les branches distantes, --prs les pull requests ouvertes du dépôt.
Une série de raccourcis permet d’agir directement depuis la liste : créer un worktree à partir du texte tapé, supprimer celui qui est sélectionné, copier un nom de branche, rafraîchir. Autant être honnête, je n’ai adopté aucun de ces raccourcis d’action : je tape trois lettres, je valide, et c’est tout ce que je demande au picker pour créer ou supprimer.
Ce qui me fait rester dans le picker, en revanche, c’est le panneau de droite, qui a huit onglets (Tab pour circuler, Alt-1 à Alt-8 pour y aller directement) : le diff net depuis la base de comparaison, les changements non commités, les commits depuis la base, le log, l’avance/retard face au remote, un résumé de branche généré par LLM, et pour les lignes qui ont une PR, la PR elle-même et son fil de commentaires.
La base de comparaison est le merge-base avec la branche par défaut. Le diff passe par le pager de Git, et ce pager est configurable. Si tu utilises delta, qui remplace l’affichage brut de git diff par une coloration syntaxique du langage, des numéros de ligne et un mode côte à côte, il s’applique aussi dans la preview du picker :
[switch.picker]
pager = "delta --paging=never --width=$COLUMNS"
--prs mérite qu’on s’y arrête, c’est ce que j’ai trouvé de plus utile dans le lot. Le flag ajoute au picker les pull requests ouvertes du dépôt, et seulement celles qui n’y sont pas déjà : une PR dont la branche est visible, comme worktree ou comme branche locale ou distante, n’apparaît pas en double. Chaque ligne ajoutée se comporte comme le raccourci pr:<numéro> : je sélectionne, WorkTrunk récupère la ref et bascule sur la branche.
Le filtre porte aussi sur le numéro, le titre et l’auteur de la PR, donc je tape trois lettres du titre au lieu de retrouver un nom de branche. Alt-o ouvre la PR dans le navigateur, les onglets pr et comments chargent la description et le fil de commentaires en arrière-plan, et une colonne affiche le statut CI et la revue. Ça remplace l’aller-retour « j’ouvre GitHub, je lis la PR, je copie le nom de la branche, je reviens au terminal ».
🚧 Attention : ça ne fonctionne pas tout seul. Il faut
ghinstallé et authentifié pour GitHub,glabpour GitLab, ou le CLI équivalent pour Gitea et Azure DevOps. Sans ça, le flag n’a rien à afficher.
C’est le seul endroit où je vois l’état de toutes mes branches et le diff de chacune sans changer de répertoire. Pour quelqu’un qui ouvre plusieurs worktrees en parallèle, c’est ce qui remplace le « c’était quoi déjà, celui-là ? » par un coup d’œil.
Détail que je trouve bien vu : wt switch -x claude sans nom de branche ouvre le picker, puis lance l’agent dans le worktree choisi.
C’est la partie qui a réglé mon problème, donc celle qui m’intéressait. Je traite ici les corvées 1, 3 et 4 : la base de données a droit à sa propre section juste après, parce que c’est la configuration la plus longue.
Les blocs TOML qui suivent sont des extraits indépendants, pas un fichier à recopier tel quel. Dans un vrai .config/wt.toml, un type de hook n’apparaît qu’une fois et les commandes d’un même type se regroupent sous la même table.
Les hooks se déclarent dans .config/wt.toml, au niveau du dépôt, et se déclenchent à des moments précis du cycle de vie du worktree : à la création, avant un merge, avant une suppression. Ils demandent une approbation manuelle au premier lancement, pour qu’un script venu d’un dépôt cloné ne s’exécute pas dans mon dos.
Un détail qui m’a coûté une soirée à comprendre : les hooks pre-* bloquent, les hooks post-* tournent en arrière-plan. Concrètement, un post-start qui copie le .env peut très bien ne pas avoir fini quand la commande passée à -x démarre. Tout ce dont la commande suivante a besoin doit donc aller en pre-start, qui bloque justement post-start et --execute jusqu’à la fin. Le reste, les serveurs de dev, les gros builds, les caches, reste en post-start.
🚧 Attention : deux choses à savoir avant d’écrire ses premiers hooks.
Le placement est un arbitrage, pas une règle. Tout mettre en
pre-startrend l’environnement garanti mais allonge chaquewt switchd’autant. Tout mettre enpost-startrend le switch instantané mais ne garantit rien à celui qui arrive derrière.La version 0.78 a renommé les variables de contexte.
worktreedevientworktree_path,repo_rootdevientrepo_path,main_worktreedevientrepo,main_worktree_pathdevientprimary_worktree_path. Les templates de configuration migrent tout seuls au chargement, mais les scripts qui lisent le JSON du hook, les templates--executeet les surcharges--varsont à reprendre à la main. Les exemples de cet article utilisent la nouvelle nomenclature.
Corvée 1, le .env. wt step copy-ignored recopie les fichiers gitignorés vers le nouveau worktree. Par défaut il les prend tous, ce qui est rarement ce que je veux. Un fichier .worktreeinclude à la racine restreint la liste :
# .worktreeinclude
.env
target/
[pre-start]
copy = "wt step copy-ignored"
Techniquement ce n’est pas un cache partagé en continu, c’est une copie par reflink : les blocs disque sont partagés tant qu’aucun fichier n’est modifié. La documentation annonce un target/ Rust de 14 Go copié en 20 secondes contre 2 minutes pour un cp -R. Je n’ai pas de projet de cette taille sous la main pour confirmer ce chiffre, mais sur mes projets la copie est effectivement instantanée.
Depuis la version 0.77, plus besoin de me fier à mon ressenti : la commande dit ce qu’elle a fait. Le résumé se termine par (reflinked, no extra disk), (full copy) ou un compte partiel. Utile, parce que le reflink dépend du système de fichiers : une même annonce de 14 Go copiés ne veut pas dire la même chose selon que les blocs ont été partagés ou que les octets ont réellement été écrits.
Corvée 3, les dépendances. Pour les projets Python, la documentation déconseille explicitement de copier le .venv, pour la raison de chemins absolus évoquée plus haut, et recommande de relancer uv sync. Je suis d’accord avec ce choix : uv sync est assez rapide pour que le jeu n’en vaille pas la chandelle. Le hook tient sur une ligne :
[post-start]
deps = "uv sync"
Il est en post-start parce que rien de ce que je lance juste après le switch n’en dépend. Si tu enchaînes sur un agent ou une suite de tests avec -x, passe-le en pre-start.
Pour les gestionnaires d’environnement comme mise ou direnv, il n’y a rien de spécifique à configurer : comme wt switch fait le cd, ils s’activent tout seuls. En revanche, chaque worktree étant un nouveau chemin sur le disque, il faut refaire l’étape de confiance une fois par worktree (direnv allow ou mise trust).
🚧 Attention : WorkTrunk ne contourne volontairement pas ce prompt de sécurité, même logique que pour l’approbation des hooks. C’est un petit frottement résiduel qui reste à ma charge, et je préfère qu’il en soit ainsi.
Corvée 4, l’image Docker. Je développe presque toujours dans un conteneur. En dev, le code est monté en volume : l’image ne bouge pas quand le code change, donc il n’y a rien de particulier à faire par worktree.
Ça se complique quand je veux une preview, ou tester ce qui partirait en production. Là je construis une image qui embarque le code source complet. Si je garde le même tag d’une branche à l’autre, j’écrase l’image précédente. Si je garde le même container_name, je ne peux pas faire tourner deux versions en même temps, Docker refuse le doublon.
La réponse est celle que je détaille dans la section suivante pour la base de données : dériver les noms de celui de la branche.
[pre-start]
set-vars = """wt config state vars set \
app_image='{{ repo }}:{{ branch | sanitize }}' \
app_container='{{ repo }}-{{ branch | sanitize }}'"""
docker build -t $(wt config state vars get app_image) .
docker run -d --name $(wt config state vars get app_container) \
$(wt config state vars get app_image)
Deux branches, deux images, deux conteneurs, aucune collision. Les variables sont définies en pre-start et non en post-start, pour être sûr qu’elles existent quand je tape la commande de build juste après le switch.
Reste le nettoyage, qui ne se fait pas tout seul :
[pre-remove]
cleanup-app = """docker rm -f "{{ vars.app_container }}" 2>/dev/null || true
docker image rm "{{ vars.app_image }}" 2>/dev/null || true"""
Les || true sont là parce que le conteneur ou l’image peuvent avoir déjà disparu : un hook de nettoyage qui échoue parce qu’il n’y a rien à nettoyer bloquerait la suppression du worktree. Supprimer l’image est un choix : elle est reconstructible, mais si tu veux pouvoir relancer une preview après coup, supprime la seconde ligne du hook et ne nettoie que le conteneur.
Restait la corvée 2, celle qui m’agaçait le plus. C’est aussi la configuration la plus intéressante, parce qu’elle montre que les hooks ne servent pas qu’à installer des dépendances : ils isolent complètement l’environnement d’exécution d’une branche, port et base de données compris.
[pre-start]
set-vars = """wt config state vars set \
container='{{ repo }}-{{ branch | sanitize }}-postgres' \
port='{{ ("db-" ~ branch) | hash_port }}' \
db_url='postgres://postgres:dev@localhost:{{ ("db-" ~ branch) | hash_port }}/{{ branch | sanitize_db }}'"""
[post-start]
db = """docker run -d --rm \
--name {{ vars.container }} \
-p {{ vars.port }}:5432 \
-e POSTGRES_DB={{ branch | sanitize_db }} \
-e POSTGRES_PASSWORD=dev \
postgres:18"""
[pre-remove]
db-stop = "docker stop {{ vars.container }} 2>/dev/null || true"
Le hook pre-start dérive un nom de conteneur, un port et une chaîne de connexion à partir du nom de branche, et les stocke comme variables persistées. Il est bloquant pour la même raison que côté Docker : définir trois variables ne coûte rien, et je veux qu’elles soient lisibles dès que la main m’est rendue. Le port est calculé par hash du nom de branche (hash_port, plage 10000-19999). Ça ne garantit rien au sens strict, dix mille ports pour un nombre illimité de branches finiront statistiquement par se croiser, mais ça m’évite surtout de tenir une liste des ports déjà pris. En cas de collision, c’est le démarrage du conteneur qui échoue, bruyamment, ce qui reste préférable à deux branches qui se parlent sans le savoir. Le hook post-start démarre ensuite Postgres en réutilisant ces variables. Le hook pre-remove arrête le conteneur quand le worktree disparaît.
J’ai gardé le démarrage de la base en post-start, donc en arrière-plan : lancer le conteneur prend une seconde, mais attendre que Postgres accepte réellement les connexions en prendrait plusieurs, à chaque wt switch. La contrepartie, c’est qu’une commande lancée dans la foulée avec -x peut trouver la base encore froide. Si ça compte pour ton projet, il faut passer ce hook en pre-start et y ajouter une attente active, en acceptant le délai au switch.
C’est ce dernier point qui me manquait le plus quand je faisais tout à la main : je créais des conteneurs, et j’en retrouvais trois qui tournaient encore une semaine plus tard.
La chaîne de connexion reste accessible ailleurs que dans les hooks, ce qui sert pour tout ce qui doit parler à la base :
DATABASE_URL=$(wt config state vars get db_url) alembic upgrade head
C’est la commande que je tape le plus souvent : appliquer les migrations de la branche sur sa propre base, sans toucher à celle d’à côté. La même substitution marche pour lancer l’application, ouvrir un psql, ou brancher n’importe quel outil qui lit DATABASE_URL.
Variante avec Docker Compose. Si la stack tourne déjà via docker-compose.yml, l’isolation est encore plus simple à obtenir. Compose dérive automatiquement son nom de projet, donc le préfixe des conteneurs, volumes et réseaux, du nom du dossier courant. Comme chaque worktree vit dans un dossier distinct, l’isolement est déjà acquis sans rien nommer à la main, contrairement à l’exemple docker run ci-dessus. Reste le port hôte, qui lui doit varier : deux stacks ne peuvent pas publier sur le même port. Le nom de la base, en revanche, peut rester identique d’un worktree à l’autre, puisque chaque worktree a son propre serveur Postgres. Je le fais quand même varier, pour reconnaître à quoi je suis connecté quand j’ouvre un client SQL.
Les deux valeurs arrivent par un fichier d’environnement que le hook génère :
# docker-compose.yml
services:
db:
image: postgres:18
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
POSTGRES_PASSWORD: dev
ports:
- "${POSTGRES_PORT:-5432}:5432"
[post-start]
db = """cat > .env.worktree <<EOF
POSTGRES_DB={{ branch | sanitize_db }}
POSTGRES_PORT={{ ("db-" ~ branch) | hash_port }}
EOF
docker compose --env-file .env --env-file .env.worktree up -d db"""
[pre-remove]
db-stop = "docker compose down -v 2>/dev/null || true"
Le fichier généré s’appelle .env.worktree et non .env, et c’est important : wt step copy-ignored vient justement de recopier mon .env, avec mes clés d’API et mes réglages locaux. L’écraser me les ferait perdre à chaque création de worktree. Compose accepte plusieurs --env-file et applique le dernier par-dessus le précédent, donc le .env fournit les valeurs locales et .env.worktree ne surcharge que le port et le nom de base. Si le projet n’a pas de .env du tout, il suffit de garder le second.
Si tu as un doute sur ce qui est réellement pris en compte, docker compose --env-file .env --env-file .env.worktree config affiche la configuration finale, toutes variables substituées. C’est comme ça que j’ai vérifié qu’une clé d’API définie dans le .env survivait bien à la surcharge.
Les valeurs par défaut (:-app, :-5432) gardent docker compose up fonctionnel hors worktree. Le -v du down supprime le volume en même temps que le conteneur, pour ne pas laisser traîner une base fantôme.
🚧 Attention : ce
.env.worktreegénéré par le hook doit être dans.gitignore. Il porte un port et un nom de base propres à une branche, il n’a rien à faire dans l’historique.
Ce qui a marché. Les quatre corvées ont disparu, et surtout elles ont disparu au bon endroit : je ne pense plus à elles au moment où je crée un worktree. L’effet secondaire intéressant, c’est que je crée maintenant des worktrees pour des choses que j’aurais faites en stash avant, parce que le coût d’entrée est tombé à une commande.
Le nettoyage automatique à la suppression est le gain que je n’avais pas anticipé. Ce n’était pas dans mon cahier des charges initial, et c’est pourtant là que je perdais le plus de choses : branches orphelines, conteneurs Postgres qui tournent encore, ports occupés sans raison.
Ce qui reste à ma charge. Le direnv allow ou mise trust à refaire par worktree. L’approbation des hooks au premier lancement dans un dépôt. Ce sont des choix de sécurité délibérés de l’outil, je les trouve justifiés, mais ce sont bien des frottements qui subsistent, et je n’ai pas cherché à les contourner.
Ce que je n’ai pas testé. Je n’ai pas essayé wtp en vrai. J’ai écarté l’option sur un critère de maintenance, pas sur une comparaison à l’usage, et il serait malhonnête de le présenter autrement.
S’il ne fallait retenir qu’un seul usage, ce serait celui-là. Et ce n’est pas pour lui que j’avais installé l’outil : c’est pourtant, à sept ans d’écart, exactement ce pour quoi on m’avait montré les worktrees en 2019, regarder le travail de quelqu’un d’autre sans sacrifier le sien.
Relire sérieusement la PR d’un contributeur, ça veut dire la faire tourner, pas seulement lire le diff d’un git diff ou dans le navigateur. C’est précisément ce que je repoussais.
Parce que faire tourner une PR sur mon dépôt principal, ça voulait dire mettre de côté mon travail en cours, basculer dessus, réinstaller les dépendances de sa branche, et surtout laisser ses migrations s’appliquer sur ma base de données. Pour ensuite tout défaire. Le coût de la revue n’était pas dans la lecture du code, il était dans le retour en arrière.
wt switch pr:123 supprime toute cette séquence. Un worktree neuf, les hooks qui s’enchaînent : .env copié, Postgres dédié sur son propre port, dépendances installées. Je teste la PR pour de vrai, dans un environnement complet qui n’est pas le mien. Puis wt remove : le conteneur s’arrête, la base part avec, et mon travail en cours n’a pas bougé d’un octet. La branche du contributeur, elle, reste tant qu’elle n’a pas été mergée, ce qui est le comportement que je veux pour une PR que je n’ai fait que relire.
C’est la combinaison qui fait le gain, pas l’une ou l’autre de ses moitiés. Le raccourci pr: tout seul n’est qu’un gh pr checkout un peu plus court. Les hooks tout seuls automatisent une création de worktree. Ensemble, ils rendent la revue d’une PR non destructive, donc assez peu coûteuse pour que je la fasse vraiment.
git worktree add, il est dans tout ce qui est gitignoré. .env, dépendances, base de données, images : c’est ce qui ne suit pas la branche qui fait mal.pre-remove est la partie qu’on bâcle quand on bricole soi-même, et c’est celle qui évite les conteneurs fantômes et les bases orphelines.Le bilan tient en une phrase : WorkTrunk répond à un vrai problème, celui que git worktree laisse entier. Git me donne un répertoire de travail propre et s’arrête là. Tout ce qui ne suit pas la branche, le .env, les dépendances, la base de données, les images, restait à ma charge, et c’est précisément ce qui me faisait renoncer aux worktrees dans les moments où ils servent le plus.
Ce qui rend l’outil intéressant au-delà de mon cas, c’est le choix des hooks déclaratifs. Ils ne règlent pas quatre corvées, ils règlent la catégorie : tout ce que tu sais écrire en une ligne de shell devient une étape du cycle de vie d’un worktree, déclenchée au bon moment et nettoyée à la fin. Mes exemples parlent de Postgres et de Docker, les tiens parleront d’autre chose, le mécanisme est le même.
C’est aussi ce qui en fait une brique solide pour travailler avec des agents. Un agent de codage a besoin d’exactement ce que WorkTrunk sait fabriquer : un environnement isolé, complet dès la première seconde, et jetable sans regret. wt switch -c ma-branche -x claude crée le worktree, déroule les hooks et lance l’agent dedans. Plusieurs agents en parallèle, chacun sur sa branche, avec sa base et son port, sans qu’aucun ne marche sur les plates-bandes des autres. Je n’avais pas installé l’outil pour ça, et c’est devenu une des raisons pour lesquelles je le garde.
Enfin, le projet est vivant, et ça compte dans la décision. Le rythme des releases, la qualité de la documentation et le soin mis aux détails, comme l’annonce explicite de ce que copy-ignored a réellement fait, se sentent à l’usage.
Merci à Maximilian Roos (max-sixty sur GitHub) pour WorkTrunk. Le dépôt est sur github.com/max-sixty/worktrunk et la documentation, vraiment bien faite, sur worktrunk.dev. Si l’outil te rend les mêmes services qu’à moi, une étoile sur le dépôt ou un retour d’usage est la moindre des choses.
git worktree vs wtCe tableau est une référence, pas un parcours de découverte : personne n’a besoin de ces vingt lignes pour commencer, wt switch et wt merge suffisent. Il sert à situer précisément ce que WorkTrunk ajoute par-dessus Git, commande par commande. Plusieurs commandes wt n’ont pas d’équivalent natif : elles couvrent des besoins que Git ne gère pas du tout.
| Tâche | git worktree (natif) |
wt (WorkTrunk) |
|---|---|---|
| Créer un worktree + branche | git worktree add -b feature ../repo.feature |
wt switch --create feature |
| Créer et lancer une commande dedans | git worktree add -b feature ../repo.feature && cd ../repo.feature && claude |
wt switch -c feature -x claude |
| Basculer vers un worktree existant | cd ../repo.feature |
wt switch feature |
| Lister les worktrees | git worktree list (chemins bruts) |
wt list (statut, avance/retard, CI, messages) |
| Parcourir/choisir un worktree | pas d’équivalent | wt switch sans argument (picker interactif avec preview live) |
| Comparer une branche sans y basculer | git diff main...feature (depuis un autre worktree) |
wt step diff --branch feature |
| Committer | git add -A && git commit -m "..." |
wt step commit (message généré par LLM à partir du diff) |
| Merger proprement (squash + rebase + fast-forward) | git checkout main && git merge --squash feature && git commit && git branch -d feature (+ nettoyage manuel) |
wt merge |
| Étapes du merge une par une | pas d’équivalent groupé | wt step commit / squash / rebase / push |
| Supprimer un worktree | git worktree remove ../repo.feature |
wt remove (+ suppression de branche si déjà mergée) |
| Forcer la suppression (changements non commités) | git worktree remove --force |
wt remove --force |
| Protéger un worktree contre la suppression | git worktree lock ../repo.feature --reason "..." |
git worktree lock directement, WorkTrunk réutilise la commande native ; depuis 0.78, wt remove et wt merge la respectent, même avec --force |
| Nettoyer les métadonnées orphelines | git worktree prune (fichiers administratifs seulement, jamais les branches) |
wt step prune (worktrees et branches déjà intégrées à main, avec garde d’âge minimum calculée depuis 0.78 sur le reflog de la branche, pas sur la date du commit) |
Copier .env, node_modules/, caches de build |
pas d’équivalent (copie manuelle) | wt step copy-ignored |
| Exécuter une commande dans tous les worktrees | boucle shell manuelle | wt step for-each -- <commande> |
| Hooks automatiques (création, commit, merge, suppression) | pas d’équivalent (scripts externes) | .config/wt.toml + wt hook |
| Reprendre une PR/MR GitHub/GitLab en worktree | gh pr checkout puis git worktree add manuel |
wt switch pr:123 |
Le tableau résume la philosophie de l’outil : git worktree reste la brique bas niveau que WorkTrunk pilote, il ne réinvente pas lock/unlock et les délègue tels quels à Git. L’essentiel de la valeur ajoutée vient des commandes sans équivalent natif, et pour mon usage, c’est copy-ignored et les hooks qui ont tout changé.