JJ Cheat Sheet
186 points
28 days ago
| 16 comments
| justinpombrio.net
| HN
sundarurfriend
28 days ago
[-]
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.

reply
steveklabnik
28 days ago
[-]
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".

reply
sundarurfriend
27 days ago
[-]
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/

reply
stouset
28 days ago
[-]
> 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?

reply
steveklabnik
28 days ago
[-]
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.
reply
stouset
27 days ago
[-]
Yeah, completely understood. I've suffered from related SSH issues, so I get the motivation. I suppose it's just unfortunate that the supporting libraries aren't quite there yet.
reply
steveklabnik
27 days ago
[-]
This kind of thing is why jj started with a strong library vs binary split; folks will be able to use the library and have full compatibility with other tools.
reply
progmetaldev
27 days ago
[-]
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.

reply
bulatb
27 days ago
[-]
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.
reply
progmetaldev
26 days ago
[-]
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.
reply
bulatb
24 days ago
[-]
Great! Happy to help.

Git is like that Brooks quote became software. "Show me Git commands and I'll continue to be mystified; show me history and I won't need your commands."

The official book on their website goes through most of Git like this, if you want to know more. It's really night and day once you know what it's actually doing.

reply
zipy124
27 days ago
[-]
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

reply
wodenokoto
27 days ago
[-]
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
reply
mkl
27 days ago
[-]
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).
reply
myst
27 days ago
[-]
You problem is Windows, not jj.
reply
rtpg
28 days ago
[-]
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.

reply
notmywalrus
28 days ago
[-]
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

reply
abound
27 days ago
[-]
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.
reply
setheron
27 days ago
[-]
Amazing alias I've adopted. Should be standard.
reply
oniony
28 days ago
[-]
This is great, thanks.
reply
steveklabnik
28 days ago
[-]
If you truly miss bookmarks moving like branches, you can give this configuration option a try: https://github.com/jj-vcs/jj/discussions/3549
reply
rtpg
28 days ago
[-]
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!

reply
steveklabnik
28 days ago
[-]
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!
reply
christophilus
28 days ago
[-]
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.

reply
rtpg
28 days ago
[-]
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.

reply
martinvonz
27 days ago
[-]
You can try a different conflict marker style: https://jj-vcs.github.io/jj/latest/config/#conflict-marker-s...
reply
nchmy
28 days ago
[-]
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

reply
justinpombrio
28 days ago
[-]
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.

reply
vermilingua
27 days ago
[-]
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.

reply
notmywalrus
27 days ago
[-]
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.

reply
vermilingua
27 days ago
[-]
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?
reply
ljm
27 days ago
[-]
I think it's exactly the opposite and the mental model of branching is counter-productive with jj. It's much more in line with trunk based development and the idea of managing 'stacks' of changes (kinda like merge trains where multiple smaller PRs in turn depend on each other).

If you had multiple people working on a branch and only some were using jj, then those not using jj would likely have issues with all of the rebasing and force pushes that happen behind the scenes to keep the history clean.

If you were to enforce linear history (like in GitHub) and used rebase merges exclusively then I think it's ideal, especially in terms of reviewing smaller, incremental changes rather than huge pull request diffs.

reply
notmywalrus
27 days ago
[-]
I find it encourages linear history more by making rebasing easier. Definitely possible, the jj project itself uses a linear history.
reply
steveklabnik
27 days ago
[-]
I use jj and enforce a linear history on my upstream github.
reply
steveklabnik
27 days ago
[-]
> 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.

reply
infogulch
28 days ago
[-]
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.
reply
steveklabnik
28 days ago
[-]
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)

reply
danpalmer
28 days ago
[-]
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.

reply
ljm
27 days ago
[-]
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.

reply
steveklabnik
27 days ago
[-]
jj also has workspaces, which are the same as a git worktree, if you want that workflow.
reply
jmholla
27 days ago
[-]
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.
reply
necauqua
27 days ago
[-]
You can set `snapshot.auto-track` config to "none()" (which is a fileset, so you could actually have something like src/* there).

That way, only files that were already present in the wc commit are amended, and new files are kept untracked until you explicitly `jj file track` them. And jj status finally fully shows them in the main branch too (it's broken in the v0.26 release sadly)

reply
danpalmer
27 days ago
[-]
I guess it depends on your workflow. If you stage each file carefully in git, it's effectively opt-in as you suggest. If you `git add .` though, as many people do out of habit, then it's not going to help much. The same is true really for jj, if you don't edit your commits then yeah, it'll be included by default, but if you craft your commits then you'll spot it.

Also jj doesn't push until you push your changes when backing on to git. This is pluggable though, the backend I work with is not git, and does happen to upload all commits off my machine immediately (although still effectively in a private fork).

reply
justinpombrio
28 days ago
[-]
This might be what you're looking for: https://jj-vcs.github.io/jj/latest/git-comparison/
reply
gjm11
28 days ago
[-]
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.)

reply
steveklabnik
28 days ago
[-]
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...

reply
notmywalrus
28 days ago
[-]
> > 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`.

reply
steveklabnik
27 days ago
[-]
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!
reply
justinpombrio
28 days ago
[-]
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!

reply
arxanas
27 days ago
[-]
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).

reply
justinpombrio
27 days ago
[-]
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.
reply
weinzierl
27 days ago
[-]
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.

reply
bjackman
27 days ago
[-]
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.

reply
oniony
27 days ago
[-]
I thought Google moved off of their struggling Perforce server onto their homegrown Piper VCS.
reply
bjackman
27 days ago
[-]
Piper is an extended reimplementation of Perforce

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

reply
steveklabnik
27 days ago
[-]
And while it's not public, jj has a piper backend, so that folks at Google can use jj

(This is also why I have some confidence that jj truly is backend agnostic, it has two real backends already.)

reply
arxanas
27 days ago
[-]
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.

reply
aseipp
27 days ago
[-]
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.
reply
jjfanboy
27 days ago
[-]
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. :(
reply
bedros
27 days ago
[-]
Anyone knows of vs code editor extension that works with .jj dir
reply
ilyagr
27 days ago
[-]
reply
conradludgate
27 days ago
[-]
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?

reply
martinvonz
27 days ago
[-]
> 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.

reply
glandium
27 days ago
[-]
> 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.

reply
necauqua
27 days ago
[-]
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.

reply
conradludgate
27 days ago
[-]
To me it still all seems confusing. I don't have a good mental model for it.

How does it handle things like `git rebase -x 'make fmt'` which might edit each rebased commit automatically, or `git rebase -x 'make lint'` which might fail but not leave a conflict marker. I didn't see any docs for exec in jj rebase so I guess this feature doesn't work

There's also no examples for how you work out which commit has a conflict marker. It's probably obvious in jj status but it would be nice to document, as I'm not yet interested in spending the time to test it.

And I imagine it all breaks in colocated repos too (I need submodule support, and I want to use gh cli etc)

reply
steveklabnik
27 days ago
[-]
> How does it handle things like `git rebase -x

It currently doesn't; `jj run` is a command to do this, separate from rebase, which has a PR open but hasn't been merged yet.

> There's also no examples for how you work out which commit has a conflict marker

Every bit of UI shows a bright red (conflict) on conflicted changes, it's hard to miss.

> And I imagine it all breaks in colocated repos too

It doesn't break in colocated repos in the sense that the feature works when you're working with jj, but you'd want to update your submodules and use gh cli on non-conflicted things.

reply
arxanas
27 days ago
[-]
> To me it still all seems confusing. I don't have a good mental model for it. > > How does it handle things like `git rebase -x 'make fmt'` which might edit each rebased commit automatically, or `git rebase -x 'make lint'` which might fail but not leave a conflict marker. I didn't see any docs for exec in jj rebase so I guess this feature doesn't work

As a mental model, you can pretend that a commit with a conflict in it has suspended the subsequent rebase operation for descendant commits. When you resolve the conflicts in that commit, it's as if jj automatically resumed the rebase operation for the descendant commits. This process repeats until you've applied all of the rebased commits and resolved all of the conflicts (or not, if you decide to not resolve all of the conflicts).

Today, you can write something like

    for commit in "$(jj log -T ...)"; do jj edit "$commit" && make fmt; done
which would also edit the commits automatically. It's probably not too hard to break out of the loop when the first conflict is detected to match the Git workflow more directly. (I can figure it out if you end up being interested.)

> There's also no examples for how you work out which commit has a conflict marker. It's probably obvious in jj status but it would be nice to document, as I'm not yet interested in spending the time to test it.

Does this overview help? https://jj-vcs.github.io/jj/v0.26.0/tutorial/#conflicts

> And I imagine it all breaks in colocated repos too (I need submodule support, and I want to use gh cli etc)

It depends on the exact subset of features you need.

- jj can't update submodules, which works for some workflows but not others.

- Generally speaking, you can use the `gh` CLI reasonably well alongside jj. In jj, it's common to create commits not addressed by any branch, but `gh` doesn't support those well. You can either try to adopt your Git workflows directly and always use branches, or else you'll need to make sure to create the branch just-in-time before performing a `gh` invocation that would need a branch.

reply
tripple6
27 days ago
[-]
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?
reply
bjackman
27 days ago
[-]
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.

reply
steveklabnik
27 days ago
[-]
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?

reply
martinvonz
27 days ago
[-]
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`

reply
Aissen
27 days ago
[-]
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.

reply
steveklabnik
27 days ago
[-]
jj has jj absorb already.
reply
Aissen
27 days ago
[-]
I heard it worked even better than git-absorb, is that true?
reply
steveklabnik
27 days ago
[-]
I have not used either, so I cannot answer that.
reply
Martinussen
27 days ago
[-]
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?
reply
martinvonz
27 days ago
[-]
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.
reply
martinvonz
27 days ago
[-]
I think you misunderstood. Did you see the list of features? My example is not the only thing jj does.
reply
globular-toast
27 days ago
[-]
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?
reply
steveklabnik
27 days ago
[-]
> 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.

reply
globular-toast
26 days ago
[-]
> 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!

reply
martinvonz
26 days ago
[-]
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).

reply
geenat
27 days ago
[-]
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

reply
aseipp
27 days ago
[-]
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.

reply
wyldfire
27 days ago
[-]
Does jujutsu have anything like "git am"? I'd like to take a series of patches and jj-ify them so I can play with it. I get that maybe I can't expect to cherry pick a commit because it's naturally different from git. but if I have a patch it seems like I should be able to apply those as jj changes?
reply
KingMob
27 days ago
[-]
There's no succinct command designed to work with mailed patches yet, but you could mimic it with a little scripting.

Maybe something like this?

``` for patch_file in "$@"; do jj new

    patch -p1 < "$patch_file"

    author=$(extract_author "$patch_file")
    commit_message=$(extract_commit_message "$patch_file")

    jj describe -m "$commit_message" --author "$author"
done ```
reply
wsycharles0o
28 days ago
[-]
I’m confused. Isn’t this exactly the same as git commands?
reply
steveklabnik
28 days ago
[-]
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.
reply
lgas
28 days ago
[-]
> 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.

reply
steveklabnik
28 days ago
[-]
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.

reply
ashu1461
28 days ago
[-]
What is that one popular use case which git does really bad and jj does good ?
reply
steveklabnik
28 days ago
[-]
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.

reply
ashenke
27 days ago
[-]
So I've been hovering around jj for a while, trying to understand how it works, without suceeding in having a good mental model around it. I looked at this megamerge workflow and it's the first one that "clicked" for me (even though I'm not sure I understand the magic behind jj absorb, but I'll look into it). One thing though is that I don't quite understand how it would work in the context of a real git repo. Let's say I work on two branches, main and feature1, I'd create a [merge] commit that is has the two bookmarks as parents. But if after a fetch main has new changes, do I need to discard the current [merge] and recreate it again ? I can't find a way to just say "Update the parents so they track these bookmarks". I just tried it on a real repo, working on a change in a branch, then on a change on another branch, then adding a third branch I needed to work on that was not a parent yet, but this seems like a lot of boilerplate to always recreate my megamerge setup so I think I'm missing something
reply
steveklabnik
27 days ago
[-]
> One thing though is that I don't quite understand how it would work in the context of a real git repo.

Just to be clear, `jj` stores all of its stuff in a real git repo. I know you probably meant "using git instead of jj" but I wanted to point that out because it means that you can just go look at what it does. I will say that sometimes that can be confusing, and I'd be very careful running git commands that modify history inside that directory.

So for example, here's a simple mega merge:

     jj log
    @        oyktpwwq steve@steveklabnik.com 2025-02-12 13:25:41 79d7cf16
    ├─┬─┬─╮  (empty) (no description set)
    │ │ │ ○  qvqxlymp steve@steveklabnik.com 2025-02-12 13:25:34 9403fc75
    │ │ ├─╯  (empty) C
    │ │ ○  wrqmrkot steve@steveklabnik.com 2025-02-12 13:25:32 2df38e4e
    │ ├─╯  (empty) B
    │ ○  oluunuku steve@steveklabnik.com 2025-02-12 13:25:29 19d713aa
    ├─╯  (empty) A
    ○  vukunsxq steve@steveklabnik.com 2025-02-12 13:25:25 main git_head() d29e7411
    │  (empty) (no description set)
    ◆  zzzzzzzz root() 00000000
the first commit after the root is bookmarked as `main`, our three changes are on top of it, with a mega merge.

I've used a colocated repo, so the `.git` dir is at the top level, so we can just run `git log`:

     git log --graph --all
    *---.   commit 79d7cf16869f0f293b18ff4fa339d2d053b537c0 (refs/jj/keep/79d7cf16869f0f293b18ff4fa339d2d053b537c0)
    |\ \ \  Merge: d29e741 19d713a 2df38e4 9403fc7
    | | | | Author: Steve Klabnik <steve@steveklabnik.com>
    | | | | Date:   Wed Feb 12 13:25:41 2025 -0600
    | | | |
    | | | * commit 9403fc75d2ec85491d990c7322b0f5f8f500fa78 (refs/jj/keep/9403fc75d2ec85491d990c7322b0f5f8f500fa78)
    | | |/  Author: Steve Klabnik <steve@steveklabnik.com>
    | | |   Date:   Wed Feb 12 13:25:34 2025 -0600
    | | |
    | | |       C
    | | |
    | | * commit 2df38e4eccbd31ef70a810dd86c637cc2110912c (refs/jj/keep/2df38e4eccbd31ef70a810dd86c637cc2110912c)
    | |/  Author: Steve Klabnik <steve@steveklabnik.com>
    | |   Date:   Wed Feb 12 13:25:32 2025 -0600
    | |
    | |       B
    | |
    | * commit 19d713aab65d0e572994634b2487ce7637d6752a (refs/jj/keep/19d713aab65d0e572994634b2487ce7637d6752a)
    |/  Author: Steve Klabnik <steve@steveklabnik.com>
    |   Date:   Wed Feb 12 13:25:29 2025 -0600
    |
    |       A
    |
    * commit d29e7411f098bba0120cfd5b819b732dabcb6f73 (HEAD, refs/jj/keep/d29e7411f098bba0120cfd5b819b732dabcb6f73)
      Author: Steve Klabnik <steve@steveklabnik.com>
      Date:   Wed Feb 12 13:25:25 2025 -0600
So this is the initial state.

Let's say that we pull down our remote, and main has changed, as you've said. There is a new commit on top of it.

We can simulate this in jj. (q is valid because we have so few changes there's no others that start with q yet)

     jj new v --no-edit -m "changes from upstream"
    Created new commit trzwxsrr c8318f5f (empty) changes from upstream
    
     jj bookmark create -r t main@origin
    Created 1 bookmarks pointing to trzwxsrr c8318f5f main@origin | (empty) changes from upstream
here's our log in `jj`:

     jj log
    @        oyktpwwq steve@steveklabnik.com 2025-02-12 13:25:41 79d7cf16
    ├─┬─┬─╮  (empty) (no description set)
    │ │ │ ○  qvqxlymp steve@steveklabnik.com 2025-02-12 13:25:34 9403fc75
    │ │ ├─╯  (empty) C
    │ │ ○  wrqmrkot steve@steveklabnik.com 2025-02-12 13:25:32 2df38e4e
    │ ├─╯  (empty) B
    │ ○  oluunuku steve@steveklabnik.com 2025-02-12 13:25:29 19d713aa
    ├─╯  (empty) A
    │ ○  trzwxsrr steve@steveklabnik.com 2025-02-12 13:27:16 main@origin c8318f5f
    ├─╯  (empty) changes from upstream
    ○  vukunsxq steve@steveklabnik.com 2025-02-12 13:25:25 main git_head() d29e7411
    │  (empty) (no description set)
    ◆  zzzzzzzz root() 00000000
and in `git`:

     git log --graph --all
    * commit c8318f5fd641a9b271b67e90e7ca1c465b1a92da (refs/jj/keep/c8318f5fd641a9b271b67e90e7ca1c465b1a92da, main@origin)
    | Author: Steve Klabnik <steve@steveklabnik.com>
    | Date:   Wed Feb 12 13:27:16 2025 -0600
    |
    |     changes from upstream
    |
    | *-.   commit 79d7cf16869f0f293b18ff4fa339d2d053b537c0 (refs/jj/keep/79d7cf16869f0f293b18ff4fa339d2d053b537c0)
    |/|\ \  Merge: d29e741 19d713a 2df38e4 9403fc7
    | | | | Author: Steve Klabnik <steve@steveklabnik.com>
    | | | | Date:   Wed Feb 12 13:25:41 2025 -0600
    | | | |
    | | | * commit 9403fc75d2ec85491d990c7322b0f5f8f500fa78 (refs/jj/keep/9403fc75d2ec85491d990c7322b0f5f8f500fa78)
    | | |/  Author: Steve Klabnik <steve@steveklabnik.com>
    | | |   Date:   Wed Feb 12 13:25:34 2025 -0600
    | | |
    | | |       C
    | | |
    | | * commit 2df38e4eccbd31ef70a810dd86c637cc2110912c (refs/jj/keep/2df38e4eccbd31ef70a810dd86c637cc2110912c)
    | |/  Author: Steve Klabnik <steve@steveklabnik.com>
    | |   Date:   Wed Feb 12 13:25:32 2025 -0600
    | |
    | |       B
    | |
    | * commit 19d713aab65d0e572994634b2487ce7637d6752a (refs/jj/keep/19d713aab65d0e572994634b2487ce7637d6752a)
    |/  Author: Steve Klabnik <steve@steveklabnik.com>
    |   Date:   Wed Feb 12 13:25:29 2025 -0600
    |
    |       A
    |
    * commit d29e7411f098bba0120cfd5b819b732dabcb6f73 (HEAD, refs/jj/keep/d29e7411f098bba0120cfd5b819b732dabcb6f73, main)
      Author: Steve Klabnik <steve@steveklabnik.com>
      Date:   Wed Feb 12 13:25:25 2025 -0600
So now we get to the issue: git would want us to rebase every one of these branches individually. When we rebase A (19d713aab65d0e572994634b2487ce7637d6752a) on top of main@origin (c8318f5fd641a9b271b67e90e7ca1c465b1a92da), well: (don't do this in a real repo, since this is an example I'm okay with trashing things, I even made a copy of the repo before doing this)

     git checkout 19d713aab65d0e572994634b2487ce7637d6752a
    Previous HEAD position was d29e741
    HEAD is now at 19d713a A

     git rebase main@origin

     git log --all --graph
    * commit f397936ac0d4510682e68ab470eb81863a5c2a73 (HEAD)
    | Author: Steve Klabnik <steve@steveklabnik.com>
    | Date:   Wed Feb 12 13:25:29 2025 -0600
    |
    |     A
    |
    * commit c8318f5fd641a9b271b67e90e7ca1c465b1a92da (refs/jj/keep/c8318f5fd641a9b271b67e90e7ca1c465b1a92da, main@origin)
    | Author: Steve Klabnik <steve@steveklabnik.com>
    | Date:   Wed Feb 12 13:27:16 2025 -0600
    |
    |     changes from upstream
    |
    | *-.   commit 79d7cf16869f0f293b18ff4fa339d2d053b537c0 (refs/jj/keep/79d7cf16869f0f293b18ff4fa339d2d053b537c0)
    |/|\ \  Merge: d29e741 19d713a 2df38e4 9403fc7
    | | | | Author: Steve Klabnik <steve@steveklabnik.com>
    | | | | Date:   Wed Feb 12 13:25:41 2025 -0600
    | | | |
    | | | * commit 9403fc75d2ec85491d990c7322b0f5f8f500fa78 (refs/jj/keep/9403fc75d2ec85491d990c7322b0f5f8f500fa78)
    | | |/  Author: Steve Klabnik <steve@steveklabnik.com>
    | | |   Date:   Wed Feb 12 13:25:34 2025 -0600
    | | |
    | | |       C
    | | |
    | | * commit 2df38e4eccbd31ef70a810dd86c637cc2110912c (refs/jj/keep/2df38e4eccbd31ef70a810dd86c637cc2110912c)
    | |/  Author: Steve Klabnik <steve@steveklabnik.com>
    | |   Date:   Wed Feb 12 13:25:32 2025 -0600
    | |
    | |       B
    | |
    | * commit 19d713aab65d0e572994634b2487ce7637d6752a (refs/jj/keep/19d713aab65d0e572994634b2487ce7637d6752a)
    |/  Author: Steve Klabnik <steve@steveklabnik.com>
    |   Date:   Wed Feb 12 13:25:29 2025 -0600
    |
    |       A
    |
    * commit d29e7411f098bba0120cfd5b819b732dabcb6f73 (refs/jj/keep/d29e7411f098bba0120cfd5b819b732dabcb6f73, main)
      Author: Steve Klabnik <steve@steveklabnik.com>
      Date:   Wed Feb 12 13:25:25 2025 -0600
    
We only get one branch. So what we'd have to do is go back and fix up every single branch here, including our merge. And so yeah, you can do it, but it's a giant pain in the butt. And for the children, you really need --onto, so you'd end up doing something like

    $ git checkout 2df38e4eccbd31ef70a810dd86c637cc2110912c
    $ git rebase HEAD --onto f397936ac
    $ git checkout 9403fc75d2ec85491d990c7322b0f5f8f500fa78
    $ git rebase HEAD --onto <new head of branch the first rebase committed>
for every branch. HOWEVER, git has introduced --update-refs, which can automate this. But it's from 2022, and my Ubuntu is a bit old, and so I cannot actually try it out.

That allll being said, I also don't know how well --update-refs does with the mega merge commit itself.

> this seems like a lot of boilerplate to always recreate my megamerge setup so I think I'm missing something

I mean, the entire thesis here is that stuff is easier in jj than it is in git: it doesn't mean you're missing something it means that git can be a bit clunky sometimes.

reply
Izkata
27 days ago
[-]
> So now we get to the issue: git would want us to rebase every one of these branches individually.

> [..]

> We only get one branch. So what we'd have to do is go back and fix up every single branch here, including our merge.

Not really, sounds close to git's --rebase-merges (formerly --preserve-merges):

  git rebase --onto main@origin main 79d7cf16869f0f293b18ff4fa339d2d053b537c0 --rebase-merges
I'm guessing putting "--update-refs" on top of it would give you exactly what you want. I also have a version without it though.
reply
steveklabnik
26 days ago
[-]
Ah nice, thank you!
reply
3eb7988a1663
28 days ago
[-]
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.

reply
simonmic
26 days ago
[-]
- Easy and robust undo.

- Intuitive, simpler (but still general and powerful) UI.

reply
ipaddr
28 days ago
[-]
Sounds like something ready for the mass population.
reply
steveklabnik
28 days ago
[-]
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.

reply
aidenn0
28 days ago
[-]
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.
reply
KingMob
27 days ago
[-]
Unironically, it is. The underlying storage engine is git, which is rock-solid.

But to put it this way, I've been using jj for almost a year, and even without a 1.0 release, editor support, or mature tooling, it's still such an improvement over git, I've stopped using git 98% of the time.

reply
arxanas
27 days ago
[-]
It's a bit unfortunate because most of the listed commands are indeed equivalent to Git commands. To give an example of new capabilities, in Git, you can't do the equivalent of

    jj rebase -r 'mine() & diff_contains("TODO")' -d 'heads(@::)'
in any reasonable number of commands, which will

1) find all of the commits I authored with the string `TODO` in the diff

2) extract them from their locations in the commit graph

3) rebase all non-matching descendant commits onto their nearest unaffected ancestor commits

4) find the most recent work I'm doing on the current branch (topologically, the descendant commit of the current commit which has no descendants of its own, called a "head")

5) rebase all extracted commits onto that commit, while preserving the relative topological order and moving any branches.

Furthermore, any merge conflicts produced during the process are stored for later resolution; I don't have to resolve them until I'm ready to do so. (That kind of VCS workflow is not useful for some people, but it's incredibly useful for me.)

reply
porridgeraisin
27 days ago
[-]
So you're taking every single TODO commit and rebasing it on top of current? Why would we do that?
reply
arxanas
27 days ago
[-]
I might do it if the commits in my stack are mostly independent and I want to commute the ones with `TODO` to be later. This might be so that I can

- run checks on the sequence of done commits, without getting wrong/unhelpful results because of the presence of an unfinished commit in the sequence

- put the done commits up for review now

(This is predicated on certain workflows being acceptable / useful for the user, such as modifying existing commits and reordering commits.)

It's also just an example. You can swap in your favorite query into `-r` if you don't have any need for moving `TODO` commits around. Regardless, Git is not easily able to replicate most such invocations.

I previously meant to link to the jj revset docs here: https://jj-vcs.github.io/jj/v0.26.0/revsets/

reply
necauqua
27 days ago
[-]
??.. wdym why, what?.

Also this is just an example, there are a lot of very complex (from the point of git) things that are trivial like that in jj.

Although if you're asking "why would we do that" then I don't even know what to tell you lol

reply
nchmy
27 days ago
[-]
I think it was a very valid question. I was, and still am, wondering the same thing
reply
necauqua
26 days ago
[-]
I guess you could question the specific example, but to me the question sounded like "why would you do rebases".

Like, for various reasons?.

The point was that it's way easier and way safer to do rebases in jj, and it straight up allows to do complex stuff like the mentioned example easily and inderstandably, idk

reply
mystickphoenix
27 days ago
[-]
Something that I've been struggling to wrap my brain around is:

1. Can I use jj inside a repo that was already initialized with git? I think the answer is yes, but I haven't found a tl;dr for it.

2. What does the workflow look like to use jj on an existing git repo that all of your coworkers use git for?

reply
steveklabnik
26 days ago
[-]
> Can I use jj inside a repo that was already initialized with git?

Yes.

> What does the workflow look like to use jj on an existing git repo that all of your coworkers use git for?

I struggle a little to answer this because on some level, the answer is "whatever you'd like it to be." That is, from your co-workers' perspective, nothing changes. You push some changes up to a git repo, they have no clue you used jj to do it. But I also feel like that maybe isn't answering your question.

reply
mystickphoenix
25 days ago
[-]
I suppose what I'm looking for is maybe a translation from git to jj from the perspective of working in a repo with other users that are using git. Something along the lines of:

1. init jj in an existing git repo

2. instead of branching, do x, y, z

3. instead of committing after changes are done, do x, y, z

4. when pushing, do x, y, z

5. if someone else pushes to the same branch, here's how to handle it

6. if someone rebases and force pushes the branch, here's how to handle it

7. if you have merge conflicts, here's how to handle that

I think I'm having a hard time trying to grok the jj "mental model" while simultaneously understanding how it translates to an existing git repo.

I suspect for jj to get traction outside of single devs or companies that use jj exclusively, some extra focus in the docs giving guidance in the liminal space between would be super helpful.

reply
steveklabnik
25 days ago
[-]
Ahh, I see, thanks.

> some extra focus in the docs giving guidance in the liminal space between would be super helpful.

I'm working on this! https://steveklabnik.github.io/jujutsu-tutorial/real-world-w... describes the most basic two workflows for 2-3. https://steveklabnik.github.io/jujutsu-tutorial/sharing-code... tries to explain 4-6 (but isn't as complete as it should be), and there's an unwritten place for 7 to exist in the future.

I don't think these things are perfect yet, and I'm actually re-writing the whole thing, and it's going to focus on exactly these things, and there's interest in upstreaming the whole thing. That is partially why I asked, because I'm trying to make it so that these things are more easy for you in the future :) So I do really appreciate the answer.

reply
mystickphoenix
21 days ago
[-]
Just wanted to say thank you for working on this, I'm going to take another read-through of those 2 links and see if it makes more sense given the extra context of this thread. Either way, I look forward to reading the updates and seeing if they click better in my brainpan ;)
reply
steveklabnik
20 days ago
[-]
You're welcome, happy to hear any and all feedback.
reply
159jonie158
26 days ago
[-]
ROBLOX BLOX FRUIT
reply