Fluent jj / aggressive aliasing
2026-04-19
I am very tired so I will get away with as much quoting myself as possible.
This is probably one of the most important parts of my ~philosophy of Computer~. From resources for improving as a software engineer:
- Get really comfortable with using your VCS. Learn to do black magic with git, and learn to do it fast. This will probably take a lot of aliases — scroll down to the “Git aliases” section on this page to see what I mean. You’ll write bad quality commit messages like “update project files” if you have to type out “git add blah” and “git commit” and “git push” every time you commit, and you’ll make larger, less useful, less self-contained commits too. If staging just what you want and committing and pushing it as as simple as “a”, “c”, “w”, you’ll do it more often. If amending a commit is as easy as “cx”, and reordering commits is as easy as “ri HEAD~10”, you’ll do it more. This is a basic observation from linguistics: more commonly used words/signs evolve to become shorter/simpler. Apply this to your own environment: see what you use most (check your shell history! pipe it into sort/uniq and find out what you do most!), create aliases for those, and retrain yourself to use them. (e.g. with git, create an alias for
gititself that just instead echos “bad! use the new aliases!”, and after a dozen or so tries you’ll get it. You can apply this technique in lots of places where your muscle memory would otherwise prevent you from adopting an improved/different workflow; see also unbinding keys in your editor.)
(Annoying caveat: fish deduplicates your history, so you can’t use this techinque!)
And from the first link:
Firstly, for all our git aliases, we create a shell alias with a
gprefix; i.e. if you havegit cdefined for commit, thengcat the shell invokes it. (The bare letters are used by jj aliases.)This isn’t overkill — I talk a bit about this early in Nix revisited, but I’d like to elaborate now: Git is a Swiss Army knife. It’s way better than what came before it in lots of ways, and I’m sure (and glad) folks are improving on it, but I’m very comfy with it, and not because I’ve abstracted it all away from me so I don’t have to think about it, but because I brought it close enough that all its little implements (continuing with the SAK analogy) are like my own fingers. Err. Or something.
(Git used to get the bare aliases, but upon switching to jj full-time, jj got those and Git ones all got a g prefix; it’s still handy to have the Git ones around, but if you’re just getting started with aggressive aliasing, there’s no need to create sets for both.)
Anyway, if Git is a Swiss Army knife, jj is … uhh. A combat spork? I dunno. You don’t need thirty different commands, some of which operate in a confusing range of ways (reset), others oddly specific, and the occasional Mega Tool which throws your entire workflow out the window and replaces it with a DSL (rebase -i).
To do this, you need to learn about the subtle ways in which jj adjusts your view on a Git repository. Git probably took some learning; jj takes much less, but it is weird because it shifts your perspective on something existing, rather than being totally new. You have to unlearn some things! It will not take long, but you do have to do it. Try Steve’s Jujutsu Tutorial; worked for me ¯\_(ツ)_/¯
Command aliases
Once done, start bringing the operations closer to hand. Here are mine, in groups and then roughly in order of use; remember that these are all shell aliases, so to jj new …, all I type is n …. Yours should be different — should be yours! — but maybe this will give some ideas.
Read:
-
s:status. (Actually an alias for plainjj, combined withui.default-command = ["status", "--no-pager"].) -
l:log. -
h:show. (sis taken, so proceed to the next letter.) -
d:diff. -
xd:interdiff.
Write:
-
n:new. It’s really nice to just be able tonto checkpoint.- Remember that
nin jj is “commit”, “switch branch”, “merge”, and more, and not because it does different things: jj’s model means they’re all the same basic operation. Combat spork.
- Remember that
-
m,mm:describe -m, anddescriberespectively. (mis the origin of the mnemonic and more frequently used.) -
r:rebase. -
p:split. -
e:edit. -
q,qi:squash,squash -i. -
z,zi:restore,restore -i. -
a:abandon. -
x:resolve. -
dp:duplicate.
Bookmarks and Git interop:
-
v,w:git fetch,git push. (Remember these arejj git fetchetc.) -
ny:bookmark advance. -
b,bt,bl,bd,bf:bookmark,… track,… list,… delete,… forget -
rl,ra,rr:git remote list,… add,… remove. -
i:git clone.
Fancy:
-
ab:absorb. -
me:metaedit. -
ep:evolog -p. -
op:op log -p. -
ntr:new trunk(). -
ud,rd:undo,redo. -
wa,wf,wl,wu:workspace add,… forget,… list,… update-stale. -
arr:arrange.
Argument aliases
We can do better. There are certain modifiers you’ll find yourself typing repetitively, and why should they be any different?
I map all the above aliases to call jj through a dispatcher fish function. You can write them in anything to do anything; here’s what mine does, in order of use:
- Changes
%blahto-r blah.- ex.:
m 'blah' %@-changes the commit message of the parent commit toblah. - ex.:
dp %a::b -o cduplicates changesa::bontoc.
- ex.:
- Changes
^blahto--remote blah. If you specify multiple^arguments, the command is run once for each remote.- ex.:
v ^a ^b ^cpulls from three remotes in succession.
- ex.:
- Performs the following argument substitutions:
-
!i→--ignore-immutable. -
!b→--allow-backwards. -
!s→--stat. -
!ua→--update-author(for use withme). -
!p→--no-pager(but I barely use this tbh).
-
- If
!Uis found, setsDELTA_FEATURES=+in the environment. (I have it set to+side-by-sideby default, so this shows a unified diff, which is nice for thin terminals.)
It’s not much, but I really find they help a lot! Make sure you do shell quoting carefully; I use fish interactively, so using a fish function made that very easy for me.
Bonus section
Some bits and pieces that make my jj experience nicer; you can refer to my config to see how to set some of them up:
- Delta is a syntax-highlighting, side-by-side capable diff pager. It works really well.
-
Mergiraf is a syntax-aware git merge driver! At least 50% of the time I can just type
xand the conflict is automatically resolved correctly. - Mark commits with messages starting
!!as private; don’t let them get pushed, define “closest pushable” commit as excluding them, and havejj bookmark advance(ny) advance to said commit:
I’ll name a commit[revset-aliases] "private()" = "description(glob:'!!*')" "closest_pushable(to)" = "heads(::to & mutable() & ~description(exact:'') & ~private() & (~empty() | merges()))" [git] private-commits = "private()" [revsets] bookmark-advance-to = "closest_pushable(@)"!! wipif I’m sure I don’t want to merge it.- Sometimes I’ll keep some local changes in a
!! localcommit (can bookmark it aslocalfor convenience), and work out of merges of my targetxand the local-only changes withn x local, selectively squashing local-only changes back withqi -t localand then splitting target changes out withp -t x. Rinse and repeat.
- Sometimes I’ll keep some local changes in a
- Make it easy to find the most recent common ancestor of any commit (especially the current one) with the trunk (
main,master, etc.):
I use this interactively; ex.[revset-aliases] "tfp()" = "tfp(@)" "tfp(with)" = "fork_point(trunk()|with)"d -f 'tfp()',z -f 'tfp()' FILE, maybe evenr %'tfp()::@ & ~tfp()' -o tfp(z)if you like.
hth!