Skip to content(if available)orjump to list(if available)

JJ Cheat Sheet

JJ Cheat Sheet

31 comments

·February 12, 2025

sundarurfriend

The biggest blocker for me to switch to using jj currently is https://github.com/jj-vcs/jj/issues/3949 which is that it doesn't have a `core.fileMode` option equivalent. What this means in practice is that it's unusable from WSL when your repo is on an NTFS filesystem. It gets confused by NTFS's lack of executable permission bit and makes spurious changes/commits.

It's a small thing, but while jj adds some convenience, it doesn't add enough (for me) to offset the inconvenience of changing my workflow to not use WSL. (Another relatively minor inconvenience is its inability to use your SSH configs. So if you have multiple key pairs and need to use specific ones that aren't jj's default pick, an ssh-agent is the only way.)

That said, I would 100% recommend jj over git for any new programmer who hasn't yet had to contort their brain already into the git ways. All the things that git's UI does a great job of obscuring and presenting in a confusing way, jj presents in a straightforward way that makes sense and is easy to remember.

steveklabnik

Ah, I use `jj` on WSL every day, so I was confused at first, but that's because I don't share the drive there any more. That absolutely makes sense as a blocker. The autocrlf bit is a bit of a bummer on Windows, too.

> Another relatively minor inconvenience is its inability to use your SSH configs.

This should be much better as of last week's release, when you can say "please use git as a subprocess rather than libgit2/gix".

stouset

> This should be much better as of last week's release, when you can say "please use git as a subprocess rather than libgit2/gix".

I understand the decision here from an SSH-support situation, but doesn't this feel like a bit of a step backwards?

infogulch

Is there a short description of how jj works specifically from the perspective of a seasoned git user? I more or less understand git -- how to use it as well as its building blocks -- so the caveats and generalizations and glossing-over that are appropriate for a more general audience seem to get in the way of my understanding what's going on underneath.

steveklabnik

jj is truly its own VCS, so to deeply understand it, it's more than short. But it does map to git, and so you can sorta explain it in git terms. It's really kind of like "what if you tried to build hg on top of git?"

jj is kind of hard to really explain because a bunch of the design decisions have subtle but important impacts on other decisions, so your first impression of a feature may be slightly wrong because you don't get the implications yet.

jj is sort of the same as git: you have a DAG of snapshots of your project. The differences are in how you interact with those things. To try and put it in git terms:

1. Commits are mutable, not immutable (but we'll talk about is more later)

2. You're always working in the context of some commit

3. When you modify a file, it becomes part of that commit (we'll talk about the index in a minute)

4. You don't need to care about branches at all, the "detached head" state is the nrom.

5. commits are immutable in the "immutable data structures" sense, in that whenever you modify them, it's almost like you're adding a commit to them. this is why jj calls its "commits" "changes", change IDs stay stable as you edit them, and they produce new git commits for every edit.

6. Because of how this all fits together, you don't need an explicit index; if you want one, you can just `jj new` twice to get two changes on top of each other, and then edit your files. When you have what you want, `jj squash` will move the diff into the parent commit, and now it's "part of that feature" or whatever. If you want `git add -p`, that's `jj squash -i`.

That is kind of it on some level, but in reality, it's kind of hard to convey how a few, smaller, more orthogonal primitives let you do everything you can do in git, but easier. (I tried to actively think of cases last night and only came up with two or three that were easier in git than jj, and jj will have fixes for most of those soonish.)

stashing is another great example of a feature of git that's just a workflow pattern in jj.

There's just... it's a lot. It's hard to know what the best thing really is. Other than `jj undo` :)

(I've got this on the brain since I am literally working on my tutorial right now)

danpalmer

This is a pretty good summary of my experience and a small set of steps to understand to see why it's different – the idea that features of git are workflows/patterns in JJ is a nice one.

> it's kind of hard to convey how a few, smaller, more orthogonal primitives let you do everything you can do in git, but easier

Some of this didn't really click for me until I experienced it (and I'm still very much learning). The one that sticks out is how you're always in a commit. Where in git you work in "modes" – editing in the index, rebasing, committing, etc. In jj you're always "stable" and can do anything from that point.

The way this is sold is things like "mutable commits" or "first class conflicts", but for me the real power was just realising that I can always move to another commit/change without having to pre-plan how to do that, always being able to edit my commit message right now without having to finish up something else first. Now going back to git feels like the tool is slowing me down and not keeping up with the pace and style I want to work in. I was surprised that this was the thing I most enjoy, because it's a little hard to motivate.

rtpg

Workflow-wise I was struggling a bit to figure out how to work with the bookmarks in jj not moving along.

I now have a bit of a new strategy:

- When starting up a branch, right up the "final commit message", along with a bunch of notes on what I think I need and other TODOs

- While working, when I want to checkpoint some work, I use jj split to chop up some chunk of work, describe it, and then edit up my TODOs

this way the tip of my branch is always this WIP commit describing my end goal, I can find it, and I can add a bookmark to it.

Instead of git "I add changes over time and make my commit graph move forward", it's "I have a final commit node and add commits _behind it_". Been working well enough.

christophilus

I’ve been using it for a bit. It is magical. I had some merge conflicts in a GitHub PR. I pulled everything down with jj, and the conflicts were gone. Pushed and presto.

I do end up with descriptionless bookmarks that won’t push without a flag. So, I’m still doing something wrong.

But it’s already saved me a few times this week during some gnarly refactoring and merges.

rtpg

Unfortunately my merging experience has been mixed. I find the idiosyncratic merge markers to be overtly futzy (why are you indenting everything by a single character?), and the conflict resolution still is quite hand-hold-y.

It's never been wrong, but I am slightly unconvinced at how well merge conflict resolution works relative to git + rerere.

steveklabnik

If you truly miss bookmarks moving like branches, you can give this configuration option a try: https://github.com/jj-vcs/jj/discussions/3549

rtpg

I have been finding the workflow I described to be quite helpful, because it also provides me a scratchpad that I associate to the branch. And since the tip is never the git head so when I have to fallback to git I'm "sure" I'm not operating on that.

I'm still experimenting with things, but I think my overall takeaway is that in jj my working copy is "on branch", so I should lean into that rather than try to emulate my older workflows too much. And this new workflow... I just find it better!

steveklabnik

Oh yeah, I don't think your workflow is bad, you're not the first person who I've heard of give it a try. Just wanting to make sure you knew it was possible!

notmywalrus

You may be interested in the semi-standard `jj tug` alias [1], that moves "the most recent" bookmark up to `@-`

[1]: https://github.com/jj-vcs/jj/discussions/5568

oniony

This is great, thanks.

nchmy

Thanks for this! Just yesterday I decided to finally start really using jj with real work, whereas I had only fiddled around with it a few times while following tutorials over the past year.

One suggestion - reverse the direction of the arrows: q->r rather than q<-r

justinpombrio

Thanks for the kind word!

Unfortunately the arrows are kind of confusing regardless of which way they go. You're suggesting they point forward in time, from the old commit to the new commit. The way they're drawn is the direction of the reference: a commit points at its parent. The argument in favor of each way the arrows could go feel about equally strong to me, and my understanding is that the convention in repo diagrams is for arrows to go in the direction of the reference, so that's what I went with.

gjm11

I think I'm confused by the box about "jj abandon", which (unlike all the other boxes) talks about "edits" rather than "files" -- which must be a deliberate choice because the legend at the bottom lists "edits" and "files" separately.

Shouldn't "edits" be attached to the arrows rather than the nodes in the graphs? So not r [edit3] --> q [edit2] --> p [edit1] but r --[edit3]--> q --[edit2]--> p --[edit1]--> o, where o is p's predecessor. (I think you could do without "edit1" here.)

And then "jj abandon q", if I'm understanding it right, turns r --[edit3]--> q --[edit2]--> p into r --[edit3]--> p.

(I am not certain I've understood the official docs for "jj abandon" correctly, so it's very possible that I'm wrong about what it does, in which case obviously the above is wrong. But whatever it does, if you're distinguishing "files" from "edits", surely the "edits" go on the edges rather than the nodes of the revision-graph.)

steveklabnik

It's trying to point out the common argument that's passed to each command: `jj restore` with no arguments is virtually the same as `jj abandon`, but in practice that means that `jj restore` tends to be called with a file as an argument, and `jj abandon` gets called with a revision (which he's calling edit here). It is in fact a node. The arrows are still just relationships between nodes.

> if I'm understanding it right, turns r --[edit3]--> q --[edit2]--> p into r --[edit3]--> p.

You are right with the outcome but wrong about why. `jj abandon -r q` would turn `r --> q --> p` into `r --> p`, but you're passing the node as the argument (r is for revision) not the edge.

Hilariously, I am literally working on writing version 2 of my tutorial right now, and I'm literally talking about `jj abandon`. What do you think about this? It cuts off where I literally am right now: https://gist.github.com/steveklabnik/71165f9ff5e13b1e95902c4...

notmywalrus

> > if I'm understanding it right, turns r --[edit3]--> q --[edit2]--> p into r --[edit3]--> p.

> You are right with the outcome but wrong about why. `jj abandon -r q` would turn `r --> q --> p` into `r --> p`

Well, it can do two things. Given: `r(f3) --[e3]--> q(f2) --[e2]--> p(f1)`

`jj abandon -r q` makes `r(f1+e3) --[e3]--> p(f1)`, as if you had rebased `r` onto `p`.

`jj abandon -r q --restore-descendants` makes `r(f3) --[e2+e3]--> p(f1)`, as if you had squashed `r` into `q`.

justinpombrio

Yes, I think you understand perfectly. `abandon` was the diagram I struggled the most to draw.

I like the idea of putting the edits on the arrows, but there are a couple senses in which the edit is associated with the change itself rather than an edge between two changes:

1. A change with two parents starts out by merging them, and then it can make edits on top of that merge. If the edits go on an edge instead of on a node, which of the two edges do those changes belong on?

2. If you move a change (e.g. by rebasing it), its diff comes with it. I guess you could say that when you rebase, you're not moving just the node but also the edge from it to its parent?

Even so, diffs on edges feels very right. I may update that diagram.

EDIT: Updated!

wsycharles0o

I’m confused. Isn’t this exactly the same as git commands?

steveklabnik

Many jj commands share names with git commands, yes. That doesn't always mean they do the same exact thing. There's lots that are different too.

lgas

> That doesn't always mean they do the same exact thing. There's lots that are different too.

That's good. Gotta keep people on their toes.

steveklabnik

With `jj undo`, it's not a huge deal. I'm (mostly) kidding.

jj has to weigh trying to be the best VCS it can be alongside factors like "how much will this confuse someone who's used to git." Both things are important, and balancing them is a tricky process.

Take `jj commit` for example. This "does what git commit does," sorta. But also, not really? But if you're five minutes into `jj`, coming from `git`, then yeah, `jj commit` is exactly what you think it is. And that lets you be comfortable. Even though, from the `jj` perspective, it's a very bad name.

ashu1461

What is that one popular use case which git does really bad and jj does good ?

steveklabnik

Virtually everything is easier. How much easier is up for debate.

Part of the issue is that everything is interlocking. Small gains in multiple places end up feeling so, so much nicer, even if they may not seem like it. So for example, the index in git is a workflow pattern in jj, not a built in feature. This means that you don't have `git reset` with `--soft` vs `--hard` vs `--mixed`: You just have `jj edit`. This decision also means `jj rebase` can be entirely in memory, which means it's fast. But that wouldn't matter if conflicts weren't a first-class concept in jj, so things like rebase always succeed and immediately. Which doesn't sound like a big deal but you find yourself being able to rebase way more often and way more easily...

It's not that git is bad. And it's not that it's bad at a specific thing. It's just that taking some of the other sides of some of the tradeoffs means that you get something that's smaller but also more powerful. And that's cool.

I think the "mega merge" plus `jj absorb` might be one of the more flashy things: https://steveklabnik.github.io/jujutsu-tutorial/advanced/sim...

but I don't even do that. The basics are still just nicer.

3eb7988a1663

I am laughing. Fearless merging/rebasing/context switching? Having just one way of doing things?

Best to ask the other way around at the current JJ flaws. Which right now would be tooling support and you actually have to be on top of your .gitignore.

ipaddr

Sounds like something ready for the mass population.

steveklabnik

It is true that if you're looking for a finished, polished product, jj is very much not that. It's pre-1.0. Lots of things will change.

I still refuse to use anything else these days, but I can understand why someone else might not want to.

aidenn0

This was true of svn and git; both had command named the same as VCSs that were popular when they were introduced, but had semantics that were subtly (or not so subtly) different.