Merge lp:~doxxx/bzr/mergetools into lp:bzr

Proposed by Gordon Tyler
Status: Superseded
Proposed branch: lp:~doxxx/bzr/mergetools
Merge into: lp:bzr
Diff against target: 711 lines (+670/-2)
5 files modified
bzrlib/help_topics/en/configuration.txt (+33/-0)
bzrlib/mergetools.py (+317/-0)
bzrlib/tests/__init__.py (+1/-0)
bzrlib/tests/test_mergetools.py (+317/-0)
doc/en/release-notes/bzr-2.3.txt (+2/-2)
To merge this branch: bzr merge lp:~doxxx/bzr/mergetools
Reviewer Review Type Date Requested Status
Vincent Ladeuil Needs Fixing
John A Meinel Needs Fixing
Martin Packman (community) Abstain
Review via email: mp+38663@code.launchpad.net

This proposal has been superseded by a proposal from 2010-11-16.

Commit message

(doxxx) Added external merge tool support.

Description of the change

External merge tool support in bzr and related GUI tools is not very well defined and there are currently two incompatible mechanisms for setting an external merge tool.

This merge proposal seeks to improve this by defining a bzrlib.mergetools module which provides an API for managing a list of external merge tools. Each merge tool is defined with a name and command-line and stored in the global Bazaar config by default. A default merge tool can be set, which is intended to be used by GUI apps like qconfig and qconflicts.

Related branches:

lp:~doxxx/bzr/mergetools-commands adds a 'bzr mergetools' command which allows the user to manage the list of mergetools, and adds a '--resolve-using' option to 'bzr merge' and 'bzr remerge' to invoke an external merge tool for text conflicts. I haven't submitted a merge proposal for this yet since it's still a work in progress.

lp:~qbzr-dev/qbzr/mergetools changes qconfig and qconflicts to make use of 'bzrlib.mergetools'.

To post a comment you must log in.
Revision history for this message
Gordon Tyler (doxxx) wrote :

Could I get some feedback on this? Right approach? Too limited in scope?

Revision history for this message
John C Barstow (jbowtie) wrote :

It looks like a big step in the right direction, and enough of an improvement to be worth merging from my perspective. There are two additional things I'd really like to see, but IMO they could be handled as issues in their own right.

1) I would like to see 'bzr diff' handled in the same way, so there is a consistent approach for external tools.

2) I think that the 'add' operation should allow for optionally specifying a name. This covers the ability to add the same tool twice with different flags, the ability to work with two executables that happen to have the same name, and the ability to choose an alias that conveys additional information (such as 'cygwin-sdiff' on a Windows platform).

If we choose to merge this, I think the user and admin guides will need to be updated, with an eye to explaining both how to use the functionality and how to determine when it is most appropriate.

Revision history for this message
Martin Packman (gz) wrote :

I don't know about any of the high level design stuff, but noticed a few things reading through the diff that I was getting round to writing up.

I like that you're using cmdline here, but why are you then getting in the business of quoting the arguments again? If there's a reason not to pass subprocess.call a list, that should be documented. Also, you're using shell=True always, as that's creating an intermediate process you should say why you need it.

Using percent-followed-by-letter is slightly misleading for the templating, as this is a little more magical than straight interpolation. Also odd that I could use %TMP% in my line and be suprised by just %T expanding. I'm guessing there are good historical reasons though.

What happens with non-ascii filenames? I see no handling, so will you just be throwing UnicodeError somewhere?

+def _resolve_using_merge_tool(tool_name, conflicts):

I'm not mad about this function or the --resolve-using interface, but don't have any great alternatives.

+ If you need to include options in your external merge tool's
+ command-line, insert '--' before the command-line to prevent bzr from
+ processing them as options to the ``bzr mergetools`` command:

This seems silly. Why is:

+ bzr mergetools --add -- kdiff3 %b %t %o -o %r

Preferable to `bzr mergetools --add "kdiff3 %b %t %o -o %r"`?

+ def get_name(self):
+ name = os.path.basename(self.get_executable())
+ if sys.platform == 'win32' and name.lower().endswith('.exe'):
+ name = name[:-4]

What's special about .exe over .bat or .com? Should this be using os.path.splitext and the PATHEXT envvar?

+ def is_available(self):
+ executable = self.get_executable()
+ return os.path.exists(executable) or _find_executable(executable)

Don't like this look-before-you-leap style. Would prefer you catch the right OSError from subprocess.call to detect and warn about missing executables.

+# Authors: Robert Collins <email address hidden>

You conned Robert into writing your tests for you? :)

review: Abstain
Revision history for this message
Gordon Tyler (doxxx) wrote :
Download full text (4.4 KiB)

Thanks John and Martin for the feedback!

> 1) I would like to see 'bzr diff' handled in the same way, so there is a
> consistent approach for external tools.

I have been giving this some thought but I wasn't sure if it would be trying to do too much in one patch.

> 2) I think that the 'add' operation should allow for optionally specifying a
> name. This covers the ability to add the same tool twice with different flags,
> the ability to work with two executables that happen to have the same name,
> and the ability to choose an alias that conveys additional information (such
> as 'cygwin-sdiff' on a Windows platform).

I have thought about this as well. It complicates the storage in the config, although not to the point of being infeasible. And as you say, it's almost certainly going to be necessary in some situations. I'll see what I can do.

> I like that you're using cmdline here, but why are you then getting in the
> business of quoting the arguments again? If there's a reason not to pass
> subprocess.call a list, that should be documented. Also, you're using
> shell=True always, as that's creating an intermediate process you should say
> why you need it.

I am passing a list to subprocess.call. The commandline is given by the user and stored as a single string and I use cmdline.split to convert it to a list before calling subprocess.call. Therefore, when performing filename substitution, I have to quote the filenames so that cmdline.split will split it correctly. However, I probably could do the cmdline.split first and then do filename substitution in each argument without worrying about quoting. I'll try that.

I don't recall why I was using shell=True. It must have seemed like a good idea at the time. I'll remove it.

> Using percent-followed-by-letter is slightly misleading for the templating, as
> this is a little more magical than straight interpolation. Also odd that I
> could use %TMP% in my line and be suprised by just %T expanding. I'm guessing
> there are good historical reasons though.

It's the same syntax as the old external merge tool support as well as the extmerge plugin. Python string interpolation syntax may be too arcane/verbose for users. Although, I suppose they are probably going to be developers. Still, "kdiff3 %(base)s %(this)s %(other)s -o %(result)s" is kinda icky.

> What happens with non-ascii filenames? I see no handling, so will you just be
> throwing UnicodeError somewhere?

Good point, I should be using unicode literals.

> +def _resolve_using_merge_tool(tool_name, conflicts):
>
> I'm not mad about this function or the --resolve-using interface, but don't
> have any great alternatives.

Before this, I had a separate command for invoking a merge tool on conflicts but a quick survey on the bzr list indicated this wasn't what people were expecting, and they suggested something like the above.

> + If you need to include options in your external merge tool's
> + command-line, insert '--' before the command-line to prevent bzr from
> + processing them as options to the ``bzr mergetools`` command:
>
> This seems silly. Why is:
>
> + bzr mergetools --add -- kdiff3 %b %t %o -o ...

Read more...

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (34.1 KiB)

Sorry for the delay. This looks pretty nice. And thanks too for a
very clear cover letter.

It should get a NEWS entry, a whatsnew entry and some description in
the user guide. (whatsnew could be very similar to your cover
letter.) Experience shows that if we don't document new features when
they are added, it is harder to come back and do them later.

Strictly speaking it seems this is redundant with editing the config
file, or with vila's 'config' command, but perhaps the configuration
here is complex enough that it's worth having a special option.

New Pythons have a "hello {name}" syntax
<http://docs.python.org/library/string.html#format-string-syntax> that
would be ideal for this: less error-prone than "%(name)s" but more
explicit and extensible than "%n". Unfortunately that is only in 2.6
and later, but perhaps we could simulate it on old Pythons...?

On 17 October 2010 13:48, Gordon Tyler <email address hidden> wrote:
> Gordon Tyler has proposed merging lp:~doxxx/bzr/mergetools into lp:bzr.
>
> Requested reviews:
>  bzr-core (bzr-core)
> Related bugs:
>  #489915 [master] external diff/merge configuration needs serious rework
>  https://bugs.launchpad.net/bugs/489915
>
>
> External merge tool support in bzr and related GUI tools is not very well defined and there are currently two incompatible mechanisms for setting an external merge tool.
>
> This merge proposal seeks to improve this by defining a bzrlib.mergetools module which provides an API for managing a list of external merge tools, adding a 'bzr mergetools' command which allows the user to manage the list of mergetools, and adding a '--resolve-using' option to 'bzr merge' and 'bzr remerge' to invoke an external merge tool for text conflicts.
>
> There will be a corresponding merge proposal for qbzr to make use of 'bzrlib.mergetools' in qconfig and qconflicts.
>
> Merge tools are defined by their command-line. The name of a merge tool, for the purposes of display and reference in the 'bzr merge' and 'bzr remerge' commands, is derived from the basename of the executable, stripping '.exe' if on win32. In the future, this could be separated out into a distinct field to allow multiple definitions of a merge tool using the same executable but different arguments, which may be necessary for jbowtie's binary merge idea.
>
> The mergetools module also tracks a default merge tool that will be used by qconfig and qconflicts. The 'bzr mergetools' command doesn't provide a method for setting it since it is not necessary for any of the bzr commands that make use of external merge tools.
>
> The 'bzr mergetools' command provides the following operations:
>
> - add: Adds a new external merge tool
> - update: Updates an existing merge tool by name
> - remove: Removes an existing merge tool by name
> - detect: Automatically detects known merge tools on the PATH
> - list: Lists the external merge tools that have been defined
>
> --
> https://code.launchpad.net/~doxxx/bzr/mergetools/+merge/38663
> Your team bzr-core is requested to review the proposed merge of lp:~doxxx/bzr/mergetools into lp:bzr.
>
> === modified file 'bzrlib/builtins.py'
> --- bzrlib/builtins.py  2010-10-15 11:30:54 +0000
> +++ b...

Revision history for this message
Alexander Belchenko (bialix) wrote :

Martin Pool пишет:
> New Pythons have a "hello {name}" syntax
> <http://docs.python.org/library/string.html#format-string-syntax> that
> would be ideal for this: less error-prone than "%(name)s" but more
> explicit and extensible than "%n". Unfortunately that is only in 2.6
> and later, but perhaps we could simulate it on old Pythons...?

Martin, actually bzr uses exactly the same syntax in version-info
command. It should not be too hard to re-use that template engine,
right? ;-)

Revision history for this message
Gordon Tyler (doxxx) wrote :

> It should get a NEWS entry, a whatsnew entry and some description in
> the user guide. (whatsnew could be very similar to your cover
> letter.) Experience shows that if we don't document new features when
> they are added, it is harder to come back and do them later.

I've already added entries to the release notes, which seem to be the intended replacement for NEWS. I'll try find the whatsnew doc and update it. I'll also try my hand ad updating the user guide, but a doc-writer I am not, so it will most likely require the ministrations of Neil M-B.

> Strictly speaking it seems this is redundant with editing the config
> file, or with vila's 'config' command, but perhaps the configuration
> here is complex enough that it's worth having a special option.

Once I add support for naming the merge tools, the resultant config structure may be a bit too complex for newbies to edit. While newbies would most likely be using qbzr and thus qconfig, I would still like to make it possible to manage the tools via the command-line.

> New Pythons have a "hello {name}" syntax
> <http://docs.python.org/library/string.html#format-string-syntax> that
> would be ideal for this: less error-prone than "%(name)s" but more
> explicit and extensible than "%n". Unfortunately that is only in 2.6
> and later, but perhaps we could simulate it on old Pythons...?

Should easy enough to emulate, especially since we'll only support very basic substitution.

Revision history for this message
Gordon Tyler (doxxx) wrote :

Any advice on how to make this Unicode-safe? I've got some local changes here that use Unicode literals for everything that is combined with the user-provided command-line. Is that all I need or is there something more I need to do?

Also, in a blackbox test, how would I validate that my 'bzr mergetools' command is modifying the bzr config correctly, without actually affecting the user's real bzr config?

Revision history for this message
Gordon Tyler (doxxx) wrote :

I've figured out how to do the blackbox tests with spiv's help.

Revision history for this message
Martin Pool (mbp) wrote :

On 26 October 2010 21:56, Gordon Tyler <email address hidden> wrote:
> Any advice on how to make this Unicode-safe? I've got some local changes here that use Unicode literals for everything that is combined with the user-provided command-line. Is that all I need or is there something more I need to do?

Can you be more specific?

> Also, in a blackbox test, how would I validate that my 'bzr mergetools' command is modifying the bzr config correctly, without actually affecting the user's real bzr config?

If you subclass TestCaseinTempDir you should automatically get a
temporary BZR_HOME and config framework.

--
Martin

Revision history for this message
Gordon Tyler (doxxx) wrote :
Download full text (7.8 KiB)

Martin (gz) asked "What happens with non-ascii filenames? I see no handling, so will you just be throwing UnicodeError somewhere?"

I'm not sure what handling I'm meant to do other than use unicode literals (e.g. u'blah') when working with the command-line in a merge tool, like so:

=== modified file 'bzrlib/mergetools.py'
--- a/bzrlib/mergetools.py 2010-10-27 01:56:55 +0000
+++ b/bzrlib/mergetools.py 2010-10-27 04:35:10 +0000
@@ -39,11 +39,11 @@

 substitution_help = {
- '%b' : 'file.BASE',
- '%t' : 'file.THIS',
- '%o' : 'file.OTHER',
- '%r' : 'file (output)',
- '%T' : 'file.THIS (temp copy, used to overwrite "file" if merge succeeds)'
+ u'%b' : u'file.BASE',
+ u'%t' : u'file.THIS',
+ u'%o' : u'file.OTHER',
+ u'%r' : u'file (output)',
+ u'%T' : u'file.THIS (temp copy, used to overwrite "file" if merge succeeds)
'
 }

@@ -53,7 +53,8 @@
     return retcode

-_WIN32_PATH_EXT = [ext.lower() for ext in os.getenv('PATHEXT', '').split(';')]
+_WIN32_PATH_EXT = [unicode(ext.lower())
+ for ext in os.getenv('PATHEXT', '').split(';')]

 class MergeTool(object):
@@ -80,20 +81,20 @@
         return name

     def get_commandline(self):
- return ' '.join(self._commandline)
+ return u' '.join(self._commandline)

     def get_commandline_as_list(self):
         return self._commandline

     def get_executable(self):
         if len(self._commandline) < 1:
- return ''
+ return u''
         return self._commandline[0]

     def get_arguments(self):
         if len(self._commandline) < 2:
- return ''
- return ' '.join(self._commandline[1:])
+ return u''
+ return u' '.join(self._commandline[1:])

     def set_executable(self, executable):
         self._commandline[:1] = [executable]
@@ -130,26 +131,26 @@
         tmp_file = None
         subst_args = []
         for arg in args:
- arg = arg.replace('%b', filename + '.BASE')
- arg = arg.replace('%t', filename + '.THIS')
- arg = arg.replace('%o', filename +'.OTHER')
- arg = arg.replace('%r', filename)
- if '%T' in arg:
- tmp_file = tempfile.mktemp("_bzr_mergetools_%s.THIS" %
+ arg = arg.replace(u'%b', filename + u'.BASE')
+ arg = arg.replace(u'%t', filename + u'.THIS')
+ arg = arg.replace(u'%o', filename + u'.OTHER')
+ arg = arg.replace(u'%r', filename)
+ if u'%T' in arg:
+ tmp_file = tempfile.mktemp(u"_bzr_mergetools_%s.THIS" %
                                            os.path.basename(filename))
- shutil.copy(filename + ".THIS", tmp_file)
- arg = arg.replace('%T', tmp_file)
+ shutil.copy(filename + u".THIS", tmp_file)
+ arg = arg.replace(u'%T', tmp_file)
             subst_args.append(arg)
         return subst_args, tmp_file

 _KNOWN_MERGE_TOOLS = (
- 'bcompare %t %o %b %r',
- 'kdiff3 %b %t %o -o %r',
- 'xxdiff -m -O -M %r %t %b %o',
- 'meld %b %T %o',
- 'opendiff %t %o -ancestor %b -merge %r',
- 'winmergeu %r',
+ u'bcompare %t %o %b %r',
+ u'kdiff3 %b %...

Read more...

Revision history for this message
Martin Pool (mbp) wrote :

On 27 October 2010 00:41, Gordon Tyler <email address hidden> wrote:
> Martin (gz) asked "What happens with non-ascii filenames? I see no handling, so will you just be throwing UnicodeError somewhere?"

I think that particular diff you attached seems reasonable -
human-readable strings that aren't part of binary file formats are
most naturally in unicode.

The best way to handle this would be just to add some tests that use
non-ascii filenames. (They'll need to depend on the unicode
filesystem feature.)

--
Martin

Revision history for this message
Martin Packman (gz) wrote :

> I'm not sure what handling I'm meant to do other than use unicode literals
> (e.g. u'blah') when working with the command-line in a merge tool, like so:

I'd suggest step one should be testing, so you know what is breaking where. Just using unicode literals won't save you because of python limitations:

<http://bugs.python.org/issue1759845>

> I am passing a list to subprocess.call. The commandline is given by the user
> and stored as a single string and I use cmdline.split to convert it to a list
> before calling subprocess.call. Therefore, when performing filename
> substitution, I have to quote the filenames so that cmdline.split will split
> it correctly. However, I probably could do the cmdline.split first and then do
> filename substitution in each argument without worrying about quoting. I'll
> try that.

I see _optional_quote_arg is still being used. I think you should be storing a list not a string in self._commandline so you can delete that and the related functions.

> Because the quoting, if necessary, could get extremely funky. I figured if
> they could just write exactly like they normally would, then it would be less
> confusing.

We don't want the user to be doing any quoting. Needing to wrap every %var in quotes just in case it contains a space is shell-level stupidity we're avoiding by handling that for them.

> This is used in the GUI to indicate that the selected tool is not available
> and to disable the button used to launch the tool.

I really don't like grovelling through the filesystem with _find_executable as an idea. (Does it even have a clear licence?) The 'detect' command for merge tools does seem reasonable though so maybe it's unavoidable.

Revision history for this message
Gordon Tyler (doxxx) wrote :

> I see _optional_quote_arg is still being used. I think you should be storing a
> list not a string in self._commandline so you can delete that and the related
> functions.

I pushed new revisions almost a day ago which did exactly that.

> We don't want the user to be doing any quoting. Needing to wrap every %var in
> quotes just in case it contains a space is shell-level stupidity we're
> avoiding by handling that for them.

I meant quoting which may be necessary for passing arguments correctly to the merge tool they're using. I agree that the user should not have to quote the substitution markers in case they're used with files containing spaces. That's part of the problem with the current system.

> I really don't like grovelling through the filesystem with _find_executable as
> an idea. (Does it even have a clear licence?) The 'detect' command for merge
> tools does seem reasonable though so maybe it's unavoidable.

That find_executable function is exactly as it appears on the webpage I reference in the comment above it. There is no license that I can see. Since it was on a site that explicitly accepts and re-publishes "snippets" of code, I assumed that it was basically in the public domain.

I really like the detect command because it takes a lot of the work out of configuring the merge tools, which is one of the primary goals I'm trying to accomplish here.

Revision history for this message
Gordon Tyler (doxxx) wrote :

I've made merge tool names user-configurable as John Barstow suggested. Unfortunately, I couldn't make the merge tool name optional in the `bzr mergetools --add` command since I don't know how to make an option that optionally takes a value.

Revision history for this message
Gordon Tyler (doxxx) wrote :

The way the merge tools are stored in the bzr config has changed now. Instead of just a list of command-lines, it's now one option which contains a list of tool names and an option named for each tool which contains the command-line. Additionally, the command-lines are stored in list form to preserve spaces within arguments. This does make it a little less user-editable. I could change it back to storing as a simple string but then I'd have to check and quote arguments that contain spaces.

Any comments?

Revision history for this message
Martin Packman (gz) wrote :

> I pushed new revisions almost a day ago which did exactly that.

Ha, the problem with writing up some comments one evening and posting them the next, you're way too fast. :)

The new unicode test looks like a good start, and sorry, should have mentioned failures on tests like that tend to break things badly currently, you'll want testtools 0.9.5 or later and either <lp:~gz/bzr/escape_selftest_console_output_633216> or a utf-8 console. However, there does need to be a test that actually launches some kind of real subprocess, as that's where non-ascii filenames are most likely to break down. Don't need a real merge tool, echo on the shell would probably do.

I agree storing lists in the config file hurts editability. Slightly annoying as to get back to a string does mean quoting even though templated command lines are unlikely to ever need it. On windows you can use subprocess.list2cmdline but you might want your quoting function back for nix even though `" ".join(map(repr, commandline))` should basically work.

Revision history for this message
Martin Pool (mbp) wrote :

On 27 October 2010 23:05, Gordon Tyler <email address hidden> wrote:
> I've made merge tool names user-configurable as John Barstow suggested. Unfortunately, I couldn't make the merge tool name optional in the `bzr mergetools --add` command since I don't know how to make an option that optionally takes a value.

Our ui style says we don't have optional option values; it makes the
syntax a bit confusing.

You could have a second option to specify the tool name?

--
Martin

Revision history for this message
Gordon Tyler (doxxx) wrote :

> > I've made merge tool names user-configurable as John Barstow suggested.
> Unfortunately, I couldn't make the merge tool name optional in the `bzr
> mergetools --add` command since I don't know how to make an option that
> optionally takes a value.
>
> Our ui style says we don't have optional option values; it makes the
> syntax a bit confusing.
>
> You could have a second option to specify the tool name?

I've been thinking about this and I think it would be confusing to have --add behave differently --update and --remove. I'll keep it as it is, requiring a name to be given.

Revision history for this message
Gordon Tyler (doxxx) wrote :

I tried writing Unicode blackbox tests but testtools and bzr's selftest is quite broken for tests that output Unicode or UTF-8, so I've shelved that for later. Any idea when this is going get fixed?

Revision history for this message
Gordon Tyler (doxxx) wrote :

I changed the storage format back to strings in the config file, but this time making sure they're quoted as necessary.

Revision history for this message
Martin Packman (gz) wrote :

Current revision covers all of my queries bar the non-ascii one. See my last comment for answers on your testing problems Gordon, the fix should hopefully land shortly once PQM gets an updated testtools package.

Branch still wants a high level review by someone else.

Revision history for this message
Gordon Tyler (doxxx) wrote :

Thanks, Martin. Will notification of the update to PQM and bzr be psoted to the bzr list?

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (5.9 KiB)

I think the idea of having a common mergetools set is a good one. And is
something that we want to bring into core. I think this still needs some
updates, but it is getting close to a point where I would be happy to merge it.

18 + if merge_tool is None:
19 + available = '\n '.join([mt.get_name() for mt in
20 + mergetools.get_merge_tools()
21 + if mt.is_available()])
22 + raise errors.BzrCommandError('Unrecognized merge tool: %s\n\n'
23 + 'Available merge tools:\n'
24 + ' %s' % (tool_name, available))

^- This specific code seems to be something that should live in mergetools itself. I generally don't like top-level functions in 'builtins.py'. Maybe just move the whole thing to mergetools and use "mergetools.resolve_using_mergetool()" inside the command code?

Importing "mergetools" should also be done in the "lazy_import" section. which you may have already done.

63 + retval = self._do_merge(merger, change_reporter, allow_pending,
64 + verified)
65 + if retval != 0 and resolve_using is not None:
66 + _resolve_using_merge_tool(resolve_using, tree.conflicts())
67 + return retval

^- The spacing is odd here (yes, I realize Copy & Paste messed it up). But what I see is about 8 character indentation, when I would expect 4. Maybe you have a tab char? (sources with tab characters will be rejected by the test suite.)

104 ('cmd_resolve', ['resolved'], 'bzrlib.conflicts'),
105 ('cmd_conflicts', [], 'bzrlib.conflicts'),
106 ('cmd_sign_my_commits', [], 'bzrlib.sign_my_commits'),
107 +<<<<<<< TREE
108 ('cmd_test_script', [], 'bzrlib.cmd_test_script'),
109 +=======
110 + ('cmd_test_script', [], 'bzrlib.tests.script'),
111 + ('cmd_mergetools', [], 'bzrlib.mergetools'),
112 +>>>>>>> MERGE-SOURCE

^- most lists like this we try to keep in sorted order. I realize this one isn't, but it probably should be. (That way, most inserts go into a different location, rather than all-at-the-end and it helps avoid spurious conflicts like this.)

There is duplicate code between "MergeTool.__init__" and "MergeTool.set_commandline". In general, it seems like there are a lot of functions in there that we won't actually want to use. So potentially paring it down a bit would be a good thing. However, if we do want it, then __init__ should probably just call self.set_commandline() rather than copying the code locally.

I agree that "cmd_mergetools" seems a bit redundant with "cmd_config", but it also has stuff like --detect, which means it can do more than config would.

It is probably fine to leave it as is.

716 === added file 'bzrlib/tests/test_mergetools.py'
717 --- bzrlib/tests/test_mergetools.py 1970-01-01 00:00:00 +0000
718 +++ bzrlib/tests/test_mergetools.py 2010-10-30 22:41:24 +0000
719 @@ -0,0 +1,242 @@
720 +# -*- coding: utf-8 -*-
We don't use coding statements. if you need non-ascii characters, use their escaped form. (u'\xb5', etc.)

The tests are a bit clustered (and some lines are longer than 80 chars). Rather than doing:

749 +class TestMergeTool(tests.TestCaseInTempDir):
750 + def test_basics(self):
751 + mt = mergetools.MergeTool('tool', '/path/to/tool --opt %b -x %t %o --stuff %r')
752 + self.assertEquals('/path/to/tool --opt %b -x %t %o --stuff %...

Read more...

review: Needs Fixing
Revision history for this message
Gordon Tyler (doxxx) wrote :

Thanks for the review, John. That all seems quite reasonable. I'll get right on it!

Revision history for this message
Gordon Tyler (doxxx) wrote :

John, I think I've taken care of all the things you mentioned. I removed the get_arguments and set_arguments methods on MergeTool since, as you said, they weren't being used and they're somewhat redundant. The rest of the getter/setter methods are being used in one way or another, or are intended for use by qbzr/etc.

I also used u"\u1234" syntax for the unicode tests since I wasn't exactly sure the u"\xb5" syntax was right.

Revision history for this message
Gary van der Merwe (garyvdm) wrote :

I would personally like a %B - 'file.BASE (temp copy, used to overwrite "file" if merge succeeds)' option, so that I can do meld %t %B %o.

I'm setting this mp back to "Needs Review."

Revision history for this message
Vincent Ladeuil (vila) wrote :

Wow, nice work and numerous reviews already, but still, this has not landed...

This is certainly related to the size of the patch so splitting it into several parts will help, but since your have already invested quite a bunch of time on it, I suspect you won't be rejoiced by such a proposal :-/

Anyway, I have some high level remarks:

- if you want to split it anyway (who knows ;) the configuration part and the command behaviour modifications sounds like good candidates,

- you use a FakeConfig for some tests which is a bit risky, especially considering that you didn't take the {option} syntax into account as suggested by poolie (and I agree this would be better than using the obscure %B %t %o options). I think that configuring such tools is rarely done and will most of the time be done by copying an existing one. In this context having verbose options means you don't have to search the documentation for the meaning of the short ones. I won't block on that though as I'd like this syntax to be supported by bzrlib.config and that's outside the scope of your proposal.

- like John, I think there is some overlapping with the ``bzr config`` command and that will be a maintenance burden down the road, mergetools can keep the list and detect subcommands, also note that checking the external merge tool availability at this point won't fly, you are forbidding users to configure them under special setups and you still need to check their availability when you *use* them. Also, by using special commands to add/modify/delete there you also forbid setting such tools in more specific ways in locations.conf and branch.conf (which I want to allow for *any* option in the new config scheme),

- you said that the merge tool is called for text conflicts but I see no tests that you don't try to call it for other types of conflicts,

- bzrlib.mergetools.py use DOS line endings, please use Unix line endings (and watch for line with only spaces :-/),

- regarding docs, most of the cmd_mergetools docstring should be part of configuration doc, that's where people are directed for questions about config options,

- _KOWN_MERGE_TOOLS really want to be a registry :)

I hope I'm sounding too negative here, I really think this proposal is valuable and I will do my best to help you land it.

review: Needs Fixing
Revision history for this message
Gordon Tyler (doxxx) wrote :
Download full text (3.9 KiB)

Thanks for the review, Vincent. :)

On Thu, November 11, 2010 11:32 am, Vincent Ladeuil wrote:
> This is certainly related to the size of the patch so splitting it into
> several parts will help, but since your have already invested quite a
> bunch of time on it, I suspect you won't be rejoiced by such a proposal
> :-/

I'm not sure how I would actually go about splitting it. Branch from
lp:~doxxx/bzr/mergetools and then remove the extra bits from
lp:~doxxx/bzr/mergetools?

> - if you want to split it anyway (who knows ;) the configuration part and
> the command behaviour modifications sounds like good candidates,

Agreed.

> - you use a FakeConfig for some tests which is a bit risky, especially
> considering that you didn't take the {option} syntax into account as
> suggested by poolie (and I agree this would be better than using the
> obscure %B %t %o options). I think that configuring such tools is rarely
> done and will most of the time be done by copying an existing one. In this
> context having verbose options means you don't have to search the
> documentation for the meaning of the short ones. I won't block on that
> though as I'd like this syntax to be supported by bzrlib.config and that's
> outside the scope of your proposal.

I think I wrote those tests before I discovered that TestCaseInTempDir
would isolate tests from changing the user's real config. I'll try
changing the tests to use a real config object.

Regarding verbose placeholders, I'm beginning to agree with you and other
reviewers. So, I can do the substitution myself for now and then once the
config object is capable of doing it, we can change mergetools to use
that?

> - like John, I think there is some overlapping with the ``bzr config``
> command and that will be a maintenance burden down the road, mergetools

The problem I have with using ``bzr config`` is that it requires that the
user understand the layout of the config file, that they must add the name
to one setting and then create another setting using that name which
contains the commandline. They need to get the quoting right and the
commas right, etc. This is not just some simple "name=value" setting like
email. Why can't we have both?

> can keep the list and detect subcommands, also note that checking the
> external merge tool availability at this point won't fly, you are
> forbidding users to configure them under special setups and you still need
> to check their availability when you *use* them. Also, by using special

Checking of availability only logs a warning if it's not available. I
think its useful because it warns them immediately so that they're not
surprised later when they're trying do resolve conflicts and they have to
stop what they're doing to go fix the configuration.

> commands to add/modify/delete there you also forbid setting such tools in
> more specific ways in locations.conf and branch.conf (which I want to
> allow for *any* option in the new config scheme),

That's a good point. I don't really have a good solution for that.

> - you said that the merge tool is called for text conflicts but I see no
> tests that you don't try to call it for other types of conflicts,

Not sure how to...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (6.0 KiB)

>>>>> Gordon Tyler <email address hidden> writes:

    > Thanks for the review, Vincent. :)
    > On Thu, November 11, 2010 11:32 am, Vincent Ladeuil wrote:
    >> This is certainly related to the size of the patch so splitting it into
    >> several parts will help, but since your have already invested quite a
    >> bunch of time on it, I suspect you won't be rejoiced by such a proposal
    >> :-/

    > I'm not sure how I would actually go about splitting it. Branch from
    > lp:~doxxx/bzr/mergetools and then remove the extra bits from
    > lp:~doxxx/bzr/mergetools?

Whatever works :) The above is ok. Then you'll have to decide whether
you want to regularly merge from one branch to the other, use a pipeline
or a loom. And don't forget to set the pre-requisite branch when submitting.

    >> - if you want to split it anyway (who knows ;) the configuration part and
    >> the command behaviour modifications sounds like good candidates,

    > Agreed.

Great.

    >> - you use a FakeConfig for some tests which is a bit risky, especially
    >> considering that you didn't take the {option} syntax into account as
    >> suggested by poolie (and I agree this would be better than using the
    >> obscure %B %t %o options). I think that configuring such tools is rarely
    >> done and will most of the time be done by copying an existing one. In this
    >> context having verbose options means you don't have to search the
    >> documentation for the meaning of the short ones. I won't block on that
    >> though as I'd like this syntax to be supported by bzrlib.config and that's
    >> outside the scope of your proposal.

    > I think I wrote those tests before I discovered that TestCaseInTempDir
    > would isolate tests from changing the user's real config. I'll try
    > changing the tests to use a real config object.

Ok.

    > Regarding verbose placeholders, I'm beginning to agree with you
    > and other reviewers. So, I can do the substitution myself for now
    > and then once the config object is capable of doing it, we can
    > change mergetools to use that?

Hehe, chiken-and-egg problem :)

All things being equal, I think implementing it directly into config may
be require less work overall, but let's not introduce more dependencies
there, postpone this precise point and we'll see what is available when
you come to it (configobj provides the necessary hooks and most of the
infrastructure, including the common errors (definition loops, undefined
option) so no need to re-implement from scratch). I still need to submit
my braindump on the new config scheme I envision where I've already
describe some related stuff. I should clean it up and submit it
tomorrow.

    >> - like John, I think there is some overlapping with the ``bzr config``
    >> command and that will be a maintenance burden down the road, mergetools

    > The problem I have with using ``bzr config`` is that it requires that the
    > user understand the layout of the config file, that they must add the name
    > to one setting and then create another setting using that name which
    > contains the commandline. They need to get the quoting right and the
    > commas right...

Read more...

Revision history for this message
Gary van der Merwe (garyvdm) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/11/2010 20:19, Vincent Ladeuil wrote:
> How about using option names like 'bzr.mergetool.bcompare'
> 'bzr.mergetool.kdiff3' and so on ? Then we could query the config object
> for option whose names start with 'bzr.mergetool'. Would this address
> some of your concerns ?

I was thinking it may be better to have the config formated like this:

[DEFAULT]
default_mergetool = meld

[MERGETOOLS]
meld = /usr/bin/meld %t %r %o
kdiff3 = ....

Similar to [ALIASES], bzr-bookmark's [BOOKMARKS], qbzr's [EXTDIFF]. But
unfortunately bzr config does not provide access to view/change these.
It would nice if it could.

Gary

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkzcO8QACgkQd/3EdwGKOh0mvQCcD7G/Rjk9TK9Q4NNs/2wtazLx
H94AnA2djx2oIhd1+fnxUlf3wB7mm5sZ
=oRR8
-----END PGP SIGNATURE-----

Revision history for this message
Gordon Tyler (doxxx) wrote :

On Thu, November 11, 2010 1:19 pm, Vincent Ladeuil wrote:
> > I'm not sure how I would actually go about splitting it. Branch from
> > lp:~doxxx/bzr/mergetools and then remove the extra bits from
> > lp:~doxxx/bzr/mergetools?
>
> Whatever works :) The above is ok. Then you'll have to decide whether
> you want to regularly merge from one branch to the other, use a pipeline
> or a loom. And don't forget to set the pre-requisite branch when
> submitting.

Won't the removal of extra bits in the original get merged into the second
when I try and merge new changes in the original into the second?

> All things being equal, I think implementing it directly into config may
> be require less work overall, but let's not introduce more dependencies
> there, postpone this precise point and we'll see what is available when
> you come to it (configobj provides the necessary hooks and most of the
> infrastructure, including the common errors (definition loops, undefined
> option) so no need to re-implement from scratch). I still need to submit
> my braindump on the new config scheme I envision where I've already
> describe some related stuff. I should clean it up and submit it
> tomorrow.

Okay, leaving it as is then.

> How about using option names like 'bzr.mergetool.bcompare'
> 'bzr.mergetool.kdiff3' and so on ? Then we could query the config object
> for option whose names start with 'bzr.mergetool'. Would this address
> some of your concerns ?

I already create option names like that but I didn't see a way to query
for options whose names start with a prefix, thus the separate list
option. I'll take another look at the config stuff to see how to do this.

But yeah, if I can get this working then it does become more
user-editable, in which case I would be okay with removing the --add,
--update and --remove options from the mergetools command.

Should these options go into a different section in the config file? Or
would that prevent putting them in locations.conf and branch.conf?

What about tool names which have "funny" characters in them like spaces or
other stuff? What's legal in an option name?

> isinstance(conflict, conflicts.TextConflict) to filter only text
> conflicts, bt.test_conflicts, bb.test_conflicts should contain almost
> all the tests related to conflicts otherwise.

Thanks.

> Right, my editor does to, but if we can keep a coherent line ending in
> the code base, that would be better.

Will do.

> Well, two places for the same doc mean they should be both maintained,
> that's error-prone. All other config options are described
> bzrlib/help_topics/en/configuration.txt, it makes our life easier when
> we want to point users to this place.

Okay.

> >> - _KOWN_MERGE_TOOLS really want to be a registry :)
>
> > I'll have a look at that.
>
> May be not :) If you rely on a naming rule for that...

It's just a place to list the names and command-lines of merge tools that
we already know about, so that we can try detect them on the path. Is a
registry suitable for that?

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> Gary van der Merwe <email address hidden> writes:

    > On 11/11/2010 20:19, Vincent Ladeuil wrote:
    >> How about using option names like 'bzr.mergetool.bcompare'
    >> 'bzr.mergetool.kdiff3' and so on ? Then we could query the config object
    >> for option whose names start with 'bzr.mergetool'. Would this address
    >> some of your concerns ?

    > I was thinking it may be better to have the config formated like this:

    > [DEFAULT]
    > default_mergetool = meld

    > [MERGETOOLS]
    > meld = /usr/bin/meld %t %r %o
    > kdiff3 = ....

    > Similar to [ALIASES], bzr-bookmark's [BOOKMARKS], qbzr's [EXTDIFF]. But
    > unfortunately bzr config does not provide access to view/change these.
    > It would nice if it could.

bzr config *intentionally* (so far) doesn't provide access to sections
because I thought we had to chose between using section names as path
(like in locations.conf) or arbitrary strings (like in bazaar.conf).

And I still think that we should promote the use of paths in all
configuration files, for compatibility we could still allows ALIASES and
BOOKMARKS but I think this kind of section can be embedded in the option
name instead. So your example above can *today* be written:

bzr.mergetool.default = meld
bzr.mergetool.meld = /usr/bin/meld %t %r %o
bzr.mergetool.kdiff3 = ...

and doing so allows *today* to have different definitions in
bazaar.conf, locations.conf and branch.conf.

Plugins would use their own dedicated name space:

qbzr.log.window_size = ...
qbzr.annotate.window_size = ...

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (3.2 KiB)

>>>>> Gordon Tyler <email address hidden> writes:

    > On Thu, November 11, 2010 1:19 pm, Vincent Ladeuil wrote:
    >> > I'm not sure how I would actually go about splitting it. Branch from
    >> > lp:~doxxx/bzr/mergetools and then remove the extra bits from
    >> > lp:~doxxx/bzr/mergetools?
    >>
    >> Whatever works :) The above is ok. Then you'll have to decide whether
    >> you want to regularly merge from one branch to the other, use a pipeline
    >> or a loom. And don't forget to set the pre-requisite branch when
    >> submitting.

    > Won't the removal of extra bits in the original get merged into the second
    > when I try and merge new changes in the original into the second?

You have to create the second branch on top of the first one of course.

    > I already create option names like that but I didn't see a way to
    > query for options whose names start with a prefix, thus the
    > separate list option. I'll take another look at the config stuff
    > to see how to do this.

Look at the cmd_config implementation, _show_matching_options implements
such a filtering (with name='^bzr.mergetool.'), if you need it we should
certainly plan to make it available in a form reusable for your use
case, unless we provide a way to access such a set of options as a dict
(this will complicate the model (as in: should the dict be defined for
*each* config file OR be the union of the definitions in all config
files or something else)) but I think it's too soon to wander into such
complications now.

    > But yeah, if I can get this working then it does become more
    > user-editable, in which case I would be okay with removing the --add,
    > --update and --remove options from the mergetools command.

Cool.

    > Should these options go into a different section in the config
    > file? Or would that prevent putting them in locations.conf and
    > branch.conf?

Yes, using sections for that conflicts with using section names for
paths (see my other comment).

    > What about tool names which have "funny" characters in them like
    > spaces or other stuff? What's legal in an option name?

I haven't checked precisely what is legal and what is not, but python
identifiers are :) So no spaces, no funny names. I don't think it's a
problem.

<snip/>

    >> >> - _KOWN_MERGE_TOOLS really want to be a registry :)
    >>
    >> > I'll have a look at that.
    >>
    >> May be not :) If you rely on a naming rule for that...

    > It's just a place to list the names and command-lines of merge
    > tools that we already know about, so that we can try detect them
    > on the path. Is a registry suitable for that?

A registry will give you a place to store the default values that you
don't want to put in bazaar.conf and *this* is not implement today in
bzrlib.config.I'd like to have a config-like object that is *not*
associated to a file on disk but to a set of objects declarations so you
can say, for example:

kdiff3 = ConfigOption('bzr.mergetool.kdiff3, 'kdiff3 %b %t %o -o %r',
                      '''How kdiff3 should be called to ..blah.''')

and bzr config bzr.mergetool will display:
kdiff3 %b %t %o -o %r

i.e. this will define the...

Read more...

Revision history for this message
Gordon Tyler (doxxx) wrote :

Okay, I think I've managed to extract all modifications to bzr commands. I've also made a few changes as suggested by Vincent.

Revision history for this message
Vincent Ladeuil (vila) wrote :

@Gordon: Don't forget to put the state to NeedsReview when you feel ready for it.

Revision history for this message
Gordon Tyler (doxxx) wrote :

On 11/13/2010 6:08 AM, Vincent Ladeuil wrote:
> @Gordon: Don't forget to put the state to NeedsReview when you feel ready for it.

I still need to double-check the stuff we discussed to make sure I
didn't miss anything. :)

Revision history for this message
Marius Kruger (amanica) wrote :

On 11 November 2010 19:07, Gordon Tyler <email address hidden> wrote:
> On Thu, November 11, 2010 11:32 am, Vincent Ladeuil wrote:
>> - bzrlib.mergetools.py use DOS line endings, please use Unix line endings
>> (and watch for line with only spaces :-/),
>
> Sorry, I didn't know that it mattered. My editor is capable of reading DOS
> and Unix line endings, so I don't really notice whether its one or the
> other. I'll fix it and check if it has a trim spaces option (I think it
> does but I disabled it because somebody else complained a while back about
> spurious changes due to whitespace stripping).

You should not turn the trim spaces option on because then you will be
making spurious changes everywhere (unless it can be set to just trim
the lines you are editing). Currently we allow trailing whitespace but
we don't allow spurious changes allover the place. As far as I know
the rule is that reviewers should not complain about it because its
not important.

--
<>< Marius ><>

Revision history for this message
Gordon Tyler (doxxx) wrote :

On 11/15/2010 10:26 AM, Marius Kruger wrote:
> On 11 November 2010 19:07, Gordon Tyler <email address hidden> wrote:
>> On Thu, November 11, 2010 11:32 am, Vincent Ladeuil wrote:
>>> - bzrlib.mergetools.py use DOS line endings, please use Unix line endings
>>> (and watch for line with only spaces :-/),
>>
>> Sorry, I didn't know that it mattered. My editor is capable of reading DOS
>> and Unix line endings, so I don't really notice whether its one or the
>> other. I'll fix it and check if it has a trim spaces option (I think it
>> does but I disabled it because somebody else complained a while back about
>> spurious changes due to whitespace stripping).
>
> You should not turn the trim spaces option on because then you will be
> making spurious changes everywhere (unless it can be set to just trim
> the lines you are editing). Currently we allow trailing whitespace but
> we don't allow spurious changes allover the place. As far as I know
> the rule is that reviewers should not complain about it because its
> not important.

Yeah, I discovered this and turned off the option and restored the
whitespace to what it was. I'll just have to be careful about leaving
spaces on empty lines.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/help_topics/en/configuration.txt'
2--- bzrlib/help_topics/en/configuration.txt 2010-09-29 05:35:26 +0000
3+++ bzrlib/help_topics/en/configuration.txt 2010-11-12 03:34:57 +0000
4@@ -584,3 +584,36 @@
5 If present, defines the ``--strict`` option default value for checking
6 uncommitted changes before sending a merge directive.
7
8+
9+External Merge Tools
10+--------------------
11+
12+mergetools.<name>
13+~~~~~~~~~~~~~~~~~
14+
15+Defines an external merge tool called <name> with the given command-line.
16+Arguments containing spaces should be quoted using single or double quotes. The
17+executable may omit its path if it can be found on the PATH.
18+
19+The following markers can be used in the command-line to substitute filenames
20+involved in the merge conflict:
21+
22+%b -> file.BASE
23+%t -> file.THIS
24+%o -> file.OTHER
25+%r -> file (output)
26+%T -> file.THIS (temp copy, used to overwrite "file" if merge succeeds)
27+
28+For example:
29+
30+ mergetools.kdiff3 = kdiff3 %b %t %o -o %r
31+
32+default_mergetool
33+~~~~~~~~~~~~~~~~~
34+
35+Specifies which external merge tool (as defined above) should be selected by
36+default in tools such as ``bzr qconflicts``.
37+
38+For example:
39+
40+ default_mergetool = kdiff3
41
42=== added file 'bzrlib/mergetools.py'
43--- bzrlib/mergetools.py 1970-01-01 00:00:00 +0000
44+++ bzrlib/mergetools.py 2010-11-12 03:34:57 +0000
45@@ -0,0 +1,317 @@
46+# Copyright (C) 2010 Canonical Ltd.
47+#
48+# This program is free software; you can redistribute it and/or modify
49+# it under the terms of the GNU General Public License as published by
50+# the Free Software Foundation; either version 2 of the License, or
51+# (at your option) any later version.
52+#
53+# This program is distributed in the hope that it will be useful,
54+# but WITHOUT ANY WARRANTY; without even the implied warranty of
55+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
56+# GNU General Public License for more details.
57+#
58+# You should have received a copy of the GNU General Public License
59+# along with this program; if not, write to the Free Software
60+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
61+
62+"""Registry for external merge tools, e.g. kdiff3, meld, etc."""
63+
64+import os
65+import shutil
66+import subprocess
67+import sys
68+import tempfile
69+
70+from bzrlib.lazy_import import lazy_import
71+lazy_import(globals(), """
72+from bzrlib import (
73+ cmdline,
74+ config,
75+ errors,
76+ trace,
77+ ui,
78+ workingtree,
79+)
80+""")
81+
82+from bzrlib.commands import Command
83+from bzrlib.option import Option
84+
85+
86+substitution_help = {
87+ u'%b' : u'file.BASE',
88+ u'%t' : u'file.THIS',
89+ u'%o' : u'file.OTHER',
90+ u'%r' : u'file (output)',
91+ u'%T' : u'file.THIS (temp copy, used to overwrite "file" if merge succeeds)'
92+}
93+
94+
95+def subprocess_invoker(executable, args, cleanup):
96+ retcode = subprocess.call([executable] + args)
97+ cleanup(retcode)
98+ return retcode
99+
100+
101+_WIN32_PATH_EXT = [unicode(ext.lower())
102+ for ext in os.getenv('PATHEXT', '').split(';')]
103+
104+
105+def tool_name_from_executable(executable):
106+ name = os.path.basename(executable)
107+ if sys.platform == 'win32':
108+ root, ext = os.path.splitext(name)
109+ if ext.lower() in _WIN32_PATH_EXT:
110+ name = root
111+ return name
112+
113+
114+class MergeTool(object):
115+ def __init__(self, name, commandline):
116+ """Initializes the merge tool with a name and a command-line (a string
117+ or sequence of strings).
118+ """
119+ self.set_commandline(commandline)
120+ self.set_name(name) # needs commandline set first when name is None
121+
122+ def __repr__(self):
123+ return '<MergeTool %s: %r>' % (self._name, self._commandline)
124+
125+ def __eq__(self, other):
126+ if type(other) == MergeTool:
127+ return cmp(self, other) == 0
128+ else:
129+ return False
130+
131+ def __ne__(self, other):
132+ if type(other) == MergeTool:
133+ return cmp(self, other) != 0
134+ else:
135+ return True
136+
137+ def __cmp__(self, other):
138+ if type(other == MergeTool):
139+ return cmp((self._name, self._commandline),
140+ (other._name, other._commandline))
141+
142+ def __str__(self):
143+ return self.get_commandline()
144+
145+ def get_name(self):
146+ return self._name
147+
148+ def set_name(self, name):
149+ if name is None:
150+ self._name = tool_name_from_executable(self.get_executable())
151+ else:
152+ self._name = name
153+
154+ def get_commandline(self, quote=False):
155+ if quote:
156+ args = _quote_args(self._commandline)
157+ else:
158+ args = self._commandline
159+ return u' '.join(args)
160+
161+ def get_commandline_as_list(self):
162+ return self._commandline
163+
164+ def set_commandline(self, commandline):
165+ if isinstance(commandline, basestring):
166+ self._commandline = cmdline.split(commandline)
167+ elif isinstance(commandline, (tuple, list)):
168+ self._commandline = list(commandline)
169+ else:
170+ raise TypeError('%r is not valid for commandline; must be string '
171+ 'or sequence of strings' % commandline)
172+
173+ def get_executable(self):
174+ if len(self._commandline) < 1:
175+ return u''
176+ return self._commandline[0]
177+
178+ def set_executable(self, executable):
179+ self._commandline[:1] = [executable]
180+
181+ def is_available(self):
182+ executable = self.get_executable()
183+ return os.path.exists(executable) or _find_executable(executable)
184+
185+ def invoke(self, filename, invoker=None):
186+ if invoker is None:
187+ invoker = subprocess_invoker
188+ args, tmp_file = self._subst_filename(self._commandline, filename)
189+ def cleanup(retcode):
190+ if tmp_file is not None:
191+ if retcode == 0: # on success, replace file with temp file
192+ shutil.move(tmp_file, filename)
193+ else: # otherwise, delete temp file
194+ os.remove(tmp_file)
195+ return invoker(args[0], args[1:], cleanup)
196+
197+ def _subst_filename(self, args, filename):
198+ tmp_file = None
199+ subst_args = []
200+ for arg in args:
201+ arg = arg.replace(u'%b', filename + u'.BASE')
202+ arg = arg.replace(u'%t', filename + u'.THIS')
203+ arg = arg.replace(u'%o', filename + u'.OTHER')
204+ arg = arg.replace(u'%r', filename)
205+ if u'%T' in arg:
206+ tmp_file = tempfile.mktemp(u"_bzr_mergetools_%s.THIS" %
207+ os.path.basename(filename))
208+ shutil.copy(filename + u".THIS", tmp_file)
209+ arg = arg.replace(u'%T', tmp_file)
210+ subst_args.append(arg)
211+ return subst_args, tmp_file
212+
213+
214+_KNOWN_MERGE_TOOLS = (
215+ u'bcompare %t %o %b %r',
216+ u'kdiff3 %b %t %o -o %r',
217+ u'xxdiff -m -O -M %r %t %b %o',
218+ u'meld %b %T %o',
219+ u'opendiff %t %o -ancestor %b -merge %r',
220+ u'winmergeu %r',
221+)
222+
223+
224+def detect_merge_tools():
225+ tools = [MergeTool(None, commandline) for commandline in _KNOWN_MERGE_TOOLS]
226+ return [tool for tool in tools if tool.is_available()]
227+
228+
229+def get_merge_tools(conf=None):
230+ """Returns list of MergeTool objects."""
231+ if conf is None:
232+ conf = config.GlobalConfig()
233+ tools = []
234+ for (oname, value, section, conf_id) in conf._get_options():
235+ if oname.startswith('mergetools.'):
236+ tools.append(MergeTool(oname[len('mergetools.'):], value))
237+ return tools
238+
239+
240+def set_merge_tools(merge_tools, conf=None):
241+ if conf is None:
242+ conf = config.GlobalConfig()
243+ # remove entries from config for tools which do not appear in merge_tools
244+ tool_names = [tool.get_name() for tool in merge_tools]
245+ for (oname, value, section, conf_id) in conf._get_options():
246+ if oname.startswith('mergetools.'):
247+ if oname[len('mergetools.'):] not in tool_names:
248+ conf.remove_user_option(oname)
249+ # set config entries
250+ for tool in merge_tools:
251+ oname = 'mergetools.%s' % tool.get_name()
252+ value = tool.get_commandline(quote=True)
253+ if oname == '' or value == '':
254+ continue
255+ conf.set_user_option(oname, value)
256+
257+
258+def find_merge_tool(name, conf=None):
259+ if conf is None:
260+ conf = config.GlobalConfig()
261+ merge_tools = get_merge_tools(conf)
262+ for merge_tool in merge_tools:
263+ if merge_tool.get_name() == name:
264+ return merge_tool
265+ return None
266+
267+
268+def find_first_available_merge_tool(conf=None):
269+ if conf is None:
270+ conf = config.GlobalConfig()
271+ merge_tools = get_merge_tools(conf)
272+ for merge_tool in merge_tools:
273+ if merge_tool.is_available():
274+ return merge_tool
275+ return None
276+
277+
278+def get_default_merge_tool(conf=None):
279+ if conf is None:
280+ conf = config.GlobalConfig()
281+ name = conf.get_user_option('default_mergetool')
282+ if name is None:
283+ trace.mutter('no default merge tool defined')
284+ return None
285+ merge_tool = find_merge_tool(name, conf)
286+ trace.mutter('found default merge tool: %r', merge_tool)
287+ return merge_tool
288+
289+
290+def set_default_merge_tool(name, conf=None):
291+ if conf is None:
292+ conf = config.GlobalConfig()
293+ if name is None:
294+ conf.remove_user_option('default_mergetool')
295+ else:
296+ if isinstance(name, MergeTool):
297+ name = name.get_name()
298+ if find_merge_tool(name, conf) is None:
299+ raise errors.BzrError('invalid merge tool name: %r' % name)
300+ trace.mutter('setting default merge tool: %s', name)
301+ conf.set_user_option('default_mergetool', name)
302+
303+
304+def resolve_using_merge_tool(tool_name, conflicts):
305+ merge_tool = find_merge_tool(tool_name)
306+ if merge_tool is None:
307+ available = '\n '.join([mt.get_name() for mt in get_merge_tools()
308+ if mt.is_available()])
309+ raise errors.BzrCommandError('Unrecognized merge tool: %s\n\n'
310+ 'Available merge tools:\n'
311+ ' %s' % (tool_name, available))
312+ for conflict in conflicts:
313+ merge_tool.invoke(conflict.path)
314+
315+
316+def _quote_args(args):
317+ return [_quote_arg(arg) for arg in args]
318+
319+
320+def _quote_arg(arg):
321+ if u' ' in arg and not _is_arg_quoted(arg):
322+ return u'"%s"' % _escape_quotes(arg)
323+ else:
324+ return arg
325+
326+
327+def _is_arg_quoted(arg):
328+ return (arg[0] == u"'" and arg[-1] == u"'") or \
329+ (arg[0] == u'"' and arg[-1] == u'"')
330+
331+
332+def _escape_quotes(arg):
333+ return arg.replace(u'"', u'\\"')
334+
335+
336+# courtesy of 'techtonik' at http://snippets.dzone.com/posts/show/6313
337+def _find_executable(executable, path=None):
338+ """Try to find 'executable' in the directories listed in 'path' (a
339+ string listing directories separated by 'os.pathsep'; defaults to
340+ os.environ['PATH']). Returns the complete filename or None if not
341+ found
342+ """
343+ if path is None:
344+ path = os.environ['PATH']
345+ paths = path.split(os.pathsep)
346+ extlist = ['']
347+ if sys.platform == 'win32':
348+ pathext = os.environ['PATHEXT'].lower().split(os.pathsep)
349+ (base, ext) = os.path.splitext(executable)
350+ if ext.lower() not in pathext:
351+ extlist = pathext
352+ for ext in extlist:
353+ execname = executable + ext
354+ if os.path.isfile(execname):
355+ return execname
356+ else:
357+ for p in paths:
358+ f = os.path.join(p, execname)
359+ if os.path.isfile(f):
360+ return f
361+ else:
362+ return None
363
364=== modified file 'bzrlib/tests/__init__.py'
365--- bzrlib/tests/__init__.py 2010-10-18 17:06:37 +0000
366+++ bzrlib/tests/__init__.py 2010-11-12 03:34:57 +0000
367@@ -3748,6 +3748,7 @@
368 'bzrlib.tests.test_merge3',
369 'bzrlib.tests.test_merge_core',
370 'bzrlib.tests.test_merge_directive',
371+ 'bzrlib.tests.test_mergetools',
372 'bzrlib.tests.test_missing',
373 'bzrlib.tests.test_msgeditor',
374 'bzrlib.tests.test_multiparent',
375
376=== added file 'bzrlib/tests/test_mergetools.py'
377--- bzrlib/tests/test_mergetools.py 1970-01-01 00:00:00 +0000
378+++ bzrlib/tests/test_mergetools.py 2010-11-12 03:34:57 +0000
379@@ -0,0 +1,317 @@
380+# Copyright (C) 2010 Canonical Ltd
381+#
382+# This program is free software; you can redistribute it and/or modify
383+# it under the terms of the GNU General Public License as published by
384+# the Free Software Foundation; either version 2 of the License, or
385+# (at your option) any later version.
386+#
387+# This program is distributed in the hope that it will be useful,
388+# but WITHOUT ANY WARRANTY; without even the implied warranty of
389+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
390+# GNU General Public License for more details.
391+#
392+# You should have received a copy of the GNU General Public License
393+# along with this program; if not, write to the Free Software
394+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
395+
396+import os
397+import re
398+import sys
399+
400+from bzrlib import (
401+ config,
402+ mergetools,
403+ tests
404+)
405+from bzrlib.tests.features import backslashdir_feature
406+
407+
408+class TestBasics(tests.TestCase):
409+ def setUp(self):
410+ super(TestBasics, self).setUp()
411+ self.tool = mergetools.MergeTool('sometool',
412+ '/path/to/tool --opt %b -x %t %o --stuff %r')
413+
414+ def test_get_commandline(self):
415+ self.assertEqual('/path/to/tool --opt %b -x %t %o --stuff %r',
416+ self.tool.get_commandline())
417+
418+ def test_get_commandline_as_list(self):
419+ self.assertEqual(['/path/to/tool', '--opt', '%b', '-x', '%t', '%o',
420+ '--stuff', '%r'],
421+ self.tool.get_commandline_as_list())
422+
423+ def test_get_executable(self):
424+ self.assertEqual('/path/to/tool', self.tool.get_executable())
425+
426+ def test_get_name(self):
427+ self.assertEqual('sometool', self.tool.get_name())
428+
429+ def test_set_name(self):
430+ self.tool.set_name('bettertool')
431+ self.assertEqual('bettertool', self.tool.get_name())
432+
433+ def test_set_name_none(self):
434+ self.tool.set_name(None)
435+ self.assertEqual('tool', self.tool.get_name())
436+
437+ def test_set_commandline(self):
438+ self.tool.set_commandline('/new/path/to/bettertool %b %t %o %r')
439+ self.assertEqual(['/new/path/to/bettertool', '%b', '%t', '%o', '%r'],
440+ self.tool.get_commandline_as_list())
441+
442+ def test_set_executable(self):
443+ self.tool.set_executable('othertool')
444+ self.assertEqual(['othertool', '--opt', '%b', '-x', '%t', '%o',
445+ '--stuff', '%r'],
446+ self.tool.get_commandline_as_list())
447+
448+ def test_quoted_executable(self):
449+ self.requireFeature(backslashdir_feature)
450+ self.tool.set_commandline(
451+ '"C:\\Program Files\\KDiff3\\kdiff3.exe" %b %t %o -o %r')
452+ self.assertEqual('C:\\Program Files\\KDiff3\\kdiff3.exe',
453+ self.tool.get_executable())
454+
455+ def test_comparison(self):
456+ other_tool = mergetools.MergeTool('sometool',
457+ '/path/to/tool --opt %b -x %t %o --stuff %r')
458+ self.assertTrue(self.tool == other_tool)
459+ self.assertTrue(self.tool != None)
460+
461+ def test_comparison_none(self):
462+ self.assertFalse(self.tool == None)
463+ self.assertTrue(self.tool != None)
464+
465+ def test_comparison_fail_name(self):
466+ other_tool = mergetools.MergeTool('sometoolx',
467+ '/path/to/tool --opt %b -x %t %o --stuff %r')
468+ self.assertFalse(self.tool == other_tool)
469+ self.assertTrue(self.tool != other_tool)
470+
471+ def test_comparison_fail_commandline(self):
472+ other_tool = mergetools.MergeTool('sometool',
473+ '/path/to/tool --opt %b -x %t %o --stuff %r extra')
474+ self.assertFalse(self.tool == other_tool)
475+ self.assertTrue(self.tool != other_tool)
476+
477+
478+class TestUnicodeBasics(tests.TestCase):
479+ def setUp(self):
480+ super(TestUnicodeBasics, self).setUp()
481+ self.tool = mergetools.MergeTool(u'someb\u0414r',
482+ u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r')
483+
484+ def test_get_commandline(self):
485+ self.assertEqual(u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r',
486+ self.tool.get_commandline())
487+
488+ def test_get_commandline_as_list(self):
489+ self.assertEqual([u'/path/to/b\u0414r', u'--opt', u'%b', u'-x', u'%t',
490+ u'%o', u'--stuff', u'%r'],
491+ self.tool.get_commandline_as_list())
492+
493+ def test_get_executable(self):
494+ self.assertEqual(u'/path/to/b\u0414r', self.tool.get_executable())
495+
496+ def test_get_name(self):
497+ self.assertEqual(u'someb\u0414r', self.tool.get_name())
498+
499+ def test_set_name(self):
500+ self.tool.set_name(u'betterb\u0414r')
501+ self.assertEqual(u'betterb\u0414r', self.tool.get_name())
502+
503+ def test_set_name_none(self):
504+ self.tool.set_name(None)
505+ self.assertEqual(u'b\u0414r', self.tool.get_name())
506+
507+ def test_set_commandline(self):
508+ self.tool.set_commandline(u'/new/path/to/betterb\u0414r %b %t %o %r')
509+ self.assertEqual([u'/new/path/to/betterb\u0414r', u'%b', u'%t', u'%o',
510+ u'%r'],
511+ self.tool.get_commandline_as_list())
512+
513+ def test_set_executable(self):
514+ self.tool.set_executable(u'otherb\u0414r')
515+ self.assertEqual([u'otherb\u0414r', u'--opt', u'%b', u'-x', u'%t',
516+ u'%o', u'--stuff', u'%r'],
517+ self.tool.get_commandline_as_list())
518+
519+ def test_quoted_executable(self):
520+ self.requireFeature(backslashdir_feature)
521+ self.tool.set_commandline(
522+ u'"C:\\Program Files\\KDiff3\\b\u0414r.exe" %b %t %o -o %r')
523+ self.assertEqual(u'C:\\Program Files\\KDiff3\\b\u0414r.exe',
524+ self.tool.get_executable())
525+
526+ def test_comparison(self):
527+ other_tool = mergetools.MergeTool(u'someb\u0414r',
528+ u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r')
529+ self.assertTrue(self.tool == other_tool)
530+ self.assertFalse(self.tool != other_tool)
531+
532+ def test_comparison_none(self):
533+ self.assertFalse(self.tool == None)
534+ self.assertTrue(self.tool != None)
535+
536+ def test_comparison_fail_name(self):
537+ other_tool = mergetools.MergeTool(u'someb\u0414rx',
538+ u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r')
539+ self.assertFalse(self.tool == other_tool)
540+ self.assertTrue(self.tool != other_tool)
541+
542+ def test_comparison_fail_commandline(self):
543+ other_tool = mergetools.MergeTool(u'someb\u0414r',
544+ u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r extra')
545+ self.assertFalse(self.tool == other_tool)
546+ self.assertTrue(self.tool != other_tool)
547+
548+
549+class TestMergeToolOperations(tests.TestCaseInTempDir):
550+ def test_filename_substitution(self):
551+ def dummy_invoker(executable, args, cleanup):
552+ self._commandline = [executable] + args
553+ cleanup(0)
554+ mt = mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r')
555+ mt.invoke('test.txt', dummy_invoker)
556+ self.assertEqual(
557+ ['kdiff3',
558+ 'test.txt.BASE',
559+ 'test.txt.THIS',
560+ 'test.txt.OTHER',
561+ '-o',
562+ 'test.txt'],
563+ self._commandline)
564+ mt.invoke('file with space.txt', dummy_invoker)
565+ self.assertEqual(
566+ ['kdiff3',
567+ "file with space.txt.BASE",
568+ "file with space.txt.THIS",
569+ "file with space.txt.OTHER",
570+ '-o',
571+ "file with space.txt"],
572+ self._commandline)
573+ mt.invoke('file with "space and quotes".txt', dummy_invoker)
574+ self.assertEqual(
575+ ['kdiff3',
576+ "file with \"space and quotes\".txt.BASE",
577+ "file with \"space and quotes\".txt.THIS",
578+ "file with \"space and quotes\".txt.OTHER",
579+ '-o',
580+ "file with \"space and quotes\".txt"],
581+ self._commandline)
582+
583+ def test_expand_commandline_tempfile(self):
584+ def dummy_invoker(executable, args, cleanup):
585+ self.assertEqual('some_tool', executable)
586+ self.failUnlessExists(args[0])
587+ cleanup(0)
588+ self._tmp_file = args[0]
589+ self.build_tree(('test.txt', 'test.txt.BASE', 'test.txt.THIS',
590+ 'test.txt.OTHER'))
591+ mt = mergetools.MergeTool('some_tool', 'some_tool %T')
592+ mt.invoke('test.txt', dummy_invoker)
593+ self.failIfExists(self._tmp_file)
594+
595+ def test_is_available_full_tool_path(self):
596+ mt = mergetools.MergeTool(None, sys.executable)
597+ self.assertTrue(mt.is_available())
598+
599+ def test_is_available_tool_on_path(self):
600+ mt = mergetools.MergeTool(None, os.path.basename(sys.executable))
601+ self.assertTrue(mt.is_available())
602+
603+ def test_is_available_nonexistent(self):
604+ mt = mergetools.MergeTool(None, "ThisExecutableShouldReallyNotExist")
605+ self.assertFalse(mt.is_available())
606+
607+ def test_empty_commandline(self):
608+ mt = mergetools.MergeTool('', '')
609+ self.assertEqual('', mt.get_commandline())
610+
611+ def test_no_arguments(self):
612+ mt = mergetools.MergeTool('tool', 'tool')
613+ self.assertEqual('tool', mt.get_commandline())
614+
615+
616+class TestModuleFunctions(tests.TestCaseInTempDir):
617+ def test_get_merge_tools(self):
618+ conf = config.GlobalConfig()
619+ conf.set_user_option('mergetools.kdiff3', 'kdiff3 %b %t %o -o %r')
620+ conf.set_user_option('mergetools.winmergeu', 'winmergeu %r')
621+ conf.set_user_option('mergetools.funkytool', 'funkytool "arg with spaces" %T')
622+ tools = mergetools.get_merge_tools(conf)
623+ self.log(repr(tools))
624+ self.assertEqual(3, len(tools))
625+ self.assertEqual('kdiff3', tools[0].get_name())
626+ self.assertEqual('kdiff3 %b %t %o -o %r', tools[0].get_commandline())
627+ self.assertEqual('winmergeu', tools[1].get_name())
628+ self.assertEqual('winmergeu %r', tools[1].get_commandline())
629+ self.assertEqual('funkytool', tools[2].get_name())
630+ self.assertEqual('funkytool "arg with spaces" %T',
631+ tools[2].get_commandline(quote=True))
632+
633+ def test_set_merge_tools(self):
634+ conf = config.GlobalConfig()
635+ tools = [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
636+ mergetools.MergeTool('winmergeu', 'winmergeu %r'),
637+ mergetools.MergeTool('funkytool',
638+ 'funkytool "arg with spaces" %T')
639+ ]
640+ mergetools.set_merge_tools(tools, conf)
641+ self.assertEqual('funkytool "arg with spaces" %T',
642+ conf.get_user_option('mergetools.funkytool'))
643+ self.assertEqual('kdiff3 %b %t %o -o %r',
644+ conf.get_user_option('mergetools.kdiff3'))
645+ self.assertEqual('winmergeu %r',
646+ conf.get_user_option('mergetools.winmergeu'))
647+
648+ def test_set_merge_tools_duplicates(self):
649+ conf = config.GlobalConfig()
650+ mergetools.set_merge_tools(
651+ [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
652+ mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r')],
653+ conf)
654+ tools = mergetools.get_merge_tools(conf)
655+ self.assertEqual(1, len(tools))
656+ self.assertEqual('kdiff3', tools[0].get_name())
657+ self.assertEqual('kdiff3 %b %t %o -o %r', tools[0].get_commandline())
658+
659+ def test_set_merge_tools_empty_tool(self):
660+ conf = config.GlobalConfig()
661+ mergetools.set_merge_tools(
662+ [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
663+ mergetools.MergeTool('',''),
664+ mergetools.MergeTool('blah','')],
665+ conf)
666+ tools = mergetools.get_merge_tools(conf)
667+ self.assertEqual(1, len(tools))
668+ self.assertEqual('kdiff3', tools[0].get_name())
669+ self.assertEqual('kdiff3 %b %t %o -o %r', tools[0].get_commandline())
670+
671+ def test_set_merge_tools_remove_one(self):
672+ conf = config.GlobalConfig()
673+ tools = [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
674+ mergetools.MergeTool('winmergeu', 'winmergeu %r'),
675+ mergetools.MergeTool('funkytool',
676+ 'funkytool "arg with spaces" %T')
677+ ]
678+ mergetools.set_merge_tools(tools, conf)
679+ del tools[0]
680+ mergetools.set_merge_tools(tools, conf)
681+ self.assertEqual(sorted([
682+ ('mergetools.funkytool', 'funkytool "arg with spaces" %T', 'DEFAULT', 'bazaar'),
683+ ('mergetools.winmergeu', 'winmergeu %r', 'DEFAULT', 'bazaar'),
684+ ]),
685+ sorted(conf._get_options()))
686+
687+ def test_detect(self):
688+ # only way to reliably test detection is to add a known existing
689+ # executable to the list used for detection
690+ old_kmt = mergetools._KNOWN_MERGE_TOOLS
691+ mergetools._KNOWN_MERGE_TOOLS = ['sh', 'cmd']
692+ tools = mergetools.detect_merge_tools()
693+ tools_commandlines = [mt.get_commandline() for mt in tools]
694+ self.assertTrue('sh' in tools_commandlines or
695+ 'cmd' in tools_commandlines)
696+ mergetools._KNOWN_MERGE_TOOLS = old_kmt
697
698=== modified file 'doc/en/release-notes/bzr-2.3.txt'
699--- doc/en/release-notes/bzr-2.3.txt 2010-11-11 23:48:07 +0000
700+++ doc/en/release-notes/bzr-2.3.txt 2010-11-12 03:34:57 +0000
701@@ -62,8 +62,8 @@
702 API Changes
703 ***********
704
705-.. Changes that may require updates in plugins or other code that uses
706- bzrlib.
707+* Added ``bzrlib.mergetools`` module with helper functions for working with
708+ the list of external merge tools. (Gordon Tyler)
709
710 Internals
711 *********