[PATCH 1 of 4] hgmerge: add new hgmerge package under mercurial

Steve Borho steve at borho.org
Mon Jan 7 15:20:34 CST 2008


# HG changeset patch
# User Steve Borho <steve at borho.org>
# Date 1199739435 21600
# Node ID 1dabe5c4867c4f1d855494ecd0f76d6436882c6e
# Parent  3ef279074c77c3cf3f6b35f0f73dee2fdba5aa41
hgmerge: add new hgmerge package under mercurial

diff --git a/mercurial/hgmerge/README.txt b/mercurial/hgmerge/README.txt
new file mode 100644
--- /dev/null
+++ b/mercurial/hgmerge/README.txt
@@ -0,0 +1,96 @@
+Cross-platform merging for Mercurial.
+
+The purpose of hgmerge is to provide a file revision merging interface, with
+good default behavior and user configurability.  It is the default merge
+behavior for Mercurial when HGMERGE is not set in your environmant and ui.merge
+is not set in your hgrc file(s).
+
+When unconfigured, hgmerge will attempt to perform a 3-way merge using an
+included simplemerge script that runs without user interaction.  If no conflicts
+are found, the merge is successful.  When conflicts are found, hgmerge will
+search for interactive 3-way merge tools on your computer and use the first one
+it finds.  If no tools are detected (or if requested tool is not found), the
+partially merged file with conflict markers is left in the working directory to
+be resolved manually.  Note that Mercurial does not track 'conflict' file
+status, so users have to be diligent to not check in partially merged files.
+
+The 'hg debuginstall' command will report the list of plug-ins that are detected
+on your machine.
+
+By adding entries in their hgrc files, users can:
+* completely override the search by specifying a default plug-in
+* specify plugins to use for specific file extensions
+* modify the built-in plug-ins (override characteristics)
+* define search precedence for plug-ins
+* define entirely new plug-ins.
+
+Hgmerge defines two new hgrc sections.  [hgmerge] is used to define default and
+file extension based plug-ins.  [hgmerge-plugins] is used to configure built-in
+plugins and to define entirely new plugins.
+
+Example hgmerge section:
+[hgmerge]
+default = kdiff3
+ext.png = mypngmerge
+ext.lib = takemine
+
+When a file extension plug-in is specified (e.g., .png), hgmerge will bypass the
+initial simplemerge step and directly call the specified plug-in.  This is
+useful for e.g. binary formats that cannot be merged as text.  There are two
+special plug-ins intended for file extension use: 'takemine' and 'takeother'
+(with predictable behaviors).  These two will not show up in the list of
+installed plug-ins but are always available.
+
+Note that unless the plug-in was selected by a file extension match, hgmerge
+will specially handle file types which are unmergeable by most merge tools
+(symlinks, binary files, etc).  Unmergeable files bypass the entire simplemerge
+and plugin architecture and instead the user will be asked to chose between the
+'local' and 'other' versions of the file.
+
+Plug-In Configuration
+=====================
+Merge plug-ins can be configured through Mercurial's configuration system
+(hgrc). One may modify existing plug-ins or define new ones. Plug-in
+configurations exist in the section [hgmerge-plugins]. 
+
+Plug-ins can be of one of two types: tool and custom. The default type is tool.
+
+Tool Plug-Ins
+-------------
+A tool definition represents an external program and parameters for running
+that, such as its arguments. The argument line can contain variables, the
+following are supported: $base, $local, $other, $output.
+
+The following options can be set for a tool:
+	executable: Either just the name of the executable or its pathname. Per
+	default the same as the plug-in's name.
+	arguments: The arguments to pass to the tool (default: $base $local $other
+	$output)
+	priority: The priority in which to evaluate this plug-in.
+	stdout: Should the tool's standard output be used as the merge result?
+	check_conflicts: Check whether there are conflicts even though the tool
+	reported none?
+	win.regpath_installdir: Specify a pathname in the Windows registry defining
+	the tool's installation directory. The format of this option is like this:
+	<key name>\<value name>. If the key itself actually holds the value, end
+	the pathname with a backslash, so it's clear there is no value name
+	component.
+	win.regpath_installpath: Like the former, except that the registry value
+	is taken to specify the installation path of the tool's executable.
+	
+Example tool configuration:
+[hgmerge-plugins]
+gvimdiff.type = tool
+gvimdiff.arguments = --nofork -d -g -O $output $other $base
+gvimdiff.priority = 1
+gvimdiff.win.regpath_installpath = Software\Vim\GVim\path
+kdiff3.executable = ~/bin/kdiff3   # override built in plug-in value
+
+Note that the plugin type defaults to tool and can be left unspecified, and the
+priority defaults to 0 (higher priority tools are detected first).
+
+Custom Plug-Ins
+---------------
+A "custom" plug-in is defined by a Python class. TODO ...
+
+# vim: textwidth=80
diff --git a/mercurial/hgmerge/TODO.txt b/mercurial/hgmerge/TODO.txt
new file mode 100644
--- /dev/null
+++ b/mercurial/hgmerge/TODO.txt
@@ -0,0 +1,5 @@
+* Allow user to turn off automatic invocation of merge tool
+* Port tests to standard Mercurial system
+
+MacOS         - needs wrappers for more OSX tools
+              - needs testing
diff --git a/mercurial/hgmerge/__init__.py b/mercurial/hgmerge/__init__.py
new file mode 100644
--- /dev/null
+++ b/mercurial/hgmerge/__init__.py
@@ -0,0 +1,544 @@
+# -*- coding: utf-8 -*-
+""" Logic for merging changes in two versions of a file.
+ at var stockplugins: Sequence of stock plug-in representations.
+"""
+import shutil
+import StringIO
+import filecmp
+import random
+import traceback
+    
+from mercurial.hgmerge._common import *
+import mercurial.util as hgutil
+import _simplemerge
+import _plugins as hgmergeplugs
+import _pluginapi as hgpluginapi
+
+stockplugins = hgmergeplugs.plugins
+
+# Initially not defined
+plugins = None
+
+class filedesc(object):
+    ''' Describe properties of a file.
+    @ivar name: The file's name.
+    @ivar islink: Is the file a symlink?
+    '''
+    def __init__(self, name, islink):
+        self.name, self.islink = (name, islink)
+        self._eoltp = None
+        
+    def __str__(self):
+        return self.name
+    
+    def __eq__(self, rhs):
+        return rhs.name == self.name
+    
+    def _eoltype(self):
+        ''' The EOL type of the file.
+        '''
+        if self._eoltp is None:
+            if self.islink:
+                self._eoltp = 'symlink'
+            else:
+                self._eoltp = eoltype(self.name)
+        return self._eoltp
+    eoltype = property(_eoltype)
+    
+    def _linkdest(self):
+        ''' The symlink destination.
+        '''
+        assert self.islink
+        if os.path.islink(self.name):
+            link = os.readlink(self.name)
+        else:
+            # This is a tempfile holding the symlink contents
+            link = _readfile(self.name)
+        return link
+    linkdest = property(_linkdest)
+
+class _optexception(Exception):
+    pass
+
+class _invalidoptval(_optexception):
+    ''' Invalid value for option.
+    '''
+    def __init__(self, option, val):
+        _optexception.__init__(self, 'Invalid value for %s: %s' % (option, val))
+        self.option, self.value = option, val
+        
+class _missingopt(_optexception):
+    ''' A required option is missing.
+    '''
+    def __init__(self, option):
+        _optexception.__init__(self, 'Option %s is missing' % option)
+        self.option = option
+
+class _pluginopts(object):
+    ''' Plug-in options handler baseclass.
+    '''
+    def __init__(self, ui, sectname, name):
+        self.attrs = {}
+        self.__ui, self.__sect, self.__name = ui, sectname, name
+        self._set_int('priority', default=0)
+        
+    def _set_string(self, prop, **kwds):
+        self._set_opt(prop, self.__ui.config, **kwds)
+        
+    def _set_bool(self, prop, **kwds):
+        self._set_opt(prop, self.__ui.configbool, **kwds)
+        
+    def _set_list(self, prop, **kwds):
+        self._set_opt(prop, self._configlist, **kwds)
+        
+    def _set_int(self, prop, **kwds):
+        self._set_opt(prop, self._configint, **kwds)
+        
+    def _set_opt(self, prop, getfunc, attr=None, default=None):
+        try:
+            val = getfunc(self.__sect, '%s.%s' % (self.__name, prop),
+                default=default)
+        except ValueError:
+            if getfunc is self.__ui.config:
+                raise
+            # Conversion error
+            val = None
+            raise _invalidoptval(prop, val)
+            self.__warn('Invalid value for option %s\n' % prop)
+        if val is None:
+            return
+        
+        if attr is None:
+            attr = prop.replace('.', '_')
+        self.attrs[attr] = val
+        
+    def _configint(self, sect, field, default=None):
+        val = self.__ui.config(sect, field, default=default)
+        if val is not None:
+            return int(val)
+        
+    def _configlist(self, sect, field, default=None):
+        ''' Replace ui.configlist since it doesn't seem to allow elements with
+        whitespace.
+        '''
+        val = self.__ui.config(sect, field, default=default)
+        if val is not None:
+            import shlex
+            return shlex.split(val)
+
+class _toolopts(_pluginopts):
+    ''' Tool plug-in options handler.
+    '''
+    def __init__(self, ui, sectname, name):
+        _pluginopts.__init__(self, ui, sectname, name)
+        
+        self.type = 'tool'
+        self._set_string('executable')
+        self._set_list('arguments')
+        self._set_string('win.regpath_installdir')
+        self._set_string('win.regpath_installpath')
+        for prop in ('stdout', 'check_conflicts'):
+            self._set_bool(prop)
+
+class _customopts(_pluginopts):
+    ''' Custom plug-in options handler.
+    '''
+    def __init__(self, ui, sectname, name):
+        _pluginopts.__init__(self, ui, sectname, name)
+        self.type = 'custom'
+
+def _create_tool(name, opts, prevplug, ui):
+    kwds = {}
+    return hgpluginapi.toolplugin(name, **opts.attrs)
+    
+def _create_custom(name, opts, prevplug, ui):
+    # Get hold of Python module and compile it
+    try: modfpath = conf['path']
+    except KeyError:
+        ui.warn('Missing conf parameter: %s\n' % 'path')
+        return None
+
+def _create_plug(type_, name, opts, prevplug, ui):
+    def warn_invalid(reason):
+        ui.warn('Invalid plug-in definition: %s\n%s\n' % (name, reason))
+    
+    if type_ == 'tool':
+        plug = _create_tool(name, opts, prevplug, ui)
+        assert plug is not None
+    elif type_ == 'custom':
+        plug = _create_custom(name, opts, prevplug, ui)
+        assert plug is not None
+    else:
+        warn_invalid('Invalid type: %s' % type_)
+        # raise AssertionError(type_)
+        return None
+    if plug is None:
+        return plug
+    
+    return plug
+
+def _modify_plug(plug, opts, ui):
+    plug.set_options(opts, ui)
+
+def _setup_plugs(ui):
+    def handle_conf(name, plugs):
+        for p in plugs:
+            if p.name.lower() == name.lower():
+                plug = p
+                break
+        else:
+            plug = None
+            
+        tp = ui.config(sectname, '%s.type' % (name,))
+        if tp is None:
+            if plug is not None:
+                tp = plug.type
+            else:
+                tp = 'tool'
+        if tp == 'tool':
+            optscls = _toolopts
+        elif tp == 'custom':
+            optscls = _customopts
+        else:
+            ui.warn('Couldn\'t use %s\'s configuration, bad type: %s\n'
+                % (name, tp))
+            return
+        
+        try: opts = optscls(ui, sectname, name)
+        except _invalidoptval, err:
+            # Invalid value for a required option
+            ui.warn('Couldn\'t use %s\'s configuration, bad value for %s: %s\n'
+                % (name, err.option, err.value))
+            return
+        except _missingopt, err:
+            ui.warn('Couldn\'t use %s\'s configuration, missing option %s\n' %
+                (name, err.option))
+            return
+        
+        # If this is a new plug-in, define a new plug-in, otherwise, apply to
+        # existing
+        
+        tp = opts.type
+        if plug is None or (tp is not None and tp != plug.type):
+            # Define new plug
+            newplug = _create_plug(tp, name, opts, plug, ui)
+            if newplug is not None:
+                plugs.append(newplug)
+            assert plugs[-1] is not None
+        else:
+            # Modify existing plug
+            _modify_plug(plug, opts, ui)
+    
+    sectname = 'hgmerge-plugins'
+    
+    # Read plug-in configurations in the order they are encountered
+    
+    global plugins
+    plugins = list(hgmergeplugs.plugins)
+    
+    plugconfs = []
+    for field, val in ui.configitems(sectname):
+        try: name = field.split('.', 1)[0]
+        except ValueError:
+            name = field
+        if name in plugconfs:
+            # This was already handled
+            continue
+        plugconfs.append(name)
+        
+        handle_conf(name, plugins)
+        
+    # Sort the plug-ins based on priority
+    def getkey(plug):
+        return plug.priority
+    plugins.sort(key=getkey, reverse=True)
+
+def _get_requested(ui, fname):
+    ''' Get plug-in requested by user, through available configuration
+    mechanisms.
+    
+    None is returned if no plug-in is specified or it can't be detected. The
+    second return value indicates whether the user has chosen this plug-in
+    for the extension of the file being merged.
+    @return: plug-in, extfilter (boolean).
+    '''
+    plug = None
+    extfilter = False
+    config = dict(ui.configitems('hgmerge'))
+    requested = None
+
+    # Then there may be a plug-in defined for this file extension
+    if fname is not None:
+        ext = os.path.splitext(fname)[1]
+        key = 'ext%s' % ext
+        if config.has_key(key):
+            requested = config[key]
+            extfilter = True
+
+    if requested:
+        plug = requested
+    else:
+        # Fall back to generic plug-in
+        try:
+            plug = config['default']
+            requested = True
+        except KeyError:
+            pass
+        
+    if not requested:
+        return (None, extfilter)
+
+    # Evaluate requested tools (see if they are available etc.)
+
+    if plug == 'takemine':
+        plugin = hgmergeplugs.takemine()
+    elif plug == 'takeother':
+        plugin = hgmergeplugs.takeother()
+    for plugin in plugins:
+        if plugin.name == plug and plugin.detect(ui):
+            break
+    else:
+        plugin = None
+        ui.warn('Could not find requested plug-in: %s\n' % plug)
+            
+    return (plugin, extfilter)
+
+class simplemerge(hgpluginapi.customplugin):
+    def __init__(self):
+        hgpluginapi.customplugin.__init__(self, 'simplemerge')
+        
+    def merge(self, base, local, other, output, ui):
+        # Simplemerge writes directly to the local version, so provide
+        # the output file
+        r = _simplemerge.simplemerge(base, local, other, output, ui,
+            quiet=True)
+        return r
+
+def get_plugin(repo, fname=None):
+    ''' Get interactive merge plug-in (alternative to simplemerge).
+    
+    @param repo: Mercurial repository.
+    @param fname: Name of file being merged.
+    @return: Plug-in, requested. If the plug-in can't be detected, it will be
+    None. The I{requested} return value indicates whether the user requested a
+    specific plug-in.
+    
+    @note: The user may, via configuration, specify the plug-in to use. If the
+    requested plug-in isn't found, None is returned. User specifications are
+    ordered like so: filetype (extension), global.
+    '''
+    ui = repo.ui
+    
+    global plugins
+    if plugins is None:
+        _setup_plugs(ui)
+        
+    inter, extfilter = _get_requested(ui, fname)
+    if extfilter or inter is not None:
+        # A special plug-in was requested
+        return (inter, extfilter)
+    
+    for plug in plugins:
+        if plug.detect(ui):
+            ui.debug('Auto-detected %s interactive merge tool' % plug.name)
+            inter = plug
+            break
+    
+    return (inter, False)
+
+def query_plugins(ui, rescan=False):
+    ''' Get available plug-ins.
+    
+    @param rescan: Force re-scanning of plug-in definitions?
+    '''
+    global plugins
+    if plugins is None or rescan:
+        _setup_plugs(ui)
+    plugs = []
+    for plug in plugins:
+        if plug.detect(ui):
+            plugs.append(plug)
+    return plugs
+
+def _is_mergeable(base, local, other):
+    ''' Are the files mergeable?
+    @return: Success?
+    '''
+    mergetypes = ('dos', 'unix', 'mac')
+
+    for eoltp in (local.eoltype, other.eoltype, base.eoltype):
+        if eoltp not in mergetypes:
+            return False
+        
+    return True
+
+def _readfile(fname):
+    f = file(fname, 'rb')
+    try: data = f.read()
+    finally: f.close()
+    return data
+
+def _ask_unmergeable(ui, local, other, output):
+    ''' File is not mergeable, ask user which version to keep.
+    @return: Success?
+    '''
+    def _describe(fdesc):
+        '''Describe a file to the user'''
+        if not fdesc.islink:
+            eoltp = fdesc.eoltype
+            if eoltp in ('dos', 'unix', 'mac'):
+                return 'is a %s style text file browsable here\n%s' % (
+                    eoltp, fdesc.name)
+            elif eoltp == 'binary':
+                return 'is a binary file browsable here:\n' + fdesc.name
+            else:
+                return 'is an unknown file type browsable here:\n' + fdesc.name
+        else:
+            link = fdesc.linkdest
+            return 'is a symlink to ' + link
+
+    prompt = '%s is not mergable.\n' % output.name
+    prompt += 'local %s\n\n' % _describe(local)
+    prompt += 'other %s\n\n' % _describe(other)
+    prompt += '(k)eep local or take (o)ther?'
+    rslt = ui.prompt(prompt, '[ko]', 'k')
+    if rslt  == 'o':
+        # Replace local file with 'other' file.
+
+        os.remove(local.name)
+        if other.islink and hasattr(os, 'symlink'):
+            os.symlink(other.linkdest, local.name)
+        else:
+            shutil.copy2(other.name, local.name)
+    
+    return True
+
+def merge(repo, local, base, other, pluginspec=None):
+    ''' Perform a three-way file merge.
+
+    We first try to merge the changes automatically, without user interaction,
+    using simplemerge. If there are merge conflicts however, we invoke a merge
+    plug-in automatically, which typically launches a graphical tool for the
+    user to interact with.
+    
+    We have an ordered set of stock plug-ins to choose from, but the user can
+    assign these priority or configure them in other ways, or even add new
+    plug-ins. This configuration happens in hgrc.
+
+    Note that the file arguments must be passed as L{filedesc} instances.
+    @param repo: Mercurial repository.
+    @param base: Base file version.
+    @param local: Local file version.
+    @param other: Other file version.
+    @param pluginspec: Optionally specify plug-in to use. Same as returned by
+    get_plugin. 
+    @return: 0 - success, 1 - conflict, 2 - error.
+    '''
+    def run_merge(plugin, backup):
+        ''' Run the specified merge plug-in. '''
+        ui = repo.ui
+        mergeargs = (base.name, backup, other.name, local.name, ui)
+        try:
+            try:
+                if not plugin.premerge(*mergeargs):
+                    ui.debug('Plug-in %s premerge failed' % plugin.name)
+                else:
+                    r = plugin.merge(*mergeargs)
+                    if r in(0, 1):
+                        # Allow cleaning up for merges that didn't explicitly
+                        # fail
+                        plugin.postmerge(*mergeargs)
+            except Exception:
+                msg = traceback.format_exc()
+                ui.warn('Caught exception:\n%s\n' % msg)
+                return 2
+            return r
+        finally:
+            pass
+        
+    ui = repo.ui
+
+    if pluginspec is None:
+        pluginspec = get_plugin(repo, local.name)
+    interactive, extfilter = pluginspec
+
+    if isinstance(interactive, hgmergeplugs.takemine):
+        # Just keep local version
+        return 0
+    
+    if extfilter and interactive is None:
+        # The user requested a certain plug-in for this filetype, so don't
+        # use simplemerge
+        return 2
+
+    if not extfilter and not _is_mergeable(base, local, other):
+        if _ask_unmergeable(ui, local, other, local):
+            return 0
+        else:
+            return 2
+
+    # The original file is moved to a backup before merging, only once we've
+    # determined a successful merge is the backup removed
+    
+    backup = '%s.orig.%d' % (local.name, random.randint(0, 1000))
+    hgutil.rename(local.name, backup)
+    # Create an empty output file with the exact same permissions as the
+    # original
+    file(local.name, 'w').close()
+    shutil.copystat(backup, local.name)
+    # Also make a copy for reference
+    changetest = '%s.chg.%d' % (local.name, random.randint(0, 1000))
+    shutil.copy2(backup, changetest)
+
+    try:
+        try:
+            r = 1   # For fall-back to interactive
+            onlysimple = True
+            if not extfilter:
+                # Try non-interactive merge
+                r = run_merge(simplemerge(), backup)
+                
+            if r == 1 and interactive is not None:
+                # Fall back to interactive merge
+                
+                # Make the output mirror the original local copy, in case
+                # its contents matter to the plug-in
+                shutil.copy2(backup, local.name)
+                onlysimple = False
+                r = run_merge(interactive, backup)
+        except Exception:
+            r = 2
+            
+        if r == 0 or (r == 1 and onlysimple):
+            # The backup isn't needed
+            ui.debug('Removing backup\n')
+            try: os.remove(backup)
+            except EnvironmentError: pass
+            
+        if r != 0:
+            if r not in (1, 2):
+                ui.debug('Got unexpected return value: %r\n' % (r,))
+                r = 2
+                
+            if not onlysimple or r != 1:
+                ui.warn('Merging failed (error code %s). The original file has '
+                    'been backed up to %s.\n' % (r, backup))
+            elif onlysimple:
+                ui.warn('Merging finished with conflicts\n.')
+            return r
+
+        # Compare local against changetest
+        if filecmp.cmp(local.name, changetest):
+            ui.write('%s seems unchanged.\n' % local.name)
+            if repo.ui.prompt("Was the merge successful? [y/n]",
+                    "[yn]", 'n') == 'y':
+                return 0
+            else:
+                return 1
+    finally:
+        # Cleanup
+        
+        try: os.remove(changetest)
+        except EnvironmentError: pass
+
+    return r
diff --git a/mercurial/hgmerge/_common.py b/mercurial/hgmerge/_common.py
new file mode 100644
--- /dev/null
+++ b/mercurial/hgmerge/_common.py
@@ -0,0 +1,36 @@
+''' Common utilities.
+'''
+import os.path
+import stat
+    
+def eoltype(name):
+    ''' Get the EOL type for a file.
+    '''
+    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'
diff --git a/mercurial/hgmerge/_pluginapi.py b/mercurial/hgmerge/_pluginapi.py
new file mode 100644
--- /dev/null
+++ b/mercurial/hgmerge/_pluginapi.py
@@ -0,0 +1,319 @@
+''' API for merge plug-ins.
+'''
+import re
+import os.path
+
+import mercurial.util as hgutil
+from mercurial.hgmerge._common import eoltype
+    
+class _plugin(object):
+    ''' The baseclass of all plug-ins.
+    
+    @ivar name: Name of plug-in.
+    @ivar type: Plug-in type, either 'tool' or 'custom'.
+    @ivar priority: Plug-in priority.
+    '''
+    def __init__(self, name, type_, priority=0):
+        self.name, self.type = (name, type_)
+        self.priority = priority
+        
+    def set_options(self, options, ui):
+        ''' Set options.
+        '''
+        for attr, val in options.attrs.items():
+            setattr(self, attr, val)
+        
+    def premerge(self, base, local, other, output, ui):
+        ''' Prepare for merge.
+        
+        Default do-nothing implementation.
+        @param base: Base file.
+        @param local: Local file.
+        @param other: Other file.
+        @param output: File for merge output.
+        @param ui: Mercurial UI.
+        @return: Can the files be merged by this tool?
+        '''
+        return True
+            
+    def postmerge(self, base, local, other, output, ui):
+        ''' Invoke after merging.
+        
+        Default do-nothing implementation.
+        @param base: Base file.
+        @param local: Local file.
+        @param other: Other file.
+        @param output: File for merge output.
+        @param ui: Mercurial UI.
+        @return: None.
+        '''
+
+class toolplugin(_plugin):
+    ''' The baseclass of plug-ins which execute a tool.
+    
+    Common to tool plug-ins is that they have a certain executable and a list
+    of arguments. The arguments may contain variables, that are expanded before
+    the tool is executed. This class allows for configuring of the executable
+    and its arguments.
+    
+    The executable may initially be specified as either None, just a tool name,
+    or a pathname. After calling premerge, it will be defined as the tool's
+    absolute pathname. The argument list may also be modified in premerge.
+    
+    The Supported Variables
+    =======================
+    $base, $local, $other, $output
+    
+    @ivar executable: The pathname of the tool's executable.
+    @ivar arguments: The argument list to the tool, may contain variables.
+    '''
+    def __init__(self, name, executable=None, arguments=None, stdout=False,
+        check_conflicts=False, win_regpath_installdir=None,
+        win_regpath_installpath=None, priority=0):
+        ''' Constructor.
+        @param name: Specify plug-in name.
+        @param executable: Specify the tool executable.
+        @param arguments: Optionally specify the tool arguments.
+        @param stdout: Direct standard out to output file?
+        @param check_conflicts: Check for standard conflict markers after merge?
+        @param win_regpath_installdir: On Windows, optionally provide registry
+        path with which to look for installation directory.
+        @param win_regpath_installpath: On Windows, optionally provide registry
+        path with which to look for the executable's installation path.
+        '''
+        _plugin.__init__(self, name, 'tool', priority=priority)
+        
+        if executable is None:
+            executable = name.lower()    # Sensible default
+        if arguments is None:
+            arguments = ['$base', '$local', '$other', '$output']
+        else:
+            arguments = list(arguments)
+        self.executable, self.arguments = (executable, arguments)
+        (self._stdout, self._check_conflicts, self._win_regpath_installdir,
+            self._win_regpath_installpath) = (stdout, check_conflicts,
+                win_regpath_installdir, win_regpath_installpath)
+        
+    def detect(self, ui):
+        ''' Attempt to detect tool.
+        
+        This default implementation follows this strategy:
+        1. Try registry.
+        2. Try shell path.
+        @param ui: Mercurial UI.
+        @return: Success?
+        '''
+        for func in (self._detect_in_reg, self._detect_in_path):
+            path = func()
+            if path is not None:
+                self._finalize(path)
+                return True
+        return False
+
+    def merge(self, base, local, other, output, ui):
+        ''' Perform merge.
+        
+        In this default implementation, the result from the process is returned
+        directly.
+        @param base: Base file.
+        @param local: Local file.
+        @param other: Other file.
+        @param output: File for merge output.
+        @param ui: Mercurial UI.
+        @return: 0 - success, 1 - failure (conflicts), else error
+
+        @note: Only call this after detect.
+        '''
+        substtbl = {'base': base, 'local': local, 'other': other, 'output':
+            output}
+        args = []
+        for arg in self.arguments:
+            m = re.match(r'\$([^$]+)', arg)
+            if m:
+                # A variable, substitute it
+                key = m.group(1)
+                if key in substtbl:
+                    args.append(substtbl[key])
+            else:
+                # A literal
+                args.append(arg)
+        
+        if self._stdout:
+            outfile = file(output, 'w')
+        else:
+            outfile = None
+        r = runcommand([self.executable] + args, ui, stdout=outfile)
+        if r == 0 and self._check_conflicts:
+            # Verify that there are no conflicts
+            if checkconflicts(output):
+                ui.status('Conflict markers detected in %s' % output)
+                return 1
+            
+        return r
+            
+    def postmerge(self, base, local, other, output, ui):
+        ''' Invoke after merging.
+        
+        Default implementation: If requested, fix EOL in output.
+        @param base: Base file.
+        @param local: Local file.
+        @param other: Other file.
+        @param output: File for merge output.
+        @param ui: Mercurial UI.
+        @return: None.
+        '''
+        cleanupeol(base, output, ui)
+    
+    def _detect_in_path(self):
+        ''' Try to detect executable in shell path.
+        
+        The executable is searched for along the shell's path. If it is found,
+        _finalize is called.
+        @return: Found path.
+        '''
+        path = hgutil.find_exe(self.executable)
+        if path is not None:
+            return path
+        return None
+            
+    def _detect_in_reg(self):
+        ''' Try to detect installation in (Windows) registry.
+        @return: Found path.
+        '''
+        regpath = self._win_regpath_installpath
+        if regpath is not None:
+            path = lookup_reg(regpath)
+            if path is not None:
+                if os.access(path, os.X_OK):
+                    return path
+        
+        regpath = self._win_regpath_installdir
+        if regpath is not None:
+            path = lookup_reg(regpath)
+            if path is not None:
+                exepath = os.path.join(path, self.executable + ".exe")
+                if os.access(exepath, os.X_OK):
+                    return exepath
+        return None
+
+    def _finalize(self, executable, arguments=None):
+        ''' Finalize executable and arguments.
+        '''
+        if arguments is None:
+            arguments = self.arguments
+        self.executable, self.arguments = executable, arguments
+
+class customplugin(_plugin):
+    ''' Baseclass for all plug-ins implemented directly in Python.
+    '''
+    def __init__(self, name, priority=0):
+        _plugin.__init__(self, name, 'custom', priority=priority)
+        
+    def detect(self, ui):
+        return True
+
+def checkconflicts(file):
+    '''Look for conflict markers.  Return True if conflicts are found'''
+    conflicts = re.compile("^(<<<<<<< .*|=======|>>>>>>> .*)$", re.M)
+    f = open(file)
+    try:
+        for l in f:
+            if conflicts.match(l):
+                return True
+    finally: f.close()
+    return False
+
+def runcommand(arguments, ui, stdout=None):
+    ''' Run command in separate process.
+    @return: Process exit code.
+    '''
+    cmdline = [hgutil.shellquote(arg) for arg in arguments] + ['<',
+        hgutil.nulldev]
+    cmdline = ' '.join(cmdline)
+    ui.debug(cmdline, '\n')
+    fp = hgutil.popen(cmdline)
+    # Read the output regardless of whether we need it, since otherwise we might
+    # have a broken pipe
+    for l in fp:
+        if stdout is not None:
+            stdout.write(l)
+    ret = fp.close()
+    if ret is None:
+        # None indicates no error
+        return 0
+    return ret
+
+def lookup_reg(path):
+    ''' Look up a path in the Windows registry.
+    @return: The value for the path if found, else None.
+    '''
+    try:
+        from _winreg import HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, \
+        QueryValueEx, OpenKey
+    except ImportError: return None
+    
+    def query_val(scope, key, valname):
+        try:
+            keyhandle = OpenKey(scope, key)
+            return QueryValueEx(keyhandle, valname)[0]
+        except EnvironmentError:
+            return None
+
+    # Find out the key and value name parts of the pathname
+
+    end = None  # The end of the key part
+    i = 0
+    while i < len(path):
+        c = path[i]
+        if c == '\\':
+            try: n = path[i+1]
+            except IndexError: pass
+            else:
+                if n == '\\':
+                    i += 2
+                    continue
+            end = i
+        i+= 1
+    if end is not None and end != len(path)-1:
+        key, valname = path[:end], path[end+1:]
+    else:
+        key, valname = path, None
+        if end == len(path)-1:
+            # Skip trailing \
+            key = key[:-1]
+            
+    val = query_val(HKEY_CURRENT_USER, key, valname)
+    if val is not None:
+        return val
+    return query_val(HKEY_LOCAL_MACHINE, key, valname)
+
+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, ui):
+    ''' Revert EOL format to base format after merge.
+    
+    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])
+        ui.status("Fixup line format of %s (%s->%s)" % (output,
+                  outputtype, basetype))
+    except KeyError:
+        ui.warn("Unable to fixup %s (%s->%s)" % (output,
+                  outputtype, basetype))
diff --git a/mercurial/hgmerge/_plugins.py b/mercurial/hgmerge/_plugins.py
new file mode 100644
--- /dev/null
+++ b/mercurial/hgmerge/_plugins.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-s
+''' Plug-in classes. '''
+import os.path
+import shutil
+
+from mercurial.hgmerge._common import *
+from mercurial.hgmerge._pluginapi import customplugin, toolplugin
+
+class guiffy(toolplugin):
+    def __init__(self):
+        toolplugin.__init__(self, 'guiffy',
+            arguments=['-s', '-eauto',
+            '-h1mine', '-h2theirs', '$local', '$other', '$base', '$output'])
+
+    def premerge(self, base, local, other, output, ui):
+        # Guiffy complains when output file already exists, so we remove it.
+        os.remove(output)
+
+class takemine(customplugin):
+    ''' Ignore changes from other heads.
+    '''
+    def __init__(self):
+        customplugin.__init__(self, 'takemine')
+        
+    def merge(self, base, local, other, output, ui):
+        ui.status('Taking local file, ignoring remote changes')
+        shutil.copy2(local, output)
+        return 0
+
+class takeother(customplugin):
+    '''Ignore local changes.
+    '''
+    def __init__(self):
+        customplugin.__init__(self, 'takeother')
+    
+    def merge(self, base, local, other, output, ui):
+        ui.status('Taking other file, ignoring local changes')
+        shutil.copy2(other, output)
+        return 0
+    
+class kdiff3(toolplugin):
+    def __init__(self):
+        toolplugin.__init__(self, 'kdiff3', arguments=['--auto', "--L1", "Base",
+            "--L2", "Local", "--L3", "Other", '$base', '$local', '$other',
+            '-o', '$output'], win_regpath_installdir='Software\\KDiff3\\')
+    
+    def postmerge(self, base, local, other, output, ui):
+        ''' Implement in order to clean up after kdiff3. '''
+        try: os.remove('%s.orig' % output)
+        except EnvironmentError: pass
+
+plugins = (
+    kdiff3(),
+    toolplugin('gvimdiff', arguments=['--nofork', '-d', '-g', '-O', '$output',
+        '$other', '$base'], win_regpath_installpath='Software\\Vim\\GVim\\path'),
+    toolplugin('merge', arguments=['$output', '$base', '$other'],
+        check_conflicts=True),
+    toolplugin('gpyfm', arguments=['$output', '$base', '$other']),
+    toolplugin('meld', arguments=['$local', '$output', '$other']),
+    toolplugin('tkdiff', arguments=['$local', '$other', '-a', '$base', '-o',
+        '$output']),
+    toolplugin('xxdiff', arguments=['--show-merged-pane',
+        '--exit-with-merge-status', '--title1', 'mine', '--title2', 'ancestor',
+        '--title3', 'theirs', '--merged-filename', '$output', '--merge',
+        '$local', '$base', '$other']),
+    toolplugin('diffmerge', arguments=['--nosplash', '--merge',
+        '--caption=Mercurial Merge', '--title1=Base', '--title2=Mine',
+        '--title3=Theirs', '$base', '$output', '$other']),
+    toolplugin('p4merge', arguments=['$base', '$local', '$other', '$output'],
+        win_regpath_installdir='Software\\Perforce\\Environment\\P4INSTROOT'),
+    toolplugin('tortoisemerge', arguments=['/base:$output', '/mine:$local',
+        '/theirs:$other', '/merged:$output'], win_regpath_installpath=
+        'Software\\TortoiseSVN\\TMergePath'),
+    toolplugin('ecmerge', arguments=['$base', '$local', '$other',
+        '--mode=merge3', '--title0=base', '--title1=mine', '--title2=theirs',
+        '--to=$output'], win_regpath_installpath=
+        'Software\\Ellié Computing\\Merge\\Path'),
+    toolplugin('filemerge', arguments=['-left', '$other', '-right', '$local',
+        '-ancestor', '$base', '-merge', '$output']),
+    )
\ No newline at end of file
diff --git a/contrib/simplemerge b/mercurial/hgmerge/_simplemerge.py
old mode 100755
new mode 100644
copy from contrib/simplemerge
copy to mercurial/hgmerge/_simplemerge.py
--- a/contrib/simplemerge
+++ b/mercurial/hgmerge/_simplemerge.py
@@ -25,15 +25,20 @@ from mercurial import util, mdiff, fancy
 from mercurial import util, mdiff, fancyopts
 from mercurial.i18n import _
 
+import os.path
+import sys
 
 class CantReprocessAndShowBase(Exception):
     pass
 
 
 def warn(message):
-    sys.stdout.flush()
-    sys.stderr.write(message)
-    sys.stderr.flush()
+    global _ui
+    if _ui is not None:
+        _ui.warn(message)
+    else:
+        sys.stderr.write(message)
+        sys.stderr.flush()
 
 
 def intersect(ra, rb):
@@ -449,7 +454,10 @@ class Merge3(Merge3Text):
         Merge3Text.__init__(self, basetext, atext, btext, base, a, b)
 
 
-def simplemerge(local, base, other, **opts):
+def simplemerge(base, local, other, output, ui=None, **opts):
+    ''' Simple three-way file merge.
+    @return: 0 - success, 1 - conflict, other integer - error.
+    '''
     def readfile(filename):
         f = open(filename, "rb")
         text = f.read()
@@ -461,6 +469,9 @@ def simplemerge(local, base, other, **op
             elif not opts.get('quiet'):
                 warn(_('warning: %s\n') % msg)
         return text
+
+    global _ui
+    _ui = ui
 
     name_a = local
     name_b = other
@@ -476,11 +487,10 @@ def simplemerge(local, base, other, **op
     basetext = readfile(base)
     othertext = readfile(other)
 
-    orig = local
-    local = os.path.realpath(local)
+    output = os.path.realpath(output)
     if not opts.get('print'):
-        opener = util.opener(os.path.dirname(local))
-        out = opener(os.path.basename(local), "w", atomictemp=True)
+        opener = util.opener(os.path.dirname(output))
+        out = opener(os.path.basename(output), "w", atomictemp=True)
     else:
         out = sys.stdout
 
@@ -498,6 +508,8 @@ def simplemerge(local, base, other, **op
         if not opts.get('quiet'):
             warn(_("warning: conflicts during merge.\n"))
         return 1
+    
+    return 0
 
 options = [('L', 'label', [], _('labels to use on conflict markers')),
            ('a', 'text', None, _('treat all files as text')),
@@ -545,7 +557,8 @@ def main(argv):
             return 0
         if len(args) != 3:
                 raise ParseError(_('wrong number of arguments'))
-        return simplemerge(*args, **opts)
+        local, base, other = args
+        return simplemerge(base, local, other, local, **opts)
     except ParseError, e:
         sys.stdout.write("%s: %s\n" % (sys.argv[0], e))
         showhelp()
@@ -557,6 +570,4 @@ def main(argv):
         return 255
 
 if __name__ == '__main__':
-    import sys
-    import os
     sys.exit(main(sys.argv))
diff --git a/mercurial/hgmerge/design.txt b/mercurial/hgmerge/design.txt
new file mode 100644
--- /dev/null
+++ b/mercurial/hgmerge/design.txt
@@ -0,0 +1,26 @@
+Hgmerge's design explained
+
+Since hgmerge is a layer on top of a number of different merge tools, we need a
+way to handle the variations between these tools. Our representation of these
+tools should also be as simple and compact as possible, since users should be
+allowed to modify and extend our stock definitions.
+
+Our system for interfacing with merge tools, then, is to have a set of plug-in
+classes. For the simplest possible configuration, one of the plug-ins is a
+completely generic class, paramplugin, whose operation is defined through
+parameters. Users should be able to define new tools declaratively by specifying
+parameters for this plug-in. If a tool doesn't map to this standard plug-in, a
+custom plug-in must be written.
+
+Plug-in Interface
+=================
+The whole plug-in interface consists of four methods: detect, premerge, merge
+and postmerge. The only method that is strictly required is merge. The detect
+method should return True if the plug-in can be used.
+
+Detecting Tools
+===============
+First hgmerge checks for user specifications, either globally or per file
+extension. If nothing is specified, auto-detection is attempted.
+
+# vim: textwidth=80


More information about the Mercurial-devel mailing list