Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.
I also often end up with in a dirty repo state with multiple changes belonging to separate features or abstractions. I usually just pick the changes I want to group into a commit and clean up the state.
Since it's git compatible, it feels like it must work to add files and keep files uncommitted, but just by reading this tutorial I'm unsure.
`jj new` simply means "create a new commit [ontop of <location>]" - you don't have to describe it immediately. I never do.
I know that the intention was to do that, and I tried forcing the habit, but I too found it counter-productive to invariably end up re-writing the description.
I don't usually do that right away, but I often use squash or absorb to move additional changes into a commit I already made in my stack. I think the spirit still applies if you take that course.
My preferred workflow is to start with a new change, pick the changes I want, then use jj commit to describe the change and create a new empty one on top. Feels very similar to my old git workflow.
If I end up with multiple features or abstractions in one change (equivalent to the “dirty repo”), jj split works very well as an alternative to the git add/git commit/repeat workflow tidying up one’s working copy.
> Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.
A good way to think of it is that jj new is an empty git staging area. There's still a `jj commit` command that allows you to desc then jj new.
> I also often end up with in a dirty repo state with multiple changes belonging to separate features or abstractions. I usually just pick the changes I want to group into a commit and clean up the state.
jj split allows you do to this pretty well.
> Since it's git compatible, it feels like it must work to add files and keep files uncommitted, but just by reading this tutorial I'm unsure.
In jj you always have a commit - it's just sometimes empty, sometimes full, has a stable changeid regardless. jj treats the commit as a calculated value based on the contents of your folder etc, rather than the unit of change.
I often will use `jj new -B@` (which I made an alias for) followed by `jj squash -i` to split changes. I had no idea about `jj split`, so I need look into that!
I'm using jj exactly this way, but `jj commit -i` is still somewhat backwards compared to `git commit -i`: jj displays the commit timestamp by default instead of the author timestamp like git. In addition, in jj the author timestamp of a commit is set to the time you started and not ended a commit/change. This results in unexpected timestamps when working with git-using people or tools. Also, it's rather weird if you use a previously empty commit for your work which was created months earlier by a previous `jj commit`, resulting in a timestamp neither correlating to when you started nor ended your work.
I guess the idea of jj's authors is that jj's commits are far more squishy and can always be changed, so a fixed finished timestamp makes less sense. I still prefer git's behaviour, marking work as finished and then keep the author (but not commit) timestamps on amends.
I use this jj alias to get git's timestamp behaviour:
[aliases]
c = ["util", "exec", "--", "bash", "-c", """
set -euo pipefail
change_id=$(jj log -r @ --no-graph -T 'change_id')
desc=$(jj log -r $change_id --no-graph -T 'description')
commit_author=$(jj log -r $change_id --no-graph -T 'author.email()')
configured_author=$(jj config get user.email)
jj commit -i "$@"
if [ -z "$desc" ] && [ z"$commit_author" = z"$configured_author" ]; then
echo "Adjusting author date"
jj metaedit --update-author-timestamp --quiet $change_id
fi
"""]
[templates]
# display author timestamp instead of commit timestamp in log
'commit_timestamp(commit)' = 'commit.author().timestamp()'
> A good way to think of it is that jj new is an empty git staging area. There's still a `jj commit` command that allows you to desc then jj new.
This always made me feel uncomfy using `jj`. Something that I didn't realise for a while is that `jj` automatically cleans up/garbage collects empty commits. I don't write as much code as I used to, but I still have to interact with, debug and test our product a _lot_ in order to support other engineers, so my workflow was effectively:
```
git checkout master
git fetch
git rebase # can be just git pull but I've always preferred doing this independently
_work_/investigate
git checkout HEAD ./the-project # cleanup the things I changed while investigating
```
Running `jj new master@origin` felt odd because I was creating a commit, but... when I realised that those commits don't last, things felt better. When I then realised that if I made a change or two while investigating, that these were basically stashed for free, it actually improved my workflow. I don't often have to go back to them, but knowing that they're there has been nice!
> Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.
Yes, but this is not backwards, the way you do it in git is backwards. =)
I don't think the term "version control" has any implication about precedence, and I don't understand what you mean by "the versions predate the control". In git, you add items to the worktree (control), then you commit (create a version), so doesn't that mean git does it "wrong" according to what you're saying? In jj, you are always on a committed version and the contents of that commit are controlled by your edits, if you want your edits to be on a different commit, you usually just change to that commit and make the edits, although there are other ways to move edits around (which is also true in git).
The point is that there actually isn't a correct order to do these operations, just one that you're familiar with. Other orders of operations are valid, and may be superior for your or your team's workflow.
Nothing stops you from making changes in a commit that has no description and then at the end doing `jj commit -m` to describe them and make a new commit in one go, which is essentially the same as git. The difference is that it's essentially amending in place as you make changes rather than needing to stage first.
Personally haven’t used jj but as far as dvcs’s are concerned Fossil is great complement to Git because it does things differently than git and truly has a decentralized feel.
The autosync feature is really nice too, and you can store backup repos in cloud storage folders and auto sync to those as well.
Fossil is delightful and definitely nails a feeling of decentralization that I think we ruined completely with `git` by constantly centering around centralized repositories.
I also find it interesting that so many people want to switch to something that's not `git` but are simultaneously somehow super invested in it being basically just `git`.
Most teams could switch to Fossil and just have a better time overall. It's made for smaller, high-trust teams, `git` is not. Fossil also manages to actually support external contributions just fine; it's just that it's not the default.
jj is very flexible when it comes to workflow. One thing to note is that commits don't have to have messages. What I tend to do is to run `jj new` frequently while I work on something and leave all of them without messages. Then when I'm ready to make my actual commit, I squash the temporary commits together and then add a message. If my changes are separable, I can split the commits before squashing. This workflow acts as a kind of undo history. I can easily go back to what I had 5 minutes ago and try a different approach, but then also jump back to my original changes if I want. It makes experimentation much easier compared to git.
If you're already super comfortable in git, it's not. I'm saying this as someone who recently converted from git to jj and never wants to go back.
You also don't have to follow what the GP said. I never say `jj describe` before writing code. I write the code then just say `jj commit -m "Foo stuff"`, just like I would in git.
The bigger difference I've noticed is:
1. Switching between changesets just feels more natural than git ever did. If I just run `jj` it shows me my tree of commits (think of it like showing you git's branches + their commits), and if I want to edit the code in one of them I just say `jj edit xyz`, or if I want to create a new commit on top of another one and branch it off in a new direction, I just say `jj new xyz`. It took a little bit for my brain to "get" jj and how it works because I was so used to git's branches, but I'm really enjoying the mental model.
2. `jj undo`. This alone is enough to convert me. I screwed something up when trying to sync something and had a bunch of conflicts I really didn't want to resolve and I knew could have been avoided if I did things differently, but my screwup was several operations ago! So I ran `jj undo`. And ran it again. And again. And again. And then I was back to my clean state several stages ago before I screwed up, despite having made several changes and operations since then. With git? Yeah I could have gotten it fixed and gone back. But every time I've had to do something like that in git, I'm only 25% confident I'm doing it right and I'm not screwing things up further.
3. Rebasing. When I would try to sync git to an upstream GitHub repo that used rebasing for PRs, I would always get merge conflicts. This was because I stack my changes on top of each other, but only merge in one at a time. Resyncing means my PR got a new commit hash, even though none of the code changed, and now git couldn't figure out how to merge this new unknown commit with my tree, even though it was the same commit I had locally, just a different hash. With jj? I never get merge conflicts anymore from that.
Overall the developer experience is just more enjoyable for me. I can't say jj's flow is fundamentally and objectively better than git's flow with branches, but personally and subjectively, I like it better.
It's not, you can literally do everything this tool does with Git, and 80% of the features could be replaced with commands in your shell rc file also using vanilla git.
This tool was described perfectly the other day. JJ is the Dvorak of Git. Most people could careless about Dvorak layout, 99.8% of people use qwerty just fine. Those 0.02% though, they're loud, and they want everyone to know how great the reinvention of bread is.
it's actually git that makes you think backwards - in jj the working tree is a commit, in git it isn't until you at least stage it.
the working tree being a commit has wide ranging implications as all the commands that work with commits start working with the working tree by default.
This is me! I often find that in the process of making one change, I have also made several other changes, and only recognize that they are distinct after following the ideas to their natural conclusion.
Hence I have multiple workspaces, and I shelve changes a lot (IntelliJ. I end up with dirty repos too and that can be painful to cherry-pick from. Sometimes I just create a git patch so I can squirrel the diffs into a tmp file while I cleanup the commit candidate. I often let changes sit for several days while I work on something else so that I can come back and decide if it’s actually right.
It’s chaotic and I hide all this from coworkers in a bid to seem just a bit more professional.
I admire people who are very deliberate and plan ahead. But I need to get the code under my fingers before I have conviction about it.
> I often find that in the process of making one change, I have also made several other changes, and only recognize that they are distinct after following the ideas to their natural conclusion.
I do that all the time. With git, everything starts "unstaged", so I'd use magit to selectively stage some parts and turn those into a sequence of commits, one on top of another.
With jj I'd do it "backwards": everything starts off committed (with no commit message), so I'd open the diff (`D` in majutsu), selecting some parts and "split" (`S` in majutsu) to put those into a new commit underneath the remaining changes. Once the different changes are split into separate commits, I'd give each a relevant commit message.
I'm about the same. jj is kind of perfect for that. Example:
# I've finished something significant! Carve it out from the working "change" as its own commit.
`jj commit --interactive` # aka `jj commit -i` or `jj split`, depending on how you prefer to think of it: making a commit for some work, or splitting a separate commit out of the working change.
# Oops, missed a piece.
`jj squash --interactive` # aka `jj squash -i`
# Let me look at what's left.
`jj diff`
# Oh right, I had started working on something else. I could just leave it in the working change, but let me separate it out into its own commit even though it's unfinished, since I can always add pieces to it later.
`jj commit -i`
# Wait, no, I kind of want it to come before that thing I finished up. Shoot, I messed up.
`jj undo`
# Let me try that again, this time putting it underneath.
`jj split -B @-` # aka `jj split --insert-before @-`. @ is the working change, @- is its immediate parent(s), @-- is all grandparents, etc.
# Note that instead of undoing and re-selecting the parts, you could also `jj rebase -r @- -B @--` to reorder. And in practice, you'll often be doing `jj log` to see what things are and using their change ids instead of things like `@--`.
# I also have some logging code I don't need anymore. Let me discard it.
`jj diffedit`
# Do some more work. I have some additions to that part I thought was done.
`jj squash -i`
# And some additions to that other part.
`jj squash -i --into @--`
# etc.
There's a lot more that you could do, but once you internalize the ideas that (1) everything is a commit, and (2) commits (and changes) can have multiple parents thus form a DAG, then almost everything else you want to do becomes an obvious application of a small handful of core commands.
Note: to figure out how to use the built-in diff viewer, you'll need to hover over the menu with the mouse, but you really just need f for fold/unfold and j/k for movement, then space for toggle.
> It wants me to start with the new and describe command
jj doesn't "want" anything.
I always end a piece of work with `new`: it puts an empty, description-less commit as the checked-out HEAD, and is my way of saying "I'm finished with those changes (for now); any subsequent changes to this directory should go in this (currently empty) commit"
The last thing I do to a commit, once all of its contents have settled into something reasonable, is describe it.
In fact, I mostly use `commit` (pressing `C` in majutsu), which combines those two things: it gives the current commit a description, and creates a new empty commit on top.
Think of it this way: the current change is like a staging area/index in git. Leave it without a a description while you're working (just like git's staging area). Rely on jj's auto-snapshotting to capture all your changes. Then, when you're ready to do something else, give it a description ("jj describe") and switch to a new blank change ("jj new"), and that becomes your new "staging area"/index.
I'm giving jj a try but one aspect of it I dislike is edits to files are automatically committed, so you need to defensively create empty new commits for your changes. As in, want to browse the repo from a commit 2 weeks ago? Well if you just checkout that commit and then edit a file, you've automatically changed that commit in your repo and rebased everything after it on top of your new changes. So instead you create a new branch off of the old commit and add an empty commit to that branch so any file changes don't end up rewriting the past 2 weeks of history. git is much nicer in that I can do whatever I want to the files and it won't change the repo until _I tell it to_.
Just don't ever use `edit`, use `new` instead; then your changes are tracked without making a mess. I think that's much nicer than juggling stashes in git.
If I'm understanding the thread correctly, I have a git alias to `git commit --amend --no-edit`, for exactly this workflow. When I'm hacking on something locally and want to just keep amending a commit. I only ever do this if it's HEAD though.
I go back and forth between the two approaches, but because of the whole "accidentally made some temporary changes and now it's a pain to separate/undo them because not all changes were temporary", I also usually do a jj new and then jj squash.
As a git-ist (?), if I'd ever move away from git, it would be to avoid tooling that has idioms like this (like git too has), if `jj` just gonna surface a bunch of new "bad ideas" (together with what seems like really good ideas), kind of makes it feel like it isn't worth picking up unless you don't already know git.
jj edit has good use cases, but it's not the default command you need. For instance, say you were working on some changes but had to change branches for a few minutes to do something. If you didn't manage to create a commit and want to go back to the previous staging area, you would use the jj edit command rather than jj new. It's very intuitive in my experience, something I can't say is true for managing git commits (unless you've spent years forcing it into muscle memory). I never need to run jj help. I run help commands with git all the time.
`edit` is still useful; just, for ..editing (!) something, instead of viewing it.
If you have some unfinished changes at the tip and want to temporarily checkout something 2 weeks ago, you `jj new` to there (similar to `git stash; git switch whatever`), and then later `jj edit your-old-tip` to go back (equivalent to `git switch main; git stash pop`; I think `jj edit` being an extended replacement for stash-popping things is a reasonable way to think about it). (and if you don't have any uncommitted changes, you always `jj new`)
jj also has a concept of immutable commits (defaulting to include tagged commits, and trunk at origin, which it'll disallow editing as a layer of defense)
The idiom here is use `edit` if you want to edit a commit, and use `new` if you want to make a new commit. This works identically whether you specify the commit via branch name or commit id. I'm not sure why people are saying not to use `edit` ever. It's basically just a shorthand for staging and amending changes in an existing commit, and there's still a use case for that; it's just not "I want to see the changes on this old branch".
> The idiom here is use `edit` if you want to edit a commit
You know, you guys have fun with that, I'll continue using git which (probably) has the same amount of warts, but I already know them. I'll continue to refer new VCS users to jj, seems a lot easier to learn, but really don't have the interest to re-learn a bunch of ever-changing idioms.
I disagree with the people saying "never use edit". There are plenty of people saying conflicting things about git too, and I'd argue that understanding edit versus new isn't anywhere close to the level of wart that having to get people to agree on merging versus rebasing. Like you said though, have fun with that!
No system is perfect, but there's nothing wrong with `jj edit` and `jj new`. Both commands are completely reasonable and do what you think they would do.
I think it's because it's easy to make annoying mistakes (still easy to fix with undo) with edit. And it gains relatively little over new+squash. Edit is a useful power-feature, but I think for a novice, "never use it, only use the more well understood workflow of new+squash" is a good heuristic.
You would have had to run `jj edit` in order for this to happen, so I think it's a stretch to say you didn't ask for the edit?
This is the main difference though: in git files can be `staged`, `unstaged` or `committed`, so at any one time there are 3 entire snapshots of the repo "active".
In `jj` there is only one kind of snapshot (a change) and only one is "active" (the current working directory). When you make changes to the working directory you are modifying that "change".
As others have mentioned, the equivalent to `git checkout` would be `jj new`, which ensures a new empty change exists above the one you are checking out, so that any changes you make go into that new change rather than affecting the existing one.
Using `jj edit` will edit a commit you specify, and `jj new` will make a new empty commit after the one you specify. These work exactly the same whether you specify a commit by branch or by the hash. I'd argue that you're getting exactly what you ask for with these commands, and by comparison, what "checkout" is asking for is much less obvious (and depends on context). We've just internalized the bad behavior of git for so long that it's become normalized.
`jj new` works like `git checkout` most by creating an empty revision on the top. `jj edit` on the other hand resembles `git checkout; [edits...]; git add -A; git commit --amend --no-edit`.
ooo that will be a nice improvement. So many times I've run `jj status`, then saw a file I wanted gitignored, so I'll edit my gitignore, but the file has already been added to the repo so I have to `mv <file> /tmp/ && jj status && mv /tmp/<file> .` to get the file out of the repo.
jj edit is the biggest jj footgun I can think of, as other comments said just use jj new. But also if you do accidentally edit or change something jj undo works surprisingly well.
I found when using jj it worked best for me when I stopped thinking in commits (which jj treats as very cheap “snapshots” of your code) and instead focus on the “changes”. Felt weird for me at first, but I realized when I was rebasing with git that’s how I viewed the logical changes I made anyway, jj just makes it explicit.
jj auto-rebasing doesn’t matter until you push changes, and once you do it marks them immutable, preventing you from accidentally rebasing changes that have been shared.
> jj edit is the biggest jj footgun I can think of
Honestly, this is only because `git checkout` is so convoluted that we've collectively changed our expectations around the UX. "checkout" can mean switching to another branch (and creating it if you specify a flag but erroring if you don't), looking at a commit (in which case you have "detached HEAD" and can't actually make changes until you make a branch) or resetting a file to the current state of HEAD (and mercy on your soul if you happen to name a branch the same as one of your files). Instead of having potentially wildly different behavior based on the "type" of the thing you pass to it, `jj edit` only accepts one type: the commit you want to edit. A branch (or "bookmark", as jj seems to call it now) is another way of specifying the commit you want to edit, but it's still saying "edit the commit" and not "edit the bookmark". Unfortunately, the expectation for a lot of people seems to be that "edit" should have the same convoluted behavior as git, and I'm not sure how to bridge that gap without giving up part of what makes jj nice in the first place.
It's not "wildly" different behavior based on the thing it's pointing to. In all 3 cases, the command is pointed at a commit and the behavior is the same. Once you know that branches/HEAD are just named pointers to commits, then it becomes obvious you are always just working on commits and branches/ids/HEAD etc are just ways of referencing them.
If you don't provide it a <tree-ish> it reads from the index (staged files). So you're right its not really pointed anywhere, since the index isn't a ref.
That's my overall point: the argument itself (with respect to the current state of the repo) is what determines the behavior. I don't think this is anywhere close to as intuitive as commands that only ever accept one "type" of argument (and erroring if it's different).
I stand corrected by this one scenario, but I’ve been using git for over a decade and never found that useful. Just don’t use checkout on a file path, there is no need.
I find this kind of advice to be a more scathing indictment of an interface than a critic could ever muster: asking users to forego available functionality so that some sense of order can be imposed.
edit is useful and there are good reasons to use it, 'never use edit' is like 'never use goto' i.e. false - but if you're just starting out, jj new/jj squash is the way to go indeed.
(my particular favorite reasons to use jj edit are git-native tools which expect to work with uncommitted files e.g. autoformatters, linters, etc. which have been scripted in CI/dev workflows such that they cannot accept a list of files as params)
That goes in the same bucket as rebase. Until you know what it does, you'll be fine avoiding it.
Since people are sharing their experiences and my recent one is relevant to edit, I'll go:
Working on a feature recently, I ended up making 3 changes ("commits") on top of each other and hopping between them via jj edit.
The first change wasn't feature specific, it was extending the base project in preparation.
The second change just added a doc describing all the changes needed for the feature.
The third change removed the doc as parts were implemented, bit by bit.
As I progressed on the third change & found stuff I'd missed at the start of this process, I jumped back to edit the first change (maybe I had a bug in that base project extension) and the second change (oh hey, I found something else that needed to be done for the feature).
It sounds crazy compared to a git workflow, but at the end of the process I have 3 changes, all tested & working. If I was doing this with git, I'd have to rebase/squash to get the final changes into a neat clear history.
If you don't run checkout on file paths, how do you undo changes to specific files that you haven't committed yet? Like you've edited but not committed <foo>, <bar>, and <baz>. You realize your edits to <bar> are a mistake. I'd just run `git checkout <bar>` to revert those changes, what do you do?
It is also really useful when you realize you want <bar> to be the version from a commit two weeks ago. I guess you could always switch to the branch 2 weeks ago, copy the file to /tmp/, switch back, and copy the file into place, but `git checkout c23a99b -- <bar>` is so quick and easy. Or does this example not fall under the "dont run checkout on a path" since it is taking a treeish first before the path?
"Just don't accidentally do things wrong" is also the way to avoid null pointer errors, type mismatches in dynamically typed languages, UB in C/C++. It works, until it doesn't, and in practice that happens pretty quickly. Personally, I like things that have proper safety checks.
But branches are not just named pointers to a commit. If they were, then checking out the pointer would be the same as checking out the commit itself. But I can check out a commit and I can check out a branch and depending on which I've done, I'm in two different states.
Either I'm in branch state, where making a commit bumps the branch pointer and means the commit will be visible in the default log output, or I'm in "detached head" mode, and making a commit will just create a new commit somewhere that by default is hidden into I learn what a reflog is. And the kicker is: these two states look completely identical - I can have exactly the same files in my repository, and exactly the same parent commit checked out, but the hidden mode changes how git will respond to my commands.
In fairness, none of this is so difficult that you can't eventually figure it out and learn it. But it's not intuitive. This is the sort of weirdness that junior developers stumble over regularly where they accidentally do the wrong kind of checkout, make a bunch of changes, and then suddenly seem to have lost all their work.
This is one of the ways that I think the JJ model is so much clearer. You always checkout a commit. Any argument you pass to `jj new` will get resolved to a commit and that commit will be checked out. The disadvantage is that you need to manually bump the branch pointer, but the advantage is that you don't necessarily need branch pointers unless you want to share a particular branch with other people, or give it a certain name. Creating new commits on anonymous branches is perfectly normal and you'll never struggle to find commits by accidentally checking out the wrong thing.
No they don't. As you noted, one state is "detached head" and any competently set up shell PS1 will tell you that, or that you're on a branch by displaying the name of the branch vs the commit.
> Creating new commits on anonymous branches is perfectly normal
Sorry, that that's an example of more intuitive behavior on jj's partc, you've lost me. I've done that intentionally with git, but I know what I'm doing in that case. For someone new to version control, committing to an unnamed branch doesn't seem like a desired operation no matter which system you're using. What's wrong with requiring branches to be named?
> For someone new to version control, committing to an unnamed branch doesn't seem like a desired operation no matter which system you're using.
We have data on this! I can't cite anything public, but companies like Meta have to train people who are used to git to use tools like sapling, which does not require named branches. In my understanding, at first, people tend to name their branches, but because they don't have to, they quickly end up moving towards not naming.
> What's wrong with requiring branches to be named?
Because it's not necessary. It's an extra step that doesn't bring any real benefits, so why bother?
Now, in some cases, a name is useful. For example, knowing which branch is trunk. But for normal development and submitting changes? It's just extra work to name the branch, and it's going to go away anyway.
Fascinating. The benefit it brings is you can map the branch to its name. Of the, say, 10 branches you've got checked out, how do you know which branch maps to jira-123 and which one maps to jira-234, or if you're using names, which anonymous branch maps to addFeatureA or fixBugB?
More to the point though, what tooling is there on top of raw jj/git? Specifically, there's a jira cli (well, multiple) as well as a gh cli for github as well as gitlab has one as well. When you call the script that submits the branch to jira/github/gitlab, how does it get the ticket name to submit the code to the system under? Hopefully no one's actually opening up jira/github/gitlab by hand and having to click a bunch of buttons! So I'll be totally transparent about my bias here in that my tooling relies on the branch being named jira-123 so it submits it to jira and github from the command line and uses the branch name as part of the automated PR creation and jira ticket modification.
> Of the, say, 10 branches you've got checked out, how do you know which branch maps to jira-123 and which one maps to jira-234, or if you're using names, which anonymous branch maps to addFeatureA or fixBugB?
The descriptions of the changes. I shared some jj log output in another comment, here it is with more realistic messages, taken from a project of mine:
@ vvxvznow
│ (empty) (no description set)
│ ○ uuowqquz
├─╯ Fix compiler panic in error rendering for anonymous struct methods (rue-fwi9)
│ ○ uvlpytpm
├─╯ Stabilize anonymous struct methods feature
◆ lwywpyls trunk
│ Fix array return type unification in type inference
That (rue-fwi9) is the equivalent of jira-123, if I super care about it being obvious, I might put it in the message. But also, I might not, as you can see with the other two. You could also pass flags to see more verbose output, if the first line isn't clear enough, but in general, the convention for git as well is to have that short summary that explains your change, so if it's confusing, you probably need to do better on that.
> Specifically, there's a jira cli (well, multiple) as well as a gh cli for github as well as gitlab has one as well.
These systems do require branches in order to open a pull request. In these cases, I use `jj git push -c <change id>`, which will create a branch name for me, and push it up. This is configured to produce a branch name like steveklabnik/push-mrzwmwmvkowx for a change with the id mrzwmwmv, and ultimately, it's still easier to name locally with m or mr depending on if the prefix is ambiguous. That said, from there I do usually just click the button and then "open pull request" on GitHub, but like, all of these tools (gh is the only one I've used, but I can't imagine that the others do not work, since ultimately, it's a git repo) just work if you want to use them.
Other systems do not even require a branch to submit, and so you don't even need to do this. I would say "submit mr" and it would return me the URL for the created change request. Gerrit does this on top of plain old git.
> how does it get the ticket name to submit the code to the system under?
I haven't worked with Jira in a long time, but with GitHub, if I make a change that fixes issue 5, I put "Fixes #5" in my description, and when the PR is created, it updates ticket #5 to link the PR to that change automatically, no other process needed.
Right, this is a good point: you can if you want to, or if you're working with a system that requires them.
Just in practice, anonymous branches end up feeling very natural, especially during development, and especially if your code review tooling doesn't require names.
> any competently set up shell PS1 will tell you that
I certainly hope your shell is not running `git` commands automatically for you. If so, that is a RCE vulnerability since you could extract a tarball/zip that you don't expect to be a git repository but it contains a `.git` folder with a `fsmonitor` configured to execute a malicious script: https://github.com/califio/publications/blob/main/MADBugs/vi...
Might want to let git know. It's been a part of the git source code since 2006. If there were an RCE vulnerability from using __git_ps1, one would hope it would have been found by now!
I was able to reproduce it using that script in my PS1 when `GIT_PS1_SHOWUNTRACKEDFILES=1` which triggers a call to `git ls-files`. Without that, it seems to be just calling `git rev-parse` which does not execute fsmonitor.
I was also able to reproduce it with `GIT_PS1_SHOWDIRTYSTATE=1` which invokes `git diff`.
They look identical to people who don't know what to look for, and who don't realise that these two states are different, which is the key thing. You can also distinguish them by running `git status`, but that's kind of the point: there's some magic state living in .git/ that changes how a bunch of commands you run work, and you need to understand how that state works in order to correctly use git. Why not just remove that state entirely, and make all checkouts behave identically to each other, the only difference being which files are present in the filesystem, and what the parent commit was?
What's wrong with unnamed branches? I mean, in git the main issue is that they're not surfaced very clearly (although they exist). But if you can design an interface where unnamed branches are the default, where they're always visible, and where you can clearly see what they're doing, what's wrong with avoiding naming your branches until you really need to?
I think this is the key thing that makes jj so exciting to me: it's consistently a simpler mental model. You don't need to understand the different states a checkout can be in, because there aren't any - a checkout is a checkout is a checkout. You don't need to have a separate concept of a branch, because branches are just chains of commits, and the default jj log commands is very good at showing chains of commits.
or
fragmede@laptop:(abcdef)~/projects/project-foo$
Depending on if abranch is checked out, or abcdef which may be HEAD of abranch is checked out.
If you're having to run `git status` by hand to figure out which of the two states you're in, something's gone wrong. (That something being your PS1 config.) If people are having trouble with that, I can see why switching to a system that doesn't have that problem, it just that it doesn't seem like it should even be problem to begin with. (It's not that it's not useful to have unnamed branches and to commit to them, just that it's not a intro-to-git level skill. Throwing people into the deep end of the git pool and being surprised when some people sink, isn't a good recipe for getting people to like using git.)
> What's wrong with unnamed branches?
As you point out, those commits kinda just go into the ether, and must be dug out via reflog, so operationally, why would you do that to yourself. Separate from that though, do you "cd" into the project directory, and then just randomly start writing code, or is there some idea of what you're working on. Either a (Jira) ticket name/number, or at least some idea of the bug or feature you wanna work on. Or am I crazy (which I am open to the possibilty) and that people do just "cd" into some code and just start writing stuff?
VCS aside, nothing worse than opening Google docs/a document folder and seeing a list of 50 "Untitled document" files an my habit of naming branches comes from that. Even though I'm capable of digging random commits out of reflog, if all of those commits are on unnamed branches, and have helpful commit messages like "wip" or "poop", figuring out the right commit is gonna be an exercise in frustration.
As long as you've got something that works for you though, to each their own. I've been using too long for me to change.
> preventing you from accidentally rebasing changes that have been shared.
I think this ruins it for me then. I push my in-progress work, to my in-progress branches (then git-squash or whatever later, if needed). It makes switching between (lab) computers, dead or not, trivial.
Is there some "live remote" feature that could work for me, that just constantly force pushes to enabled branches?
Nothing stops you from doing the equivalent of `git push --force` in `jj`. The flag is just named differently: `--ignore-immutable`. This is a global flag though, so it's available to all commands, and `jj` requires it whenever you're making changes to immutable commits, even locally. I'd argue that this is one of the killer features of `jj`, since by comparison `git rebase` treats everything the same whether you're squashing your own local commits on a feature branch or messing with the history of `main` in a way that would break things for everyone.
Yes, almost all JJ users do this constantly. Just "track" the particular branch. JJ has an idea that only some commits are immutable, the set of "immutable heads", and the default logic is something like "The main branch is always immutable, remote branches are immutable, 'tracked' remote branches are mutable." In other words, tracking a remote branch removes it from the set of immutable heads.
How are you "checking out" the old commit? It sounds like you're using `jj edit`, which I'd argue does what it says on the tin. Switch to using `jj new <branch>` and your problem goes away.
That avoids the problem for the specific workflow of checking out an old revision (and it was what I was describing with checking out a new branch off the old commit and adding a blank commit to that branch), but another way this design bites me: At work I am constantly jumping around numerous repos because I might be working on repo <foo> but then someone on my team will ask for help with repo <bar>. So I'll turn on screen sharing, open up repo <bar> and I'll type out psuedo-code into <bar> as I'm explaining things to them.
So if the last thing I did on <bar> was finish some work by making a new commit, then writing some changes, and then giving it a commit message with `jj desc`, then I am now polluting that commit with the unrelated explanatory psuedo-code. So when switching to a repo I'm not actively working in, I need to defensively remember to check the current `jj status` before typing in any files to make sure I am on an empty commit. With git, I can jump around repos and make explanatory edits willy-nilly, confident that my changes are distinct from real meaningful commits.
I guess one way to describe it is: we want to make it easy to make good commits and hard to make bad commits. jj seems to be prioritizing the former to the detriment of the latter. My personality prioritizes rigorous safety / lack of surprises.
Fwiw I generally solve this by using `jj commit` instead of `jj desc` unless I'm specifically targeting something that isn't my working copy. Technically it violates the "we want commands to be orthogonal" guideline we use to write Jujutsu (otherwise this would indeed be `jj desc; jj new`) but as a habit it's never let me down
Ah, thanks! That's a command I haven't learned yet, so I'll have to check it out. I learned jj from the tutorial that was posted and I don't think it covered `jj commit` at all.
I didn't cover it for various reasons, but I think it's good to teach now that I've had more time to consider this sort of thing, so the next iteration will likely start by beginning with jj commit.
In a pure `jj` model, commit might not even be necessary as it's own subcommand (since you could easily define an alias for `desc` followed by `new`). We're still living in a world where most people who would consider adopting `jj` are git users currently, so I wonder if starting with `commit` and then following it up with an explanation of "here's how you can change the commit message without needing to make a new commit" and "here's how you can make a new commit without changing the name of the current one" would end up fitting people's expectations better.
I tend to learn "bottom-up", so I like the new + describe as a way of learning, but people want to jump in and get going with tools, so commit fits that expectation better.
I think you have somehow picked up an overcomplicated workflow, and this is case is actually something that `jj` is much better at.
If I'm in the middle of working on <foo> and someone asks about <bar>: `jj new <bar>`. When I'm done (and do whatever I want with those new changes in <bar>, including deferring deciding what to do), I just `jj edit <foo>` and I'm back exactly where I left off. It's a bit like `git stash` without having to remember to stash in advance, and using regular commit navigation rather than being bolted on the side.
I think the right intuition to have with jj is that `jj st` should show an empty change unless you are actively working on something. `jj commit`, as mentioned below, is a good example of this - it automatically creates a new change and checks it out. The "squash flow" also does this well - you use the branch tip as a staging area and squash work into other changes on the branch as you go along. Either way, once the work is finished, there's an empty change at the tip of the branch.
This is also supported by jj implicitly - whenever you check out a different commit, if the change you were on is empty, has no description, and is the tip of a branch, it's automatically deleted to clean things up for you.
Jujutsu has a concept of mutable vs immutable commits to solve this. Usually everything in a remote branch is immutable. To work on a branch, I track it and that makes it mutable.
if you loose an edit jj op log is incredible, I've saved a ton of work more-so now with AI's making mistakes. Also workspaces are super fast compared to git worktree's - same concept, different implementation.
I agree, that was a bit of an interesting approach but more-so than not it's been better in DX even though you have to 'unlearn' long term it's been a benefit IMO, but a soft one, not something you can measure easily.
this is a core feature and it makes jj possible - you're supposed to get used to jj new and jj squash into the previous bookmarked commit, which you map to the git branch head/PR.
IOW you're supposed to work on a detached git head and jj makes this easy and pleasant.
The last paragraph might be the most important one:
> There's one other reason you should be interested in giving jj a try: it has a git compatible backend, and so you can use jj on your own, without requiring anyone else you're working with to convert too. This means that there's no real downside to giving it a shot; if it's not for you, you're not giving up all of the history you wrote with it, and can go right back to git with no issues.
But this is not true. They are interoperable but far from seamless. Those features mainly support migration use cases or things like git deployment from an repo managed in jj. Operations git does are not in jj’s log. You have to constantly import them. The project recommends a single primary interface.
But it is true. I (and many others) happily use jj on teams that use git without anyone else on the team using jj or knowing (or caring) what I'm using.
If you constantly switch between the two, you're going to have a hard time, but you can take a git repo, try jj for a while, and if you decide to go back, you don't lose anything.
How so? I've used `jj` locally on teams where most (if not all) of the other team members were using git, and they only found out I was using `jj` when I told them.
Yeah same here, have been using jj exclusively, the only reason people notice is because my branch names default to the changeid in my setup so I've had questions about the random looking strings.
What problems, exactly, are you suggesting exist? I have used jj extensively on git teams and it has been seamless. The only people who have noticed or cared are the coworkers I’ve gotten to join me.
You're confusing mixing git and jj in your local copy of the repo vs what it looks like to other people. You can use jj locally, and it interoperates perfectly with any git remote, and no one has to know you're even using it. From the point of view of other people, it doesn't matter.
I think you are talking about colocation, which is slightly different than the `jj git push` `jj git fetch` type commands.
Colocation has its uses bit is a bit finicky. The push/pull compatibility works perfectly fine (with some caveats of github being broken that can be worked around).
Most importantly, submodules are not fully supported, which are used by almost every open source project at least in the space I work in (embedded). So you can't use jj to easily contribute back to those project. It can be done but you always have to be cognizant of whether a submodule has changed between two branches or when you sync, since they don't update automatically the way they do with git.
It's been over a year since I last used git manually in the CLI, and I've exclusively worked with git remotes. The only time I had any friction was on a team where stale code-gen output was checked into the repo and for whatever reason no one was willing to either add it to the `.gitignore` or commit (pun intended) to keeping it up to date, meaning that I had to manually remove the changes from when I compiled before pushing. I would have argued in favor of adding to .gitignore or keeping it up to date even if I didn't use `jj` though because I think having stale output checked in is just silly.
I did try this, but for whatever reason it kept getting added back automatically. I forget the details of exactly why it was happening because it was close to a year ago, and in the compatibility guide it says this is supported, but I'm not sure if it was at the time or I was running into something different. This was a contract gig for me where I knew it would be ending within a month or so, which meant I didn't bother spending a ton of time trying to figure out a long-term solution.
Big caveat: do not try to use Git and JJ in the same directory. It's probably fine if you only use JJ, but if you mix them you will horribly break things.
Jujutsu uses git as its primary backing store and synthesizes anything else it needs on top on-the-fly. Any incompatibility here is considered a serious bug.
Obviously I can’t argue against your lived experience, but it is neither typical nor common. This is quite literally an explicitly-supported use, and one that many people do daily.
I suppose it depends what you mean by "horribly break things".
The only thing I've noticed is that `jj` will leave the git repo with either a detached HEAD, or with a funny `@` ref checked out.
I don't think that would trouble someone who's experienced with git and knows its "DAG of commits" model.
For someone who's less experienced, or only uses git for a set of branches with mostly linear history (like a sort of "fancy undo"), I could imagine getting a shock when trying to `git commit` and not seeing them on any of the branches!
Submodules work fine but yeah, it's frustrating that lfs is taking so long. But there seems to be some momentum recently https://github.com/jj-vcs/jj/pull/9068
What "not supported" means with submodules specifically is that jj doesn't have commands to manage them. You can use git commands to manage them, and it does, in my understanding, work. There's just no native support yet.
This is sort of similar to how you can create lightweight tags with jj tag, but you need to push them with git push --tags.
For each change you've made in the current revision, it finds the last commit where you made a change near there, and moves your changes to that commit.
Really handy when you forgot to make a change to some config file or .gitignore. You just "jj new", make the changes, and "jj absorb". No need to make a new commit or figure out where to rebase to.
Oh, and not having to deal with merge conflicts now is awesome. My repository still has merge conflicts from months ago. I'll probably go and delete those branches as I have no intention to resolve them.
Why should you care about jj? Look, ethereal, balaeric Gothenburg indie from the 2010s may not be important in the grand scheme of things, but their strong hip hop influence was genuinely exciting at the time. When the great chill wave summer of 2009 crested, you definitely cared about jj’s otherworldly grooves and lil Wayne samples. Even if they never did reach the euphoric highs of Washed Out or even label mates The Tough Alliance.
So, I haven't updated the tutorial in a long time. My intent is to upstream it, but I've been very very busy at the startup I'm at, ersc.io, and haven't had the chance. I'm still using jj every day, and loving it.
jj automatically hides "uninteresting" changes. Most of the time, this is good.
Occasionally, I need to see more changes. It is not obvious to me how I get jj to show me elided changes. I mean, sure, I can explicitly ask jj to show me the one ancestor of the last visible change, and then show me the ancestor of that one, etc. Is some flag to say: "just show me 15 more changes that you would otherwise elide"?
I use `jj log -r ..` for that, which is just an open ended range. It's not the "15 more" but it's what's worked for me. I suspect you could do it with some sort of revset stuff, but I like to keep it simple.
I think one major difference between git and jj is how immutable their DAG is, due to the difference in how they refer to their unit of change (i.e. stable change ID with changing commit IDs vs. immutable commit ID). One implication of that is change history in a git repo feels much more immutable to the one in a jj repo. Consequently operations that involves changing the history like, undo/rebase feels much easier/flexible. Is my understanding correct?
Sorta! I think it can feel that way at times, but also the opposite. jj’s changes are immutable in the same way commits are, when you modify a change, it makes a new immutable commit and associates that with the change. So on the literal level, they’re the same.
But it’s true that mutating history is easy and sometimes even automatic with jj, whereas it’s not with git. So that could make it feel more mutable. On the other hand, jj has the concept of mutable vs immutable commits; jj will present you from modifying certain changes unless you pass in a flag to override it. So in some ways, it’s more immutable than git.
I think the mental model is like C vs python.
Git gives you a forensic trace back in time. jj gives you a story with chapters. Look under the hood you'll still see forensic map of state transitions, but this not what we want to navigate most of the time. Sometimes we need to rewrite an early chapter to make the latest chapter make more sense.
the fact that almost by definition stuff that jj does is possible in git makes it hard for some folks to let go of the baggage that git has; it's simply hard to imagine a world where you can't git add ('how do you commit what you need committed and not commit the rest?') or not having to resolve conflicts immediately ('why would I want not to?')
...and it turns out when you answer these questions differently ('working tree is a commit', 'conflicts can committed) but still want git compatibility, jj kinda falls out of the design space by necessity.
One of the things that makes jj worth trying out is simply the fact that it is different than git, and having exposure to more than one way of doing things is a good thing.
Even if you don't adopt it (and I didn't), it's easy to think that "this way is the only way", and seeing how systems other than your own preferred one manage workflows and issues is very useful for perspective.
That doesn't mean you should try everything regardless (we all only have so much time), but part of being a good engineer is understanding the options and tradeoffs, even of well loved and totally functional workflows.
"It's more powerful and easier" is a great claim, but I need examples in this opening page to convince me of the pain I could save myself or the awesome things I'm living without.
does trivially working on 3 PRs in a single checkout and pushing focused changes to each one independently without thinking twice count?
if you don't need this, you might not see any value in jj and that's ok. you might use magit to get the same workflow (maybe? haven't used magit personally) and that's also ok.
in large enough monorepos and teams and big enough changes you either do it like this or have a humongous giga-PR which eventually starts conflicting with everything.
Guess he was talking about the presentation, not what the tool can achieve. It has no hard proof on the first page, which could easily just be a LinkedIn pitch, but not on hackernews
It might count, but it is easy with git as well, what is the feature in jj that makes this easier? Switching branches and pushing changes to remotes is the core feature of git and in my opinion really easy so I'm curious how jj improves on it.
I know how I would do this in git, but don't really see how this would be in jj. I currently don't use it in my workflow, but if it is super easy in jj then I could see myself switching.
the beauty of it is there's not much to show; I use a crude jjui approach where I have an octopus merge working tree commit (in command line terms, jj new PR_A PR_B PR_C) and either use native jj absorb (S-A in jjui) which guesses where to squash based on the path or, when I'm feeling fancy, rebase the octopus via jjui set parents (S-M) function (also handy to clean up parents when one of the PRs gets merged).
This creates an empty commit that merges all 3 branches, you can think of this as your staging area.
When you want to move specific changes to an existing commit, let's say a commit with an ID that starts with `zyx` (all jj commands highlights the starting characters that make the commit / change unambiguous):
jj squash -i --to zyx
Then select your changes in the TUI. `-i` stands for interactive.
If you want to move changes to a new commit on one of the branches:
jj split -i -A branch1
Then select the changes you want moved. `-A` is the same as `--insert-after`, it inserts the commit between that commit and any children (including the merge commit you're on).
There's one thing that's a bit annoying, the commit is there but the head of the branch hasn't been moved, you have to move it manually (I used + to get the child to be clearer, but I usually just type the first characters of the new change id):
Consider using the table of contents on the left of the page to view "Real World Workflows", "Branching, Merging, and Conflicts", and then "Sharing Your Code with Others" and then evaluate how JJ does things against your current git workflow. This requires some minor effort on your part.
Yeah we moved on from SVN to git because SVN branches were truly a pain in the ass to work with. I truly do not have any rough edges or big pains in my day to day git workflow.
The general idea here is that jj has fewer and more orthogonal concepts than git. This makes it more regular, which is what I mean by "easy."
So for example, there is no index as a separate concept. But if you like to stage changes, you can accomplish this through a workflow, rather than a separate feature. This makes various things less complex: the equivalent of git reset doesn't need --hard, --soft, --mixed, because the index isn't a separate concept: it's just a commit. This also makes it more powerful: you can use any command that works on commits on your index.
Specific commands don't really showcase the appeal of jj. If anything they might scare someone at first glance. It's the fact that the workflows are intuitive and you never find yourself reaching for help to get something done. You really need to try it to understand it.
jj is better for some workflows, which, if you're a git expert as you claim, you conciously or subconciously avoid as 'too much work' or 'too brittle'.
if you don't care about them after accepting this realization... it's fine. git is good enough.
I’m not a fit expert by any means. The workflows being described do not appeal to me but not because of the way fit works. They sound confusing and I don’t understand what benefit I’m getting out of them. Like, it’s a solution to a problem I’m not sure exists (for me)
- You aren't forced to resolve rebase/merge conflicts immediately. You can switch branches halfway through resolving conflicts and then come back later and pick up where you left off. You can also just ignore the conflicts and continue editing files on the conflicted branch and then resolve the conflicts later.
- Manipulating commits is super easy (especially with jjui). I reorder commits all the time and move them between branches. Of course you can also squash and split commits, but that's already easy in git. Back when I was using git, I would rarely touch previous commits other than the occasional squash or rename. But now I frequently manipulate the commit history of my branch to make it more readable and organized.
- jj acts as a VCS for your VCS. It has an operation log that is a history of the state of the git repository. So anything that would be destructive in git (e.g. rebase, pull, squash, etc) can be undone.
- Unnamed branches is the feature that has changed my workflow the most. It's hard to explain, so I probably won't do it justice. Basically you stop thinking about things in terms of branches and instead just see it as a graph of commits. While I'm experimenting/exploring how to implement or refactor something, I can create "sub-branches" and switch between them. Similar to stashes, but each "stash" is just a normal branch that can have multiple commits. If I want to test something but I have current changes, I just `jj new`. And if I want to go back, I just make a new commit off of the previous one. And all these commits stick around, so I can go back to something I tried before. Hopefully this made some sense.
Also note that jj is fully compatible with git. I use it at work and all my coworkers use git. So it feels more like a git client than a git replacement.
All of these features sound like the recipe for a confusing nightmare!
"You can switch branches halfway through resolving conflicts and then come back later and pick up where you left off. You can also just ignore the conflicts and continue editing files on the conflicted branch and then resolve the conflicts later."
"Similar to stashes, but each "stash" is just a normal branch that can have multiple commits. If I want to test something but I have current changes, I just `jj new`. And if I want to go back, I just make a new commit off of the previous one. And all these commits stick around, so I can go back to something I tried before."
typical for experienced git users who already 'just don't do' things which git punishes you for; after a decade it's hard to even imagine any other way, not to mention that it might be better. been there, done that, jj is legit after letting go of (some of) git.
Anonymous branches are amazing for when you are trying out a bunch of different approaches to a problem. As I search the space of possible solutions for what I'm really looking for, I end up with a tree of various approaches.
Then when you rebase, the entire tree of anonymous branches can be rebased onto main in 1 command. This is why the first class conflicts and not having to resolve conflicts immediately is so important: when i'm rebasing, an entire tree of branches is getting rebased and so if you had to resolve conflicts right away it would be incredibly cumbersome, because I'm rebasing like 30+ commits and a bunch of anonymous branches in a single operation.
I work on top of an octopus merge of all my in-flight PRs. ON top of that merge commit i have a bunch of anonymous branches with various things going on. When I'm ready to submit a PR, I take one of those anonymous branches and rebase it onto main and make it an additional parent of my 'dev-base' merge commit. Then i give that branch a name and submit it as a PR.
Every day when I start working, I rebase this entire subgraph of branches in a single command onto main. all my PRs are up to date, all my anonymous branches are up to date, etc... Takes like 2 seconds. If some of my anonymous branches are in a conflicted state, that's ok, i don't have to deal with it until I want to work on that change again.
These anonymous branches aren't confusing because they all show up in the default revset that is shown when looking at the jj log. I can easily browse through them with jjui TUI and instantly see which ones are what. It's really not confusing at all.
The "you don't need to worry about resolving conflicts" thing is confusing when you hear it with words, so let me show you what it looks like in practice.
Let's say I have two branches off of trunk. They each have one commit. That looks like this (it looks so much nicer with color, I'm going to cut some information out of the default log so that it's easier to read without the color):
So both `foo` and `bar` are on top of trunk, and I'm also working on a third branch on top of trunk (@). Those vvxv and such are the change ids, and you can also see the named trunk there as well.
Now, I fetch from my remote, and want to rebase my work on top of them: a `jj git fetch`, and then let's rebase `foo` first: that's `jj rebase uu -o trunk` (you only need uu instead of uuowqquz because it's a non-ambiguous prefix, just like git). Uh oh! a conflict!
Note that jj did not put us into a "hey there's a conflict, you need to resolve it" state. It just did what you asked: it rebased it, there's a conflict, it lets you know.
So why is this better? Well, for a few reasons, but I think the simplest is that we now have choice: with git, I would be forced to deal with this conflict right now. But maybe I don't want to deal with this conflict right now: I'm trying to update my branches in general. Is this conflict going to be something easy to resolve? In this case, it's one commit. But what if each of these branches had ten commits, with five of them conflicted and five not? It might be a lot of work to fix this conflict. So the cool thing is: we don't actually have to. We could continue our "let's rebase all the branches" task and rebase bar as well. Maybe it doesn't have a conflict, and we'd rather go work on bar before we come back and deal with foo. Heck, sometimes, I've had a conflicted branch, and then a newer version of trunk makes the conflict go away! I only have to choose to address the conflict at the moment I want to return to work on foo.
There's broader implications here, but in practice, it's just that it's simply nicer to have choice.
Turns out, git sorta trains you to be very, very afraid of breaking something.
jj answers this in a few ways:
1. everything is easily reversible, across multiple axes.
2. yes, everything is basically a stash, and it's a live stash — as in, I don't have to think about it because if it's in my editor, it's already safely stored as the current change. I can switch to a different one, create a new one, have an agent work on another one, etc, all without really caring about "what if I forgot to commit or stash something". Sounds like insanity from a git POV but it really is freeing.
3. Because of 2, you can just leave conflicts alone and go work on something else (because they are, like you said, essentially stashed). It's fine and actually very convenient.
The thing the article doesn't mention, that makes this all safe, is that trunk / "main" is strictly immutable. All this flexibility is *just* for unmerged WIP. (There are escape hatches though, naturally!)
In practice, it isn't. What you're identifying as potentially nightmarish - and no doubt quite tedious in git - are things that JJ enables you to do with a small subset of commands that work exactly how you expect them to work _in every workflow context_ in which they are needed.
Thinking specifically about conflicts: being able to defer conflicts until you're ready to deal with them is actually great. I might not be done with what I am actually working on and might want to finish that first. being forced into a possibly complicated conflict resolution when I'm in the middle of something is what I'd actually consider nightmarish.
When you want to solve the conflict: `jj new <rev>`, solve the conflict, then `jj squash`, your conflict resolution is automatically propagated to the chain of child commits from the conflict.
Remember when you used SVN or whatever before git, and you loved git because of how easy it is to make branches?
With branches, jj is to git what git was to SVN. It's an order of magnitude less friction to do branching in jj than git.
Not long ago, I pulled from main and rebased my branch onto it - merge conflicts. But I wanted to work on some other feature at the moment. Why should I have to fix this merge conflict to work on a feature on a totally different branch? With jj, I don't. I just switch to the other branch (that has no conflict), and code my new feature. Whenever I need to work on the conflicted branch, I'll go there and fix the conflict.
Once I started using jj, I realized how silly it was for git to have separate concepts for stash and index. And it's annoying that stash/index is not version controlled in git. Or is it? I have no idea.
In jj, a stash is simply yet another unnamed branch. Do whatever you want there. Add more commits. Then apply it to any branch(es) that you would like to. Or not.
Why does git need a separate concept of a stash? And wouldn't you like a version controlled stash in git?
Have you ever made a ton of changes, done a "git add", accidentally deleted some of the changes in one file, done a "git add", and thought "Oh crap!" I suppose that information can be recovered from the reflog. But wouldn't you wish "git add" was version controlled in the same way everything else is?
That's the appeal of jj. You get a better stash. You get a better index. And all with fewer concepts. You just need to understand what a branch (or graph) is, and you get all of it. Why give it a name like "stash" or "index"?
Why does git insist on giving branches names? Once you get used to unnamed branches, the git way just doesn't make sense. In jj you'll still give names wherever you need to.
I also like the powerful revision querying mechanisms that they pulled in from mercurial. They seem to work just like mercurial revset queries which can be used in various operations on sets of revisions.
I would like them to have mercurial's awesome hg fa --deleted when it comes to history trawling, but apparently for it to work well, they also need to swap out git's diff format for mercurial's smarter one, so I'll be waiting on that for a while I suppose.
"jj undo" is worth the price of admission by itself.
See the current top thread on HN about backblaze not backing up .git repos. People are flaming OP like they're an idiot for putting a git repo in a bad state. With jj, it's REALLY HARD to break your repo in a way that can't be fixed by just running "jj undo" a couple times.
jj has made me much more comfortable using non-linear DAGs in my trunk-based development workflow. Several changes with the same parent, changes with several different parents, etc.
I used to have a habit of imposing an unnecessary ordering structure on my work product. My stack of changes would look like A -> B -> C -> D, even if the order of B and C was logically interchangeable.
jj makes DAGs easier to work with because of how it handles conflicts and merges. Now I feel empowered to be more expressive and accurate about what a change actually depends on. In turn, this makes review and submission more efficient.
I was already pretty happy with svn to be honest, I dont see myself switching away from the industry standard today for no substantial reason. in my opinion git was only able to change the standard thanks to github and a popular author (i love git and its branching, but I dont think it would have been enough if it was just for that). I personally believe its going to be very difficult for jj to replicate that.
I like the distributed nature of git but miss how svn worked to some degree. Generally I agree with you, git works, I read the post and didn't see anything that makes me want to change. The small number of jjites who have been inundating comment sections on this site lately feels like the devotees of Rust (a crap language), podman (which has too many operational inconsistencies) and JetBrains IntelliJ ballywhooers who used to preach IntelliJ's features on every programming related site.
In my opinion, svn lost to git because git is just so much better in a distributed environment. To me the improvement between git and svn is much more than the improvement between svn and cvs. In fact, by many aspects, I preferred cvs, but now that we have git, they can both go to hell.
Now, it is not the only DVCS, there is also Mercurial, which is built on similar principles. It could have become the standard, but I agree with you that the reason it didn't probably has to do with Github and Linux.
jj isn't fundamentally different from git, it is actually compatible with git, which is a good and a bad thing. Good because it makes transition easy, bad because it is fundamentally the same thing and there is no pressing reason to change.
But jj definitely look nicer than git, which is not hard, under the hood, git is great, but it is ugly. It started off as a hack and it still shows. I think the common sentiment of "I know git, I don't want to learn a new VCS" mostly tells about how painful the process of learning git was. If you had started off with Mercurial, like I did, I am sure it would feel much smoother, it is just git that is messy. For jj, it looks like they took the best parts of git and mercurial, so hopefully, the transition will be rather painless.
I really wanted to like JJ, it was handy for a few months when I used it. But for me in the end I reverted back to regular git.
What triggered me to go back was I never got a really clean mental model for how to keep ontop of Github PRs, bring in changes from origin/main, and ended up really badly mangling a feature branch that multiple contributors were working on when we did want to pull it in. I'll probably try it again at some point, but working in a team through Github PRs that was my main barrier to entry.
This is good feedback, thanks. The next version of the tutorial will certainly focus on stuff like this, as I agree it's really important to teach people.
locally? sure. stacked changes in jj are great. but the moment you push to GitHub, the review UI still thinks in SHAs. a lot of the pain just moves from the author to the reviewer.
jj is great and while it was an adjustment at first, I've never looked back. I feel like when you're working with other people, things never get reviewed and merged as quickly as you'd like. With jj, it's pretty low-cost to have a bunch of PRs open at once, and you can do something like `jj new <pr1> <pr2> <pr3>` to build stuff that requires all 3. This lets me do things like... not do a big refactoring in the same PR as adding a feature. I can have them both self-contained, but still start on the next step before they're all merged. It's easy to add changes on top, switching between the individual PRs as comments come up, etc.
I always liked doing things like this. At Google where we used a custom fork of Perforce, I told myself "NEVER DO STACKED CLs HAVE YOU NOT LEARNED YOUR LESSON YET?" If one CL depended on another... don't do it. With git... I told myself the same thing, as I sat in endless interactive rebases and merge conflict commits ("git rebase abort" might have been my most-used command). With jj, it's not a problem. There are merge conflicts. You can resolve them with the peace of mind as a separate commit to track your resolution. `jj new -d 'resolve merge conflict` -A @` to add a new commit after the conflicted one. Hack on your resolution until you're happy. jj squash --into @-. Merge conflict resolved.
It is truly a beautiful model. Really a big mental health saver. It just makes it so easy to work with other people.
I'm having trouble understanding the value of this and most other supposed advantages of jj I'm seeing.
I'm trying to pinpoint if it's because 1) my workflow doesn't need jj's fancy stuff, 2) I've gotten so used to `git`'s "flaws" that I don't notice them, or 3) the git porcelain I use (magit) does a good enough job at managing the flaws.
If you need to build on something that requires changes from 3 open PRs, can't you just start a new branch from main, merge all 3 PRs into it, and get to work? As changes are applied to the open PRs, you can rebase. Obviously that might cause some merge conflicts here and there, but as long as the PRs aren't super overlapping, they should still be manageable. If there's a ton of overlap between 3 open PRs, that to me sounds like a problem in the workflow/plan, which must be dealt with regardless of the VCS or porcelain.
For me, it wasn't so much that jj enabled things I couldn't do before, though there are some things. What it enabled was me doing the things I was doing, but in a more easy way. This also leads you to do things that you can do, but sometimes avoid because it's a lot of work.
I guess that makes sense but also reinforces the confusion I have on whether jj is just another git "porcelain" (aka UI), or a replacement for git altogether.
If it aims to mainly improve the UX (do the same things you were doing before but easier), then it's irrelevant to those of us who have been lucky to find and learn sensible UXs.
If it aims to be a git replacement, I'm a little curious why the developers would decide to re-implement something from scratch only to end up with an "alternative" that is mostly compatible and doesn't radically change the internal model or add new features.
I last used GitHub Desktop years ago and had a terrible time. The git CLI is powerful but not very intuitive. It really wasn't until I learned magit that things "clicked" for me.
I know that many git UXs are pretty bad. But the way git works internally seems pretty great to me. Too often, criticism of git conflates the two.
When I mentor new devs, I explain to them how I use git. Sometimes I show them the workflow in magit, which makes it easier to visualize things. But mostly I just show them how their intended actions map onto the relevant CLI commands and I tell them to figure out how those map onto their porcelain of choice. I've developed this intuition thanks to magit, but I don't think magit is necessary. This approach seems preferable to me than onboarding new devs onto a new tool that is not the industry standard.
> This strikes me a lot like the C vs. safer programming language debate all over again.
I don't see how. Safer programming language address a clear problem in C, with trade-offs (sometimes arguably the trade-offs may not be worth it, and in my experience that's what the debate tends to be about). If jj is a replacement for git it should be clear what problem within git it aims at addressing. If the problem is in the UX, then to me and many others it's not worth the trouble.
Fundamentally, jj is its own VCS. It's just that it has pluggable backends. So when you use it with the git backend, it functions as a nicer git UI, but it's also not just that, because you can use it without git entirely. (though the major alternative backend is not open source, so while people do, unless you work at Google (or the startup I'm at...) you may personally not be able to.)
> then it's irrelevant to those of us who have been lucky to find and learn sensible UXs.
I was never someone who was upset at git's UX. I never found the 'hg is so much nicer' thing compelling. But then, I found that jj is just so much nicer to use, for me, that I haven't used git itself in years at this point. But it's also true that if you like using git, and want to keep using it, that's fine! The wonderful thing about the interop here is that I can use jj, and you can use git, and we're all good.
> I'm a little curious why the developers would decide to re-implement something from scratch only to end up with an "alternative" that is mostly compatible
Realistically, with git's dominance, compatibility is the only way that you get people to actually try out your thing. I know I wouldn't have given it a shot unless I could use it with a git repo.
> or add new features
I mean, there's two things here: one of which is, jj does have new features. I described the ability for a jj repo to exist in a conflicted state upthread, for example. jj undo is a godsend. But at the same time, at the end of the day, when you're trying to manipulate a graph of changes, there's always some way to end up in the same end state with git, because, well, you're trying to interoperate. So you can sort of handwave away a lot of features with a kind of "well I can do that in git via <totally different means>", and sure, that's true in a sense, but tools affect the way you work. I'm much more effective with jj's model of the world than I was with git's, even though I didn't actively feel that pain until I tried jj.
> It really wasn't until I learned magit
Ah, you use magit! So yeah, like, jj is like magit in the sense that it lets you interact with a git repository in a different way than the standard tool. And that's useful. I never would have used magit because I don't use emacs. (and there are some folks trying to do "magit but for jj"...)
> But the way git works internally seems pretty great to me. Too often, criticism of git conflates the two.
I agree, in general. I do think that there are still good criticisms to be made, but a lot of it is uninformed. Just how things go.
> Ah, you use magit! So yeah, like, jj is like magit in the sense that it lets you interact with a git repository in a different way than the standard tool. And that's useful. I never would have used magit because I don't use emacs.
I also use magit and I was confused by the "advantages" that jj has over git. The nice thing about magit is that it doesn't hide git. What it does add is easier typing of the flags (using transient), completions of arguments like branch names (using Emacs), DWIM behavior depending on cursor position and region selection (especially for commit hashes). Also it has nice presentation of the information which acts like hubs for all the above.
I guest jj makes sense if you're using the cli directly. But with magit, every operation is only a few keystrokes. It is to git, what vim is to editing. And I could probably cobble something close with tig or lazygit if I switched from emacs.
With agents, my go to is now have multiple of the same repository and each agents must work on a separate one.
Preventing dirty workspace by solving the co-work problem to start with. merges are much more trivial than trying to make agents remember which branch or which folder it is supposed to work on. Disk space is cheaper than mental anguish and token usage.
Many folks aren’t aware that there is also an open-source, cross-platform desktop GUI application for Jujutsu called GG: https://github.com/gulbanana/gg
I mention it because while the jj command line interface is excellent, there are certain tasks that I find easier to perform with a graphical user interface. For example, I often want to quickly tap on various revisions and see their diffs. GG makes that kind of repository browsing — and certain squash operations — much more efficient for me.
jj is genuinely great and I think it deserves way more adoption than it has right now. The mental model is so much cleaner than git, undo actually works the way you'd expect it to, and working with stacked changes feels natural instead of that constant low-grade anxiety of actually breaking something. It's probably the best frontend for version control that exists today.
For the last few months though I've been thinking a lot about what you said at the end there. What if version control actually understood the code it was tracking, not as lines of text but as the actual structures we write and think in, functions, classes, methods, the real building blocks? A rename happening on one branch and an unrelated function addition on another aren't a real conflict in any meaningful sense, they only look like one because every tool we have today treats source code as flat text files.
For enhancing this kind of structural intelligence I started working on https://github.com/ataraxy-labs/sem, which uses tree-sitter to parse code into semantic entities and operates at that level instead of lines. When you start thinking of code not as text there's another dimension where things can go, even a lot of logic at the comiler level with call graphs becomes useful.
Has anyone found a good code review workflow with `jj`? My problem is that GitHub doesn't remember the content of the last reviewed SHA, so every time I push a new change from jj, reviewers lose the delta from their last reviewed commit.
To work around this I stopped moving revs (squash/rebase) after review starts, which creates awkward local graphs if I have to merge from main for merge conflicts. Graphite works but it's $$$, and phabricator/reviewable/gerritt all have significant onboarding hurdles.
The last reviewed sha is generally available on the PR page (not the changes page) when you force push. There should be a changes since review link somewhere near the push.
When reviewing, you can also mark individual files as reviewed (useful in larger reviews where you're incrementally reviewing files). If you do this, only files that are changed will be expanded when you come back to the review.
yeah, this is where my complaint is - github shows a "compare" button when I force push, but it's not linked to the PR review. The "file changed" status is often not granular enough - if I change one line, force push, the entire file gets marked as unreviewed. the github "changes since your last review" is commit-based, not sha-based.
what I want is something like graphite/gerritt/google's critique where after each force push, the review page shows only the true delta between the two shas (similar to the "compare" button in github, bu as a reviewable unit).
poked around on github, doesn't look like the stacked PR feature has affected this "changes since your last review" selector yet :(
have a longer write up here: https://blog.tangled.org/stacking but we have "interdiffs", to view a delta from previous review. pull-requests advance in the form of immutable rounds much like the patch workflow on email.
I haven't noticed any significant change in my workflow needed to accommodate this, but it might be because I've always used rebase rather than merge. `jj rebase -d main` will put my current branch on top of the main branch, and fixing conflicts in `jj` is a breath of fresh air compared to git.
- All of everything good about it breaks down the instant you want to share work with the outside world. It's git on the backend! Except there isn't any concept of a remote jj so you have to go through the painful steps of manually naming commits, pushing, pulling, then manually advancing the current working point to match. And in doing so, you lose almost everything that gives it value in the first place - the elegant multi branch handling, anonymous commits, the evolog. Even if you want to work on the same project on two machines your only choice for this is without breaking everything via git is to rsync the folder. Yes, you can write alias to do all this like git. I might as well use git if I can't use the nice features.
- All files automatically committed is great until you accidentally put a secret in an unignored file in the repository folder. And then there is no way to ensure that it's purged (unlike in git) - the community response as far as I can tell is "Don't do this, never put a file that isn't in gitignore".
- And adding to .gitignore fails if you ever want to wind back in history - if you go back before a file was added to .gitignore, then whoops now it isn't ignored, is all part of your immutable history, and disappears if you ever switch off of that new commit state.
Anything new or special in jj that allows me to work with large binary files simply? To me, this is still unsolved in terms of providing an elegant solution (e.g. things like Git Large File Storage (Git LFS) are awkward).
I've heard that jj has support for non-git backends?
Can anyone comment on how difficult it would be to add support for another backend, any docs or examples?
I have a project[0] that does the large file thing well, but is missing most of the version control porcelain.
I've been looking for the path of least resistance to integrate it into something with a larger user base.
It looks like this treats files as blobs just like Git, and trees as single objects which fit in memory. Assuming that is a correct understanding, this core abstraction would need to change to handle large files and directories well.
All the well known version control systems do this though, and it simplifies the system significantly. It's the right model for source code, but it doesn't translate well to arbitrary data.
Yes, it will require work to do large files well. But there is general interest in upstream in having that, there's just nobody driving the work at the moment.
Seems interesting, but tbf, I only really use a small subset of git commands as it is... I've tended to practice a few workflows that just reduce the overhead and risk for conflicts.
You can also set snapshot.auto-track to tell it not to track certain files.
Another option is to make a branch with the files that you want to keep around but not push (e.g. stuff specific to your own tooling/editor/IDE), and mark that branch as private. Private commits (and their descendants) can't be pushed.
You then make a merge commit with this branch and main, make your changes, etc. You will have to rebase before pushing so that your branch isn't a descendant of the private commit.
This will involve more work, but it has the benefit that you're actually version controlling your other files.
I would like more uniformity in the way jjui handles commands when you are viewing changes vs when you are viewing files within a single change.
Often I just make my changes and leave it there without `new`, as I am not sure which file should go in a single commit. I just leave it there and I interactively commit later.
For me to use `new` more, I would like the ability to also simply be able to get a tree view of all changes, which contains file which contains file changes, so I could can have marks that span multiple changes and then either `split` or `commit` or `squash` the change, idk if there is a good word for it. Right now I can only mark within a single change, and I lose it once I navigate up.
I use jj fairly regularly and I'm trying to understand what your approach means, but having difficulty following what you want to acheive here. Seems like you're using ambiguous language that isn't aligned - wdym by marks?
I was hoping someone else had written about it here.
From my knowledge there are three different takes on git being worked on which looked interesting.
- JJ
- GitButler
- Zed
Zed version system doesn't have that much public info yet, but they wanted to build a db for storing code versions for AI agents. Not sure if this is still the direction, and I'm a bit skeptical, but interested to see what they come up with.
Even though git works well enough, I'm certain there will be another preferred way at some point in the future. There are aspects of git that are simply not intuitive, and the CLI itself is not up to standard of today's DX.
I'm still struggling most with the fact that my day-to-day work involves a git first platform like GitHub.
Although jj as a vcs system, it does feel better, working with git through it still feels like a chore, but to be fair I only gave it a day before going back to git.
Does anyone have any good resources on how to augment a git flow through the lens of a git hosting platform to work smoothly and still reap the benefits of jj?
Hmm, it was a while back so now I'm struggling to recall, but I remember feeling like I'm going against the grain of easily using GitHub. I followed this exact tutorial at the time and it looks like there are now sections on how to work with GitHub.
Perhaps I need to force myself to commit for longer...
The feeling I get with jj is it is almost like people are trying to convince others why jj is superior instead of just sharing that jj exists and let others decide themselves. It seems like every few months there is a jj post on HN which devolves into the narratives of “git works for me” and “but jj is better at X”.
I definitely am. Haven't touched git in over a year. If there was just a single feature to point at where jj meaningfully improves on git, I think it's: `jj undo`.
It is a universal undo command. It works for every change in your repository. You don't need to memorize/google/ask claude how to revert each individual kind of operation (commit, rebase, delete branch, etc.). You try a jj command, look at your repo, and if you don't like what you see, you `jj undo`.
The biggest downside for me is that no longer have the necessary expertise to help coworkers who get themselves into trouble with git.
Agreed and not only that but for any comment on this page which raises a valid exception; there is a cadre of jj devotees ready to counter them if and where they think they can. It feels like someone had a some financial state in brainwashing the world to leave git for jj.
I'm not, github sucks, git is ok. Stacked PRs are what jj makes easy but is completely broken in github and as of today they're releasing a 'stacked prs' product which only makes jj better value when working with github vs git.
Can jj iterate through a list of repositories and clone them all to local storage?
It isn't very hard to make a bash script to do it, but I have about six github repos, all of which frequently need to be put on a new machine. that kind of functionality would be cool to have out of the box.
Are you accessing these boxes via ssh or using them directly? If it's via ssh, I'd expect that you would already be using the clipboard for copying the names of them rather than typing them out manually, at which point copying `git clone <a> && git clone <b> && ...` would achieve the same thing.
So glad to see this on HN, here to support it. JJ is amazing, the hardest hurdle was not the tool but the toolchain and ecosystem when I started ~ 2 years ago. It's grown rapidly and is incredible to see the community grow!
Yes, but it's a sad state of affairs that the official jj docs point to your tutorial, which is incomplete (and IIRC, more incomplete than in the past - I think you took down some topics).
Nope, I haven't taken anything down. I have merged in some contributions from others though, it's actually grown (Manish, iirc, contributed the Gerrit section).
Oh, OK. Must be a bad memory. I often go to your tutorial looking for something I could have sworn I read over a year ago and not find it. I must have read it elsewhere.
1) Are there any benefits in replacing a personal private project git repo with jj? I usually just commit straight to master in order to just not to lose the changes and be able to roll back.
2) Are there any pros in doing so in a project with many large binary files (3d models, images)?
You can switch an existing git repo to jj by using:
jj git init --git-repo my-repo
I think (but CANNOT PROMISE) that just removing the .jj folder will bring you back, but definitely take a backup of .git before you try this in case I’m wrong.
No that is correct when in colocate mode (which is the default mode). Simply removing the .jj folder will "de-jj" the repo entirely, but will leave you in a headless state. Simple to fix with a `git switch` though.
If you are _not_ in colocate mode, the .git folder is located _inside_ the .jj folder. So worth checking!
For me, can do anything complex in terms of history rewriting without ever shooting myself in the foot (worse case just `jj undo`).
And the UI is pretty intuitive so don't really have to search for a solution, usually the command to use is obvious (tho I guess now you could delegate the UI to an LLM to ask it to give you the right incantations).
I think there are a lot of things that work well but still get improved versions. For example, grep works well but there are plenty of better versions these days.
Interesting to see more Rust CLI tools gaining traction. The undo-everything model is compelling. I've lost work to bad git rebases more times than I'd like to admit.
Thanks! I hope I didn't come off as too dismissive, I'm hearing a lot of good things about Jujutsu. As a developer though, I've never wanted to build from source (probably in the minority on that front).
Nah, you're right that installing a compiler toolchain to build a project is a pain in the butt if you don't already have it. It's a legitimate thing, but it does mean that you won't be adopting more cutting edge tools, which is also just fine! I've done the same with projects built with tools I don't have installed too.
I was going to write a big long comment, but honestly it boils down to this:
Whatever git's practical benefits over SVN and CVS back in the day (and I can go into the weeds as a user if someone wants that), git was the DVCS that took over from the centralized VCS's of that era.
There is nothing in jj, pijul, or Bram Cohen's thing that is anywhere near as dramatic a quality of life improvement as going from VCS to any DVCS. And dramatic improvement is what is needed to unseat git as the standard DVCS.
I mean, if you're not doing something so important[1] that it adds a letter to the acronym, it's probably not the next new thing in version control.
1: I originally wrote the word "novel" here. But it has to be big-- something like guaranteeing supply chain integrity. (No clue if a DVCS can even do that, but that's the level of capability that's needed for people to even consider switching from git to something else.)
Not necessarily. I used jj for a couple of weeks and found it to be a complete waste of time.
For an advanced user, it did not offer anything I cannot quickly solve in git. Which is probably the wrong thing to optimize in the first place, because even though I frequently rewrite history and split commits during normal worklfow, it takes so little time that improving something else would yield greater returns.
We (not royal we) don't usually go out of our way repeating negative experiences with these tools, so you build a very skewed view of their adoption.
Nothing will break. You just keep using the git backend if you want to keep being compatible with git.
> it’s tough to imagine why it’s worth pursuing a native and presumably incompatible backend.
Well, there's no active work on a "native" backend. There are basically three backends right now:
1. the git backend
2. A simple backend used for tests, you can think of it almost like a mock backend, you wouldn't use it for real work, but it's still useful as part of the test suite
3. the piper backend at google
There's not a lot of reason for anyone to produce another open source "native" backend, because 99% of open source projects use git.
its almost impossible for me to tell if this better or worst than git
i read few things about jj, and my conclusion
1. its different
2. very few user would really care about this difference
i think git is good (not good enough, good just good, or really good)
and unlike shells, i cant think of a reason to have mass migration to it
people use zsh because apple choose it, and pwsh because microsoft settled on it,
on linux i am sure we can do better than bash, but it good enough and nothing justified replacing it (that being said, all 3 OSes should have settled non nushell)
in summary, if we couldnt replace bash on linux, i dont think anyone can replace git, git as an scm tool if far better than bash as a shell
I may be reading too deeply but it sounds like you haven't even tried it. You should! Its really hard to live without it, once you feel it in your fingers.
> very few user would really care about this difference
Oh the user absolutely does if that user creates lots of branches and the branches are stacked on top of each other.
I get your feeling though; sometimes in my own private repositories I don’t bother creating branches at all. Then in this case jj doesn’t really make much of a difference.
Reading threads like this and the GitHub stacked PRs just makes me feel like an alien. Am I the only one that thinks that commits are a pointless unit of change?
To me - the PR is the product of output I care about. The discussion in the review is infinitely more important than a description of a single change in a whole series of changes. At no point are we going to ship a partial piece of my work - we’re going to ship the result of the PR once accepted.
I just squash merge everything now. When I do git archeology - I get a nice link to the PR and I can see the entire set of changes it introduced with the full context. A commit - at best - lets me undo some change while I’m actively developing. But even then it’s often easier to just change the code back and commit that.
You're not an alien: this is the workflow that GitHub encourages.
It's just that not every tool is GitHub. Other systems, like Gerrit, don't use the PR as the unit of change: they use the commit itself. And you do regularly ship individual commits. Instead of squashing at the end, you squash during development.
Thanks for explaining that. Having a bit of a (dim) lightbulb moment now. I’ve never used Gerrit - just GitHub and GitLab and Forgejo. So I assumed the PR/MR model was more or less universal. But if smaller development commits are being squashed into the shippable/reviewable unit - then the focus on commits makes a lot more sense.
You're welcome! It is one of those "do fish realize they're wet" kind of things, tools can shape our perception so strongly that you don't even realize that they're doing it!
You could agree that the PR is the meaningful unit for shipping, but push back gently that for agents working in parallel, the commit/changeset level matters more than it used to because agents don't coordinate the way humans do. Multiple agents touching the same repo need finer-grained units of change than "the whole PR."
Could you elaborate a bit more on this? Curious what your workflow looks like. Is this multiple agents running on the same feature/refactor/whatever unit of work? For concurrent but divergent work I just use a git worktree per feature. And I think I only ever have a single agent (with whatever subagents it spins up) per unit of work.
Is there a concise introduction / overview of jj? I've read 8 pages of this link and the author is still in preambles and considerations... Not my favourite style of article!
You have a DAG of "changes" to the repo state. Each change has a stable ID, you can modify its contents or description without changing the ID. There's always a "current" change checked out, JJ automatically snapshots your edits to files into this change. JJ provides tools to edit changes, describe them, group them into named branches (bookmarks), reorder them, split them apart, etc.
JJ defaults to being backed by git. Each change has a corresponding git commit. When you edit the contents of a change, jj makes a new git commit & points the change at that new git commit. JJ is never actually amending or editing git commits, it's using git as a content-addressed data store.
That's the mental model. It's like git with a lot of accidental complexity (staging area, stashes, commit ID instability) removed.
There are a few ways you can work with this model. I like the following:
When I want to start new work, I fetch any changes from upstream `jj git fetch`, then make a new change after the current `main` or `master` bookmark: `jj new main`. Then I start writing code. When I want to commit it, I type `jj commit` and write a description. If I find I want to make an edit to a previous change, I edit my working copy and interactively squash to that change ID with `jj squash -i -r <change_id>`. When I'm ready to push those changes, I name the branch HEAD with `jj bookmark create`, then push it with `jj git push -b <bookmark_name>`. If there are review comments I squash edits into the appropriate changes or add new changes, and move the bookmark to the new head with `jj bookmark move`. If I want to merge two (or more) branches, I use `jj new <branch_1_name> <branch_2_name> <...>` to make a new commit with those branch names as parents. If I want to rebase some changes I use `jj rebase`. JJ doesn't care about conflicts, I fix them after a rebase has completed, instead of in the middle.
You've got some decent replies, but if you give me some background, like how comfortable you are with git, how much you care about certain details, I'd be happy to respond here with something more concise.
I tried jj for a few months. It was fun to learn a new thing, but I haven't had a single case of "wow, this would have been a pain with git". Then I went back to git (it's been 6 months now) and I haven't had a single case of "this is so painful, I wish something better existed".
So it felt like the XKCD on "standards": I now have one versioning system, if I learn jj I will have two. What for?
Don't get me wrong: it's nice that jj exists and some people seem to love it. But I don't see a need for myself. Just like people seem to love Meson, but the consequence for me is that instead of dealing with CMake and Autotools, I now have to deal with CMake, Autotools and Meson.
EDIT: no need to downvote me: I tried jj and it is nice. I am just saying that from my point of view, it is not worth switching for me. I am not saying that you should not switch, though you probably should not try to force me to switch, that's all.
> Then I went back to git (it's been 6 months now) and I haven't had a single case of "this is so painful, I wish something better existed".
The core issues are: how long did it take you to get there, how many lucky decisions did you have to make to not run into git footguns, and how many other people accidentally made different choices and so have very different experiences from you?
What you're saying is that other people may find jj easier for them, right?
I am fine with that. I am just saying that the "you should use jj, you will finally stop shooting yourself in the foot regularly" doesn't work so well for me, because I don't remember shooting myself in the foot with git.
JJ might be good (this article couldn't convey why in the "What is jj and why should I care?" page) but it's not 10x better than git, so it will likely die. Sorry, nothing personal, Mercurial/hg was a little bit better than git and died too. Network effects.
What has a change is ast-based version control.
You adding a feature to a function that uses a struct I renamed shouldn't be a conflict. Those actions don't confliuct with each other, unless you treat code as text - rather than a representation of the logic.
Ending merge conflicts might make a new version control 10x better than git, and therefore actually replace it.
> JJ might be good (this article couldn't convey why in the "What is jj and why should I care?" page) but it's not 10x better than git, so it will likely die. Sorry, nothing personal, Mercurial/hg was a little bit better than git and died too. Network effects.
The difference is that I can (and do) use `jj` with existing git repos today without needing anyone else using the repo to change what they're doing. There's no need to replace something when it can exist alongside it indefinitely.
I use the CLI often enough, but still most of my time is in a GUI. It just makes the diffs easier, the flow simpler, etc.
As such, I wanted to break into jj via the GUI, and only adopt the command line after I could visualize the concepts and differences in my head.
Alas, the GUI I tried - a VSCode plugin - did more to confuse me than to help, and it made it very easy to catastrophically rewrite the history by accident. I tried another UI and it kept failing and leaving me with cleanup work. I couldn't find a third UI that looked promising.
So, I gave up. One less jj user on the planet - no biggie. But I really wonder if it would be worth the effort for some of the jj pushers to try to get a proper UI in place - I bet I am not the only one that likes to learn visually.
I followed steve's excellent tutorial about two months ago, and haven't looked back. I have never felt so competent at git (vcs rather) as I do now. jj is so much simpler and easier for me to reason about.
We still have some repos in Subversion and most things in git. It’s still exciting for every repo we get migrated out of svn. That’s a high bar to cross if we’re talking further improvements compared to git though.
>You can request to not use the pager by using jj st --no-pager, or if you hate the pager and want to turn it off, you can configure that with
$ jj config set --user ui.paginate never
In one feature they can’t help themselves from calling it two different things already.
Why do this? Why can’t the very clearly smart people making things step 1/2 step outside themselves and think about it like they are the users they want?
Earlier they talk about the native format and how it isn’t ready… so that to start you need
jj git init
… but… if they’re planning a native format that makes no sense as a command. It would be ‘jj native init’ later?
Early planning keys/plans imo but those rarely change so as to not accept your early adopters.
These seem like small things but to me it’s a warning.
1. No one with good vision would give a single feature two names. It’s dumb. Here is our pager feature. Cool, how do I access it? Oh you set the ui.paginate options of course!!
2. It’s almost like we have some established ways to denote arguments that are pretty popular… ‘jj init —-git’ for example? By using ‘jj git init’ I would expect all of the git compatible commands to be be ‘jj git xxx’ because that is a reasonable expectation.
This is a problem with the voodoo. These obscure nonsense commands only makes sense when you are accustomed to them. If there’s no reasonable expectation that you could just figure it out on your own. Go on vacation and come back and be surprised when you forget the voodoo. Not to mention that every tool has to have its own unique voodoo.
Almost like the professional world has figured out that made by software engineers for software engineers will never be popular. And then engineers don’t understand the effects of why you might want tool to be intuitive and popular.
You're right that, looking solely at `init`, a flag could make sense to choose the backend.
The bigger picture here though: `jj git` is the subcommand that prefixes all commands that are git specific, rather than being backend agnostic. There is also `jj git clone`, `jj git fetch`, `jj git push`, etc.
For a different backend, say Google's piper backend, there's `jj piper <whatever>`.
This means that backend specific features aren't polluting the interface of more general features.
The on-disk repository compatibility is automatic. But if you're trying to fetch something via a specific protocol, you use the command for the protocol you want to use.
There is no extra step between `git push` and `jj git push`, they're both one step.
I meant the extra step being why would I bother with jj if I’m having to specific gut inside of jj?
The issue is pretty obvious to me. GIT is the standard and that likely won’t change for some time. So if jj makes my git life better, awesome, but it’s just a wrapper and I need to know all the git voodoo now with jj voodoo on top, I don’t quite get it.
I am quite happy for anyone to use whatever tools they find to be good. I'm also happy for anyone to use jj with whatever server they want to.
It is right to be skeptical of me, but I hope to keep that integrity by continuing to talk about things that I believe are legitimately good, regardless of anything else.
Thanks for the reply Steve - I think it's only natural, and charitably unintentional :) But almost everything I've seen upvoted around the Internet from you has been about jj and being directly tied to east river source control... I think that's a reasonable framing. I can only hope me signalling this maybe changes something. While I'm not a fan of jj (I'd much rather Pijul were to eat the world), I think you as a person is really nice and always have been "for the community", but I can't shake this current framing!
You can't say it's adjacent to it, when your job directly involves the technology. You'd stop posting because anything you say about jj could be interpreted as a sales pitch for jj, and a lot of people can be turned off by that. That's one reason. Our lack of creativity is not proof of no more.
But I don't think "stop writing" is the only strategy to jump on...
Or maybe I'm just extremely unlucky to have only caught these kinds of posts and gained this framing! Totally possible.
That's sort of where I've been stuck with with jj.
Maybe someone can convince me otherwise, but to me it hasn't felt sufficiently better than git to justify bothering re-learning this stuff, even if it's relatively easy.
Git wins by not needing to be replaced badly enough. Latley you can just ask an agent to "amend the last commit" so even that is being abstracted away.
We all need to give ourselves a push and finally make the next step in version control. Github, Google, Microsoft, Meta (did I forget anyone relevant? Probably) should just join forces and finally make it happen, which should not be a problem with a new system that is backend compatible to Git. Sure, Github may lose some appeal to their brand name, but hey, this is actually for making the world a better place.
It is not about starting over, like moving from CVS or Subversion to Git. jj is backend compatible to Git, so nothing really had to change on the backend.
It's just that although Git was created by Linus Torvalds it is not perfect and could be more beginner friendly. But efforts to improve this should be concerted, not individual efforts.
And it does not have to be jj. I just think there is room for improvement, and not to alienate old farts it could be called GitNext, GitStep, GitFlow or similar to emphasize that is still is just Git, only with an improved front end.
Maybe Linus Torvalds himself should start the initiative.
Linus? Too tired of the open source community to risk having to deal with it more. Hasn't released anything since 2005, he just drifts on the waves. So sad he doesn't see the human energy wasted on his projects, and doesn't move them into the modern era, where compatibility with the past can be dropped in favor of a much tighter feature set, while also coming free of C/C++. In short: don't count on Linus, he's been a coward, he's too comfortable leading from the back.
Every time I see a statement like this I wonder what specific features of git that people feel like are terrible enough that it’s time to completely start over. Besides “the UX is kinda shit and it’s confusing to learn”, which there are many solutions for already that don’t involve reinventing a pretty good wheel.
How we got git was cvs was totally terrible[1], so Linus refused to use it. Larry McEvoy persuaded Linus to use Bitkeeper for the Linux kernel development effort. After trying Bitkeeper for a while, Linus did the thing of writing v0 of git in a weekend in a response to what he saw as the shortcomings of Bitkeeper for his workflow.[2]
But the point is there had already been vcs that saw wide adoption, serious attempts to address shortcomings in those (perforce and bitkeeper in particular) and then git was created to address specific shortcomings in those systems.
It wasn't born out of just a general "I wish there was something easier than rebase" whine or a desire to create the next thing. I haven't seen anything that comes close to being compelling in that respect. jj comes into that bucket for me. It looks "fine". Like if I was forced to use it I wouldn't complain. It doesn't look materially better than git in any way whatsoever though, and articles like this which say "it has no index" make me respond with "Like ok whatever bro". It really makes no practical difference to me whether the VCS has an index.
[1] I speak as someone who maintained a CVS repo with nearly 700 active developers and >20mm lines of code. When someone made a mistake and you had to go in and edit the repo files in binary format it was genuinely terrifying.
[2] In a cave. From a box of scraps. You get the idea.
To be fair the "shortcomings" that spurred it on mainly were the Samba guys (or just one) reverse-engineering Bitkeeper causing the kernel free license getting pulled, which caused Linus to say "I can build my own with blackjack and pre-commit hooks" and then he did, addressing it toward his exact use case.
It gained tons of popularity mainly because of Linus being behind it; similar projects already existed when it was released.
When I tried both at that time hg was just really slow so I just adopted git for all my personal projects because it was fast and a lot better than cvs. I imagine others were the same.
Git is basically fine even though the verbs are backwards - e.g. you shouldn't need to name branches, commits should be far more automatic, but the basic mechanisms are fine.
Coming from mercurial (which is older than git), git doesn't understand a branch. Instead of a branch you get a tag that moves, which is very different. Too often I'm trying to figure out where something came in, and but there is just a series of commits with no information of which commits are related. Git then developed the squash+rebase workflow which softof gets around this, but it makes commit longer (bad), and loses the real history of what happened.
Git was not the first DVCS, there were better ones even when it was made. But Linus pushed git and people followed like sheep.
(I'm using git, both because everyone else is, and also because github exists - turns out nobody even wants a DVCS, they want a central version control system with the warts of SVN fixed).
Git is older than mercurial by 12 days. Bazaar has git beat by about the same amount of time. The major DVCSes all came out within a month of each other.
> But Linus pushed git and people followed like sheep.
I don't think this is true. Until around 2010-2011 or so, projects moving to DVCS seemed to pick up not git but mercurial. The main impetus I think was not Linux choosing git but the collapse of alternate code hosting places other than GitHub, which essentially forced git.
the lack of a proper branch history is also the main pain point for me. but i disagree that noone wants a DCVS. having a full copy of the history locally, and being able to clone from any repo to anywhere else and even merge repos (without merging branches) is a major win for me.
If git would change two defaults, that would make me really happy:
1. git merge ONLY does merges (no fast forward/rebase). git pull ONLY does a fast forward
2. git log by default is git log --first-parent. Just show commits where the parent is the current branch. This makes the merge workflow really easy to understand an linear, because in the end, you only care about commits on the trunk.
I've gone all in on jj with a OSS framework I'm building. With just a little extra context, the agents have been amazingly adapt at slicing and dicing with jj. Gives them a place to play without stomping on normal git processes.
The cli and a few concepts have evolved with time past the model's knowledge cutoff dates, so you have to steer things a bit with skills and telling it to use --help a bit more regularly.
I find it reasonably good with lots of tweaking over time. (With any agent - ask it to do a retrospective on the tool use and find ways to avoid pain points when you hit problems and add that to your skill/local agents.md).
I expect git has a lot more historical information about how to fix random problems with source control errors. JJ is better at the actual tasks, but the models don't have as much in their training data.
It is definitely worth a try. Just being able to squash changes to earlier commits without having to fiddle with fixups and interactive rebases is worth it for me. jj absorb is great too.
Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.
I also often end up with in a dirty repo state with multiple changes belonging to separate features or abstractions. I usually just pick the changes I want to group into a commit and clean up the state.
Since it's git compatible, it feels like it must work to add files and keep files uncommitted, but just by reading this tutorial I'm unsure.
It doesn't need you to think that way at all.
`jj new` simply means "create a new commit [ontop of <location>]" - you don't have to describe it immediately. I never do.
I know that the intention was to do that, and I tried forcing the habit, but I too found it counter-productive to invariably end up re-writing the description.
I don't usually do that right away, but I often use squash or absorb to move additional changes into a commit I already made in my stack. I think the spirit still applies if you take that course.
My preferred workflow is to start with a new change, pick the changes I want, then use jj commit to describe the change and create a new empty one on top. Feels very similar to my old git workflow.
If I end up with multiple features or abstractions in one change (equivalent to the “dirty repo”), jj split works very well as an alternative to the git add/git commit/repeat workflow tidying up one’s working copy.
I also like `jj commit [paths]` to commit just a subset of files when I don't need hunk based splitting.
Like `jj commit -m 'Feature A' file1 file2` then `jj commit -m 'Feature B' file3 file 4`
I use jj commit -i a lot when writing the paths is too tedious. What's nice is you can pass -i into most commands (squash, split, absorb, etc).
> Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.
A good way to think of it is that jj new is an empty git staging area. There's still a `jj commit` command that allows you to desc then jj new.
> I also often end up with in a dirty repo state with multiple changes belonging to separate features or abstractions. I usually just pick the changes I want to group into a commit and clean up the state.
jj split allows you do to this pretty well.
> Since it's git compatible, it feels like it must work to add files and keep files uncommitted, but just by reading this tutorial I'm unsure.
In jj you always have a commit - it's just sometimes empty, sometimes full, has a stable changeid regardless. jj treats the commit as a calculated value based on the contents of your folder etc, rather than the unit of change.
I often will use `jj new -B@` (which I made an alias for) followed by `jj squash -i` to split changes. I had no idea about `jj split`, so I need look into that!
I'm using jj exactly this way, but `jj commit -i` is still somewhat backwards compared to `git commit -i`: jj displays the commit timestamp by default instead of the author timestamp like git. In addition, in jj the author timestamp of a commit is set to the time you started and not ended a commit/change. This results in unexpected timestamps when working with git-using people or tools. Also, it's rather weird if you use a previously empty commit for your work which was created months earlier by a previous `jj commit`, resulting in a timestamp neither correlating to when you started nor ended your work.
I guess the idea of jj's authors is that jj's commits are far more squishy and can always be changed, so a fixed finished timestamp makes less sense. I still prefer git's behaviour, marking work as finished and then keep the author (but not commit) timestamps on amends.
I use this jj alias to get git's timestamp behaviour:
> A good way to think of it is that jj new is an empty git staging area. There's still a `jj commit` command that allows you to desc then jj new.
This always made me feel uncomfy using `jj`. Something that I didn't realise for a while is that `jj` automatically cleans up/garbage collects empty commits. I don't write as much code as I used to, but I still have to interact with, debug and test our product a _lot_ in order to support other engineers, so my workflow was effectively:
``` git checkout master git fetch git rebase # can be just git pull but I've always preferred doing this independently _work_/investigate git checkout HEAD ./the-project # cleanup the things I changed while investigating ```
Running `jj new master@origin` felt odd because I was creating a commit, but... when I realised that those commits don't last, things felt better. When I then realised that if I made a change or two while investigating, that these were basically stashed for free, it actually improved my workflow. I don't often have to go back to them, but knowing that they're there has been nice!
> Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.
Yes, but this is not backwards, the way you do it in git is backwards. =)
git promises "version control", this clearly implies that the versions predate the control: in this picture the git workflow is not backwards.
I don't think the term "version control" has any implication about precedence, and I don't understand what you mean by "the versions predate the control". In git, you add items to the worktree (control), then you commit (create a version), so doesn't that mean git does it "wrong" according to what you're saying? In jj, you are always on a committed version and the contents of that commit are controlled by your edits, if you want your edits to be on a different commit, you usually just change to that commit and make the edits, although there are other ways to move edits around (which is also true in git).
The point is that there actually isn't a correct order to do these operations, just one that you're familiar with. Other orders of operations are valid, and may be superior for your or your team's workflow.
Nothing stops you from making changes in a commit that has no description and then at the end doing `jj commit -m` to describe them and make a new commit in one go, which is essentially the same as git. The difference is that it's essentially amending in place as you make changes rather than needing to stage first.
Personally haven’t used jj but as far as dvcs’s are concerned Fossil is great complement to Git because it does things differently than git and truly has a decentralized feel.
The autosync feature is really nice too, and you can store backup repos in cloud storage folders and auto sync to those as well.
Fossil is delightful and definitely nails a feeling of decentralization that I think we ruined completely with `git` by constantly centering around centralized repositories.
I also find it interesting that so many people want to switch to something that's not `git` but are simultaneously somehow super invested in it being basically just `git`.
Most teams could switch to Fossil and just have a better time overall. It's made for smaller, high-trust teams, `git` is not. Fossil also manages to actually support external contributions just fine; it's just that it's not the default.
That totally works and it's how I use jj. jj commit -i does what you would want
jj is very flexible when it comes to workflow. One thing to note is that commits don't have to have messages. What I tend to do is to run `jj new` frequently while I work on something and leave all of them without messages. Then when I'm ready to make my actual commit, I squash the temporary commits together and then add a message. If my changes are separable, I can split the commits before squashing. This workflow acts as a kind of undo history. I can easily go back to what I had 5 minutes ago and try a different approach, but then also jump back to my original changes if I want. It makes experimentation much easier compared to git.
think of jj like,
I want to build xyz,
```
jj desc -m "feat: x y & z"
```
do the work.
```
jj split
```
Split up the parts and files that you want to be separate and name them.
This will also allow you to rename stuff.
```
jj bookmark create worklabel-1 -r rev1
jj bookmark create worklabel-2 -r rev2
# Push both commits
# since we just split them they are likely not inter-dependent
# so you can rebase them both to base
# assuming rev1 is already on top of base
jj rebase -s rev2 -d base
jj git push
```
That is it.
I am dumb. why is that better than a git branch or a git worktree ?
If you're already super comfortable in git, it's not. I'm saying this as someone who recently converted from git to jj and never wants to go back.
You also don't have to follow what the GP said. I never say `jj describe` before writing code. I write the code then just say `jj commit -m "Foo stuff"`, just like I would in git.
The bigger difference I've noticed is:
1. Switching between changesets just feels more natural than git ever did. If I just run `jj` it shows me my tree of commits (think of it like showing you git's branches + their commits), and if I want to edit the code in one of them I just say `jj edit xyz`, or if I want to create a new commit on top of another one and branch it off in a new direction, I just say `jj new xyz`. It took a little bit for my brain to "get" jj and how it works because I was so used to git's branches, but I'm really enjoying the mental model.
2. `jj undo`. This alone is enough to convert me. I screwed something up when trying to sync something and had a bunch of conflicts I really didn't want to resolve and I knew could have been avoided if I did things differently, but my screwup was several operations ago! So I ran `jj undo`. And ran it again. And again. And again. And then I was back to my clean state several stages ago before I screwed up, despite having made several changes and operations since then. With git? Yeah I could have gotten it fixed and gone back. But every time I've had to do something like that in git, I'm only 25% confident I'm doing it right and I'm not screwing things up further.
3. Rebasing. When I would try to sync git to an upstream GitHub repo that used rebasing for PRs, I would always get merge conflicts. This was because I stack my changes on top of each other, but only merge in one at a time. Resyncing means my PR got a new commit hash, even though none of the code changed, and now git couldn't figure out how to merge this new unknown commit with my tree, even though it was the same commit I had locally, just a different hash. With jj? I never get merge conflicts anymore from that.
Overall the developer experience is just more enjoyable for me. I can't say jj's flow is fundamentally and objectively better than git's flow with branches, but personally and subjectively, I like it better.
It's not, you can literally do everything this tool does with Git, and 80% of the features could be replaced with commands in your shell rc file also using vanilla git.
This tool was described perfectly the other day. JJ is the Dvorak of Git. Most people could careless about Dvorak layout, 99.8% of people use qwerty just fine. Those 0.02% though, they're loud, and they want everyone to know how great the reinvention of bread is.
it's actually git that makes you think backwards - in jj the working tree is a commit, in git it isn't until you at least stage it.
the working tree being a commit has wide ranging implications as all the commands that work with commits start working with the working tree by default.
This is me! I often find that in the process of making one change, I have also made several other changes, and only recognize that they are distinct after following the ideas to their natural conclusion.
Hence I have multiple workspaces, and I shelve changes a lot (IntelliJ. I end up with dirty repos too and that can be painful to cherry-pick from. Sometimes I just create a git patch so I can squirrel the diffs into a tmp file while I cleanup the commit candidate. I often let changes sit for several days while I work on something else so that I can come back and decide if it’s actually right.
It’s chaotic and I hide all this from coworkers in a bid to seem just a bit more professional.
I admire people who are very deliberate and plan ahead. But I need to get the code under my fingers before I have conviction about it.
> I often find that in the process of making one change, I have also made several other changes, and only recognize that they are distinct after following the ideas to their natural conclusion.
I do that all the time. With git, everything starts "unstaged", so I'd use magit to selectively stage some parts and turn those into a sequence of commits, one on top of another.
With jj I'd do it "backwards": everything starts off committed (with no commit message), so I'd open the diff (`D` in majutsu), selecting some parts and "split" (`S` in majutsu) to put those into a new commit underneath the remaining changes. Once the different changes are split into separate commits, I'd give each a relevant commit message.
I'm about the same. jj is kind of perfect for that. Example:
# I've finished something significant! Carve it out from the working "change" as its own commit.
# Oops, missed a piece.
# Let me look at what's left.
# Oh right, I had started working on something else. I could just leave it in the working change, but let me separate it out into its own commit even though it's unfinished, since I can always add pieces to it later.
# Wait, no, I kind of want it to come before that thing I finished up. Shoot, I messed up.
# Let me try that again, this time putting it underneath.
# Note that instead of undoing and re-selecting the parts, you could also `jj rebase -r @- -B @--` to reorder. And in practice, you'll often be doing `jj log` to see what things are and using their change ids instead of things like `@--`.
# I also have some logging code I don't need anymore. Let me discard it.
# Do some more work. I have some additions to that part I thought was done.
# And some additions to that other part.
# etc.
There's a lot more that you could do, but once you internalize the ideas that (1) everything is a commit, and (2) commits (and changes) can have multiple parents thus form a DAG, then almost everything else you want to do becomes an obvious application of a small handful of core commands.
Note: to figure out how to use the built-in diff viewer, you'll need to hover over the menu with the mouse, but you really just need f for fold/unfold and j/k for movement, then space for toggle.
> It wants me to start with the new and describe command
jj doesn't "want" anything.
I always end a piece of work with `new`: it puts an empty, description-less commit as the checked-out HEAD, and is my way of saying "I'm finished with those changes (for now); any subsequent changes to this directory should go in this (currently empty) commit"
The last thing I do to a commit, once all of its contents have settled into something reasonable, is describe it.
In fact, I mostly use `commit` (pressing `C` in majutsu), which combines those two things: it gives the current commit a description, and creates a new empty commit on top.
Think of it this way: the current change is like a staging area/index in git. Leave it without a a description while you're working (just like git's staging area). Rely on jj's auto-snapshotting to capture all your changes. Then, when you're ready to do something else, give it a description ("jj describe") and switch to a new blank change ("jj new"), and that becomes your new "staging area"/index.
The workflows are conceptually identical.
Not necessarily, I often make changes on unrelated commits. You can always use jj split to extract the change and put it somewhere else.
I'm giving jj a try but one aspect of it I dislike is edits to files are automatically committed, so you need to defensively create empty new commits for your changes. As in, want to browse the repo from a commit 2 weeks ago? Well if you just checkout that commit and then edit a file, you've automatically changed that commit in your repo and rebased everything after it on top of your new changes. So instead you create a new branch off of the old commit and add an empty commit to that branch so any file changes don't end up rewriting the past 2 weeks of history. git is much nicer in that I can do whatever I want to the files and it won't change the repo until _I tell it to_.
Just don't ever use `edit`, use `new` instead; then your changes are tracked without making a mess. I think that's much nicer than juggling stashes in git.
... unless you actually want to edit a change!
well, you can do jj new <revision>, make your edit, and then do jj squash which will add the changes to the prev revision
i do this for example when i want to see a specific edit highlighted in my editor, it's a nice workflow i think
This is exactly how someone explained Git to me 12 years ago or so, and I’ve finally wrapped my head around it. Not changing now.
If I'm understanding the thread correctly, I have a git alias to `git commit --amend --no-edit`, for exactly this workflow. When I'm hacking on something locally and want to just keep amending a commit. I only ever do this if it's HEAD though.
Yes, one way to think about jj in a sort of low-level way is that every jj command does the equivalent of that, every time.
(You can also set up watchman and have that happen on every file change...)
still use new, and then squash your changes in. that way you can actually see what changes you made
then you `new` & `squash` :)
I go back and forth between the two approaches, but because of the whole "accidentally made some temporary changes and now it's a pain to separate/undo them because not all changes were temporary", I also usually do a jj new and then jj squash.
> Just don't ever use `edit`, use `new` instead
As a git-ist (?), if I'd ever move away from git, it would be to avoid tooling that has idioms like this (like git too has), if `jj` just gonna surface a bunch of new "bad ideas" (together with what seems like really good ideas), kind of makes it feel like it isn't worth picking up unless you don't already know git.
jj edit has good use cases, but it's not the default command you need. For instance, say you were working on some changes but had to change branches for a few minutes to do something. If you didn't manage to create a commit and want to go back to the previous staging area, you would use the jj edit command rather than jj new. It's very intuitive in my experience, something I can't say is true for managing git commits (unless you've spent years forcing it into muscle memory). I never need to run jj help. I run help commands with git all the time.
`edit` is still useful; just, for ..editing (!) something, instead of viewing it.
If you have some unfinished changes at the tip and want to temporarily checkout something 2 weeks ago, you `jj new` to there (similar to `git stash; git switch whatever`), and then later `jj edit your-old-tip` to go back (equivalent to `git switch main; git stash pop`; I think `jj edit` being an extended replacement for stash-popping things is a reasonable way to think about it). (and if you don't have any uncommitted changes, you always `jj new`)
jj also has a concept of immutable commits (defaulting to include tagged commits, and trunk at origin, which it'll disallow editing as a layer of defense)
The idiom here is use `edit` if you want to edit a commit, and use `new` if you want to make a new commit. This works identically whether you specify the commit via branch name or commit id. I'm not sure why people are saying not to use `edit` ever. It's basically just a shorthand for staging and amending changes in an existing commit, and there's still a use case for that; it's just not "I want to see the changes on this old branch".
> Just don't ever use `edit`,
> The idiom here is use `edit` if you want to edit a commit
You know, you guys have fun with that, I'll continue using git which (probably) has the same amount of warts, but I already know them. I'll continue to refer new VCS users to jj, seems a lot easier to learn, but really don't have the interest to re-learn a bunch of ever-changing idioms.
I disagree with the people saying "never use edit". There are plenty of people saying conflicting things about git too, and I'd argue that understanding edit versus new isn't anywhere close to the level of wart that having to get people to agree on merging versus rebasing. Like you said though, have fun with that!
No system is perfect, but there's nothing wrong with `jj edit` and `jj new`. Both commands are completely reasonable and do what you think they would do.
I think it's because it's easy to make annoying mistakes (still easy to fix with undo) with edit. And it gains relatively little over new+squash. Edit is a useful power-feature, but I think for a novice, "never use it, only use the more well understood workflow of new+squash" is a good heuristic.
Wow, that’s a total deal breaker to me. Using git may require a complex mental model, but at least it’s not doing anything I didn’t ask for.
You would have had to run `jj edit` in order for this to happen, so I think it's a stretch to say you didn't ask for the edit?
This is the main difference though: in git files can be `staged`, `unstaged` or `committed`, so at any one time there are 3 entire snapshots of the repo "active".
In `jj` there is only one kind of snapshot (a change) and only one is "active" (the current working directory). When you make changes to the working directory you are modifying that "change".
As others have mentioned, the equivalent to `git checkout` would be `jj new`, which ensures a new empty change exists above the one you are checking out, so that any changes you make go into that new change rather than affecting the existing one.
Thanks for the explanation! I wish I could edit my comment to reflect the truth.
Using `jj edit` will edit a commit you specify, and `jj new` will make a new empty commit after the one you specify. These work exactly the same whether you specify a commit by branch or by the hash. I'd argue that you're getting exactly what you ask for with these commands, and by comparison, what "checkout" is asking for is much less obvious (and depends on context). We've just internalized the bad behavior of git for so long that it's become normalized.
`jj edit` is quite literally asking for that.
GP is holding it wrong. If you don’t want to edit a commit, don’t ask to edit it. Use `jj new`.
`jj new` works like `git checkout` most by creating an empty revision on the top. `jj edit` on the other hand resembles `git checkout; [edits...]; git add -A; git commit --amend --no-edit`.
You can disable the auto staging of new files since recently which removed the main grype for me
ooo that will be a nice improvement. So many times I've run `jj status`, then saw a file I wanted gitignored, so I'll edit my gitignore, but the file has already been added to the repo so I have to `mv <file> /tmp/ && jj status && mv /tmp/<file> .` to get the file out of the repo.
You can `jj file untrack` instead of that mv bit.
Oh neat, thanks! I (clearly) did not know that command.
jj edit is the biggest jj footgun I can think of, as other comments said just use jj new. But also if you do accidentally edit or change something jj undo works surprisingly well.
I found when using jj it worked best for me when I stopped thinking in commits (which jj treats as very cheap “snapshots” of your code) and instead focus on the “changes”. Felt weird for me at first, but I realized when I was rebasing with git that’s how I viewed the logical changes I made anyway, jj just makes it explicit.
jj auto-rebasing doesn’t matter until you push changes, and once you do it marks them immutable, preventing you from accidentally rebasing changes that have been shared.
> jj edit is the biggest jj footgun I can think of
Honestly, this is only because `git checkout` is so convoluted that we've collectively changed our expectations around the UX. "checkout" can mean switching to another branch (and creating it if you specify a flag but erroring if you don't), looking at a commit (in which case you have "detached HEAD" and can't actually make changes until you make a branch) or resetting a file to the current state of HEAD (and mercy on your soul if you happen to name a branch the same as one of your files). Instead of having potentially wildly different behavior based on the "type" of the thing you pass to it, `jj edit` only accepts one type: the commit you want to edit. A branch (or "bookmark", as jj seems to call it now) is another way of specifying the commit you want to edit, but it's still saying "edit the commit" and not "edit the bookmark". Unfortunately, the expectation for a lot of people seems to be that "edit" should have the same convoluted behavior as git, and I'm not sure how to bridge that gap without giving up part of what makes jj nice in the first place.
It's not "wildly" different behavior based on the thing it's pointing to. In all 3 cases, the command is pointed at a commit and the behavior is the same. Once you know that branches/HEAD are just named pointers to commits, then it becomes obvious you are always just working on commits and branches/ids/HEAD etc are just ways of referencing them.
> In all 3 cases, the command is pointed at a commit and the behavior is the same
What's the name of the branch this is pointed at? If I have to run another git command to find out, then it's not "pointed" at it.
If you don't provide it a <tree-ish> it reads from the index (staged files). So you're right its not really pointed anywhere, since the index isn't a ref.
That's my overall point: the argument itself (with respect to the current state of the repo) is what determines the behavior. I don't think this is anywhere close to as intuitive as commands that only ever accept one "type" of argument (and erroring if it's different).
I stand corrected by this one scenario, but I’ve been using git for over a decade and never found that useful. Just don’t use checkout on a file path, there is no need.
I find this kind of advice to be a more scathing indictment of an interface than a critic could ever muster: asking users to forego available functionality so that some sense of order can be imposed.
< glances around at all the people telling me to never use `jj edit` >
edit is useful and there are good reasons to use it, 'never use edit' is like 'never use goto' i.e. false - but if you're just starting out, jj new/jj squash is the way to go indeed.
(my particular favorite reasons to use jj edit are git-native tools which expect to work with uncommitted files e.g. autoformatters, linters, etc. which have been scripted in CI/dev workflows such that they cannot accept a list of files as params)
That goes in the same bucket as rebase. Until you know what it does, you'll be fine avoiding it.
Since people are sharing their experiences and my recent one is relevant to edit, I'll go:
Working on a feature recently, I ended up making 3 changes ("commits") on top of each other and hopping between them via jj edit.
The first change wasn't feature specific, it was extending the base project in preparation.
The second change just added a doc describing all the changes needed for the feature.
The third change removed the doc as parts were implemented, bit by bit.
As I progressed on the third change & found stuff I'd missed at the start of this process, I jumped back to edit the first change (maybe I had a bug in that base project extension) and the second change (oh hey, I found something else that needed to be done for the feature).
It sounds crazy compared to a git workflow, but at the end of the process I have 3 changes, all tested & working. If I was doing this with git, I'd have to rebase/squash to get the final changes into a neat clear history.
If you don't run checkout on file paths, how do you undo changes to specific files that you haven't committed yet? Like you've edited but not committed <foo>, <bar>, and <baz>. You realize your edits to <bar> are a mistake. I'd just run `git checkout <bar>` to revert those changes, what do you do?
It is also really useful when you realize you want <bar> to be the version from a commit two weeks ago. I guess you could always switch to the branch 2 weeks ago, copy the file to /tmp/, switch back, and copy the file into place, but `git checkout c23a99b -- <bar>` is so quick and easy. Or does this example not fall under the "dont run checkout on a path" since it is taking a treeish first before the path?
Interesting - I use git checkout constantly, whenever I have a file in another branch or commit that I want to drag into this one wholesale.
"Just don't accidentally do things wrong" is also the way to avoid null pointer errors, type mismatches in dynamically typed languages, UB in C/C++. It works, until it doesn't, and in practice that happens pretty quickly. Personally, I like things that have proper safety checks.
But branches are not just named pointers to a commit. If they were, then checking out the pointer would be the same as checking out the commit itself. But I can check out a commit and I can check out a branch and depending on which I've done, I'm in two different states.
Either I'm in branch state, where making a commit bumps the branch pointer and means the commit will be visible in the default log output, or I'm in "detached head" mode, and making a commit will just create a new commit somewhere that by default is hidden into I learn what a reflog is. And the kicker is: these two states look completely identical - I can have exactly the same files in my repository, and exactly the same parent commit checked out, but the hidden mode changes how git will respond to my commands.
In fairness, none of this is so difficult that you can't eventually figure it out and learn it. But it's not intuitive. This is the sort of weirdness that junior developers stumble over regularly where they accidentally do the wrong kind of checkout, make a bunch of changes, and then suddenly seem to have lost all their work.
This is one of the ways that I think the JJ model is so much clearer. You always checkout a commit. Any argument you pass to `jj new` will get resolved to a commit and that commit will be checked out. The disadvantage is that you need to manually bump the branch pointer, but the advantage is that you don't necessarily need branch pointers unless you want to share a particular branch with other people, or give it a certain name. Creating new commits on anonymous branches is perfectly normal and you'll never struggle to find commits by accidentally checking out the wrong thing.
> these two states look completely identical
No they don't. As you noted, one state is "detached head" and any competently set up shell PS1 will tell you that, or that you're on a branch by displaying the name of the branch vs the commit.
> Creating new commits on anonymous branches is perfectly normal
Sorry, that that's an example of more intuitive behavior on jj's partc, you've lost me. I've done that intentionally with git, but I know what I'm doing in that case. For someone new to version control, committing to an unnamed branch doesn't seem like a desired operation no matter which system you're using. What's wrong with requiring branches to be named?
> For someone new to version control, committing to an unnamed branch doesn't seem like a desired operation no matter which system you're using.
We have data on this! I can't cite anything public, but companies like Meta have to train people who are used to git to use tools like sapling, which does not require named branches. In my understanding, at first, people tend to name their branches, but because they don't have to, they quickly end up moving towards not naming.
> What's wrong with requiring branches to be named?
Because it's not necessary. It's an extra step that doesn't bring any real benefits, so why bother?
Now, in some cases, a name is useful. For example, knowing which branch is trunk. But for normal development and submitting changes? It's just extra work to name the branch, and it's going to go away anyway.
Fascinating. The benefit it brings is you can map the branch to its name. Of the, say, 10 branches you've got checked out, how do you know which branch maps to jira-123 and which one maps to jira-234, or if you're using names, which anonymous branch maps to addFeatureA or fixBugB?
More to the point though, what tooling is there on top of raw jj/git? Specifically, there's a jira cli (well, multiple) as well as a gh cli for github as well as gitlab has one as well. When you call the script that submits the branch to jira/github/gitlab, how does it get the ticket name to submit the code to the system under? Hopefully no one's actually opening up jira/github/gitlab by hand and having to click a bunch of buttons! So I'll be totally transparent about my bias here in that my tooling relies on the branch being named jira-123 so it submits it to jira and github from the command line and uses the branch name as part of the automated PR creation and jira ticket modification.
> Of the, say, 10 branches you've got checked out, how do you know which branch maps to jira-123 and which one maps to jira-234, or if you're using names, which anonymous branch maps to addFeatureA or fixBugB?
The descriptions of the changes. I shared some jj log output in another comment, here it is with more realistic messages, taken from a project of mine:
That (rue-fwi9) is the equivalent of jira-123, if I super care about it being obvious, I might put it in the message. But also, I might not, as you can see with the other two. You could also pass flags to see more verbose output, if the first line isn't clear enough, but in general, the convention for git as well is to have that short summary that explains your change, so if it's confusing, you probably need to do better on that.
> Specifically, there's a jira cli (well, multiple) as well as a gh cli for github as well as gitlab has one as well.
These systems do require branches in order to open a pull request. In these cases, I use `jj git push -c <change id>`, which will create a branch name for me, and push it up. This is configured to produce a branch name like steveklabnik/push-mrzwmwmvkowx for a change with the id mrzwmwmv, and ultimately, it's still easier to name locally with m or mr depending on if the prefix is ambiguous. That said, from there I do usually just click the button and then "open pull request" on GitHub, but like, all of these tools (gh is the only one I've used, but I can't imagine that the others do not work, since ultimately, it's a git repo) just work if you want to use them.
Other systems do not even require a branch to submit, and so you don't even need to do this. I would say "submit mr" and it would return me the URL for the created change request. Gerrit does this on top of plain old git.
> how does it get the ticket name to submit the code to the system under?
I haven't worked with Jira in a long time, but with GitHub, if I make a change that fixes issue 5, I put "Fixes #5" in my description, and when the PR is created, it updates ticket #5 to link the PR to that change automatically, no other process needed.
You can name branches in JJ too, they're just called bookmarks.
git checkout main git pull git switch -c jira-234 ... git commit git push -u origin main
jj git fetch jj new main ... jj commit jj b(ookmark) c(reate) jira-234 -r @- jj b(ookmark) t(rack) jira-234@origin jj git push
Right, this is a good point: you can if you want to, or if you're working with a system that requires them.
Just in practice, anonymous branches end up feeling very natural, especially during development, and especially if your code review tooling doesn't require names.
> any competently set up shell PS1 will tell you that
I certainly hope your shell is not running `git` commands automatically for you. If so, that is a RCE vulnerability since you could extract a tarball/zip that you don't expect to be a git repository but it contains a `.git` folder with a `fsmonitor` configured to execute a malicious script: https://github.com/califio/publications/blob/main/MADBugs/vi...
Might want to let git know. It's been a part of the git source code since 2006. If there were an RCE vulnerability from using __git_ps1, one would hope it would have been found by now!
https://github.com/git/git/blob/master/contrib/completion/gi...
I was able to reproduce it using that script in my PS1 when `GIT_PS1_SHOWUNTRACKEDFILES=1` which triggers a call to `git ls-files`. Without that, it seems to be just calling `git rev-parse` which does not execute fsmonitor.
I was also able to reproduce it with `GIT_PS1_SHOWDIRTYSTATE=1` which invokes `git diff`.
As far as I am aware, this has been brought to the attention of the git maintainers years ago: https://github.com/justinsteven/advisories/blob/main/2022_gi...
They look identical to people who don't know what to look for, and who don't realise that these two states are different, which is the key thing. You can also distinguish them by running `git status`, but that's kind of the point: there's some magic state living in .git/ that changes how a bunch of commands you run work, and you need to understand how that state works in order to correctly use git. Why not just remove that state entirely, and make all checkouts behave identically to each other, the only difference being which files are present in the filesystem, and what the parent commit was?
What's wrong with unnamed branches? I mean, in git the main issue is that they're not surfaced very clearly (although they exist). But if you can design an interface where unnamed branches are the default, where they're always visible, and where you can clearly see what they're doing, what's wrong with avoiding naming your branches until you really need to?
I think this is the key thing that makes jj so exciting to me: it's consistently a simpler mental model. You don't need to understand the different states a checkout can be in, because there aren't any - a checkout is a checkout is a checkout. You don't need to have a separate concept of a branch, because branches are just chains of commits, and the default jj log commands is very good at showing chains of commits.
My command looks like either:
or fragmede@laptop:(abcdef)~/projects/project-foo$
Depending on if abranch is checked out, or abcdef which may be HEAD of abranch is checked out.
If you're having to run `git status` by hand to figure out which of the two states you're in, something's gone wrong. (That something being your PS1 config.) If people are having trouble with that, I can see why switching to a system that doesn't have that problem, it just that it doesn't seem like it should even be problem to begin with. (It's not that it's not useful to have unnamed branches and to commit to them, just that it's not a intro-to-git level skill. Throwing people into the deep end of the git pool and being surprised when some people sink, isn't a good recipe for getting people to like using git.)
> What's wrong with unnamed branches? As you point out, those commits kinda just go into the ether, and must be dug out via reflog, so operationally, why would you do that to yourself. Separate from that though, do you "cd" into the project directory, and then just randomly start writing code, or is there some idea of what you're working on. Either a (Jira) ticket name/number, or at least some idea of the bug or feature you wanna work on. Or am I crazy (which I am open to the possibilty) and that people do just "cd" into some code and just start writing stuff?
VCS aside, nothing worse than opening Google docs/a document folder and seeing a list of 50 "Untitled document" files an my habit of naming branches comes from that. Even though I'm capable of digging random commits out of reflog, if all of those commits are on unnamed branches, and have helpful commit messages like "wip" or "poop", figuring out the right commit is gonna be an exercise in frustration.
As long as you've got something that works for you though, to each their own. I've been using too long for me to change.
> preventing you from accidentally rebasing changes that have been shared.
I think this ruins it for me then. I push my in-progress work, to my in-progress branches (then git-squash or whatever later, if needed). It makes switching between (lab) computers, dead or not, trivial.
Is there some "live remote" feature that could work for me, that just constantly force pushes to enabled branches?
Nothing stops you from doing the equivalent of `git push --force` in `jj`. The flag is just named differently: `--ignore-immutable`. This is a global flag though, so it's available to all commands, and `jj` requires it whenever you're making changes to immutable commits, even locally. I'd argue that this is one of the killer features of `jj`, since by comparison `git rebase` treats everything the same whether you're squashing your own local commits on a feature branch or messing with the history of `main` in a way that would break things for everyone.
Yes, almost all JJ users do this constantly. Just "track" the particular branch. JJ has an idea that only some commits are immutable, the set of "immutable heads", and the default logic is something like "The main branch is always immutable, remote branches are immutable, 'tracked' remote branches are mutable." In other words, tracking a remote branch removes it from the set of immutable heads.
So just run:
and the default settings will Do What You Want. This is intended as a kind of safeguard so that you do not accidentally update someone else's work.
Some people configure the set of immutable heads to be the empty set so they can go wild.
This is all incredible. I even see a great looking GUI [1]!
[1] https://jj-gui.com/
This is literally jj's schtick and reason for existing, so I wouldn't be surprised if you decide it is not the tool for you.
Yeah, that's a very real possibility. On the bright side, jj is git-compatible so at least the two camps can live together in harmony.
How are you "checking out" the old commit? It sounds like you're using `jj edit`, which I'd argue does what it says on the tin. Switch to using `jj new <branch>` and your problem goes away.
That avoids the problem for the specific workflow of checking out an old revision (and it was what I was describing with checking out a new branch off the old commit and adding a blank commit to that branch), but another way this design bites me: At work I am constantly jumping around numerous repos because I might be working on repo <foo> but then someone on my team will ask for help with repo <bar>. So I'll turn on screen sharing, open up repo <bar> and I'll type out psuedo-code into <bar> as I'm explaining things to them.
So if the last thing I did on <bar> was finish some work by making a new commit, then writing some changes, and then giving it a commit message with `jj desc`, then I am now polluting that commit with the unrelated explanatory psuedo-code. So when switching to a repo I'm not actively working in, I need to defensively remember to check the current `jj status` before typing in any files to make sure I am on an empty commit. With git, I can jump around repos and make explanatory edits willy-nilly, confident that my changes are distinct from real meaningful commits.
I guess one way to describe it is: we want to make it easy to make good commits and hard to make bad commits. jj seems to be prioritizing the former to the detriment of the latter. My personality prioritizes rigorous safety / lack of surprises.
Fwiw I generally solve this by using `jj commit` instead of `jj desc` unless I'm specifically targeting something that isn't my working copy. Technically it violates the "we want commands to be orthogonal" guideline we use to write Jujutsu (otherwise this would indeed be `jj desc; jj new`) but as a habit it's never let me down
Ah, thanks! That's a command I haven't learned yet, so I'll have to check it out. I learned jj from the tutorial that was posted and I don't think it covered `jj commit` at all.
I didn't cover it for various reasons, but I think it's good to teach now that I've had more time to consider this sort of thing, so the next iteration will likely start by beginning with jj commit.
In a pure `jj` model, commit might not even be necessary as it's own subcommand (since you could easily define an alias for `desc` followed by `new`). We're still living in a world where most people who would consider adopting `jj` are git users currently, so I wonder if starting with `commit` and then following it up with an explanation of "here's how you can change the commit message without needing to make a new commit" and "here's how you can make a new commit without changing the name of the current one" would end up fitting people's expectations better.
Yes, I do think that the latter is correct now.
I tend to learn "bottom-up", so I like the new + describe as a way of learning, but people want to jump in and get going with tools, so commit fits that expectation better.
From your "polluted" snapshot, you can run `jj commit -i` and use the TUI to select only what you want.
Just like you can run `git add -p`
yes but no
Explain the difference.
git add -p doesn't create a commit.
I think you have somehow picked up an overcomplicated workflow, and this is case is actually something that `jj` is much better at.
If I'm in the middle of working on <foo> and someone asks about <bar>: `jj new <bar>`. When I'm done (and do whatever I want with those new changes in <bar>, including deferring deciding what to do), I just `jj edit <foo>` and I'm back exactly where I left off. It's a bit like `git stash` without having to remember to stash in advance, and using regular commit navigation rather than being bolted on the side.
I think the right intuition to have with jj is that `jj st` should show an empty change unless you are actively working on something. `jj commit`, as mentioned below, is a good example of this - it automatically creates a new change and checks it out. The "squash flow" also does this well - you use the branch tip as a staging area and squash work into other changes on the branch as you go along. Either way, once the work is finished, there's an empty change at the tip of the branch.
This is also supported by jj implicitly - whenever you check out a different commit, if the change you were on is empty, has no description, and is the tip of a branch, it's automatically deleted to clean things up for you.
Jujutsu has a concept of mutable vs immutable commits to solve this. Usually everything in a remote branch is immutable. To work on a branch, I track it and that makes it mutable.
if you loose an edit jj op log is incredible, I've saved a ton of work more-so now with AI's making mistakes. Also workspaces are super fast compared to git worktree's - same concept, different implementation.
I agree, that was a bit of an interesting approach but more-so than not it's been better in DX even though you have to 'unlearn' long term it's been a benefit IMO, but a soft one, not something you can measure easily.
> edits to files are automatically committed
this is a core feature and it makes jj possible - you're supposed to get used to jj new and jj squash into the previous bookmarked commit, which you map to the git branch head/PR.
IOW you're supposed to work on a detached git head and jj makes this easy and pleasant.
The last paragraph might be the most important one:
> There's one other reason you should be interested in giving jj a try: it has a git compatible backend, and so you can use jj on your own, without requiring anyone else you're working with to convert too. This means that there's no real downside to giving it a shot; if it's not for you, you're not giving up all of the history you wrote with it, and can go right back to git with no issues.
Funnily enough, that's how I used git with CVS and Subversion, too.
But this is not true. They are interoperable but far from seamless. Those features mainly support migration use cases or things like git deployment from an repo managed in jj. Operations git does are not in jj’s log. You have to constantly import them. The project recommends a single primary interface.
But it is true. I (and many others) happily use jj on teams that use git without anyone else on the team using jj or knowing (or caring) what I'm using.
If you constantly switch between the two, you're going to have a hard time, but you can take a git repo, try jj for a while, and if you decide to go back, you don't lose anything.
Right, but that’s different from working in a team environment where everyone else continues using git.
How so? I've used `jj` locally on teams where most (if not all) of the other team members were using git, and they only found out I was using `jj` when I told them.
fwiw I don't use it personally but some people on my team use it while the others use git, and nobody complains.
Yeah same here, have been using jj exclusively, the only reason people notice is because my branch names default to the changeid in my setup so I've had questions about the random looking strings.
No?
What problems, exactly, are you suggesting exist? I have used jj extensively on git teams and it has been seamless. The only people who have noticed or cared are the coworkers I’ve gotten to join me.
You're confusing mixing git and jj in your local copy of the repo vs what it looks like to other people. You can use jj locally, and it interoperates perfectly with any git remote, and no one has to know you're even using it. From the point of view of other people, it doesn't matter.
I think you are talking about colocation, which is slightly different than the `jj git push` `jj git fetch` type commands.
Colocation has its uses bit is a bit finicky. The push/pull compatibility works perfectly fine (with some caveats of github being broken that can be worked around).
Most importantly, submodules are not fully supported, which are used by almost every open source project at least in the space I work in (embedded). So you can't use jj to easily contribute back to those project. It can be done but you always have to be cognizant of whether a submodule has changed between two branches or when you sync, since they don't update automatically the way they do with git.
It's been over a year since I last used git manually in the CLI, and I've exclusively worked with git remotes. The only time I had any friction was on a team where stale code-gen output was checked into the repo and for whatever reason no one was willing to either add it to the `.gitignore` or commit (pun intended) to keeping it up to date, meaning that I had to manually remove the changes from when I compiled before pushing. I would have argued in favor of adding to .gitignore or keeping it up to date even if I didn't use `jj` though because I think having stale output checked in is just silly.
For what it's worth, you can have your own local gitignore by adding patterns to .git/info/exclude. It's quite useful in this exact situation.
I did try this, but for whatever reason it kept getting added back automatically. I forget the details of exactly why it was happening because it was close to a year ago, and in the compatibility guide it says this is supported, but I'm not sure if it was at the time or I was running into something different. This was a contract gig for me where I knew it would be ending within a month or so, which meant I didn't bother spending a ton of time trying to figure out a long-term solution.
Big caveat: do not try to use Git and JJ in the same directory. It's probably fine if you only use JJ, but if you mix them you will horribly break things.
This isn't true?
It is when I tried it.
Jujutsu uses git as its primary backing store and synthesizes anything else it needs on top on-the-fly. Any incompatibility here is considered a serious bug.
Obviously I can’t argue against your lived experience, but it is neither typical nor common. This is quite literally an explicitly-supported use, and one that many people do daily.
I suppose it depends what you mean by "horribly break things".
The only thing I've noticed is that `jj` will leave the git repo with either a detached HEAD, or with a funny `@` ref checked out.
I don't think that would trouble someone who's experienced with git and knows its "DAG of commits" model.
For someone who's less experienced, or only uses git for a set of branches with mostly linear history (like a sort of "fancy undo"), I could imagine getting a shock when trying to `git commit` and not seeing them on any of the branches!
> I don't think that would trouble someone who's experienced with git and knows its "DAG of commits" model.
I think most people that have git experience don't know what a DAG is and have never used reflog.
Unless you use LFS, submodules, or hooks at your org.
Submodules work fine but yeah, it's frustrating that lfs is taking so long. But there seems to be some momentum recently https://github.com/jj-vcs/jj/pull/9068
The git compatibility page states that submodules are not supported
https://docs.jj-vcs.dev/latest/git-compatibility/
What "not supported" means with submodules specifically is that jj doesn't have commands to manage them. You can use git commands to manage them, and it does, in my understanding, work. There's just no native support yet.
This is sort of similar to how you can create lightweight tags with jj tag, but you need to push them with git push --tags.
One of my favorite jj features is "jj absorb".
For each change you've made in the current revision, it finds the last commit where you made a change near there, and moves your changes to that commit.
Really handy when you forgot to make a change to some config file or .gitignore. You just "jj new", make the changes, and "jj absorb". No need to make a new commit or figure out where to rebase to.
Oh, and not having to deal with merge conflicts now is awesome. My repository still has merge conflicts from months ago. I'll probably go and delete those branches as I have no intention to resolve them.
git absorb exists too fyi
Not by default: https://github.com/tummychow/git-absorb
And If `jj absorb` gets it wrong, you can run `jj undo`.
This is such a killer feature to me. I'm not scared to start potentially gnarly rebases anymore because I can painlessly undo.
Yes. With "jj undo", I'm not scared to do anything. The brief time I had to go back to using vanilla "git", I didn't enjoy being extra cautious.
Using a version control tool shouldn't require much self discipline.
Why should you care about jj? Look, ethereal, balaeric Gothenburg indie from the 2010s may not be important in the grand scheme of things, but their strong hip hop influence was genuinely exciting at the time. When the great chill wave summer of 2009 crested, you definitely cared about jj’s otherworldly grooves and lil Wayne samples. Even if they never did reach the euphoric highs of Washed Out or even label mates The Tough Alliance.
Preach... thank you
Hey folks!
So, I haven't updated the tutorial in a long time. My intent is to upstream it, but I've been very very busy at the startup I'm at, ersc.io, and haven't had the chance. I'm still using jj every day, and loving it.
Happy to answer any questions!
jj automatically hides "uninteresting" changes. Most of the time, this is good.
Occasionally, I need to see more changes. It is not obvious to me how I get jj to show me elided changes. I mean, sure, I can explicitly ask jj to show me the one ancestor of the last visible change, and then show me the ancestor of that one, etc. Is some flag to say: "just show me 15 more changes that you would otherwise elide"?
I use `jj log -r ..` for that, which is just an open ended range. It's not the "15 more" but it's what's worked for me. I suspect you could do it with some sort of revset stuff, but I like to keep it simple.
Easy: `jj log -n 25`
(Default is 10 iirc, so if you want 15 more... 25)
If you want everything, ever: `jj log -r ::`
Or every ancestor of your current change: `jj log -r ..@`
IIRC -n only limits the output, not expands it. jj log and jj long -n 25 show the same results for me.
I think one major difference between git and jj is how immutable their DAG is, due to the difference in how they refer to their unit of change (i.e. stable change ID with changing commit IDs vs. immutable commit ID). One implication of that is change history in a git repo feels much more immutable to the one in a jj repo. Consequently operations that involves changing the history like, undo/rebase feels much easier/flexible. Is my understanding correct?
Sorta! I think it can feel that way at times, but also the opposite. jj’s changes are immutable in the same way commits are, when you modify a change, it makes a new immutable commit and associates that with the change. So on the literal level, they’re the same.
But it’s true that mutating history is easy and sometimes even automatic with jj, whereas it’s not with git. So that could make it feel more mutable. On the other hand, jj has the concept of mutable vs immutable commits; jj will present you from modifying certain changes unless you pass in a flag to override it. So in some ways, it’s more immutable than git.
Just really depends on your perspective.
I think the mental model is like C vs python. Git gives you a forensic trace back in time. jj gives you a story with chapters. Look under the hood you'll still see forensic map of state transitions, but this not what we want to navigate most of the time. Sometimes we need to rewrite an early chapter to make the latest chapter make more sense.
the fact that almost by definition stuff that jj does is possible in git makes it hard for some folks to let go of the baggage that git has; it's simply hard to imagine a world where you can't git add ('how do you commit what you need committed and not commit the rest?') or not having to resolve conflicts immediately ('why would I want not to?')
...and it turns out when you answer these questions differently ('working tree is a commit', 'conflicts can committed) but still want git compatibility, jj kinda falls out of the design space by necessity.
One of the things that makes jj worth trying out is simply the fact that it is different than git, and having exposure to more than one way of doing things is a good thing.
Even if you don't adopt it (and I didn't), it's easy to think that "this way is the only way", and seeing how systems other than your own preferred one manage workflows and issues is very useful for perspective.
That doesn't mean you should try everything regardless (we all only have so much time), but part of being a good engineer is understanding the options and tradeoffs, even of well loved and totally functional workflows.
being a good engineer is also understanding when something is a waste of time because the gain is insignificant 99% of the time
"It's more powerful and easier" is a great claim, but I need examples in this opening page to convince me of the pain I could save myself or the awesome things I'm living without.
does trivially working on 3 PRs in a single checkout and pushing focused changes to each one independently without thinking twice count?
if you don't need this, you might not see any value in jj and that's ok. you might use magit to get the same workflow (maybe? haven't used magit personally) and that's also ok.
Actually it is a anti-demo, because while software allows you to do it, I don't think many software engineers can work on this.
in large enough monorepos and teams and big enough changes you either do it like this or have a humongous giga-PR which eventually starts conflicting with everything.
Guess he was talking about the presentation, not what the tool can achieve. It has no hard proof on the first page, which could easily just be a LinkedIn pitch, but not on hackernews
It might count, but it is easy with git as well, what is the feature in jj that makes this easier? Switching branches and pushing changes to remotes is the core feature of git and in my opinion really easy so I'm curious how jj improves on it.
rebases don't lose branches and jj absorb trivially squashes changes to the correct head (or leaves changes alone if it can't find where to squash).
is it possible in git? yeah, I've done it; there's a reason I haven't done it more than a few times with git, though. ergonomics matter.
Can you show how you would do this in jj?
I know how I would do this in git, but don't really see how this would be in jj. I currently don't use it in my workflow, but if it is super easy in jj then I could see myself switching.
the beauty of it is there's not much to show; I use a crude jjui approach where I have an octopus merge working tree commit (in command line terms, jj new PR_A PR_B PR_C) and either use native jj absorb (S-A in jjui) which guesses where to squash based on the path or, when I'm feeling fancy, rebase the octopus via jjui set parents (S-M) function (also handy to clean up parents when one of the PRs gets merged).
This is how I'd do it:
This creates an empty commit that merges all 3 branches, you can think of this as your staging area.
When you want to move specific changes to an existing commit, let's say a commit with an ID that starts with `zyx` (all jj commands highlights the starting characters that make the commit / change unambiguous):
Then select your changes in the TUI. `-i` stands for interactive.
If you want to move changes to a new commit on one of the branches:
Then select the changes you want moved. `-A` is the same as `--insert-after`, it inserts the commit between that commit and any children (including the merge commit you're on).
There's one thing that's a bit annoying, the commit is there but the head of the branch hasn't been moved, you have to move it manually (I used + to get the child to be clearer, but I usually just type the first characters of the new change id):
This is good feedback, for sure, thank you. It's sometimes hard to come up with truly concise examples, but that's also why they're so valuable.
Consider using the table of contents on the left of the page to view "Real World Workflows", "Branching, Merging, and Conflicts", and then "Sharing Your Code with Others" and then evaluate how JJ does things against your current git workflow. This requires some minor effort on your part.
The official JJ docs also have a "bird's eye view" introduction and tutorial available here: https://docs.jj-vcs.dev/latest/tutorial/.
EDIT: Jujutsu for Git experts: <https://docs.jj-vcs.dev/latest/git-experts/>. This outlines some of the main advantages relatively succinctly.
Yeah we moved on from SVN to git because SVN branches were truly a pain in the ass to work with. I truly do not have any rough edges or big pains in my day to day git workflow.
There is no index anymore. I guess that is the "easier" part.
That is, but not directly.
The general idea here is that jj has fewer and more orthogonal concepts than git. This makes it more regular, which is what I mean by "easy."
So for example, there is no index as a separate concept. But if you like to stage changes, you can accomplish this through a workflow, rather than a separate feature. This makes various things less complex: the equivalent of git reset doesn't need --hard, --soft, --mixed, because the index isn't a separate concept: it's just a commit. This also makes it more powerful: you can use any command that works on commits on your index.
This is repeated across jj's design in general.
Specific commands don't really showcase the appeal of jj. If anything they might scare someone at first glance. It's the fact that the workflows are intuitive and you never find yourself reaching for help to get something done. You really need to try it to understand it.
> that the workflows are intuitive
It can't be both intuitive and yet too complicated to show examples at the same time.
The only intuitive interface is the nipple. All other things are learned.
I feel very comfortable using git. Maybe jj is better, but not seeing is not believing.
> but not seeing is not believing.
Classic denying the antecedent :-)
https://en.wikipedia.org/wiki/Denying_the_antecedent
It's not supposed to be a modus ponens deduction.
Just an expression of what the Missouri state's nickname says.
jj is better for some workflows, which, if you're a git expert as you claim, you conciously or subconciously avoid as 'too much work' or 'too brittle'.
if you don't care about them after accepting this realization... it's fine. git is good enough.
I’m not a fit expert by any means. The workflows being described do not appeal to me but not because of the way fit works. They sound confusing and I don’t understand what benefit I’m getting out of them. Like, it’s a solution to a problem I’m not sure exists (for me)
A couple things off the top of my head:
- You aren't forced to resolve rebase/merge conflicts immediately. You can switch branches halfway through resolving conflicts and then come back later and pick up where you left off. You can also just ignore the conflicts and continue editing files on the conflicted branch and then resolve the conflicts later.
- Manipulating commits is super easy (especially with jjui). I reorder commits all the time and move them between branches. Of course you can also squash and split commits, but that's already easy in git. Back when I was using git, I would rarely touch previous commits other than the occasional squash or rename. But now I frequently manipulate the commit history of my branch to make it more readable and organized.
- jj acts as a VCS for your VCS. It has an operation log that is a history of the state of the git repository. So anything that would be destructive in git (e.g. rebase, pull, squash, etc) can be undone.
- Unnamed branches is the feature that has changed my workflow the most. It's hard to explain, so I probably won't do it justice. Basically you stop thinking about things in terms of branches and instead just see it as a graph of commits. While I'm experimenting/exploring how to implement or refactor something, I can create "sub-branches" and switch between them. Similar to stashes, but each "stash" is just a normal branch that can have multiple commits. If I want to test something but I have current changes, I just `jj new`. And if I want to go back, I just make a new commit off of the previous one. And all these commits stick around, so I can go back to something I tried before. Hopefully this made some sense.
Also note that jj is fully compatible with git. I use it at work and all my coworkers use git. So it feels more like a git client than a git replacement.
All of these features sound like the recipe for a confusing nightmare!
"You can switch branches halfway through resolving conflicts and then come back later and pick up where you left off. You can also just ignore the conflicts and continue editing files on the conflicted branch and then resolve the conflicts later."
"Similar to stashes, but each "stash" is just a normal branch that can have multiple commits. If I want to test something but I have current changes, I just `jj new`. And if I want to go back, I just make a new commit off of the previous one. And all these commits stick around, so I can go back to something I tried before."
Yeah I legit do not understand the appeal. I’m willing to be wrong but it’s not clicking with me at all
typical for experienced git users who already 'just don't do' things which git punishes you for; after a decade it's hard to even imagine any other way, not to mention that it might be better. been there, done that, jj is legit after letting go of (some of) git.
Anonymous branches are amazing for when you are trying out a bunch of different approaches to a problem. As I search the space of possible solutions for what I'm really looking for, I end up with a tree of various approaches.
Then when you rebase, the entire tree of anonymous branches can be rebased onto main in 1 command. This is why the first class conflicts and not having to resolve conflicts immediately is so important: when i'm rebasing, an entire tree of branches is getting rebased and so if you had to resolve conflicts right away it would be incredibly cumbersome, because I'm rebasing like 30+ commits and a bunch of anonymous branches in a single operation.
I work on top of an octopus merge of all my in-flight PRs. ON top of that merge commit i have a bunch of anonymous branches with various things going on. When I'm ready to submit a PR, I take one of those anonymous branches and rebase it onto main and make it an additional parent of my 'dev-base' merge commit. Then i give that branch a name and submit it as a PR.
Every day when I start working, I rebase this entire subgraph of branches in a single command onto main. all my PRs are up to date, all my anonymous branches are up to date, etc... Takes like 2 seconds. If some of my anonymous branches are in a conflicted state, that's ok, i don't have to deal with it until I want to work on that change again.
These anonymous branches aren't confusing because they all show up in the default revset that is shown when looking at the jj log. I can easily browse through them with jjui TUI and instantly see which ones are what. It's really not confusing at all.
https://ofcr.se/jujutsu-merge-workflow
The "you don't need to worry about resolving conflicts" thing is confusing when you hear it with words, so let me show you what it looks like in practice.
Let's say I have two branches off of trunk. They each have one commit. That looks like this (it looks so much nicer with color, I'm going to cut some information out of the default log so that it's easier to read without the color):
So both `foo` and `bar` are on top of trunk, and I'm also working on a third branch on top of trunk (@). Those vvxv and such are the change ids, and you can also see the named trunk there as well.
Now, I fetch from my remote, and want to rebase my work on top of them: a `jj git fetch`, and then let's rebase `foo` first: that's `jj rebase uu -o trunk` (you only need uu instead of uuowqquz because it's a non-ambiguous prefix, just like git). Uh oh! a conflict!
Note that jj did not put us into a "hey there's a conflict, you need to resolve it" state. It just did what you asked: it rebased it, there's a conflict, it lets you know.
So why is this better? Well, for a few reasons, but I think the simplest is that we now have choice: with git, I would be forced to deal with this conflict right now. But maybe I don't want to deal with this conflict right now: I'm trying to update my branches in general. Is this conflict going to be something easy to resolve? In this case, it's one commit. But what if each of these branches had ten commits, with five of them conflicted and five not? It might be a lot of work to fix this conflict. So the cool thing is: we don't actually have to. We could continue our "let's rebase all the branches" task and rebase bar as well. Maybe it doesn't have a conflict, and we'd rather go work on bar before we come back and deal with foo. Heck, sometimes, I've had a conflicted branch, and then a newer version of trunk makes the conflict go away! I only have to choose to address the conflict at the moment I want to return to work on foo.
There's broader implications here, but in practice, it's just that it's simply nicer to have choice.
I thought the same until I started using it.
Turns out, git sorta trains you to be very, very afraid of breaking something.
jj answers this in a few ways:
1. everything is easily reversible, across multiple axes.
2. yes, everything is basically a stash, and it's a live stash — as in, I don't have to think about it because if it's in my editor, it's already safely stored as the current change. I can switch to a different one, create a new one, have an agent work on another one, etc, all without really caring about "what if I forgot to commit or stash something". Sounds like insanity from a git POV but it really is freeing.
3. Because of 2, you can just leave conflicts alone and go work on something else (because they are, like you said, essentially stashed). It's fine and actually very convenient.
The thing the article doesn't mention, that makes this all safe, is that trunk / "main" is strictly immutable. All this flexibility is *just* for unmerged WIP. (There are escape hatches though, naturally!)
In practice, it isn't. What you're identifying as potentially nightmarish - and no doubt quite tedious in git - are things that JJ enables you to do with a small subset of commands that work exactly how you expect them to work _in every workflow context_ in which they are needed.
Thinking specifically about conflicts: being able to defer conflicts until you're ready to deal with them is actually great. I might not be done with what I am actually working on and might want to finish that first. being forced into a possibly complicated conflict resolution when I'm in the middle of something is what I'd actually consider nightmarish.
When you want to solve the conflict: `jj new <rev>`, solve the conflict, then `jj squash`, your conflict resolution is automatically propagated to the chain of child commits from the conflict.
Remember when you used SVN or whatever before git, and you loved git because of how easy it is to make branches?
With branches, jj is to git what git was to SVN. It's an order of magnitude less friction to do branching in jj than git.
Not long ago, I pulled from main and rebased my branch onto it - merge conflicts. But I wanted to work on some other feature at the moment. Why should I have to fix this merge conflict to work on a feature on a totally different branch? With jj, I don't. I just switch to the other branch (that has no conflict), and code my new feature. Whenever I need to work on the conflicted branch, I'll go there and fix the conflict.
Once I started using jj, I realized how silly it was for git to have separate concepts for stash and index. And it's annoying that stash/index is not version controlled in git. Or is it? I have no idea.
In jj, a stash is simply yet another unnamed branch. Do whatever you want there. Add more commits. Then apply it to any branch(es) that you would like to. Or not.
Why does git need a separate concept of a stash? And wouldn't you like a version controlled stash in git?
Have you ever made a ton of changes, done a "git add", accidentally deleted some of the changes in one file, done a "git add", and thought "Oh crap!" I suppose that information can be recovered from the reflog. But wouldn't you wish "git add" was version controlled in the same way everything else is?
That's the appeal of jj. You get a better stash. You get a better index. And all with fewer concepts. You just need to understand what a branch (or graph) is, and you get all of it. Why give it a name like "stash" or "index"?
Why does git insist on giving branches names? Once you get used to unnamed branches, the git way just doesn't make sense. In jj you'll still give names wherever you need to.
I also like the powerful revision querying mechanisms that they pulled in from mercurial. They seem to work just like mercurial revset queries which can be used in various operations on sets of revisions.
I would like them to have mercurial's awesome hg fa --deleted when it comes to history trawling, but apparently for it to work well, they also need to swap out git's diff format for mercurial's smarter one, so I'll be waiting on that for a while I suppose.
> So anything that would be destructive in git (e.g. rebase, pull, squash, etc) can be undone.
It’s possible to recover from these with git reflog, though.
`jj undo` compared to what exactly?
I've used git for years, and used reflog once or twice.
I've used jj for only a year, and have used "jj undo" countless times.
There's a huge benefit to having a simpler mental model.
"jj undo" is worth the price of admission by itself.
See the current top thread on HN about backblaze not backing up .git repos. People are flaming OP like they're an idiot for putting a git repo in a bad state. With jj, it's REALLY HARD to break your repo in a way that can't be fixed by just running "jj undo" a couple times.
jj has made me much more comfortable using non-linear DAGs in my trunk-based development workflow. Several changes with the same parent, changes with several different parents, etc.
I used to have a habit of imposing an unnecessary ordering structure on my work product. My stack of changes would look like A -> B -> C -> D, even if the order of B and C was logically interchangeable.
jj makes DAGs easier to work with because of how it handles conflicts and merges. Now I feel empowered to be more expressive and accurate about what a change actually depends on. In turn, this makes review and submission more efficient.
Do you use a mega-merge + absorb workflow on top of the faned-out changes?
I was already pretty happy with svn to be honest, I dont see myself switching away from the industry standard today for no substantial reason. in my opinion git was only able to change the standard thanks to github and a popular author (i love git and its branching, but I dont think it would have been enough if it was just for that). I personally believe its going to be very difficult for jj to replicate that.
I like the distributed nature of git but miss how svn worked to some degree. Generally I agree with you, git works, I read the post and didn't see anything that makes me want to change. The small number of jjites who have been inundating comment sections on this site lately feels like the devotees of Rust (a crap language), podman (which has too many operational inconsistencies) and JetBrains IntelliJ ballywhooers who used to preach IntelliJ's features on every programming related site.
In my opinion, svn lost to git because git is just so much better in a distributed environment. To me the improvement between git and svn is much more than the improvement between svn and cvs. In fact, by many aspects, I preferred cvs, but now that we have git, they can both go to hell.
Now, it is not the only DVCS, there is also Mercurial, which is built on similar principles. It could have become the standard, but I agree with you that the reason it didn't probably has to do with Github and Linux.
jj isn't fundamentally different from git, it is actually compatible with git, which is a good and a bad thing. Good because it makes transition easy, bad because it is fundamentally the same thing and there is no pressing reason to change.
But jj definitely look nicer than git, which is not hard, under the hood, git is great, but it is ugly. It started off as a hack and it still shows. I think the common sentiment of "I know git, I don't want to learn a new VCS" mostly tells about how painful the process of learning git was. If you had started off with Mercurial, like I did, I am sure it would feel much smoother, it is just git that is messy. For jj, it looks like they took the best parts of git and mercurial, so hopefully, the transition will be rather painless.
I really wanted to like JJ, it was handy for a few months when I used it. But for me in the end I reverted back to regular git.
What triggered me to go back was I never got a really clean mental model for how to keep ontop of Github PRs, bring in changes from origin/main, and ended up really badly mangling a feature branch that multiple contributors were working on when we did want to pull it in. I'll probably try it again at some point, but working in a team through Github PRs that was my main barrier to entry.
This is good feedback, thanks. The next version of the tutorial will certainly focus on stuff like this, as I agree it's really important to teach people.
locally? sure. stacked changes in jj are great. but the moment you push to GitHub, the review UI still thinks in SHAs. a lot of the pain just moves from the author to the reviewer.
I see that is an issue with many people, but now with github adding support for stacked PRs, I guess that would change
jj is great and while it was an adjustment at first, I've never looked back. I feel like when you're working with other people, things never get reviewed and merged as quickly as you'd like. With jj, it's pretty low-cost to have a bunch of PRs open at once, and you can do something like `jj new <pr1> <pr2> <pr3>` to build stuff that requires all 3. This lets me do things like... not do a big refactoring in the same PR as adding a feature. I can have them both self-contained, but still start on the next step before they're all merged. It's easy to add changes on top, switching between the individual PRs as comments come up, etc.
I always liked doing things like this. At Google where we used a custom fork of Perforce, I told myself "NEVER DO STACKED CLs HAVE YOU NOT LEARNED YOUR LESSON YET?" If one CL depended on another... don't do it. With git... I told myself the same thing, as I sat in endless interactive rebases and merge conflict commits ("git rebase abort" might have been my most-used command). With jj, it's not a problem. There are merge conflicts. You can resolve them with the peace of mind as a separate commit to track your resolution. `jj new -d 'resolve merge conflict` -A @` to add a new commit after the conflicted one. Hack on your resolution until you're happy. jj squash --into @-. Merge conflict resolved.
It is truly a beautiful model. Really a big mental health saver. It just makes it so easy to work with other people.
I'm having trouble understanding the value of this and most other supposed advantages of jj I'm seeing. I'm trying to pinpoint if it's because 1) my workflow doesn't need jj's fancy stuff, 2) I've gotten so used to `git`'s "flaws" that I don't notice them, or 3) the git porcelain I use (magit) does a good enough job at managing the flaws.
If you need to build on something that requires changes from 3 open PRs, can't you just start a new branch from main, merge all 3 PRs into it, and get to work? As changes are applied to the open PRs, you can rebase. Obviously that might cause some merge conflicts here and there, but as long as the PRs aren't super overlapping, they should still be manageable. If there's a ton of overlap between 3 open PRs, that to me sounds like a problem in the workflow/plan, which must be dealt with regardless of the VCS or porcelain.
For me, it wasn't so much that jj enabled things I couldn't do before, though there are some things. What it enabled was me doing the things I was doing, but in a more easy way. This also leads you to do things that you can do, but sometimes avoid because it's a lot of work.
I guess that makes sense but also reinforces the confusion I have on whether jj is just another git "porcelain" (aka UI), or a replacement for git altogether.
If it aims to mainly improve the UX (do the same things you were doing before but easier), then it's irrelevant to those of us who have been lucky to find and learn sensible UXs. If it aims to be a git replacement, I'm a little curious why the developers would decide to re-implement something from scratch only to end up with an "alternative" that is mostly compatible and doesn't radically change the internal model or add new features.
I last used GitHub Desktop years ago and had a terrible time. The git CLI is powerful but not very intuitive. It really wasn't until I learned magit that things "clicked" for me. I know that many git UXs are pretty bad. But the way git works internally seems pretty great to me. Too often, criticism of git conflates the two.
> then it's irrelevant to those of us who have been lucky to find and learn sensible UXs
Only if you're a solo dev that doesn't work on a team or have to mentor new devs that haven't developed good intuitions for this.
This strikes me a lot like the C vs. safer programming language debate all over again.
When I mentor new devs, I explain to them how I use git. Sometimes I show them the workflow in magit, which makes it easier to visualize things. But mostly I just show them how their intended actions map onto the relevant CLI commands and I tell them to figure out how those map onto their porcelain of choice. I've developed this intuition thanks to magit, but I don't think magit is necessary. This approach seems preferable to me than onboarding new devs onto a new tool that is not the industry standard.
> This strikes me a lot like the C vs. safer programming language debate all over again.
I don't see how. Safer programming language address a clear problem in C, with trade-offs (sometimes arguably the trade-offs may not be worth it, and in my experience that's what the debate tends to be about). If jj is a replacement for git it should be clear what problem within git it aims at addressing. If the problem is in the UX, then to me and many others it's not worth the trouble.
> When I mentor new devs, I explain to them how I use git.
Now imagine not needing to do that.
> I don't see how. Safer programming language address a clear problem in C
Being productive in C means training and experience in avoiding the footguns of C. See above.
Fundamentally, jj is its own VCS. It's just that it has pluggable backends. So when you use it with the git backend, it functions as a nicer git UI, but it's also not just that, because you can use it without git entirely. (though the major alternative backend is not open source, so while people do, unless you work at Google (or the startup I'm at...) you may personally not be able to.)
> then it's irrelevant to those of us who have been lucky to find and learn sensible UXs.
I was never someone who was upset at git's UX. I never found the 'hg is so much nicer' thing compelling. But then, I found that jj is just so much nicer to use, for me, that I haven't used git itself in years at this point. But it's also true that if you like using git, and want to keep using it, that's fine! The wonderful thing about the interop here is that I can use jj, and you can use git, and we're all good.
> I'm a little curious why the developers would decide to re-implement something from scratch only to end up with an "alternative" that is mostly compatible
Realistically, with git's dominance, compatibility is the only way that you get people to actually try out your thing. I know I wouldn't have given it a shot unless I could use it with a git repo.
> or add new features
I mean, there's two things here: one of which is, jj does have new features. I described the ability for a jj repo to exist in a conflicted state upthread, for example. jj undo is a godsend. But at the same time, at the end of the day, when you're trying to manipulate a graph of changes, there's always some way to end up in the same end state with git, because, well, you're trying to interoperate. So you can sort of handwave away a lot of features with a kind of "well I can do that in git via <totally different means>", and sure, that's true in a sense, but tools affect the way you work. I'm much more effective with jj's model of the world than I was with git's, even though I didn't actively feel that pain until I tried jj.
> It really wasn't until I learned magit
Ah, you use magit! So yeah, like, jj is like magit in the sense that it lets you interact with a git repository in a different way than the standard tool. And that's useful. I never would have used magit because I don't use emacs. (and there are some folks trying to do "magit but for jj"...)
> But the way git works internally seems pretty great to me. Too often, criticism of git conflates the two.
I agree, in general. I do think that there are still good criticisms to be made, but a lot of it is uninformed. Just how things go.
> Ah, you use magit! So yeah, like, jj is like magit in the sense that it lets you interact with a git repository in a different way than the standard tool. And that's useful. I never would have used magit because I don't use emacs.
I also use magit and I was confused by the "advantages" that jj has over git. The nice thing about magit is that it doesn't hide git. What it does add is easier typing of the flags (using transient), completions of arguments like branch names (using Emacs), DWIM behavior depending on cursor position and region selection (especially for commit hashes). Also it has nice presentation of the information which acts like hubs for all the above.
I guest jj makes sense if you're using the cli directly. But with magit, every operation is only a few keystrokes. It is to git, what vim is to editing. And I could probably cobble something close with tig or lazygit if I switched from emacs.
With agents, my go to is now have multiple of the same repository and each agents must work on a separate one.
Preventing dirty workspace by solving the co-work problem to start with. merges are much more trivial than trying to make agents remember which branch or which folder it is supposed to work on. Disk space is cheaper than mental anguish and token usage.
What's your naming scheme?
REPO_NAME_0
REPO_NAME_1
REPO_NAME_2
REPO_NAME_3
https://git-scm.com/docs/git-worktree
Many folks aren’t aware that there is also an open-source, cross-platform desktop GUI application for Jujutsu called GG: https://github.com/gulbanana/gg
I mention it because while the jj command line interface is excellent, there are certain tasks that I find easier to perform with a graphical user interface. For example, I often want to quickly tap on various revisions and see their diffs. GG makes that kind of repository browsing — and certain squash operations — much more efficient for me.
If you’re interested in more information about GG, my co-host and I talked about it in a recent episode of the Abstractions podcast at about the 8:17 mark: https://shows.arrowloop.com/@abstractions/episodes/052-they-...
jjui TUI is incredible also
What is the current story for serving jj repositories over HTTPS? Git has https://git-scm.com/book/ms/v2/Git-on-the-Server-The-Protoco... . Does jj have something better, equivalent, or nothing (at this time) at all?
Unless you're at Google (or writing your own backend), jj talks to a git server, and so you'd just do exactly that: host a git repo over https.
jj is genuinely great and I think it deserves way more adoption than it has right now. The mental model is so much cleaner than git, undo actually works the way you'd expect it to, and working with stacked changes feels natural instead of that constant low-grade anxiety of actually breaking something. It's probably the best frontend for version control that exists today.
For the last few months though I've been thinking a lot about what you said at the end there. What if version control actually understood the code it was tracking, not as lines of text but as the actual structures we write and think in, functions, classes, methods, the real building blocks? A rename happening on one branch and an unrelated function addition on another aren't a real conflict in any meaningful sense, they only look like one because every tool we have today treats source code as flat text files.
For enhancing this kind of structural intelligence I started working on https://github.com/ataraxy-labs/sem, which uses tree-sitter to parse code into semantic entities and operates at that level instead of lines. When you start thinking of code not as text there's another dimension where things can go, even a lot of logic at the comiler level with call graphs becomes useful.
Has anyone found a good code review workflow with `jj`? My problem is that GitHub doesn't remember the content of the last reviewed SHA, so every time I push a new change from jj, reviewers lose the delta from their last reviewed commit.
To work around this I stopped moving revs (squash/rebase) after review starts, which creates awkward local graphs if I have to merge from main for merge conflicts. Graphite works but it's $$$, and phabricator/reviewable/gerritt all have significant onboarding hurdles.
I wonder if the recent github stack system could help with that (https://github.github.com/gh-stack/guides/ui/)
waitlist only :/ waiting to get in so i can test it out!
github added support for this in the last six hours - https://github.github.com/gh-stack/faq/#will-this-work-with-...
The last reviewed sha is generally available on the PR page (not the changes page) when you force push. There should be a changes since review link somewhere near the push.
When reviewing, you can also mark individual files as reviewed (useful in larger reviews where you're incrementally reviewing files). If you do this, only files that are changed will be expanded when you come back to the review.
yeah, this is where my complaint is - github shows a "compare" button when I force push, but it's not linked to the PR review. The "file changed" status is often not granular enough - if I change one line, force push, the entire file gets marked as unreviewed. the github "changes since your last review" is commit-based, not sha-based.
what I want is something like graphite/gerritt/google's critique where after each force push, the review page shows only the true delta between the two shas (similar to the "compare" button in github, bu as a reviewable unit).
poked around on github, doesn't look like the stacked PR feature has affected this "changes since your last review" selector yet :(
https://tangled.org does exactly what you want :)
have a longer write up here: https://blog.tangled.org/stacking but we have "interdiffs", to view a delta from previous review. pull-requests advance in the form of immutable rounds much like the patch workflow on email.
we have been interdiffing and stacking for a while on to dogfood, sample PR: https://tangled.org/tangled.org/core/pulls/1265/round/1?diff...
I haven't noticed any significant change in my workflow needed to accommodate this, but it might be because I've always used rebase rather than merge. `jj rebase -d main` will put my current branch on top of the main branch, and fixing conflicts in `jj` is a breath of fresh air compared to git.
The problems with jj that led me to abandon are:
- All of everything good about it breaks down the instant you want to share work with the outside world. It's git on the backend! Except there isn't any concept of a remote jj so you have to go through the painful steps of manually naming commits, pushing, pulling, then manually advancing the current working point to match. And in doing so, you lose almost everything that gives it value in the first place - the elegant multi branch handling, anonymous commits, the evolog. Even if you want to work on the same project on two machines your only choice for this is without breaking everything via git is to rsync the folder. Yes, you can write alias to do all this like git. I might as well use git if I can't use the nice features.
- All files automatically committed is great until you accidentally put a secret in an unignored file in the repository folder. And then there is no way to ensure that it's purged (unlike in git) - the community response as far as I can tell is "Don't do this, never put a file that isn't in gitignore".
- And adding to .gitignore fails if you ever want to wind back in history - if you go back before a file was added to .gitignore, then whoops now it isn't ignored, is all part of your immutable history, and disappears if you ever switch off of that new commit state.
Anything new or special in jj that allows me to work with large binary files simply? To me, this is still unsolved in terms of providing an elegant solution (e.g. things like Git Large File Storage (Git LFS) are awkward).
Not yet. It's desired to do something better here, but there's no active development that I'm aware of right now.
(LFS support is in progress though)
I've heard that jj has support for non-git backends? Can anyone comment on how difficult it would be to add support for another backend, any docs or examples?
I have a project[0] that does the large file thing well, but is missing most of the version control porcelain. I've been looking for the path of least resistance to integrate it into something with a larger user base.
[0] https://github.com/gotvc/got
To add a new backend, there's a trait that you implement for your backend: https://github.com/jj-vcs/jj/blob/713a0d0898448392d38fdcbaba...
I suspect if you came by the jj discord, folks could help you with more detail than that.
Thanks for the link.
It looks like this treats files as blobs just like Git, and trees as single objects which fit in memory. Assuming that is a correct understanding, this core abstraction would need to change to handle large files and directories well.
All the well known version control systems do this though, and it simplifies the system significantly. It's the right model for source code, but it doesn't translate well to arbitrary data.
Yes, it will require work to do large files well. But there is general interest in upstream in having that, there's just nobody driving the work at the moment.
Seems interesting, but tbf, I only really use a small subset of git commands as it is... I've tended to practice a few workflows that just reduce the overhead and risk for conflicts.
For those, who want to/need to keep some files uncommitted, the workaround I found is to put gitignore into some nested directory:
jj won't track those files under ./junk/
Also might be relevant for claude, since it wants to put its settings into the repo itself as `.claude/`:
For some more common files, I use global gitignore file as
I run jj in colocated mode so I put stuff in .git/info/exclude if I want it ignored but not part of the main .gitignore
You can also set snapshot.auto-track to tell it not to track certain files.
Another option is to make a branch with the files that you want to keep around but not push (e.g. stuff specific to your own tooling/editor/IDE), and mark that branch as private. Private commits (and their descendants) can't be pushed.
You then make a merge commit with this branch and main, make your changes, etc. You will have to rebase before pushing so that your branch isn't a descendant of the private commit.
This will involve more work, but it has the benefit that you're actually version controlling your other files.
I love jj, but mostly I use jjui.
I would like more uniformity in the way jjui handles commands when you are viewing changes vs when you are viewing files within a single change.
Often I just make my changes and leave it there without `new`, as I am not sure which file should go in a single commit. I just leave it there and I interactively commit later.
For me to use `new` more, I would like the ability to also simply be able to get a tree view of all changes, which contains file which contains file changes, so I could can have marks that span multiple changes and then either `split` or `commit` or `squash` the change, idk if there is a good word for it. Right now I can only mark within a single change, and I lose it once I navigate up.
I use jj fairly regularly and I'm trying to understand what your approach means, but having difficulty following what you want to acheive here. Seems like you're using ambiguous language that isn't aligned - wdym by marks?
In jjui, you can select multiple changes to squash/rebase by pressing space bar before pressing S or r. Is that what you mean?
yes exactly, pressing space bars to select them. I referred these selection as marks.
Has anyone tried both jj and gitbutler extensively yet? Both seem to have some interesting new ideas on top of git.
I was hoping someone else had written about it here.
From my knowledge there are three different takes on git being worked on which looked interesting. - JJ - GitButler - Zed
Zed version system doesn't have that much public info yet, but they wanted to build a db for storing code versions for AI agents. Not sure if this is still the direction, and I'm a bit skeptical, but interested to see what they come up with.
Even though git works well enough, I'm certain there will be another preferred way at some point in the future. There are aspects of git that are simply not intuitive, and the CLI itself is not up to standard of today's DX.
I'm still struggling most with the fact that my day-to-day work involves a git first platform like GitHub.
Although jj as a vcs system, it does feel better, working with git through it still feels like a chore, but to be fair I only gave it a day before going back to git.
Does anyone have any good resources on how to augment a git flow through the lens of a git hosting platform to work smoothly and still reap the benefits of jj?
I'm not sure what the hangup is, TBH. I use `jj` with GitHub and ADO every day, and it works great.
What specific challenges are you running into that make it feel like a chore?
Hmm, it was a while back so now I'm struggling to recall, but I remember feeling like I'm going against the grain of easily using GitHub. I followed this exact tutorial at the time and it looks like there are now sections on how to work with GitHub.
Perhaps I need to force myself to commit for longer...
What problems are you running into?
The jujutsu docs have a page for this - it has everything I needed.
https://docs.jj-vcs.dev/latest/github/
Here are some notes on Jujutsu from Evan Martin that I found interesting/useful:
https://neugierig.org/software/blog/2025/08/jj-bookmarks.htm...
Nobody is asking for a git replacement? I keep seeing these posts and I don't know who wants them.
jj has almost 30,000 stars on github. You might not be looking for a different git ux, but plenty of people are!
You could have said the same thing about subversion.
Subversion is a fine VCS. But git offers a better approach with being offline-first and decentralized. It also makes merging branches a lot easier.
I don't know enough about jj to praise it, but I don't think git will be the last VCS that will become widely popular.
I wasn't asking. I loved git.
But then after trying jj, I wrote this tutorial because I love it even more.
The feeling I get with jj is it is almost like people are trying to convince others why jj is superior instead of just sharing that jj exists and let others decide themselves. It seems like every few months there is a jj post on HN which devolves into the narratives of “git works for me” and “but jj is better at X”.
Where X = something I’ve never wanted to do
Exactly the same thing happened when git showed up (alongside the same things for bzr, darcs, hg, etc. too!)
I definitely am. Haven't touched git in over a year. If there was just a single feature to point at where jj meaningfully improves on git, I think it's: `jj undo`.
It is a universal undo command. It works for every change in your repository. You don't need to memorize/google/ask claude how to revert each individual kind of operation (commit, rebase, delete branch, etc.). You try a jj command, look at your repo, and if you don't like what you see, you `jj undo`.
The biggest downside for me is that no longer have the necessary expertise to help coworkers who get themselves into trouble with git.
Agreed and not only that but for any comment on this page which raises a valid exception; there is a cadre of jj devotees ready to counter them if and where they think they can. It feels like someone had a some financial state in brainwashing the world to leave git for jj.
I'm asking for a github replacement, alas it might come from github itself with stacked PRs.
git is good, but jj is good, too. nobody asked for a better CVS either, until someone did.
are you conflating github and git? I'm just confused as to how jj is a replacement to github
I'm not, github sucks, git is ok. Stacked PRs are what jj makes easy but is completely broken in github and as of today they're releasing a 'stacked prs' product which only makes jj better value when working with github vs git.
Can jj iterate through a list of repositories and clone them all to local storage?
It isn't very hard to make a bash script to do it, but I have about six github repos, all of which frequently need to be put on a new machine. that kind of functionality would be cool to have out of the box.
That’s not really a script but a basic one liner.
No, but to be honest, why would you want it to? That's... well, you already showed the trivial one-liner.
Are you accessing these boxes via ssh or using them directly? If it's via ssh, I'd expect that you would already be using the clipboard for copying the names of them rather than typing them out manually, at which point copying `git clone <a> && git clone <b> && ...` would achieve the same thing.
So glad to see this on HN, here to support it. JJ is amazing, the hardest hurdle was not the tool but the toolchain and ecosystem when I started ~ 2 years ago. It's grown rapidly and is incredible to see the community grow!
Still not finished unfortunately :( Guess Steve is currently busy writing the next big thing in programming languages (https://steveklabnik.com/writing/thirteen-years-of-rust-and-...) ?
It hasn't been touched in 3 months: https://github.com/rue-language/rue .
To be honest, while Steve's tutorial was what got me interested in jj, other tutorials were better in actually helping me understand it.
This is why I'm glad we have many of them! Not everyone is going to resonate with my writing style.
Yes, but it's a sad state of affairs that the official jj docs point to your tutorial, which is incomplete (and IIRC, more incomplete than in the past - I think you took down some topics).
Nope, I haven't taken anything down. I have merged in some contributions from others though, it's actually grown (Manish, iirc, contributed the Gerrit section).
Oh, OK. Must be a bad memory. I often go to your tutorial looking for something I could have sworn I read over a year ago and not find it. I must have read it elsewhere.
Nope, I have had zero time for personal projects lately, Rue is on the backburner until I do.
I've been busy at https://ersc.io/ (and spending time with my family, and playing Marathon...)
1) Are there any benefits in replacing a personal private project git repo with jj? I usually just commit straight to master in order to just not to lose the changes and be able to roll back. 2) Are there any pros in doing so in a project with many large binary files (3d models, images)?
You don't replace. jj is backed by git anyway.
You can switch an existing git repo to jj by using:
jj git init --git-repo my-repo
I think (but CANNOT PROMISE) that just removing the .jj folder will bring you back, but definitely take a backup of .git before you try this in case I’m wrong.
No that is correct when in colocate mode (which is the default mode). Simply removing the .jj folder will "de-jj" the repo entirely, but will leave you in a headless state. Simple to fix with a `git switch` though.
If you are _not_ in colocate mode, the .git folder is located _inside_ the .jj folder. So worth checking!
Okay but why would they use jj when they do trunk-based dev
I do trunk based dev. My colleagues prefer git. I still prefer to use jj.
Please update the "Stacked PRs" workflow article Steve...
I'll get there... someday...
For those who switched, what is the main selling point that made you stay with Jujutsu?
For me, can do anything complex in terms of history rewriting without ever shooting myself in the foot (worse case just `jj undo`).
And the UI is pretty intuitive so don't really have to search for a solution, usually the command to use is obvious (tho I guess now you could delegate the UI to an LLM to ask it to give you the right incantations).
If it ain't broke...
I think there are a lot of things that work well but still get improved versions. For example, grep works well but there are plenty of better versions these days.
Really? I use grep multiple times per day, works great, I've seen no need to change. Same with git.
As a Mercurial user, git was broken from day 1 :-)
Interesting to see more Rust CLI tools gaining traction. The undo-everything model is compelling. I've lost work to bad git rebases more times than I'd like to admit.
> $ cargo install jj-cli@0.23.0 --locked
I won't install Rust just to test your software. Make a debian package like everyone else.
I believe the full docs page does indicate that there are binaries to install via popular package managers [1]
[1]: https://docs.jj-vcs.dev/latest/install-and-setup/
I did check that page, as far as I can tell you still need to run Cargo which I don't want to do because I don't care about Rust.
I'm not complaining for the sake of complaining, I'm saying if they want to play in the Big Boy leagues, they need to do things right.
You do need Cargo to build from source.
If you're on Arch, gentoo, or openSUSE, you can use the package. It is true that Debian has not packaged jj yet.
It'll get there, and it's fine if you'd rather wait until things are more mature.
It's available in Debian sid, although a few versions behind: https://packages.debian.org/search?searchon=names&suite=all&...
You know, I went and searched before I posted. I wonder why it didn’t come up! Thanks.
Thanks! I hope I didn't come off as too dismissive, I'm hearing a lot of good things about Jujutsu. As a developer though, I've never wanted to build from source (probably in the minority on that front).
Nah, you're right that installing a compiler toolchain to build a project is a pain in the butt if you don't already have it. It's a legitimate thing, but it does mean that you won't be adopting more cutting edge tools, which is also just fine! I've done the same with projects built with tools I don't have installed too.
Immediately after that line:
> If you're not a Rust developer, please read the documentation to figure out how to install things on your platform
Rather selective reading we have here, don't we?
I did! No apt install jujutsu. I also did 'apt search jujutsu'.
Don't ask me to care about yet another language's package manager, I already know way more than I wish to.
I was going to write a big long comment, but honestly it boils down to this:
Whatever git's practical benefits over SVN and CVS back in the day (and I can go into the weeds as a user if someone wants that), git was the DVCS that took over from the centralized VCS's of that era.
There is nothing in jj, pijul, or Bram Cohen's thing that is anywhere near as dramatic a quality of life improvement as going from VCS to any DVCS. And dramatic improvement is what is needed to unseat git as the standard DVCS.
I mean, if you're not doing something so important[1] that it adds a letter to the acronym, it's probably not the next new thing in version control.
1: I originally wrote the word "novel" here. But it has to be big-- something like guaranteeing supply chain integrity. (No clue if a DVCS can even do that, but that's the level of capability that's needed for people to even consider switching from git to something else.)
Nope, git is good enough, and is the global standard. We don't need more new VCS.
Might be true, but Subversion was also good enough and a de-facto standard.
Great opinion. Have you tried it? It takes just 30 minutes to wash all the Stockholm syndrome off of you.
Not necessarily. I used jj for a couple of weeks and found it to be a complete waste of time.
For an advanced user, it did not offer anything I cannot quickly solve in git. Which is probably the wrong thing to optimize in the first place, because even though I frequently rewrite history and split commits during normal worklfow, it takes so little time that improving something else would yield greater returns.
We (not royal we) don't usually go out of our way repeating negative experiences with these tools, so you build a very skewed view of their adoption.
Their opinion is great, why do you feel like you need to counter it with [but, but jj is for the clean masses, not the unclean users].
What happens when jj introduces this native repository format - break compatibility with all the popular git hosts?
If jj is so great now and works with git as a backend, it’s tough to imagine why it’s worth pursuing a native and presumably incompatible backend.
Nothing will break. You just keep using the git backend if you want to keep being compatible with git.
> it’s tough to imagine why it’s worth pursuing a native and presumably incompatible backend.
Well, there's no active work on a "native" backend. There are basically three backends right now:
1. the git backend
2. A simple backend used for tests, you can think of it almost like a mock backend, you wouldn't use it for real work, but it's still useful as part of the test suite
3. the piper backend at google
There's not a lot of reason for anyone to produce another open source "native" backend, because 99% of open source projects use git.
That makes sense, thanks for the reply. For some reason I was under the impression that there was an active drive towards a backend that was not git.
You're welcome. I think it's natural to assume that.
its almost impossible for me to tell if this better or worst than git i read few things about jj, and my conclusion
i think git is good (not good enough, good just good, or really good) and unlike shells, i cant think of a reason to have mass migration to it
people use zsh because apple choose it, and pwsh because microsoft settled on it, on linux i am sure we can do better than bash, but it good enough and nothing justified replacing it (that being said, all 3 OSes should have settled non nushell)
in summary, if we couldnt replace bash on linux, i dont think anyone can replace git, git as an scm tool if far better than bash as a shell
I may be reading too deeply but it sounds like you haven't even tried it. You should! Its really hard to live without it, once you feel it in your fingers.
> very few user would really care about this difference
Oh the user absolutely does if that user creates lots of branches and the branches are stacked on top of each other.
I get your feeling though; sometimes in my own private repositories I don’t bother creating branches at all. Then in this case jj doesn’t really make much of a difference.
Reading threads like this and the GitHub stacked PRs just makes me feel like an alien. Am I the only one that thinks that commits are a pointless unit of change?
To me - the PR is the product of output I care about. The discussion in the review is infinitely more important than a description of a single change in a whole series of changes. At no point are we going to ship a partial piece of my work - we’re going to ship the result of the PR once accepted.
I just squash merge everything now. When I do git archeology - I get a nice link to the PR and I can see the entire set of changes it introduced with the full context. A commit - at best - lets me undo some change while I’m actively developing. But even then it’s often easier to just change the code back and commit that.
You're not an alien: this is the workflow that GitHub encourages.
It's just that not every tool is GitHub. Other systems, like Gerrit, don't use the PR as the unit of change: they use the commit itself. And you do regularly ship individual commits. Instead of squashing at the end, you squash during development.
Thanks for explaining that. Having a bit of a (dim) lightbulb moment now. I’ve never used Gerrit - just GitHub and GitLab and Forgejo. So I assumed the PR/MR model was more or less universal. But if smaller development commits are being squashed into the shippable/reviewable unit - then the focus on commits makes a lot more sense.
You're welcome! It is one of those "do fish realize they're wet" kind of things, tools can shape our perception so strongly that you don't even realize that they're doing it!
You could agree that the PR is the meaningful unit for shipping, but push back gently that for agents working in parallel, the commit/changeset level matters more than it used to because agents don't coordinate the way humans do. Multiple agents touching the same repo need finer-grained units of change than "the whole PR."
Could you elaborate a bit more on this? Curious what your workflow looks like. Is this multiple agents running on the same feature/refactor/whatever unit of work? For concurrent but divergent work I just use a git worktree per feature. And I think I only ever have a single agent (with whatever subagents it spins up) per unit of work.
Is there a concise introduction / overview of jj? I've read 8 pages of this link and the author is still in preambles and considerations... Not my favourite style of article!
I wrote a brief intro to Jujutsu here: https://mkaz.blog/code/jujutsu-vcs
You have a DAG of "changes" to the repo state. Each change has a stable ID, you can modify its contents or description without changing the ID. There's always a "current" change checked out, JJ automatically snapshots your edits to files into this change. JJ provides tools to edit changes, describe them, group them into named branches (bookmarks), reorder them, split them apart, etc.
JJ defaults to being backed by git. Each change has a corresponding git commit. When you edit the contents of a change, jj makes a new git commit & points the change at that new git commit. JJ is never actually amending or editing git commits, it's using git as a content-addressed data store.
That's the mental model. It's like git with a lot of accidental complexity (staging area, stashes, commit ID instability) removed.
There are a few ways you can work with this model. I like the following:
When I want to start new work, I fetch any changes from upstream `jj git fetch`, then make a new change after the current `main` or `master` bookmark: `jj new main`. Then I start writing code. When I want to commit it, I type `jj commit` and write a description. If I find I want to make an edit to a previous change, I edit my working copy and interactively squash to that change ID with `jj squash -i -r <change_id>`. When I'm ready to push those changes, I name the branch HEAD with `jj bookmark create`, then push it with `jj git push -b <bookmark_name>`. If there are review comments I squash edits into the appropriate changes or add new changes, and move the bookmark to the new head with `jj bookmark move`. If I want to merge two (or more) branches, I use `jj new <branch_1_name> <branch_2_name> <...>` to make a new commit with those branch names as parents. If I want to rebase some changes I use `jj rebase`. JJ doesn't care about conflicts, I fix them after a rebase has completed, instead of in the middle.
That sounds a bit faffy. In solo or small team work git is often git pull, edit code, git commit -a, git push.
With jj you have to fetch, start new space off a bookmark, edit code, commit it, update the bookmark and finally push?
I can be a bit verbose, it's true :)
You've got some decent replies, but if you give me some background, like how comfortable you are with git, how much you care about certain details, I'd be happy to respond here with something more concise.
jj sounds awesome. I think I’ll give it a shot.
But I found this article a bit long winded and ended up asking an LLM about it instead.
am a big fan, just started using it a few days ago
I tried jj for a few months. It was fun to learn a new thing, but I haven't had a single case of "wow, this would have been a pain with git". Then I went back to git (it's been 6 months now) and I haven't had a single case of "this is so painful, I wish something better existed".
So it felt like the XKCD on "standards": I now have one versioning system, if I learn jj I will have two. What for?
Don't get me wrong: it's nice that jj exists and some people seem to love it. But I don't see a need for myself. Just like people seem to love Meson, but the consequence for me is that instead of dealing with CMake and Autotools, I now have to deal with CMake, Autotools and Meson.
EDIT: no need to downvote me: I tried jj and it is nice. I am just saying that from my point of view, it is not worth switching for me. I am not saying that you should not switch, though you probably should not try to force me to switch, that's all.
> Then I went back to git (it's been 6 months now) and I haven't had a single case of "this is so painful, I wish something better existed".
The core issues are: how long did it take you to get there, how many lucky decisions did you have to make to not run into git footguns, and how many other people accidentally made different choices and so have very different experiences from you?
What you're saying is that other people may find jj easier for them, right?
I am fine with that. I am just saying that the "you should use jj, you will finally stop shooting yourself in the foot regularly" doesn't work so well for me, because I don't remember shooting myself in the foot with git.
For me, it was kind of the same. I used jj. Really liked it, but did not find it all that much better than git.
Then, for various reasons, I switched back to git.
By day 2, I was missing jj.
Stuff like "jj undo" really is nice.
JJ might be good (this article couldn't convey why in the "What is jj and why should I care?" page) but it's not 10x better than git, so it will likely die. Sorry, nothing personal, Mercurial/hg was a little bit better than git and died too. Network effects.
What has a change is ast-based version control.
You adding a feature to a function that uses a struct I renamed shouldn't be a conflict. Those actions don't confliuct with each other, unless you treat code as text - rather than a representation of the logic.
Ending merge conflicts might make a new version control 10x better than git, and therefore actually replace it.
> JJ might be good (this article couldn't convey why in the "What is jj and why should I care?" page) but it's not 10x better than git, so it will likely die. Sorry, nothing personal, Mercurial/hg was a little bit better than git and died too. Network effects.
The difference is that I can (and do) use `jj` with existing git repos today without needing anyone else using the repo to change what they're doing. There's no need to replace something when it can exist alongside it indefinitely.
Unless you need to work with a repo that uses submodules or lfs.
Did you confirm that the network effects are applicable here before posting that?
Nope. As mentioned "What is jj and why should I care?" didn't cover it and I timeboxed jj advocacy to the page that says it will get me to care.
Call me crazy, but jj is more confusing than git.
To be honest, JJ is dick in Chinese, literally.
https://chinese.stackexchange.com/questions/31646/is-jj-slan...
I use the CLI often enough, but still most of my time is in a GUI. It just makes the diffs easier, the flow simpler, etc.
As such, I wanted to break into jj via the GUI, and only adopt the command line after I could visualize the concepts and differences in my head.
Alas, the GUI I tried - a VSCode plugin - did more to confuse me than to help, and it made it very easy to catastrophically rewrite the history by accident. I tried another UI and it kept failing and leaving me with cleanup work. I couldn't find a third UI that looked promising.
So, I gave up. One less jj user on the planet - no biggie. But I really wonder if it would be worth the effort for some of the jj pushers to try to get a proper UI in place - I bet I am not the only one that likes to learn visually.
Unfortunately OpenAI's models really suck at JJ, while, on the other hand, Opus 4.6 is much better at it, probably due to a later training cut off.
I followed steve's excellent tutorial about two months ago, and haven't looked back. I have never felt so competent at git (vcs rather) as I do now. jj is so much simpler and easier for me to reason about.
Does jj have partial clones of remote repo?
jj does not have partial clones, but it does have sparse checkouts.
Thank you for the info.
Sparse checkouts from remote repo directly or do I need to clone it in whole locally and only then can do a sparse checkouts into some work directory?
I'm 99% sure it's "clone the whole locally and then sparse checkout from there".
this looks pretty interesting.
16 year-old me would have been very impressed by this!
Is that a compliment, or the opposite?
A bit of both. I guess it's nice, but nothing I actually care about.
I had a similar thought: there surely are lots of young folks who will be all excited with this (I was back in the CVS/SVN days when git appeared).
But nowadays I'm extremely lazy to attempt to learn this new thing. Git works, I kind of know it and I understand its flow.
Same here, I’m not experiencing so much friction to justify looking for an alternative
We still have some repos in Subversion and most things in git. It’s still exciting for every repo we get migrated out of svn. That’s a high bar to cross if we’re talking further improvements compared to git though.
I was 38 or 39 when I found jj.
See? You weren’t even over 50! Young whippersnappers…
>You can request to not use the pager by using jj st --no-pager, or if you hate the pager and want to turn it off, you can configure that with
In one feature they can’t help themselves from calling it two different things already.
Why do this? Why can’t the very clearly smart people making things step 1/2 step outside themselves and think about it like they are the users they want?
Earlier they talk about the native format and how it isn’t ready… so that to start you need
… but… if they’re planning a native format that makes no sense as a command. It would be ‘jj native init’ later?
Early planning keys/plans imo but those rarely change so as to not accept your early adopters.
These seem like small things but to me it’s a warning.
1. Pagination with a pager is a reasonable default. See `git log`.
2. The native format would be `jj init`. For precedent, see how uv dealt with its pip compatibility: `uv pip install` was obsoleted by `uv add`.
1. No one with good vision would give a single feature two names. It’s dumb. Here is our pager feature. Cool, how do I access it? Oh you set the ui.paginate options of course!!
2. It’s almost like we have some established ways to denote arguments that are pretty popular… ‘jj init —-git’ for example? By using ‘jj git init’ I would expect all of the git compatible commands to be be ‘jj git xxx’ because that is a reasonable expectation.
This is a problem with the voodoo. These obscure nonsense commands only makes sense when you are accustomed to them. If there’s no reasonable expectation that you could just figure it out on your own. Go on vacation and come back and be surprised when you forget the voodoo. Not to mention that every tool has to have its own unique voodoo.
Almost like the professional world has figured out that made by software engineers for software engineers will never be popular. And then engineers don’t understand the effects of why you might want tool to be intuitive and popular.
You're right that, looking solely at `init`, a flag could make sense to choose the backend.
The bigger picture here though: `jj git` is the subcommand that prefixes all commands that are git specific, rather than being backend agnostic. There is also `jj git clone`, `jj git fetch`, `jj git push`, etc.
For a different backend, say Google's piper backend, there's `jj piper <whatever>`.
This means that backend specific features aren't polluting the interface of more general features.
>There is also `jj git clone`, `jj git fetch`, `jj git push`, etc.
If the compatibility isn’t automatic… why would I bother with jj commands here at all? “Git with extra steps”
The on-disk repository compatibility is automatic. But if you're trying to fetch something via a specific protocol, you use the command for the protocol you want to use.
There is no extra step between `git push` and `jj git push`, they're both one step.
I meant the extra step being why would I bother with jj if I’m having to specific gut inside of jj?
The issue is pretty obvious to me. GIT is the standard and that likely won’t change for some time. So if jj makes my git life better, awesome, but it’s just a wrapper and I need to know all the git voodoo now with jj voodoo on top, I don’t quite get it.
If you're happy with git, you should keep using it.
Now that Steve is part of a GitHub competitor to push jj, I see all these posts as just sales pitches.
This tutorial predates his involvement with ERSC.
I am quite happy for anyone to use whatever tools they find to be good. I'm also happy for anyone to use jj with whatever server they want to.
It is right to be skeptical of me, but I hope to keep that integrity by continuing to talk about things that I believe are legitimately good, regardless of anything else.
Thanks for the reply Steve - I think it's only natural, and charitably unintentional :) But almost everything I've seen upvoted around the Internet from you has been about jj and being directly tied to east river source control... I think that's a reasonable framing. I can only hope me signalling this maybe changes something. While I'm not a fan of jj (I'd much rather Pijul were to eat the world), I think you as a person is really nice and always have been "for the community", but I can't shake this current framing!
I mean, most people seem to think I solely post about AI these days, so it’s kinda funny to run into someone that feels otherwise!
I’m not sure why I’d stop posting about a project I’ve been passionate about for years, just because my job is adjacent to it.
You can't say it's adjacent to it, when your job directly involves the technology. You'd stop posting because anything you say about jj could be interpreted as a sales pitch for jj, and a lot of people can be turned off by that. That's one reason. Our lack of creativity is not proof of no more.
But I don't think "stop writing" is the only strategy to jump on...
Or maybe I'm just extremely unlucky to have only caught these kinds of posts and gained this framing! Totally possible.
OK I read it, I'm not interested, git does exactly what I want.
That's sort of where I've been stuck with with jj.
Maybe someone can convince me otherwise, but to me it hasn't felt sufficiently better than git to justify bothering re-learning this stuff, even if it's relatively easy.
Git wins by not needing to be replaced badly enough. Latley you can just ask an agent to "amend the last commit" so even that is being abstracted away.
We all need to give ourselves a push and finally make the next step in version control. Github, Google, Microsoft, Meta (did I forget anyone relevant? Probably) should just join forces and finally make it happen, which should not be a problem with a new system that is backend compatible to Git. Sure, Github may lose some appeal to their brand name, but hey, this is actually for making the world a better place.
The new solution is better. It’s so good we must get all the big players to mandate its usage.
If ur making an appeal on a forum like this u could have gone with ur favorite feature, or anything else really.
It is not about starting over, like moving from CVS or Subversion to Git. jj is backend compatible to Git, so nothing really had to change on the backend.
It's just that although Git was created by Linus Torvalds it is not perfect and could be more beginner friendly. But efforts to improve this should be concerted, not individual efforts.
And it does not have to be jj. I just think there is room for improvement, and not to alienate old farts it could be called GitNext, GitStep, GitFlow or similar to emphasize that is still is just Git, only with an improved front end.
Maybe Linus Torvalds himself should start the initiative.
Linus? Too tired of the open source community to risk having to deal with it more. Hasn't released anything since 2005, he just drifts on the waves. So sad he doesn't see the human energy wasted on his projects, and doesn't move them into the modern era, where compatibility with the past can be dropped in favor of a much tighter feature set, while also coming free of C/C++. In short: don't count on Linus, he's been a coward, he's too comfortable leading from the back.
Every time I see a statement like this I wonder what specific features of git that people feel like are terrible enough that it’s time to completely start over. Besides “the UX is kinda shit and it’s confusing to learn”, which there are many solutions for already that don’t involve reinventing a pretty good wheel.
Right.
How we got git was cvs was totally terrible[1], so Linus refused to use it. Larry McEvoy persuaded Linus to use Bitkeeper for the Linux kernel development effort. After trying Bitkeeper for a while, Linus did the thing of writing v0 of git in a weekend in a response to what he saw as the shortcomings of Bitkeeper for his workflow.[2]
But the point is there had already been vcs that saw wide adoption, serious attempts to address shortcomings in those (perforce and bitkeeper in particular) and then git was created to address specific shortcomings in those systems.
It wasn't born out of just a general "I wish there was something easier than rebase" whine or a desire to create the next thing. I haven't seen anything that comes close to being compelling in that respect. jj comes into that bucket for me. It looks "fine". Like if I was forced to use it I wouldn't complain. It doesn't look materially better than git in any way whatsoever though, and articles like this which say "it has no index" make me respond with "Like ok whatever bro". It really makes no practical difference to me whether the VCS has an index.
[1] I speak as someone who maintained a CVS repo with nearly 700 active developers and >20mm lines of code. When someone made a mistake and you had to go in and edit the repo files in binary format it was genuinely terrifying.
[2] In a cave. From a box of scraps. You get the idea.
To be fair the "shortcomings" that spurred it on mainly were the Samba guys (or just one) reverse-engineering Bitkeeper causing the kernel free license getting pulled, which caused Linus to say "I can build my own with blackjack and pre-commit hooks" and then he did, addressing it toward his exact use case.
It gained tons of popularity mainly because of Linus being behind it; similar projects already existed when it was released.
Mercurial was there, was better and more complete.
Too sad it didnt win the VCS wars.
When I tried both at that time hg was just really slow so I just adopted git for all my personal projects because it was fast and a lot better than cvs. I imagine others were the same.
I went with bzr mainly because it had an easy way to plugin "revision" into my documents in a way I could understand and monotonously increment.
hg was slow though I don't know how bzr compared as I was using it pretty light-weight.
Git is basically fine even though the verbs are backwards - e.g. you shouldn't need to name branches, commits should be far more automatic, but the basic mechanisms are fine.
GitHub is an abomination.
You might already be aware, but jj fixes exactly those complaints you have with git
Coming from mercurial (which is older than git), git doesn't understand a branch. Instead of a branch you get a tag that moves, which is very different. Too often I'm trying to figure out where something came in, and but there is just a series of commits with no information of which commits are related. Git then developed the squash+rebase workflow which softof gets around this, but it makes commit longer (bad), and loses the real history of what happened.
Git was not the first DVCS, there were better ones even when it was made. But Linus pushed git and people followed like sheep.
(I'm using git, both because everyone else is, and also because github exists - turns out nobody even wants a DVCS, they want a central version control system with the warts of SVN fixed).
> Coming from mercurial (which is older than git)
Git is older than mercurial by 12 days. Bazaar has git beat by about the same amount of time. The major DVCSes all came out within a month of each other.
> But Linus pushed git and people followed like sheep.
I don't think this is true. Until around 2010-2011 or so, projects moving to DVCS seemed to pick up not git but mercurial. The main impetus I think was not Linux choosing git but the collapse of alternate code hosting places other than GitHub, which essentially forced git.
way way back in the day I did some digging into all three - and picked bazaar for my personal projects. that didn't last long lol
the lack of a proper branch history is also the main pain point for me. but i disagree that noone wants a DCVS. having a full copy of the history locally, and being able to clone from any repo to anywhere else and even merge repos (without merging branches) is a major win for me.
If git would change two defaults, that would make me really happy:
Clicked on this hoping it would be the irc client, very disappointed!
For those in the know, how does jujutsu stack up to something like Darcs?
jj is still snapshot based.
Is it better for AIs? That’s the only reason I would care.
I've had mixed results.
Most models don't have a 100% correct CLI usage and either hallucinate or use some deprecated patterns.
However `jj undo` and the jj architecture generally make it difficult for agents to screw something up in a way that cannot be recovered.
I've gone all in on jj with a OSS framework I'm building. With just a little extra context, the agents have been amazingly adapt at slicing and dicing with jj. Gives them a place to play without stomping on normal git processes.
Try using https://github.com/danverbraganza/jujutsu-skill
This is enough of a command reference that with it, agents are able to work with jj pretty well.
The cli and a few concepts have evolved with time past the model's knowledge cutoff dates, so you have to steer things a bit with skills and telling it to use --help a bit more regularly.
I find it reasonably good with lots of tweaking over time. (With any agent - ask it to do a retrospective on the tool use and find ways to avoid pain points when you hit problems and add that to your skill/local agents.md).
I expect git has a lot more historical information about how to fix random problems with source control errors. JJ is better at the actual tasks, but the models don't have as much in their training data.
動画などでわかりやすいものが見たい
FWIW, it's a pretty decent fried fish chain in Chicago as well.
And chicken. And they have a few other things like Italian beef.
They’re branching out, too. We had one in our neighborhood in Houston before moving back here to Illinois.
This doesn't seem different enough to be worth the transitional cost, even if you don't need to actually move away from a git backend.
It is definitely worth a try. Just being able to squash changes to earlier commits without having to fiddle with fixups and interactive rebases is worth it for me. jj absorb is great too.