Need for Rebasing
We’ve all been there. We make a change to some source code, commit and
push to master
, only to get this horrid message in response:
Pushing to (some-remote-server) ! [rejected] master -> master (fetch first) error: failed to push some refs to '(some-remote-server)' hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing hint: to the same ref. You may want to first integrate the remote changes hint: (e.g., 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Without thinking clearly1, you may have typed: git pull
Now you are confronted with the Merge branch
message, leaving your
previously nice and linear Git history with a twig:
A twig is what I call an unintentional Git branch caused by nothing more than a straight-forward merge.
What to do?
Rebasing for the Win!
Since your change (and the icky merge) is local to your repository
(you haven’t issued a push
to the remote repository),
manipulate the Git history with the rebase
command. While the
word rebase sounds scary, it can be pretty simple.
Cleaning our history is a two-step process:
Look for the Parent
Issue a git log --pretty=oneline --graph
to get a short listing
of the commits.2
a357445 * Merge branch 'master' |\ 3a8dec0 | * Added a license to the end of the readme. b47700f | * Added a readme explaining the project 9aea7d5 * | Good quotes from Life of Brian |/ d956c7c * Found something from Flying Circus c8f5e02 * Found something from Holy Grail ...
You need the commit ID of the trunk before the twig happened. In
the example above, this would be d956c7c
. The characters |/
point to the commit you want. ;-)
Interactive Rebase!
Rebase with the --interactive
setting, as in:
git rebase --interactive d956c7c
Your editor will display your change at the top, and the commits
from the other shmucks on your team underneath.
Note: The commits are in historical order, with the oldest entry at the
top (which is yours). Don’t be surprised since the log
command
puts the oldest entry at the bottom.
pick 9aea7d5 Good quotes from Life of Brian pick b47700f Added a readme explaining the project pick 3a8dec0 Added a license to the end of the readme.
Simply move your commit to the bottom3, so it is now the newest entry:
pick b47700f Added a readme explaining the project pick 3a8dec0 Added a license to the end of the readme. pick 9aea7d5 Good quotes from Life of Brian
Save and exit out, and your are done!
Summary
That’s right, a simple two step process. Check the logs and you will see a nice clean history, with no annoying twigs. In my case, it looks like:
2cd1c60 * master Good quotes from Life of Brian 3a8dec0 * origin/master Added a license to the end of the readme. b47700f * Added a readme explaining the project d956c7c * Found something from Flying Circus
I suggest practicing this. Practice how? Download the following two scripts (not without looking at them first):
Running them creates a clone of a Git repository in some temporary
directory, e.g. /tmp/sandbox
:
$ ./gitrepo-history-conflict.sh /tmp/sandbox # ...lots of git messages... All done. Checkout the goodies: /tmp/sandbox/my-proj $ ./gitrepo-history-nice.sh /tmp/sandbox # ...blah, blah, blah... All done. Checkout the goodies: /tmp/sandbox/da-quotes
The first repository has the commits conflict with the unstaged changes, and the repository created by the second script doesn’t. Jump in there and have fun figuring out the correct way to keep your Git history tidy.4
Footnotes:
Save yourself a world of pain by issuing pull
commands with the --rebase
option:
git pull --rebase
Better yet, have this the default by setting this in your
$HOME/.gitconfig
file:
git config --global branch.autosetuprebase always
Of course, this only works for new clones and branches, so you may want to do:
git config --global branch.*branch-name*.rebase true
Note: If you are using Magit, then set this up as your default:
I often create the following alias in my .gitconfig
file:
[alias] lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
This allows me to see a nice, colored history of changes by typing:
git lg
.
Actually, who am I kidding. I’m always using Magit which does this by default. ;-)
If you use Emacs, simply type M-n
(Alt n) to move the entry
commit entry down.
I guess I never really explained why a clean Git history important. While clarity is important, rolling back changes in a linear history is easier. The review system, Gerrit, really depends on a linear history, and this is my primary motivation for such effort.