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. 🙂
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.
git branch # * master # september-2017-uber-merge # sgstair-simulator_cache_fixes
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.
git checkout branch-name # to checkout branch-name git checkout master # to return to the master branch git checkout # actually does nothing
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:
git remote # origin # upstream
git branch, this will not tell you which remote you are tracking. Instead you can do this to check that.
git status # On branch master # Your branch is up-to-date with 'origin/master'. # nothing to commit, working tree clean
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.
git checkout upstream/master # Note: checking out 'upstream/master'. # # You are in 'detached HEAD' state. You can look around, make experimental # changes and commit them, and you can discard any commits you make in this # state without impacting any branches by performing another checkout. # # If you want to create a new branch to retain commits you create, you may # do so (now or later) by using -b with the checkout command again. Example: # # git checkout -b <new-branch-name> # # HEAD is now at 3502a28d... Merge pull request #1211 from sgstair/comment_avatar
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.
git checkout -b fun # Switched to a new branch 'fun'
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.
git status # On branch fun # nothing to commit, working tree clean git branch # * fun # master # september-2017-uber-merge # sgstair-simulator_cache_fixes git push # fatal: The current branch fun has no upstream branch. # To push the current branch and set the remote as upstream, use # # git push --set-upstream origin fun #
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.
git checkout -b sgstair-notification_api fun
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.
git checkout -b branch-of-current-branch git checkout -b branch-of-other-branch fun # where 'fun' is another branch git checkout -b branch-of-upstream upstream/master # WARNING!
All of these are valid ways to create the branch, but the last case, where we explicitly reference upstream/master it can cause problems.
git checkout branch-of-current-branch git status # On branch branch-of-current-branch # nothing to commit, working tree clean git checkout branch-of-other-branch git status # On branch branch-of-other-branch # nothing to commit, working tree clean git checkout branch-of-upstream # On branch branch-of-upstream # Your branch is up-to-date with 'upstream/master'. # nothing to commit, working tree clean
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.
git branch --unset-upstream # This is usually what you want git branch -u origin/some-branch # But you could set it git branch -u origin/some-branch branch-of-upstream # if it's not the active branch
So finally, here’s how to concisely checkout a Pull Request WITHOUT using your local master.
git checkout -b sgstair-notification_api upstream/master git branch --unset-upstream git pull https://github.com/sgstair/ludumdare.git notification_api
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.
git checkout september-2017-uber-merge git branch # local-minimum-notification_ui # master # * september-2017-uber-merge # sgstair-notification_api # sgstair-simulator_cache_fixes
Now we merge them, one Pull Request at a time.
git merge sgstair-simulator_cache_fixes git push # no problem, first one is always clean git merge sgstair-notification_api # Auto-merging sandbox/simulate_ld_event # CONFLICT (content): Merge conflict in sandbox/simulate_ld_event # Auto-merging public-api/vx/node.php # CONFLICT (content): Merge conflict in public-api/vx/node.php # Auto-merging .gitignore # CONFLICT (content): Merge conflict in .gitignore # Automatic merge failed; fix conflicts and then commit the result. git status # On branch september-2017-uber-merge # Your branch is up-to-date with 'origin/september-2017-uber-merge'. # You have unmerged paths. # (fix conflicts and run "git commit") # (use "git merge --abort" to abort the merge) # # Changes to be committed: # # modified: public-api/vx/note.php # new file: public-api/vx/notification.php # modified: src/shrub/src/constants.php # modified: src/shrub/src/node/node_core.php # modified: src/shrub/src/note/note_core.php # new file: src/shrub/src/notification/constants.php # new file: src/shrub/src/notification/notification.php # new file: src/shrub/src/notification/notification_core.php # new file: src/shrub/src/notification/table_create.php # modified: src/shrub/src/user/table_create.php # modified: src/shrub/src/user/user.php # new file: src/shrub/src/user/user_notification.php # # Unmerged paths: # (use "git add <file>..." to mark resolution) # # both modified: .gitignore # both modified: public-api/vx/node.php # both modified: sandbox/simulate_ld_event #
If you know that a file should be preferred over another, you can use this special form of
git checkout --ours .gitignore # to keep our version git checkout --theirs sandbox/simulate_ld_event # to use their version git diff # You can check if it worked with a diff # diff --cc sandbox/simulate_ld_event # index 1dea9916,ece62277..00000000 # --- a/sandbox/simulate_ld_event # +++ b/sandbox/simulate_ld_event # Result is simple (NOTE: only the sandbox/simulate_ld_event changes are shown) # Finally, don't forget to add the files after merging them!! git add .gitignore git add sandbox/simulate_ld_event
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:
sudo apt install meld
GIT will be automatically configured to use meld with
git mergetool # run the default merge tool for all conflicts
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.
git commit -m "merged 'sgstair-notification_api'" git push git status # On branch september-2017-uber-merge # Your branch is up-to-date with 'origin/september-2017-uber-merge'. # Untracked files: # (use "git add <file>..." to include in what will be committed) # # .gitignore.orig # public-api/vx/node.php.orig # # nothing added to commit but untracked files present (use "git add" to track)
You may notice some
.orig files scattered about now. You can clean them up by either deleting them by hand, or using
git clean -fd
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:
git merge --no-ff local-minimum-notification_ui
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.
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:
git branch -d branch-name # Deleted branch branch-name (was 3502a28d).
If there are issues with the branch (i.e. uncommitted data), you can forcefully delete it like so:
git branch -D branch-name # Deleted branch branch-name (was 3502a28d).
TODO: Add more notes here about switching branch problems with changes.