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