[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