Git interactive rebase, squash, amend and other ways of rewriting history

Tute Costa
Edited by Stefanni Brasil

“Please rebase on top of main 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.

Change the last commit message with git amend

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

To modify the commit message you run the amend command:

git commit --amend

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

Modify the last commit files changes with git amend

Git amend can also be useful to include files you forgot to track, or modifications to the files you have 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 the git add and git rm commands.

You can also edit the author. For example:

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

Modify the most recent commit files without editing the commit message

To add or remove files/changes from the latest commit without editing its message, add the --no-edit flag:

git commit -m "Update README with latest deploy changes"
git add README.md config/routes.rb
git rm notes.txt
git commit --amend --no-edit

The new commit will have the files changed and keep the previous message.

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.

Modify 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.

Modify the latest commit messages

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 main

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

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

The library maintainer asks as to “rebase on top of main”, 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/main

We want to reapply our commits, one by one, in order, onto upstream’s main. 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 main
git rebase upstream/main

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

GitHub has a Sync Fork button in the web UI to keep the local repository up to sync with an upstream repository. It’s a handy feature for open source contributors.

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 git 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. It’s important to keep them updated of the changes.

One gentler alternative to force push is to git push force with lease when working with others in a branch. It allows one to force push without the risk of unintentionally overwriting their work.

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 are recommended to rebase their local repository frequently. When those commits land into main, we never rewrite them again.

So rewrite git history, and communicate with your teammates how your team wants to keep the git history clean and reversible.

Achievement Unlocked! You now know how to rewrite git history while being a good citizen.