Rewrite History in Git

A while ago I had to do my first interesting history rewrite with git, and I couldn't find a step-by-step source with the things I wanted to do.

To remedy that, here is a little sample exercise. Kudos to foo at StackOverflow who got me started in the right direction!

Baseline repo

Let's start off with creating a little repo with some content. This will help set the stage for the work we're going to do.

$> # setup a new repo
$> mkdir -p ~/scratch/git-practice
$> cd ~/scratch/git-practice
$> git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
Initialized empty Git repository in /home/mlrdev/scratch/git-practice/.git/
$> git config user.email "someone@somewhere.com"
$> git config user.name "John McSomeone"

$> # make an initial commit
$> echo howdy > howdy.txt
$> git add howdy.txt
$> git commit -m "first commit"
[master (root-commit) 4ec5531] first commit
 1 file changed, 1 insertion(+)
 create mode 100644 howdy.txt

$> # pretend we're on a work branch
$> git checkout -b work
Switched to a new branch 'work'

$> # add a 'hi mom' message
$> echo hi mom > text.txt
$> git add text.txt
$> git commit -m "hi mom commit"
[work fedd043] hi mom commit
 1 file changed, 1 insertion(+)
 create mode 100644 text.txt

$> # add a 'hi there' message
$> echo hi there > other.txt
$> git add other.txt
$> git commit -m "hi there commit"
[work 01b4bf5] hi there commit
 1 file changed, 1 insertion(+)
 create mode 100644 other.txt

$> # update 'hi mom' to 'hi friend'
$> sed 's/mom/friend/' text.txt > new-text.txt
$> rm text.txt
$> mv new-text.txt text.txt
$> git add text.txt
$> git commit -m "mom to friend"
[work 13ae5d5] mom to friend
 1 file changed, 1 insertion(+), 1 deletion(-)

OK, so this is the history that we end up with.

$> git log
commit 13ae5d5e55ca716440b771dff4898d714fd641c9 (HEAD -> work)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:38:38 2024 -0700

    mom to friend

commit 01b4bf54d82188ca4fb2dcb203e8d5a669e19297
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:38:11 2024 -0700

    hi there commit

commit fedd0433ae0fbc1185101a87060b5ab49be052fe
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:37:52 2024 -0700

    hi mom commit

commit 4ec55319a7820863d2c78a5d0788a4048dc886bb (master)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:36:48 2024 -0700

    first commit

Fixing history

OK, my exercise is to make it so it looks like there is a 'hi friend' commit at the beginning, and then the second commit says 'hi other'.

We can use the git rebase -i command to rebase the history interactively. This gives us the opportunity to replay changes and change how they're applied or interact with them to change the outcomes or messages.

When we specify the branch in the git rebase -i BRANCH command, the BRANCH will not be affected. Instead, it simply acts as the baseline upon we will rewrite history.

Let's see how that goes.

$> git rebase -i master

At this point, I'm greeted in vim (I had git config core.editor vim at some point) with the following contents.

pick fedd043 hi mom commit
pick 01b4bf5 hi there commit
pick 13ae5d5 mom to friend

# Rebase 4ec5531..13ae5d5 onto 4ec5531 (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
#
# 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.
#

That's a pretty nifty menu of options right there!

One strategy is to edit the first one and discard the last one and simply replay things. Another strategy is to squash everything and then splice.

Fix by editing

We're going to edit things interactively with this approach, doing our small update directly in the first commit. Let's update the top lines to be as follows, changing the first line to edit and deleting the last line.

edit 6ba5996 hi mom commit
pick 77b3be7 hi there commit

Save and exit, and we'll see the following.

Stopped at fedd043...  hi mom commit
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue

We are now left partway through our history, and we can see this if we check our status.

$> git status
interactive rebase in progress; onto 4ec5531
Last command done (1 command done):
   edit fedd043 hi mom commit
Next command to do (1 remaining command):
   pick 01b4bf5 hi there commit
  (use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch 'work' on '4ec5531'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

nothing to commit, working tree clean

Let's replay our update now and finish the process.

$> sed 's/mom/friend/' text.txt > new-text.txt
$> rm text.txt
$> mv new-text.txt text.txt
$> git add text.txt
$> git commit --amend -m "hi friend commit"
[detached HEAD 1579a69] hi friend commit
 Date: Fri Jun 7 18:37:52 2024 -0700
 1 file changed, 1 insertion(+)
 create mode 100644 text.txt
$> git rebase --continue
Successfully rebased and updated refs/heads/work.

Let's check our history now.

commit 641f3e8de22b0ae172e69b3a2013ed9f80080617 (HEAD -> work)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:54:36 2024 -0700

    hi there commit

commit 1579a69bd507712f5f35adbe88f86c663e3e5530
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:54:23 2024 -0700

    hi friend commit

commit 4ec55319a7820863d2c78a5d0788a4048dc886bb (master)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:36:48 2024 -0700

    first commit

Fix by squashing and splitting

This time, we'll first squash all the changes together, and then split things into smaller chunks to get what we want.

Let's start a new work branch and do the same changes, but this time we'll try a different strategy.

$> git checkout master
Switched to branch 'master'
$> git checkout -b more-work
Switched to a new branch 'more-work'
$> # ... go and apply all the changes from the original setup ...

Once again, we'll run git rebase -i master, but this time we're going to set our rebase strategy differently.

pick 1538362 hi mom commit
squash 28a3bc0 hi there commit
squash 213b1c9 mom to friend

As soon as we exit the editor, we get to edit the message for the squashed commit, which starts off a simple concatenation.

# This is a combination of 3 commits.
# This is the 1st commit message:

hi mom commit

# This is the commit message #2:

hi there commit

# This is the commit message #3:

mom to friend

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Fri Jun 7 18:59:45 2024 -0700
#
# interactive rebase in progress; onto 4ec5531
# Last commands done (3 commands done):
#    squash 28a3bc0 hi there commit
#    squash 213b1c9 mom to friend
# No commands remaining.
# You are currently rebasing branch 'more-work' on '4ec5531'.
#
# Changes to be committed:
#       new file:   other.txt
#       new file:   text.txt
#

I'm just going to rename the message to 'my squashed commit', save and exit.

[detached HEAD 6381781] my squashed commit
 Date: Fri Jun 7 18:59:45 2024 -0700
 2 files changed, 2 insertions(+)
 create mode 100644 other.txt
 create mode 100644 text.txt
Successfully rebased and updated refs/heads/more-work.
$> # back to he command line!

Let's check our log, and we'll see that indeed, history is just the original master branch following by a single change.

$> git log
commit 63817818f063972bb83c426f2e6810dbd1c816d8 (HEAD -> more-work)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:59:45 2024 -0700

    my squashed commit

commit 4ec55319a7820863d2c78a5d0788a4048dc886bb (master)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:36:48 2024 -0700

    first commit

We can now pop off this last change and turn it into the two changes we wanted to end up with. Let's put that change into our work tree and check our status.

$> git reset HEAD~
$> git log
commit 4ec55319a7820863d2c78a5d0788a4048dc886bb (HEAD -> more-work, master)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:36:48 2024 -0700

    first commit

$> git commit
On branch more-work
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        other.txt
        text.txt

nothing added to commit but untracked files present (use "git add" to track)

Now we simply commit the changes we have in our work tree with whatever changes we want and in whatever order, and we'll have rewritten our branch history.

$> git add other.txt
$> git commit -m "starting with other"
[more-work f6bbc96] starting with other
 1 file changed, 1 insertion(+)
 create mode 100644 other.txt

$> git add text.txt
$> git commit -m "hi friend"
[more-work 86246c3] hi friend
 1 file changed, 1 insertion(+)
 create mode 100644 text.txt

And again, we land with the history we wanted.

$> git log
commit 86246c39b790ed18cebd36b408c49a470ca9af81 (HEAD -> more-work)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 19:11:26 2024 -0700

    hi friend

commit f6bbc9617eb7f6e5040ccd1b3de2d2ebbbf53f58
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 19:11:01 2024 -0700

    starting with other

commit 4ec55319a7820863d2c78a5d0788a4048dc886bb (master)
Author: John McSomeone <someone@somewhere.com>
Date:   Fri Jun 7 18:36:48 2024 -0700

    first commit

Happy rewriting history!

Tags:  git

Home