The git history command deserves more attention - Lalit Maganti
Working with lots of changes in parallel on git can be painful. You end up<br>juggling branches and commits, and running scary rebase -i commands that can<br>leave your tree in a half-broken state if you so much as sneeze.<br>jj, an alternative to git, gets discussed a<br>lot these days (1,<br>2,<br>3,<br>4) and is<br>often pitched as a solution. While I’m very sold on the problems jj is<br>trying to solve, the way it solves them hasn’t quite hit home with me. Every 3<br>months, for the last 1.5 years, I try it out for a few days, really trying to<br>make it part of my workflow but eventually I give up and go back to git [1].<br>That’s where git history comes in. It’s an experimental command that<br>arrived across two releases, 2.54 (April, reword and split subcommands) and<br>2.55 (June, fixup subcommand). It got a flurry of attention on each release<br>day, and then, as far as I can tell, not much community discussion since. Which<br>is a shame, because IMO it already delivers several of the benefits people tout<br>for jj without needing to switch your whole workflow. And the cool part is<br>that it’s part of the core git distribution, so you can try it without<br>installing anything.<br>There are three subcommands: fixup, reword and split.<br>fixup<br>git history fixup fixes an old commit that has something wrong in it, then<br>autorebases all your branches to match.<br>You stage the fix as usual with git add, then run git history fixup<br>to fold those staged changes into the target commit. It’s like a<br>git commit --fixup plus an autosquash rebase but with the extra magic that it<br>also updates any other branch which contained that commit.<br>That last part goes further than git rebase --update-refs, which only moves<br>refs sitting inside the range you’re actively rebasing. git history instead<br>finds and rewrites every local branch descended from the commit (while also<br>having an option to limit it to only the current branch). On the other hand it<br>does not work in the presence of merge commits which, for some usages of git,<br>is going to be a dealbreaker.<br>Here’s how it works in practice:<br>Before, with a fix staged for B:
After git history fixup B:
B* is B with the fix folded in. Rewriting a commit gives it a new hash, so<br>C and D are automatically re-created on top as C* and D*, and the<br>feat-1 and feat-2 branch tips move with them.<br>The most important property, common to all three commands, is that it’s atomic:<br>it never leaves your tree in a half-broken state. It manages this by refusing<br>any operation that could produce a conflict.<br>To be clear, this is strictly less powerful than jj. jj treats conflicts<br>as first class so it can carry a conflicted state through a rebase and let you<br>sort it out later. git history doesn’t do this yet but the docs leave the<br>door open:<br>“This limitation is by design as history rewrites are not intended to be<br>stateful operations. The limitation can be lifted once (if) Git learns about<br>first-class conflicts.”
So basically, this limitation may change in the future; excited to see if it<br>does!<br>reword<br>git history reword updates the commit message on an old commit and<br>automatically rebases everything on top. This is very useful for going back and<br>fixing commit messages when the design shifts as you iterate.<br>git history reword opens your editor with that commit’s existing<br>message. You edit it, save, and the rest of the stack is rebuilt on top with the<br>branches following along. It’s exactly like fixup but for commit messages<br>instead of the tree contents.<br>Because it only changes a message, reword (like split later) never touches<br>your index or working tree at all; it works purely on the commit graph. So both<br>let you can rewrite a commit on a branch you don’t have checked out without<br>disturbing whatever you’re in the middle of.<br>Before:
After git history reword B:
Only B’s message changes, but that still gives it a new hash, so C is<br>rebuilt on top as C* and feat-1 follows along.<br>split<br>git history split takes one commit and splits it into two, interactively<br>picking what you care about from each. It’s the equivalent of git add -p, but<br>without needing gymnastics with git rebase. I’ve found this to be the most<br>specialized of the three, but invaluable when I need it.<br>Specifically, git history split drops you into a hunk-by-hunk prompt<br>over that commit’s diff. The hunks you keep make up the first commit, the rest<br>fall into the second.<br>Before, with B bundling two unrelated changes:
After git history split B:
B becomes B1 and B2, and C is rebuilt on top of the pair as C*.<br>Conclusion<br>Judging by how many people are using jj, I do think there’s still some key<br>mental shift which I’m not yet making. And to be clear, git history doesn’t<br>close the full gap: jj still gives you an<br>operation log with easy undo,<br>models your working copy as a commit,<br>and can<br>carry conflicts through a...