Overview

  • Multiple commits makes reviewing easier
  • Fixups make this easier
  • Rebasing can be painful

Fixups

In a PR review, some feedback about Commit Two requires making some changes. After the changes have been made, we can fixup Commit Two to avoid extra commits. First we need to grab the reference.

git log
commit fa11fa11fa11fa11fa11fa11fa11fa11fa11fa11
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Mon Jan 29 15:09:29 2024 -0600

    Commit Three

# copy this commit has (or first 7)
commit facefacefacefacefacefacefacefacefaceface
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Mon Jan 29 09:44:37 2024 -0600

    Commit Two

commit cafecafecafecafecafecafecafecafecafecafe
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Fri Jan 26 15:09:01 2024 -0600

    Commit One

Now we can use the --fixup flag to update an existing commit

git commit --fixup a9d3dea #updating Commit Two

Just to confirm here is the new log

git log
# the commit message is prefixed with fixup! which is how git figures
# out where to merge this in the next step
commit beeffeedbeeffeedbeeffeedbeeffeedbeeffeed
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Mon Jan 29 15:09:29 2024 -0600

    fixup! Commit Two

commit fa11fa11fa11fa11fa11fa11fa11fa11fa11fa11
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Mon Jan 29 15:09:29 2024 -0600

    Commit Three

commit facefacefacefacefacefacefacefacefaceface
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Mon Jan 29 09:44:37 2024 -0600

    Commit Two

commit cafecafecafecafecafecafecafecafecafecafe
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Fri Jan 26 15:09:01 2024 -0600

    Commit One

Now, we can perform a rebase to combine our fixup commit with Commit Two

# we can use ~1 to mean the commit before this commit
git rebase -i --autosquash a9d3dea~1

This will open the text editor you have configured with the key core.editor

pick cafecafec Commit One
pick facefacef Commit Two
fixup beeffeedb fixup! Commit Two
pick facefacef Commit Three

# Rebase baseba119..beeffeed6 onto baseba119 (2 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
#
# 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

Now, we have rewritten our git history as if our fixup changes were part of the original set of change.

git log
commit fa11fa11fa11fa11fa11fa11fa11fa11fa11fa11
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Mon Jan 29 15:09:29 2024 -0600

    Commit Three

# note: this commit hash has changed
commit ba11ba11ba11ba11ba11ba11ba11ba11ba11ba11
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Mon Jan 29 09:44:37 2024 -0600

    Commit Two

commit cafecafecafecafecafecafecafecafecafecafe
Author: Robert Masen <r.f.masen@gmail.com>
Date:   Fri Jan 26 15:09:01 2024 -0600

    Commit One

I personally have this script in my path named git-fixup

# ! /bin/bash
# ~/.local/bin/git-fixup
# this can be used via `git fixup <commit-hash>`
git commit --fixup $1 && git rebase -i --autosquash $1~1

Manual Rebase

Sometimes when attempting to rebase a complicated set of changes, or maybe when switching base branches it can become very difficult to pull apart the changes. It may be tempting to perform git merge <target-base> to pull in all of the commits but this very difficult to unwind after the fact. One way to deal with this is to perform a "manual rebase".

To start we need to get the list of commits we are looking to rebase, this will assume our target branch is named develop

git log --pretty=oneline develop..HEAD > cherry-picks

This will add a line to the file ./cherry-picks for each commit that appears in develop but not HEAD. Note: you may need to remove entries from this list especially if you are changing the base branch.

# ./cherry-picks
ca5eca5eca5eca5eca5eca5eca5eca5eca5eca5e (HEAD -> work-branch) Commit Three
7ac07ac07ac07ac07ac07ac07ac07ac07ac07ac0 Commit Two
babebabebabebabebabebabebabebabebabebabe Commit One

Now we can create a temporary branch to keep these commits around and then reset our branch

git checkout -b tmp/work-branch
git checkout -
git reset --hard origin/develop

This will remove all of your changes on your work branch and reset the state to match exactly our target branch. Now we can cherry-pick our commits on top of the base.

git cherry-pick babebabeb
git cherry-pick 7ac07ac07
git cherry-pick ca5eca5ec

In the event that any of these commits have conflicts, you'll need to deal with the merge conflict and use git cherry-pick --continue before moving on to the next commit.

This has essentially performed the work that rebase is supposed to do but manually.