Configuring Jujutsu
There are a lot of reasons to use jujutsu, but this post is not
about that. I have this terrible habit of turning every knob on a tool
before I even fully absorb how it works; and boy does jj
have a lot of knobs.
Templates
jj
let you tweak nearly every single character of its
output. In fact, jj
includes a full-blown templating
language to do so. Lets start from the basics:
Format all timestamps in relative fashion, like “5 hours ago”:
[template-aliases]
"format_timestamp(timestamp)" = "timestamp.ago()";
The default nodes in the log graph are a bit large for my taste, we
can modify the log_node
template to fix that:
[templates]
log_node = '''
label("node",
coalesce(
if(!self, label("elided", "~")),
if(current_working_copy, label("working_copy", "@")),
if(conflict, label("conflict", "×")),
if(immutable, label("immutable", "*")),
label("normal", "·")
)
)
'''
This uses smaller iconography in jj log
(these icons are
also more widely available in fonts, in my experience):
@ wuuownsw me@oppi.li 21 minutes ago 30d1bd12
│ (no description set)
· plmznxvy me@oppi.li 5 hours ago push-plmznxvyqrqw git_head() 051c142e
│ appview: pulls: bump sourceRev for stacks without causing resubmits
│ * towvwqxk x@icyphox.sh 4 hours ago master a588f625
│ │ appview: pages/markup: don't double camo in post process
│ * ryzqnyvs me@oppi.li 4 hours ago ea4b520a
│ │ appview: fix stack merging
│ * kqvutzxr me@oppi.li 4 hours ago ff73ca23
├─╯ appview: rework RepoLanguages
* vwpqwmms jeynesbrook@gmail.com 8 hours ago d759587b
│ appview: repo: inject language percentage into repo index
~
Much nicer! And speaking of log
, I find it hard to parse
the output when every change occupies two lines instead of just one
line, we can remedy that quickly; in fact, jj
has a builtin
template for single-line log outputs:
λ jj log -T builtin_log_oneline
@ wuuownsw me@oppi.li 22 minutes ago 30d1bd12 (no description set)
· plmznxvy me@oppi.li 5 hours ago push-plmznxvyqrqw git_head() 051c142e appview: pulls: bump sourceRev for stacks without causing resubmits
│ * towvwqxk x@icyphox.sh 4 hours ago master a588f625 appview: pages/markup: don't double camo in post process
│ * ryzqnyvs me@oppi.li 4 hours ago ea4b520a appview: fix stack merging
│ * kqvutzxr me@oppi.li 4 hours ago ff73ca23 appview: rework RepoLanguages
├─╯
* vwpqwmms jeynesbrook 8 hours ago d759587b appview: repo: inject language percentage into repo index
│
~
Bit too long! I personally do not always need author and time
information when quickly scrolling through the log, so I created my own
template based on builtin_log_oneline
:
[templates]
log = '''
if(root,
format_root_commit(self),
label(if(current_working_copy, "working_copy"),
concat(
separate(" ",
format_short_change_id_with_hidden_and_divergent_info(self),
if(empty, label("empty", "(empty)")),
if(description,
description.first_line(),
label(if(empty, "empty"), description_placeholder),
),
bookmarks,
tags,
working_copies,
if(git_head, label("git_head", "HEAD")),
if(conflict, label("conflict", "conflict")),
if(config("ui.show-cryptographic-signatures").as_boolean(),
format_short_cryptographic_signature(signature)),
) ++ "\n",
),
)
)
'''
Which produces:
@ wuuownsw (no description set)
· plmznxvy appview: pulls: bump sourceRev for stacks without causing resubmits push-plmznxvyqrqw HEAD
│ * towvwqxk appview: pages/markup: don't double camo in post process master
│ * ryzqnyvs appview: fix stack merging
│ * kqvutzxr appview: rework RepoLanguages
├─╯
* vwpqwmms appview: repo: inject language percentage into repo index
│
~
Sweet! To get a more detailed log, you can always use a different
template for the output:
jj log -T builtin_log_detailed
.
With git, I set commit.verbose
to true, this lets me
view the diff when composing a commit messaege in my
$EDITOR
:
λ git commit
# no message passed, opens $EDITOR to compose message
# -- inside vim --
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch trunk
# Changes to be committed:
# new file: scripts/handle.js
#
# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
diff --git a/scripts/handle.js b/scripts/handle.js
new file mode 100644
index 0000000..d8a07f3
--- /dev/null
+++ b/scripts/handle.js
@@ -0,0 +1,104 @@
+// Run using node handle.js
+// Install axios using npm install axios
+// nodejs v18+
+const axios = require("axios");
.
.
.
The equivalent in jj
requires modifying the
draft_commit_description
template:
[templates]
draft_commit_description ='''
concat(
coalesce(description, default_commit_description, "\n"),
surround(
"\nJJ: This commit contains the following changes:\n", "",
indent("JJ: ", diff.stat(72)),
),
"\nJJ: ignore-rest\n",
diff.git(),
)
'''
Revsets
As a not-so-power-user, I presently only use revsets to improve my
jj log
experience. In the context of jj log
,
giving it a revset means “here is a bag of revisions, graph them for me
neatly”.
I find myself using the “rebase” flow quite often:
- I hack on a stack of changes
trunk
is updated by my fellow collaborators- I rebase my stack using
jj rebase -s <mine> -d <trunk>
And I scrub through the log output to roughly figure out how much work it would be to rebase, what I need for this is:
- the changes added to
trunk
since I last diverged from it - the changes add to my stack since I last diverged from
trunk
X--Y--Z my stack
/
F--A--B--C--D trunk
If you want jj log
to print this (and by “this”, I mean,
the graph above, exactly as presented), you have to supply it a
revset
argument that grabs X, Y, Z, A, B, C, D and F. Some
examples of revsets are:
@ # the rev marking the working copy
x # the rev identified by shorthand `x`
x | y # the set of two revs, [x, y]
@ | trunk() # the set of two revs, [@, trunk()]
.. # everything in this repository
all() # also everything in this repository
fork_point(@ | trunk()) # the rev where @ and trunk() forked off
And the revset that captures “ahead-behind” style output is:
trunk()..@ | @..trunk() | trunk() | @:: | fork_point(trunk() | @)
Which includes:
trunk()..@
: the new changes that my collaborators have added@..trunk()
: the new changes that I have addedtrunk()
: the trunk rev itself@::
: all descendants of@
fork_point(trunk() | @)
: the rev from which the two streams of work diverged
Rougly, when working on a stack, this is what I can see by supplying the above revset expression (simplified output):
λ jj log -r 'trunk()..@ | @..trunk() | trunk() | @:: | fork_point(trunk() | @)'
@ xdihgmke <-- me
· vurstull
· plmznxvy
· ihefghyy
│ * towvwqxk trunk <-- trunk
│ * ryzqnyvs
│ * kqvutzxr
├─╯
* vwpqwmms <-- fork-point
And sometimes, when I am editing older parts of my stack
(@::
helps us grab xdihgmke
and
vurstull
):
λ jj log -r 'trunk()..@ | @..trunk() | trunk() | @:: | fork_point(trunk() | @)'
· xdihgmke
· vurstull
@ plmznxvy <-- me
· ihefghyy
│ * towvwqxk trunk <-- trunk
│ * ryzqnyvs
│ * kqvutzxr
├─╯
* vwpqwmms <-- fork-point
To rebase, I would run:
jj rebase -s ihefghyy -d towvwqxk
Aliases
I imagine that most git power users are already familiar with aliases. The only alias I see myself using often is:
[aliases]
tug = ["bookmark" "move" "--from" "heads(::@- & bookmarks())" "--to" "@-"];
In action:
# ugh my bookmark is way behind
λ jj log
@ xdihgmke
· vurstull
· cyzmakil
· plmznxvy
· ihefghyy some-bookmark
│
~
λ jj tug
# ready to push!
λ jj log
@ xdihgmke
· vurstull some-bookmark*
· cyzmakil
· plmznxvy
· ihefghyy
│
~
This alias was stolen from this wonderful gist by Austin Seipp.
Experimental features
I use one experimental feature, that is only available on some of the newer versions of jujutsu:
# built from master
λ jj version
jj 0.29.0-8c7ca30074767257d75e3842581b61e764d022cf
[git]
write-change-id-header = true
This writes an extra commit-header (not to be confused with commit-trailer, which goes directly in the commit body) with the jujutsu change-id, as seen by inspecting the commit object:
λ git cat-file commit 4edebe96159bf81c3be662d0a2b4c7b08a062968
tree a9b7e9e3eed8a22c35829bf586d7560ec8396124
parent edc0d2750d4848bc05cfd3255ee1ca916bea9156
author oppiliappan <me@oppi.li> 1747919102 +0100
committer oppiliappan <me@oppi.li> 1747919102 +0100
change-id skrrxvvxlpzrqzpxlxksvryrykpxkvon <-- this
This allows code forges to extract change-ids from commits, and most crucially: allows tracking changes across force pushes. This enables the interdiff format of code-review. Stacking, commit-wise-review, and interdiff are all supported on tangled (you can read more here), if you submit a branch with this experimental feature enabled.
Misc
Couple of other quality of life additions:
[ui]
default-command = "status"
pager = "delta"
I also use nix and home-manager to configure jj, you can find my configuration here.
I'm Akshay, programmer, pixel-artist & programming-language enthusiast.
I am currently building tangled.sh — a decentralized code-collaboration platform.
Reach out at oppili@libera.chat.