Git Merge
Git-merge allows you to join together two different histories into one. If you’re even slightly experienced with using Git or another VCS on projects with a team, you’re probably familiar with it.
Here’s a basic workflow that assume you use a master
, develop
, and feature branches:
Your master
branch is only used for deploying to production. Your develop
branch is only used to deploy to staging. You use feature branches (based off of develop
) to make changes and then merge those changes back into the develop
branch.
$ git checkout develop
$ git merge my_feature_branch
When you’re ready to deploy to production, those changes are merged into the master
branch.
$ git checkout master
$ git merge develop
For a basic overview of the basics of merging, refer to the earlier section on Git Fundamentals.
Merging Best Practices #
Here are a few best practices to keep in mind as you work with git-merge.
Commit Changes First
Before you attempt a merge, commit any changes you have in the working tree. Merges can go wrong (resulting in conflicts that you have to resolve) and you want to have as few variables as possible. By having your changes you committed before you start a merge, you also remove the chance that you cannot back out of a merge (see Aborting a Merge for more information).
Use --no-ff
Option
When you merge one branch into another two different types of merges are possible:
- Fast Forward Merge
- Non-Fast Forward Merge
Let’s look at Fast Forward Merge first.
Sometimes — and probably more often than you think, especially if you’re working on a low volume or maintenance only project — the develop
branch doesn’t undergo any changes at all while you were making changes on my_feature_branch
. You are merging my_feature_branch
into develop
and develop
is in the same state that it was when you branched off to start my_feature_branch
.
As far as Git is concerned this doesn’t require an actual merge commit. Instead, Git simply points HEAD at the last commit in the my_feature_branch
. This is often referred to as “resolving as a fast-forward” and Git will just point develop
at the latest commit made in the feature branch. This creates a linear progression and removes the feature branch from the history.
The other type of merge is a Non-Fast Forward Merge.
If you have changes in the branch you’re merging into (my_feature_branch -> develop
), then Git will create a merge commit showing the merged changes. This merge commit will show in the history the feature branch.
We could also refer to this as a typical or normal merge. Both my_feature_branch
and develop
branches have changes in them. Git creates a commits for the merge that shows the changes and marks the merge.
So, why does this matter?
With a non-fast forward merge commit, Git uses the commit to document in develop
the commits you made in the feature branch, preserving the true history of the repository. Otherwise, a history of that feature branch could be lost completely.
If you’re very particular about how your repository history is recorded, this matters a lot.
The default behavior of git-merge
is to do a fast-forward merge when it’s possible. To ensure that you don’t lose any repository history, you should always use the --no-ff
option when creating merges.
$ git merge my_feature_branch --no-ff
Extra: Git Merge Disguised as Pull #
One place you’ve might have already used git-merge but maybe haven’t realized it is with git-pull (using the default options).
$ git pull origin develop
Git will fetch the remote repository (origin develop) and then merge it into the local version. This happens automatically and doesn’t require that you merge manually.
Aborting a Merge #
Sometimes — hopefully not too often — a merge goes unexpectedly terribly. The most likely scenario is that you end up with a merge conflict that you need to resolve. Sometimes this is expected (especially if you’re using generated or compiled files), other times you expected a smooth merge but something has changed or went wrong with branch or repository.
It is during these times that you will most likely need to abort the merge.
$ git merge --abort
This will “abort the current conflict resolution process, and try to reconstruct the pre-merge state”, according to the manual for git-merge command.
I want to reiterate what I said earlier: one important step is ensure that you have committed or stashed all changes in the working tree before you start a merge. This is for safety because sometimes a merge cannot be completely and correctly undone if there are uncommitted changes.
Resolving a Merge Conflict #
Now that we’ve looked at ways we can avoid merge conflicts, let’s walk through how we can deal with them when they, inevitably, occur.
We are going to look at three different ways to handle merge conflicts. First, by hand (old school!) and then using a couple of tools that come with Git.
One quick note: we won’t cover using third party tools or applications to handle conflict resolution. There is nothing wrong with those tools but my approach is to first learn how to use Git natively before potentially complicating things with a layer of application interfaces. Okay, onward!
Solving Conflicts Manually
Our first way of solving a Git merge conflict is to do it manually. This means going into the conflicting files and choosing which version we want to keep and which we want to discard.
This is particularly handy for complex conflicts where just picking one side of the merge won’t suffice. But it’s also easy enough to do for simple conflicts, too.
Steps for Manual Resolution
- Open afflicted files
- Find conflicts (if unsure, search for
<<<<<<<
using grep —grep -lr '<<<<<<<' .
) - Review which side of the merge you want to keep.
- Save your changes.
- Commit the changes to complete the merge.
One thing to keep in mind: a git-merge
is a routine that doesn’t end until the merge completes. This includes all conflicts.
Let’s say you don’t want to continue the merge because it’s just too much work or you’re not prepared to complete it right now. You have to back out of the merge routine and get the repository back to a stable, workable state. You do that, by the way, with git merge —abort that we discussed earlier.
Solving Conflicts with git-checkout
Another tool we can use is the git-checkout
command. Instead of resolving the conflict right in the files, we just tell Git to check out a version of the repository based on which side of the merge we want to keep.
One word of warning for this approach: This is an all or nothing resolution. We choose one side or the other. Sometimes merge conflicts can be that simple but many times they are not. If you need to choose different sides of the merge in a single conflict resolution, then you’ll want to use the previous method of manual resolution.
To use this method, we do the following:
- Run the normal merge, which results in a conflict
- If we want to accept the side of the merge we are merging into, we can run
git checkout --ours [path/file]
- If we want to accept the other side of the merge — the side that we are merging into the currently checked out branch — then we will run
git checkout --theirs [path/file]
.
Let’s define the ours
and theirs
terms a bit more.
ours
— This refers to the branch that we have currently checked out. This is important because it might not be “our” branch with our changes. It could be another branch, like master, that doesn’t yet contain the changes we made.theirs
— This refers to the branch that we merging into our currently checked out branch. This is likely the branch that contains your changes. But maybe not! Always double check.
So, to review: ours
is the branch we have checked out; theirs
is the branch we are merging but don’t have checked out.
This works best if there is only one file with a conflict. If you have multiple files, you’ll have to do this every single time or you can do it by grep
ing for the conflicts and then piping the results to xargs
where you’ll run the git-checkout
for each conflicted file.
$ grep -lr '<<<<<<<' . | xargs git checkout --ours/--theirs
It’s out of scope to go into what xargs
is but just know that it allows us to pipe every file found from grep
and run git checkout
against it.
Now we can commit our changed files to resolve the merge conflict and end our merging routine.
Solving Conflicts with git-mergetool
git-mergetool
is a command in Git that lets you specify an external merge tool you can use when resolving merge conflicts.
Why is this necessary?
Technically, it’s not necessary. It is, however, nice to have some extra information (like a enchanced diff).
“Valid values include emerge, gvimdiff, kdiff3, meld, vimdiff, and tortoisemerge” according to the Git documentation.fn
Let’s use vimdiff
because it comes installed on many systems (maybe even yours).
When we have a merge conflict we need to resolve, we run:
$ git mergetool --tool=vimdiff
This will open the vimdiff
tool and it shows the three parts of the merge (this is why you hear of merges called three-way merges).
On the left is the LOCAL version of the file. This is the version of the file in the local branch (the one we have checked out).
The middle column is BASE, which is the common ancestor of the two files (BASE and REMOTE).
The right column is the REMOTE version of the file, which is located in the branch we’re merging into our checked out branch (LOCAL).
At the bottom we see the conflicted file, which contains the merge conflict annotation.
What we need to do now is choose which version of the file we want to keep in order to resolve our merge conflict.
We do this by using the Vim syntax:
:diffg RE
The RE
means we are choosing the REMOTE version of the file. The bottom portion of the screen will change to show the file as it is in REMOTE.
To close out the vimdiff
session we write the changes to the file and quit all.
:wqa
Now we’re ready to commit our change.
$ git commit -am "resolving merge conflict"
And we’re all set.
Reverting a Merge #
Sometimes you get in a situation — and this is a no-judgement zone, we’ve all been there — where you merge branches and you messed up and need to undo the merge because, well, because your co-workers are upset you broke the project!
Let’s say that happened. How do you revert a merge?
Following the same example we had last time using my_feature_branch
and develop
, let’s undo the merge into develop
.
Because the merge is a commit that points the HEAD to a specific commit, we can undo the merge commit and roll back to the pre-merge state.
To revert the previous commit (our commit merge), we do:
git revert HEAD
We can also specify the exact merge commit that we want to revert using the same revert
command but with a couple additional options.
$ git revert -m 1 dd8d6f587fa24327d5f5afd6fa8c3e604189c8d4
We specify the merge using the SHA of the merge commit. The -m
switch followed by the 1
indicates that we want to keep the parent side of the merge (the branch we are merging into).
The outcome of this revert is that Git will create a new commit that rolls back the changes from the merge.
Reverting a Reverted Merge #
With our merge undone and the issues in the merge fixed, we’re ready to re-merge those changes. In Git this is called reverting a reverted merge. Yes, that’s a thing and it’s possible!
If we try to merge the previously merged branch again without any changes at all:
git merge my_feature_branch
then Git will politely tell us that everything is “up-to-date” because those changes are already in the repository history.
If we make changes to the my_feature_branch
branch and commit them and then try to re-merge, only the changes that we just made and committed will be merged. Again, this is because those changes are already in the history.
So how do we do this?
Just like we did earlier, using the revert
command, we now want to revert the merge revert commit.
What?
Merges are commits. Reverted merges are also commits. So to undo a reverted merge, we revert the commit.
$ git revert dd8d6g587fa86327d5f5afd6fa8c3e604189c9s2
And now we are where we need to be.