Jujutsu VCS

19 septembre 2026

En ce moment je suis en train d’explorer jj pour voir si ça vaut le coup de faire le saut depuis git.

C’est un peu déroutant au départ quand on s’est déjà formé un modèle mental pour travailler avec git, mais on s’y fait assez rapidement, et on commence à trouver une certaine élégance dans la manière de faire différente.

Quelques concepts.

Commit VS Revision

L’unité de travail n’est plus le “commit”, mais la “révision”.

Le concept de commit est toujours présent étant donné que le “backend” de stockage reste git, mais ça devient de la tambouille interne de laquelle on n’a pas à se préoccuper. On travaille avec des révisions qui sont identifiés par un “change ID” stable, l’équivalent du hash de commit git.

jj describe -m 'Hello, World!' && jj new

# équivalent git
git add --all && git commit -m 'Hello, World!'

💡 Astuce

jj commit permet de faire jj desc && jj new en une seule commande. Pratique quand on est habitué à la méthode git qui consiste faire ses changements, et ensuite commit une fois le travail terminé.

Avec git, si on a oublié un truc on revient sur le commit à coup de git commit --amend. Avec jj, le describe est complètement décorrélé du moment où on acte le changement. Par exemple on peut très bien commencer par faire son jj desc pour indiquer sur quoi on travaille, et faire le jj new une fois qu’on a terminé et passe à autre chose.

jj describe -m 'feat!: replace capitalism by communism'
rm -rf ./capitalism/
mkdir ./communism/
echo "Let's end private property of the means of production." >>./communism/how-to.md
echo "Workers should be the owners of their company." >>./communism/how-to.md
jj new -m 'chore: live a happy life'

# équivalent git
git commit -m 'feat!: replace capitalism by communism' --allow-empty
...
git add --all
git commit --amend --no-edit

La commande jj new crée une nouvelle révision, dans laquelle toutes les futures modifications (jusqu’au prochain jj new) seront incluses. Par défaut sa description est vide, mais on peut également l’indiquer via -m si on sait déjà ce sur quoi on va passer.

Index VS Working copy

Vous l’aurez donc remarqué avec les exemples ci-dessus, avec jj on n’a pas fait de add. En effet, contrairement à git il n’y a pas d’index dans lequel il faut “stage” les changements avant de “commit”.

Tout le travail au sein du projet est intégré automatiquement à la “working copy”, c’est un peu comme si un git add automatique tournait en arrière-plan. Dans les faits, j’ai l’impression que c’est plus ou moins ce que jj fait à chaque jj status, il fait un “snapshot” qui est stocké dans git.

On peut le voir par exemple en faisant quelques modifications entre plusieurs jj status:

$ jj git init
Initialized repo in "."
$ jj log
@  mowmywvw nicolas@karolak.fr 2026-09-19 16:44:17 5186e1bb
│  (empty) (no description set)
◆  zzzzzzzz root() 00000000
$ jj status
The working copy has no changes.
Working copy  (@) : wtwprlxq 5186e1bb (empty) (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)

On peut voir plusieurs choses pour ce premier status:

$ touch hello
$ jj status
Working copy changes:
A hello
Working copy  (@) : wtwprlxq ef5503bd (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
$ touch world
$ jj status
Working copy changes:
A hello
A world
Working copy  (@) : wtwprlxq c88e167b (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)

Et si on fait un git log on va retrouver les commits git bien qu’on n’ait rien fait au niveau de jj (en dehors des status) :

$ git log --all
commit c88e167b5ac4058e1e4b1c3fdbb8f878c45d00c9
Author: Nicolas Karolak <nicolas@karolak.fr>
Date:   Sat Sep 19 16:22:41 2026 +0200

commit ef5503bd8deb9767c47c7f85162e8f6122620898
Author: Nicolas Karolak <nicolas@karolak.fr>
Date:   Sat Sep 19 16:22:41 2026 +0200

commit 5186e1bbcd0f44df24c3099f663a1a8e4274f5fc
Author: Nicolas Karolak <nicolas@karolak.fr>
Date:   Sat Sep 19 16:22:23 2026 +0200

Alors que le jj log n’a pas bougé, excepté le commit-id :

$ jj log
@  wtwprlxq nicolas@karolak.fr 2026-09-19 16:22:49 c88e167b
│  (no description set)
◆  zzzzzzzz root() 00000000

Maintenant imaginons qu’on a terminé de travailler et qu’on ne veuille pas intégrer le fichier world à notre révision, on lance la commande jj split qui va ouvrir un éditeur interactif au sein duquel on peut sélectionner les fichiers ou chunks à intégrer. Une fois la sélection faite, on confirme avec la touche c, ça ouvre l’éditeur de texte pour renseigner/modifier la description.

ℹ️ Note

On peut split n’importe quelle révision en passant -r à jj split -r <change-id>. Ce qui est nettement plus simple qu’un git rebase -i [...].

Branch VS Bookmark

Une autre différence notable entre git et jj est la manière dont sont gérées les branches, appelées “bookmarks” pour ce dernier. Mon modèle mental simplifié pour comment fonctionne une branche sous git est le suivant : une branche “contient” les commits, quand on fait un commit il fait partie de la branche.

En réalité une branche est seulement un pointeur vers un commit, seulement celui-ci est automatiquement attaché au dernier commit. Avec jj, le mouvement de ce pointeur n’est pas automatique. Il faut explicitement déplacer le bookmark avec la commande jj bookmark <action>, l’action pouvant être set ou move par exemple.

jj bookmark set main -r @-

Le @ est l’équivalent du HEAD sous Git : il indique sur quelle révision on se trouve dans l’historique. @- correspond donc à la révision parente de @.

Une fois le bookmark déplacé on peut pousser les changements via jj git push.

Sinon si on n’avait pas déplacé main on aurait pu faire jj git push -c @-, qui aurait créé et poussé une branche avec un nom aléatoire.

On peut également créer un bookmark via jj bookmark create feat/hello-world.

Conclusion provisoire

Voilà à peu près où j’en suis dans mon apprentissage de jj.

Pour l’instant, en dehors d’une UX un peu plus plaisante par certains aspects (simplicité de split/squash/rebase, jj undo aussi est merveilleux, etc.), je ne vois pas encore de raison de faire la transition. Notamment car lazygit existe et simplifie grandement les opérations compliquées sous git, mais si ce n’était pas le cas je n’aurais probablement pas hésité à changer de crèmerie. Aussi, pour l’instant toutes les intégrations dans les éditeurs, outils qu’il y autour, forges, etc. sont centrées autour de git. J’ai peur que ça implique certaines contorsions pour certains workflows/outils. Par exemple, il n’y pas encore d’équivalent pour les git hooks, donc pas moyen de faire fonctionner prek.

À noter qu’il existe un équivalent à lazygit pour jj : blazingjj, un fork de lazyjj qui lui n’est plus maintenu.