Remix.run Logo
sfink 14 hours ago

Heh, sorry, I guess you triggered my defensive "book-length response" reaction. Here's the next book:

As you say, jj uses git primives so the core data model is the same. I say "core" because jj subtracts out the staging area and stash and adds in "changes" and the operations log. But otherwise, your understanding of the git data model translates seamlessly, with one exception: git-style branches aren't really a thing, they can only be emulated. What I mean is that in git, the topological head that is associated with a branch ref (apologies if I'm getting terminology wrong), when accessed through a branch name, is taken to represent not just that commit but all ancestors of it as well. So "merging a branch" or "checking out a branch" are natural things to do.

jj prefers to be grounded in the commit DAG. Since you're always "updating" a commit (really replacing it with new ones), the only other state is the location of that commit in the DAG. You specify that before that change exists, with the default being the same as in git: the single descendant of the last thing you were working on (the branch tip in git, the @ change in jj). `jj new` (or `jj commit`) creates a single child change of your current change, unless you pass flags putting it elsewhere in the graph -- and it is very common to put it elsewhere if that's the thing you want to do.

An example: say you have a sequence of changes (and thus commits) where you're working on a feature: A->B->C, with C being what you're editing now (the @ change). You realize that your latest change, C, might not be the best approach and decide to try another. You do `jj new B` to create an alternate descendant and implement a C2 approach. Now you have A->B->{C,C2}. Which of C or C2 is the tip of your "feature branch"? Answer: who cares? At least until you want to push it somewhere. My default log command will show the whole DAG rooted at A, the first change off of "trunk" commits. I might do further work by creating descendants of C2 and thus have A->B->C and B->C2->D->E, and I might rebase or duplicate ->D->E on top of C if I want to try it out with that approach. Or I might inject or modify things earlier in the graph like A->X->B'->Y->{C,C2} (where B' is an updated B). If I push part of that DAG up to a server that I have declared to be shared with other people, then jj will know what parts of the graph should no longer be mutated and prevent me from doing that (by default).

All of this follows the core git model, it's just that mutating the graph structure is expected and common with jj and only restricted modifications seem to be common with git. You can do any and all of it with git, but rebase, undo, and conflicts are better-supported and feel less exceptional with jj. And you can jump to anywhere in the graph, or move around pieces of the graph, anytime and without necessarily changing the @ (working change) you're looking at.

This also means that jj doesn't need a `git switch` or `git checkout` command. That notion is replaced by making your current change (there's always a current change, it's the one that will be associated with any edits you make) be the child of wherever in the DAG you want, probably with `jj new <parent>`. As with git, there's a snapshot associated with it, so the other purpose of `git checkout` (to make your on-disk copy of a file match the contents of a given commit) is `jj restore filename`. In `git help checkout`, it says "Switch branches or restore working tree files". The "switch branches" part is `jj new`, the "restore working tree files" part is `jj restore`. `jj new` is equivalent to `git checkout --detach` or `git checkout <branch> <start-commit>` (or plain `git checkout <treeish>`). If you only consider the "make (part or all of) the working tree match that of this commit", then all of the different variants `git checkout` being in one command makes sense. But jj distinguishes things that operate on the DAG vs trees (file contents). And it doesn't need different flags or modes for the index/stage vs a commit; everything is a commit. `git stash` is unnecessary, the equivalent is `jj new <ancestor>` (you're just moving your working directory to be based on a different commit; the previous patches aren't lost or anything, they stay where they were in the DAG. Grab them from there if you want them and put them anywhere else in the graph, with or without changing your working directory as you wish.) `git reset` is redundant with `jj restore` -- well, mostly; the other part of what it does is handled by `jj abandon`. (Again, `restore` for tree contents, `abandon` for the graph node.) `git merge` is `jj new` with multiple parents instead of one so a command isn't needed.

One way to say it: with jj, `new` + `describe` + `rebase` + `abandon`, you can do all the graph manipulation. Add in `squash` + `restore` and you can do all content manipulation too. That's it for local operation. Well, maybe `jj file track/untrack`. The flags are pretty consistent across commands and don't fundamentally change what they do. Everything else is either for working with servers and other repositories, convenience utilities for specific situations, details like configuration, or jj-specific functionality like the operation log.

The other major difference is that jj adds in the notion of persistent "changes" (I wish we'd come up with a better name) that in normal operation correspond to a single "visible" commit, but which commit that is changes over time. A change has a single semantic purpose and doesn't lose its identity when rebased or merged or the contents of its files are changed. This difference is actually starting to diminish, because git (or rather, the git forges and tooling) has started preserving the footers that identify these change IDs over various operations. So if you're using git in connection with gitbutler or gerrit or whatever, you'll be getting the benefit of these persistent identifiers even if you continue using the git cli.

Sorry, that's a lot, but it'd be my attempt at "jj fundamentals for someone who understands git primitives well".