Jujutsu VCS
19 septembre 2026En 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 commitpermet de fairejj desc && jj newen 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:
- la working copy est vide
- on a déjà un change-id :
wtwprlxq - et un commit-id :
5186e1bb
$ touch hello
$ jj status
Working copy changes:
A hello
Working copy (@) : wtwprlxq ef5503bd (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
- on a maintenant une modification dans la working copy
- le change-id n’a pas changé (il ne changera pas jusqu’au prochain
jj new) - le commit-id a changé :
ef5503bd
$ 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)
- un nouveau changement
- change-id toujours inchangé
- commit-id encore différent :
c88e167b
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’ungit 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.