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.
> 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".
> 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!)
I understand the decision here from an SSH-support situation, but doesn't this feel like a bit of a step backwards?
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.
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.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.
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.
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!
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.
It's never been wrong, but I am slightly unconvinced at how well merge conflict resolution works relative to git + rerere.
One suggestion - reverse the direction of the arrows: q->r rather than q<-r
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.
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.
- 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.
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.
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.
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)
> 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.
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.
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)
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).
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.)
> 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...
> 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`.
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!
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).
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.
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.
(Piper is piper, expanded recursively. It's so meta, even this acronym)
(This is also why I have some confidence that jj truly is backend agnostic, it has two real backends already.)
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.
Better link: https://marketplace.visualstudio.com/items?itemName=visualjj...
Maybe I'm in the minority, but I like fixing conflicts as I go. What am I missing?
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.
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.
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.
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)
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.
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.
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.
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?
`git commit --fixup=X foo; git stash; git rebase -i X^; git stash pop`
`jj squash --into X foo`
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.
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.
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!
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).
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
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.
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
```That's good. Gotta keep people on their toes.
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.
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.
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.
> [..]
> 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.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.
- Intuitive, simpler (but still general and powerful) UI.
I still refuse to use anything else these days, but I can understand why someone else might not want to.
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.
jj rebase -r 'mine() & diff_contains("TODO")' -d 'heads(@::)'
in any reasonable number of commands, which will1) 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.)
- 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/
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
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
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?
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.
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.
> 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.