by Adam Brett

Stop using `git pull`: A better workflow

This article was published on Wednesday, December 10, 2014 which was more than 18 months ago , this means the content may be out of date or no longer relevant. You should verify that the technical information in this article is still current before relying upon it for your own purposes.

Just a short one today, but an important one that not a lot of people know about.

When you want to pull in changes from a remote branch in git, you usually do something like this:

cd ~/projects/project
git checkout some-branch
git pull origin some-branch

This works if you haven't made any changes since your last pull, but if you have, git will merge the pulled commits with your local changes. This has three major problems:

  1. It's un-predictable
  2. It creates a merge commit where there shouldn't be one
  3. It creates a nasty bump in your graph that shouldn't be there

It looks something like this:

$ git log --graph
*   commit 93a57e8abda664a3cd569f6beead407b071240b5
|\  Merge: 36ee576 e04fd5e
| | Author: Adam Brett <[email protected]>
| | Date:   Wed Dec 10 11:29:07 2014 +0000
| |
| |     Merge branch 'master' of /tmp/tmp.I8393vgR
| |
| |     * 'master' of /tmp/tmp.I8393vgR:
| |       Mod: add title to readme
| |
| * commit e04fd5e5bc743587330304a1e9ca902c3bb267c1
| | Author: Adam Brett <[email protected]>
| | Date:   Wed Dec 10 11:28:18 2014 +0000
| |
| |     Mod: add title to readme
| |
* | commit 36ee57669ad70f224a5db6adec99209090e42149
|/  Author: Adam Brett <[email protected]>
|   Date:   Wed Dec 10 11:27:51 2014 +0000
|
|       Add: License file
|
* commit 37fe2632121c64c4062804121d8ea786a9478f07
Author: Adam Brett <[email protected]>
Date:   Wed Dec 10 11:27:33 2014 +0000

Initial commit

Now this is a pretty simple repo, with just three commits, but using git pull has created a 4th, and a nasty bump in our graph. Now multiply this by every time you run git pull on your project, and all the developers on your team. Mess.

All of this can be avoided by removing git pull from your workflow entirely. Seriously. Stop using it. Right now.

The correct way to merge upstream changes is like this:

git fetch origin --prune

First, fetch the changes from the remote. This will update remote-tracking branches in the form origin/branch-name, rather than updating your local branch directly.

The --prune flag will do some basic housekeeping, and make sure that any branches that were deleted on the remote are also deleted locally (but won't delete any locally checked-out branches, those are different from remote-tracking branches).

At this point you can checkout the remote-tracking braches (git checkout origin/branch-name), and inspect the changes that you've just pulled down to make sure you actually want them.

Next switch to the branch you want to pull the changes on to and attempt to do a fast-forward merge. This will only work if you don't have any local changes:

git checkout branch-name
git merge --ff-only origin/branch-name

If this fails, you'll get a message something like this:

$ git merge --ff-only origin/master
fatal: Not possible to fast-forward, aborting.

If you see this message, you likely have local commits that differ from the upstream branch. That's ok, you just need to rebase your branch instead:

git rebase --preserve-merges origin/branch-name

By rebasing, your branch will be rolled back to the first common point with origin/branch-name. Your local branch will then be fast-forwarded to bring it in-line with origin/branch-name, and your commits will be re-applied one-by-one.

Any merge conflicts that come up will prompt you to resolve them before that commit is applied, which will make the changes part of that commit. Assuming all went well, it should look something like this:

$ git rebase origin/master
First, rewinding head to replay your work on top of it...
Applying: Add: License file

Once it's finished, your local branch will be up-to-date with the changes from the remote, and your commits will be the newest ones in the history, which leads to a much cleaner history, and no superfluous merge commits:

$ git log --graph
* commit 084ef64575688006929c260a4c6f78473915eedd
| Author: Adam Brett <[email protected]>
| Date:   Wed Dec 10 11:27:51 2014 +0000
|
|     Add: License file
|
* commit e04fd5e5bc743587330304a1e9ca902c3bb267c1
| Author: Adam Brett <[email protected]>
| Date:   Wed Dec 10 11:28:18 2014 +0000
|
|     Mod: add title to readme
|
* commit 37fe2632121c64c4062804121d8ea786a9478f07
Author: Adam Brett <[email protected]>
Date:   Wed Dec 10 11:27:33 2014 +0000

Initial commit

Much better. If you have any locally merged branches not on the remote branch (e.g. merging a feature branch in to master) where you want to keep the merge commit, make sure you include the --preserve-merges option, otherwise rebase will flatten those as well.

Git Alias

Incase typing 3 commands instead of 1 and having to make a decision half way through is too much for you, I've created a handy git alias you can use to do all of this in a single step:

update-from = "!f() { git fetch $1 --prune; git merge --ff-only $1/$2 || git rebase --preserve-merges $1/$2; }; f"

Add this to ~/.gitconfig, then you can run git update-from <remote> <branch-name> to do all of this in one step:

$ git update-from origin master
fatal: Not possible to fast-forward, aborting.
First, rewinding head to replay your work on top of it...
Applying: Add: License file

Now you don't have any excuses!

What about git pull --rebase?

This option does essentially the same thing as above, with the exception that it will flatten all merge commits, and sometimes that's not what you want to do (e.g. merging a feature branch in to master). Git v1.8.5 added the --rebase=preserve option to git pull --rebase to avoid this behaviour and will keep the merge commits which makes git pull (when used with --rebase=preserve) considerably less dangerous.

Even so, I still prefer to run the steps individually. It's best to checkout and inspect the remote-tracking branches pulled down by git fetch before you run the merge or rebase so you can attempt to predict the state that your local branch will be left in. This is good a practice to get into the habit of; someone may have added 20G of temporary files or logs, or done something equally silly since you last updated your branch.

My feeling is that being explicit in what your commands forces you to think about what you're doing and what effect you will have on the history and your code.

Also, git pull --rebase won't clean up your remote-tracking branches!

For exclusive content, including screen-casts, videos, and early beta access to my projects, subscribe to my email list below.


I love discussion, but not blog comments. If you want to comment on what's written above, head over to twitter.