<!--
  canonical-url: https://www.avonture.be/fr/blog/git-worktree
  published:     2026-06-29
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# git worktree : travailler sur deux branches en même temps

> git worktree permet de checkout plusieurs branches comme de vrais répertoires, en même temps — sans stash, sans perte de contexte. Gardez votre stack Docker en route pendant que vous traitez un hotfix en parallèle.

---
![git worktree : travailler sur deux branches en même temps](/img/v2/git_worktree.webp)

**TL;DR**

Vous êtes plongé dans une branche de feature — du code à moitié écrit, des containers Docker qui tournent, des fichiers ouverts partout. Et là : « hotfix urgent à faire sur main ». Avec le workflow habituel, vous stashez tout, vous changez de branch, vous corrigez, vous committez, vous poussez, puis vous dépilez le stash en espérant que rien n'explose. Avec `git worktree`, vous checkoutez une deuxième branch dans un dossier séparé. Les deux branches existent en même temps, comme de vrais répertoires, avec leurs propres terminaux et leurs propres stacks Docker.

Il est 15h, un jeudi. Vous travaillez sur une nouvelle fonctionnalité depuis deux jours. Les containers Docker tournent, votre éditeur a huit fichiers ouverts, et vous êtes au milieu d'un refactoring qui ne compile pas encore.

Votre téléphone vibre. La production est cassée. Un correctif doit partir sur `main` dans les trente minutes.

Vous faites quoi ?

## Le workflow du stash et sa douleur {#the-stash-workflow-and-its-pain}

L'approche classique :

```bash
$ git stash push -m "WIP: feature/user-notifications"
$ git checkout main
$ git pull
// ... on corrige le bug ...
$ git commit -m "fix: correct invoice calculation"
$ git push
$ git checkout feature/user-notifications
$ git stash pop
```

Ça marche. La plupart du temps. Mais :

* Vos containers Docker pointent maintenant vers le mauvais code — il faut les redémarrer
* Votre éditeur a perdu le contexte — tous ces fichiers ouverts ont disparu ou pointent vers les versions de main
* Si `git stash pop` part en conflit, vous voilà avec un joyeux bazar à trois branches à résoudre, encore sous le stress du hotfix
* Vous ne pouvez avoir qu'une seule branch active à la fois

Le problème de fond : un git stash est un état fragile. Ce n'est pas une branch. Il n'a pas d'historique. Il peut entrer en conflit. Et il disparaît dès que vous le dépilez.

*Puisqu'on parle de jongler avec les branches : [Git - Some tips for your .gitconfig file](/blog/git-config) montre comment trier `git branch` par date du dernier commit, et [Showing the last 3 updated branches when you jump in a git repo](/blog/git-branches-gst) les affiche automatiquement quand vous entrez dans un repository.*

## Ce que fait git worktree {#what-git-worktree-does}

Un git worktree est un deuxième (ou troisième, ou quatrième) répertoire de travail lié au même repository git.

Au lieu d'un dossier avec une seule branch checkoutée, vous obtenez :

```plaintext
~/projects/my-blog/                     ← main branch (original)
~/projects/my-blog-hotfix/              ← hotfix/invoice-fix branch (worktree)
~/projects/my-blog-feature-auth/        ← feature/auth branch (worktree)
```

Chaque dossier est un **vrai répertoire** avec ses propres fichiers, son propre terminal, ses propres processus en cours. Ils partagent la même base `.git` (commits, objets, historique), mais chacun a une branch checkoutée indépendamment.

Vous ne quittez jamais votre branch de feature. Le hotfix vit juste à côté.

## Les commandes de base {#the-basic-commands}

### Ajouter un worktree {#add-a-worktree}

```bash
$ git worktree add <path> [branch]
```

Crée un nouveau répertoire à l'emplacement `<path>` avec `[branch]` checkoutée :

```bash
$ git worktree add ../my-blog-hotfix hotfix/invoice-fix
```

Si la branch n'existe pas encore, créez-la en même temps :

```bash
$ git worktree add -b hotfix/invoice-fix ../my-blog-hotfix main
```

Cela crée `hotfix/invoice-fix` à partir de `main`, checkoutée dans `../my-blog-hotfix`.

### Lister tous les worktrees {#list-all-worktrees}

```bash
$ git worktree list
```

Sortie :

```plaintext
/home/christophe/projects/my-blog              a4b2c1d [main]
/home/christophe/projects/my-blog-hotfix       e8f3d2a [hotfix/invoice-fix]
```

### Supprimer un worktree {#remove-a-worktree}

```bash
git worktree remove ../my-blog-hotfix
```

Cela supprime le dossier et désenregistre le worktree. La branch elle-même (`hotfix/invoice-fix`) existe toujours — vous pouvez la supprimer séparément avec `git branch -d`.

## Le voir en action avec Docker {#seeing-it-in-action-with-docker}

`git` n'a pas besoin d'être installé — il est déjà sur votre machine. Ce qui vaut la peine d'être testé sans risque, c'est le scénario lui-même : un repo jetable, déjà en plein refactoring sur une branch de feature avec des modifications non committées, et un bug de calcul de TVA qui attend sur `main`. Exactement la situation du jeudi après-midi ci-dessous, sans le stress.

> **Pourquoi Docker d'abord ?:**
>
> Le Dockerfile ci-dessous construit un repo déjà positionné sur `feature/user-notifications` avec deux
> fichiers non stagés — le même point de départ que l'étape 1 ci-dessous — plus le navigateur fzf `gwt`
> décrit plus bas, branché via un vrai autoload ZSH. Rien n'est touché sur vos propres repos.

**Dockerfile**

```docker
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# Ubuntu's docker image only enables "main" + "restricted" by default; fzf
# (used by the gwt() navigator below) lives in "universe".
RUN sed -i 's/^Components: main restricted$/Components: main restricted universe multiverse/' \
        /etc/apt/sources.list.d/ubuntu.sources && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
        git \
        zsh \
        fzf \
        vim && \
    rm -rf /var/lib/apt/lists/*

RUN git config --global user.email "demo@example.com" && \
    git config --global user.name "Demo" && \
    git config --global init.defaultBranch main

# A bare repo standing in for GitHub/GitLab, so `git push origin ...` in the
# scenario below has somewhere real to go instead of failing on a missing
# remote.
RUN git init -q --bare /root/origin.git

# Build the exact starting point of "The hotfix scenario, step by step":
# a repo on `feature/user-notifications`, mid-refactor, with uncommitted
# changes — and a billing bug on `main` waiting to be hotfixed.
RUN mkdir -p /root/projects/my-blog/src/components /root/projects/my-blog/src/api /root/projects/my-blog/src/billing && \
    cd /root/projects/my-blog && \
    git init -q && \
    git remote add origin /root/origin.git && \
    printf 'export function Notifications() {\n  return null;\n}\n' \
        > src/components/Notifications.tsx && \
    printf 'export function fetchNotifications() {\n  return [];\n}\n' \
        > src/api/notifications.ts && \
    printf 'export function calculateVat(amount, rate) {\n  return amount * rate;\n}\n' \
        > src/billing/invoice.ts && \
    git add -A && \
    git commit -q -m "chore: update dependencies" && \
    git push -q origin main && \
    git checkout -q -b feature/user-notifications && \
    printf 'export function Notifications({ items }) {\n  return items.map((i) => i.text);\n}\n' \
        > src/components/Notifications.tsx && \
    printf 'export function fetchNotifications() {\n  return fetch("/api/notifications").then((r) => r.json());\n}\n' \
        > src/api/notifications.ts

# ~/.zsh/fns/gwt — the exact fzf-powered worktree navigator from this
# article, wired via real zsh autoload
RUN mkdir -p /root/.zsh/fns

RUN printf '%s\n' \
    '# ~/.zsh/fns/gwt' \
    '# git worktree navigator — select a worktree with fzf and cd into it' \
    '' \
    'gwt() {' \
    '  local worktrees selected dir' \
    '' \
    '  worktrees=$(git worktree list 2>/dev/null) || {' \
    '    echo "Not inside a git repository."' \
    '    return 1' \
    '  }' \
    '' \
    '  if [[ -z "$1" ]]; then' \
    '    # Interactive: pick a worktree with fzf' \
    '    selected=$(echo "$worktrees" \' \
    '      | fzf --prompt="Worktree > " \' \
    '            --preview="ls -la {1}" \' \
    '            --height=40%)' \
    '    [[ -z "$selected" ]] && return 0' \
    '    dir=$(echo "$selected" | awk '\''{print $1}'\'')' \
    '  else' \
    '    # Direct: create a new worktree for the given branch' \
    '    local branch="$1"' \
    '    dir="${$(git rev-parse --show-toplevel)}-${branch//\//-}"' \
    '    git worktree add -b "$branch" "$dir" main || return 1' \
    '    echo "Created worktree at $dir"' \
    '  fi' \
    '' \
    '  cd "$dir"' \
    '}' \
    > /root/.zsh/fns/gwt

RUN printf '%s\n' \
    'fpath=(~/.zsh/fns $fpath)' \
    'autoload -Uz gwt' \
    >> /root/.zshrc

WORKDIR /root/projects/my-blog

# Signal clearly that this shell runs inside a container
RUN echo "PROMPT='%B%F{yellow}🐳 %F{cyan}%n%f%b %~ %# '" >> /root/.zshrc

CMD ["/bin/zsh", "-i", "-l"]
```

Construisez-le et lancez-le :

```bash
$ docker build -t worktree-demo .
[+] Building 24.1s (8/8) FINISHED
✔ exporting to image
$ docker run --rm -it worktree-demo
🐳 root ~/projects/my-blog # git status
On branch feature/user-notifications
Changes not staged for commit:
modified:   src/api/notifications.ts
modified:   src/components/Notifications.tsx
```

C'est l'étape 1 ci-dessous, déjà en place. Maintenant, déroulez le reste du scénario pour de vrai, exactement comme
écrit — `git worktree add -b hotfix/invoice-fix ../my-blog-hotfix main`, corrigez
`src/billing/invoice.ts` dans le nouveau dossier, committez, `git worktree remove`, et vérifiez avec `git
status` que votre branch de feature n'a pas bougé. `gwt` est branché aussi, si vous voulez essayer le
navigateur fzf de la section « Une fonction ZSH » plus bas.

> **Lancez les commandes de l'étape 4 une par une:**
>
> Ne collez pas tout le bloc de l'étape 4 d'un coup : `vim` est un éditeur plein écran, donc tout ce
> que vous collez après `vim src/billing/invoice.ts` est tapé *dans vim* au lieu d'être exécuté comme
> commandes shell. Lancez cette ligne seule, faites la modification (voir l'astuce à l'étape 4), sauvez et quittez, puis collez
> le reste. `git push` fonctionne aussi dans ce container — l'image branche un repo bare local comme `origin`,
> il y a donc une vraie cible où pousser, sans compte GitHub.

## Le scénario du hotfix, étape par étape {#the-hotfix-scenario-step-by-step}

Retour au jeudi après-midi. Voici la même situation traitée avec des worktrees.

### 1. Vous êtes sur votre branch de feature, en plein travail {#1-youre-on-your-feature-branch-in-the-middle-of-work}

```bash
// État actuel
$ git branch
* feature/user-notifications
main
$ git status
On branch feature/user-notifications
Changes not staged for commit:
modified: src/components/Notifications.tsx
modified: src/api/notifications.ts
```

Vous n'y touchez pas. Vous ne stashez rien.

### 2. Créez un worktree de hotfix à côté de votre projet {#2-create-a-hotfix-worktree-next-to-your-project}

```bash
git worktree add -b hotfix/invoice-fix ../my-blog-hotfix main
```

```bash
Preparing worktree (new branch 'hotfix/invoice-fix')
HEAD is now at a4b2c1d chore: update dependencies
```

### 3. Ouvrez un nouveau terminal, allez dans le dossier du hotfix {#3-open-a-new-terminal-cd-into-the-hotfix-folder}

```bash
cd ../my-blog-hotfix
```

Ce répertoire contient les fichiers de `main`. Le répertoire de votre branch de feature est intact, toujours en cours d'exécution, toujours ouvert dans votre éditeur.

### 4. Corrigez le bug et poussez {#4-fix-the-bug-and-push}

```bash
// Dans ../my-blog-hotfix
vim src/billing/invoice.ts
git add src/billing/invoice.ts
git commit -m "fix: correct VAT calculation for EU invoices"
git push origin hotfix/invoice-fix
// on ouvre la PR, on merge, terminé
```

> **Le vrai correctif TVA:**
>
> `calculateVat()` retourne `amount * rate` — juste la part de TVA, pas le total de la facture. Dans vim :
> appuyez sur `i`, changez la ligne en `return amount * (1 + rate);`, puis `Esc` suivi de `:wq` pour sauver
> et quitter. Seulement ensuite, lancez les lignes `git add`/`commit`/`push`.

### 5. Nettoyez le worktree {#5-clean-up-the-worktree}

```bash
// De retour dans le repo d'origine
git worktree remove ../my-blog-hotfix
git branch -D hotfix/invoice-fix
```

> **Pourquoi -D et pas -d:**
>
> Vous êtes toujours sur `feature/user-notifications` ici, qui n'a jamais reçu le commit du hotfix — seul
> `origin/hotfix/invoice-fix` l'a reçu, via la PR mergée. `git branch -d` refuse de supprimer une branch qu'il
> ne voit pas comme mergée dans votre branch *courante*, et sort l'erreur « not fully merged ». Comme vous savez
> qu'elle est bien mergée sur le remote, `-D` (force) est le bon choix, pas un contournement d'erreur.

### 6. Retour à votre branch de feature — rien n'a changé {#6-back-to-your-feature-branch--nothing-changed}

```bash
cd ~/projects/my-blog
// Containers Docker : toujours en route
// Éditeur : toujours ouvert sur les mêmes fichiers
//git status : exactement comme vous l'aviez laissé
```

Pas de stash, pas de conflits, pas de changement de contexte.

## Une contrainte : une branch ne peut être que dans un seul worktree à la fois {#one-constraint-a-branch-can-only-be-in-one-worktree-at-a-time}

Git l'impose. Si `feature/user-notifications` est checkoutée dans votre répertoire principal, vous ne pouvez pas ajouter un second worktree avec la même branch. Si vous essayez :

```bash
fatal: 'feature/user-notifications' is already checked out at '/home/christophe/projects/my-blog'
```

C'est volontaire — deux répertoires écrivant simultanément sur la même branch corrompraient l'arbre de travail. Chaque worktree a sa propre branch.

## Docker + worktrees : chaque branch a sa propre stack {#docker--worktrees-each-branch-gets-its-own-stack}

C'est là que les worktrees deviennent vraiment puissants dans un workflow Docker.

Chaque worktree est un répertoire séparé. Chaque répertoire peut avoir son propre fichier `.env` et sa propre stack Docker Compose en route, tant que vous évitez les conflits de ports.

Une convention pratique : un port différent par worktree. Gardez les ports dans `.env`, qui est dans le `.gitignore` :

```bash
// ~/projects/my-blog/.env
APP_PORT=3000
DB_PORT=5432
// ~/projects/my-blog-hotfix/.env
APP_PORT=3001
DB_PORT=5433
```

Vous pouvez maintenant lancer les deux stacks en même temps :

```bash
// Terminal 1 — branch de feature
cd ~/projects/my-blog
docker compose up
// Terminal 2 — branch de hotfix
cd ~/projects/my-blog-hotfix
docker compose up
```

Les deux serveurs tournent en même temps. Vous pouvez tester le hotfix sur `http://localhost:3001` sans arrêter le développement de votre feature sur `http://localhost:3000`.

> **Projets Compose nommés:**
>
> Docker Compose utilise le nom du dossier comme nom de projet par défaut. Comme vos worktrees sont dans des dossiers différents, Compose crée automatiquement des réseaux et des ensembles de containers séparés — aucune configuration supplémentaire n'est nécessaire.

## Une fonction ZSH pour naviguer entre les worktrees {#a-zsh-function-to-navigate-between-worktrees}

Si vous suivez le pattern du [workflow ZSH modulaire](/blog/modular-zsh-workflow), vous pouvez ajouter un navigateur interactif de worktrees basé sur [fzf](/blog/linux-fzf-introduction) :

**gwt.zsh**

```bash
# ~/.zsh/fns/gwt
# git worktree navigator — select a worktree with fzf and cd into it

gwt() {
  local worktrees selected dir

  worktrees=$(git worktree list 2>/dev/null) || {
    echo "Not inside a git repository."
    return 1
  }

  if [[ -z "$1" ]]; then
    # Interactive: pick a worktree with fzf
    selected=$(echo "$worktrees" \
      | fzf --prompt="Worktree > " \
            --preview="ls -la {1}" \
            --height=40%)
    [[ -z "$selected" ]] && return 0
    dir=$(echo "$selected" | awk '{print $1}')
  else
    # Direct: create a new worktree for the given branch
    local branch="$1"
    dir="${$(git rev-parse --show-toplevel)}-${branch//\//-}"
    git worktree add -b "$branch" "$dir" main || return 1
    echo "Created worktree at $dir"
  fi

  cd "$dir"
}
```

Utilisation :

```bash
gwt                         # fzf picker — select any active worktree and cd into it
gwt feature/dark-mode       # create + cd into a new worktree for that branch
```

## Astuces pratiques {#practical-tips}

### Nommez les worktrees de manière cohérente {#name-worktrees-consistently}

Un schéma de nommage prévisible évite la confusion :

```bash
# Pattern: <repo-name>-<branch-slug>
git worktree add ../my-blog-hotfix-invoice hotfix/invoice-fix
git worktree add ../my-blog-feat-auth      feature/auth
```

### Les worktrees fonctionnent avec VS Code {#worktrees-work-with-vs-code}

Ouvrez un dossier de worktree dans une nouvelle fenêtre VS Code :

```bash
$ code ../my-blog-hotfix
```

Chaque fenêtre fonctionne indépendamment, avec ses propres fichiers ouverts et ses propres sessions de terminal.

### Élaguez les worktrees obsolètes {#prune-stale-worktrees}

Si vous supprimez manuellement un dossier de worktree (au lieu d'utiliser `git worktree remove`), git peut encore croire qu'il existe. Nettoyez les références obsolètes :

```bash
git worktree prune
```

## Conclusion {#conclusion}

|                         | `git stash`        | `git worktree`                |
| ----------------------- | ------------------ | ----------------------------- |
| Changement de branch    | Oui, obligatoire   | Non — les branches coexistent |
| Containers Docker       | À redémarrer       | Tournent indépendamment       |
| Contexte de l'éditeur   | Perdu              | Préservé                      |
| Développement parallèle | Non                | Oui                           |
| Risque de conflit       | Au `stash pop`     | Aucun                         |
| Nettoyage               | Automatique au pop | Manuel : `worktree remove`    |

Le stash reste utile pour les petits trucs rapides — économiser une demi-ligne de frappe avant un `git pull`. Dès qu'il s'agit de services en cours d'exécution, d'une session d'éditeur ou de plus de cinq minutes de travail, un worktree est plus propre.

La première fois que vous traiterez un hotfix sans quitter votre branch de feature, vous vous demanderez comment vous avez pu travailler autrement.
