Interactive rebasing and squashing

Sometimes we want to squash multiple commits into one.

Maybe we did a lot of small commits with poor messages while working on a feature. Maybe we want to push a single commit containing all the changes for the feature—making it easy to revert later on, if needed.

In practice this is done with interactive rebasing.












Essentially squashing takes this.


Squashing before












And turns it into this.

Squashing after












Interactive rebasing allows squashing commits but also allows us to edit commit messages, reorder commits, and even remove commits.












Exercise: Squashing commits - set the stage

We need to set the stage first by creating some commits.

  • Navigate to the remote-git repository.
  • Checkout the feature branch.
  • Append 3 changes to the README.md file and commit each change with a unique message.











Solution: Squashing commits - set the stage

Navigate to the remote-git repository.

git checkout feature
echo "Change 1" >> README.md
git add README.md
git commit -m "Change 1"
echo "Change 2" >> README.md
git add README.md
git commit -m "Change 2"
echo "Change 3" >> README.md
git add README.md
git commit -m "Change 3"











Interactive rebase steps

To begin, we need to provide a point in time to rebase with. The simplest way to do this is typically with HEAD~n, where n is the number of commits back we want to go. This format is also called a commit-ish.

So, HEAD~1 means one commit back from HEAD.



The command for interactive rebase is:

git rebase -i <commit-ish>











Exercise: Squashing commits - interactive rebase

Now we will squash the commits.

We want all the 3 latest changes to be a single commit and update the commit message.

  • Start an interactive rebase with the last 3 commits.











Solution: Squashing commits - interactive rebase

git rebase -i HEAD~3

Output:

pick a4ebcc2 Change 1
pick 04b43c8 Change 2
pick 8406816 Change 3

# Rebase 7142285..8406816 onto 7142285 (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.

To squash the commits, we need to change the pick command to squash or s for the commits we want to squash.


pick a4ebcc2 Change 1
s 04b43c8 Change 2
s 8406816 Change 3

And we save with :wq and Enter.



This brings up the editor to edit the commit message.

Output:

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

Change 1

# This is the commit message #2:

Change 2

# This is the commit message #3:

Change 3

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Wed Sep 25 00:24:11 2024 +0200
#
# interactive rebase in progress; onto 7142285
# Last commands done (3 commands done):
#    squash 04b43c8 Change 2
#    squash 8406816 Change 3
# No commands remaining.
# You are currently rebasing branch 'main' on '7142285'.
#
# Changes to be committed:
#       modified:   README.md
#
~
~
~


We edit the commit message to something like Feature X.

Output:

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

Feature X

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Wed Sep 25 00:24:11 2024 +0200
#
# interactive rebase in progress; onto 7142285
# Last commands done (3 commands done):
#    squash 04b43c8 Change 2
#    squash 8406816 Change 3
# No commands remaining.
# You are currently rebasing branch 'main' on '7142285'.
#
# Changes to be committed:
#       modified:   README.md
#
~
~
~

And again we save with :wq and Enter.


Output:

[detached HEAD e3f045e] Feature X
 Date: Wed Sep 25 00:24:11 2024 +0200
 1 file changed, 3 insertions(+)
Successfully rebased and updated refs/heads/main.











Check the log to see the changes.

git log --oneline --graph

We will see that the 3 commits have been squashed into one with the message Feature X.

Output:

* e3f045e (HEAD -> main) Feature X











When we push to the remote respository, and in some cases you may need to do git push --force to overwrite the remote branch.

Know that it exists. Use --force with caution.