Reordering git commits (not patches) with interactive rebase
This is the third article in a series.
([1]
[2]) You may want to reread the
earlier ones, which were in 2015. I'll try to summarize.
The original issue considered the implementation of some program
feature X. In commit A, the feature had not yet been implemented.
In the next commit C it had been implemented, and was enabled. Then
there was a third commit, B, that left feature X implemented but
disabled it:
no X X on X off
A ------ C ------ B
but what I wanted was to have the commits in this order:
no X X off X on
A ------ B ------ C
so that when X first appeared in the history, it was disabled, and
then a following commit enabled it.
The first article in the series began:
I know, you want to say “Why didn't you just use git-rebase ?”
Because git-rebase wouldn't work here, that's why.
Using interactive rebase here “to reorder B and C” will not work
because git-rebase reorders patches, not commits. It will attempt
to apply the B→C diff as a patch to A, and will fail, because
the patch is attempting to disable a feature that isn't implemented in
commit A.
My original articles described a way around this, using the plumbing
command git-commit-tree to construct the desired commits with the
desired parents. I also proposed that one could write a
git-reorder-commits command to automate the process, but my proposal
gave it a clumsy and bizarre argument convention.
Recently, Curtis Dunham wrote to me with a much better idea that uses the
interactive rebase UI to accomplish the same thing much more cleanly.
If we had B checked out and we tried git rebase -i A , we would get a
little menu like this:
pick ccccccc implement feature X
pick bbbbbbb disable feature X
As I said before, just switching the order of these two pick
commands doesn't work, because the bbbbbbb diff can't be applied on
the base commit A.
M. Dunham's suggestion is to use git-rebase -i as usual, but instead
of simply reversing the order of the two pick commands, which
doesn't work, also change them to exec git snap :
exec git snap bbbbbbb disable feature X
exec git snap ccccccc implement feature X
But what's git snap ? Whereas pick means
run git show to construct a patch from the next commit,
then apply that patch to the current tree
git snap means:
get the complete tree from the next commit,
and commit it unchanged
That is, “take a snapshot of that commit”.
It's simple to implement:
# read the tree from the some commit and store it in the index
git read-tree $SHA^{tree}
# then commit the index, re-using the old commit message
git commit -C $SHA
There needs to be a bit of cleanup to get the working tree back into
sync with the new index.
M. Dunham's actual implementation
does this with git-reset (which I'm not sure is quite sufficient),
and has some argument checking, but that's the main idea.
I hadn't know about the exec command in a git-rebase script, but
it seems like it could do all sorts of useful things. The
git-rebase man page suggests
inserting exec make at points in your script, to check that your
reordering hasn't broken the build along the way.
Thank you again, M. Dunham!
[Other articles in category /prog]
permanent link
|