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.
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.
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!
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()'`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.
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'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.
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.
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.
Like `jj commit -m 'Feature A' file1 file2` then `jj commit -m 'Feature B' file3 file 4`
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.
The workflows are conceptually identical.
The autosync feature is really nice too, and you can store backup repos in cloud storage folders and auto sync to those as well.
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.
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.
Yes, but this is not backwards, the way you do it in git is backwards. =)
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.
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.
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.
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.
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.
> 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.
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)
i do this for example when i want to see a specific edit highlighted in my editor, it's a nice workflow i think
(You can also set up watchman and have that happen on every file change...)
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.
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.
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?
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.
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.
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.
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
Just in practice, anonymous branches end up feeling very natural, especially during development, and especially if your code review tooling doesn't require names.
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.
fragmede@laptop:(abranch)~/projects/project-foo$
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.
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...
https://github.com/git/git/blob/master/contrib/completion/gi...
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...
echo "something" >> foo.txt
git checkout foo.txt
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.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.
(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)
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?
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?
So just run:
jj bookmark track myname/somecoolfeature --remote origin
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 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.
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.
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.
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.
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 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.
[snapshot]
auto-track = '~glob:**/*'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.
GP is holding it wrong. If you don’t want to edit a commit, don’t ask to edit it. Use `jj new`.
> 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.
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.
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).
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.
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 think most people that have git experience don't know what a DAG is and have never used reflog.
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.
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.
This is such a killer feature to me. I'm not scared to start potentially gnarly rebases anymore because I can painlessly undo.
Using a version control tool shouldn't require much self discipline.
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!
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.
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"?
(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 ..@`
...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.
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.
- 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.
"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."
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!)
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):
@ vvxvznow
│ (empty) (no description set)
│ ○ uuowqquz
├─╯ foo
│ ○ uvlpytpm
├─╯ bar
◆ lwywpyls trunk
│ feat: blah blah blah
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!
@ vvxvznow
│ (empty) (no description set)
│ × uuowqquz (conflict)
│ │ foo
│ ◆ tswtuqmu
├─╯ chore: whatever
~ (elided revisions)
│ ○ uvlpytpm
├─╯ bar
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.
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.
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.
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.
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.
It’s possible to recover from these with git reflog, though.
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.
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.
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.
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.
jj new branch1 branch2 branch3
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):
jj bookmark move branch1 --to branch1+It can't be both intuitive and yet too complicated to show examples at the same time.
I feel very comfortable using git. Maybe jj is better, but not seeing is not believing.
if you don't care about them after accepting this realization... it's fine. git is good enough.
Classic denying the antecedent :-)
Just an expression of what the Missouri state's nickname says.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
> 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.
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.
> 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.
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.
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.
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-...
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.
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.
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...
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.
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 :(
- 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.
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.
I suspect if you came by the jj discord, folks could help you with more detail than that.
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.
(LFS support is in progress though)
mkdir junk
echo '*' > junk/.gitignore
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/`:
mkdir junk/.claude
bwrap ... --bind "$(pwd)/junk/.claude" "$(pwd)/.claude" ...
For some more common files, I use global gitignore file as # ~/.gitconfig
[core]
excludesFile = ~/gitconf/gitignore_global
# ~/gitconf/gitignore_global
.envrc
.direnv/*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.
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?
What specific challenges are you running into that make it feel like a chore?
Perhaps I need to force myself to commit for longer...
The jujutsu docs have a page for this - it has everything I needed.
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.
https://neugierig.org/software/blog/2025/08/jj-bookmarks.htm...
But then after trying jj, I wrote this tutorial because I love it even more.
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.
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.
git is good, but jj is good, too. nobody asked for a better CVS either, until someone did.
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.
for url in url1 url2 ..; do git clone $url; done
That’s not really a script but a basic one liner.I've been busy at https://ersc.io/ (and spending time with my family, and playing Marathon...)
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.
If you are _not_ in colocate mode, the .git folder is located _inside_ the .jj folder. So worth checking!
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 won't install Rust just to test your software. Make a debian package like everyone else.
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.
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.
> 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?
Don't ask me to care about yet another language's package manager, I already know way more than I wish to.
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.)
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.
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.
> 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.
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 itpeople 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
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.
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.
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.
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.
With jj you have to fetch, start new space off a bookmark, edit code, commit it, update the bookmark and finally push?
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.
But I found this article a bit long winded and ended up asking an LLM about it instead.
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, for various reasons, I switched back to git.
By day 2, I was missing jj.
Stuff like "jj undo" really is nice.
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?
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.
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.
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.
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.
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?
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.
$ 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.
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`.
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.
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.
If the compatibility isn’t automatic… why would I bother with jj commands here at all? “Git with extra steps”
There is no extra step between `git push` and `jj git push`, they're both one step.
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.
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.
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.
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.
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 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.
GitHub is an abomination.
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.
It gained tons of popularity mainly because of Linus being behind it; similar projects already existed when it was released.
Too sad it didnt win the VCS wars.
hg was slow though I don't know how bzr compared as I was using it pretty light-weight.
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.If ur making an appeal on a forum like this u could have gone with ur favorite feature, or anything else really.
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.
For those in the know, how does jujutsu stack up to something like Darcs?
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.
This is enough of a command reference that with it, agents are able to work with jj pretty well.
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.
They’re branching out, too. We had one in our neighborhood in Houston before moving back here to Illinois.