My current jjvcs configuration
I’ve been using jj exclusively for source control for at least half a year now, and I have really been enjoying it. I still find it quite hard to explain what makes it different or worth the switch from git, when I tried to talk about it with some friends and colleagues. Even myself took at least 3 failed attempts before actually diving in.
Even after reading the official tutorial and a book I still wasn’t convinced nor grasping what made it different. In fact, the catalyst was GitButler (before it got all these AI stuff) which was amazing experience to work on 4 or 5 different things at the same time, have all of them checked out locally yet still maintain well isolated PRs. I never have to worry about “do I just put this tiny change in even though it’s completely unrelated” anymore.
However, at least back then GitButler sometimes gets into really weird states, especially when trying to sync with remote, it eventually got really annoying because I work on different machines all the time, and I still prefer a command line interface to some degree, so I gave jj one more try and jump straight into the deep end.
To be honest, the transition was quite smooth, though I was already fairly proficient with git so it’s definitely a YMMV case. It’s only when I had to use git to help someone else that I shockingly realize how deeply I’ve tuned to my workflow in jj.
Instead of trying to write yet another tutorial, I just want to share my current config and explain how I use each part, hopefully it helps if someone struggles with certain workflow or is looking for inspirations.
[user]
name = "LOU Xun"
email = "my personal email"
[[--scope]]
--when.repositories = ["~/work"]
[--scope.user]
email = "my work email"
Just sets up the committer, jj allows for overriding configs based on various conditions, here I use my work email for anything work-related.
[signing]
behavior = "drop"
backend = "gpg"
key = "3E24C42F59C79E77"
[git]
sign-on-push = true
I’ve been using YubiKey to sign my commits for many years now, jj further improves this by only doing the signing on push, so I can keep experimenting on my sofa without having to reach for and connect my YubiKey.
[colors]
"diff token" = { underline = false }
Not sure if it’s changed now, but the original diff view has underlines on the changes, which are super annoying to me, fortunately there is config to turn it off.
[revset-aliases]
'WHERE_GIT' = 'first_parent(@)'
'DEFAULT' = 'coalesce(bookmarks(exact:"master"), bookmarks(exact:"main"))'
'NO_MOVE' = 'none()'
[[--scope]]
--when.repositories = ["~/work"]
[--scope.revset-aliases]
'NO_MOVE' = 'main'
I always use jj in the “colocated” mode, meaning that the repo is also a git repo so git tools can generally still work (if anything, editor integration for blame is non-negotiable).
Originally, jj has git_head() to help indicate where the git repo is checked out at, but it’s been removed in more recent versions, so WHERE_GIT replicates that information. I have found it nice to have especially during the early days of switching to jj, but have not really needed this information for a while.
DEFAULT is a convenient revset that points to master if it exists or else main. I personally always use master but work repos are using main so this helps enable some commands to work in both scenarios.
NO_MOVE is there to prevent moving bookmarks, at work we disable pushing directly to main in most repos so I want to avoid hitting that accidentally (mostly after a PR merge situation).
[aliases]
pre-commit = [
"util",
"exec",
"--",
"bash",
"-c",
"jj diff -r @ --name-only --no-pager | xargs pre-commit run -c tools/pre-commit/config.yaml --files",
]
pre-commit-branch = ["util", "exec", "--", "bash", "-c", """
#!/usr/bin/env bash
set -euo pipefail
FROM=$(jj log --no-graph -r "fork_point(trunk() | @)" -T "commit_id")
TO=$(jj log --no-graph -r "@" -T "commit_id")
pre-commit run -c tools/pre-commit/config.yaml --from="$FROM" --to="$TO" "$@"
""", ""]
hist = ["log", "-r", "::@"]
bl = ["bookmark", "list"]
bla = ["bookmark", "list", "-a"]
gf = ["git", "fetch"]
pp = ["git", "push"]
# fetch, then switch to DEFAULT is @ is empty
ff = ["util", "exec", "--", "bash", "-c", """
#!/usr/bin/env bash
set -euo pipefail
jj git fetch
if [[ $(jj log -r '@' -T 'self.empty()' -G) == "true" ]]; then
echo "\n---"
jj new DEFAULT
fi
""", ""]
cc = ["git", "push", "-c"]
# update bookmarks and push
up = ["util", "exec", "--", "bash", "-c", """
#!/usr/bin/env bash
set -euo pipefail
jj bookmark move --from 'heads(::@- & bookmarks()) ~ NO_MOVE' --to @- && \
jj git push
"""]
# make a new parallel branch
bb = ["new", "-A", "DEFAULT", "-B", "@", "--no-edit", "-m"]
# split to a new commit in parallel branch
ss = ["util", "exec", "--", "bash", "-c", """
#!/usr/bin/env bash
set -euo pipefail
bk=$(jj log -Gr $1 -T 'self.bookmarks()')
jj split -B @ -A $1
jj b m "$bk" -t "$bk"+
jj st
""", ""]
I have quite a few aliases, most of them are self explainatory, here are some additional note:
pre-commit runs the git pre-commit tool (also using our work repo config location) on just the current change, while pre-commit-branch targets the current branch.
bb and ss are my most recent addition and specifically helps parallel branching, when I’m already on a branch like this:
<$> jj st
The working copy has no changes.
Working copy (@) : wkly [nope!] (...)
Parent commit (@-): xktx louxun/uyqzlmxppkzv | change import lint rule
I can run jj bb [msg] to create a parallel branch against DEFAULT and continue to work on the combined changes:
<$> jj bb "demo paralle branch"
Created new commit uomu [nope!] demo paralle branch
Rebased 1 descendant commits
Working copy (@) now at: wkly [nope!] (...)
Parent commit (@-) : xktx louxun/uyqzlmxppkzv | change import lint rule
Parent commit (@-) : uomu [nope!] demo paralle branch
<$> jj bb "demo paralle branch2"
Created new commit toyk [nope!] demo paralle branch2
Rebased 1 descendant commits
Working copy (@) now at: wkly [nope!] (...)
Parent commit (@-) : xktx louxun/uyqzlmxppkzv | change import lint rule
Parent commit (@-) : uomu [nope!] demo paralle branch
Parent commit (@-) : toyk [nope!] demo paralle branch2
Then I can use jj squash to squash working copy directly into one of the new branch, or use jj ss [target] to make a new commit and automatically move the bookmark.
[ui]
editor = "nvim"
diff-editor = ":builtin"
diff-formatter = ":git"
conflict-marker-style = "git"
show-cryptographic-signatures = true
[remotes.origin]
auto-track-bookmarks = "glob:louxun/*"
[templates]
log = "builtin_log_oneline"
git_push_bookmark = '"louxun/" ++ change_id.short()'
[[--scope]]
--when.commands = ["status"]
[--scope.ui]
paginate = "never"
[[--scope]]
--when.commands = ["diff"]
[--scope.ui]
pager = ["sh", "-c", "diff-so-fancy | less --tabs=1,5 -R"]
I tried difftastic for a bit but it still got various issues so I’m back using the trusty diff-so-fancy. There are also configs for the branch (bookmark) names that jj cc automatically generates.
[template-aliases]
'format_short_change_id(id)' = 'id.shortest(4)'
'format_short_commit_id(id)' = ''
'format_timestamp(timestamp)' = 'timestamp.ago().remove_suffix(" ago")'
'format_short_cryptographic_signature(signature)' = '''
if(signature,
label("signature status", concat(
label(signature.status(), coalesce(
if(signature.status() == "good", "✓"),
if(signature.status() == "unknown", "?"),
"✗",
)),
))
)
'''
'format_short_signature_oneline(signature)' = '''
if(signature.email().local().contains("+"),
truncate_end(10, signature.email().local().substr(10, -1)),
signature.email().local(),
)
'''
'empty_commit_marker' = 'label("empty", "[nope!]")'
description_placeholder = '''
label("description placeholder", "(...)")
'''
Finally here are my jj log format customization, all in all it ends up like this:
@ lmrn aquarhead 43 seconds [nope!] (...)
◆ xmxs aquarhead 20 hours master ✓ More parallel branch helpers
│
~