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"""Utility functions for managing external merge tools such as kdiff3."""
18
19import os
20import shutil
21import subprocess
22import sys
23import tempfile
24
25from .lazy_import import lazy_import
26lazy_import(globals(), """
27from breezy import (
28    cmdline,
29    osutils,
30    trace,
31)
32""")
33
34
35known_merge_tools = {
36    'bcompare': 'bcompare {this} {other} {base} {result}',
37    'kdiff3': 'kdiff3 {base} {this} {other} -o {result}',
38    'xdiff': 'xxdiff -m -O -M {result} {this} {base} {other}',
39    'meld': 'meld {base} {this_temp} {other}',
40    'opendiff': 'opendiff {this} {other} -ancestor {base} -merge {result}',
41    'winmergeu': 'winmergeu {result}',
42}
43
44
45def check_availability(command_line):
46    cmd_list = cmdline.split(command_line)
47    exe = cmd_list[0]
48    if sys.platform == 'win32':
49        exe = _get_executable_path(exe)
50        if exe is None:
51            return False
52        base, ext = os.path.splitext(exe)
53        path_ext = [s.lower()
54                    for s in os.getenv('PATHEXT', '').split(os.pathsep)]
55        return os.path.exists(exe) and ext in path_ext
56    else:
57        return (os.access(exe, os.X_OK) or
58                osutils.find_executable_on_path(exe) is not None)
59
60
61def invoke(command_line, filename, invoker=None):
62    """Invokes the given merge tool command line, substituting the given
63    filename according to the embedded substitution markers. Optionally, it
64    will use the given invoker function instead of the default
65    subprocess_invoker.
66    """
67    if invoker is None:
68        invoker = subprocess_invoker
69    cmd_list = cmdline.split(command_line)
70    exe = _get_executable_path(cmd_list[0])
71    if exe is not None:
72        cmd_list[0] = exe
73    args, tmp_file = _subst_filename(cmd_list, filename)
74
75    def cleanup(retcode):
76        if tmp_file is not None:
77            if retcode == 0:  # on success, replace file with temp file
78                shutil.move(tmp_file, filename)
79            else:  # otherwise, delete temp file
80                os.remove(tmp_file)
81    return invoker(args[0], args[1:], cleanup)
82
83
84def _get_executable_path(exe):
85    if os.path.isabs(exe):
86        return exe
87    return osutils.find_executable_on_path(exe)
88
89
90def _subst_filename(args, filename):
91    subst_names = {
92        'base': filename + u'.BASE',
93        'this': filename + u'.THIS',
94        'other': filename + u'.OTHER',
95        'result': filename,
96    }
97    tmp_file = None
98    subst_args = []
99    for arg in args:
100        if '{this_temp}' in arg and 'this_temp' not in subst_names:
101            fh, tmp_file = tempfile.mkstemp(u"_bzr_mergetools_%s.THIS" %
102                                            os.path.basename(filename))
103            trace.mutter('fh=%r, tmp_file=%r', fh, tmp_file)
104            os.close(fh)
105            shutil.copy(filename + u".THIS", tmp_file)
106            subst_names['this_temp'] = tmp_file
107        arg = _format_arg(arg, subst_names)
108        subst_args.append(arg)
109    return subst_args, tmp_file
110
111
112# This would be better implemented using format() from python 2.6
113def _format_arg(arg, subst_names):
114    arg = arg.replace('{base}', subst_names['base'])
115    arg = arg.replace('{this}', subst_names['this'])
116    arg = arg.replace('{other}', subst_names['other'])
117    arg = arg.replace('{result}', subst_names['result'])
118    if 'this_temp' in subst_names:
119        arg = arg.replace('{this_temp}', subst_names['this_temp'])
120    return arg
121
122
123def subprocess_invoker(executable, args, cleanup):
124    retcode = subprocess.call([executable] + args)
125    cleanup(retcode)
126    return retcode
127