Say you're working on a local branch, adding some new feature:
echo "Hello!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"
echo "Goodbye world!" >farewell.txt
git add farewell.txt
git commit -m"Add farewell.txt"
Then you realize greeting.txt
is missing "world":
echo "Hello world!" >greeting.txt
git commit -a -m"fixup greeting.txt"
Interactive rebase with fixup
The files now look correct, but if you haven't pushed yet you can still clean up
this commit history. For this, we need to introduce a new tool: the interactive
rebase. We're going to edit the last three commits this way, so we'll run
git rebase -i HEAD~3
(-i
for interactive). This'll open your text editor
with something like this:
pick 8d3fc77 Add greeting.txt
pick 2a73a77 Add farewell.txt
pick 0b9d0bb fixup greeting.txt
# Rebase f5f19fb..0b9d0bb onto f5f19fb (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# f, fixup <commit> = like "squash", but discard this commit's log message
This is the rebase plan, and by editing this file you can instruct git on how to edit history. When we save and close our editor, git is going to remove all of these commits from its history, then execute each line one at a time. By default, it's going to pick each commit, summoning it from the heap and adding it to the branch. If we don't edit this file at all, we'll end up right back where we started, picking every commit as-is. We're going to use a lovely feature: fixup. Edit the third line to change the operation from "pick" to "fixup" and move it to immediately after the commit we want to "fix up":
pick 8d3fc77 Add greeting.txt
fixup 0b9d0bb fixup greeting.txt
pick 2a73a77 Add farewell.txt
When you save and quit these two commits will now be squashed into one. You can
abbreviate with f
to speed things up next time:
$ git log -2 --oneline
fcff6ae (HEAD -> master) Add farewell.txt
a479e94 Add greeting.txt
keep base
The --keep-base
option is really helpful if the master
branch (or whatever
your feature was based on) has moved on since you started your feature and you
simply want to squash your commits based on the original master commit you
started with rather than the lastest master commit. You can then afterwards
rebase onto the latest commit of master seperately if you'd like:
git rebase --keep-base -i master
rebase against very first commit
I used git init to create a fresh repo, then made three commits. Now I want to
rebase to go back and amend my first commit, but if I do git rebase -i HEAD~3
git complains!
git rebase -i --root
git rebase --autosquash
The steps described above can also be performed in a more automated manner by
taking advantage of the --autosquash
option of git rebase
in combination
with the --fixup
option of git commit
:
git commit -a --fixup HEAD^
git rebase -i --autosquash HEAD~3
This will prepare the rebase plan with commits reordered and actions set up:
pick 8d3fc77 Add greeting.txt
fixup 0b9d0bb fixup! Add greeting.txt
pick 2a73a77 Add farewell.txt
# Rebase f5f19fb..0b9d0bb onto f5f19fb (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# f, fixup <commit> = like "squash", but discard this commit's log message
Squashing several commits into one
As you work, you may find it useful to write lots of commits as you reach small milestones or fix bugs in previous commits. However, it may be useful to "squash" these commits together, to make a cleaner history before merging your work into master. For this, we'll use the "squash" operation. Let's start by writing a bunch of commits:
git checkout -b squash
for c in H e l l o , ' ' w o r l d; do
echo "$c" >>squash.txt
git add squash.txt
git commit -m"Add '$c' to squash.txt"
done
That's a lot of commits to make a file that says "Hello, world"! Let's start
another interactive rebase to squash them together. Note that we checked out a
branch to try this on, first. Because of that, we can quickly rebase all of the
commits since we branched by using git rebase -i master
. The result:
pick 1e85199 Add 'H' to squash.txt
pick fff6631 Add 'e' to squash.txt
pick b354c74 Add 'l' to squash.txt
pick 04aaf74 Add 'l' to squash.txt
pick 9b0f720 Add 'o' to squash.txt
pick 66b114d Add ',' to squash.txt
pick dc158cd Add ' ' to squash.txt
pick dfcf9d6 Add 'w' to squash.txt
pick 7a85f34 Add 'o' to squash.txt
pick c275c27 Add 'r' to squash.txt
pick a513fd1 Add 'l' to squash.txt
pick 6b608ae Add 'd' to squash.txt
# Rebase 1af1b46..6b608ae onto 1af1b46 (12 commands)
#
# Commands:
# p, pick <commit> = use commit
# s, squash <commit> = use commit, but meld into previous commit
We're going to squash all of these changes into the first commit. To do this, change every "pick" operation to "squash", except for the first line, like so:
pick 1e85199 Add 'H' to squash.txt
squash fff6631 Add 'e' to squash.txt
squash b354c74 Add 'l' to squash.txt
squash 04aaf74 Add 'l' to squash.txt
squash 9b0f720 Add 'o' to squash.txt
squash 66b114d Add ',' to squash.txt
squash dc158cd Add ' ' to squash.txt
squash dfcf9d6 Add 'w' to squash.txt
squash 7a85f34 Add 'o' to squash.txt
squash c275c27 Add 'r' to squash.txt
squash a513fd1 Add 'l' to squash.txt
squash 6b608ae Add 'd' to squash.txt
When you save and close your editor, git will think about this for a moment, then open your editor again to revise the final commit message. You'll see something like this:
# This is a combination of 12 commits.
# This is the 1st commit message:
Add 'H' to squash.txt
# This is the commit message #2:
Add 'e' to squash.txt
# This is the commit message #3:
Add 'l' to squash.txt
You can reference all your old commit messages while you write a single new
message for this new squashed commit. Once you save we can pull our changes into
the master branch and get rid of this scratch branch. We can use git rebase
like
we use git merge
, but it avoids making a merge commit:
git checkout master
git rebase squash
git branch -D squash
We generally prefer to avoid using git merge
unless we're actually merging
unrelated histories. If you have two divergent branches, a git merge
is useful
to have a record of when they were... merged. In the course of your normal work,
rebase is often more appropriate.
Splitting one commit into several
Sometimes the opposite problem happens. One commit is just too big and you need to split it up. Let's start with a simple c program:
int main(int argc, char *argv[]) {
return 0;
}
We'll commit this skeleton to get started:
git add main.c
git commit -m"Add C program skeleton"
Next, let's extend the program a bit:
#include <stdio.h>
const char *get_name() {
static char buf[128];
scanf("%s", buf);
return buf;
}
int main(int argc, char *argv[]) {
printf("What's your name? ");
const char *name = get_name();
printf("Hello, %s!\n", name);
return 0;
}
For demonstration purposes we'll make the mistake of committing this big hunk of
work. In the future you can avoid this in the first place with git add -p
and git stash to test the index state:
git commit -a -m"Flesh out C program"
The first step is to start an interactive rebase. Let's rebase both commits with
git rebase -i HEAD~2
, giving us this rebase plan:
pick 237b246 Add C program skeleton
pick b3f188b Flesh out C program
# Rebase c785f47..b3f188b onto c785f47 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# e, edit <commit> = use commit, but stop for amending
Change the second commit's command from "pick" to "edit", then save and close your editor. Git will think about this for a second, then present you with this:
Stopped at b3f188b... Flesh out C program
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
We could follow these instructions to add new changes to the commit, but instead
let's do a "soft reset" git reset HEAD^
. This will "un-commit" this latest
commit, but place the changes in our working tree so we can open the file, make
our changes and then use git add -p
to add them seperately.
Reordering commits
This one is pretty easy. Let's start by setting up our sandbox:
$ echo "Goodbye now!" >farewell.txt
$ git add farewell.txt
$ git commit -m"Add farewell.txt"
$ echo "Hello there!" >greeting.txt
$ git add greeting.txt
$ git commit -m"Add greeting.txt"
$ echo "How're you doing?" >inquiry.txt
$ git add inquiry.txt
$ git commit -m"Add inquiry.txt"
$ git log
f03baa5 (HEAD -> master) Add inquiry.txt
a4cebf7 Add greeting.txt
90bb015 Add farewell.txt
Clearly, this is all out of order. Let's do an interactive rebase of the past 3
commits to resolve this. Run git rebase -i HEAD~3
and this rebase plan will
appear:
pick 90bb015 Add farewell.txt
pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt
# Rebase fe19cc3..f03baa5 onto fe19cc3 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
#
# These lines can be re-ordered; they are executed from top to bottom.
Simply re-arrange the lines and save:
pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt
pick 90bb015 Add farewell.txt
git pull --rebase
If you've been writing some commits on a branch <branch>
which has been updated
upstream, say on remote origin, normally git pull will create a merge commit. In
this respect, git pull's behavior by default is equivalent to:
git fetch origin <branch>
git merge origin/<branch>
There's another option, which is often more useful and leads to a much cleaner
history: git pull --rebase
. Unlike the merge approach, this is mostly
equivalent to the following:
git fetch origin
git rebase origin/<branch>
The merge approach is simpler and easier to understand, but the rebase approach
is almost always what you want to do if you understand how to use git rebase. If
you like, you can set it as the default behavior like so:
git config --global pull.rebase true
Using git rebase to... rebase
Ironically, the feature of git rebase that I use the least is the one it's named for: rebasing branches. Say you have the following branches:
A--B--C--D--> master
\--E--F--> feature-1
\--G--> feature-2
It turns out feature-2
doesn't depend on any of the changes in feature-1
,
that is, on commit E
, so you can just base it off of master. The fix is thus:
git rebase --onto master feature-1 feature-2
The non-interactive rebase does the default operation for all implicated commits ("pick"), which simply replays the commits in feature-2 that are not in feature-1 on top of master. Your history now looks like this:
A--B--C--D--> master
| \--G--> feature-2
\--E--F--> feature-1