Most active commenters
  • adastra22(9)
  • baq(9)
  • steveklabnik(6)
  • 1718627440(5)
  • BeetleB(4)
  • KallDrexx(3)
  • sunshowers(3)

←back to thread

Jujutsu for everyone

(jj-for-everyone.github.io)
434 points Bogdanp | 71 comments | | HN request time: 1.874s | source | bottom
Show context
marcuskaz ◴[] No.45084298[source]
> Jujutsu is more powerful than Git. Despite the fact that it's easier to learn and more intuitive, it actually has loads of awesome capabilities for power users that completely leave Git in the dust.

Like? This isn't explained, I'm curious on why I would want to use it, but this is just an empty platitude, doesn't really give me a reason to try.

replies(7): >>45084316 #>>45084327 #>>45084439 #>>45084678 #>>45088571 #>>45092597 #>>45093098 #
1. pkulak ◴[] No.45084678[source]
Say you start on Main, then make a new branch that you intend to be a PR someday. You make commit 1. Then another. Maybe 6 more. Now you realize that something in commit 1 should have been done differently. So, you "edit" commit 1. All the other commits automatically rebase on top and when you go back to your last commit, it's there. Same with _after_ you PR and someone notices something in commit 3. Edit it, push, and it's fixed.

You can do all that in Git, but I sure as hell never did; and my co-workers really appreciate PRs that are broken into lots of little commits that can be easily looked over, one by one.

replies(4): >>45084727 #>>45084733 #>>45084935 #>>45085106 #
2. lrobinovitch ◴[] No.45084727[source]
You have to force push each time you do this, right? How do your coworkers find the incremental change you made to commit 1 after you force push it, and how do you deal with collaborative branches effectively this way? And if I don't want to work this way and force push, are there other benefits of jj?
replies(2): >>45084790 #>>45085517 #
3. adastra22 ◴[] No.45084733[source]
I do this every day in git. “git rebase -i [hash]” fyi.
replies(1): >>45084782 #
4. baq ◴[] No.45084782[source]
you think you do, but you don't; jj edit is much, much better than an edit step in a rebase - it essentially keeps rebasing while you're editing, so you can always see which changes get conflicts, then you are free to resolve them, or not, at your convenience.
replies(3): >>45084799 #>>45086057 #>>45086723 #
5. baq ◴[] No.45084790[source]
the heuristic is 'if you know about rerere and especially if you use it, you should try jj'. if you never force push, you might not see value in jj. (I basically always force push.)
replies(1): >>45084944 #
6. adastra22 ◴[] No.45084799{3}[source]
So you get all merge conflicts at once? How is that better?
replies(1): >>45084858 #
7. baq ◴[] No.45084858{4}[source]
it's exponentially better because you don't need to resolve them until you're ready. conflicts are committed to the local repo like everything else, commits with conflicts are noisily warned about and you can fix them whenever instead of having no other option than immediately.
replies(2): >>45084917 #>>45085054 #
8. adastra22 ◴[] No.45084917{5}[source]
How does your repo work with conflicts? How does it compile?
replies(2): >>45085447 #>>45086310 #
9. philwelch ◴[] No.45084935[source]
I do that in Git all the time. JJ might be easier in some sense but “more powerful” implies that it can do things that are impossible in Git.
replies(3): >>45085525 #>>45085869 #>>45086493 #
10. lrobinovitch ◴[] No.45084944{3}[source]
That makes sense, good to know, thanks.

> I basically always force push

How do your colleagues deal with this, or is this mostly on experimental branches or individual projects?

replies(4): >>45085108 #>>45085122 #>>45085521 #>>45086437 #
11. ec109685 ◴[] No.45085054{5}[source]
To the sibling comment that is too deep to reply to, this covers why delayed conflict resolution is advantageous: https://news.ycombinator.com/item?id=45084835

It’s for other branches that hang off the commit that introduced the conflicts.

12. ileonichwiesz ◴[] No.45085106[source]
Okay, sure, but if I realize I should’ve done something differently in commit 1, why wouldn’t I just make a new commit with the fix?
replies(3): >>45085225 #>>45085789 #>>45087384 #
13. whateveracct ◴[] No.45085108{4}[source]
People barely ever work off my branches.
14. smw ◴[] No.45085122{4}[source]
It's generally fine if you force push a branch that you're the only one working on. In many projects, there's an expectation that the 'PR Branch' you create in order to make a github pull request is owned by you, and can be rebased/edited/force-pushed at will. It's very common to do things like `git commit --amend --no-edit` to fix a typo or lint issue and then force push to update the last commit.

This has it's problems, and there's a reason things like Geritt are popular in some more sophisticated shops, as they make it much easier to review changes to PRs in response to reviews, as an example.

15. tomstuart ◴[] No.45085225[source]
Do you want another person (or yourself in the future) to be able to read your commits, in order, to get a clear account of what changed & why? If so, you should fix up those commits to address mistakes. If not, it doesn’t matter.
replies(3): >>45085388 #>>45085450 #>>45090987 #
16. KallDrexx ◴[] No.45085388{3}[source]
Not the OP but for me, no I don't actually.

In a PR branch, my branches usually have a bunch of WIP commits, especially if I've worked on a PR across day boundaries. It's common for more complex PRs that I started down one path and then changed to another path, in which case a lot of work that went into earlier commits is no longer relevant to the picture as a whole.

Once a PR has been submitted for review, I NEVER want to change previous commits and force push, because that breaks common tooling that other team mates rely on to see what changes since their last review. When you do a force push, they now have to review the full PR because they can't be guaranteed exactly which lines changed, and your commit message for the old pr is now muddled.

Once the PR has been merged, I prefer it merged as a single squashed commit so it's reflective of the single atomic PR (because most of the intermediary commits have never actually mattered to debugging a bug caused by a PR).

And if I've already merged a commit to main, then I 100% don't want to rewrite the history of that other commit.

So personally I have never found the commit history of a PR branch useful enough that rewriting past commits was beneficial. The commit history of main is immensely useful, enough that you never want to rewrite that either.

replies(3): >>45088997 #>>45089139 #>>45108989 #
17. baq ◴[] No.45085447{6}[source]
The edited commit, assuming it doesn’t have conflicts with predecessors, builds just fine. Successor commits with conflicts that you just introduced predictably don’t - but you aren’t editing them, so it isn’t a problem. In fact, that’s exactly why this feature is so compelling.
replies(1): >>45088030 #
18. christophilus ◴[] No.45085450{3}[source]
It’s useful for me to see the mistake and the fix, as it is a good way to jog my memory about the “why” of things. Pristine commit history is not important to me.
19. Disposal8433 ◴[] No.45085517[source]
IIRC it's push force with lease, ie non destructive push force. No one will be bothered or notice what you did.

And if you have conflicts, it's really easy to rebase and fix any issue.

20. baq ◴[] No.45085521{4}[source]
The PRs are either small enough that it isn’t a problem or large enough that it isn’t a problem… the odd in-between PR experience sucks and it’s one of the cases when I sometimes add more commits instead of force pushing.

+1 to sibling gerrit recommendation; I used to use it a decade ago and it was better then than GitHub PRs today.

21. Vinnl ◴[] No.45085525[source]
I think generally when people say that for jj, they mean it can do the same things with fewer concepts.
replies(1): >>45085860 #
22. paradox460 ◴[] No.45085789[source]
You can actually do that in JJ too. And you can take a change that's full of changes to other files, run a single command, and have those changes automatically put into the most recent change (save the one you're working on) that modified it recently.
23. sswatson ◴[] No.45085860{3}[source]
The author lists that as a separate benefit, though.

My interpretation is that jj makes certain useful operations convenient to use that would be so complex in git as to be completely impractical. Something like jj undo would be a simple example: jj users can do it, and git users can’t, even though it’s logically possible in both systems.

24. sunshowers ◴[] No.45085869[source]
If you're in the middle of a git rebase -i of a stack of 20 commits, and realize while editing commit 15 that you made a mistake at commit 8, how do you go back and edit commit 8 without having to complete the rebase -i?

This is not contrived — this is an entirely realistic scenario that I use jj to handle all the time.

replies(1): >>45086836 #
25. 8n4vidtmkvmk ◴[] No.45086057{3}[source]
Jj edit isn't even the jj way of doing things. Should be jj new. Unless you have changes stacked after then you'd do jj new -A. And squish when you're done
replies(1): >>45086389 #
26. aseipp ◴[] No.45086310{6}[source]
You have this commit graph

    B --> X --> Y (main) --> Z --> @
     \
      --> G --> H
B is a base; yesterday the name "main" pointed to it, and today "main" points to Y. Z is a commit you wrote that you haven't published yet. "@" means "Working copy", which is a way of saying "what your filesystem looks like." So, at this time, you see the changes from B, X, Y, Z, but not G or H.

You want to rebase G --> H from B to Y. But unfortunately, G conflicts with X. H does not conflict with anything. When you run this rebase in Git, you will actually have to immediately fix the conflict between G and X in order for the rebase to continue. If you do not solve it right then, the entire rebase fails. Git's rebase is actually an algorithm represented by a state machine; you must solve the conflict to proceed from "conflicted state" and `git rebase --continue` the rebase algorithm. (If you imagine what you would need to do to actually implement 'git rebase' as it works today in your own code, this state machine model makes immediate sense.)

In Jujutsu, rebase is a non-stop operation and it always succeeds. There is no state machine. It will update the commit graph to look like this:

    B --> X --> Y (main) --> Z --> @
                 \
                   --> G --> H
                       C     C
Now G and H are marked as "conflicted". If any commit is marked as conflicted, then all (transitive) children are marked as conflicted, too. If you "switch over" to working on G, then you can solve the conflict and commit the solution. That will solve the conflict in G, and also H as well.

But you don't have to do anything. In the above graph, G and H are conflicted, but because they are not a parent of `@`, then it does not matter. They exist in a parallel universe that does not influence your own. You can keep compiling code as usual. If you "switched over" to G, then the conflict is "materialized" in your working copy (filesystem) by putting conflict markers in the files, and so you have to solve it to keep compiling.

In short, Jujutsu separates conflict computation (do patches X,Y have a conflict?) from conflict materialization (make the conflict appear with markers in a file), and materialization of conflicts is "lazy" -- it only happens if a conflict exists transitively in the history of your working copy. Resolution is then done at your leisure.

A more brainiac way of thinking about it is that Jujutsu is a tool for manipulating _commit graphs_, and that is a purely computational notion; adding edges, removing edges, etc are all just basic algorithms. The graph's nodes contain "content" and states like "conflicts" are just defined as a relationship C(X,Y) on nodes in the graph. But all of this is "purely computational." Imagine implementing Jujutsu's rebase command; it is just a trivial reparenting of some graph nodes, something an amateur programmer could do. Calculating the relationship `C` is a bit more involved, but not complete black magic. But none of this involves "reading files from disk" or whatever. The side effect of "update the files on your filesystem to look like state XYZ in the graph" is just that: a side effect that the tool does when it is needed. Git, in contrast, only works through "side effects" in that it tends to only operate on the working copy, and never the "holistic commit graph". And so Jujutsu works at a higher, more "pure" level.

-----

Fun fact: in some cases, you do not actually have to "switch over" to G in order to solve this conflict, either. It is actually possible to craft a "solution" to the conflict in G while on top of Z. Then you can do `jj squash --from @ --into G` and you can "teleport" the resolution into the conflicted commit, solving both G and H, without ever making it appear in the working copy. This happens in cases like "G modified a file named readme.txt that was deleted by commit X"; all you have to do is "re-delete" the file inside commit G and it is trivially solved. This is something that is, quite literally, impossible to do in Git.

replies(2): >>45086806 #>>45089453 #
27. BeetleB ◴[] No.45086389{4}[source]
As a relatively new jj user, I'm curious. Why is the jj new -A + squash better than just a jj edit?
replies(3): >>45086416 #>>45087841 #>>45089421 #
28. baq ◴[] No.45086416{5}[source]
Separation of concerns and performance; when you edit, the commit in the middle of the branch is your working copy and you’ll update the whole branch unnecessarily many times vs just once when you’re on a logical checkpoint.
replies(1): >>45088090 #
29. andrewaylett ◴[] No.45086437{4}[source]
JJ has the concept of "immutable changesets" -- if it sees a commit is referenced from a branch that it's not tracking, it assumes it ought not rebase that commit. Changesets on branches that look like the main branch are immutable too. And you can edit the revset that JJ considers immutable if you need it to be different from the default.

The net effect is that I can change "my" branches as I wish, but I can't change stuff that's been merged or other folks' branches unless I disable the safety features (either using `--ignore-immutable` or tracking the branch).

JJ also makes it really easy to push a single changeset as a branch, which means as you evolve that single commit you can keep the remote updated with your current work really easily. And it's got a specific `jj evolog` command to see how a specific changeset has evolved over time.

30. andrewaylett ◴[] No.45086493[source]
JJ has first-class support for conflicted trees, changesets, branches, and operations. The op log itself is a (really useful) feature not present in Git.

You can always end up with the same set of published commits, guaranteed. But the tools you have for manufacturing them and for interacting with their history definitely include things that are possible in JJ but not in Git.

31. 1718627440 ◴[] No.45086723{3}[source]
You can use all the git commands while doing a rebase.

When you want to work on an older commit for a longer time and don't want to stay in a rebase, you just check it out and work normally, when you are done and want to propagate your changes, then you do a single rebase.

replies(1): >>45086814 #
32. 1718627440 ◴[] No.45086806{7}[source]
Thanks, for the long explanation.

[G: original, G' with conflicts, G" resolved]

What value do you get from G' and H' existing with conflicts when you can't use the working tree until after you have resolved the conflicts?

So in Git it would be G -> G", but in JJ you can do G -> G' -> G". But G" in both cases only exist, until after you have put in the work of solving the conflict. And G' only ever exists without a usable working tree. So what do you get from having G' earlier, when you still have G" only after the same work?

replies(1): >>45088072 #
33. baq ◴[] No.45086814{4}[source]
I’m reminded of the Dropbox comment.

You can do anything with a Turing machine. That you can isn’t the point. The point is the tool does all the things you can automatically and correctly so you don’t have to. There’s no ’just do this or that during rebase, or outside of it’. There’s only ‘it rebased everything correctly without a single thought, nice’.

replies(1): >>45086981 #
34. 1718627440 ◴[] No.45086836{3}[source]
True nested rebase doesn't exist (yet) in Git. What I do is create another commit with the parent commit 8 with commit --fixup and then complete the rebase. The I can just autosquash it, without reediting anything.

You could also keep the rebased commits, abort the rebase, rebase the already rebased commits and then continue the first rebase.

replies(1): >>45086939 #
35. sunshowers ◴[] No.45086939{4}[source]
With jj, because it doesn't have modal states of any kind, you can just go back to commit 8, edit it, and everything dependent on it gets auto-rebased. You can also do jj squash --into for a workflow similar to fixup commits.

I would consider the first workflow to be impossible to do by most mere mortals in Git [1]. Meanwhile in jj it's downright trivial.

[1] There technically is a way to do this by setting a temporary branch, aborting the rebase, starting another rebase -i, carefully editing the interactive instructions, going to commit 8, editing that commit, then cherry-picking 9-15 from the temporary branch. But it's too hard to do in practice, and far too easy to get wrong.

replies(1): >>45087026 #
36. 1718627440 ◴[] No.45086981{5}[source]
SSH is definitely easier than Dropbox :-).

Yes, and my point is that having a rebase and edit everything isn't too different from first modifying everything and then doing an automatic rebase.

37. 1718627440 ◴[] No.45087026{5}[source]
> [1] There technically is a way to do this

That's what I've described?

> rebase -i, carefully editing the interactive instructions

You neither need to use interactive rebase nor carefully edit, since there is rebase --onto.

> But it's too hard to do in practice, and far too easy to get wrong.

I do this often it's not more complicated then any other rebase.

What is annoying in Git is rebaseing across multiple merges while forging committer and date information. Can JJ do that better?

replies(1): >>45087470 #
38. devnullbrain ◴[] No.45087384[source]
Some repos merge/pull changes by rebasing them on main and want eventual history not actual history.
39. sunshowers ◴[] No.45087470{6}[source]
Ah I misinterpreted what "keep the rebase commits" meant.

I'm glad you don't find it too difficult to do. It's a workflow that seemingly works well for you!

40. hdjrudni ◴[] No.45087841{5}[source]
I forget where I read it (maybe from Martin), but one reason I like to `jj new` before I start any work is just because it makes it easy to revert or abandon if I don't end up liking it. And also easy to diff. `jj new` is pretty much 'free' anyway, every time you make an edit you're creating new commits (not changes!) anyway.
replies(1): >>45088700 #
41. adastra22 ◴[] No.45088030{7}[source]
Sorry if I’m being dense, but I still don’t get how this is any different. If I interactive rebase and stop to edit a commit, isn’t that the same thing?
replies(1): >>45089153 #
42. adastra22 ◴[] No.45088072{8}[source]
To expand on this(since I asked the question), I see mostly downside as the branch with G is unusable until the conflicts are resolved. Being able to keep a merge halfway resolved is a nice CLI shortcut. There should be a stash command for this, and I expect there probably is, or it can be done with work trees.
replies(1): >>45092493 #
43. adastra22 ◴[] No.45088090{6}[source]
Not everyone works by “logical checkpoint” commit strategy. I myself want my tree clean and commits being atomic state transitions. Otherwise git bisect (which I rely on) would break.

A lot of the jj strategies in this thread are a bit more cowboy, and I’m surprised.

replies(1): >>45089163 #
44. BeetleB ◴[] No.45088700{6}[source]
Oh I'm good with jj new for any new thing I'm working on. But I was wondering why one would use jj new -A to fix a commit in the past vs jj edit,
replies(1): >>45088815 #
45. kps ◴[] No.45088815{7}[source]
Mostly to keep the new changes isolated from the original commit until they're ready, I think. The only time I use `jj edit` is to resume a leaf I left in the middle of something (where I might have used `git stash pop` but without needing to stash).
46. tux3 ◴[] No.45088997{4}[source]
When you force push a PR, Gitlab shows the changes from the last push. So it also depends which forge you use. I could see that working less well on Github or simpler Git forges
replies(1): >>45092976 #
47. Izkata ◴[] No.45089139{4}[source]
> Once the PR has been merged, I prefer it merged as a single squashed commit so it's reflective of the single atomic PR (because most of the intermediary commits have never actually mattered to debugging a bug caused by a PR).

While working on a maintenance team, most of the projects we handled were on svn where we couldn't squash commits and it as been a huge help enough times that I've turned against blind squashing in general. For example once a bug was introduced during the end-of-work linting cleanup, and a couple times after a code review suggestion. They were in rarely-triggered edge cases (like it came up several years after the code was changed, or were only revealed after a change somewhere else exposed them), but because there was no squash happening afterwards it was easy to look at what should have been happening and quickly fix.

By all means manually squash commits together to clean stuff up, but please keep the types of work separate. Especially once a merge request is opened, changes made from comments on it should not be squashed into the original work.

replies(1): >>45093055 #
48. steveklabnik ◴[] No.45089153{8}[source]
You have to finish the rebase before git will let you move in to other work. jj will not. With git you can stop in the middle to make changes, but you must continue until the rebase is done.
replies(1): >>45089925 #
49. steveklabnik ◴[] No.45089163{7}[source]
The key is to be cowboy until you’re happy with things, and then get clean, just like with git. It’s way easier to slice and dice commits with jj and so you can be sloppy at first and it’s easy to turn beautiful afterward.
replies(1): >>45089939 #
50. sfink ◴[] No.45089421{5}[source]
My personal opinion: `jj new` is better because it's non-modal. If you use `jj edit`, you're sort of switching to "edit mode": any change you make will trigger a rebase of all descendants.[1] You're live-mutating the core graph structure rather than a harmless appendage node. Also, if you notice something else that needs to be fixed, you can do it but then you'll need to remember to split it out into a separate commit before leaving edit mode.

With `jj new` + `jj squash`[2], you're collecting work that you can review as a separate thing anytime as you go along. You don't have to remember anything. If you throw in an unrelated change, you'll notice it if you review the changes before squashing them, so you can split it out then. And I'm pretty much always working in this state even when I'm at the top of my branch, so `jj new some-deep-node` doesn't really change anything. If I get called away and have no memory of what I was doing when I return, it doesn't matter: my jj state tells me exactly where things are and what I was doing.

[1] Which is not a huge problem, you have deferred conflict resolution so if something goes wrong you can probably just repair it with normal editing or your editor's undo functionality.

[2] I don't usually bother with `jj new -A`, since I'm going to squash my "out of line" temporary commit into the linear chain anyway. `jj new -A` is more similar to `jj edit` than `jj new` -- it shares some but not all of the modal disadvantages. So perhaps my answer to your actual question is: "yeah, I dunno either."

replies(1): >>45089922 #
51. codethief ◴[] No.45089453{7}[source]
I had been wondering for quite some time what people meant when they said "you can resolve conflicts later". Thanks so much for this excellent explanation!
52. BeetleB ◴[] No.45089922{6}[source]
Indeed, that was my point. jj new -A would also trigger rebases.
replies(1): >>45107189 #
53. adastra22 ◴[] No.45089925{9}[source]
Ok. There are simple workarounds. I git rebase —quit and then make a branch for this saved rebase progress to return to later. I then restart the rebase from the corresponding commit. It would be much nicer if this was all wrapped up a simple command.

There is value here, but I think it is more like “add a new command consisting of 50-100 lines of code” not “write an entirely new VCS.”

replies(1): >>45090285 #
54. adastra22 ◴[] No.45089939{8}[source]
That’s not how I use git, at all. I have a messy workspace with a lot of things going on simultaneously, and only selectively stage when something crosses the finish line. Sounds very difficult to do this in jj.
replies(3): >>45090278 #>>45091465 #>>45095204 #
55. steveklabnik ◴[] No.45090278{9}[source]
Selectively staging is easier in jj. I loved git’s index, jj does it better.
replies(1): >>45094652 #
56. steveklabnik ◴[] No.45090285{10}[source]
One little thing here, one little thing there, it all adds up. The jj model is more orthogonal, and so ends up being easier, which also translates to more power.
57. thecupisblue ◴[] No.45090987{3}[source]
Yes, but in that case, I want the fix of the original mistake to be done in a new commit.

Why?

Example #1: - I am working on implementing API calls in the client, made 3 commits and opened a PR - In the meantime, the BE team decides they screwed up and need to update the spec

If I now go and fix it in the commit #1, I lose data. I both lose the version where the API call is in its original state, and I lose the data on what really happened, pretending everything is okay.

Example #2: - I am writing a JVM implementation for our smart-lens - In commit #2 I wrongly implement something, let's say garbage collection, and I release variables after they have 2 references due to a bug. - I am now 6 commits ahead and realise "oh shit wait I have a bug"

If I edit it inline in commit #2, I lose all the knowledge of what the bug was, what the fix is, what even happened or that there was a bug.

tldr: just do an interactive rebase

replies(1): >>45112884 #
58. sgjennings ◴[] No.45091465{9}[source]
You can work exactly this way using the “gitpatch” diff editor[1].

1. Your working copy contains whatever mish-mash of changes you want.

2. When you’re ready to stage and commit these changes, run `jj commit --tool gitpatch`

3. The iterative “stage this hunk?” UI from git lets you choose what to commit.

4. Your editor opens for a commit message.

5. The changes you selected are now in a new parent commit of your working copy, and the remaining changes are left in the working copy commit.

In addition to the _same_ workflow, jj makes it easier to have other workflows as well (you may be interested in the megamerge workflow if you’re always working on multiple tasks at once).

[1]: https://zerowidth.com/2025/jj-tips-and-tricks/#hunk-wise-sty...

59. baq ◴[] No.45092493{9}[source]
The branch is indeed unusable, as opposed to the whole repository being unusable - it's a very nice upside actually.
replies(1): >>45097695 #
60. KallDrexx ◴[] No.45092976{5}[source]
Yeah I don't have much experience outside of GitHub for team projects, so maybe gitlab works better. For GitHub it just gives up and claims it can't give you diff since the last review
replies(1): >>45097268 #
61. KallDrexx ◴[] No.45093055{5}[source]
I wonder by your last comment if this is just is talking past each other.

I try very hard to keep my PRs very focused on one complete unit of work at a time. So when the squash happens that single commit represents one type of change being made to the system.

So when going through history to pinpoint the cause of the big, I can still get what logical change and unit of work caused the change. I don't see the intermediary commits of that unit of work, but I have not personally gotten value out of that level of granularity (especially on team projects where each person's commit practices are different).

If I start working on one PR that starts to contain a refactor or change imthat makes sense to isolate, I'll make that it's own pr that will be squashed.

62. adastra22 ◴[] No.45094652{10}[source]
How? The jj documentation seems to be pretty clear about getting rid of the staging area.
replies(3): >>45094935 #>>45095953 #>>45096638 #
63. baq ◴[] No.45094935{11}[source]
staging isn't needed since everything including the working copy is a commit you can split, rebase, etc. without any checkin step. yes, it does make sense and yes, it works in practice - the uncommitted vs staged vs committed states are very git-specific and don't need to be special in any way. if you check in the wrong thing (note - your working copy is a commit, so by definition you can't not have wrong things checked in at times) you split it (stage and/or commit in git).
64. BeetleB ◴[] No.45095204{9}[source]
> Sounds very difficult to do this in jj.

Pretty easy. While inaccurate, it's useful to think of jj as two separate repositories. One is the "clean" one that has everything nice and neat. The other is a repository of all your (very) incremental changes.

You have to actively decide what goes in the "clean" one. jj automatically puts stuff in the messy one. Any time you actively commit something, you're committing to the clean one. So you decide what goes in there.

When you do a push, only the "clean" commits are pushed.

65. Human-Cabbage ◴[] No.45095953{11}[source]
There are a couple main ways to achieve a workflow similar to Git's staging area.

#1: Squashing

Create a revision for the feature, then create another revision atop that.

  $ jj new main -m 'feature'
  $ jj new
  $ jj
  @  trtpzvno samfredrickson@gmail.com 2025-09-01 12:32:33 9ac76a0f
  │  (empty) (no description set)
  ○  wvzltyyr samfredrickson@gmail.com 2025-09-01 12:32:31 80b2d5d0
  │  (empty) feature
  ◆  zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
  │  all the stuff
  $ vim
  $ jj
  @  trtpzvno samfredrickson@gmail.com 2025-09-01 12:34:50 5516c2b9
  │  (no description set)
  ○  wvzltyyr samfredrickson@gmail.com 2025-09-01 12:32:31 80b2d5d0
  │  (empty) feature
  ◆  zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
  │  all the stuff
  ~
  $ jj squash -i
  # interactively choose hunks to squash into parent
  $ jj
  @  oxqnumku samfredrickson@gmail.com 2025-09-01 12:35:48 8694aa34
  │  (empty) (no description set)
  ○  wvzltyyr samfredrickson@gmail.com 2025-09-01 12:35:48 47110bff
  │  feature
  ◆  zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
  │  all the stuff
  ~
#2: Splitting

Create a revision for the feature, then split it up retroactively.

  $ jj new main -m 'feature'
  $ jj
  @  snomlyny samfredrickson@gmail.com 2025-09-01 12:38:39 84c6ecaa
  │  (empty) feature
  ◆  zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
  │  all the stuff
  ~
  $ vim
  $ jj
  @  snomlyny samfredrickson@gmail.com 2025-09-01 12:39:51 8038bdd4
  │  feature
  ◆  zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
  │  all the stuff
  ~
  $ jj split
  # interactively choose hunks to keep, splitting the rest into a new revision
  $ jj
  @  zpnpvvzl samfredrickson@gmail.com 2025-09-01 12:41:47 5656f1c5
  │  debugging junk
  ○  snomlyny samfredrickson@gmail.com 2025-09-01 12:41:44 1d17740b
  │  feature
  ◆  zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
  │  all the stuff
  ~
66. steveklabnik ◴[] No.45096638{11}[source]
Because it’s not a distinct feature, it’s just another commit. Which means you can bring all of the usual tools for modifying commits to bear. My sibling has some details, but at the high level, that’s the idea.
67. steveklabnik ◴[] No.45097268{6}[source]
GitHub is basically worst in class here, it’s true. Some forges are slightly better, others are way better. It’s so sad because I like GitHub overall but this is a huge weakness of it.
68. yencabulator ◴[] No.45097695{10}[source]
Making a local worktree/shallow clone is dirt cheap, if you're worried about `git rebase` not storing its state in a restartable fashion.

jj's handling of merge conflicts is pretty much like in Git committing the conflict markers in git and editing the commit message to say "conflicting".

69. sfink ◴[] No.45107189{7}[source]
Heh, sorry. The `-A` part went whooshing over my head until I had already written up the rest. I just saw `jj new` and got triggered into a "well akshually..."
70. keybored ◴[] No.45108989{4}[source]
Most of the bad modern Git practices summed up in one comment (one atomic, squashed comment).
71. phatskat ◴[] No.45112884{4}[source]
From what I understand of how jj works, and I’m happy to be corrected because I’m still learning it, is that going back and editing those commits doesn’t change actual original “change” - that is, jj tracks a change ID to everything, and those original commits (once pushed) are immutable. So in theory, with jj, you should be able to see the original commit and the change to fix it, and you can still couple them into a single commit without losing that change history.