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
Looking 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?
Before 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.
- Fork the main repository, creating a copy of the master repository on your own account.
- 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.
- 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]> |
After 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.