Git Merge

Git-merge allows you to join togeth­er two dif­fer­ent his­to­ries into one. If you’re even slight­ly expe­ri­enced with using Git or anoth­er VCS on projects with a team, you’re prob­a­bly famil­iar with it. 

Here’s a basic work­flow that assume you use a master, develop, and fea­ture branches:

Your master branch is only used for deploy­ing to pro­duc­tion. Your develop branch is only used to deploy to stag­ing. You use fea­ture branch­es (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 pro­duc­tion, those changes are merged into the master branch.

$ git checkout master
$ git merge develop

For a basic overview of the basics of merg­ing, refer to the ear­li­er sec­tion on Git Fun­da­men­tals.

Merg­ing Best Prac­tices #

Here are a few best prac­tices to keep in mind as you work with git-merge.

Com­mit Changes First

Before you attempt a merge, com­mit any changes you have in the work­ing tree. Merges can go wrong (result­ing in con­flicts that you have to resolve) and you want to have as few vari­ables as pos­si­ble. By hav­ing your changes you com­mit­ted before you start a merge, you also remove the chance that you can­not back out of a merge (see Abort­ing a Merge for more information).

Use --no-ff Option

When you merge one branch into anoth­er two dif­fer­ent types of merges are possible:

  • Fast For­ward Merge
  • Non-Fast For­ward Merge

Let’s look at Fast For­ward Merge first. 

Some­times — and prob­a­bly more often than you think, espe­cial­ly if you’re work­ing on a low vol­ume or main­te­nance only project — the develop branch doesn’t under­go any changes at all while you were mak­ing changes on my_feature_branch. You are merg­ing 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 con­cerned this doesn’t require an actu­al merge com­mit. Instead, Git sim­ply points HEAD at the last com­mit in the my_feature_branch. This is often referred to as resolv­ing as a fast-for­ward” and Git will just point develop at the lat­est com­mit made in the fea­ture branch. This cre­ates a lin­ear pro­gres­sion and removes the fea­ture branch from the history.

The oth­er type of merge is a Non-Fast For­ward Merge.

If you have changes in the branch you’re merg­ing into (my_feature_branch -> develop), then Git will cre­ate a merge com­mit show­ing the merged changes. This merge com­mit will show in the his­to­ry the fea­ture branch.

We could also refer to this as a typ­i­cal or nor­mal merge. Both my_feature_branch and develop branch­es have changes in them. Git cre­ates a com­mits for the merge that shows the changes and marks the merge.

So, why does this matter?

With a non-fast for­ward merge com­mit, Git uses the com­mit to doc­u­ment in develop the com­mits you made in the fea­ture branch, pre­serv­ing the true his­to­ry of the repos­i­to­ry. Oth­er­wise, a his­to­ry of that fea­ture branch could be lost completely.

If you’re very par­tic­u­lar about how your repos­i­to­ry his­to­ry is record­ed, this mat­ters a lot.

The default behav­ior of git-merge is to do a fast-for­ward merge when it’s pos­si­ble. To ensure that you don’t lose any repos­i­to­ry his­to­ry, you should always use the --no-ff option when cre­at­ing merges.

$ git merge my_feature_branch --no-ff 

Extra: Git Merge Dis­guised as Pull #

One place you’ve might have already used git-merge but maybe haven’t real­ized it is with git-pull (using the default options).

$ git pull origin develop

Git will fetch the remote repos­i­to­ry (ori­gin devel­op) and then merge it into the local ver­sion. This hap­pens auto­mat­i­cal­ly and doesn’t require that you merge manually.

Abort­ing a Merge #

Some­times — hope­ful­ly not too often — a merge goes unex­pect­ed­ly ter­ri­bly. The most like­ly sce­nario is that you end up with a merge con­flict that you need to resolve. Some­times this is expect­ed (espe­cial­ly if you’re using gen­er­at­ed or com­piled files), oth­er times you expect­ed a smooth merge but some­thing has changed or went wrong with branch or repository.

It is dur­ing these times that you will most like­ly need to abort the merge.

$ git merge --abort

This will abort the cur­rent con­flict res­o­lu­tion process, and try to recon­struct the pre-merge state”, accord­ing to the man­u­al for git-merge command.

I want to reit­er­ate what I said ear­li­er: one impor­tant step is ensure that you have com­mit­ted or stashed all changes in the work­ing tree before you start a merge. This is for safe­ty because some­times a merge can­not be com­plete­ly and cor­rect­ly undone if there are uncom­mit­ted changes.

Resolv­ing a Merge Con­flict #

Now that we’ve looked at ways we can avoid merge con­flicts, let’s walk through how we can deal with them when they, inevitably, occur. 

We are going to look at three dif­fer­ent ways to han­dle merge con­flicts. First, by hand (old school!) and then using a cou­ple of tools that come with Git. 

One quick note: we won’t cov­er using third par­ty tools or appli­ca­tions to han­dle con­flict res­o­lu­tion. There is noth­ing wrong with those tools but my approach is to first learn how to use Git native­ly before poten­tial­ly com­pli­cat­ing things with a lay­er of appli­ca­tion inter­faces. Okay, onward!

Solv­ing Con­flicts Manually

Our first way of solv­ing a Git merge con­flict is to do it man­u­al­ly. This means going into the con­flict­ing files and choos­ing which ver­sion we want to keep and which we want to discard. 

This is par­tic­u­lar­ly handy for com­plex con­flicts where just pick­ing one side of the merge won’t suf­fice. But it’s also easy enough to do for sim­ple con­flicts, too.

Steps for Man­u­al Resolution
  1. Open afflict­ed files
  2. Find con­flicts (if unsure, search for <<<<<<< using grep — grep -lr '<<<<<<<' .)
  3. Review which side of the merge you want to keep.
  4. Save your changes. 
  5. Com­mit the changes to com­plete the merge.

One thing to keep in mind: a git-merge is a rou­tine that doesn’t end until the merge com­pletes. This includes all conflicts. 

Let’s say you don’t want to con­tin­ue the merge because it’s just too much work or you’re not pre­pared to com­plete it right now. You have to back out of the merge rou­tine and get the repos­i­to­ry back to a sta­ble, work­able state. You do that, by the way, with git merge —abort that we dis­cussed earlier.

Solv­ing Con­flicts with git-checkout

Anoth­er tool we can use is the git-checkout com­mand. Instead of resolv­ing the con­flict right in the files, we just tell Git to check out a ver­sion of the repos­i­to­ry based on which side of the merge we want to keep.

One word of warn­ing for this approach: This is an all or noth­ing res­o­lu­tion. We choose one side or the oth­er. Some­times merge con­flicts can be that sim­ple but many times they are not. If you need to choose dif­fer­ent sides of the merge in a sin­gle con­flict res­o­lu­tion, then you’ll want to use the pre­vi­ous method of man­u­al resolution.

To use this method, we do the following:

  1. Run the nor­mal merge, which results in a conflict
  2. If we want to accept the side of the merge we are merg­ing into, we can run git checkout --ours [path/file]
  3. If we want to accept the oth­er side of the merge — the side that we are merg­ing into the cur­rent­ly 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 cur­rent­ly checked out. This is impor­tant because it might not be our” branch with our changes. It could be anoth­er branch, like mas­ter, that doesn’t yet con­tain the changes we made.
  • theirs — This refers to the branch that we merg­ing into our cur­rent­ly checked out branch. This is like­ly the branch that con­tains your changes. But maybe not! Always dou­ble check.

So, to review: ours is the branch we have checked out; theirs is the branch we are merg­ing but don’t have checked out.

This works best if there is only one file with a con­flict. If you have mul­ti­ple files, you’ll have to do this every sin­gle time or you can do it by greping for the con­flicts and then pip­ing the results to xargs where you’ll run the git-checkout for each con­flict­ed 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 com­mit our changed files to resolve the merge con­flict and end our merg­ing routine.

Solv­ing Con­flicts with git-mergetool

git-mergetool is a com­mand in Git that lets you spec­i­fy an exter­nal merge tool you can use when resolv­ing merge conflicts. 

Why is this necessary?

Tech­ni­cal­ly, it’s not nec­es­sary. It is, how­ev­er, nice to have some extra infor­ma­tion (like a enchanced diff).

Valid val­ues include emerge, gvimd­iff, kdiff3, meld, vimd­iff, and tor­toise­merge” accord­ing to the Git doc­u­men­ta­tion.fn

Let’s use vimdiff because it comes installed on many sys­tems (maybe even yours).

When we have a merge con­flict 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 ver­sion of the file. This is the ver­sion of the file in the local branch (the one we have checked out).

The mid­dle col­umn is BASE, which is the com­mon ances­tor of the two files (BASE and REMOTE). 

The right col­umn is the REMOTE ver­sion of the file, which is locat­ed in the branch we’re merg­ing into our checked out branch (LOCAL).

At the bot­tom we see the con­flict­ed file, which con­tains the merge con­flict annotation.

What we need to do now is choose which ver­sion 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 choos­ing the REMOTE ver­sion of the file. The bot­tom por­tion of the screen will change to show the file as it is in REMOTE.

To close out the vimdiff ses­sion we write the changes to the file and quit all.

:wqa

Now we’re ready to com­mit our change.

$ git commit -am "resolving merge conflict"

And we’re all set.

Revert­ing a Merge #

Some­times you get in a sit­u­a­tion — and this is a no-judge­ment zone, we’ve all been there — where you merge branch­es and you messed up and need to undo the merge because, well, because your co-work­ers are upset you broke the project!

Let’s say that hap­pened. How do you revert a merge?

Fol­low­ing the same exam­ple we had last time using my_feature_branch and develop, let’s undo the merge into develop.

Because the merge is a com­mit that points the HEAD to a spe­cif­ic com­mit, we can undo the merge com­mit and roll back to the pre-merge state.

To revert the pre­vi­ous com­mit (our com­mit merge), we do:

git revert HEAD

We can also spec­i­fy the exact merge com­mit that we want to revert using the same revert com­mand but with a cou­ple addi­tion­al options.

$ git revert -m 1 dd8d6f587fa24327d5f5afd6fa8c3e604189c8d4

We spec­i­fy the merge using the SHA of the merge com­mit. The -m switch fol­lowed by the 1 indi­cates that we want to keep the par­ent side of the merge (the branch we are merg­ing into).

The out­come of this revert is that Git will cre­ate a new com­mit that rolls back the changes from the merge. 

Revert­ing a Revert­ed 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 revert­ing a revert­ed merge. Yes, that’s a thing and it’s possible!

If we try to merge the pre­vi­ous­ly merged branch again with­out any changes at all:

git merge my_feature_branch

then Git will polite­ly tell us that every­thing is up-to-date” because those changes are already in the repos­i­to­ry history.

If we make changes to the my_feature_branch branch and com­mit them and then try to re-merge, only the changes that we just made and com­mit­ted will be merged. Again, this is because those changes are already in the history.

So how do we do this?

Just like we did ear­li­er, using the revert com­mand, we now want to revert the merge revert commit.

What?

Merges are com­mits. Revert­ed merges are also com­mits. So to undo a revert­ed merge, we revert the commit.

$ git revert dd8d6g587fa86327d5f5afd6fa8c3e604189c9s2

And now we are where we need to be.