Combine multiple patches

Sometimes you decide that two separate patches should really be collapsed into one to make a complete logical change.

TL;DR

For those who just need a quick reference as a reminder you can do the following.

  • gps rebase - do an interactive rebase of the patch stack
  • mark the patch(es) you want to combine up into another patch with squash or fixup in the interactive rebase buffer
  • save the buffer and quit the editor

WalkThrough

The gps rebase command is a convenience function that really runs an interactive rebase of the stack on top of it's associated upstream, e.g. git rebase -i --onto origin/main origin/main main.

So to understanding how to combine patches with this command is really simply learning how to combine commits using git's interactive rebase.

Initial State

Let's start with the following patch stack (gps ls).

Initial patch stack

Mark Patches

Let's say that we need to combine the "Add function A" and "Add function B" patches together to make a logical patch.

To do this we start by running gps rebase to kick off the interactive rebase. It presents the following in our configured editor.

pick 340a171 Add function A
pick 00e9082 Add function B
pick c1b3634 Add function C

# Rebase 016b6ec..c1b3634 onto 016b6ec (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

To combine a patch with the patch above it, we simply mark it for squash or fixup instead of its default pick. If you forget the marking you can always reference the comment Git includes in the interactive rebase buffer. So in this case we mark the "Add function B" patch with squash as follows.

Note: The stack is inverted when presented in an interactive rebase. So the bottom most commit on the stack is actually the top most commit. This can be confusing until you get used to it.

pick 340a171 Add function A
squash 00e9082 Add function B
pick c1b3634 Add function C

# Rebase 016b6ec..c1b3634 onto 016b6ec (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

Squash vs. Fixup

Git's interactive rebase provides two mechanisms of combining commits, squash and fixup. Squash combines two commits and presents you with the combined commit messages in the rebase. Fixup on the other hand combines two commits while throwing away the commit message of the commit marked withe fixup.

Warning: You need to be careful when using fixup to make sure that you aren't accidentally throwing away a ps-id from the commit message that you want to keep around.

Choose a ps-id

When you are combining patches you want to be aware that depending on what state your patches are in, one, or the other, or both might have ps-id's in their commit message. The ps-id in the commit message is how Git Patch Stack uniquely identifies patches and tracks and associates state to them.

If you are in a situation where only one of the patches involved has a ps-id you will likely want to keep that ps-id in the resulting combined patch's commit message.

If you are in a situation where multiple patches involved have ps-id's you will have to select one ps-id to keep and use as the ps-id for the new combined patch.

Confirm Rebase

We then save and quit the editor, and it combines the patches marked with squash or fixup as part of the rebase. This leaves our patch stack as follows (gps ls).

Final patch stack

Exactly the state we wanted it to be in.