Update logical commits with `git absorb`
I sometimes find myself in a particular situation with git. I have a small series of logical commits in a pull request. I need to make some edits.
I could always make the edits on top of the logical commits and push the new commits, but usually, I’d rather edit the logical commits.
My usual workflow for something like this used to be:
- Make some changes.
- If there’s only one commit, or my changes are all for the last commit, make the changes, and commit with
git commit --amend
. - Use
git add -p
and create a new commit only containing the edits for a single commit. Commit withgit commit --fixup fa1afe1
. You don't have to use the short SHA-1. There are lots of ways to name a commit, likeHEAD~3
or:/Add some bugs
.:/<regex>
returns the youngest matching commit. Some folks like it. With--fixup
, git sets the commit message for the new commit to something likefixup! fa1afe1: <original commit message>
. Git will use the commit message later to be helpful. If the original commit message needs to be changed, I'd probably commit with--squash
and add a reminder in the new commit's commit message. The rebase step will change this commit's action to "squash" instead of "fixup". In an interactive rebase, "squash" opens an editor with the original commit message on top and the commit message for the to-be-squashed commit on the bottom, and has you edit the message before continuing with the rebase. (I see there'sgit commit --fixup:amend
now, which looks to be another way to edit the commit message during the rebase.) - Keep running
git add -p
and committing until there are no more changes left. - Run
git rebase -i --autosquash main
. I don't really put--autosquash
in the command. I setrebase.autoSquash
in my git config. . It opens up the editor just likegit rebase -i
. However, because of autosquash, thosefixup! fa1afe1:
commits we just made have their action set to “fixup” and they’re reordered to be right after the commits they refer to. I take a quick look, save, and exit. Git does the rebase, and my local branch is updated. - Push these changes with
git push --force
, updating the remote branch.
§ Autosquash
Autosquash is pretty helpful. When you rebase, those fixup and squash commits will be reordered and have the appropriate action based on the commit message.
As helpful as autosquash is, it’s relatively transparent. If you made a regular commit, then did an interactive rebase, reordered that regular commit to be underneath the one you wanted to fixup, and changed the action from “pick” to “fixup”, you end up at the same place. These special commands get you some automatic human-readable information in the temporary commit, and the rebase part reads that commit message to do a bit of text editing for you.
I didn’t terribly mind the workflow (git add -p
, git commit --fixup
, git add -p
…, git rebase -i
).
I do find git add -p
over and over to be tedious and have long wished for a way to do everything git add -p
does, but with deleting/undoing changes and sorting things into multiple changesets in one go.
I stumbled upon git absorb
, though, and have found it even nicer.
§ git absorb
Enter git absorb, a snazzy git plugin. After you install it:
- Make some changes.
- Stage the changes with
git add -p
. - Run
git absorb
. It checks if the hunks you’ve staged match or are inside hunks you changed in previous commits, and puts those into fixup commits, leaving anything that remains in the staging area. - Run
git rebase -i --autosquash main
.
If you don’t want to review the changes during the interactive rebase, you can git absorb --and-rebase
.
Have a suggestion? An important correction? Let me know!
If this was helpful or enjoyable, please share it! To get new posts, subscribe to the newsletter or the RSS/Atom feed. If you have comments, questions, or feedback, please email me.