Rev 5632: (vila) Added external merge tool management module, in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Tue Jan 25 13:59:36 UTC 2011


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 5632 [merge]
revision-id: pqm at pqm.ubuntu.com-20110125135932-0o8d07i3j1flp6ou
parent: pqm at pqm.ubuntu.com-20110125091848-0bdpzjixaotg1320
parent: gordon at doxxx.net-20110121235115-9sdqamejot1h0481
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Tue 2011-01-25 13:59:32 +0000
message:
  (vila) Added external merge tool management module,
   bzrlib.mergetools. (Gordon Tyler)
added:
  bzrlib/mergetools.py           bzrlibmergetools.py-20100701052504-knb0llvufl26fxgx-1
  bzrlib/tests/test_mergetools.py bzrlibteststest_merg-20100701052504-knb0llvufl26fxgx-2
modified:
  bzrlib/config.py               config.py-20051011043216-070c74f4e9e338e8
  bzrlib/help_topics/en/configuration.txt configuration.txt-20060314161707-868350809502af01
  bzrlib/osutils.py              osutils.py-20050309040759-eeaff12fbf77ac86
  bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
  bzrlib/tests/features.py       features.py-20090820042958-jglgza3wrn03ha9e-1
  bzrlib/tests/test_cmdline.py   bzrlibteststest_cmdl-20100202043522-83yorxx3tcigi7ap-2
  bzrlib/tests/test_config.py    testconfig.py-20051011041908-742d0c15d8d8c8eb
  bzrlib/tests/test_osutils.py   test_osutils.py-20051201224856-e48ee24c12182989
  doc/en/release-notes/bzr-2.4.txt bzr2.4.txt-20110114053217-k7ym9jfz243fddjm-1
  doc/en/whats-new/whats-new-in-2.4.txt whatsnewin2.4.txt-20110114044330-nipk1og7j729fy89-1
=== modified file 'bzrlib/config.py'
--- a/bzrlib/config.py	2011-01-12 18:12:58 +0000
+++ b/bzrlib/config.py	2011-01-20 04:44:14 +0000
@@ -82,6 +82,7 @@
     errors,
     lockdir,
     mail_client,
+    mergetools,
     osutils,
     registry,
     symbol_versioning,
@@ -357,6 +358,24 @@
         else:
             return True
 
+    def get_merge_tools(self):
+        tools = {}
+        for (oname, value, section, conf_id, parser) in self._get_options():
+            if oname.startswith('bzr.mergetool.'):
+                tool_name = oname[len('bzr.mergetool.'):]
+                tools[tool_name] = value
+        trace.mutter('loaded merge tools: %r' % tools)
+        return tools
+
+    def find_merge_tool(self, name):
+        # We fake a defaults mechanism here by checking if the given name can 
+        # be found in the known_merge_tools if it's not found in the config.
+        # This should be done through the proposed config defaults mechanism
+        # when it becomes available in the future.
+        command_line = (self.get_user_option('bzr.mergetool.%s' % name) or
+                        mergetools.known_merge_tools.get(name, None))
+        return command_line
+
 
 class IniBasedConfig(Config):
     """A configuration policy that draws from ini files."""

=== modified file 'bzrlib/help_topics/en/configuration.txt'
--- a/bzrlib/help_topics/en/configuration.txt	2010-12-07 09:06:39 +0000
+++ b/bzrlib/help_topics/en/configuration.txt	2011-01-16 01:12:01 +0000
@@ -584,3 +584,37 @@
 If present, defines the ``--strict`` option default value for checking
 uncommitted changes before sending a merge directive.
 
+
+External Merge Tools
+--------------------
+
+bzr.mergetool.<name>
+~~~~~~~~~~~~~~~~~~~~
+
+Defines an external merge tool called <name> with the given command-line.
+Arguments containing spaces should be quoted using single or double quotes. The
+executable may omit its path if it can be found on the PATH.
+
+The following markers can be used in the command-line to substitute filenames
+involved in the merge conflict:
+
+{base}      file.BASE
+{this}      file.THIS
+{other}     file.OTHER
+{result}    output file
+{this_temp} temp copy of file.THIS, used to overwrite output file if merge
+            succeeds.
+
+For example:
+
+  bzr.mergetool.kdiff3 = kdiff3 {base} {this} {other} -o {result}
+
+bzr.default_mergetool
+~~~~~~~~~~~~~~~~~
+
+Specifies which external merge tool (as defined above) should be selected by
+default in tools such as ``bzr qconflicts``.
+
+For example:
+
+  bzr.default_mergetool = kdiff3

=== added file 'bzrlib/mergetools.py'
--- a/bzrlib/mergetools.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/mergetools.py	2011-01-21 23:51:15 +0000
@@ -0,0 +1,116 @@
+# Copyright (C) 2010 Canonical Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Utility functions for managing external merge tools such as kdiff3."""
+
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+from bzrlib import (
+    cmdline,
+    osutils,
+    trace,
+)
+""")
+
+
+known_merge_tools = {
+    'bcompare': 'bcompare {this} {other} {base} {result}',
+    'kdiff3': 'kdiff3 {base} {this} {other} -o {result}',
+    'xdiff': 'xxdiff -m -O -M {result} {this} {base} {other}',
+    'meld': 'meld {base} {this_temp} {other}',
+    'opendiff': 'opendiff {this} {other} -ancestor {base} -merge {result}',
+    'winmergeu': 'winmergeu {result}',
+}
+
+
+def check_availability(command_line):
+    cmd_list = cmdline.split(command_line)
+    exe = cmd_list[0]
+    if sys.platform == 'win32':
+        if os.path.isabs(exe):
+            base, ext = os.path.splitext(exe)
+            path_ext = [unicode(s.lower())
+                        for s in os.getenv('PATHEXT', '').split(os.pathsep)]
+            return os.path.exists(exe) and ext in path_ext
+        else:
+            return osutils.find_executable_on_path(exe) is not None
+    else:
+        return (os.access(exe, os.X_OK)
+                or osutils.find_executable_on_path(exe) is not None)
+
+
+def invoke(command_line, filename, invoker=None):
+    """Invokes the given merge tool command line, substituting the given
+    filename according to the embedded substitution markers. Optionally, it
+    will use the given invoker function instead of the default
+    subprocess_invoker.
+    """
+    if invoker is None:
+        invoker = subprocess_invoker
+    cmd_list = cmdline.split(command_line)
+    args, tmp_file = _subst_filename(cmd_list, filename)
+    def cleanup(retcode):
+        if tmp_file is not None:
+            if retcode == 0: # on success, replace file with temp file
+                shutil.move(tmp_file, filename)
+            else: # otherwise, delete temp file
+                os.remove(tmp_file)
+    return invoker(args[0], args[1:], cleanup)
+
+
+def _subst_filename(args, filename):
+    subst_names = {
+        'base': filename + u'.BASE',
+        'this': filename + u'.THIS',
+        'other': filename + u'.OTHER',
+        'result': filename,
+    }
+    tmp_file = None
+    subst_args = []
+    for arg in args:
+        if '{this_temp}' in arg and not 'this_temp' in subst_names:
+            fh, tmp_file = tempfile.mkstemp(u"_bzr_mergetools_%s.THIS" %
+                                            os.path.basename(filename))
+            trace.mutter('fh=%r, tmp_file=%r', fh, tmp_file)
+            os.close(fh)
+            shutil.copy(filename + u".THIS", tmp_file)
+            subst_names['this_temp'] = tmp_file
+        arg = _format_arg(arg, subst_names)
+        subst_args.append(arg)
+    return subst_args, tmp_file
+
+
+# This would be better implemented using format() from python 2.6
+def _format_arg(arg, subst_names):
+    arg = arg.replace('{base}', subst_names['base'])
+    arg = arg.replace('{this}', subst_names['this'])
+    arg = arg.replace('{other}', subst_names['other'])
+    arg = arg.replace('{result}', subst_names['result'])
+    if subst_names.has_key('this_temp'):
+        arg = arg.replace('{this_temp}', subst_names['this_temp'])
+    return arg
+
+
+def subprocess_invoker(executable, args, cleanup):
+    retcode = subprocess.call([executable] + args)
+    cleanup(retcode)
+    return retcode

=== modified file 'bzrlib/osutils.py'
--- a/bzrlib/osutils.py	2011-01-12 21:06:32 +0000
+++ b/bzrlib/osutils.py	2011-01-16 01:12:01 +0000
@@ -2403,3 +2403,36 @@
     except (ImportError, AttributeError):
         # Either the fcntl module or specific constants are not present
         pass
+
+
+def find_executable_on_path(name):
+    """Finds an executable on the PATH.
+    
+    On Windows, this will try to append each extension in the PATHEXT
+    environment variable to the name, if it cannot be found with the name
+    as given.
+    
+    :param name: The base name of the executable.
+    :return: The path to the executable found or None.
+    """
+    path = os.environ.get('PATH')
+    if path is None:
+        return None
+    path = path.split(os.pathsep)
+    if sys.platform == 'win32':
+        exts = os.environ.get('PATHEXT', '').split(os.pathsep)
+        exts = [ext.lower() for ext in exts]
+        base, ext = os.path.splitext(name)
+        if ext != '':
+            if ext.lower() not in exts:
+                return None
+            name = base
+            exts = [ext]
+    else:
+        exts = ['']
+    for ext in exts:
+        for d in path:
+            f = os.path.join(d, name) + ext
+            if os.access(f, os.X_OK):
+                return f
+    return None

=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py	2011-01-12 18:39:25 +0000
+++ b/bzrlib/tests/__init__.py	2011-01-16 01:12:01 +0000
@@ -3802,6 +3802,7 @@
         'bzrlib.tests.test_merge3',
         'bzrlib.tests.test_merge_core',
         'bzrlib.tests.test_merge_directive',
+        'bzrlib.tests.test_mergetools',
         'bzrlib.tests.test_missing',
         'bzrlib.tests.test_msgeditor',
         'bzrlib.tests.test_multiparent',

=== modified file 'bzrlib/tests/features.py'
--- a/bzrlib/tests/features.py	2011-01-12 01:01:53 +0000
+++ b/bzrlib/tests/features.py	2011-01-16 01:12:01 +0000
@@ -19,7 +19,10 @@
 import os
 import stat
 
-from bzrlib import tests
+from bzrlib import (
+    osutils,
+    tests,
+    )
 
 
 class _NotRunningAsRoot(tests.Feature):
@@ -113,16 +116,8 @@
         return self._path
 
     def _probe(self):
-        path = os.environ.get('PATH')
-        if path is None:
-            return False
-        for d in path.split(os.pathsep):
-            if d:
-                f = os.path.join(d, self.name)
-                if os.access(f, os.X_OK):
-                    self._path = f
-                    return True
-        return False
+        self._path = osutils.find_executable_on_path(self.name)
+        return self._path is not None
 
     def feature_name(self):
         return '%s executable' % self.name

=== modified file 'bzrlib/tests/test_cmdline.py'
--- a/bzrlib/tests/test_cmdline.py	2010-05-20 02:57:52 +0000
+++ b/bzrlib/tests/test_cmdline.py	2010-12-06 14:01:44 +0000
@@ -113,4 +113,3 @@
         self.assertAsTokens([(False, r'\\"'), (False, r'*.py')],
                             r'\\\\\" *.py')
         self.assertAsTokens([(True, u'\\\\')], u'"\\\\')
-

=== modified file 'bzrlib/tests/test_config.py'
--- a/bzrlib/tests/test_config.py	2011-01-10 22:20:12 +0000
+++ b/bzrlib/tests/test_config.py	2011-01-20 04:44:14 +0000
@@ -33,6 +33,7 @@
     errors,
     osutils,
     mail_client,
+    mergetools,
     ui,
     urlutils,
     tests,
@@ -70,6 +71,9 @@
 gpg_signing_command=gnome-gpg
 log_format=short
 user_global_option=something
+bzr.mergetool.sometool=sometool {base} {this} {other} -o {result}
+bzr.mergetool.funkytool=funkytool "arg with spaces" {this_temp}
+bzr.default_mergetool=sometool
 [ALIASES]
 h=help
 ll=""" + sample_long_alias + "\n"
@@ -953,6 +957,41 @@
         change_editor = my_config.get_change_editor('old', 'new')
         self.assertIs(None, change_editor)
 
+    def test_get_merge_tools(self):
+        conf = self._get_sample_config()
+        tools = conf.get_merge_tools()
+        self.log(repr(tools))
+        self.assertEqual(
+            {u'funkytool' : u'funkytool "arg with spaces" {this_temp}',
+            u'sometool' : u'sometool {base} {this} {other} -o {result}'},
+            tools)
+
+    def test_get_merge_tools_empty(self):
+        conf = self._get_empty_config()
+        tools = conf.get_merge_tools()
+        self.assertEqual({}, tools)
+
+    def test_find_merge_tool(self):
+        conf = self._get_sample_config()
+        cmdline = conf.find_merge_tool('sometool')
+        self.assertEqual('sometool {base} {this} {other} -o {result}', cmdline)
+
+    def test_find_merge_tool_not_found(self):
+        conf = self._get_sample_config()
+        cmdline = conf.find_merge_tool('DOES NOT EXIST')
+        self.assertIs(cmdline, None)
+
+    def test_find_merge_tool_known(self):
+        conf = self._get_empty_config()
+        cmdline = conf.find_merge_tool('kdiff3')
+        self.assertEquals('kdiff3 {base} {this} {other} -o {result}', cmdline)
+        
+    def test_find_merge_tool_override_known(self):
+        conf = self._get_empty_config()
+        conf.set_user_option('bzr.mergetool.kdiff3', 'kdiff3 blah')
+        cmdline = conf.find_merge_tool('kdiff3')
+        self.assertEqual('kdiff3 blah', cmdline)
+
 
 class TestGlobalConfigSavingOptions(tests.TestCaseInTempDir):
 

=== added file 'bzrlib/tests/test_mergetools.py'
--- a/bzrlib/tests/test_mergetools.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/test_mergetools.py	2011-01-20 04:44:14 +0000
@@ -0,0 +1,167 @@
+# Copyright (C) 2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import re
+import sys
+import tempfile
+
+from bzrlib import (
+    mergetools,
+    tests
+)
+from bzrlib.tests.features import backslashdir_feature
+
+
+class TestFilenameSubstitution(tests.TestCaseInTempDir):
+
+    def test_simple_filename(self):
+        cmd_list = ['kdiff3', '{base}', '{this}', '{other}', '-o', '{result}']
+        args, tmpfile = mergetools._subst_filename(cmd_list, 'test.txt')
+        self.assertEqual(
+            ['kdiff3',
+             'test.txt.BASE',
+             'test.txt.THIS',
+             'test.txt.OTHER',
+             '-o',
+             'test.txt'],
+            args)
+        
+    def test_spaces(self):
+        cmd_list = ['kdiff3', '{base}', '{this}', '{other}', '-o', '{result}']
+        args, tmpfile = mergetools._subst_filename(cmd_list,
+                                                   'file with space.txt')
+        self.assertEqual(
+            ['kdiff3',
+             'file with space.txt.BASE',
+             'file with space.txt.THIS',
+             'file with space.txt.OTHER',
+             '-o',
+             'file with space.txt'],
+            args)
+
+    def test_spaces_and_quotes(self):
+        cmd_list = ['kdiff3', '{base}', '{this}', '{other}', '-o', '{result}']
+        args, tmpfile = mergetools._subst_filename(cmd_list,
+            'file with "space and quotes".txt')
+        self.assertEqual(
+            ['kdiff3',
+             'file with "space and quotes".txt.BASE',
+             'file with "space and quotes".txt.THIS',
+             'file with "space and quotes".txt.OTHER',
+             '-o',
+             'file with "space and quotes".txt'],
+            args)
+
+    def test_tempfile(self):
+        self.build_tree(('test.txt', 'test.txt.BASE', 'test.txt.THIS',
+                         'test.txt.OTHER'))
+        cmd_list = ['some_tool', '{this_temp}']
+        args, tmpfile = mergetools._subst_filename(cmd_list, 'test.txt')
+        self.failUnlessExists(tmpfile)
+        os.remove(tmpfile)
+
+
+class TestCheckAvailability(tests.TestCaseInTempDir):
+
+    def test_full_path(self):
+        self.assertTrue(mergetools.check_availability(sys.executable))
+
+    def test_exe_on_path(self):
+        self.assertTrue(mergetools.check_availability(
+            os.path.basename(sys.executable)))
+
+    def test_nonexistent(self):
+        self.assertFalse(mergetools.check_availability('DOES NOT EXIST'))
+
+    def test_non_executable(self):
+        f, name = tempfile.mkstemp()
+        try:
+            self.log('temp filename: %s', name)
+            self.assertFalse(mergetools.check_availability(name))
+        finally:
+            os.close(f)
+            os.unlink(name)
+
+
+class TestInvoke(tests.TestCaseInTempDir):
+    def setUp(self):
+        super(tests.TestCaseInTempDir, self).setUp()
+        self._exe = None
+        self._args = None
+        self.build_tree_contents((
+            ('test.txt', 'stuff'),
+            ('test.txt.BASE', 'base stuff'),
+            ('test.txt.THIS', 'this stuff'),
+            ('test.txt.OTHER', 'other stuff'),
+        ))
+        
+    def test_success(self):
+        def dummy_invoker(exe, args, cleanup):
+            self._exe = exe
+            self._args = args
+            cleanup(0)
+            return 0
+        retcode = mergetools.invoke('tool {result}', 'test.txt', dummy_invoker)
+        self.assertEqual(0, retcode)
+        self.assertEqual('tool', self._exe)
+        self.assertEqual(['test.txt'], self._args)
+    
+    def test_failure(self):
+        def dummy_invoker(exe, args, cleanup):
+            self._exe = exe
+            self._args = args
+            cleanup(1)
+            return 1
+        retcode = mergetools.invoke('tool {result}', 'test.txt', dummy_invoker)
+        self.assertEqual(1, retcode)
+        self.assertEqual('tool', self._exe)
+        self.assertEqual(['test.txt'], self._args)
+
+    def test_success_tempfile(self):
+        def dummy_invoker(exe, args, cleanup):
+            self._exe = exe
+            self._args = args
+            self.failUnlessExists(args[0])
+            f = open(args[0], 'wt')
+            f.write('temp stuff')
+            f.close()
+            cleanup(0)
+            return 0
+        retcode = mergetools.invoke('tool {this_temp}', 'test.txt',
+                                    dummy_invoker)
+        self.assertEqual(0, retcode)
+        self.assertEqual('tool', self._exe)
+        self.failIfExists(self._args[0])
+        self.assertFileEqual('temp stuff', 'test.txt')
+    
+    def test_failure_tempfile(self):
+        def dummy_invoker(exe, args, cleanup):
+            self._exe = exe
+            self._args = args
+            self.failUnlessExists(args[0])
+            self.log(repr(args))
+            f = open(args[0], 'wt')
+            self.log(repr(f))
+            f.write('temp stuff')
+            f.close()
+            cleanup(1)
+            return 1
+        retcode = mergetools.invoke('tool {this_temp}', 'test.txt',
+                                    dummy_invoker)
+        self.assertEqual(1, retcode)
+        self.assertEqual('tool', self._exe)
+        self.assertFileEqual('stuff', 'test.txt')

=== modified file 'bzrlib/tests/test_osutils.py'
--- a/bzrlib/tests/test_osutils.py	2011-01-12 15:50:12 +0000
+++ b/bzrlib/tests/test_osutils.py	2011-01-16 01:12:01 +0000
@@ -2119,3 +2119,25 @@
         # revisited if we test against all implementations.
         self.backups.remove('file.~2~')
         self.assertBackupName('file.~2~', 'file')
+
+
+class TestFindExecutableInPath(tests.TestCase):
+
+    def test_windows(self):
+        if sys.platform != 'win32':
+            raise tests.TestSkipped('test requires win32')
+        self.assertTrue(osutils.find_executable_on_path('explorer') is not None)
+        self.assertTrue(
+            osutils.find_executable_on_path('explorer.exe') is not None)
+        self.assertTrue(
+            osutils.find_executable_on_path('EXPLORER.EXE') is not None)
+        self.assertTrue(
+            osutils.find_executable_on_path('THIS SHOULD NOT EXIST') is None)
+        self.assertTrue(osutils.find_executable_on_path('file.txt') is None)
+
+    def test_other(self):
+        if sys.platform == 'win32':
+            raise tests.TestSkipped('test requires non-win32')
+        self.assertTrue(osutils.find_executable_on_path('sh') is not None)
+        self.assertTrue(
+            osutils.find_executable_on_path('THIS SHOULD NOT EXIST') is None)

=== modified file 'doc/en/release-notes/bzr-2.4.txt'
--- a/doc/en/release-notes/bzr-2.4.txt	2011-01-25 08:43:02 +0000
+++ b/doc/en/release-notes/bzr-2.4.txt	2011-01-25 13:59:32 +0000
@@ -23,6 +23,9 @@
 * The ``lp:`` directory service now supports Launchpad's QA staging.
   (Jelmer Vernooij, #667483)
 
+* External merge tools can now be configured in bazaar.conf. See
+  ``bzr help configuration`` for more information.  (Gordon Tyler, #489915)
+
 Improvements
 ************
 
@@ -76,6 +79,9 @@
 .. Changes that may require updates in plugins or other code that uses
    bzrlib.
 
+* Added ``bzrlib.mergetools`` module with helper functions for working with
+  the list of external merge tools. (Gordon Tyler, #489915)
+
 Internals
 *********
 

=== modified file 'doc/en/whats-new/whats-new-in-2.4.txt'
--- a/doc/en/whats-new/whats-new-in-2.4.txt	2011-01-14 05:34:20 +0000
+++ b/doc/en/whats-new/whats-new-in-2.4.txt	2011-01-20 23:41:26 +0000
@@ -16,6 +16,12 @@
 2.1, 2.2 and 2.3, and can read and write repositories generated by all
 previous versions.
 
+External Merge Tools
+********************
+
+External merge tool configuration has been added to ``bzr`` core. The name
+and commandline of one or more external merge tools can be defined in
+bazaar.conf. See the help topic ``configuration`` for more details.
 
 Further information
 *******************




More information about the bazaar-commits mailing list