The Git history command deserves more attention

turbocon6 pts0 comments

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&rsquo;m very sold on the problems jj is<br>trying to solve, the way it solves them hasn&rsquo;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&rsquo;s where git history comes in. It&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;t do this yet but the docs leave the<br>door open:<br>&ldquo;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.&rdquo;

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&rsquo;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&rsquo;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&rsquo;t have checked out without<br>disturbing whatever you&rsquo;re in the middle of.<br>Before:

After git history reword B:

Only B&rsquo;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&rsquo;s the equivalent of git add -p, but<br>without needing gymnastics with git rebase. I&rsquo;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&rsquo;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&rsquo;s still some key<br>mental shift which I&rsquo;m not yet making. And to be clear, git history doesn&rsquo;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...

rsquo commit history fixup reword split

Related Articles