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
=== modified file 'bzrlib/help_topics/en/configuration.txt'
--- bzrlib/help_topics/en/configuration.txt 2010-09-29 05:35:26 +0000
+++ bzrlib/help_topics/en/configuration.txt 2010-11-12 03:34:57 +0000
@@ -584,3 +584,36 @@
584If present, defines the ``--strict`` option default value for checking584If present, defines the ``--strict`` option default value for checking
585uncommitted changes before sending a merge directive.585uncommitted changes before sending a merge directive.
586586
587
588External Merge Tools
589--------------------
590
591mergetools.<name>
592~~~~~~~~~~~~~~~~~
593
594Defines an external merge tool called <name> with the given command-line.
595Arguments containing spaces should be quoted using single or double quotes. The
596executable may omit its path if it can be found on the PATH.
597
598The following markers can be used in the command-line to substitute filenames
599involved in the merge conflict:
600
601%b -> file.BASE
602%t -> file.THIS
603%o -> file.OTHER
604%r -> file (output)
605%T -> file.THIS (temp copy, used to overwrite "file" if merge succeeds)
606
607For example:
608
609 mergetools.kdiff3 = kdiff3 %b %t %o -o %r
610
611default_mergetool
612~~~~~~~~~~~~~~~~~
613
614Specifies which external merge tool (as defined above) should be selected by
615default in tools such as ``bzr qconflicts``.
616
617For example:
618
619 default_mergetool = kdiff3
587620
=== added file 'bzrlib/mergetools.py'
--- bzrlib/mergetools.py 1970-01-01 00:00:00 +0000
+++ bzrlib/mergetools.py 2010-11-12 03:34:57 +0000
@@ -0,0 +1,317 @@
1# Copyright (C) 2010 Canonical Ltd.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Registry for external merge tools, e.g. kdiff3, meld, etc."""
18
19import os
20import shutil
21import subprocess
22import sys
23import tempfile
24
25from bzrlib.lazy_import import lazy_import
26lazy_import(globals(), """
27from bzrlib import (
28 cmdline,
29 config,
30 errors,
31 trace,
32 ui,
33 workingtree,
34)
35""")
36
37from bzrlib.commands import Command
38from bzrlib.option import Option
39
40
41substitution_help = {
42 u'%b' : u'file.BASE',
43 u'%t' : u'file.THIS',
44 u'%o' : u'file.OTHER',
45 u'%r' : u'file (output)',
46 u'%T' : u'file.THIS (temp copy, used to overwrite "file" if merge succeeds)'
47}
48
49
50def subprocess_invoker(executable, args, cleanup):
51 retcode = subprocess.call([executable] + args)
52 cleanup(retcode)
53 return retcode
54
55
56_WIN32_PATH_EXT = [unicode(ext.lower())
57 for ext in os.getenv('PATHEXT', '').split(';')]
58
59
60def tool_name_from_executable(executable):
61 name = os.path.basename(executable)
62 if sys.platform == 'win32':
63 root, ext = os.path.splitext(name)
64 if ext.lower() in _WIN32_PATH_EXT:
65 name = root
66 return name
67
68
69class MergeTool(object):
70 def __init__(self, name, commandline):
71 """Initializes the merge tool with a name and a command-line (a string
72 or sequence of strings).
73 """
74 self.set_commandline(commandline)
75 self.set_name(name) # needs commandline set first when name is None
76
77 def __repr__(self):
78 return '<MergeTool %s: %r>' % (self._name, self._commandline)
79
80 def __eq__(self, other):
81 if type(other) == MergeTool:
82 return cmp(self, other) == 0
83 else:
84 return False
85
86 def __ne__(self, other):
87 if type(other) == MergeTool:
88 return cmp(self, other) != 0
89 else:
90 return True
91
92 def __cmp__(self, other):
93 if type(other == MergeTool):
94 return cmp((self._name, self._commandline),
95 (other._name, other._commandline))
96
97 def __str__(self):
98 return self.get_commandline()
99
100 def get_name(self):
101 return self._name
102
103 def set_name(self, name):
104 if name is None:
105 self._name = tool_name_from_executable(self.get_executable())
106 else:
107 self._name = name
108
109 def get_commandline(self, quote=False):
110 if quote:
111 args = _quote_args(self._commandline)
112 else:
113 args = self._commandline
114 return u' '.join(args)
115
116 def get_commandline_as_list(self):
117 return self._commandline
118
119 def set_commandline(self, commandline):
120 if isinstance(commandline, basestring):
121 self._commandline = cmdline.split(commandline)
122 elif isinstance(commandline, (tuple, list)):
123 self._commandline = list(commandline)
124 else:
125 raise TypeError('%r is not valid for commandline; must be string '
126 'or sequence of strings' % commandline)
127
128 def get_executable(self):
129 if len(self._commandline) < 1:
130 return u''
131 return self._commandline[0]
132
133 def set_executable(self, executable):
134 self._commandline[:1] = [executable]
135
136 def is_available(self):
137 executable = self.get_executable()
138 return os.path.exists(executable) or _find_executable(executable)
139
140 def invoke(self, filename, invoker=None):
141 if invoker is None:
142 invoker = subprocess_invoker
143 args, tmp_file = self._subst_filename(self._commandline, filename)
144 def cleanup(retcode):
145 if tmp_file is not None:
146 if retcode == 0: # on success, replace file with temp file
147 shutil.move(tmp_file, filename)
148 else: # otherwise, delete temp file
149 os.remove(tmp_file)
150 return invoker(args[0], args[1:], cleanup)
151
152 def _subst_filename(self, args, filename):
153 tmp_file = None
154 subst_args = []
155 for arg in args:
156 arg = arg.replace(u'%b', filename + u'.BASE')
157 arg = arg.replace(u'%t', filename + u'.THIS')
158 arg = arg.replace(u'%o', filename + u'.OTHER')
159 arg = arg.replace(u'%r', filename)
160 if u'%T' in arg:
161 tmp_file = tempfile.mktemp(u"_bzr_mergetools_%s.THIS" %
162 os.path.basename(filename))
163 shutil.copy(filename + u".THIS", tmp_file)
164 arg = arg.replace(u'%T', tmp_file)
165 subst_args.append(arg)
166 return subst_args, tmp_file
167
168
169_KNOWN_MERGE_TOOLS = (
170 u'bcompare %t %o %b %r',
171 u'kdiff3 %b %t %o -o %r',
172 u'xxdiff -m -O -M %r %t %b %o',
173 u'meld %b %T %o',
174 u'opendiff %t %o -ancestor %b -merge %r',
175 u'winmergeu %r',
176)
177
178
179def detect_merge_tools():
180 tools = [MergeTool(None, commandline) for commandline in _KNOWN_MERGE_TOOLS]
181 return [tool for tool in tools if tool.is_available()]
182
183
184def get_merge_tools(conf=None):
185 """Returns list of MergeTool objects."""
186 if conf is None:
187 conf = config.GlobalConfig()
188 tools = []
189 for (oname, value, section, conf_id) in conf._get_options():
190 if oname.startswith('mergetools.'):
191 tools.append(MergeTool(oname[len('mergetools.'):], value))
192 return tools
193
194
195def set_merge_tools(merge_tools, conf=None):
196 if conf is None:
197 conf = config.GlobalConfig()
198 # remove entries from config for tools which do not appear in merge_tools
199 tool_names = [tool.get_name() for tool in merge_tools]
200 for (oname, value, section, conf_id) in conf._get_options():
201 if oname.startswith('mergetools.'):
202 if oname[len('mergetools.'):] not in tool_names:
203 conf.remove_user_option(oname)
204 # set config entries
205 for tool in merge_tools:
206 oname = 'mergetools.%s' % tool.get_name()
207 value = tool.get_commandline(quote=True)
208 if oname == '' or value == '':
209 continue
210 conf.set_user_option(oname, value)
211
212
213def find_merge_tool(name, conf=None):
214 if conf is None:
215 conf = config.GlobalConfig()
216 merge_tools = get_merge_tools(conf)
217 for merge_tool in merge_tools:
218 if merge_tool.get_name() == name:
219 return merge_tool
220 return None
221
222
223def find_first_available_merge_tool(conf=None):
224 if conf is None:
225 conf = config.GlobalConfig()
226 merge_tools = get_merge_tools(conf)
227 for merge_tool in merge_tools:
228 if merge_tool.is_available():
229 return merge_tool
230 return None
231
232
233def get_default_merge_tool(conf=None):
234 if conf is None:
235 conf = config.GlobalConfig()
236 name = conf.get_user_option('default_mergetool')
237 if name is None:
238 trace.mutter('no default merge tool defined')
239 return None
240 merge_tool = find_merge_tool(name, conf)
241 trace.mutter('found default merge tool: %r', merge_tool)
242 return merge_tool
243
244
245def set_default_merge_tool(name, conf=None):
246 if conf is None:
247 conf = config.GlobalConfig()
248 if name is None:
249 conf.remove_user_option('default_mergetool')
250 else:
251 if isinstance(name, MergeTool):
252 name = name.get_name()
253 if find_merge_tool(name, conf) is None:
254 raise errors.BzrError('invalid merge tool name: %r' % name)
255 trace.mutter('setting default merge tool: %s', name)
256 conf.set_user_option('default_mergetool', name)
257
258
259def resolve_using_merge_tool(tool_name, conflicts):
260 merge_tool = find_merge_tool(tool_name)
261 if merge_tool is None:
262 available = '\n '.join([mt.get_name() for mt in get_merge_tools()
263 if mt.is_available()])
264 raise errors.BzrCommandError('Unrecognized merge tool: %s\n\n'
265 'Available merge tools:\n'
266 ' %s' % (tool_name, available))
267 for conflict in conflicts:
268 merge_tool.invoke(conflict.path)
269
270
271def _quote_args(args):
272 return [_quote_arg(arg) for arg in args]
273
274
275def _quote_arg(arg):
276 if u' ' in arg and not _is_arg_quoted(arg):
277 return u'"%s"' % _escape_quotes(arg)
278 else:
279 return arg
280
281
282def _is_arg_quoted(arg):
283 return (arg[0] == u"'" and arg[-1] == u"'") or \
284 (arg[0] == u'"' and arg[-1] == u'"')
285
286
287def _escape_quotes(arg):
288 return arg.replace(u'"', u'\\"')
289
290
291# courtesy of 'techtonik' at http://snippets.dzone.com/posts/show/6313
292def _find_executable(executable, path=None):
293 """Try to find 'executable' in the directories listed in 'path' (a
294 string listing directories separated by 'os.pathsep'; defaults to
295 os.environ['PATH']). Returns the complete filename or None if not
296 found
297 """
298 if path is None:
299 path = os.environ['PATH']
300 paths = path.split(os.pathsep)
301 extlist = ['']
302 if sys.platform == 'win32':
303 pathext = os.environ['PATHEXT'].lower().split(os.pathsep)
304 (base, ext) = os.path.splitext(executable)
305 if ext.lower() not in pathext:
306 extlist = pathext
307 for ext in extlist:
308 execname = executable + ext
309 if os.path.isfile(execname):
310 return execname
311 else:
312 for p in paths:
313 f = os.path.join(p, execname)
314 if os.path.isfile(f):
315 return f
316 else:
317 return None
0318
=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2010-10-18 17:06:37 +0000
+++ bzrlib/tests/__init__.py 2010-11-12 03:34:57 +0000
@@ -3748,6 +3748,7 @@
3748 'bzrlib.tests.test_merge3',3748 'bzrlib.tests.test_merge3',
3749 'bzrlib.tests.test_merge_core',3749 'bzrlib.tests.test_merge_core',
3750 'bzrlib.tests.test_merge_directive',3750 'bzrlib.tests.test_merge_directive',
3751 'bzrlib.tests.test_mergetools',
3751 'bzrlib.tests.test_missing',3752 'bzrlib.tests.test_missing',
3752 'bzrlib.tests.test_msgeditor',3753 'bzrlib.tests.test_msgeditor',
3753 'bzrlib.tests.test_multiparent',3754 'bzrlib.tests.test_multiparent',
37543755
=== added file 'bzrlib/tests/test_mergetools.py'
--- bzrlib/tests/test_mergetools.py 1970-01-01 00:00:00 +0000
+++ bzrlib/tests/test_mergetools.py 2010-11-12 03:34:57 +0000
@@ -0,0 +1,317 @@
1# Copyright (C) 2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17import os
18import re
19import sys
20
21from bzrlib import (
22 config,
23 mergetools,
24 tests
25)
26from bzrlib.tests.features import backslashdir_feature
27
28
29class TestBasics(tests.TestCase):
30 def setUp(self):
31 super(TestBasics, self).setUp()
32 self.tool = mergetools.MergeTool('sometool',
33 '/path/to/tool --opt %b -x %t %o --stuff %r')
34
35 def test_get_commandline(self):
36 self.assertEqual('/path/to/tool --opt %b -x %t %o --stuff %r',
37 self.tool.get_commandline())
38
39 def test_get_commandline_as_list(self):
40 self.assertEqual(['/path/to/tool', '--opt', '%b', '-x', '%t', '%o',
41 '--stuff', '%r'],
42 self.tool.get_commandline_as_list())
43
44 def test_get_executable(self):
45 self.assertEqual('/path/to/tool', self.tool.get_executable())
46
47 def test_get_name(self):
48 self.assertEqual('sometool', self.tool.get_name())
49
50 def test_set_name(self):
51 self.tool.set_name('bettertool')
52 self.assertEqual('bettertool', self.tool.get_name())
53
54 def test_set_name_none(self):
55 self.tool.set_name(None)
56 self.assertEqual('tool', self.tool.get_name())
57
58 def test_set_commandline(self):
59 self.tool.set_commandline('/new/path/to/bettertool %b %t %o %r')
60 self.assertEqual(['/new/path/to/bettertool', '%b', '%t', '%o', '%r'],
61 self.tool.get_commandline_as_list())
62
63 def test_set_executable(self):
64 self.tool.set_executable('othertool')
65 self.assertEqual(['othertool', '--opt', '%b', '-x', '%t', '%o',
66 '--stuff', '%r'],
67 self.tool.get_commandline_as_list())
68
69 def test_quoted_executable(self):
70 self.requireFeature(backslashdir_feature)
71 self.tool.set_commandline(
72 '"C:\\Program Files\\KDiff3\\kdiff3.exe" %b %t %o -o %r')
73 self.assertEqual('C:\\Program Files\\KDiff3\\kdiff3.exe',
74 self.tool.get_executable())
75
76 def test_comparison(self):
77 other_tool = mergetools.MergeTool('sometool',
78 '/path/to/tool --opt %b -x %t %o --stuff %r')
79 self.assertTrue(self.tool == other_tool)
80 self.assertTrue(self.tool != None)
81
82 def test_comparison_none(self):
83 self.assertFalse(self.tool == None)
84 self.assertTrue(self.tool != None)
85
86 def test_comparison_fail_name(self):
87 other_tool = mergetools.MergeTool('sometoolx',
88 '/path/to/tool --opt %b -x %t %o --stuff %r')
89 self.assertFalse(self.tool == other_tool)
90 self.assertTrue(self.tool != other_tool)
91
92 def test_comparison_fail_commandline(self):
93 other_tool = mergetools.MergeTool('sometool',
94 '/path/to/tool --opt %b -x %t %o --stuff %r extra')
95 self.assertFalse(self.tool == other_tool)
96 self.assertTrue(self.tool != other_tool)
97
98
99class TestUnicodeBasics(tests.TestCase):
100 def setUp(self):
101 super(TestUnicodeBasics, self).setUp()
102 self.tool = mergetools.MergeTool(u'someb\u0414r',
103 u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r')
104
105 def test_get_commandline(self):
106 self.assertEqual(u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r',
107 self.tool.get_commandline())
108
109 def test_get_commandline_as_list(self):
110 self.assertEqual([u'/path/to/b\u0414r', u'--opt', u'%b', u'-x', u'%t',
111 u'%o', u'--stuff', u'%r'],
112 self.tool.get_commandline_as_list())
113
114 def test_get_executable(self):
115 self.assertEqual(u'/path/to/b\u0414r', self.tool.get_executable())
116
117 def test_get_name(self):
118 self.assertEqual(u'someb\u0414r', self.tool.get_name())
119
120 def test_set_name(self):
121 self.tool.set_name(u'betterb\u0414r')
122 self.assertEqual(u'betterb\u0414r', self.tool.get_name())
123
124 def test_set_name_none(self):
125 self.tool.set_name(None)
126 self.assertEqual(u'b\u0414r', self.tool.get_name())
127
128 def test_set_commandline(self):
129 self.tool.set_commandline(u'/new/path/to/betterb\u0414r %b %t %o %r')
130 self.assertEqual([u'/new/path/to/betterb\u0414r', u'%b', u'%t', u'%o',
131 u'%r'],
132 self.tool.get_commandline_as_list())
133
134 def test_set_executable(self):
135 self.tool.set_executable(u'otherb\u0414r')
136 self.assertEqual([u'otherb\u0414r', u'--opt', u'%b', u'-x', u'%t',
137 u'%o', u'--stuff', u'%r'],
138 self.tool.get_commandline_as_list())
139
140 def test_quoted_executable(self):
141 self.requireFeature(backslashdir_feature)
142 self.tool.set_commandline(
143 u'"C:\\Program Files\\KDiff3\\b\u0414r.exe" %b %t %o -o %r')
144 self.assertEqual(u'C:\\Program Files\\KDiff3\\b\u0414r.exe',
145 self.tool.get_executable())
146
147 def test_comparison(self):
148 other_tool = mergetools.MergeTool(u'someb\u0414r',
149 u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r')
150 self.assertTrue(self.tool == other_tool)
151 self.assertFalse(self.tool != other_tool)
152
153 def test_comparison_none(self):
154 self.assertFalse(self.tool == None)
155 self.assertTrue(self.tool != None)
156
157 def test_comparison_fail_name(self):
158 other_tool = mergetools.MergeTool(u'someb\u0414rx',
159 u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r')
160 self.assertFalse(self.tool == other_tool)
161 self.assertTrue(self.tool != other_tool)
162
163 def test_comparison_fail_commandline(self):
164 other_tool = mergetools.MergeTool(u'someb\u0414r',
165 u'/path/to/b\u0414r --opt %b -x %t %o --stuff %r extra')
166 self.assertFalse(self.tool == other_tool)
167 self.assertTrue(self.tool != other_tool)
168
169
170class TestMergeToolOperations(tests.TestCaseInTempDir):
171 def test_filename_substitution(self):
172 def dummy_invoker(executable, args, cleanup):
173 self._commandline = [executable] + args
174 cleanup(0)
175 mt = mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r')
176 mt.invoke('test.txt', dummy_invoker)
177 self.assertEqual(
178 ['kdiff3',
179 'test.txt.BASE',
180 'test.txt.THIS',
181 'test.txt.OTHER',
182 '-o',
183 'test.txt'],
184 self._commandline)
185 mt.invoke('file with space.txt', dummy_invoker)
186 self.assertEqual(
187 ['kdiff3',
188 "file with space.txt.BASE",
189 "file with space.txt.THIS",
190 "file with space.txt.OTHER",
191 '-o',
192 "file with space.txt"],
193 self._commandline)
194 mt.invoke('file with "space and quotes".txt', dummy_invoker)
195 self.assertEqual(
196 ['kdiff3',
197 "file with \"space and quotes\".txt.BASE",
198 "file with \"space and quotes\".txt.THIS",
199 "file with \"space and quotes\".txt.OTHER",
200 '-o',
201 "file with \"space and quotes\".txt"],
202 self._commandline)
203
204 def test_expand_commandline_tempfile(self):
205 def dummy_invoker(executable, args, cleanup):
206 self.assertEqual('some_tool', executable)
207 self.failUnlessExists(args[0])
208 cleanup(0)
209 self._tmp_file = args[0]
210 self.build_tree(('test.txt', 'test.txt.BASE', 'test.txt.THIS',
211 'test.txt.OTHER'))
212 mt = mergetools.MergeTool('some_tool', 'some_tool %T')
213 mt.invoke('test.txt', dummy_invoker)
214 self.failIfExists(self._tmp_file)
215
216 def test_is_available_full_tool_path(self):
217 mt = mergetools.MergeTool(None, sys.executable)
218 self.assertTrue(mt.is_available())
219
220 def test_is_available_tool_on_path(self):
221 mt = mergetools.MergeTool(None, os.path.basename(sys.executable))
222 self.assertTrue(mt.is_available())
223
224 def test_is_available_nonexistent(self):
225 mt = mergetools.MergeTool(None, "ThisExecutableShouldReallyNotExist")
226 self.assertFalse(mt.is_available())
227
228 def test_empty_commandline(self):
229 mt = mergetools.MergeTool('', '')
230 self.assertEqual('', mt.get_commandline())
231
232 def test_no_arguments(self):
233 mt = mergetools.MergeTool('tool', 'tool')
234 self.assertEqual('tool', mt.get_commandline())
235
236
237class TestModuleFunctions(tests.TestCaseInTempDir):
238 def test_get_merge_tools(self):
239 conf = config.GlobalConfig()
240 conf.set_user_option('mergetools.kdiff3', 'kdiff3 %b %t %o -o %r')
241 conf.set_user_option('mergetools.winmergeu', 'winmergeu %r')
242 conf.set_user_option('mergetools.funkytool', 'funkytool "arg with spaces" %T')
243 tools = mergetools.get_merge_tools(conf)
244 self.log(repr(tools))
245 self.assertEqual(3, len(tools))
246 self.assertEqual('kdiff3', tools[0].get_name())
247 self.assertEqual('kdiff3 %b %t %o -o %r', tools[0].get_commandline())
248 self.assertEqual('winmergeu', tools[1].get_name())
249 self.assertEqual('winmergeu %r', tools[1].get_commandline())
250 self.assertEqual('funkytool', tools[2].get_name())
251 self.assertEqual('funkytool "arg with spaces" %T',
252 tools[2].get_commandline(quote=True))
253
254 def test_set_merge_tools(self):
255 conf = config.GlobalConfig()
256 tools = [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
257 mergetools.MergeTool('winmergeu', 'winmergeu %r'),
258 mergetools.MergeTool('funkytool',
259 'funkytool "arg with spaces" %T')
260 ]
261 mergetools.set_merge_tools(tools, conf)
262 self.assertEqual('funkytool "arg with spaces" %T',
263 conf.get_user_option('mergetools.funkytool'))
264 self.assertEqual('kdiff3 %b %t %o -o %r',
265 conf.get_user_option('mergetools.kdiff3'))
266 self.assertEqual('winmergeu %r',
267 conf.get_user_option('mergetools.winmergeu'))
268
269 def test_set_merge_tools_duplicates(self):
270 conf = config.GlobalConfig()
271 mergetools.set_merge_tools(
272 [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
273 mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r')],
274 conf)
275 tools = mergetools.get_merge_tools(conf)
276 self.assertEqual(1, len(tools))
277 self.assertEqual('kdiff3', tools[0].get_name())
278 self.assertEqual('kdiff3 %b %t %o -o %r', tools[0].get_commandline())
279
280 def test_set_merge_tools_empty_tool(self):
281 conf = config.GlobalConfig()
282 mergetools.set_merge_tools(
283 [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
284 mergetools.MergeTool('',''),
285 mergetools.MergeTool('blah','')],
286 conf)
287 tools = mergetools.get_merge_tools(conf)
288 self.assertEqual(1, len(tools))
289 self.assertEqual('kdiff3', tools[0].get_name())
290 self.assertEqual('kdiff3 %b %t %o -o %r', tools[0].get_commandline())
291
292 def test_set_merge_tools_remove_one(self):
293 conf = config.GlobalConfig()
294 tools = [mergetools.MergeTool('kdiff3', 'kdiff3 %b %t %o -o %r'),
295 mergetools.MergeTool('winmergeu', 'winmergeu %r'),
296 mergetools.MergeTool('funkytool',
297 'funkytool "arg with spaces" %T')
298 ]
299 mergetools.set_merge_tools(tools, conf)
300 del tools[0]
301 mergetools.set_merge_tools(tools, conf)
302 self.assertEqual(sorted([
303 ('mergetools.funkytool', 'funkytool "arg with spaces" %T', 'DEFAULT', 'bazaar'),
304 ('mergetools.winmergeu', 'winmergeu %r', 'DEFAULT', 'bazaar'),
305 ]),
306 sorted(conf._get_options()))
307
308 def test_detect(self):
309 # only way to reliably test detection is to add a known existing
310 # executable to the list used for detection
311 old_kmt = mergetools._KNOWN_MERGE_TOOLS
312 mergetools._KNOWN_MERGE_TOOLS = ['sh', 'cmd']
313 tools = mergetools.detect_merge_tools()
314 tools_commandlines = [mt.get_commandline() for mt in tools]
315 self.assertTrue('sh' in tools_commandlines or
316 'cmd' in tools_commandlines)
317 mergetools._KNOWN_MERGE_TOOLS = old_kmt
0318
=== modified file 'doc/en/release-notes/bzr-2.3.txt'
--- doc/en/release-notes/bzr-2.3.txt 2010-11-11 23:48:07 +0000
+++ doc/en/release-notes/bzr-2.3.txt 2010-11-12 03:34:57 +0000
@@ -62,8 +62,8 @@
62API Changes62API Changes
63***********63***********
6464
65.. Changes that may require updates in plugins or other code that uses65* Added ``bzrlib.mergetools`` module with helper functions for working with
66 bzrlib.66 the list of external merge tools. (Gordon Tyler)
6767
68Internals68Internals
69*********69*********