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