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 withgit reset --hard HEAD^
. Use thegit 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