February 2009 Archive

Integrating Git with a Visual Merge Tool

February 22nd, 2009

One of the first real points of frustration a developer encounters with Git is the initial unresolved merge conflict.  And it’s only a matter of when and not if it’ll happen.

Merge Conflicts in Git

The root cause of the conflicts is unavoidable in any kind of parallel development: Updates that are made independently may modify the same or overlapping regions of the codebase.  The longer the development remains independent, the greater the probability this will take place.  For a tool that is basically based on branching and parallelism, Git is essentially inviting this “trouble.”  (Rest assured that the benefits are still well worth it.)

As an example, in the course of Insoshi development, we’ve seen merge conflicts arise from

  • Merging new edge features into contributions based on an earlier commit
  • Applying fixes made against our production code to edge

And I’m sure Insoshi community developers encountered a number of conflicts when applying the new tabbed and AJAX-ified layout against code they’d developed against the original layout.

Here’s a re-creation of a recent merge conflict in the Insoshi repository:

$ git checkout -b edge_merge_example 757bf03d90be14fd393d457ec20455700a5fc751
Switched to a new branch "edge_merge_example"
$ git branch exception_fix 07097a3b5a797b4c3d0e3ba53a0ad98a8861db79
$ git merge exception_fix
Auto-merged app/controllers/people_controller.rb
CONFLICT (content): Merge conflict in app/controllers/people_controller.rb
Auto-merged app/models/person.rb
CONFLICT (content): Merge conflict in app/models/person.rb
Removed app/views/connections/show.html.erb
Auto-merged spec/models/person_spec.rb
Automatic merge failed; fix conflicts and then commit the result.

What Git presents in the event of a conflict that requires manual resolution isn’t any different than what you would see in CVS or SVN:

app/models/person.rb

371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
      not deactivated?
    end
  end
 
  # Return the common connections with the given person.
<<<<<<< HEAD:app/models/person.rb
  def common_contacts_with(person, options = {})
    # I tried to do this in SQL for efficiency, but failed miserably.
    # Horrifyingly, MySQL lacks support for the INTERSECT keyword.
    (contacts & person.contacts).paginate(options)
=======
 
  def common_contacts_with(contact, options = {})
    # I tried to do this in SQL for efficiency, but failed miserably.
    # Horrifyingly, MySQL lacks support for the INTERSECT keyword.
    (contacts & contact.contacts).paginate(options)
>>>>>>> exception_fix:app/models/person.rb
  end
 
  protected
 
    ## Callbacks

The contents of conflicting lines from the current version and version to be merged are placed inline with the code separated by “<<<<<<<”, “>>>>>>>” and “=======” markers.  It’s ugly and depending on the extent of the conflicts, can be extremely confusing and time consuming to resolve.  Thankfully, the conflicts in this example appear to be easily manageable.  (And without delving back to far into the commit history, the conflict lines in the persons model probably were copied/cherry-picked from one branch to another and then an additional correction was made to one branch).

So for all of the things that Git does right, why doesn’t it offer a better way? The short answer is that it can’t by itself.  It needs help.

Visual Merge Tools

A manual 3-way merge is the way to resolve these conflict.  You’re using 3 versions of a file as the starting points: the version on the branch you’re on, the version on the branch you’re merging in, and their common ancestor.  The common ancestor is used as the base with updates from either of the other two are applied on top of as direct selections or edited combinations at each conflict point.

Visual merge tools provide the interface for processing the 3-way merge and reduce the time and effort required.  You’re presented with the three versions and can easily choose code from any and all of them to resolve conflicts.

If you’ve been using a VCS tool for a while, you’re probably already using a merge tool, either the one that came with it or one that you’ve picked up along the way out of necessity.  If you haven’t used one before, now’s a good time as any to start.

Merge tools that I know about include

This is by no means a complete list.  Be aware that some merge tools only offer a 2-way merge or require an upgraded/pro version to perform 3-way merges.

git mergetool

The git mergetool command allows for the integration of those tools into the merge process.  Run after merge conflicts have been identified, it loops through the files that need to be resolved and provides the specified tool with the version information necessary to invoke the 3-way merge.

git mergetool already includes support for a number open source and freely available merge tools: kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff, ecmerge, and opendiff.

Support for additional tools including DiffMerge and Araxis Merge can be added via custom configuration settings provided a command-line call exists:

git config --global mergetool.[tool].cmd [command-line call]
git config --global mergetool.[tool].trustExitCode [true|false]

The “–global” flag is used so the setting will apply across all of your Git repositories.

The command line needs to accept the following file variables passed in as parameters:

  • $LOCAL – Current branch version
  • $REMOTE – Version to be merged
  • $BASE – Common ancestor
  • $MERGED – File where results will be written

git mergetool will create the versions as temporary files and set the variables appropriately before the tool command-line is executed.

If the tool returns a proper exit code after a successful or unsuccessful merge, then the trustExitCode setting can be set to true.  Otherwise set it as false so you will be prompted as to whether the merge conflicts for a file were resolved.

Performing a Merge with Conflicts

The sequence of commands for a merge using mergetool would be

git merge
git mergetool -t [tool]
git add .
git commit

You can specify a default tool via the merge.tool setting

git config --global merge.tool [tool]

This will allow you to just simply call

git mergetool

Using a Supported Merge Tool

My main machine is a MacBook Pro and since I have Apple Developer Tools installed, I have opendiff/FileMerge installed.  It’s one of the supported merge tools so I can immediately start using it:

git mergetool -t opendiff

For the Insoshi conflict example you’ll see

$ git mergetool -t opendiff
Merging the files: app/controllers/people_controller.rb
app/models/person.rb
 
Normal merge conflict for 'app/controllers/people_controller.rb':
 {local}: modified
 {remote}: modified
Hit return to start merge resolution tool (opendiff):
 
Normal merge conflict for 'app/models/person.rb':
 {local}: modified
 {remote}: modified
Hit return to start merge resolution tool (opendiff):

The FileMerge window for app/models/person.rb looks like

opendiff

Because FileMerge is now handling the merge for this file, it’s processing all the differences and not just the conflict area.  You can see this on the bottom status:

  • status: 12 differences (12 left, 1 right, 1 conflict)

If you haven’t used FileMerge before, you’ll need to do a couple of things that aren’t immediately obvious:

  • Drag up to reveal the “Ancestor” pane at the bottom of the window
  • Click on the conflict section numbers located between the LOCAL and REMOTE panes in order to select an action

If you complete the merge and save it, mergetool will accept the result.

If you abort and quit out of the merge without saving, you will be prompted if the save was successful.

app/models/person.rb seems unchanged.
Was the merge successful? [y/n] n
merge of app/models/person.rb failed

If you’re in the middle of resolving conflicts in a number of files, this will exit out from the process.  You just need to re-run the mergetool command:

git mergetool -t opendiff

and it’ll pick up with the last unmerged file.

Using a Configured Merge Tool

DiffMerge is available for OS X.  The binary install is simple (the usual copy to the Application folder) and it comes with a shell script so it can be invoked from the command-line.  The minimal command to start a 3-way merge is

diffmerge --merge --result=$MERGED $LOCAL $BASE $REMOTE

The order of the files dictates which pane (left, middle or right) it be displayed in.  The middle pane is used as the editor for the merge and will need to start off with the common BASE version.  You can explicitly set the title of each pane via -t1=, -t2=, -t3= flags if you like (see the DiffMerge documentation [PDF]).

I’ve tested the return code for successful and aborted merge scenarios and found that it returns 0 if the DiffMerge application ran successfully, not if the actual merge was successful.  Git will rely on a comparison between the temporary $MERGED file to determine if any changes were saved.  It will prompt you in the event the file is unchanged.

The configuration settings will be

git config --global mergetool.diffmerge.cmd \
                    "diffmerge --merge --result=\$MERGED \$LOCAL \$BASE \$REMOTE"
git config --global mergetool.diffmerge.trustExitCode false

You need to escape the $’s in the variables otherwise the shell will try the do the substitution before command gets placed in the configuration.

The .gitconfig entries will look like

[mergetool "diffmerge"]
        cmd = diffmerge --merge --result=$MERGED $LOCAL $BASE $REMOTE
        trustExitCode = false

Running

git mergetool -t diffmerge

starts DiffMerge showing the current version in the left pane, the merge result in the middle pane and the remote version in the right pane:

diffmerge

This may be easier for you to use to visualize the merge process than opendiff/FileMerge.

You should see the same basic output from execution of git mergetool with DiffMerge as was seen from opendiff/FileMerge:

$ git mergetool -t diffmerge
Merging the files: app/controllers/people_controller.rb
app/models/person.rb
 
Normal merge conflict for 'app/controllers/people_controller.rb':
  {local}: modified
  {remote}: modified
Hit return to start merge resolution tool (diffmerge): 
 
Normal merge conflict for 'app/models/person.rb':
  {local}: modified
  {remote}: modified
Hit return to start merge resolution tool (diffmerge):

If you’re using Git on Windows, definitely check out these posts on configuring DiffMerge and Git:

There’s always a few extra things to be aware of on the Windows platform, especially when it comes to passing in command-line arguments in mixed UNIX shell/Windows CMD context.

A few final notes:

  • git mergetool saves the merge-conflict version of the file with a “.orig” suffix.  Make sure to delete it before adding and committing the merge or add *.orig to your .gitignore
  • A custom merge driver can be configured and set in order to override the built-in behavior of the git merge command.  This could be useful in order to deal with additional filetypes via an external diff/merge tool or if you wanted to invoke a visual merge tool for all merges.
  • IDE integrations for handling merges will depend both on the level of integration that’s possible with Git and the merge tool.