I recently had a situation where a pair of devs were working on some code, and shared that code between them. I unfortunately wasn’t around to act as an intermediary and merge in to mainline for them. This resulted in 3 separate Push Requests with a number of conflicting changes. Add to that I started merging one set of changes, making my own changes, only to later realize there was all this overlap, so I’d effectively turned this in to a 4-way merge conflict. Oops!
To make this more manageable, I removed my conflict from the picture. To make my changes properly, I need to see the final result of the 3 Push Requests merged in to one. So I created a new Uber Push Request that combined (and fixed) the conflicts between the 3.
To get there, I had to learn more about GIT. 🙂
Branch Management
In our setup, every user has a personal origin repository, which makes reference to the upstream repository. Each of these are stored on GitHub.
Also, each user has a local instance of each branch they choose to checkout. To view the local list of branches, do the following.
An asterisk will be beside the currently active branch.
When you commit a Push Request to GitHub, a new branch is created in the submitter’s repository. sS if you’re like me, you may not have realized how much branch manipulation was going on behind the scenes on GitHub.
To switch to a specific local branch.
The key thing to understand here is that the branches we see are what we have a copy of locally (on the current machine). The GitHub repos don’t necessarily have our changes (yet), just as we don’t necessarily have the changes from the GitHub repo.
To see what remotes you have available, do this:
Unfortunately unlike git branch
, this will not tell you which remote you are tracking. Instead you can do this to check that.
Creating the Uber Merge
In the codebase, I have 2 active branches unrelated to these 3 merges I’m looking to combine (well not actually unrelated, but not important yet). My master branch has a bunch of code that’s not ready to merge, as does a branch named private-user. So where this would normally be straightforward, I can’t use master as a target for this.
The first step is to switch to the upstream repository.
As the output above suggests, this command put us in to a detached HEAD state. Detached HEAD means GIT doesn’t know where to send the changes I make. It also offers a solution: create a branch.
There are actually a few ways to make branches. This one, using checkout -b
creates a local branch from the active state (in this case the current known state of upstream/master), and immediately switches to it.
Notice that unlike earlier when we did a git status
, no branch it is up-to-date with is mentioned. That said it’s a fully realized branch that currently only exist locally.
When you try to push it, it tells you how to add it to your GitHub: by setting the “upstream” of the branch. Using that exact git push
command will make it appear on your GitHub account.
In the case of the Uber Merge, we do want this one to exist on GitHub. Later on though we’ll be creating branches that we don’t care to share with GitHub.
Adding Pull Requests to the Uber Merge
At this point we need to hop over to GitHub, and find the specific Pull Requests that need merging.
At the bottom of the Conversation page you’ll find the big green Merge Pull Request button.
Beside it though is what we care about, the view Command Line Instructions link. This unrolls a set of instructions.
If it was safe for us to merge our changes in to master, we could just do what it says with one caveat (in Step 2, I come back to this in a later section). However our target is not master, so we will be making some changes.
First things first, we need a local copy of the Pull Request. That’s what Step 1 is describing.
Earlier we checked-out upstream/master in to a local repository named fun. In practice this isn’t a good name, but it does illustrate the change that needs to be made to this code. If we did what GitHub recommended, the new branch we’re creating would start from our local master, which in my case is wrong (my local master has changes I’m not ready to commit).
The key takeaway is that when you use checkout -b
, you can include an argument after the branch name to specify a branch to start from. If none is specified (like our original example), then the current active branch is used.
This is potentially where things get a bit confusing. Sorry. Maybe this will clear things up.
All of these are valid ways to create the branch, but the last case, where we explicitly reference upstream/master it can cause problems.
In the last case, an upstream branch is set if you specify the upstream name explicitly (i.e. the upstream is named upstream, instead of origin).
Changing the upstream of a branch is easy. You just need to be aware that it happened.
So finally, here’s how to concisely checkout a Pull Request WITHOUT using your local master.
Noting that: Line 1 is now upstream/master (not master), and line 2 is new.
Do this for every patch you want to merge (we need a local copy).
Merging Pull Requests in to the Uber Merge
First make sure you have your destination branch (i.e. september-2017-uber-merge).
Next, make sure you have local copies of every branch you want to merge in.
Start by switching to the destination branch.
Now we merge them, one Pull Request at a time.
If you know that a file should be preferred over another, you can use this special form of checkout
.
However, not all files can be outright merged as is. A useful tool to have installed in MELD.
On Ubuntu it’s a simple matter of:
GIT will be automatically configured to use meld with mergetool
.
If you make a mistake, you can always close MELD, and tell GIT that the merge was not successful, and try again after.
And that it. Test it, commit the changes, and finally push it.
You may notice some .orig
files scattered about now. You can clean them up by either deleting them by hand, or using clean
.
Repeat until you’ve merged everything.
And that’s it! We’re done!
The Step 2 Caveat: Squashing
I alluded to this earlier, but the default instructions GitHub gives you for merging by hand isn’t necessarily the best way to do it.
To merge, GitHub tells you this:
This is fine, but you should know the --no-ff
command actually performs a squash. What squashing means is all changes are merged in to a single commit, rather than maintaining the history of commits.
For very large projects this might be ideal, as all the intermediary steps taken to the final result aren’t necessary. But in our case we’re a small-large project, and at the moment anyway the full history isn’t an issue.
GitHub’s default is what’s called a Merge Commit, but they actually support two other types: Squash and Rebase. There are settings for controlling which options are available to you, but at this time Merge Commit is adequate for us.
Cleaning-up Branches
If you experimented a bit, or generally worked a long time in a folder, you’re going to have a bunch of stray branches that are no longer needed. To delete a branch do this:
If there are issues with the branch (i.e. uncommitted data), you can forcefully delete it like so:
TODO: Add more notes here about switching branch problems with changes.