Fixing a Pull Request from Master

So you created an awesome pull request with a couple of features, but the evil(?) maintainer (could be me) just denied it and said you shouldn’t make pull requests from master? Then you’ve come to the right place to learn what went wrong and how to fix it.

Current (Incorrect) State

2015-03-25 09_01_35-FixPRFromMaster (master) - Git ExtensionsLooking at the history of your repository, it probably looks something like the image to the right. There’s a base commit and then you’ve added one commit for each of the features to it. And everything is done on the branch master.

The image was created with Git Extensions. Although I prefer using the command line when working with git, having a tool that can display the branches and commits graphically is indispensible to understand what’s happening.

Doing everytying on master as shown in the image is wrong for a couple of reasons that might not be obvious when first starting working with git.

  • Every single feature should be created in its own branch. Branches are so lightweight in git that it’s okay to branch all the time.
  • Submitting a pull request from your own master branch is a recipe for disaster, as it effectively prevents you from getting the latest code from the upstream repository (more on that later).

Unfortunately it’s easy to end up in this situation with git. Fortunately, it’s also quite easy to fix.

How Did I End Up Here?

ForkBefore looking into how to fix things, we have to understand what happened. When working on GitHub, the way to submit code is through a pull request from one’s own fork. To get started coding, there are usually two simple steps.

  1. Fork the main repository, creating a copy of the master repository on your own account.
  2. Clone the forked repository to your local disk.

Those steps are correct, but it’s not all that’s needed.

To understand what’s needed we have to look at the three different copies of the repository that now exists.

upstream-origin-local

  • The local repository is the one existing on your computer.
  • The origin repository is the one that you cloned your local repository from. In this case it is the fork that you created.
  • The repository that the fork was created from is called upstream.

Originally, the upstream repository is the only one existing. When forked, a copy is created. That copying is a one time operation. Then origin is cloned into the local repository. Changes done locally are replicated to the origin by pushing changes and if there are multiple collaborators on working on origin changes done by others are pulled down to the local repository.

To submit changes done locally a pull request is used. First the changes need to be pushed to origin, then a pull request is issued to request that the maintainer of the upstream repository merge those changes.

What’s missing is a way to synchronize changes done in the upstream repository to the local repository. Initially that was done by forking and cloning, but forking is a one time process.

To synchronize upstream changes to the local repository a reference from the local repository to the upstream must be added.

C:\git\FixPRFromMaster [master]> git remote add upstream https://github.com/KentorIT/FixPRFromMaster

Normally the next step would be to issue a git pull to get the changes from the upstream master into the local repository. This is usually done by having the local branch track the upstream master. That’s why no changes should ever be done directly on master. That branch is reserved for tracking and receiving updates from the upstream. Before we can get the changes from the upstream master we need to reorganize our local changes.

Cleaning Up Local Branches

To clean up the branches, we first need to backup the existing branch and establish master as it should be.

C:\git\FixPRFromMaster [master]> git branch MyMessedUpBranch
C:\git\FixPRFromMaster [master]> git reset --hard cef7
HEAD is now at cef7454 Adding file.

Then it’s time to create branches for each of those features and bring in the commit(s) for each of them into the freshly created branches with git cherry-pick.

C:\git\FixPRFromMaster [master]> git checkout -b Feature1
Switched to a new branch 'Feature1'
C:\git\FixPRFromMaster [Feature1]> git cherry-pick 8c58
[Feature1 4d86bc2] Adding feature 1.
 1 file changed, 0 insertions(+), 0 deletions(-)
C:\git\FixPRFromMaster [Feature1]> git push -u origin Feature1
Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 304 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://github.com/AndersAbel/FixPRFromMaster
 * [new branch]      Feature1 -> Feature1
Branch Feature1 set up to track remote branch Feature1 from origin.
C:\git\FixPRFromMaster [Feature1]> git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
C:\git\FixPRFromMaster [master]> git checkout -b Feature2
Switched to a new branch 'Feature2'
C:\git\FixPRFromMaster [Feature2]> git cherry-pick 38ed
warning: Cannot merge binary files: file.txt (HEAD vs. 38ed08d... Adding feature 2.)
error: could not apply 38ed08d... Adding feature 2.
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
C:\git\FixPRFromMaster [Feature2 +0 ~0 -0 !1 | +0 ~0 -0 !1]> git mergetool
Merging:
file.txt
 
Normal merge conflict for 'file.txt':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (kdiff3):
C:\git\FixPRFromMaster [Feature2 +0 ~1 -0]> git diff
C:\git\FixPRFromMaster [Feature2 +0 ~1 -0]> notepad .\file.txt
C:\git\FixPRFromMaster [Feature2 +0 ~1 -0]> git add .
C:\git\FixPRFromMaster [Feature2 +0 ~1 -0]> git commit -m "Adding feature 2."
[Feature2 f7a16b9] Adding feature 2.
 1 file changed, 0 insertions(+), 0 deletions(-)
C:\git\FixPRFromMaster [Feature2]> git push -u origin Feature2
Counting objects: 9, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 309 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://github.com/AndersAbel/FixPRFromMaster
 * [new branch]      Feature2 -> Feature2
Branch Feature2 set up to track remote branch Feature2 from origin.
C:\git\FixPRFromMaster [Feature2]>

Pulling Upstream Changes and Rebasing

We’re finally ready to get the changes from the upstream repository.

C:\git\FixPRFromMaster [Feature2]> git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)
C:\git\FixPRFromMaster [master]> git pull -u upstream master
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/KentorIT/FixPRFromMaster
 * branch            master     -> FETCH_HEAD
 * [new branch]      master     -> upstream/master
Updating cef7454..010cacf
Fast-forward
 file.txt | Bin 30 -> 170 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
C:\git\FixPRFromMaster [master]>

2015-03-26 13_38_43-FixPRFromMaster (master) - Git ExtensionsAfter all the this, the branches should look significantly better (and more complicated). The local master is in synch with the upstream master. The features are in separate branches. The MyMessedUpBranch branch and the origin/master doesn’t look to good – but they doesn’t really matter. The master of origin is never used for anything anyway.

There’s only one problem left to fix, to rebase our feature branches on top of master to give the maintainer an easy merge.

Rebasing Feature Branches

Traditionally when working with source control system, it has been a pain to have branches. Not so much because creating a branch is a pain as because of the pain to merge the branch back if there’s been substantial changes to the master/main branch. With git, that is handled by rebasing the feature branch. Rebase is a process where the branch is updated to originate from the tip of the master branch, instead of some way back. A properly rebased branch cannot, by definition, give any merge conflicts.

C:\git\FixPRFromMaster [master]> git checkout Feature1
Switched to branch 'Feature1'
Your branch is up-to-date with 'origin/Feature1'.
C:\git\FixPRFromMaster [Feature1]> git rebase master
First, rewinding head to replay your work on top of it...
Applying: Adding feature 1.
Using index info to reconstruct a base tree...
M       file.txt
Falling back to patching base and 3-way merge...
warning: Cannot merge binary files: file.txt (HEAD vs. Adding feature 1.)
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Failed to merge in the changes.
Patch failed at 0001 Adding feature 1.
The copy of the patch that failed is found in:
   c:/git/FixPRFromMaster/.git/rebase-apply/patch
 
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
 
C:\git\FixPRFromMaster [(010cacf...)|REBASE +0 ~0 -0 !1 | +0 ~0 -0 !1]> git mergetool
Merging:
file.txt
 
Normal merge conflict for 'file.txt':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (kdiff3):
C:\git\FixPRFromMaster [(010cacf...)|REBASE +0 ~1 -0]> git diff --cached
diff --git a/file.txt b/file.txt
index 55251e8..fecfb7f 100644
Binary files a/file.txt and b/file.txt differ
C:\git\FixPRFromMaster [(010cacf...)|REBASE +0 ~1 -0]> git rebase --continue
Applying: Adding feature 1.
C:\git\FixPRFromMaster [Feature1]> git push -f
Counting objects: 14, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 638 bytes | 0 bytes/s, done.
Total 6 (delta 1), reused 0 (delta 0)
To https://github.com/AndersAbel/FixPRFromMaster
 + 4d86bc2...a2fa585 Feature1 -> Feature1 (forced update)
C:\git\spikes\FixPRFromMaster [Feature1]>

The price to pay for having no merge conflicts is that the conflicts occur during rebase instead. Once fixed, I force push the updated branch to origin. A rebase is a rewrite of the history, so a force push is needed. In this (rare) case it is the right thing to do, but in most cases force push should be avoided.

Now, finally I can make pull requests on GitHub for my features and hopefully get them merged by a happy maintainer.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.