#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Script for automating hg merges.
To use this script, specify it in your ~/.hgrc or mercurial.ini:
[ui]
merge = /path/to/hgmerge.py

This script is expected to be called directly from Mercurial with
the arguments: local base other

By default the script will autodetect the merge tools installed on
your machine and use the first tools it detects.

The user can override the tool detection order by specifying
HGMERGE_TOOL=[noninteractive,]interactive in their environment.

Where noninteractive is one of:
    simplemerge, merge, diff3

and interactive is one of:
    gpyfm, kdiff3, tortoisemerge, p4merge, meld, tkdiff, filemerge,
    ecmerge, xxdiff, guiffy, diffmerge, vim

    or a special wrapper: TakeMine, TakeOther, external

Or the user can creat an hgmerge section in their hgrc (Mercurial.ini on
Windows).

# An example hgmerge configuration
[hgmerge]
noninteractive  = diff3            # default noninteractive tool
interactive     = kdiff3           # default interactive tool
caseinsensitive = 1                # make extension matches case insensitive
ext.cpp         = kdiff3           # merge cpp files w/kdiff3 interactive only
ext.hxx         = simplemerge,diffmerge
ext.lib         = TakeMine         # do not merge, ignore other
ext.doc         = external WordMerge $base $local $other $output

The HGMERGE_TOOL environment variable takes precedence over the hgrc
settings.

By specifying only an interactive tool the user can bypass the
non-interactive merge attempt, which some people consider unsafe. The
script will not run without finding an interactive tool.

When the tool(s) are selected by file extension, the usual binary and
symlink checks are disabled allowing you do merge normally unmergable
files via special (and possibly proprietary) diff/merge tools.
"""

__authors__ = "Arve Knudsen <arvenk@simula.no>, Steve Borho <steve@borho.org>"
__license__ = "MIT"
__version__ = "0.5"

import sys, os, subprocess, random, stat
import shutil, filecmp, StringIO, glob, re
try:
    import pygtk
    import gtk
    has_gtk = True
except ImportError:
    has_gtk = False

def get_tools(ext=''):
    """ Determine the tools to use for non-interactive and interactive merge.
    @return: If found, (non-interactive, interactive, ext_filter), else None
    Else, C{None}.
    """
    def read_config():
        '''Read [hgmerge] section of user's configuration file'''
        config = {}
        hg = MergeTool()
        if hg.detect('hg'):
            output = ''
            try:
                output = subprocess.Popen([hg.path, 'showconfig', 'hgmerge'],
                    stdout=subprocess.PIPE).communicate()[0]
            except:
                pass
            for ii in output.splitlines():
                key,value = ii[8:].split('=', 1)
                config[key]=value
                if key.startswith('ext'):
                    config[key.upper()]=value
        return config

    non = None
    inter = None
    ext_filter = False
    config = read_config()

    # Environment variable HGMERGE_TOOL has top precedence
    requested = os.environ.get("HGMERGE_TOOL")

    # Followed by file extension based configuration
    if not requested:
        key = 'ext' + ext
        if config.has_key('caseinsensitive'):
            val = config['caseinsensitive']
            if val in ('1', 'True', 'yes'):
                key = key.upper()
        if config.has_key(key):
            requested = config[key]
            ext_filter = True

    if requested:
        try: 
            non, inter = requested.split(',', 1)
        except ValueError:
            non, inter = None, requested
    else:
        # Fall back to generic non-interactive and interactive tools
        try:
            inter = config['interactive']
            requested = True
            non = config['noninteractive']
        except KeyError:
            pass

    noninteractive = None
    if requested:
        if non:
            for name, tool in noninteractive_tools:
                if non == name and tool.detect():
                    noninteractive = tool
                    break
            else:
                print >>sys.stderr, 'Could not find noninteractive tool', non

        if inter == 'TakeMine':
            return (noninteractive, TakeMine(), ext_filter)
        elif inter == 'TakeOther':
            return (noninteractive, TakeOther(), ext_filter)
        elif inter.startswith == 'external ':
            tool = External()
            tool.set_command(inter[9:])
            return (noninteractive, tool, ext_filter)
        for name, tool in interactive_tools:
            if inter == name and tool.detect():
                return (noninteractive, tool, ext_filter)
        else:
            print >>sys.stderr, 'Could not find interactive tool', inter

    for name, tool in noninteractive_tools:
        if tool.detect():
            print 'Auto-detected %s noninteractive merge tool' % name
            noninteractive = tool
            break
    for name, tool in interactive_tools:
        if tool.detect():
            print 'Auto-detected %s interactive merge tool' % name
            return (noninteractive, tool, False)
    return None

def autodetection():
    found = False
    for name, tool in interactive_tools:
        if tool.detect():
            found = True
            print name
    if not found:
        sys.stderr.write('No interactive merge tools found\n')

class MergeFailed(Exception):
    def __init__(self, code, msg):
        Exception.__init__(self, "Merge failed: '%s'" % (msg,))
        self.code, self.msg = code, msg

def eoltype(name):
    try:
        # First check if it's a symlink
        lmode = os.lstat(name)[stat.ST_MODE]
        if stat.S_ISLNK(lmode):
            return 'symlink'

        # Look for tell-tale signs in first 1K of file
        f = open(name, "rb")
        data = f.read(1024)
        f.close()
        if '\0' in data:
            return 'binary'
        elif '\r\n' in data:
            return 'dos'
        elif '\r' in data:
            return 'mac'
        elif '\n' in data:
            return 'unix'
        elif len(data) == 1024:
            return 'binary'
        else:
            # small file with no line-feeds, return native
            if os.name == "nt":
                return 'dos'
            else:
                return 'unix'
    except IOError:
        return 'unknown'

def performmerge(base, local, other, noninteractive, interactive, ext_filter):
    """ Perform merge.

    @param base: Base version.
    @param local: Local version.
    @param other: Other version.
    @param noninteractive: Non-interactive merge tool instance
    @param interactive: Interactive merge tool instance
    @param ext_filter: Boolean indicating file extension based tool selection
    @return: Success?
    @raise MergeFailed: Merge tool failed for unknown reason.
    """
    def describe(fname, type):
        '''Describe a file to the user'''
        if type in ('dos', 'unix', 'mac'):
            return 'is a %s style text file browsable here\n%s' % (type, fname)
        elif type == 'symlink':
            if os.path.islink(fname):
                link = os.readlink(fname)
            else: # This is a tempfile holding the symlink contents
                link = file(fname).read()
            return 'is a symlink to ' + link
        elif type == 'binary':
            return 'is a binary file browsable here:\n' + fname
        else:
            return 'is an unknown file type browsable here:\n' + fname

    def selection_dialog(title, msg):
        '''Present GTK dialog for user to select local or other revision'''
        dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL,
                type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO)
        dialog.set_title(title)
        sm = 'Select No to keep local version, Yes to take other version'
        dialog.set_markup('<big><b>' + msg + '</b></big>' + sm)
        response = dialog.run()
        dialog.destroy()
        if response == gtk.RESPONSE_YES:
            return 'o'
        else:
            return 'l'

    def handle_unmergable(local, base, other):
        '''
        Try to deal with unmergable file types by asking the user
        to choose between keeping the local version or the other
        version.  Returns True if a file was selected, or False if the
        three files are safe for merging.
        '''
        mergetypes = ('dos', 'unix', 'mac')

        if os.environ.get('HG_MY_ISLINK') == '1':
            localtype = 'symlink'
            mergable = False
        else:
            localtype = eoltype(local)
            mergable = localtype in mergetypes

        if os.environ.get('HG_OTHER_ISLINK') == '1':
            othertype = 'symlink'
            mergable = False
        else:
            othertype = eoltype(other)
            mergable = mergable and othertype in mergetypes

        if os.environ.get('HG_BASE_ISLINK') == '1':
            basetype = 'symlink'
            mergable = False
        else:
            basetype = eoltype(base)
            mergable = mergable and basetype in mergetypes

        if mergable:
            return False
        else:
            title = os.environ.get('HG_FILE') + ' is not mergable, take other?'
            msg = 'local ' + describe(local, localtype) + '\n\n'
            msg += 'other ' + describe(other, othertype) + '\n\n'
            res = None
            if has_gtk:
                res = selection_dialog(title, msg)
            else:
                print title
                print msg
                while res not in ('l', 'o'):
                    res = raw_input('Choose to keep (l)ocal or (o)ther: ')

            if res == 'o':
                # Replace local file with 'other' file.
                os.unlink(local)
                if os.path.islink(other):
                    os.symlink(os.readlink(other), local)
                elif os.environ.get('HG_OTHER_ISLINK') == '1':
                    if hasattr(os, 'symlink'):
                        link = file(other).read()
                        os.symlink(link, local)
                    else: # lost information
                        shutil.copy2(other, local)
                else:
                    shutil.copy2(other, local)
        return True


    def run_merge(tool, backup, raise_=True):
        '''Run the specified merge tool wrapper'''
        savestderr = sys.stderr
        try:
            sys.stderr = StringIO.StringIO()
            try:
                r = tool.run(base, backup, other, local)
            except Exception, exc:
                print 'Caught exception:', exc
                return False
            if r == 0:
                return True
            if r == 1:
                return False
            if raise_:
                raise MergeFailed(r, sys.stderr.read())
            else:
                return False
        finally:
            sys.stderr = savestderr

    if not ext_filter and handle_unmergable(local, base, other):
        return True

    backup = local + ".orig" + ".%d" % random.randint(0, 1000)
    changetest = local + ".chg" + ".%d" % random.randint(0, 1000)

    try:
        os.rename(local, backup)

        try:
            # Try non-interactive merge
            if noninteractive is not None:
                shutil.copy2(backup, local)
                if run_merge(noninteractive, backup, raise_=False):
                    return True

            # Fall back to non-interactive merge
            shutil.copy2(backup, local)
            shutil.copy2(backup, changetest)
            if not run_merge(interactive, backup):
                return False
        except:
            # Recover local from backup
            try: os.remove(local)
            except OSError: pass
            os.rename(backup, local)
            raise

        # Compare local against changetest
        if not isinstance(interactive, TakeMine) and filecmp.cmp(local, changetest):
            if has_gtk:
                dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL,
                        type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO)
                dialog.set_title(os.environ.get('HG_FILE') + ' is unchanged')
                dialog.set_markup('<big><b>Was merge successful?</b></big>')
                response = dialog.run()
                dialog.destroy()
                if response != gtk.RESPONSE_YES:
                    return False
            else:
                print os.environ.get('HG_FILE'), 'seems unchanged.'
                res = None
                while res not in ('y', 'n'):
                    res = raw_input("Was the merge successful? [y/n] ")
                if res == 'n':
                    return False
    finally:
        # Cleanup
        try: os.remove(backup)
        except EnvironmentError: pass
        try: os.remove(changetest)
        except EnvironmentError: pass

    return True


def checkconflicts(file):
    '''Look for conflict markers.  Return True if conflicts are found'''
    conflicts = re.compile("^(<<<<<<< .*|=======|>>>>>>> .*)$", re.M)
    f = open(file)
    data = f.read()
    f.close()
    if conflicts.search(data) is None:
        return False
    else:
        return True

def changefileeol(file, old, new):
    '''Convert all EOL markers in a text file'''
    data = open(file, "rb").read()
    newdata = re.sub(old, new, data)
    if newdata != data:
        f = open(file, "wb")
        f.write(newdata)
        f.close()

def cleanupeol(base, output):
    '''
    Some merge tools have problems with implicit EOL conversion
    during the merge.  Those tools' run wrappers should call this
    function to fixup the output EOL format back to the base format
    after the merge.
    '''
    basetype = eoltype(base)
    outputtype = eoltype(output)
    if basetype == outputtype:
        return
    eol = { 'dos': '\r\n', 'unix': '\n', 'mac': '\r' }
    try:
        changefileeol(output, eol[outputtype], eol[basetype])
        print "Fixup line format of", output, outputtype, "->", basetype
    except KeyError:
        print "Unable to fixup", output, outputtype, "->", basetype


#
#
#  MergeTool base class
#
#

class MergeTool:
    def __init__(self):
        self.path = None

    def run(self, base, local, other, output):
        '''
        base   - base file, extracted in a temp dir
        local  - backup copy of local file
        other  - other file, extracted in a temp dir
        output - merge output file inside the repo

        Returns 0 - success, 1 - failure (conflicts), else 2

        The run() method presumes self.path was initialized by
        calling the detect() method.
        '''
        print >>sys.stderr, "Should not run MergeTool.run()"
        return 2

    def detect(self, name):
        '''
        The base implementation searches for the program in the
        system path, sets self.path if found.  Returns True or False
        '''
        path = os.environ["PATH"].split(os.pathsep)
        for d in path:
            if os.name == "nt":
                pathexts = os.environ["PATHEXT"].split(os.pathsep)
                for ext in pathexts:
                    exepath = os.path.join(d, name + ext)
                    if os.access(exepath, os.X_OK):
                        self.path = exepath
                        return True
            else:
                exepath = os.path.join(d, name)
                if os.access(exepath, os.X_OK):
                    self.path = exepath
                    return True
        return False

#
#
# Merge tool subclasses
#
#

class Simplemerge(MergeTool):
    '''Wrapper class for simplemerge script distributed in hg/contrib'''
    def run(self, base, local, other, output):
        if self.path.endswith('simplemerge'):
            # If we found a python script, call the python interpreter
            args = [sys.executable, self.path, output, base, other]
        else:
            # Else execute it directly
            args = [self.path, output, base, other]
        p = subprocess.Popen(args)
        r = p.wait()
        if r == 0 and checkconflicts(output):
            print "Conflict markers detected in", output
            r = 1
        return r

    def detect(self):
        if hasattr(sys, "frozen"):
            # If this is a py2exe hgmerge.exe, there's no point in looking
            # for a simplemerge script.  Instead, search for simplemerge.exe
            return MergeTool.detect(self, 'simplemerge')
        # Look for executable simplemerge script
        name = 'simplemerge'
        curdir = os.path.dirname(__file__)
        for p in [os.sep.join([curdir, name]),
                os.sep.join([curdir, '..', 'contrib', name]),
                os.path.expanduser('~/bin/' + name)] + \
                glob.glob('/usr/share/mercurial*/simplemerge'):
            if os.access(p, os.X_OK):
                self.path = p
                return True
        return MergeTool.detect(self, 'simplemerge')



class Diff3(MergeTool):
    '''Wrapper class for (g)diff3 diff/merge tool'''
    def run(self, base, local, other, output):
        # Windows gnudiff3 has problems with filenames and whitespace
        # diff3 -m local base other > output
        cmd = '"%s" -m "%s" "%s" "%s"' % (self.path, local, base, other)
        if os.name == "nt": cmd = '"' + cmd + '"'
        output_file = file(output, "w")
        p = subprocess.Popen(cmd, stdout=output_file, shell=True)
        try: r = p.wait()
        finally: output_file.close()
        if r == 0 and checkconflicts(output):
            print "Conflict markers detected in", output
            r = 1
        return r

    def detect(self):
        return MergeTool.detect(self, 'gdiff3') or MergeTool.detect(self, 'diff3')


class TakeMine(MergeTool):
    '''Wrapper class for ignoring changes from other heads'''
    def run(self, base, local, other, output):
        print 'Taking local file, ignoring remote changes'
        shutil.copy2(local, output)
        return 0

    def detect(self):
        return True


class TakeOther(MergeTool):
    '''Wrapper class for ignoring local changes'''
    def run(self, base, local, other, output):
        print 'Taking other file, ignoring local changes'
        shutil.copy2(other, output)
        return 0

    def detect(self):
        return True

class External(MergeTool):
    '''Wrapper class for scripts and other tools without wrappers'''
    def set_command(self, cmd):
        self.cmd = cmd

    def run(self, base, local, other, output):
        cmd.replace('$base', base)
        cmd.replace('$local', local)
        cmd.replace('$other', other)
        cmd.replace('$output', output)
        return subprocess.Popen(cmd).wait()

    def detect(self):
        return True

class Merge(MergeTool):
    '''Wrapper class for merge(1) tool'''
    def run(self, base, local, other, output):
        args = [self.path, output, base, other]
        r = subprocess.Popen(args).wait()
        if r == 0 and checkconflicts(output):
            print "Conflict markers detected in", output
            r = 1
        return r

    def detect(self):
        return MergeTool.detect(self, 'merge')


class Gvimdiff(MergeTool):
    '''Wrapper class for gvimdiff file merge tool'''
    def run(self, base, local, other, output):
        args = [self.path, "--nofork", "-d", "-g", "-O" ,
                output, other, base]
        return subprocess.Popen(args).wait()

    def detect(self):
        try:
            from win32con import HKEY_LOCAL_MACHINE
            import win32api, pywintypes
            gvimkey = win32api.RegOpenKey(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vim\\Gvim')
            gvimexe = win32api.RegQueryValueEx(gvimkey, 'path')[0]
            if os.access(gvimexe, os.X_OK):
                self.path = gvimexe
                return True
        except ImportError: pass
        except pywintypes.error: pass
        return MergeTool.detect(self, 'vim')


class Gpyfm(MergeTool):
    '''Wrapper class for onnv-scm gpyfm 'GuppyFoam' PyGtk file merge tool'''
    def run(self, base, local, other, output):
        args = [self.path, output, base, other]
        r = subprocess.Popen(args).wait()
        if r == 0: cleanupeol(base, output)
        return r

    def detect(self):
        return MergeTool.detect(self, 'gpyfm')


class Meld(MergeTool):
    '''Wrapper class for meld PyGtk diff/merge tool'''
    def run(self, base, local, other, output):
        args = [self.path, local, output, other]
        return subprocess.Popen(args).wait()

    def detect(self):
        return MergeTool.detect(self, 'meld')


class TkDiff(MergeTool):
    '''Wrapper class for TclTk diff/merge tool'''
    def run(self, base, local, other, output):
        args = [self.path, local, other, '-a', base, '-o', output]
        r = subprocess.Popen(args).wait()
        if r == 0: cleanupeol(base, output)
        return r

    def detect(self):
        # Check for default install location:
        #   C:\Program Files\TkDiff\tkdiff.exe
        pf = os.environ.get("PROGRAMFILES")
        if pf:
            tkdiffpath = os.sep.join([pf, 'TkDiff', 'tkdiff.exe'])
            if os.access(tkdiffpath, os.X_OK):
                self.path = tkdiffpath
                return True
        return MergeTool.detect(self, 'tkdiff')


class XXDiff(MergeTool):
    '''Wrapper class for XXDiff merge tool'''
    def run(self, base, local, other, output):
        args = [self.path, '--show-merged-pane', '--exit-with-merge-status',
                '--title1', 'mine', '--title2', 'ancestor', '--title3', 'theirs',
                '--merged-filename', output, '--merge', local, base, other]
        r = subprocess.Popen(args).wait()
        if r == 0: cleanupeol(base, output)
        return r

    def detect(self):
        return MergeTool.detect(self, 'xxdiff')


class Guiffy(MergeTool):
    '''Wrapper class for Guiffy Software diff/merge tool'''
    def run(self, base, local, other, output):
        # Guiffy complains when output file already exists, so we remove it.
        os.remove(output)
        args = [self.path, '-s', '-eauto', '-h1mine',
                '-h2theirs', local, other, base, output]
        return subprocess.Popen(args).wait()

    def detect(self):
        return MergeTool.detect(self, 'guiffy')


class DiffMerge(MergeTool):
    '''Wrapper class for SourceGear DiffMerge tool'''
    def run(self, base, local, other, output):
        args = [self.path, '--nosplash', '--merge', '--caption="Mercurial Merge"',
                '--title1=Base', '--title2=Mine', '--title3=Theirs',
                base, output, other]
        return subprocess.Popen(args).wait()

    def detect(self):
        # Check for default install location:
        #   C:\Program Files\SourceGear\DiffMerge\DiffMerge.exe
        pf = os.environ.get("PROGRAMFILES")
        if pf:
            dmpath = os.sep.join([pf, 'SourceGear', 'DiffMerge', 'DiffMerge.exe'])
            if os.access(dmpath, os.X_OK):
                self.path = dmpath
                return True
        # Fall-back to path search
        return MergeTool.detect(self, 'diffmerge')


class Kdiff3(MergeTool):
    '''Wrapper class for Kdiff3 Qt diff/merge tool'''
    def run(self, base, local, other, output):
        # Force kdiff3 to output merge file in same eol style as base file
        ftype = eoltype(base)
        if ftype == 'unix':
            typeargs = ['--cs', 'LineEndStyle=0']
        elif ftype == 'dos':
            typeargs = ['--cs', 'LineEndStyle=1']
        else:
            print "file type '%s' cannot be merged by kdiff3" % ftype
            return 1
        args = [self.path, "--auto", "-L1", "Base", "-L2", "Local", "-L3", "Other",
                base, local, other, "-o", output] + typeargs
        return subprocess.Popen(args).wait()

    def detect(self):
        try:
            from win32con import HKEY_CURRENT_USER
            import win32api, pywintypes
            kdiffpath = win32api.RegQueryValue(HKEY_CURRENT_USER, 'SOFTWARE\\KDiff3')
            kdiffexepath = kdiffpath + os.sep + "kdiff3.exe"
            if os.access(kdiffexepath, os.X_OK):
                self.path = kdiffexepath
                return True
        except ImportError: pass
        except pywintypes.error: pass
        return MergeTool.detect(self, 'kdiff3')


class P4Merge(MergeTool):
    '''Wrapper class for Perforce P4Merge tool'''
    def run(self, base, local, other, output):
        args = [self.path, base, local, other, output]
        r = subprocess.Popen(args).wait()
        if r == 0: cleanupeol(base, output)
        return r

    def detect(self):
        try:
            from win32con import HKEY_LOCAL_MACHINE
            import win32api, pywintypes
            p4key = win32api.RegOpenKey(HKEY_LOCAL_MACHINE,
                    'SOFTWARE\\Perforce\\Environment')
            root = win32api.RegQueryValueEx(p4key, 'P4INSTROOT')[0]
            exepath = root + os.sep + "p4merge.exe"
            if os.access(exepath, os.X_OK):
                self.path = exepath
                return True
        except ImportError: pass
        except pywintypes.error: pass
        return MergeTool.detect(self, 'p4merge')


class TortoiseMerge(MergeTool):
    '''Wrapper class for TortoiseSVN's TortoiseMerge tool'''
    def run(self, base, local, other, output):
        cmd = '"%s" /base:"%s" /mine:"%s" /theirs:"%s" /merged:"%s"' % \
                (self.path, base, local, other, output)
        return subprocess.Popen(cmd).wait()

    def detect(self):
        try:
            from win32con import HKEY_LOCAL_MACHINE
            import win32api, pywintypes
            tkey = win32api.RegOpenKey(HKEY_LOCAL_MACHINE, 'SOFTWARE\\TortoiseSVN')
            tmerge = win32api.RegQueryValueEx(tkey, 'TMergePath')[0]
            if os.access(tmerge, os.X_OK):
                self.path = tmerge
                return True
        except ImportError: pass
        except pywintypes.error: pass
        return MergeTool.detect(self, 'tortoisemerge')


class ECMerge(MergeTool):
    '''Wrapper class for Ellie Computing ECMerge tool'''
    def run(self, base, local, other, output):
        args = [self.path, base, local, other, '--mode=merge3', '--title0=base',
                '--title1=mine', '--title2=theirs', '--to=%s' % output,
                '--to-title=merged']
        return subprocess.Popen(args).wait()

    def detect(self):
        # Try registry first
        try:
            from win32con import HKEY_LOCAL_MACHINE
            import win32api, pywintypes
            tkey = win32api.RegOpenKey(HKEY_LOCAL_MACHINE,
                    'SOFTWARE\Ellié Computing\Merge')
            ecmerge = win32api.RegQueryValueEx(tkey, 'Path')[0]
            if os.access(ecmerge, os.X_OK):
                self.path = ecmerge
                return True
        except ImportError: pass
        except pywintypes.error: pass
        # Check for default install location:
        #   C:\Program Files\Ellié Computing\Merge\guimerge.exe
        pf = os.environ.get("PROGRAMFILES")
        if pf:
            ecmerge = os.sep.join([pf, 'Ellié Computing', 'Merge', 'guimerge.exe'])
            if os.access(ecmerge, os.X_OK):
                self.path = ecmerge
                return True
        # Fall-back to path search
        return MergeTool.detect(self, 'guimerge')


class FileMerge(MergeTool):
    '''Wrapper class for MacOS FileMerge.app tool'''
    def run(self, base, local, other, output):
        args = [self.path, '-left', other, '-right', local, '-ancestor', base,
                '-merge', output]
        r = subprocess.Popen(args).wait()
        if r != 0:
            print "FileMerge failed to launch"
            return 2
        return r

    def detect(self):
        fmpath = '/Developer/Applications/Utilities/FileMerge.app/Contents/MacOS/FileMerge'
        if os.access(fmpath, os.X_OK):
            self.path = fmpath
            return True
        return MergeTool.detect(self, 'FileMerge')


noninteractive_tools = [ ('simplemerge', Simplemerge()),
        ('merge', Merge()),
        ('diff3', Diff3()) ]

interactive_tools = [ ('gpyfm', Gpyfm()),
        ('kdiff3', Kdiff3()),
        ('tortoisemerge', TortoiseMerge()),
        ('p4merge', P4Merge()),
        ('meld', Meld()),
        ('tkdiff', TkDiff()),
        ('filemerge', FileMerge()),
        ('ecmerge', ECMerge()),
        ('xxdiff', XXDiff()),
        ('guiffy', Guiffy()),
        ('diffmerge', DiffMerge()),
        # editors have to come last, so dedicated merge tools take precedence
        ('vim', Gvimdiff())
        ]

#
#
#  Main
#
#
if __name__ == "__main__":
    if len(sys.argv) == 2 and sys.argv[1] == '--autodetect':
        autodetection()
        sys.exit(0)
    if len(sys.argv) == 2 and sys.argv[1] == '--version':
        print __version__
        sys.exit(0)
    try: local, base, other = sys.argv[1:4]
    except ValueError:
        sys.exit("Usage: local base other")

    # Find tools based on environment, extension, default, or search
    tools = get_tools(os.path.splitext(local)[-1])
    if tools is None:
        sys.stderr.write("Couldn't find any suitable merge tools\n")
        sys.exit(2)
    else:
        noninter, inter, ext_filter = tools

    # Perform merge
    if not performmerge(base, local, other, noninter, inter, ext_filter):
        sys.exit(1)

    sys.exit(0)
