Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I must be getting old because I really don’t think git needs a simplified model. But hey, if people find value from this, more power to them. The blog is well written too.


It’s not just that it’s simpler, it’s that the primitives and interaction model are also strictly more powerful. A ton of extremely useful workflows that are an utter pain in the ass with git are absolutely trivial with jj.

One example is a series of dependent PRs. This is excruciating in git if you ever need to make a fix to an earlier PR because you have to manually rebase every subsequent change. It’s trivial in jj: you either fix the revision directly or you insert a new revision inbetween. The subsequent revisions are updated automatically. You don’t even have to think.

Another is splitting work into parallel branches. So often when I’m working on one area I end up making unrelated tweaks and improvements as I go. Pulling these out into their own branches is painful in git, so people just make omnibus branches that include a handful of unrelated work alongside the main task. In jj it’s basically zero work to split out the unrelated stuff onto a new branch off main, and it doesn’t require you to task-switch to a new branch. So I end up making lots of one-line PRs that are trivial to review whenever I’m doing deeper work.


> This is excruciating in git if you ever need to make a fix to an earlier PR because you have to manually rebase every subsequent change.

Spreading the word about `git rebase --update-refs` that will automatically update any branches that point to commits along the path (very useful with stacked branches). It is less convenient than what jujutsu offers (you need to know the branches to update, where jujutsu automatically updates any dependency), but still a very useful if you don't want to or can't switch to another tool.


There isn't really any "can't switch to another tool" when it comes to git + jj. You can use it today without anyone on your team knowing - other than that your PRs suddenly became much cleaner


Some people can't install arbitrary software on their employer's hardware.


That's unfortunate. Hopefully they'll be able to use jj on their own hardware, or with future employers


>you have to manually rebase every subsequent change

What do you mean by this? You can do an interactive rebase in git as well. The real issue is non-trivial merge conflicts which is going to be an issue no matter you use.


git rebase -i is a much worse experience than jj's autorebasing of descendants. For example, with git rebase -i you can't decide to do something else in the middle of the rebase — you're in that state until the rebase is completed or abandoned.

Merge conflicts are also significantly better with jj because they don't interrupt the rest of your flow.

And most importantly, the two features work together to create something greater than the sum of its parts. See my testimonial (first one) at https://jj-vcs.github.io/jj/latest/testimonials/#what-the-us...


> with git rebase -i you can't decide to do something else in the middle of the rebase

You absolutely can, to some degree. At any point in an interactive rebase you can just make your changes and do a normal `git commit` then do `git rebase --continue` along on your merry way. Unless you're talking about suspending the rebase and like switching branches, messing around, and then resuming the rebase, which is kind of a weird thing to do.


I do mean suspending the rebase and going to do something else, yes.

It's a weird thing to do in git, because the conditions that git creates makes it weird. It is completely natural with jj, because jj doesn't have any modal states at all. (This is one of the key reasons jj is both simpler and more powerful than git — no modal states.)


You can absolutely do that in git:

    git checkout master
    git rebase [whatever]
    [rebasing stuff]
    git tag rebasing
    git checkout --detach master
    [do random other stuff]
    git tag todo
    git checkout rebasing
    [continue rebasing]
When you are not trying to modify the same branch you're rebasing, you can omit --detach and also git tag todo.

(To clarify, it has never occurred to me that I even want to do that, so I didn't knew how to do it. Yet I didn't even needed to consider a manual, it just follows naturally from the git user model even if it seams completely unidiomatic.)


I know you're trying to point out how easy it is in git, and some months ago I would have agreed. But after using jj (for only a few weeks!), your commands seem needlessly complex.

In jj, you run the rebase command. If you get conflicts, and want to switch to something else before you fix all of them, you simply do "jj edit" or "jj new" and work on something else. Then you do "jj edit" to go back to the conflicted revision and get back to fixing conflicts.

The key point is: You don't need to learn any new concepts.

In the git example above, I need to know tagging. I need to know checkout vs "checkout --detach", etc.

I'm not saying your git commands are crazy complex. They're merely more complex than one needs to solve this particular problem.


You don't need to tag, you also just specify the commit SHA, isn't that the same in jj? It's just convenience.

The difference here seams to be that in jj you're always in a commit while in git you're always outside. But that's the difference between one and two commands.

Tags are hardly a new concept, if anything they are easier then a rebase. You can't really expect to use something with knowing the core concepts. Thats true for every tool and also for software. Can you use jj without knowing what a commit is?

I didn't knew about --detach before. It was obvious from the error and easily found using autocomplete. Also you only need it, if you want to change something under yourself while you're already modifying it.


As far as I know you can't start another rebase -i while you're in this situation. But it's been almost two years since I last used rebase -i so I might be wrong.


Yes, that's because a rebase in git is nothing special, so you influence the rebase with normal git commands. When you do normal interaction like committing or resetting these become part of the rebase.


Modal states are actually good IMHO.


I feel like I just outlined a situation where something that's weird in git is completely natural in jj. Maybe I miscommunicated?


I think you communicated well, probably I did not.

> suspending the rebase and going to do something else

This is what I find to be weird, personally. When doing a rebase, there’s no way I want to do something else in the middle of it, and having a modal state feels totally natural to me.

At first approach (I read a (very good) intro[1]; I did not try), it seems there’s a lot of new things to learn (for instance the revsets language), for a very minimal gain, so I’m (still) gonna pass on it. It feels like jj is solving a lot of problems that do not exist, or are mostly solved with worktrees.

That being said it’s true that everybody’s way of working is different, so I don’t know! Maybe jj will be picked up by the younger generation and it will become the new de facto standard. Time will tell…

Personally, the current VCS tool I’d like to try now instead of jj is fossil. It seems much more interesting as it promises to allow bypassing GitHub/other forge completely by being the full forge itself. In these days, having ownership of one’s data feels primordial to me.

[1] https://ofcr.se/jujutsu-merge-workflow


> When doing a rebase, there’s no way I want to do something else in the middle of it, and having a modal state feels totally natural to me.

This is a consequence of rebases being a special, modal state that requires dedicated focus to work through to resolution.

Rebase conflicts are (to a jj user) just another fix you might want to make to a revision. Or it might be better done by making a tweak to an earlier revision, instead of the revision where the conflict first occurred. There’s no pressure to fix it right this second, you can always come back where you left off. And you can make your fixed in a separate commit that you squash into the conflicted commit if it’s particularly hairy.

> It feels like jj is solving a lot of problems that do not exist, or are mostly solved with worktrees.

Git has grown a lot of features over the years and countless flags to make some things feasible.

jj rethinks the core interaction model in a way that gets rid of all the band-aids.

There are people who still live and die by C and swear that anyone running into issues is holding it wrong or just needs to be more principled about how they handle memory. But most people have moved on to higher-level languages. It doesn’t mean C was a bad language, it doesn’t mean those programmers weren’t familiar with C, and it doesn’t mean there aren’t still some cases where C is the right answer. But most people nowadays find other languages more productive with less friction.


> This is a consequence of rebases being a special, modal state that requires dedicated focus to work through to resolution.

Believe it or not: no. Humans are notoriously bad at multitasking, so it makes sense to actually finish a task before moving to something else and forgetting what we were doing…

Relaxing this comment which is a bit aggressive: at least it’s how I work. YMMV.


> Believe it or not: no. Humans are notoriously bad at multitasking, so it makes sense to actually finish a task before moving to something else and forgetting what we were doing…

Again, see my other comment. It's like saying "Humans are bad at multitasking - they should never switch to another branch until the current feature is done."


> it's like saying "Humans are bad at multitasking - they should never switch to another branch until the current feature is done”

Neither. Rebasing is an atomic task, kinda like a commit.


> Rebasing is an atomic task

I may be misinterpreting your position, but I suspect you consider rebasing and fixing conflicts together as the atomic task. In jujutsu, these are two separate tasks. You do a rebase. Even if there are conflicts, the rebase is done. Fixing conflicts is its own task, and can be done at your leisure - no need to couple the two.

It's like writing some code that broke a test. You don't say "Hey, I'd rather not work on anything else until this is fixed." If you want, you either commit it as is (or stash if you prefer), work on something else, and come back to it later to fix the broken test.


> You don't say "Hey, I'd rather not work on anything else until this is fixed."

Hey that’s where we differ I guess! I do (most of the time, and if I truly need to switch, my workspace is cloned usually at least twice, using worktrees, so I can indeed switch whatever the current status of everything).

This is broadly what I was saying above, most of the “problems” git have are solved with worktrees, that have the undeniable advantage (IMHO) to actually separate tasks, instead of being in a state where (waving hands) everything is here in this big bowl of stuff (and I’m not saying that dismissively; once again everybody works differently; it’s just not my thing).

I do have understood the “conflicts do not need to be resolved straight away,” though I do not really get it (if there are conflicts the code does not compile anymore, so what is even the point? ok you can switch branch, but still it’s a task that I would know I have to do anyway and I could not switch to something else; my brain does not work that way).

All of that being said, since you seem to know a lot about jj, I’ll take the liberty to ask a question I do have about it, because there is indeed a workflow that I find painful with git that, IIUC, is actually solved by jj.

Let’s say I (or the team) am (is) working on two or three different things on two or three different branches. The full feature is the merge of these branches. Is jj able to maintain a branch which is the merge of all the other branches mostly (or fully) automatically?


Actually, my experience is still at the beginner level - I switched only weeks ago.

If you have 3 branches, you can create a new node that is the merge of all 3. But your question is will it keep up with the changes.

If all 3 branches are local to your machine, and have not been shared, and you edit a prior commit, then all descendants are rebased and thus that merged node will get the changes - no new merges needed.

I think your real question, though, is if one of the branches gets a new child node, can we have the merged node dynamically merge from the new child, rather than from its parent?

I honestly don't know - but I actually do have a need for this! jj has bookmarks, and you can create a merge using bookmarks, but whether the new node will autoupdate if the bookmark moves - something I'd need to experiment with.

Finally, when you have shared/pushed your code, jj will treat shared nodes as immutable, so some fancy jj stuff becomes disallowed.


I think the evergreen use case for interrupting a rebase is, your manager came over to ask if you can drop everything and look at this bug that $TOP_CLIENT is screaming about.

Git worktrees suffice, but they're still heavier weight than `jj new whatever`.


I've had maybe 3-4 occasions in my entire career where I was working with a rebase large enough where it took multiple sittings (every single one sucked badly). I'm not going to argue that it's not something that comes up, but I will say that if it's common, your workflow is probably too chaotic for me. That being said, you can still do that in git (see sibling comment to this one), but it is more involved. IMO that's good because I don't think enabling that type of chaotic, jumpy workflow is healthy or good.


Well for me it's happened hundreds of times. Specifically I've very often wanted to do another git rebase -i in the middle of the first one, usually because I changed my mind about something and want to make changes to an earlier commit in my stack.

> IMO that's good because I don't think enabling that type of chaotic, jumpy workflow is healthy or good.

Nah, it's wonderful. You see it as chaotic and jumpy because the conditions Git creates makes it feel chaotic and jumpy. It's like being terrified of multithreaded code if you're not using Rust or a purely functional language.


> Specifically I've very often wanted to do another git rebase -i in the middle of the first one, usually because I changed my mind about something and want to make changes to an earlier commit in my stack

I think this is what `git rebase --edit-todo` is for.

>Nah, it's wonderful. You see it as chaotic and jumpy because the conditions Git creates makes it feel chaotic and jumpy.

Fair enough. `jj` probably isn't right for me, but I'm happy it works well for you!


Been a long time since I used git, but --edit-todo only changes what's still to come, right? It doesn't let you go back and edit earlier commits in the stack.


Ah, yeah it does only allow you to modify the remaining todos :/


> This is what I find to be weird, personally. When doing a rebase, there’s no way I want to do something else in the middle of it, and having a modal state feels totally natural to me.

When I was pure git, I agreed.

Let me reframe your comment:

"When working on a feature, there's no way I want to work on some other feature in the middle of it"

(which is how many people felt before ever using version control and branches).

With jj, I do it all the time. The tool made the difference. A rebase is not something "special". Switching from a rebase to working on another feature in the middle is exactly just like switching to another branch in the middle of working on the current branch. It's the same (or very similar) set of commands.

When you realize this, then yeah - leaving in the middle of a rebase is pretty normal, and you wonder why people don't do it.


First class conflicts are way better than modal states.

https://ofcr.se/jujutsu-merge-workflow/

I literally will have like 4 PRs in flight at once and have an octopus merge of all my separate PRs that I can then work on top of. JJ can rebase all 4 separate branches, the octopus merge, and the work on top of the octopus merge in a single command: jj rebase -d main.

If there are conflicts I can then resolve them whenever I want.

You have no idea what you are talking about if you think git’s interactive rebase holds a candle to what jj rebase can do.


Let's say I have five commits in a row ready to go, but they should be reviewed and merged one-by-one. Upon review, I need to make a change to the first commit. How much work do you think this would be in git? How much of a pain in the ass do you think it would be if a later change conflicted with this earlier change?

It is essentially zero work in jj. I `jj edit` the revision in question, make the change, and `jj push`.


It's really not a lot at all. The weakness is in "forge" tools like GitHub that add a PR/changelist abstraction on top of git and don't support sequences of patches. If you're using just commits and maybe (mailed) patches you only do a single `git rebase` (with -i if you want) and you're done. Unless jj is literally magic and can automatically fix conflicts, I can't see how it would actually reduce the work involved in a meaningful way.


I'll be honest, it's been so long at this point since I've used git that it's hard for me to remember the exact details of many of the challenges. I know that I avoided a lot of workflows I wanted to do because of the complexity and mental overhead, and the scenario I described was absolutely one of them. Maintaining a stack of changes that needed to be merged one after another was painful if there were any unexpected hiccups along the way. Now it is painless.

Arbitrarily-complicated rebases just happen automatically with jujutsu. Inserting, moving around, and directly editing commits are first-class operations, and descendants automatically rewrite themselves in-place to incorporate changes made to their parents. Rebase conflicts are also first-class citizens so they don't block you and force you to deal with them right now or in any particular order. Having to rebase seven related conflicts one-after-another is no longer a thing, nor is realizing you fucked something up halfway through and having to restart.

Coming from git it honestly feels like magic even if it strictly isn't. It genuinely hard to understand how much unnecessary toil git makes you put up with on a day to day basis until you suddenly don't need to deal with it any more.


As far as I know, by default Git doesn’t enable the “reuse recorded resolution” feature so if you made a change to the first commit you’d have to manually do the same thing for any subsequent commits.


If you have 5 different branches, sure. Again, the reason you create a bunch of branches for separate review is because that's what the "git forge" abstraction generally expects. It's not actually how code reviews are done by the people who wrote it.

You can also just enable that feature (rerere).


What is the "right" way to do it then?

Also probably most of us are stuck with whatever git*.com supports anyways...


Basically see how the LKML works. Individual commits are typically expected to be self-contained to the best extent possible, with patch sets or patch series being used when that's not feasible. Patches are usually sent over email, but there are a lot of ways to do it.

It's not an ideal way to operate for most shops, but there's really no reason you can't have a PR/MR/changelist/whatever that is a single branch with X commits on it and you ask the reviewer to review each commit individually instead of as a whole unit (as GitHub and other forges usually expect you to).

That and don't let reviews pile up such that you have 5 dependent in-flight reviews, something else is wrong if that's happening.


> Basically see how the LKML works. Individual commits are typically expected to be self-contained to the best extent possible, with patch sets or patch series being used when that's not feasible. Patches are usually sent over email, but there are a lot of ways to do it.

What is the advantage to this, other than maybe being easier to send over email?

To me, the important part is you have a logical "unit of change" that you're proposing to the codebase, whether that's a single commit or a branch or a jj bookmark or whatever seems more an artifact of the underlying transport layer (email or github or whatever) than any kind of intended functionality of the design.


There's quite a few advantages to having a single commit being the unit of change:

- reverts are significantly easier

- bisect will work because the code is expected to code/run at every commit

- rebase becomes easier in the common case

- the transport does not matter, at all. Fully anonymous contributions are possible

- commit messages are easier to write because the changes are smaller and more focused

Several commits could encompass changes necessary to make a particular feature work (ie. patchset) but because each commit is self contained they can be reviewed and tested individually (in order, usually) instead of a one big diff. It's easier on the reviewer, though there's likely a bit more overhead.


Use `--update-refs`.

Think about the word "branch". If you have a linear sequence of commits that's one branch by definition. But you go and label the middle of that branch as branches too and then get annoyed when git, you know, does some branching there.


> It's not actually how code reviews are done by the people who wrote it.

What do you mean by that? Even if you do review by emailing patchsets those are still managed locally using branches, to my knowledge.


I mean the unit of review is the patch (set), which does not necessarily have a branch associated with it. You can use a branch, but you could just as easily send commits from master and the reviewer can apply them directly on their master branch if desired. The idea of "branch per reviewable unit" was largely created by GitHub.


No work, I set `rebase.updateRefs = true` years ago.


> The subsequent revisions are updated automatically

How do you make sure, that a commit isn't changed under you, because someone thought, it would be a good idea to change an earlier revision? I think having immutable commits including all the previous history is a feature, not a bug.


JJ has a concept of mutable and immutable changes. Typically, mutable changes are ones that you're currently working on locally. If you create a branch locally and push to it, then the changes on that branch will also be considered mutable. But changes created by other people in branches that you didn't create are considered immutable, as are e.g. changes that already exist in the default branch. There are a handful of other ways that you can convert changes between the two states, but generally the distinction is fairly intuitive: mutable changes are ones that you own, and where it would normally be "safe" to force push etc, and immutable changes are ones that probably also exist on someone else's machine.

All changes can be modified, but if you try and update a change marked as "immutable", then Jujutsu will error out unless you provide a specific flag. This generally provides a lot of protection against unexpected rebases from other people.

You also have a local immutable history of every update to your copy of the repo. This includes fetches/pulls, so you can easily see and undo those changes if something does go wrong along the way. But if people are deliberately force-pushing to master, you're going to end up in weird states whatever to you use.


Jj treats any public/pushed commits/branches as immutable by default.


How does it know that? Does it also work, when I clone or pull instead?

Is that only enforced on the committer side? I mean git also doesn't force push by default, but that still means someone else can do that and I need to notice it.


There is a way to express which sets of revisions are immutable. This uses the revset feature which itself a whole amazing thing that hasn’t come up in the conversation yet. This is a mechanism by which you can concisely express and refer to entire sets of commits in your repo history, and lots of commands can operate on multiple revisions as easily as they can operate on one.

Just like git, it is only enforced by the tool itself on the client side. But it’s safer in practice because `git push -f` is a common habit when you’re working on a branch by yourself and it’s easy to fat-finger that in the wrong place. I have only ever had to use `jj --ignore-immutable` when explicitly doing repo surgery.

If you want branches like `main` to be truly immutable, you need to enforce that at the repo.


I don't have answers for you. But the docs are useful and people in their discord are helpful as well


I don’t think simplified model is even the best selling point, but it’s definitely up there. IMO one of the killer features that you absolutely cannot get in git is universal undo. For example, rebases can always be a little tedious and tricky no matter how experienced you are. Not only does jj make that whole process easier and safer to work through, but if you do still manage to get to a state where you just want to go back it’s literally just an undo away.

I’d give [1] a read if you’re interested.

1. https://zerowidth.com/2025/what-ive-learned-from-jj/#commits...


It's not a "simplified model". The jujutsu model is more capable, but also more generic and thus easier to use and re-use. It's just better.


Couldn't agree more. I guess we _are_ getting old after all.

I've been using git since ~2014, never really thought about changing it: it's clear, well documented, and ok difficult learning curve but hey that's the fun part.


Honestly one of the biggest selling points of jujutsu for me is its `op log`. You can fuck up your repo in git and that's it -- you're screwed. Or you find some extreme magic on the internet that saves you.

With jj you just "jj op undo <operation_id_that_fucked_your_repo>" and you're fine.

Editing prior commits is also pretty easy, which in turn makes fixing merge conflicts pretty easy too.


Like… reflog?


Kinda!

jj has two kinds of these logs: the evolog and the op log.

The git reflog is based on, well, refs. Whenever a ref is updated, you get an entry. This log is per ref. That's HEAD, your branches, your tags, and your stash.

jj's evolog is sorta similar, but also different: it's a log, but per change (think commit in git). This means it is broader than per ref, as it includes not just commits that correspond to a ref, but all of them.

jj's oplog is a log per repository. This lets you do things like `jj undo`, which lets you get the entire repository, not just one ref or commit, back to the previous state, easily.


Yeah this doesn't make sense in git terms. A commit is immutable, it never changes so it doesn't have a history, it's just always there.


That's still true in jj, too.

But instead of commits, most of the time you're working with (the not greatly-named) "changes", which are a conceptual history of related commits. E.g., a single change ID points to the most recent commit in a list.

As long as you keep editing on a change, it keeps accumulating commits under the hood. When you move to another change (via `jj commit`, `jj new`, etc), all your new commits end up under a different change.


Ah, so when people here talk about changing commits they are actually talking about changing changes (nicely named, indeed)?

What happens when you merge or split changes with the change history? It sounds a bit like applying a VCS on top of a VCS.


Yeah so people will be a bit loosey-goosey with the terminology. But all of these changes are immutable, that is, when you change a change, you’re generating a new commit. “A VCS on top of a VCS” isn’t a terrible way of thinking about it: imagine if every commit in your repo had its own history.

Merging is making a change that has more than one parent. You can then resolve any conflicts within that change.

Splitting a change into two will update one commit with one half and make a new change with the other half.


You can have merge conflicts, when you combine two commits (--amend in git terminology, not sure how it's called in jj)?


Oh, since you said 'merge' I thought you meant like git merge. Amending a commit works the same way as git. The difference is just that the oplog will have the full history, including before you amended, so you can roll back easily.


So the history of the second commit gets discarded?


Ah, I see what you're saying. I think so, the evolog ends up showing that stuff was squashed elsewhere, I believe.


Yeah, I love jj, but I'm not a fan of that naming. It threw me for a loop for the first couple months.

I'm not sure what happens, but I suspect a merge would create a single merge commit with the tips of the parents' histories, and then add new commits from there on.

A split is probably conceptually similar to making two new branches, except each of the new commits gets one of the branch tips. Really just guessing here, I have no idea if that's how it works, or if they share commits or not.


no. reflog stores snapshots with descriptions; op log stores operations.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: