If you're just starting out with Git, you'll inevitably run into commits into your feature branches like the following:

Merge branch 'test' of git.lullabot.com:lbcom into test

What are these commits, and how did they get created? Usually, it's the result of adding a commit to your local copy of a branch, and then pulling upstream changes into that branch. Since your local commit isn't on the remote repository yet, when git pull runs git merge origin/[branch] [branch], it will automatically do a "recursive" merge and create a commit with the remote changes. Then, when you push your changes up, you end up with both a merge from the remote integration branch into your local branch, and a merge from your feature branch into the integration branch.

Let's take a look at an example of how this situation can happen, and a way to resolve it cleanly. First, let's create two temporary git repositories: "upstream", to represent the remote repository, and "downstream", to represent your local clone of the repository.

~/ $ cd /tmp tmp/ $ git init upstream Initialized empty Git repository in /private/tmp/upstream/.git/ tmp/ $ cd upstream upstream/ $ git config --local receive.denyCurrentBranch ignore # This allows us to push into upstream even when it has a branch checked out upstream/ $ echo 'Demo for how to handle upstream commits after you have merged into the upstream branch.' > README.txt upstream/ $ git add README.txt upstream/ $ git commit -m 'Adding a README file.' [master (root-commit) b8b6630] Adding a README file. 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 README.txt upstream/ $ cd /tmp tmp/ $ git clone upstream downstream Cloning into downstream... done.

Now that we have our upstream repository with one commit on master, and a downstream clone of it, let's add another commit to the upstream repository:

tmp/ $ cd upstream upstream/ $ echo 'Adding another upstream commit.' >> README.txt upstream/ $ git add README.txt upstream/ $ git commit -m 'Adding an upstream commit.' [master 9e625ab] Adding an upstream commit. 1 files changed, 1 insertions(+), 0 deletions(-)

The next step is to add a feature branch and merge commit to the downstream clone. This simulates parallel development between two different developers:

upstream/ $ cd /tmp/downstream downstream/ $ git checkout -b 1234/awesome-feature-branch Switched to a new branch '1234/awesome-feature-branch' downstream/ $ echo 'Adding a downstream commit before pulling into my local master branch.' >> README.txt downstream/ $ git add README.txt downstream/ $ git commit -m 'Adding a downstream commit.' [1234/awesome-feature-branch dff13db] Adding a downstream commit. 1 files changed, 1 insertions(+), 0 deletions(-) downstream/ $ git checkout master Switched to branch 'master' downstream/ $ git merge --no-ff 1234/awesome-feature-branch Merge made by recursive. README.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)

We've completed our feature branch, and have merged it to our local copy of master. Time to push up our merge and share it with the world!

downstream/ $ git push To /tmp/upstream ! [rejected] master -> master (non-fast-forward) error: failed to push some refs to '/tmp/upstream' To prevent you from losing history, non-fast-forward updates were rejected Merge the remote changes (e.g. 'git pull') before pushing again. See the 'Note about fast-forwards' section of 'git push --help' for details. downstream/ $ git pull remote: Counting objects: 5, done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. From /tmp/upstream b8b6630..9e625ab master -> origin/master Auto-merging README.txt CONFLICT (content): Merge conflict in README.txt Automatic merge failed; fix conflicts and then commit the result. downstream/ $ vim README.txt downstream/ $ git add README.txt downstream/ $ git commit [master 46208d7] Merge branch 'master' of /tmp/upstream downstream/ $ git push Counting objects: 11, done. Delta compression using up to 2 threads. Compressing objects: 100% (5/5), done. Writing objects: 100% (7/7), 779 bytes, done. Total 7 (delta 2), reused 0 (delta 0) Unpacking objects: 100% (7/7), done. To /tmp/upstream 9e625ab..46208d7 master -> master

What does our history graph look like now?

  
downstream/ $ git lg
*   46208d7 - (HEAD, origin/master, origin/HEAD, master) Merge branch 'master' of /tmp/upstream (2011-07-29 14:49:38 -0400) <Andrew Ber
|\  
| * 9e625ab - Adding an upstream commit. (2011-07-29 14:33:55 -0400) <Andrew Berry>
* |   1bffd57 - Merge branch '1234/awesome-feature-branch' (2011-07-29 14:37:17 -0400) <Andrew Berry>
|\ \  
| |/  
|/|   
| * dff13db - (1234/awesome-feature-branch) Adding a downstream commit. (2011-07-29 14:35:01 -0400) <Andrew Berry>
|/  
* b8b6630 - Adding a README file. (2011-07-29 14:32:11 -0400) <Andrew Berry>
  

That's pretty confusing. How could we have done this better? Instead of using git pull, let's use git pull --ff-only. Better yet, let's alias that command to git pl by running git config --global alias.pl 'pull --ff-only'. The following was done after I undid the above merges in both repositories using git reset. Never do git reset on a real public branch!

downstream/ $ git pl From /tmp/upstream b8b6630..9e625ab master -> origin/master fatal: Not possible to fast-forward, aborting. downstream/ $ git lg * 9de839b - (HEAD, master) Merge branch '1234/awesome-feature-branch' (2011-07-29 14:52:21 -0400) <Andrew Berry> |\
| * dff13db - (1234/awesome-feature-branch) Adding a downstream commit. (2011-07-29 14:35:01 -0400) <Andrew Berry> |/
| * 9e625ab - (origin/master, origin/HEAD) Adding an upstream commit. (2011-07-29 14:33:55 -0400) <Andrew Berry> |/
* b8b6630 - Adding a README file. (2011-07-29 14:32:11 -0400) <Andrew Berry> downstream/ $ git reset --hard origin/master HEAD is now at 9e625ab Adding an upstream commit. downstream/ $ git merge --no-ff 1234/awesome-feature-branch Auto-merging README.txt CONFLICT (content): Merge conflict in README.txt Resolved 'README.txt' using previous resolution. Automatic merge failed; fix conflicts and then commit the result. downstream/ $ vim README.txt downstream/ $ git add README.txt downstream/ $ git commit [master b8d200d] Merge branch '1234/awesome-feature-branch' downstream/ $ git push Counting objects: 10, done. Delta compression using up to 2 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (6/6), 674 bytes, done. Total 6 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (6/6), done. To /tmp/upstream 9e625ab..b8d200d master -> master

What does our repository look like now?

downstream/ $ git lg * cf13636 - (HEAD, origin/master, origin/HEAD, master) Merge branch '1234/awesome-feature-branch' (2011-08-01 20:44:09 -0400) <Andrew |\
| * dff13db - (1234/awesome-feature-branch) Adding a downstream commit. (2011-07-29 14:35:01 -0400) <Andrew Berry> * | 9e625ab - Adding an upstream commit. (2011-07-29 14:33:55 -0400) <Andrew Berry> |/
* 9568144 - Adding a README file. (2011-08-01 20:41:38 -0400) <Andrew Berry>

The Takeaway

  • Try to prevent extra merge commits when they don't show anything useful about the development of a feature branch.
  • Don't use git pull by default, and if you do, be prepared to undo local merge commits with git reset --hard HEAD^. Use the git pl alias above to simplify this.
  • If you do run into a situation where someone else has pushed a commit to the integration branch before you, use git reset --hard origin/[branchname] on your local copy of the integration branch to remove your merge and create a new one at the tip of the branch.

Footnote

For the command-line-addicted, my alias for git lg in ~/.gitconfig is: [alias] lg = log --all --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%ci) %C(bold blue)<%an>%Creset' --abbrev-commit

Andrew Berry

Thumbnail
Andrew Berry is a architect and developer who works at the intersection of business and technology.