Git Interactive Rebase, Squash, Amend and Other Ways of Rewriting History

This article is also available in: French

“Please rebase on top of master and we’ll merge your pull request”.

“Can you please squash your commits together so we get a clean, reversible git history?”.

“Can you rewrite your commit’s message to describe better the problem it solves, and how it solves it?”.

Questions like these are commonly asked in pull requests. Let’s see why they exist, how to perform them, and their possible problems.

Reword the last commit message

One of the simplest history rewrites we can do with git is changing the last commit message. Let’s say right after making a commit you find a typo in its description, or you find a better way of describing the changeset. To make the correction you run:

git commit --amend

It will open an editor with the last commit message, so you can modify it. After saving, a new commit will be created with the same changes and the new message, replacing the commit with the previous message.

This can be useful to include files you forgot to track, or include modifications to the files you just commited. To do so, you can add the changes and then perform the amend:

git add README.md config/routes.rb
git rm notes.txt
git commit --amend

Aside from editing the commit message, the new commit will contain the changes specified with git add and git rm. You can also edit the author. For example:

git commit --amend --author="Tute Costa and Dan Croak <tute+dan@thoughtbot.com>"

Achievement Unlocked! You can now change the last commit of your repository to include newer changes to the files, and/or to improve the commit message. But don’t start amending all-the-things before understanding the last section of this blog post titled “DANGER”.

Reword other commit messages

Would love to speak about this now, but we need to understand a more general tool before. Stay tuned! Everything else will be easier once we read about…

Interactive Rebase

git rebase re-applies commits, one by one, in order, from your current branch onto another. It accepts several options and parameters, so that’s a tip of the iceberg explanation, enough to bridge the gap in between StackOverflow or GitHub comments and the git man pages.

An interesting option it accepts is --interactive (-i for short), which will open an editor with a list of the commits which are about to be changed. This list accepts commands, allowing the user to edit the list before initiating the rebase action.

Let’s see an example.

Reword other commit messages, take 2

Let’s say I want to reword any of the last 4 commits of this blog. I then run git rebase -i HEAD~4, and here is what I see:

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

# Rebase 8db7e8b..fa20af3 onto 8db7e8b
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# 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.
#
# Note that empty commits are commented out

We see the four last commits, from older to newer. See the comment below the list of commits? Good job explaining, git! pick (p for short) is the default action. In this case it would reapply the commit as is, no changes in its contents or message. Saving (and executing) this file would make no changes to the repository.

If I say reword (r for short) in a commit I want to edit:

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
r 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

When I save and quit the editor, git will follow the described commands, landing myself into the editor again, as if I had amended commit 3e7ee36. I edit that commit message, save and quit the editor, and here is the output:

robots.thoughtbot.com tc-git-rebase % git rebase -i HEAD~4
[detached HEAD dd62a66] Stop all the highlighting
 Author: Caleb Hearth
 Date: Fri Oct 31 10:52:26 2014 -0500
 2 files changed, 39 insertions(+), 42 deletions(-)
Successfully rebased and updated refs/heads/tc-git-rebase.

Now Caleb says in his commit message “Stop all the highlighting”, whether you are a kid or not.

Achievement Unlocked! You can now change the message of any commit you want. You may do so, just make sure you understand the “DANGER” section.

Squash commits together

Two other commands rebase interactive offers us are:

  • squash (s for short), which melds the commit into the previous one (the one in the line before)
  • fixup (f for short), which acts like “squash”, but discards this commit’s message

We’ll continue to work on the rebase example we worked before. We had four commits, my own for this blog post, and three others from Caleb, which were related to his previous post on PGP:

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

Let’s say I want to meld Caleb’s commits together, because they belong to the same logical changeset, and so we can git revert it easily if we find we prefer not to have those changes in this repository. We’ll want to keep the first commit message, and squash the two subsequent commits into the previous one. I change pick to squash where appropriate:

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
s 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

Save, and I land into the editor to decide the commit message of the melded three commits (see how they are concatenated one after the other):

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

Besides demystifying a relatively complex tool, protocol, and etiquette,
this post is intended to help with problems such as the one outlined in
this tweet:

> Emailed sensitive info to someone with PGP. They replied, with my
> original email, all in clear text. They didn't realize it.

# This is the 2nd commit message:

Fix PostChecker::Post#urls

# This is the 3rd commit message:

Hey kids, stop all the highlighting

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author:    Caleb Hearth
# Date:      Tue Sep 2 09:39:07 2014 -0500
#
# rebase in progress; onto 71d4789
# You are currently editing a commit while rebasing branch 'tc-git-rebase' on '71d4789'.

I decide to remove the third commit message, and add a more relevant note to the second commit message. Save the editor, and the four commits were transformed into two: the one from Caleb, and mine after. Good!

We could have used the fixup command, if we had seen earlier that we want the changes, but not the commit message, of the third commit. In that case, the commands would have looked like:

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
f 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

When saved, the editor would have included the third commit message already commented out for us:

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

Besides demystifying a relatively complex tool, protocol, and etiquette,
this post is intended to help with problems such as the one outlined in
this tweet:

> Emailed sensitive info to someone with PGP. They replied, with my
> original email, all in clear text. They didn't realize it.

- https://twitter.com/csoghoian/status/505366816685060096

* Use examples that reasonably approximates `gpg2` output.
* Reject backslash from <abbr title="Uniform Resource Locator">URL</abbr>s
  before checking them

  Markdown allows backslashes in <abbr title="Uniform Resource
  Locator">URL</abbr>s to escape characters, which are passed directly to the
  <abbr title="Uniform Resource Locator">URL</abbr>, so `http://some\_url.com`
  becomes `http://some_url.com`.

  This mitigates issues with markdown syntax highlighting thinking that
  emphasized text has started, such as `_this text_`.

# This is the 2nd commit message:

Fix PostChecker::Post#urls

# The 3rd commit message will be skipped:

#     Hey kids, stop all the highlighting

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author:    Caleb Hearth
# Date:      Tue Sep 2 09:39:07 2014 -0500
#
# rebase in progress; onto 71d4789
# You are currently editing a commit while rebasing branch 'tc-git-rebase' on
'71d4789'.

Save, and outputs:

[detached HEAD 809241b] Introduce OpenPGP and teach basic usage
 Author: Caleb Hearth
 Date: Tue Sep 2 09:39:07 2014 -0500
 2 files changed, 1429 insertions(+), 1 deletion(-)
 create mode 100644 source/posts/2014/10-31-pgp-and-you.md
Successfully rebased and updated refs/heads/tc-git-rebase.

Result is the same: 2 commits instead of 4, each with a single, different blog post.

Achievement Unlocked! You can now merge commits together. As always, be mindful of the DANGER section.

Rebase on top of master

We fork an open source library, start working on a feature branch, and master in the upstream project moves ahead. Our history looks like:

       A---B---C feature
     /
D---E---F---G upstream/master

The library maintainer asks as to “rebase on top of master”, so we fix any merge conflicts that may arise between both branches, and keep our changeset together. The maintainer would like to see a history like:

               A'--B'--C' feature
             /
D---E---F---G upstream/master

We want to reapply our commits, one by one, in order, onto upstream’s master. Sounds like the description of the rebase command! Let’s see what commands would land us into the desired scenario:

# Point our `upstream` remote to the original fork
git remote add upstream https://github.com/thoughtbot/factory_bot.git

# Fetch latest commits from `upstream` (the original fork)
git fetch upstream

# Checkout our feature branch
git checkout feature

# Reapply it onto upstream's master
git rebase upstream/master

# Fix conflicts, then `git rebase --continue`, repeat until done
# Push to our fork
git push --force origin feature

Achievement Unlocked! Your feature branch will be applied on top of latest master of the original fork.

And so we get to…

DANGER: You are rewriting history

See the --force in the last git push command? That means we are overwriting repository’s history. This is always safe to do in commits we don’t share with other team members, or in branches that belong to us (see my initials in the example of this blog post).

But if you force push editions that were already shared with the team (commits that exist outside of my repository, like the changes I made to the PGP commits that have been already shared), then everyone’s branch gets out of sync.

Rewriting history means abandoning existing commits and creating new ones, that may be very similar but are different. If others base work on your previous commits, and then you rewrite and force-push your commits, your team members will have to re-merge their work (if they notice the potential loss).

At thoughtbot we prefix our branches with our initials, signaling that those commits may get rewritten and others shouldn’t add commits to the branch. When those commits land into master or a shared branch we never rewrite them again.

So rewrite git history, provided rewritten commits exist only in your repository, or you and your team know that no one else should base work off of them.

Achievement Unlocked! You now know how to rebase while being a good citizen.