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

JJ Cheat Sheet

JJ Cheat Sheet

120 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".

sundarurfriend

I got introduced to jj via Chris Krycho's post on it, but working my way through your tutorial [1] is what made me feel like I understand it enough to actively use in real projects, and also convinced me that jj is the way forward. So, thank you for that!

> The autocrlf bit is a bit of a bummer on Windows, too.

autocrlf is always a bit of mess, to be fair, even when using git. There's never a single good setting because some projects do, for whatever reason, use CRLF for their line endings. (I recently spent 15+ minutes going through my git config and editor config and carefully making sense of things, trying to see why there were spurious line ending changes in my commit, before realizing this project I was contributing to used a mix of LF and CRLF endings in different files!)

[1] https://steveklabnik.github.io/jujutsu-tutorial/

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?

steveklabnik

It’s just a tough spot to be in: libgit2 has always worked a bit differently than the binaries, gix is incomplete. Ideally gix will be good enough that it could just be used, but things aren’t there yet.

progmetaldev

Does this mean that NTFS filesystems are affected, even if using from a PowerShell or cmd.exe window, if you are aware? I use git bash or PowerShell daily, but have only used WSL directly for certain processes I am more familiar with in bash than on Windows shells. I've done quite a bit of text processing in WSL, where I just have more experience in the tooling on Linux/bash shell than in Windows, even though I often write dotnet software that runs on Windows, yet uses the open source dotnet and it could run on Linux with a few changes (like making sure I can access MS SQL Server on Azure from Linux, or run MS SQL Server for Linux - I haven't done this, and the CMS I currently use only supports SQLite for development, but require MS SQL Server for production). I might be able to get away with using SQLite for production by only allowing a single user to make edits at a time, and using heavy caching.

I came from a world of Mercurial, and I would love to be able to commit very often, and then be able to squash all those commits into a single commit. I feel git rebase does that, but I haven't been able to truly grok how to do that without running the possibility of completely destroying all changes I've made. I can't lose a giant feature (which is what I generally build) that may take an entire week to build, because I used the wrong git rebase command. I would love to be able to change an individual file and commit it to compare against new changes, but then pull all of these temporary changes/commits and merge/squash them all into a single commit, in case I need to rollback everything due to some breaking update.

bulatb

Rewriting a commit in Git (with rebase, --amend, --squash, whatever) creates a new commit with your changes, keeping the original around but detached from the branch. For example, amending the tip of a feature branch (git checkout feature; git commit --amend) turns this:

  A --- B --- C <- [main]
         \
          X --- Y --- Z <- [feature]
into this:

  A --- B --- C <- [main]
         \
          X --- Y --- Z' <- [feature]
                 \
                  Z

The old commit is not destroyed, just taken off the path you walk from "feature" back in time. Even if you `rebase -i main` on "feature" and drop Y and Z, they'll still be around, just not in your branch:

  A --- B --- C <- [main]
         \     \
          \     X' <- [feature]
           \
            X --- Y --- Z
If you're worried about rebase going bad, before you start, create a temporary branch (git checkout -b before-risky-rebase; git checkout -) to mark that line of history where "feature" points at the good state.

  A --- B --- C <- [main]
         \
          X --- Y --- Z <- [feature,before]
If anything goes wrong that `rebase --abort` doesn't fix, get out of there somehow and `git checkout before-risky-rebase`, or, on "feature," `git reset --hard before-risky-rebase`. Here, the backup branch "before" is still what "feature" was before the rebase:

  A --- B --- C <- [main]
         \     \
          \     X' --- (bad) --- (oops) <- [feature]
           \
            X --- Y --- Z <- [before]
As long as you don't force-push anything, it doesn't really matter if you damage the now-broken branch even more while getting out. Reset "feature" to your backup and it never happened. You can even damage "main" and still `reset --hard` to origin/main (if you have one) or the tip "main" had before you broke it.

Even if you don't remember to create the backup branch, the hashes of your old commits now bypassed are still in the reflog. You can always find the hash where "feature" used to point and manually move the pointer back:

  git checkout feature
  git reflog
  (find the hash)
  git reset <hash> # --hard or --mixed if needed
Not that this is obvious or trivial or anything. It shouldn't be this hard. But your commits are safe from almost any way you might destroy them once they're somewhere in your history, at least until unreachable commits are eventually garbage-collected.

progmetaldev

I appreciate you taking the time to write all this out for me! This is really helpful in understanding rebasing, and generally how the commits work.

zipy124

you can instead use btrfs for your drive so you can access it on windows and linux, as the windows btrfs driver[1] is rather mature these days!

[1]: https://github.com/maharmstone/btrfs

wodenokoto

What’s the benefit of running gut from WSL and keeping the files on NTSF? I keep everything in wsl, but now I’m wondering if I should move my git repo to OneDrive

mkl

My reason is the combination of a good command line and also easy use of native GUI apps. The main problem is that operations involving lots of little files can be slow (WSL1 is better than WSL2 at this).

myst

You problem is Windows, not jj.

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.

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

abound

This is excellent, thank you! I've had my own `jj bm` (for "bookmark move") alias to do this, but that implementation is way better.

setheron

Amazing alias I've adopted. Should be standard.

oniony

This is great, thanks.

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!

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.

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.

vermilingua

Something I've worried about trying to switch to jj: is there a chance that using it will cause noticable artifacts or issues in the upstream git repo? We're quite strict on tooling in my team and I don't want to get to a state where I can't digest the jj changes back into normal git commits for pushing.

There's plenty about how you can use jj as a replacement but no clear guidance on what upstream commits will actually look like if you use it.

notmywalrus

The blockers for many are:

- No support for LFS

- No support for hooks (precommit, etc)

- No? Bad? support for submodules

- No? Bad? support for line ending styles

If you don't care about those, you _should_ be able to use jj completely "undetected". It does encourage rewriting history more than some git workflows like.

In terms of issues in the git repo -- there shouldn't be any. jj uses git as a backend, all commits are stored as git commits, etc. If you colocate the repo, you're able to use git commands directly.

vermilingua

Awesome, cheers. It also seems to encourage merges over linear history (I guess because the history of "bookmarks" is deemphasised), is a linear history still achievable?

steveklabnik

> No? Bad? support for submodules

No support in the sense that jj won't do anything with them, but you can collocate the repository and use git to deal with them and it's fine.

> - No? Bad? support for line ending styles

That's a No, currently, yeah.

null

[deleted]

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.

ljm

I remember at an old job where the feedback loop was quite slow, so you always ended up with multiple branches or PRs in play at a time. I ended up using git worktree to basically have each branch as its own separate workspace (rip my hard drive) because the process of stashing and switching, pulling, and untangling wip commits got old fast.

I still juggle a few plates now (for better or worse) and especially with VisualJJ that experience is much nicer. I can just switch between bookmarks and do what I need to, and `absorb` makes that super nice in terms of addressing PR feedback.

jmholla

My biggest concern with moving to `jj` is how it handles new files. I don't always want files I create in my repository to end up on a remote and I have been under the impression `jj` assumes everything it hasn't been told to ignore is part of the repository.

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`.

steveklabnik

Ah intriguing, I didn't know about `--restore-descendants`, and I can see how that makes it feel like you're operating on edges, even if you're passing a revision in. Thanks!

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!

arxanas

I believe `jj abandon` indeed operates on edges rather than nodes. It looks the diagram is updated now.

I believe that `jj squash` and `jj backout` also operate on edges rather than nodes, but the examples here don't make it clear. `jj squash` ought to combine the edge `r -> q` with the edge for `q -> parent(q)` (not depicted) and ultimately leave the `r -> q` edge as "empty", and `jj backout` ought to create an edge that has the inverse diff of another edge (which, in this case, is indistinguishable from `s`'s node changing to be equivalent to `q`'s node).

justinpombrio

Yup, that's all correct. I drew `squash` and `backout` in terms of files in order to avoid needing notation for the opposite of an edit and the composition of two edits.

weinzierl

I tried to get into Jujutsu several times and tried to love it, but somehow it seems there is little overlap between the issues I have with git (which there are plenty) and the problems jj tries to solve.

My impression is that the main motivation behind jj is that Google realized how difficult and costly it is to train all new hires in their internal tooling but did not want to open-source it completely. So they came up with a thin UI layer, made it workable with git as a backend and published it in the hope it will catch on.

bjackman

I have no opinion on whether JJ is good but I am pretty confident your impression about the reason for its shortcomings is wrong.

Google has a basically tolerable and pretty easy-to-learn Mercurial-based frontend for its bizarre legacy Perforce system.

Everything about JJ screams to me that it's been created as the passion project of someone who really wants to build a better VCS, making it compatible with Git was necessary to give it a chance of adoption, and making it compatible with Piper (Google's Perforce thing) was a way to get it funded as a potential benefit to Google.

Top-down Google engineering would never produce a project like this IMO.

oniony

I thought Google moved off of their struggling Perforce server onto their homegrown Piper VCS.

bjackman

Piper is an extended reimplementation of Perforce

(Piper is piper, expanded recursively. It's so meta, even this acronym)

arxanas

It's certainly true that jj's features won't appeal to everyone. I think a lot of its features are quality-of-life features (consistent commands and concepts, general undo), and a lot of its features don't help a certain class of users (flexible commit rewriting/rebasing), so it's not surprising that some seasoned Git users won't find it that helpful.

I think it's unfair to call it a "thin UI layer". My own project git-branchless https://github.com/arxanas/git-branchless might more legitimately be called a "thin UI layer", since it really is a wrapper around Git.

jj involves features like first-class conflicts, which are actually fairly hard to backport to Git in a useful way. But the presence of first-class conflicts also converts certain workflows from "untenable" to "usable".

Another comment also points out that it was originally a side-project, rather than a top-down Google mandate.

aseipp

Your impressions are not correct. Jujutsu's creation and its Git support both predate Google's direct involvement or its anointment as the eventual successor to Fig (Google's internal fork of Mercurial). Martin just happened to work there at the time he started jj, and only later did it become his full-time job to work on it. Though the occasional Googler who isn't Martin will contribute a patch or two when they want to fix something, Google otherwise isn't really involved.

jjfanboy

I love jj and you could pry it from my cold dead hands but I can't make sense of nearly any of these pictograms. :(

bedros

Anyone knows of vs code editor extension that works with .jj dir

conradludgate

I still don't understand the way jj handles conflicts. If I rebase and then fix a conflict later, are those conflict markers going to appear in some of the commits on github when I push?

Maybe I'm in the minority, but I like fixing conflicts as I go. What am I missing?

martinvonz

> I still don't understand the way jj handles conflicts.

See https://jj-vcs.github.io/jj/latest/conflicts/.

> If I rebase and then fix a conflict later, are those conflict markers going to appear in some of the commits on github when I push?

We error out if you try to push conflicts because the Git remote would probably not know how to interpret them. We will probably add an option to allow it later because it can be useful to be able to share conflicts with others if you know that they're also using jj.

> Maybe I'm in the minority, but I like fixing conflicts as I go. What am I missing?

You can still do that. Hopefully the above answers your question.

glandium

> See https://jj-vcs.github.io/jj/latest/conflicts/.

Honestly, this page doesn't really make a compelling case as to how checking out the commit with a conflict and amending is better than git rebase/whatever --continue. Overall, it's also quite abstract. Concrete examples would be deeply appreciated.

necauqua

It's very obviously miles better because there's no global rebase state?.

So you can just leave the conflict there an go work on something else then come back.

Also it's not sequential like --continue you've mentioned.

Also you can rebase the conflicting commits themselves, and by doing so potentially resolve the conflict and the resolution will propagate.

For example manually undoing the rebase while useless (there's jj undo after all) shows that.

tripple6

I've seen several posts on jj, but could anyone please tell what can't be done by git, or what is harder in git but super-easy in jj by providing the sequence of git commands and jj commands for comparison?

bjackman

You can do everything in Git really. I don't see anything in jj that Git literally can't do, but as someone who spends a lot of time faffing around rebasing huge branches I can really see the appeal of something that does all the same stuff better.

If I mostly worked on a small-to-medium size project with close connections between developers, where mostly you just get your code merged pretty quickly or drop it, then I wouldn't see any value in it. But for Linux kernel work git can often be pretty tiresome, even if it never actually gets in the way.

I thought that nothing could beat Git until I tried Fig (Google's Mercurial thing). It ends up being awful coz it's so bloody slow, but it convinced me that a more advanced model of the history can potentially life easier more often than it makes it harder.

Fig's model differs from Git in totally different ways than jj's does but just abstractly it showed me that VCS could be meaningfully improved.

steveklabnik

Yes, at the end of the day, there's nothing you can't do in git that you can do in jj. This is easy to demonstrate, since jj has a git backend. There are minor things about that that do show some things, like for example, change IDs are totally local, and not shared, since git doesn't have the notion of a change ID, but that's not what we're talking about really.

At the end of the day, every DVCS is ultimately "here is a repository that is a bunch of snapshots of your working directory and a graph of those snapshots" plus tools to work with the graph, including tools to speak to other repositories.

From any given snapshot A -> B, both git and jj can get you there. The question is, which tools are more effective for getting the work done that you want to do?

martinvonz

To move the changes in file `foo` in the working copy into a past commit `X`:

`git commit --fixup=X foo; git stash; git rebase -i X^; git stash pop`

`jj squash --into X foo`

Aissen

You can simplify this:

git commit --fixup X ; git rebase --interactive --autostash --autosquash X^

If you do that often, an alias might help; I have one for the second command above. You might want to look at git-fixup or git-absorb for automatically finding the "X" commit.

Aside: I really ought to try jj, it looks very promising.

steveklabnik

jj has jj absorb already.

Martinussen

That looks more like a git alias than a job for an entirely new tool, to me. How many of the core functions do you really need to cover before `jj` itself becomes redundant?

martinvonz

I apologize if my sibling comment sounded harsh. I think you were saying that jj could be implemented as some Git aliases. Given the information available in this thread, that might seem reasonable. I didn't realize that this thread did not include a link to the project's docs. Sorry about that.

martinvonz

I think you misunderstood. Did you see the list of features? My example is not the only thing jj does.

globular-toast

So jj calls commits "changes", and this is less confusing? Interesting. I find when you dig into people's understanding of git (or version control in general), a lot of them understand it as storing a sequence of diffs. This small thing breaks their understanding of the whole system. Calling them "changes" seems like it would reinforce this belief. Or is that the idea? Does jj embrace this perhaps more intuitive "sequence of diffs" view, but more successfully hide the "sequence of commits" reality?

steveklabnik

> So jj calls commits "changes", and this is less confusing?

Sort of. It has both changes and commits, actually. (and sometimes commits are called revisions.)

     jj log
    @  muzrswxs steve@steveklabnik.com 2025-02-12 10:23:11 85b41b31
    │  (empty) (no description set)
    ○  wotxrwpp steve@steveklabnik.com 2025-02-12 10:23:09 24ce0a16
    │  (empty) (no description set)
    ○  ztxxskuu steve@steveklabnik.com 2025-02-11 18:20:56 1b3e12ac
    │  run sqlx migrate as part of deploy process
    ◆  qwovsnvt steve@steveklabnik.com 2025-02-11 17:45:24 trunk b224ca8b
    │  <redacted>
Okay, so muzrswxs is a change ID. It's true that we're connecting changes in a graph, and that that forms history. So in that sense, it's like a git commit. But because changes are mutable (well, the ○ ones and @ are, the ◆ there is not), they are implemented as a sequence of commits. So if you look on the far right there, you'll see 85b41b31 and then below it, 24ce0a16. Below that, 1b3e12ac. These are commit IDs.

The first two changes are empty, so what happens if we modify a file?

     jj log
    @  muzrswxs steve@steveklabnik.com 2025-02-12 10:28:18 404a73b1
    │  (no description set)
    ○  wotxrwpp steve@steveklabnik.com 2025-02-12 10:23:09 24ce0a16
    │  (empty) (no description set)
    ○  ztxxskuu steve@steveklabnik.com 2025-02-11 18:20:56 1b3e12ac
    │  run sqlx migrate as part of deploy process
    ◆  qwovsnvt steve@steveklabnik.com 2025-02-11 17:45:24 trunk b224ca8b
    │  <redacted>
Note that (empty) went away on that head change there, and its change ID is still muzrswxs. But the commit ID has changed from 85b41b31 to 404a73b1. None of the parents changed, of course.

We can even take a look at this history:

     jj evolog --summary
    @  muzrswxs steve@steveklabnik.com 2025-02-12 10:28:18 404a73b1
    │  (no description set)
    │  M README.md
    ○  muzrswxs hidden steve@steveklabnik.com 2025-02-12 10:23:11 85b41b31
       (empty) (no description set)
The evolution log will show us how our change has evolved over time: first we had 85b41b31, then we modified README.d and now we're at 404a73b1.

> I find when you dig into people's understanding of git (or version control in general), a lot of them understand it as storing a sequence of diffs. This small thing breaks their understanding of the whole system.

I agree with you in some sense, but also, kinda don't. That is, I agree that thinking git stores diffs is not correct, but I'm not fully sold on how big of a deal it is to be incorrect here. And once you really get into things, like, how packfiles are implemented, diffs are present.

> Calling them "changes" seems like it would reinforce this belief. Or is that the idea? Does jj embrace this perhaps more intuitive "sequence of diffs" view, but more successfully hide the "sequence of commits" reality?

I can assure you that these names are a heated kind of debate internally. I actually said two days ago "hey, so we have changes, commits, and then revision as a synonym for commit. shouldn't commit be a synonym for revision? because 'revision' is kind of an abstract idea, but 'commit' is git-specific, so like, I think it should be "we have changes, and changes have revisions, but the git backend implements revisions as commits" and that thread is still going this morning, with links to many previous discussions. Someone even wrote a blog post a year ago https://blog.waleedkhan.name/patch-terminology/

jj is still figuring out how best to present its ideas. I really like "change and revision" to describe these two things, but a lot of folks are concerned that "change" is too generic and is hard to figure out, that is, when I said this above

> its change ID is still muzrswxs. But the commit ID has changed

This is two different uses of the word "change". Is that confusing? Maybe. Is it confusing enough to find another word? Not sure.

globular-toast

> I agree with you in some sense, but also, kinda don't. That is, I agree that thinking git stores diffs is not correct, but I'm not fully sold on how big of a deal it is to be incorrect here.

It's not so much about what actually happens underneath, that should be irrelevant and git just does what it does for practical reasons ultimately (as you point out, with packfiles, but this is definitely not a detail any git user needs to be aware of).

The problem I see is that git actually exposes both views of things. A seasoned git user will be used to the "duality" of commits vs diffs (ie. they are two different views of the same thing). Git exposes diffs directly when cherry-picking or rebasing, but at most other times you are working with commits. You don't push/pull diffs, you push/pull commits. It seems like a small thing, but every time I've dug into why somebody is having trouble with git it seems to be they view the world only as diffs.

So my question really was whether jj attempts to expose only one or the other. Looking at your explanation I would say it doesn't. It seems to me like changes are very similar to branches in git. At least this is how I think of branches in git, but I tend to be the "git guy" in every place I've worked. I mutate branches all day long by doing git commit amend etc.

It seems like the real point here is to get rid of "branch" as that is an overloaded concept and split it into two things: change and bookmark. In many ways it just seems like a reinforcement of the way I (and I guess other "git guys") use git anyway. Interesting!

martinvonz

We were actually talking about this for quite a while on Discord yesterday. The duality is visible to the user in jj as least as much as it is in git.

Some command arguments treat commits as snapshots (e.g. `jj restore --from/--into`, `jj diff --from/--to`, `jj file list -r`) and some commands arguments instead inspect the diffs (e.g. `jj rebase -r/-b/-s`, `jj diff -r`, `jj squash --from`, `jj log <path>`).

The first-class conflicts (https://jj-vcs.github.io/jj/latest/conflicts/) allow jj to be much better at treating commits as diffs than git is. In particular, there's no real difference between merge commits and other commits; any commit can introduce conflicts and any commit can resolve conflicts. We define the changes in a commit as relative to the auto-merged parents. That means that the diff-centric command arguments work in a consistent way for merges too. For example, if you create a new merge commit (`jj new A B ...`), it might have conflicts, but we still consider it empty/unchanged. If you resolve the conflicts, then `jj diff` will show you the conflict resolutions, and `jj rebase` will rebase the conflict resolutions (a bit like git rerere, but it also works on hunks outside of the conflict areas).

geenat

Auto-commit of large files and no way to un-bloat your repo is a showstopper for me no matter how good the DX might be.

Large file handling needs to be sane in any new VCS, IMHO, as this is a main failing of git (..without the extra legwork of git-lfs).

Edit: https://github.com/jj-vcs/jj/issues/80 Could maybe bring jj up to parity with git here

aseipp

Large file handling has improved in recent versions, FWIW; large files are left untracked if they violate the size limit (no auto track), you have to selectively add them at that point. Note that you can unbloat your local copy by pruning the operation log and then running jj gc if you accidentally add blobs and stuff; though if you push the blobs somewhere you obviously can't undo that so easily, that's no different than Git.

Git's underlying storage format just isn't a very good fit for any kind of "large-ish file" storage; Git LFS is mostly just hack and it is unlikely to be supported anytime soon. Our hands are a bit tied on that front.

My impression is that most of the interest and momentum for solving the "large files problem" would preferably be invested in a native non-Git backend for Jujutsu.