I've now seen a dozen of articles that explain that jj is wonderful and better than Git for everything. This tutorial is of the same kind. Now that I've read extensively about the good part, I'd be more interested by the bad and the ugly. Because my experience with jj was more balanced.
When I tried jj, I found a few pain points that made me return to Git. For instance, I was sharing a branch with a co-worker where we were just piling commits as soon as they were ready (after `pull --rebase` if necessary). Since jj doesn't have names branches, that workflow was easy with git and tedious with jj – even with the `tug` alias. The process in the "Tracking remote bookmarks" chapter of this tutorial still doesn't look nice to me.
Another pain point was that jj could not colocate with light clones, like `git clone --filter=blob:none`. Maybe that's fixed now.
I’m slightly confused. jj has named branches. They’re just called “bookmarks”.
Once you track the remote bookmark, `jj git fetch` will update your local one to match the remote.
Bookmarks are similar to branches (and I believe implemented as branches?) but aren't quite the same thing. In particular, you can't check out a bookmark, you can only check out commits. If you do something like `jj edit <bookmark>`, it'll resolve <bookmark> to figure out which commit it points to, and then check out that commit. If you create a new commit now, the bookmark won't automatically be moved along, because jj doesn't know if you want to add a new commit to the <bookmark> branch, or if you're trying to create a new branch that forks off <bookmark>.
Whereas `git checkout <bookmark>` puts you in a state where you've checked out the _branch_ and not the commit, which means if you create more commits, they'll automatically be added to <bookmark>. To create a branch forking off from <bookmark>, you need to first create a new named branch, then start adding commits to it. (There are other ways as well, but this is the most standard approach.)
The tradeoff with Jujutsu's approach is that it's very easy to create lots of lightweight spin-off branches (just `jj new <fork point>`, no need to come up with names first). But it's slightly harder to do the traditional linear approach where you're consistently committing in a straight line on a single branch, and then syncing that branch with another remote. This is because for each new commit you create, you also need to update the bookmark manually.
In the parent poster's case, it's probably complicated further by them working on the same branch as someone else. Assuming there's no rebasing going on, this shouldn't be too complicated, but it's another case where in git you would check out a branch, and `git pull` will automatically forward you to the head of the branch, whereas `jj git fetch` will just move the bookmark without moving you.
It sounds like the workflow that this person is using doesn't fit that well with how jj "wants" to be used. I believe there's been some talk in jj of a way to automatically update branches when creating a new commit, which would help somewhat, but I think it's also a natural effect of having built their workflow around git, and that flow just not quite working so well in jj.
> In particular, you can't check out a bookmark, you can only check out commits.
That's because "branches" in git are an imaginary concept, they are simply a label you give to a certain commit, nothing else, nothing more.
Moreover, in git this is further complicated by the fact that you may have branches with the same identical name on different environments (your local and remotes). So you can have, by any means, 4 `feat/foo` branches all being the "right" `feat/foo`, and you need to decide which one is the right one (we tend to default to some origin, but what if you have local work that is ahead of origin? maybe on two different computers? Meanwhile your branch is on a fork of a different origin. Git branches do nothing but complicate a simpler model where what matters is the commit, history and diff, yet we fixate on label names which are just conveniences).
At the end of the day branches do nothing but add complexity, I much prefer the jj model, bookmarks are just _local_ names you give to some edit, you move the bookmark manually, and it _may_ point to a git branch just to preserve the git backend.
> That's because "branches" in git are an imaginary concept, they are simply a label you give to a certain commit, nothing else, nothing more.
They're really not. The whole Git ecosystem makes a very clear distinction between branches and tags, even though they are virtually the same thing in the underlying data model.
For example, if you checked out a branch and then do a git commit, the branch name tag will be automatically moved to the new commit. Conversely, if you checked out a tag (or a random commit that is not the head of a branch) and then run git commit, the new commit will not get any tag.
Another example is `git rebase`, whose semantics only make sense if you consider that branch names refer to branches, not just to their latest commits, since they often modify many children of those branches.
> Moreover, in git this is further complicated by the fact that you may have branches with the same identical name on different environments (your local and remotes).
This is presented as more complicated than it is. If you are working with multiple remotes, you may end up in these complex situations, true. But Git is pretty clear about the names and identities of each of these. You have the local branch, `feat/foo`, which typically has as a default upstream the remote branch named `feat/foo` on a remote called `origin`; the local copy of that branch is the tracking branch called `origin/feat/foo`. If you have other remotes `remote1` and `remote2`, the tracking branches of those are called `remote1/feat/foo` and `remote2/feat/foo`. You will never want to commit directly to any of these tracking branches - instead, you'll create either separate local branches with each as an upstream(`feat/foo-remote1`, `feat/foo-remote2` - if you want different commits for each remote), or directly push from `feat/foo` to all of them if the intent is to keep them in sync (`git push feat/foo:remote1/feat/foo` or similar).
> the branch name tag will be automatically moved to the new commit
On your specific repository.
Of course, where else? All git operations only affect local branches, except git push.
The problem is that branches _aren't_ an imaginary concept in git. The easiest way to see this is by looking at the difference between `git checkout master` and `git checkout ae596fe`. The first one checks out a branch, but the second one checks out a headless commit. I can still do many of the same operations in both states (for example, I can create new commits based on the current one), but how the repository changes will differ depending on what I've got checked out.
I agree with you that this is mostly unnecessary complexity — branches being anonymous is one of the big reasons I like jj, and why I think it's so useful to people just getting started with version control, because it is a simpler conceptual model. But it's not correct to say that branches in git are imaginary.
Yeah, I don't get GP either, maybe their setup is different and more complex?
Another major caveat is that JJ doesn't work with Git LFS yet - though some progress is being made: https://github.com/jj-vcs/jj/issues/80
I ran into one thing with jj that I would say is pretty bad. I love it other than the way it bit me in this one case.
I have a repo with some code that generates a credential and writes the credential to a location specified in .gitignore so it isn't picked up by version control.
I used `jj edit` to roll back to a change before the credential path was added to the ignore file to make an unrelated change.
The result? jj instantly started tracking the credential and I didn't notice it before pushing to GitHub.
Fortunately I did figure it out pretty quickly, but that could have gone very poorly.
See also https://github.com/jj-vcs/jj/issues/7237.
https://github.com/jj-vcs/jj/discussions/3549 exists to simplify the tug workflow somewhat.
One thing that has bitten me a few times is jj randomly losing my changes. As in, I'll be working in Cursor, not run any mutating jj command (maybe I'll run jj status or jj log, but nothing else), and later on my changes are gone from my repository (often times with a message about a stale workspace).
I'm not sure if this is somehow related to my IDE, working in a huge monorepo, or something else, but it has been quite painful.
Aside from that, though, I do really like the flexibility jj provides.
My best guess: something (likely cursor) is running git commands that mutate the repo behind your back. I don’t use cursor so I can’t say for sure, though.
Your changes are almost certainly in the op log, even as just bare snapshot operations. Have you been able to search that and recover them?
(i've found jj undo quite robust, i'd be surprised if you ever actually lost work tbh)
If this happened with Git I feel confident enough to recover my work. I'm not familiar enough with jj, so usually when this happens I've used VS Code's timeline feature to recover my work.
`jj op` has `log` to show you the op log, `show`/`diff` which work like their non-op versions, and `jj op restore` which is sorta like `git reset --hard HEAD@{whatever}` but for your whole repo state
That's surprising. I have no real insight, but... was that a colocated repo? Is there any chance Cursor was creating/modifying the Git commits?
Yeah, it was a colocated repo. I've been using Cursor, but this also happened with VS Code at least once. I don't think that either tool should be doing anything with Git aside from maybe running git fetch.
In both cases you might have had a plugin that worked on Git - I think one is even part of default VS Code install (at least I don't recall having to install it)
> Another pain point was that jj could not colocate with light clones, like `git clone --filter=blob:none`. Maybe that's fixed now.
It's not
I was curious to give Jujutsu a try but while procrastinating I found light clones and can't go back. Hopefully they support it eventually and then I'll get to procrastinate on trying it again.
> Since jj doesn't have names branches
False. You need to call `jj branch set -r@ XYX` manually which can be a PITA but you only need to do that once you push. Or there is `jj git push --named XYZ=@` which moves the branch.
False. The branch subcommand has been removed. You have to use bookmarks now.
Ok, you need to call `jj bookmark set -r@ XYX` (or `jj b s -r@ XYX`), so what?
They're not quite the same thing as named branches in Git. In jj, a branch is a chain of commits. This chain of commits cannot be named. It's possible to create a bookmark, which is a name for a particular commit, and it's possible to use that bookmark to refer to a branch of commits (e.g. jj rebase's -b flag), but this is different from naming a branch like you would in git.
This is a subtle difference, and most of the time it doesn't matter at all, but in this case there is a difference between how bookmarks work in jj, and how branches work in git.
- [deleted]
Such nice community.. I think I’ll stick to git, even though Torvalds isn’t the nicest person in the world
I'm replying to a comment that starts with accusation: "False.". My point is that correct version differs just by a single word. It can be just a tipo in Disposal8433's comment, not "false".
Some people will look anywhere to justify their stubborn refusal to try new things. I wouldn't get too upset by it
I agree the parent is being a bit rude, but also, Linus hasn’t been involved in git for years, just fyi.
Don't base your opinion on some idiotic comments on HN, they are probably not part of a "community". Just try it, it's a legit super good tool.