I think that people mostly get a bad taste in their mouths because they try to use submodules for building multi-repo workspaces where a developer might need to commit in some/all of the repos. They're a bad fit for that problem, but it's mostly because that's not what they were designed to do.
I'd love to see the jj team tackle case #2, personally. I bet they'd do a pretty good job of it.
I understand that Google (and may some of the other fangs) have tooling like that internally - but I haven't had the pleasure of using it.
For instance, consider the problem of having an external Open Source project your company maintains or heavily contributes to, and which also has common libraries shared with internal non-public work. Or, the same problem applies if you have a downstream Open Source project that needs to incorporate and vendor an upstream one but easily contribute changes upstream.
Some folks do this by generating the external repo as a filtered version of an internal monorepo, but that's awful in so many ways, and breaks the ability to properly treat the external repo as a first-class thing that can merge PRs normally. It leads to workflows where people submitting PRs feel like their work gets absorbed internally and maybe eventually spit back out the other end, rather than just being merged.
It would require forge integration, but I'd like a world where I could make a PR to `company/open-source-subdir` and the company could merge that PR and that was that without any extra steps because open-source-subdir is just a publicly published subset of the `company` repo.
Consider the case where the repositories are owned by different entities, for instance, or have different governance. For instance, Project X needs to vendor Project Y, and have a few downstream patches that they're working on upstreaming.
Right now, the two major options include:
- Use a submodule. Experience all the pain of submodules.
- Use some tooling to fold the repo into your own repo. Extract commits from it when you want to upstream. Experience all the pain of not having the original commits and the ability to easily pull/merge/cherry-pick/rebase, plus pain when you make changes across that repo and other code.
No they are not. In theory they could be good, but the actual implementation falls down in ... let me count the ways:
1. Transitive dependencies. I sure do love that my company's submodule-based repo has 12 copies of one of our dependencies.
2. Checkouts don't work any more. You can't simply `git switch <branch>`, especially if that other branch has a different set of submodules. And good fucking luck if one branch has a submodule and another branch has a not-submodule in the same location.
3. They don't work with worktrees. In theory... maybe. In practice, the documentation says not to try and in my experience it is right!
4. The submodule URLs are now baked into the git repo. This means you can't mirror the repo anymore easily. I've even had cases where I couldn't even clone the repo because the authors had used `ssh://` URLs which required permissions I didn't have. It's insane that the authentication method gets baked into the repo. I have no idea why they implemented it like this.
5. The tooling experience is just way worse. Want to see a diff of everything you've changed? Well you can't. If you've changed anything in a submodule you just get a hash difference, or at best a list of commits (which is better but it's not even the default!).
Before you instinctively reach for the "well obviously it must work like that" part of your brain, take a moment to think if it should work like this. I can think of several ways to do it better (beyond just making the implementation less buggy).
Except, dependencies are rarely read-only.
Which, this is exactly how pinning dependencies works. However if you are mutating your dependencies frequently and want the reference to them to change at the same time, this is the big pain with submodules- gotta do both yourselves. Not to mention there are now logistical problems to answer as this cannot happen atomically in all scenarios, let alone automatically.
Then it's a series of either git ammends or `git checkout -b` etc.
Now, since there is so much high praise in this comment and sibling comments, what am I really missing? From the post it just seems like the person hates branches for an unspecified reason.
Here's my workflow, of the past 15 years:
- git checkout main - git pull
Do some changes. Do some more changes. Now:
- git checkout -b <feature-name> - git status - gvim into every file and then :Gvdiffsplit, select what I want to stage for each file - git push # open PR if I think it's ready
For the remaining changes not in the commit, I either create a separate commit, or discard.
An honest question of curiosity, how does jj improve this workflow?
- While I'm working on something I can do `jj desc` and start writing the commit message. Every edit is automatically being added to this change.
- My work tree is dirty and I quickly want to switch to a clean slate. In Git: (1) either do `git stash` where I'm definitely is going to forget about it or (2) do `git commit -a -m wip && git switch -c some-random-branch-name`. In jj: `jj new @-`. That's it! If I run `jj log` then my previous change shows up. No need to come up with arbitrary names. It's so refreshing to move changes around.
- I'm working on a stack of changes and sometimes need to make edits to different parts. In Git (1): Each change is its own branch and I need to switch around and do a bunch of rebases to keep them in sync. In Git (2): I have one branch with multiple commits. I make changes towards the final state and then do `git rebase -i` to move them upwards to where they belong. Biggest downside: I'm not actually testing the changes at the point where they end up and I'm not guaranteed it makes sense. In jj: I do `jj new <CHANGE>` to make changes further up in the stack. Once I'm happy with it I do `jj squash` and every dependent change is automatically rebased on top.
- And finally: I can solve merge conflicts when I want to! If any rebasing leads to a merge conflict I don't have to deal with it right away.
For example: let's say you have a few feature branches in flight at the same time, and you want to make a change to one of them to address some PR feedback. In your git workflow, that presumably means something like `git stash; git checkout feature-name; vim-and-actually-make-the-change; git add -up; git commit; git push; git checkout whatever-you-were-doing-before; git stash pop`, ± a `--amend` depending on your PR philosophy. A common workflow in jj is to instead sit on top of a merge commit with all the feature branches at once, and handle this like `vim-and-actually-make-the-change; jj squash -i --into feature-name; jj git push`. You can do something like the latter in git too, of course, it just tends to get hairy relatively quickly with rebases and merge conflicts.
In your case, jj’s ability to slice and dice commits is really nice; jj split is built in and works similarly, but you also have other tools too.
jj will start recording all of your changes as a real commit that it constantly rewrites as you edit files. You can write the log message for it up front or later. Think of it as an index that every change automatically gets staged to, except it’s just a commit, so instead of two concepts that work differently, you just have one. And you get the benefits of real commits, so for instance you can’t accidentally lose anything you staged that isn’t committed.
When you would create a branch and then stage changes with Git, what you would do in jj is split. The changes you don’t pick end up as the next auto-updating commit, and the changes you do pick use the log message you already set when you were working on them. So if you’ve got a bunch of changes you want to record as a series of commits, you just split however many times you want.
You don’t have to think about branching while you are doing this. The HEAD of master doesn’t automatically move when you are making these changes, so the effect is that you’re working on an anonymous branch already without having to create one. You can give it a name whenever you want by setting a bookmark. Or if you decide that the changes you make need to go into two branches, then you can add two bookmarks.
For instance, if you have:
A (master) => [changes]
Then [changes] is already a commit. Suppose you realise that you have fixed a bug and added a feature, but you want these as separate pull requests. You’d split, giving the bug fix a log message:
A (master) => B (bug fix) => [changes]
Then you’d give the feature a log message:
A (master) => B (bug fix) => C (feature)
Even though we started on master and made a bunch of commits without even thinking about branches, we haven’t changed master at all. So in effect, it’s like B and C are on some anonymous branch that was transparently created for you.
Now you want to open the pull requests, so you add a bookmark for B and a bookmark for C, and push them to your remote. B and C show up as branches that you can open pull requests for.
So your workflow is basically the same as it is now, there’s just fewer moving parts for you to think about as you work, and fewer concepts for newbies to learn.
However, unlike git, jj bookmarks are pinned to change IDs instead of immutable commit SHA-1s. This means that stacked PRs just work: Change something in the pr-1 bookmark, and all dependent bookmarks (pr-2, pr-3, ...) are automatically updated. A `jj git push --tracked` later and everything is pushed.
This is one of the strengths I appreciate about graphite which is that the PRs are always on the preceding branch but it knows that when you go to merge it should actually really retarget and merge against main.
Yeah – the key thing here is that there is work to be done on the server, so JJ likely either needs its own forge or a GitHub App that handles managing PRs for each JJ commit.
I'm a huge fan of the JJ paradigm – this is something I'd love for us to be able to do in the future once one or both of: - we have more bandwidth to go down this road - JJ is popular enough that its worthwhile for us to do
That said I'd also love to see if anyone in the community comes up with an elegant GH app for this!!
Ironic, since if there are a bunch of people in my boat, the lack of us in jj's user base will make it that much harder for jj to cross the "popular enough to be worth supporting" threshold.
There is definitely room for an improved forge experience that takes advantage of the additional powers of jj, but it's no worse an experience using them today than it is with git.
Basically if I have five stacked PRs, and the newest four get an approval, I want everything to stay in place no merges. Then when the base (oldest) PR gets approved, I’d like the PRs to all get merged, separately , one after the other, without further interaction from me.
Does GitHub’s merge queue implementation support that?
cf. https://jj-vcs.github.io/jj/latest/cli-reference/#jj-paralle...
As the parents of M are merged, I rebase the whole stack. When M has a single parent left, I abandon M and retarget the PR to merge R into that parent.
It requires a little babysitting, but the PR shows the diff I want it to.
Gitpatch attempts to build a Git hosting with native support for patches and commit-based review system, where each commit is its own patch. It's also smart to handle force pushes and can update or reorder patches as needed.
Do you have any plans to allow for self-hosting?
yeah, I plan to release it under AGPL at some point when it’s more complete. Currently it still needs more work. But no timeline yet.
Are there others who've previously made heavy use of VS Code's builtin Git staging support and have successfully migrated to JJ? Anything I'm missing?
Getting really good diff and conflict editor support into VS Code, Zed, et al is going to be a huge win when it comes.
The change selection TUI is one of the things that I'm happiest with in jj over the equivalent in git. It's a huge quality of life improvement over git's version.
Could it be even better? Probably... but compared to `git add -p`... it is already way better.
won't scale to what exactly?
The result is that `jj evolog -p` will show detailed history of your actions. But all but the most recent commit are hidden, so neatly tucked away behind the same change as usual.
Another favorite is git no longer yelling at me and having meltdowns about switching branches - "files would be overwritten, please stash". This never happens in jj, by design. It's nicer than "auto-stash" options of recent git versions.
Thanks
- Decide your changes are perfect, so add a commit message to this one and then create a new one on to to carry on
- Decide you only want some of them so use `jj split -i` to select which ones you want and then it creates two commits - the stuff you want in a new named commit, and the stuff you didn't in a new working copy commit. This is the JJ workflow equivalent to `git add -p` adding to the staging area then committing
I am an extremely fervent believer in jj and use it exclusively since December '24, but I think it's useful to be accurate as possible for these kinds of trade offs. I don't use watchman snapshots specifically because of this downside.
It’s good alternatives of popular tools exist but git would not be my first bet as a tool that needs fixing…
I always enjoy how on jj articles, 90% of commenters tried it and switched, 10% never bothered to try it, and 0% tried it but decided not to switch.
I may change my mind. Especially if they provide a less shit alternative to submodules and LFS. (And I agree this guy is just being contrarian - jj definitely does fix some Git annoyances.)
The "aha" moment you might be missing is that you should consider your latest revision to just be the staging area. `jj commit -i` (shorthand for `jj describe; jj split -i`) is effectively `git add -i; git commit`. If you're worried about accidentally pushing unfinished work, don't be! It won't let you push changes without a message by default, and you update bookmarks (e.g., branch pointers) at your discretion anyway. Both of these mean that `jj git push` isn't going to accidentally push a bunch of in-flight work.
Think of it less like jj commits everything by default, and more like your working copy gets the benefits of change tracking and being able to take part in repo operations even when you haven’t taken a moment to make a commit.
In git I can just revert all the changes and I haven't modified anything important. In `jj` won't I have actually added all of those debug printfs to the top commit of that branch? Now I have to manually revert the edit?
As I understand it, the answer is "aha, but you just have to remember to `jj new` before you do any edits. The problem is I'm very sure I will constantly forget to do that. I guess you could say Git is opt-in to modifying commits whereas jj is opt-out, and I think I prefer opt-in.
I have very little jj experience but does that sound accurate? (Genuine question; I would love something better than Git.)
That said, if you do for whatever reason run `jj edit branch` instead (which enables the case you are discussing), jj will have snapshotted the previous change so you can still automatically revert the debug prints using
jj evolog; jj restore --from old_commit_idAs others have noted, checking out a branch and accidentally modifying one of the commits on it is actually kind of hard to do. `jj git fetch` pulls in remote changes to your repo, but you don't "check out a branch": you either `jj edit` an existing commit or create a new one entirely with `jj new`. So you're unlikely to accidentally overwrite a commit by accident. I even find myself using `jj edit` less and less these days; if I want to change an existing commit (even if it's in the middle of a chain of commits), it's easy to `jj new <commit-to-modify>` and then `jj squash` the modifications into it. This allows me to double-check the modifications I'm making before I decide to commit to them (pun intended).
That said, I absolutely have forgotten to `jj new` before. Mostly this happens when I plop down at the laptop and start making changes without remembering to make a new revision. Whatever revision I happened to be on gets all those edits. Sometimes I work for quite awhile before realizing this, and so having to pick out which changes belong where piecemeal would be a ton of work.
But this is precisely the power of having all these changes in the repo as a revision. I can use all my SCM tooling to solve the problem for me! For the sake of this example, let's assume that I've pushed all my changes to a remote branch and then stupidly continued editing them. Now my local copy of branchname and branchname@origin have diverged; they both have the same revision, but different git contents.
# Oh no! I've accidentally made edits to `master`. `omny` points to two
# separate git commits; one is my local copy of `master` and one is the
# remote.
> jj log
@ omny me@example.com 2025-08-14 13:22:15 master* ae25
│ Made some changes
│ ◆ omny hidden me@example.com 2022-03-26 14:49:58 master@origin 79d4
╭─╯ Made some changes
◆ notn me@example.com 2022-03-23 17:03:05 b192
│ Earlier changes
# First, rebase our local changes onto the remote commit. This neatly
# splits out the changes I just made from the changes that were there
# before I messed up.
> jj rebase --branch master --destination master@origin
Rebased 1 commits to destination
Working copy (@) now at: omny?? 67a9 master*
Parent commit (@-) : omny?? 79d4 master@origin
# You can see that now my local changes have been rebased on top of
# the remote copy. Unfortunately, they still have share the same
# revision id so jj is not happy with me! We'll have to put those
# changes into a new revision and make sure our local `master` points
# to the same id as the one at `origin`.
> jj log
@ omny?? me@example.com 2025-08-14 13:28:14 master* 67a9
│ Made some changes
◆ omny?? me@example.com 2022-03-26 14:49:58 master@origin 79d4
│ Made some changes
~
# Make a new revision for our changes to go into.
> jj new
Working copy (@) now at: plwu 70d8 (empty)
Parent commit (@-) : omny 79d4 master
# Abandon the local revision where we accidentally changed the
# `master` branch. Normally this would get rid of our changes,
# but `--restore-descendants` makes sure that the filesystem
# contents of the new revision we just made remain unchanged.
# Since we're getting rid of the revision that made those edits,
# those edits have to be moved up into that new revision in order
# for it to look the same as it did before!
#
# Abandon would also normally delete our local copy of the
# `master` bookmark. But `--retain-bookmarks` keeps it, and pushes
# it back one revision. This is exactly what we need!
> jj abandon master --restore-descendants --retain-bookmarks
Abandoned 1 commits:
omny?? 67a9 master*
Rebased 1 descendant commits (while preserving their content) onto parents of abandoned commits
Working copy (@) now at: plwu 69ce
Parent commit (@-) : omny 79d4 master
# Everything is happy again! My recent changes are now on their
# very own revision.
> jj log
@ plwu me@example.com 2025-08-14 13:33:45 69ce
│ (no description set)
◆ omny me@example.com 2022-03-26 14:49:58 master 79d4
│ Make some changes
~
I want to be clear about something here. I have never done this before. This isn't a pattern that's ingrained into me. I've certainly accidentally edited the wrong revision before, but it's always been relatively easy to split out the changes I made so I've never needed to really solve this in the general case before.I read your comment, figured this would probably be the easiest way to do it using the primitives I'm familiar with, and I tried it. And it worked! I didn't need to go digging through manpages to figure out how to do it, it was a simple composition of commands I use every single day. I did this on a live, real repo I am actively working on where I had edits in flight, and just for giggles I squashed them into master. I had zero fear about doing this because a `jj op restore` would get me back to safety no matter what.
This will also work basically unmodified if you haven't pushed the changes. The only difference is you'd use the raw revision id instead of `master` and you'd use git commit ID of the earlier changes you wanted to keep (pulled from `jj evolog`), which you'd substitute everywhere I used `master@origin`. This works for every case where you have two sets of changes in one revision and you want to pull out the later changes from the earlier ones.
And some people just happen to disagree - doesn't automatically mean they just like "being contrarian". I took the "Yup..." to mean "this is what I was expecting, because it agrees with what I have seen before on this topic".
> I always enjoy how on jj articles, 90% of commenters tried it and switched, 10% never bothered to try it, and 0% tried it but decided not to switch.
And some unknown quantity of readers don't see anything compelling enough to either try it and/or comment on it after they have (or have not) tried it.
When Linus and his lieutenants switch over and recommend it as loudly as some do here, then I'll take another look. Very unlikely IMHO.
This is almost certainly true.
> 9% tried and didn't see any reason to switch, and around 1% have switched and won't shut up about it
This is almost certainly not true. People are far more inclined to give negative reviews than positive reviews.
As a (former) advanced git user, nothing could be further from the truth. We are always the most passionate cohort of jj users in these threads. This is almost certainly because jj unlocks a bunch of additional abilities and workflows that we've been torturing out of git (or given up on, or didn't even conceive was possible) for years.
On the flip side if all you ever do it git pull, git commit, git push, jj is probably not going to offer you much.
>For an advanced git user, it doesn't offer all that much.
arrogant, and completely absurdly wrong. I've used Git for 20 years. `jj` the single best improvement to my development workflow in... well, since adopting Git.
> I used it for a couple of weeks and can't say that it saved me any time or any amount of work
I would bet 5 figures that's a lie.
> When Linus and his lieutenants switch over and recommend it as loudly as some do here, then I'll take another look. Very unlikely IMHO.
So despite all this chest puffing, an appeal to authority would tip the scales for you?
I am an unabashed jj evangelist and I don’t think they’re lying when they say it didn’t save them any time. Adoption costs might be small but they’re not zero. Some workflows are easy with either tool. And some people just don’t “get” it and struggle to adapt to a different mental model. That’s okay!
> So despite all this chest puffing, an appeal to authority would tip the scales for you?
I think GP was simply saying this would be a clear sign to them that if it’s mature enough to handle kernel developers’ needs, that’s a good sign it’s worth the effort to switch. It definitely would be! I can’t wait to hear if and when kernel developers start switching; it will be a huge positive indicator.
It’s trivial to prove that’s not true. Just look at the last popular Jujutsu post on HN.
https://news.ycombinator.com/item?id=44643984
https://news.ycombinator.com/item?id=44643763
https://news.ycombinator.com/item?id=44646902
https://news.ycombinator.com/item?id=44645769
https://news.ycombinator.com/item?id=44662803
https://news.ycombinator.com/item?id=44644040
And those are just the replies to the top comment which matched “went back”.
Obviously not everyone who tries something is going to switch, even if you take as an axiom that the other thing is objectively better. But I still think it’s notable that—without explicitly prompting that type of response—so few people seem to have negative experiences with jj given how tribal and passionate engineers tend to be over tooling.
I have recently posted some critical questions. I didn't do that because I "enjoy being contrarian" (rather me enjoying a discussion). I did that to flesh out, what the benefits of JJ are. I think VCSs are interesting, I think Git has some issues, but in my opinion most problems are caused by a misguided mental models, I haven't/can't tried JJ. Most things I heard were workflows that are claimed are not possible in Git, but are possible and most-times only a few commands. There are other design choices which I disagree with and which might bother me.
("haven't/can't tried JJ" meaning I enjoy running a stable distro, but wouldn't mind compiling it myself. But Rust and Rust software is a fast moving target, so it is a hassle to deal with if you aren't invested in it's ecosystem. Also it violates the GNU standard, which makes it unfriendly for endusers, but this seams to be the norm nowadays.)
If you messed up with jj commands, you can use the op log to fix https://jj-vcs.github.io/jj/latest/operation-log/
AFAIK, jj runs "import" before and "export" (to git) after every invocation. That means it always has a consistent view.
jj can also handle concurrent edits by itself, think in a repo shared across a network. That said, I wouldn't think concurrent git commands are safe.
Stealing Fintan's `jj tug` alias from this post is something I have already found useful. Highly recommend if anyone is on the edge of trying to just give it a shot!
The only thing I’m missing now is support for git submodules, especially when working with workspaces.
This requires me to keep using git worktrees with collocated jj in each of them, which is suboptimal.
vinnyhaps•5mo ago