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