1# Copyright (C) 2005, 2006 Canonical Ltd
2# Copyright (C) 2005, 2008 Aaron Bentley, 2006 Michael Ellerman
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18"""Diff and patch functionality"""
19
20import errno
21import os
22from subprocess import Popen, PIPE
23import sys
24import tempfile
25
26from .errors import NoDiff3, BzrError
27from .textfile import check_text_path
28
29class PatchFailed(BzrError):
30
31    _fmt = """Patch application failed"""
32
33
34class PatchInvokeError(BzrError):
35
36    _fmt = """Error invoking patch: %(errstr)s%(stderr)s"""
37    internal_error = False
38
39    def __init__(self, e, stderr=''):
40        self.exception = e
41        self.errstr = os.strerror(e.errno)
42        self.stderr = '\n' + stderr
43
44
45_do_close_fds = True
46if os.name == 'nt':
47    _do_close_fds = False
48
49
50def write_to_cmd(args, input=""):
51    """Spawn a process, and wait for the result
52
53    If the process is killed, an exception is raised
54
55    :param args: The command line, the first entry should be the program name
56    :param input: [optional] The text to send the process on stdin
57    :return: (stdout, stderr, status)
58    """
59    process = Popen(args, bufsize=len(input), stdin=PIPE, stdout=PIPE,
60                    stderr=PIPE, close_fds=_do_close_fds)
61    stdout, stderr = process.communicate(input)
62    status = process.wait()
63    if status < 0:
64        raise Exception("%s killed by signal %i" % (args[0], -status))
65    return stdout, stderr, status
66
67
68def patch(patch_contents, filename, output_filename=None, reverse=False):
69    """Apply a patch to a file, to produce another output file.  This is should
70    be suitable for our limited purposes.
71
72    :param patch_contents: The contents of the patch to apply
73    :type patch_contents: str
74    :param filename: the name of the file to apply the patch to
75    :type filename: str
76    :param output_filename: The filename to produce.  If None, file is \
77    modified in-place
78    :type output_filename: str or NoneType
79    :param reverse: If true, apply the patch in reverse
80    :type reverse: bool
81    :return: 0 on success, 1 if some hunks failed
82    """
83    args = ["patch", "-f", "-s", "--posix", "--binary"]
84    if reverse:
85        args.append("--reverse")
86    if output_filename is not None:
87        args.extend(("-o", output_filename))
88    args.append(filename)
89    stdout, stderr, status = write_to_cmd(args, patch_contents)
90    return status
91
92
93def diff3(out_file, mine_path, older_path, yours_path):
94    def add_label(args, label):
95        args.extend(("-L", label))
96    check_text_path(mine_path)
97    check_text_path(older_path)
98    check_text_path(yours_path)
99    args = ['diff3', "-E", "--merge"]
100    add_label(args, "TREE")
101    add_label(args, "ANCESTOR")
102    add_label(args, "MERGE-SOURCE")
103    args.extend((mine_path, older_path, yours_path))
104    try:
105        output, stderr, status = write_to_cmd(args)
106    except OSError as e:
107        if e.errno == errno.ENOENT:
108            raise NoDiff3
109        else:
110            raise
111    if status not in (0, 1):
112        raise Exception(stderr)
113    with open(out_file, 'wb') as f:
114        f.write(output)
115    return status
116
117
118def patch_tree(tree, patches, strip=0, reverse=False, dry_run=False,
119               quiet=False, out=None):
120    """Apply a patch to a tree.
121
122    Args:
123      tree: A MutableTree object
124      patches: list of patches as bytes
125      strip: Strip X segments of paths
126      reverse: Apply reversal of patch
127      dry_run: Dry run
128    """
129    return run_patch(tree.basedir, patches, strip, reverse, dry_run,
130                     quiet, out=out)
131
132
133def run_patch(directory, patches, strip=0, reverse=False, dry_run=False,
134              quiet=False, _patch_cmd='patch', target_file=None, out=None):
135    args = [_patch_cmd, '-d', directory, '-s', '-p%d' % strip, '-f']
136    if quiet:
137        args.append('--quiet')
138
139    if sys.platform == "win32":
140        args.append('--binary')
141
142    if reverse:
143        args.append('-R')
144    if dry_run:
145        if sys.platform.startswith('freebsd'):
146            args.append('--check')
147        else:
148            args.append('--dry-run')
149        stderr = PIPE
150    else:
151        stderr = None
152    if target_file is not None:
153        args.append(target_file)
154
155    try:
156        process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=stderr)
157    except OSError as e:
158        raise PatchInvokeError(e)
159    try:
160        for patch in patches:
161            process.stdin.write(bytes(patch))
162        process.stdin.close()
163
164    except IOError as e:
165        raise PatchInvokeError(e, process.stderr.read())
166
167    result = process.wait()
168    if not dry_run:
169        if out is not None:
170            out.write(process.stdout.read())
171        else:
172            process.stdout.read()
173    if result != 0:
174        raise PatchFailed()
175
176    return result
177
178
179def iter_patched_from_hunks(orig_lines, hunks):
180    """Iterate through a series of lines with a patch applied.
181    This handles a single file, and does exact, not fuzzy patching.
182
183    :param orig_lines: The unpatched lines.
184    :param hunks: An iterable of Hunk instances.
185
186    This is different from breezy.patches in that it invokes the patch
187    command.
188    """
189    with tempfile.NamedTemporaryFile() as f:
190        f.writelines(orig_lines)
191        f.flush()
192        # TODO(jelmer): Stream patch contents to command, rather than
193        # serializing the entire patch upfront.
194        serialized = b''.join([hunk.as_bytes() for hunk in hunks])
195        args = ["patch", "-f", "-s", "--posix", "--binary",
196                "-o", "-", f.name, "-r", "-"]
197        stdout, stderr, status = write_to_cmd(args, serialized)
198    if status == 0:
199        return [stdout]
200    raise PatchFailed(stderr)
201