1. 74
    1. 23

      Separately from jj (I’m a fan), it’s a criticism of how Git very much could have a suite of highly-flexible tools that all work well together, but doesn’t for strictly UI + design reasons.

      It’s not like the Git object model is what’s preventing its UI from being sane (vs Mercurial revlogs, for example). The jj-specific thing in the article is change IDs, which are nice but not necessary for this workflow.

      [rant] Stashes in particular shouldn’t exist; their design is a hack.

      • Cursed trivia: the stash stack is stored as entries in the reflog of the appropriate special reference.
      • If Git is actually good at lightweight branching, then it should be no problem to lightweight-branch your current work, then do something else, and come back to it later, and maybe move some stuff around, rather than using the stash feature.
        • I’m sure it’s more lightweight than many of its predecessors, but it’s hard to call its branching “lightweight” in an objective sense when it contains workarounds for not having lightweight branching within itself.
      • Removing stashes as a feature and then wrapping existing functionality to accomplish the same workflow would improve the Git UI.
        • I’ve seen Git UIs that, instead of literally stashing changes, will auto-generate a branch for the current changes and then switch to the parent commit, which is a nice way of accomplishing the same workflow while removing the extra concept.
        • You could add e.g. git commit --stash to the CLI today to do the same.

      Obviously, you can do all this stuff today with enough abstraction around the Git toolbox. But, like the article mentions, the author constantly has to make immaterial decisions, not because Git is capable of too little, but too much.

      1. 6

        For some other workflows, change IDs do seem to be a lot more fundamental. I’ve tried to accomplish my “patchset” workflow (maintaining bunches of independent changes, all merged together, all constantly rebased on top of an upstream) with git-branchless and it did nooooot work at all. With jj it’s basically trivial.

        1. 2

          How do change IDs help with that workflow?

          1. 2

            I haven’t tried git-branchless, but I’m assuming it would have a similar result as doing the same with plain git; your rebases will produce totally different commits, including the rebase of the super-merge (which might end up looking completely divergent to all the other branches). jj preserves the identity of all the changes and their relationships through the rebase.

            1. 8

              Git does the same if you rebase with the magical --update-refs --rebase-merges options. With these options the branchless workflow - including keeping branches and supermerges intact - just work. I have written a detailed comment about how to combine that with a declarative rebase todo-list, and you don’t even need git-branchless for it.

              Obviously I will concede that git’s CLI UX isn’t quite as polished as jujutsu’s, but it’s nothing that can’t be fixed by writing some shell wrappers.

              I would also argue that the git manoeuvre section makes it quite a bit more complicated than it should be, as the only real UX problem here is the stashing, which can be handled simply by using the --autostash option. I also think it’s much nicer to be able to just visually move the commit, than having to manually note down the ID with jj log and then do the rebase.

              1. 4

                I agree that you can probably do a mega-merge workflow in stock Git with the rebase sequencer, but regarding

                it’s nothing that can’t be fixed by writing some shell wrappers.

                I think that’s underselling the complexity of the desired operations a bit 😅.

                • For example, the git-branchless code to support its rebase interface has to do topological sorting on the commit graph, identify connected components, and perform a basic two-phase constraint resolution.
                  • That doesn’t include e.g. the performance optimizations necessary for large repositories, some of which have to run during the compilation phase rather than the execution phase.
                  • I wish that it were feasible to do it in a shell script!
                  • Fun fact: for on-disk rebases, git-branchless actually generates and writes a rebase plan to disk and directly calls git rebase --interactive to execute it (so I do appreciate that Git itself is largely capable of it).
                • jj no doubt has similar code to git-branchless, plus handling for its Git-incompatible features like first-class conflicts, of course.
                1. 2

                  I think that’s underselling the complexity of the desired operations a bit

                  I disagree, are you sure, that you actually tried out --update-refs --rebase-merges? I encourage you to look at my linked comment, because there are no hidden gotchas. I understand that your git-branchless tool has probably been able to do it a long time before --update-refs --rebase-merges even existed, but nowadays it’s really as simple as that, there is no need for support scripts to do any topological sorting shenanigans (and internally git just uses a simple three-phase traversal).

                  For purposes of using a stacked-branches workflow there just is no longer a need for using git-branchless (or jujutsu for that matter), it works just fine in stock git - and my git shell wrappers are not really much more sophisticated than simple shell aliases.

                  In another comment you also mention that git-branchless is bad at rebasing merges, which is also working just fine with stock git (that’s the entire purpose of --rebase-merges), I wonder what git-branchless does differently internally to cause problems there.

                  1. 7

                    I encourage you to look at my linked comment, because there are no hidden gotchas.

                    Last I checked, Git was not able to actually rebase merge commits. Instead, it recreates the merge commit. If you had any changes in the commit, then those changes would be lost AFAIU. Yes, having changes in a merge commit is what Git calls an “evil merge”, so you can of course argue that you’re shooting yourself in the foot if you have changes in the merge commit. If you do the same with Jujutsu, however, you won’t actually shoot yourself in the foot.

                    1. 3

                      Last I checked, Git was not able to actually rebase merge commits.

                      No it can, that’s the entire purpose of --rebase-merges.

                      Instead, it recreates the merge commit.

                      That’s blatantly false, it just reuses the commit message of the previous merge commit (that’s what the -C in the interactive rebase is for after all). The actual merge is a proper merge of the new rebased histories.

                      changes in a merge commit is what Git calls an “evil merge”

                      I don’t know why you are so focused on evil merges again, but with --rebase-merges there are no evil merges. It’s simply not a problem, as git properly remerges the new histories as you would expect.

                      Also I am forced to have to call you out on it, since you for the second time in a row very obviously didn’t take the time to check out --update-refs --rebase-merges or at the very least take a look at the man page git-rebase(1) section REBASING MERGES: I took the actual time to try out jujutsu to see what the differences are. I think it’s slightly disrespectful that you disregard these options and then only quote my superficial starting sentence and ignore the entire rest of my comment.

                      P.S. It also kinda hurts to see this completely incorrect statement upvoted more than the actually well written-out and much more technical sibling comment, seems lobste.rs has the same problem as Reddit after all where hype over new technology (and certainly so with the third jujutsu post within one week) stands out more than technical discussion.

                      1. 3

                        When I said that Git isn’t able to actually rebase merge commits, I meant that it’s not able to rebase the changes in the merge commit. I know it preserves the metadata. Sorry, I think I’ve just gotten so used to thinking that that’s how merge commits should be rebased that I didn’t think to clarify what I meant by it.

                        I’m pretty sure the Git project would like to rebase merge commits better, they just haven’t figured out how. And I don’t blame them. I had spent a lot of time thinking about that myself while working on Mercurial at work. Then I just stumbled on a good solution while adding support for conflicts in Jujutsu. See this thread on the Git mailing list from earlier this year if you’re curious: https://lore.kernel.org/git/ee35f3b2-bf20-6fcc-2c71-38499aa592fe@gmx.de/ where they (and I) discuss how to improve it.

                        I think the rest of the misunderstanding are a consequence of me not explaining clearly.

                        I think it’s slightly disrespectful that you disregard these options and then only quote my superficial starting sentence and ignore the entire rest of my comment.

                        It is true that I didn’t try them, but I think I know pretty well how they work nonetheless. (I’ve actually done a bit of work on the rebase code in Git itself, but that was before both --update-refs and --rebase-merges.) I figured arxanas would reply to the rest of your post (which they did).

                        1. 3

                          Thanks for clearing that up, that makes a lot more sense. Indeed you are right, git does not carry along the conflict resolution when rebasing merges with merge conflicts.

                          However, a quick test confirms that rerere was able to pass along the resolution anyway, so I fail to see where this would ever cause a problem in practice, especially since the stacked patchset workflow makes hardly any sense for patchsets that are in conflict with each other. It’s mostly used when they depend on each other or when they are completely independent, both of which don’t cause any conflicts in the first place.

                          1. 2

                            Let’s say you have patches A, B, and C. A and B are independent but C depends on both of them. If reviews of the PRs take a while, it can be useful to have both A and B out for review in parallel and be able to merge them in any order.

                    2. 7

                      I read your comment when you posted it originally and now as well, and I don’t think we disagree about the capabilities or utility of git rebase --update-refs --rebase-merges. In fact, I used to use a rebase-todo-oriented workflow for a brief period of time before I gave up because it was too much overhead for the stacked branch operations I wanted to do.

                      I think we disagree on what operations are necessary or desirable for these stacked-branch workflows. For example, I believe that the general behavior of jj rebase -r X -d Y is not possible to express with a single non-interactive git rebase invocation (or shell script), but I think that’s a highly desirable operation for a stacked branches workflow. Perhaps you disagree?

                      • It gets even more complicated when you want to support flags like --insert-before/ --insert-after.
                      • It is possible to start an interactive rebase and adjust the plan yourself to accomplish similar goals, if you’re willing to spend the time to do so; I was not, and I opted to automate it.

                      As far as I know (and maybe you can correct me here), there are some fundamental lack of features in the git rebase interface, which means that, in general, you have to either decompose your intended operation into multiple lower-level operations that satisfy the below constraints, or manually do some surgery in the interactive rebase editor:

                      • For the source commits, git rebase can only express a commit range with a single “root” (shared ancestor) and “head” (shared descendant):
                        • If you want to rebase a set of commits without a common head, then I believe the established workaround is to generate a dummy merge commit as the head, use it to specify the range, and then remove it once you’re done.
                        • If you want to rebase a set of commits without a common root, then I think that’s just not possible (but that’s fairly strange, and I imagine that it’s quite rare and not a problem in practice).
                          • More realistic is that the source commits do share some common ancestor, like you want to address two branches off of main, but you don’t want to include all of the commits to main which are descendants of the common ancestor.
                        • I believe git replay will remove the “single-head” limitation.
                      • For the source commits, git rebase cannot express non-contiguous ranges.
                        • One common use-case is to “splice” a commit (or set of commits) into an unrelated branch; you have to ensure that its descendants are rebased onto the appropriate ancestor commits. (I think it can be worked around with the interactive rebase editor, but you will need to organize it so that both the source and destination commits, as well as the descendants of the source commit, all appear in the boilerplate, or else it gets really tedious.)
                      • For the destination commits, git rebase can only express exactly one parent.

                      I don’t think you can overcome these limitations with just a shell script, or at least git-branchless and jj didn’t figure out a general approach that was as simple as the three-phase approach for Git that you linked. (There is no fundamental limitation in the sequencer; you can definitely write a rebase plan by hand or use a tool like git-branchless to generate it for you.)

                      The git-branchless bugs regarding merge commits primarily relate to the implementation difficulty of overcoming these limitations and supporting a more general rebase operation. (I don’t use a merge workflow, so I never prioritized fixing those bugs. I don’t believe there’s any fundamental design reasons that would prevent it.)


                      Tangentially: I believe that jj’s first-class conflicts and better default conflict rendering are good reasons to consider using it in both mega-merge and linear stacked branches workflow, separately from the actual rebase functionality. There have been times when I did certain operations with jj in an otherwise git- or git-branchless-only project specifically to rely on these features.

                      1. 3

                        Thanks for taking the time to write this out.

                        the general behavior of jj rebase -r X -d Y is not possible to express with a single non-interactive git rebase invocation (or shell script), but I think that’s a highly desirable operation for a stacked branches workflow. Perhaps you disagree?

                        No, I agree, and yes you don’t want to do that with non-interactive git rebase. However I don’t understand why you want to exclude the rebase todo-list as a workflow, because arguably it’s a more intuitive workflow than being forced to manually note down commit IDs from jj log (which also sucks if you want to rebase multiple commits at the same time).

                        Furthermore for more contrived graph examples, the forced splitting of the rebase todo-list into separate graph components is much more easier to grasp than a jj log displaying them all intertwined or any GUI for that matter. That’s a big “IMHO” obviously, but I always struggled with understanding the --graph --pretty style formats as soon as there are more than two simultaneous lines.

                        It gets even more complicated when you want to support flags like –insert-before/ –insert-after.

                        It’s just as simple if you use the rebase todo-list.

                        It is possible to start an interactive rebase and adjust the plan yourself to accomplish similar goals, if you’re willing to spend the time to do so

                        I don’t see how moving a single line with a text editor (and it is as simple as that, even in complicated “Move between branches” workflows) is time consuming at all.

                        If you want to rebase a set of commits without a common head

                        In the branchless workflow this can’t happen as you are always working in a supermerge of all branches (so they all have a common head). However even in the case that you want to move commits from your current stacked patchset to a completely different branch, you can simply create a new stacked patchset that contains the common necessary subsets (which is as simple as merging the related branches from the stacked patchset). Then afterwards you can reuse the usual rebase with the branchless workflow. There is no need to delete a “dummy merge” afterwards.

                        If you want to rebase a set of commits without a common root

                        Every commit should have a common root, at latest the initial commit. Obviously you don’t want to have your entire history in your rebase todo-list, but usually the first common commit also comes much later than that.

                        need to organize it so that both the source and destination commits, as well as the descendants of the source commit, all appear in the boilerplate

                        The boilerplate is already generated by git, however I agree that splicing a commit that is not the current HEAD is a usecase that is tedious with git, as noone likes stopping in the middle of a rebase. Git does not currently have a way to do a jj split -r for non-trivial revisions, I will give that point to jujutsu. ;)

                        1. 4

                          However I don’t understand why you want to exclude the rebase todo-list as a workflow, because arguably it’s a more intuitive workflow than being forced to manually note down commit IDs from jj log (which also sucks if you want to rebase multiple commits at the same time).

                          Actually (perhaps it’s an unpopular opinion?), I do miss the interactive rebase editor in jj:

                          • I hope that jj improves the WYSIWYG workflows in the future, possibly by introducing its own interactive rebase tool, or possibly by offering some other UI-based tool.
                            • I would personally prefer a tool that lets me modify e.g. the existing log output interactively, rather than a linear todo-list, for reasons discussed below.
                          • I think it just hasn’t been prioritized since the existing CLI tools handle most of the use-cases, although potentially with more friction.
                          • I don’t think that the current CLI is an optimal way to express all common rebase patterns in a succinct way.
                            • But, overall, if I had to choose either git log + git rebase -i or jj log + jj rebase (non-interactive), I would probably choose the jj suite.

                          However, I think the representation of non-linear graph structures in git rebase -i can be fairly convoluted once the graph structure is complex enough:

                          • If I have ten or twenty or fifty commits in my current work with a great number of diverging sub-branches, then reading all the labels and trying to recreate the topology in my head is difficult for me.
                          • In such cases, I typically have many experimental sub-branches, rather than many mostly-competed sub-branches.
                            • I’ve actually had 50+ commits in a single unit of local development before. Most of them are experimental commits in those cases. The max depth might be more like 15-25 in those cases, though (not sure).
                          • With the linear todo-list, it’s much harder to determine at a glance, for example, what the nearest common ancestor of two arbitrary commits in my current work is.

                          I personally can work with the jj log output more effectively in the highly-divergent cases:

                          • It’s obvious at each divergence point that divergence is occurring, and limits the amount of forward-/backward-scanning I have to do (but doesn’t completely eliminate it).
                          • I agree criss-crossed lines in both git and jj graphs quickly become difficult to read.
                          • It’s fortunate that the change IDs in jj are abbreviated so I can type them quickly instead of copy-and-pasting, but I agree that the ideal UI would render them unnecessary altogether for these operations, and git rebase -i is better in many cases, like if you want to just swap two lines.

                          In the branchless workflow this can’t happen as you are always working in a supermerge of all branches (so they all have a common head).

                          It seems like you have a more specific workflow in mind with the term “branchless”. By “branchless”, I just mean that I’m working with anonymous heads rather than named branches, hence there are no branches:

                          • Such a workflow certainly includes the “mega-merge” workflow that I believe you’re describing.
                          • I don’t often use merge commits. Instead, I do a lot of divergent tree-structured development where most heads aren’t compatible anyways and shouldn’t/can’t be merged together.
                          • When there aren’t common heads, I think we agree that git rebase -i doesn’t let you operate on them easily.
                          • That being said, I don’t think my approach is automatically superior approach: I eventually have to merge some of the commits together, and I just happen to do it by linearizing the history.
                            • The repositories I work in usually practice trunk-based development, so I’d have to arbitrarily linearize them at some point anyways.

                          I think there are definite advantages to a mega-merge workflow that I could start to use in my local workflows at some point, though:

                          • I think code review tooling would benefit if my reviewers could understand the exact dependencies between code review items.
                          • I am probably losing some flexibility and doing work early by linearizing commits too early in some cases. In particular, sometimes I have to reorder commits to land them as soon as a reviewer approves them.
                          • However, the code review tools I’ve used honestly don’t support non-linear review patterns well, nor do they support non-linear committing/landing/merging to main. So a lot of the benefits that I would immediately make use of, for the benefit of code reviewers, actually aren’t available to me due to deficiencies in other non-local tooling.

                          It’s just as simple if you use the rebase todo-list. […] I don’t see how moving a single line with a text editor (and it is as simple as that, even in complicated “Move between branches” workflows) is time consuming at all. […] Every commit should have a common root, at latest the initial commit. Obviously you don’t want to have your entire history in your rebase todo-list, but usually the first common commit also comes much later than that.

                          I mentioned previously that the divergent structure in the todo-list is hard for me to reconstruct mentally. Also, I don’t have a common head since I’m not merging most heads together, so I can’t directly use the todo-list for many kinds of operations.

                          But besides those things, in my workflows, there can be thousands of ancestors on main in the list when I’m trying to move a commit from one line of development to another.

                          • For example, because one of those development lines is not rebased onto the latest main, likely because it wouldn’t merge cleanly.
                          • The todo-list is very unpleasant for me to use in those cases, and I prefer to use a tool that operates on disjoint commit sets and doesn’t render unimportant commits.
                          • git log itself also has this as a limitation/downside. I believe there’s no way to elide certain sub-ranges of the commit graph.

                          Thanks for sharing your perspective. If I’m understanding the differences in our workflows correctly:

                          • It sounds like your workflow works a lot better for you than it would for me since you more often have a meaningful common “root” and “head” commit for the branches you’re working with.
                          • Unfortunately, I think it would be difficult for me to adopt either a meaningful common root or meaningful common head and still support the divergent development cases that I tend to do in practice.
                          • I should probably use merge commits more often than I do right now.
                2. 3

                  Exactly, and rerere barely helped at all there BTW.

                  1. 2

                    I don’t see what change IDs or consistent commit identity has to do with the mega-merge workflow, or how rebases produce totally different commits as a result. I could imagine some other feature of jj helps (first-class conflicts, merged-tree simplification, maybe it has a different algorithm for rebasing merges), but how do change IDs or consistent commit identity help? As mentioned in the sibling comment, it’s even possible to do in stock Git; the main problems in my view would be addressed by better conflict and merge handling.

                    (git-branchless actually tracks commit identity across rewrites, but it stores edges like Mercurial’s commit evolution rather than nodes like jj’s change IDs. But I would expect git-branchless to be bad at rebasing merges because there’s several open bugs and the UI is really bad at present.)

            2. 3

              I think it’s more about how you as a person work, I use stashes completely different than branches, and I wouldn’t want the same UI.

              but yeah, maybe some aliases that would make it bearable, e.g:

              git brs = show only stash branches, e.g. if I named them "st/NAME"
              git br = show non-stash branches, aka proper branches not named "st/NAME"
              
            3. 8

              I’ve been using jj for a few months now (after using Git for over a decade), ever since I read @steveklabnik’s Against Names blog post here on lobste.rs. It’s incredible how much it’s changed my workflow for the better, and this article is a pretty good example.

              I’m a fan of clean, logical git histories, because they make code reviews simpler - and jj makes accomplishing that so much easier than git rebase. They way I’ve been describing it to people at work is “What if everything was rebase?” Which isn’t totally accurate, and jj does more than that, but it’s how I think about it.

              1. 5

                My notes for this post actually include the line “jj: you’re always in interactive rebase” :D!

              2. 7

                I hovered over “big, old, legacy codebase” and when I saw the destination I was both surprised and scared. My apologies for any code 10 years younger Wesley wrote.

                1. 5

                  Haha, what a feeling! A quick blame over the codebase shows 6.4% of lines are still last modified by you, so I think you’ve done very well.

                  1. 2

                    Another Radiopaedia alumnus checking in, but much less prolific than @wezm. My apologies in advance for having to deal with my first steps learning React.

                    1. 3

                      Not to worry — I’ve been on and off the codebase in one way or another for nearly 4 years now, and I don’t think I’ve yet had to swear your name while paging breathlessly through git blame!

                  2. 3

                    I do sometimes feel the need to apologize to people when I find out they joined teams I left and had to work with my code. RIP Cloud SQL, Google Open Source Projects Office engineers :(

                  3. 3

                    With jj, it seems like you constantly have to shuffle around these logical commit IDs that have no meaning at all.

                    If I rebase a stacked series of branches with --update-refs, I just identify the fixup line, move it up after it’s corresponding commit (identified by its description, of course), and change it to f for fixup. Because jj doesn’t have interactive rebasing, it seems like you can’t do this?

                    The interactive rebase is like a GUI, it shows me things I don’t need all the time. Jujutsu seems like it insists on me shuffling around its logical IDs. But I’d rather just have my own good names, i.e. branch names.

                    1. 6

                      You may’ve already, but if not I’d say give it a legit try. Maybe for a few weeks.

                      I’ve fully switched over at this point. And, FWIW (n=1, here), I don’t feel like I”m “constantly having to shuffle around” change IDs.

                      If I wanted, I could use bookmark (ie branch) names. But, really, for short-lived branches (which is nearly all I need), it just feels unnecessary in jj.

                      The workflow, once I broke free from the git garbage (poison, one might argue) in my muscle memory, feels streamlined and productive and simpler, while remaining at least as functional.

                      1. 2

                        I gave jj a try and found it really nice to use on clean, well established projects. But on my personal, experimental projects it was quite a hassle. I don’t think it’s fault of jj, as it was not built with such use cases in mind: namely, a lot of uncommitted files, half written tests that will eventually be merged, code that is used in the repo, but must never be committed to the repo, and so on.

                        1. 4

                          I do similar stuff by using a squash workflow: I have a “local” change that has a lot of random stuff I don’t really want to push, and run a lot of code from there. When I’m ready to push, I create a change before the local, and squash the things I actually want to add to the repo into that change, and push that as a PR.

                          The benefit of this over just untracked files is that I still get the revision history, so if I end up wanting to revert one of the changes to my local files it’s easy to do so.

                          1. 3

                            Sounds like a nice workflow.

                            But the split command needed to selectively take pieces of a change is IMO such a subpar experience compared to git add -p, at least out of the box, where it ended up launching the extremely clunky (for me) emacs-ediff.

                            Would be great if something incremental like git add -p eventually landed in jj.

                            1. 3

                              I don’t use split, because I don’t love the UI. Instead, I use squash: I create an empty Change before the one I’m working on, and squash the files I want into the (new) parent. And jj squash --interactive lets me select specific sections of files if I need to.

                              Definitely agree it would be nice to have something with the ui of git add -p

                              1. 4

                                If you have specific feedback about how the built-in jj split UI could be improved vs git add -p, you could leave it in Issues or Discussions for the scm-record repo.

                                1. 2

                                  Speaking personally, I think you already captured some of my issues in your README (discoverability, predictability). But more than that, it’s the fact that, being used to git add -p, I want something like that, instead of a worse version of magit (worse mainly because of the different key-bindings compared to my editor, I must say).

                                  Just my 2 cents :)

                          2. 3

                            You can disable auto add of new files as of a few weeks.

                            1. 1

                              Specifically, it looks like the way to disable auto-track is to configure snapshot.auto-track to be none(). Then you would use jj file track to track files.

                              If you only want to ignore files in certain directories or files with predictable names, you can use a .gitignore file instead.

                            2. 3

                              Do private commits work for the case you’re describing here?

                              https://martinvonz.github.io/jj/latest/config/#set-of-private-commits

                          3. 2

                            Out of curiosity, in your example you move a fixup to the proper place and change the type to f for fixup. Are you aware of git rebase -i --autosquash which does it automatically or am I missing something? Of course, with the interactive rebase you can do much more than that but I was wondering.

                            1. 2

                              I know about it but it’s not a major burden so I haven’t looked into it. And people are telling me to just learn Jujutsu so maybe I shouldn’t be learning new Git features?

                              1. 1

                                I’m not the person you asked, but I am an example of someone who knows about --autosquash yet still prefers the workflow of git rebase -i and manually typing the f. The main reason is that, for my project at work, I don’t like how the required “fixup! ” prefix makes the commit stand out in git log:

                                1. SOMEPROJ-1234: update dependency
                                2. fixup! SOMEPROJ-1234: fix failing test
                                3. SOMEPROJ-1234: add support for foo

                                Given this project’s requirement to mention a ticket in each commit, the prefix looks too out of place. So I prefer to write “(squash)” after the ticket name in the message and then search for lines with that phrase by eye in my git rebase -i editor:

                                1. SOMEPROJ-1234: update dependency
                                2. SOMEPROJ-1234: (squash) fix failing test
                                3. SOMEPROJ-1234: add support for foo
                                1. 1

                                  Why include the ticket number in the fixup commit’s message? It gets absorbed into the previous commit; your history will not include it post rebase.

                                  1. 1

                                    Good question – I had to think a bit to remember. It’s because my workflow sometimes includes pushing the fixup commits as-is to the remote branch. I do that when I was pairing with another dev on my computer and I want to make it easy for them to see the history of the work we accomplished that day, which would be harder if I squashed before pushing. When working on large changesets like that, I only squash (and then force push) after my coworker and I think we won’t need more changes and want to consider the final breakdown of commits.

                            2. 2

                              I largely feel like all of these git stash && git checkout idioms could be swapped out with a git worktree and it would prevent dealing with the current state?

                              I also suspect git rebase can drop you into a detached state and do the commit inline, with rebase.autoStash = true it would probably land you closer to what jujutsu is doing?

                              1. 4

                                Making a worktree is a lot heavier than dealing with current state. A running checkout of this app uses about 2GB in full, including node_modules, vendored bits and pieces, etc., .env-type things and database config which are per-env and not committed; those need to be manually copied over for a worktree. It definitely feels like a workaround.

                                And yes, it could; that’s the edit solution, except authoring there instead of stashing the change authored against develop. (But I really do want to write it against develop specifically first.)

                                1. 2

                                  Not to mention a work tree may not cover any project specific IDE config etc.

                              2. 2

                                My problem with switching away from git is I need to know git well in order to fix others’ git mess ups. It’s part of the job to use this particular tool. Otherwise, I’d be using Pijul.

                                1. 5

                                  The most wonderful thing about jj is that you don’t have to switch away from git - it can be used either as a wrapper around git, or colocated with git (docs). So I wouldn’t think of it as “switching” from Git - it can be used alongside git to improve your own workflow.

                                  1. 5

                                    I started using jj two weeks ago, it’s really cheap to try. You can continue using all the normal git commands and slowly try the jj bits if you find them helpful. And jj’s undo feature is worth it even if you decide to otherwise stick to the git model.

                                2. 1

                                  I don’t know enough about jujutsu to have an opinion on it, but this use case seems contrived.

                                  After realizing there needs to be a new test midway through a change, I don’t see the problem with just writing the test right then and there, committing it, and continuing on with the change. Why re-order the commit history? Does it really break the “progression of commits”? Realizing there needed to be a test is part of the progression.

                                  1. 12

                                    Incidentally, “this use case seems contrived” reads dissonantly when the opening paragraph describes me realising that this use case happened, and so deciding to write it up. It actually happened the way I described in the jj section, and had I been using git, I would’ve approached as outlined in the git section. Someone doing things differently to you (or valuing different things) doesn’t make it contrived.

                                    1. 7

                                      That’s fair - I apologize for saying it was contrived.

                                      I disagree with the need to rewrite git history, but if somebody chooses to do so, I can see how jujutsu would be helpful, and it would be easier than rebasing.

                                      1. 18

                                        I’ve been using jj for over half a year now, and what I’ve experienced is this: when things like history rewriting become both safer and easier, one starts to think of them as an everyday tool, instead of kept in a glass box that says “Break in case of emergency”.

                                        To go with the blog post’s example, inserting a test commit before all the feature’s WIP commits has the advantage that you have an isolated, pre-WIP commit where the tests should definitely run and fail. There’s no chance the test commit will be subtly dependent on the WIP changes, and if there’s ever any doubt the test fails correctly, you can go back to that commit to check.

                                        You might think this is only a minor advantage, but here’s the thing: jj makes doing this so trivial, there’s no reason you wouldn’t do it for only a slight improvement.

                                        1. 3

                                          Thank you!

                                          And yes, it definitely assists workflows that not everyone would love to begin with, but for those who do, it’s such a lovely way to work with git repositories!

                                      2. 5

                                        Why you can’t write the tests on the tip of the feature branch (adapted from my comment on HN):

                                        The article describes a scenario where you “feel reasonably assured that green CI will mean you’re on the right track, and you’ve been removing parts of the old parser as you introduce the new”. But this one parser feature you discovered was not under test. “The original parser is half-taken to bits,” so perhaps you already deleted the implementation of that feature, not knowing it was depended on. If the implementation is missing on the current branch, of course you can’t test it.

                                        Okay, let’s say you didn’t delete the implementation yet – but the implementation calls some of the methods you already modified. If you write a test on your current branch, it would only show that your already-modified parser passes the test. You couldn’t be confident that the expectations in the test you wrote actually match the behavior of the old parser. If the old parser acted differently from your tests (even if it was due to a bug), you need to know about that so you can update callers of the old parser to not rely on that behavior any more.

                                        To be confident that your new test accurately describes behavior you need to support, you need to run the new test against the “develop” branch.

                                        1. 1

                                          I thought of that, but you can always checkout “develop” (or any other commit, for that matter), cherry pick the commit with the new tests, and then run them.

                                          But there’s never a real state where the test exists but the in progress change doesn’t, so why bother creating it? All that really matters is that the test passes in the final state. If somebody wants to verify against another version of the code, let them do the work.

                                          1. 2

                                            I can see there can would be situations where might feel that way about it, but in this case, the codebase is huge, and old — a Rails app which is older than my child currently in secondary school. None of the original team is still present, and you can imagine the layers of change as successive teams have done things differently. Everyone who works on it today is, to some extent, always performing code archaeology.

                                            There’s no such state in the codebase where that particular test exists, sure, but I’m not as interested in the test as I am the functionality. There isn’t currently such a test, but the intended function is clear to me right now, so I’ll write one.

                                            I’ll write it against the trunk because, while I can see what I think it ought to do, legacy codebases have a way of delighting you with surprise, and there’s always the possibility I’m wrong. Or, there’s the possibility there is in fact a bug, which my test then fails on.

                                            This is a really good find! This is exactly how you extend your knowledge of an existing running system. I deeply care to know whether that test would pass, whether or not this entire branch ends up landing, because it suggests there’s surprising things in the codebase that are currently known to zero people. Since it went green, I didn’t need to expedite landing the test in trunk, but if it went red, then that’d be its own task to address, maybe to be resolved with the landing of the change, or expedited if it turns out to be severe (e.g. quietly corrupting data).

                                            In short, I do care, because I want to understand the system as much as possible, and ensuring I have the relevant parts of the status quo understood right is helpful when making changes to it! Hence approaching in this order.

                                        2. 5

                                          What @roryokane said, but in short, (a) the branch wasn’t in a state where such a test would pass, and (b) even if it was, it wouldn’t necessarily tell us what we need, viz. “does the behaviour still work as well as it did before this branch?”. It’s a good thing to nimbly move around your source history!

                                          Reordering the commit history is nice because then I can run that test on the commits that I expect may break that test, and fix those commits if need be (rather than just tacking another commit onto the end). My commits are meant to be taken as whole review units, and ideally are amenable to tools like git bisect; the branch is a progression of stacking changes, but that doesn’t mean it has to be authored linearly in time.