/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/diff.py

  • Committer: Jelmer Vernooij
  • Date: 2019-06-03 23:48:08 UTC
  • mfrom: (7316 work)
  • mto: This revision was merged to the branch mainline in revision 7328.
  • Revision ID: jelmer@jelmer.uk-20190603234808-15yk5c7054tj8e2b
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2014 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
from __future__ import absolute_import
 
18
 
 
19
import difflib
 
20
import os
 
21
import re
 
22
import string
 
23
import sys
 
24
 
 
25
from .lazy_import import lazy_import
 
26
lazy_import(globals(), """
 
27
import errno
 
28
import patiencediff
 
29
import subprocess
 
30
import tempfile
 
31
 
 
32
from breezy import (
 
33
    cmdline,
 
34
    controldir,
 
35
    errors,
 
36
    osutils,
 
37
    textfile,
 
38
    timestamp,
 
39
    views,
 
40
    )
 
41
 
 
42
from breezy.workingtree import WorkingTree
 
43
from breezy.i18n import gettext
 
44
""")
 
45
 
 
46
from .registry import (
 
47
    Registry,
 
48
    )
 
49
from .sixish import text_type
 
50
from .trace import mutter, note, warning
 
51
from .tree import FileTimestampUnavailable
 
52
 
 
53
 
 
54
DEFAULT_CONTEXT_AMOUNT = 3
 
55
 
 
56
 
 
57
class AtTemplate(string.Template):
 
58
    """Templating class that uses @ instead of $."""
 
59
 
 
60
    delimiter = '@'
 
61
 
 
62
 
 
63
# TODO: Rather than building a changeset object, we should probably
 
64
# invoke callbacks on an object.  That object can either accumulate a
 
65
# list, write them out directly, etc etc.
 
66
 
 
67
 
 
68
class _PrematchedMatcher(difflib.SequenceMatcher):
 
69
    """Allow SequenceMatcher operations to use predetermined blocks"""
 
70
 
 
71
    def __init__(self, matching_blocks):
 
72
        difflib.SequenceMatcher(self, None, None)
 
73
        self.matching_blocks = matching_blocks
 
74
        self.opcodes = None
 
75
 
 
76
 
 
77
def internal_diff(old_label, oldlines, new_label, newlines, to_file,
 
78
                  allow_binary=False, sequence_matcher=None,
 
79
                  path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
 
80
    # FIXME: difflib is wrong if there is no trailing newline.
 
81
    # The syntax used by patch seems to be "\ No newline at
 
82
    # end of file" following the last diff line from that
 
83
    # file.  This is not trivial to insert into the
 
84
    # unified_diff output and it might be better to just fix
 
85
    # or replace that function.
 
86
 
 
87
    # In the meantime we at least make sure the patch isn't
 
88
    # mangled.
 
89
 
 
90
    if allow_binary is False:
 
91
        textfile.check_text_lines(oldlines)
 
92
        textfile.check_text_lines(newlines)
 
93
 
 
94
    if sequence_matcher is None:
 
95
        sequence_matcher = patiencediff.PatienceSequenceMatcher
 
96
    ud = unified_diff_bytes(
 
97
        oldlines, newlines,
 
98
        fromfile=old_label.encode(path_encoding, 'replace'),
 
99
        tofile=new_label.encode(path_encoding, 'replace'),
 
100
        n=context_lines, sequencematcher=sequence_matcher)
 
101
 
 
102
    ud = list(ud)
 
103
    if len(ud) == 0:  # Identical contents, nothing to do
 
104
        return
 
105
    # work-around for difflib being too smart for its own good
 
106
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
 
107
    if not oldlines:
 
108
        ud[2] = ud[2].replace(b'-1,0', b'-0,0')
 
109
    elif not newlines:
 
110
        ud[2] = ud[2].replace(b'+1,0', b'+0,0')
 
111
 
 
112
    for line in ud:
 
113
        to_file.write(line)
 
114
        if not line.endswith(b'\n'):
 
115
            to_file.write(b"\n\\ No newline at end of file\n")
 
116
    to_file.write(b'\n')
 
117
 
 
118
 
 
119
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
 
120
                       tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
 
121
    r"""
 
122
    Compare two sequences of lines; generate the delta as a unified diff.
 
123
 
 
124
    Unified diffs are a compact way of showing line changes and a few
 
125
    lines of context.  The number of context lines is set by 'n' which
 
126
    defaults to three.
 
127
 
 
128
    By default, the diff control lines (those with ---, +++, or @@) are
 
129
    created with a trailing newline.  This is helpful so that inputs
 
130
    created from file.readlines() result in diffs that are suitable for
 
131
    file.writelines() since both the inputs and outputs have trailing
 
132
    newlines.
 
133
 
 
134
    For inputs that do not have trailing newlines, set the lineterm
 
135
    argument to "" so that the output will be uniformly newline free.
 
136
 
 
137
    The unidiff format normally has a header for filenames and modification
 
138
    times.  Any or all of these may be specified using strings for
 
139
    'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.  The modification
 
140
    times are normally expressed in the format returned by time.ctime().
 
141
 
 
142
    Example:
 
143
 
 
144
    >>> for line in bytes_unified_diff(b'one two three four'.split(),
 
145
    ...             b'zero one tree four'.split(), b'Original', b'Current',
 
146
    ...             b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
 
147
    ...             lineterm=b''):
 
148
    ...     print line
 
149
    --- Original Sat Jan 26 23:30:50 1991
 
150
    +++ Current Fri Jun 06 10:20:52 2003
 
151
    @@ -1,4 +1,4 @@
 
152
    +zero
 
153
     one
 
154
    -two
 
155
    -three
 
156
    +tree
 
157
     four
 
158
    """
 
159
    if sequencematcher is None:
 
160
        sequencematcher = difflib.SequenceMatcher
 
161
 
 
162
    if fromfiledate:
 
163
        fromfiledate = b'\t' + bytes(fromfiledate)
 
164
    if tofiledate:
 
165
        tofiledate = b'\t' + bytes(tofiledate)
 
166
 
 
167
    started = False
 
168
    for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
 
169
        if not started:
 
170
            yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
 
171
            yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
 
172
            started = True
 
173
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
 
174
        yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
 
175
        for tag, i1, i2, j1, j2 in group:
 
176
            if tag == 'equal':
 
177
                for line in a[i1:i2]:
 
178
                    yield b' ' + line
 
179
                continue
 
180
            if tag == 'replace' or tag == 'delete':
 
181
                for line in a[i1:i2]:
 
182
                    yield b'-' + line
 
183
            if tag == 'replace' or tag == 'insert':
 
184
                for line in b[j1:j2]:
 
185
                    yield b'+' + line
 
186
 
 
187
 
 
188
def _spawn_external_diff(diffcmd, capture_errors=True):
 
189
    """Spawn the external diff process, and return the child handle.
 
190
 
 
191
    :param diffcmd: The command list to spawn
 
192
    :param capture_errors: Capture stderr as well as setting LANG=C
 
193
        and LC_ALL=C. This lets us read and understand the output of diff,
 
194
        and respond to any errors.
 
195
    :return: A Popen object.
 
196
    """
 
197
    if capture_errors:
 
198
        # construct minimal environment
 
199
        env = {}
 
200
        path = os.environ.get('PATH')
 
201
        if path is not None:
 
202
            env['PATH'] = path
 
203
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
 
204
        env['LANG'] = 'C'
 
205
        env['LC_ALL'] = 'C'
 
206
        stderr = subprocess.PIPE
 
207
    else:
 
208
        env = None
 
209
        stderr = None
 
210
 
 
211
    try:
 
212
        pipe = subprocess.Popen(diffcmd,
 
213
                                stdin=subprocess.PIPE,
 
214
                                stdout=subprocess.PIPE,
 
215
                                stderr=stderr,
 
216
                                env=env)
 
217
    except OSError as e:
 
218
        if e.errno == errno.ENOENT:
 
219
            raise errors.NoDiff(str(e))
 
220
        raise
 
221
 
 
222
    return pipe
 
223
 
 
224
 
 
225
# diff style options as of GNU diff v3.2
 
226
style_option_list = ['-c', '-C', '--context',
 
227
                     '-e', '--ed',
 
228
                     '-f', '--forward-ed',
 
229
                     '-q', '--brief',
 
230
                     '--normal',
 
231
                     '-n', '--rcs',
 
232
                     '-u', '-U', '--unified',
 
233
                     '-y', '--side-by-side',
 
234
                     '-D', '--ifdef']
 
235
 
 
236
 
 
237
def default_style_unified(diff_opts):
 
238
    """Default to unified diff style if alternative not specified in diff_opts.
 
239
 
 
240
        diff only allows one style to be specified; they don't override.
 
241
        Note that some of these take optargs, and the optargs can be
 
242
        directly appended to the options.
 
243
        This is only an approximate parser; it doesn't properly understand
 
244
        the grammar.
 
245
 
 
246
    :param diff_opts: List of options for external (GNU) diff.
 
247
    :return: List of options with default style=='unified'.
 
248
    """
 
249
    for s in style_option_list:
 
250
        for j in diff_opts:
 
251
            if j.startswith(s):
 
252
                break
 
253
        else:
 
254
            continue
 
255
        break
 
256
    else:
 
257
        diff_opts.append('-u')
 
258
    return diff_opts
 
259
 
 
260
 
 
261
def external_diff(old_label, oldlines, new_label, newlines, to_file,
 
262
                  diff_opts):
 
263
    """Display a diff by calling out to the external diff program."""
 
264
    # make sure our own output is properly ordered before the diff
 
265
    to_file.flush()
 
266
 
 
267
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='brz-diff-old-')
 
268
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='brz-diff-new-')
 
269
    oldtmpf = os.fdopen(oldtmp_fd, 'wb')
 
270
    newtmpf = os.fdopen(newtmp_fd, 'wb')
 
271
 
 
272
    try:
 
273
        # TODO: perhaps a special case for comparing to or from the empty
 
274
        # sequence; can just use /dev/null on Unix
 
275
 
 
276
        # TODO: if either of the files being compared already exists as a
 
277
        # regular named file (e.g. in the working directory) then we can
 
278
        # compare directly to that, rather than copying it.
 
279
 
 
280
        oldtmpf.writelines(oldlines)
 
281
        newtmpf.writelines(newlines)
 
282
 
 
283
        oldtmpf.close()
 
284
        newtmpf.close()
 
285
 
 
286
        if not diff_opts:
 
287
            diff_opts = []
 
288
        if sys.platform == 'win32':
 
289
            # Popen doesn't do the proper encoding for external commands
 
290
            # Since we are dealing with an ANSI api, use mbcs encoding
 
291
            old_label = old_label.encode('mbcs')
 
292
            new_label = new_label.encode('mbcs')
 
293
        diffcmd = ['diff',
 
294
                   '--label', old_label,
 
295
                   old_abspath,
 
296
                   '--label', new_label,
 
297
                   new_abspath,
 
298
                   '--binary',
 
299
                   ]
 
300
 
 
301
        diff_opts = default_style_unified(diff_opts)
 
302
 
 
303
        if diff_opts:
 
304
            diffcmd.extend(diff_opts)
 
305
 
 
306
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
 
307
        out, err = pipe.communicate()
 
308
        rc = pipe.returncode
 
309
 
 
310
        # internal_diff() adds a trailing newline, add one here for consistency
 
311
        out += b'\n'
 
312
        if rc == 2:
 
313
            # 'diff' gives retcode == 2 for all sorts of errors
 
314
            # one of those is 'Binary files differ'.
 
315
            # Bad options could also be the problem.
 
316
            # 'Binary files' is not a real error, so we suppress that error.
 
317
            lang_c_out = out
 
318
 
 
319
            # Since we got here, we want to make sure to give an i18n error
 
320
            pipe = _spawn_external_diff(diffcmd, capture_errors=False)
 
321
            out, err = pipe.communicate()
 
322
 
 
323
            # Write out the new i18n diff response
 
324
            to_file.write(out + b'\n')
 
325
            if pipe.returncode != 2:
 
326
                raise errors.BzrError(
 
327
                    'external diff failed with exit code 2'
 
328
                    ' when run with LANG=C and LC_ALL=C,'
 
329
                    ' but not when run natively: %r' % (diffcmd,))
 
330
 
 
331
            first_line = lang_c_out.split(b'\n', 1)[0]
 
332
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
 
333
            m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
 
334
            if m is None:
 
335
                raise errors.BzrError('external diff failed with exit code 2;'
 
336
                                      ' command: %r' % (diffcmd,))
 
337
            else:
 
338
                # Binary files differ, just return
 
339
                return
 
340
 
 
341
        # If we got to here, we haven't written out the output of diff
 
342
        # do so now
 
343
        to_file.write(out)
 
344
        if rc not in (0, 1):
 
345
            # returns 1 if files differ; that's OK
 
346
            if rc < 0:
 
347
                msg = 'signal %d' % (-rc)
 
348
            else:
 
349
                msg = 'exit code %d' % rc
 
350
 
 
351
            raise errors.BzrError('external diff failed with %s; command: %r'
 
352
                                  % (msg, diffcmd))
 
353
 
 
354
    finally:
 
355
        oldtmpf.close()                 # and delete
 
356
        newtmpf.close()
 
357
 
 
358
        def cleanup(path):
 
359
            # Warn in case the file couldn't be deleted (in case windows still
 
360
            # holds the file open, but not if the files have already been
 
361
            # deleted)
 
362
            try:
 
363
                os.remove(path)
 
364
            except OSError as e:
 
365
                if e.errno not in (errno.ENOENT,):
 
366
                    warning('Failed to delete temporary file: %s %s', path, e)
 
367
 
 
368
        cleanup(old_abspath)
 
369
        cleanup(new_abspath)
 
370
 
 
371
 
 
372
def get_trees_and_branches_to_diff_locked(
 
373
        path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
374
    """Get the trees and specific files to diff given a list of paths.
 
375
 
 
376
    This method works out the trees to be diff'ed and the files of
 
377
    interest within those trees.
 
378
 
 
379
    :param path_list:
 
380
        the list of arguments passed to the diff command
 
381
    :param revision_specs:
 
382
        Zero, one or two RevisionSpecs from the diff command line,
 
383
        saying what revisions to compare.
 
384
    :param old_url:
 
385
        The url of the old branch or tree. If None, the tree to use is
 
386
        taken from the first path, if any, or the current working tree.
 
387
    :param new_url:
 
388
        The url of the new branch or tree. If None, the tree to use is
 
389
        taken from the first path, if any, or the current working tree.
 
390
    :param add_cleanup:
 
391
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
392
        will register cleanups that must be run to unlock the trees, etc.
 
393
    :param apply_view:
 
394
        if True and a view is set, apply the view or check that the paths
 
395
        are within it
 
396
    :returns:
 
397
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
398
        specific_files, extra_trees) where extra_trees is a sequence of
 
399
        additional trees to search in for file-ids.  The trees and branches
 
400
        will be read-locked until the cleanups registered via the add_cleanup
 
401
        param are run.
 
402
    """
 
403
    # Get the old and new revision specs
 
404
    old_revision_spec = None
 
405
    new_revision_spec = None
 
406
    if revision_specs is not None:
 
407
        if len(revision_specs) > 0:
 
408
            old_revision_spec = revision_specs[0]
 
409
            if old_url is None:
 
410
                old_url = old_revision_spec.get_branch()
 
411
        if len(revision_specs) > 1:
 
412
            new_revision_spec = revision_specs[1]
 
413
            if new_url is None:
 
414
                new_url = new_revision_spec.get_branch()
 
415
 
 
416
    other_paths = []
 
417
    make_paths_wt_relative = True
 
418
    consider_relpath = True
 
419
    if path_list is None or len(path_list) == 0:
 
420
        # If no path is given, the current working tree is used
 
421
        default_location = u'.'
 
422
        consider_relpath = False
 
423
    elif old_url is not None and new_url is not None:
 
424
        other_paths = path_list
 
425
        make_paths_wt_relative = False
 
426
    else:
 
427
        default_location = path_list[0]
 
428
        other_paths = path_list[1:]
 
429
 
 
430
    def lock_tree_or_branch(wt, br):
 
431
        if wt is not None:
 
432
            wt.lock_read()
 
433
            add_cleanup(wt.unlock)
 
434
        elif br is not None:
 
435
            br.lock_read()
 
436
            add_cleanup(br.unlock)
 
437
 
 
438
    # Get the old location
 
439
    specific_files = []
 
440
    if old_url is None:
 
441
        old_url = default_location
 
442
    working_tree, branch, relpath = \
 
443
        controldir.ControlDir.open_containing_tree_or_branch(old_url)
 
444
    lock_tree_or_branch(working_tree, branch)
 
445
    if consider_relpath and relpath != '':
 
446
        if working_tree is not None and apply_view:
 
447
            views.check_path_in_view(working_tree, relpath)
 
448
        specific_files.append(relpath)
 
449
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
 
450
    old_branch = branch
 
451
 
 
452
    # Get the new location
 
453
    if new_url is None:
 
454
        new_url = default_location
 
455
    if new_url != old_url:
 
456
        working_tree, branch, relpath = \
 
457
            controldir.ControlDir.open_containing_tree_or_branch(new_url)
 
458
        lock_tree_or_branch(working_tree, branch)
 
459
        if consider_relpath and relpath != '':
 
460
            if working_tree is not None and apply_view:
 
461
                views.check_path_in_view(working_tree, relpath)
 
462
            specific_files.append(relpath)
 
463
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
 
464
                                 basis_is_default=working_tree is None)
 
465
    new_branch = branch
 
466
 
 
467
    # Get the specific files (all files is None, no files is [])
 
468
    if make_paths_wt_relative and working_tree is not None:
 
469
        other_paths = working_tree.safe_relpath_files(
 
470
            other_paths,
 
471
            apply_view=apply_view)
 
472
    specific_files.extend(other_paths)
 
473
    if len(specific_files) == 0:
 
474
        specific_files = None
 
475
        if (working_tree is not None and working_tree.supports_views() and
 
476
                apply_view):
 
477
            view_files = working_tree.views.lookup_view()
 
478
            if view_files:
 
479
                specific_files = view_files
 
480
                view_str = views.view_display_str(view_files)
 
481
                note(gettext("*** Ignoring files outside view. View is %s") % view_str)
 
482
 
 
483
    # Get extra trees that ought to be searched for file-ids
 
484
    extra_trees = None
 
485
    if working_tree is not None and working_tree not in (old_tree, new_tree):
 
486
        extra_trees = (working_tree,)
 
487
    return (old_tree, new_tree, old_branch, new_branch,
 
488
            specific_files, extra_trees)
 
489
 
 
490
 
 
491
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
 
492
    if branch is None and tree is not None:
 
493
        branch = tree.branch
 
494
    if spec is None or spec.spec is None:
 
495
        if basis_is_default:
 
496
            if tree is not None:
 
497
                return tree.basis_tree()
 
498
            else:
 
499
                return branch.basis_tree()
 
500
        else:
 
501
            return tree
 
502
    return spec.as_tree(branch)
 
503
 
 
504
 
 
505
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
 
506
                    external_diff_options=None,
 
507
                    old_label='a/', new_label='b/',
 
508
                    extra_trees=None,
 
509
                    path_encoding='utf8',
 
510
                    using=None,
 
511
                    format_cls=None,
 
512
                    context=DEFAULT_CONTEXT_AMOUNT):
 
513
    """Show in text form the changes from one tree to another.
 
514
 
 
515
    :param to_file: The output stream.
 
516
    :param specific_files: Include only changes to these files - None for all
 
517
        changes.
 
518
    :param external_diff_options: If set, use an external GNU diff and pass
 
519
        these options.
 
520
    :param extra_trees: If set, more Trees to use for looking up file ids
 
521
    :param path_encoding: If set, the path will be encoded as specified,
 
522
        otherwise is supposed to be utf8
 
523
    :param format_cls: Formatter class (DiffTree subclass)
 
524
    """
 
525
    if context is None:
 
526
        context = DEFAULT_CONTEXT_AMOUNT
 
527
    if format_cls is None:
 
528
        format_cls = DiffTree
 
529
    with old_tree.lock_read():
 
530
        if extra_trees is not None:
 
531
            for tree in extra_trees:
 
532
                tree.lock_read()
 
533
        new_tree.lock_read()
 
534
        try:
 
535
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
536
                                                   path_encoding,
 
537
                                                   external_diff_options,
 
538
                                                   old_label, new_label, using,
 
539
                                                   context_lines=context)
 
540
            return differ.show_diff(specific_files, extra_trees)
 
541
        finally:
 
542
            new_tree.unlock()
 
543
            if extra_trees is not None:
 
544
                for tree in extra_trees:
 
545
                    tree.unlock()
 
546
 
 
547
 
 
548
def _patch_header_date(tree, path):
 
549
    """Returns a timestamp suitable for use in a patch header."""
 
550
    try:
 
551
        mtime = tree.get_file_mtime(path)
 
552
    except FileTimestampUnavailable:
 
553
        mtime = 0
 
554
    return timestamp.format_patch_date(mtime)
 
555
 
 
556
 
 
557
def get_executable_change(old_is_x, new_is_x):
 
558
    descr = {True: b"+x", False: b"-x", None: b"??"}
 
559
    if old_is_x != new_is_x:
 
560
        return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
 
561
    else:
 
562
        return []
 
563
 
 
564
 
 
565
class DiffPath(object):
 
566
    """Base type for command object that compare files"""
 
567
 
 
568
    # The type or contents of the file were unsuitable for diffing
 
569
    CANNOT_DIFF = 'CANNOT_DIFF'
 
570
    # The file has changed in a semantic way
 
571
    CHANGED = 'CHANGED'
 
572
    # The file content may have changed, but there is no semantic change
 
573
    UNCHANGED = 'UNCHANGED'
 
574
 
 
575
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
 
576
        """Constructor.
 
577
 
 
578
        :param old_tree: The tree to show as the old tree in the comparison
 
579
        :param new_tree: The tree to show as new in the comparison
 
580
        :param to_file: The file to write comparison data to
 
581
        :param path_encoding: The character encoding to write paths in
 
582
        """
 
583
        self.old_tree = old_tree
 
584
        self.new_tree = new_tree
 
585
        self.to_file = to_file
 
586
        self.path_encoding = path_encoding
 
587
 
 
588
    def finish(self):
 
589
        pass
 
590
 
 
591
    @classmethod
 
592
    def from_diff_tree(klass, diff_tree):
 
593
        return klass(diff_tree.old_tree, diff_tree.new_tree,
 
594
                     diff_tree.to_file, diff_tree.path_encoding)
 
595
 
 
596
    @staticmethod
 
597
    def _diff_many(differs, old_path, new_path, old_kind, new_kind):
 
598
        for file_differ in differs:
 
599
            result = file_differ.diff(old_path, new_path, old_kind, new_kind)
 
600
            if result is not DiffPath.CANNOT_DIFF:
 
601
                return result
 
602
        else:
 
603
            return DiffPath.CANNOT_DIFF
 
604
 
 
605
 
 
606
class DiffKindChange(object):
 
607
    """Special differ for file kind changes.
 
608
 
 
609
    Represents kind change as deletion + creation.  Uses the other differs
 
610
    to do this.
 
611
    """
 
612
 
 
613
    def __init__(self, differs):
 
614
        self.differs = differs
 
615
 
 
616
    def finish(self):
 
617
        pass
 
618
 
 
619
    @classmethod
 
620
    def from_diff_tree(klass, diff_tree):
 
621
        return klass(diff_tree.differs)
 
622
 
 
623
    def diff(self, old_path, new_path, old_kind, new_kind):
 
624
        """Perform comparison
 
625
 
 
626
        :param old_path: Path of the file in the old tree
 
627
        :param new_path: Path of the file in the new tree
 
628
        :param old_kind: Old file-kind of the file
 
629
        :param new_kind: New file-kind of the file
 
630
        """
 
631
        if None in (old_kind, new_kind):
 
632
            return DiffPath.CANNOT_DIFF
 
633
        result = DiffPath._diff_many(
 
634
            self.differs, old_path, new_path, old_kind, None)
 
635
        if result is DiffPath.CANNOT_DIFF:
 
636
            return result
 
637
        return DiffPath._diff_many(
 
638
            self.differs, old_path, new_path, None, new_kind)
 
639
 
 
640
 
 
641
class DiffDirectory(DiffPath):
 
642
 
 
643
    def diff(self, old_path, new_path, old_kind, new_kind):
 
644
        """Perform comparison between two directories.  (dummy)
 
645
 
 
646
        """
 
647
        if 'directory' not in (old_kind, new_kind):
 
648
            return self.CANNOT_DIFF
 
649
        if old_kind not in ('directory', None):
 
650
            return self.CANNOT_DIFF
 
651
        if new_kind not in ('directory', None):
 
652
            return self.CANNOT_DIFF
 
653
        return self.CHANGED
 
654
 
 
655
 
 
656
class DiffSymlink(DiffPath):
 
657
 
 
658
    def diff(self, old_path, new_path, old_kind, new_kind):
 
659
        """Perform comparison between two symlinks
 
660
 
 
661
        :param old_path: Path of the file in the old tree
 
662
        :param new_path: Path of the file in the new tree
 
663
        :param old_kind: Old file-kind of the file
 
664
        :param new_kind: New file-kind of the file
 
665
        """
 
666
        if 'symlink' not in (old_kind, new_kind):
 
667
            return self.CANNOT_DIFF
 
668
        if old_kind == 'symlink':
 
669
            old_target = self.old_tree.get_symlink_target(old_path)
 
670
        elif old_kind is None:
 
671
            old_target = None
 
672
        else:
 
673
            return self.CANNOT_DIFF
 
674
        if new_kind == 'symlink':
 
675
            new_target = self.new_tree.get_symlink_target(new_path)
 
676
        elif new_kind is None:
 
677
            new_target = None
 
678
        else:
 
679
            return self.CANNOT_DIFF
 
680
        return self.diff_symlink(old_target, new_target)
 
681
 
 
682
    def diff_symlink(self, old_target, new_target):
 
683
        if old_target is None:
 
684
            self.to_file.write(b'=== target is \'%s\'\n' %
 
685
                               new_target.encode(self.path_encoding, 'replace'))
 
686
        elif new_target is None:
 
687
            self.to_file.write(b'=== target was \'%s\'\n' %
 
688
                               old_target.encode(self.path_encoding, 'replace'))
 
689
        else:
 
690
            self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
 
691
                               (old_target.encode(self.path_encoding, 'replace'),
 
692
                                new_target.encode(self.path_encoding, 'replace')))
 
693
        return self.CHANGED
 
694
 
 
695
 
 
696
class DiffText(DiffPath):
 
697
 
 
698
    # GNU Patch uses the epoch date to detect files that are being added
 
699
    # or removed in a diff.
 
700
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
 
701
 
 
702
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
703
                 old_label='', new_label='', text_differ=internal_diff,
 
704
                 context_lines=DEFAULT_CONTEXT_AMOUNT):
 
705
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
706
        self.text_differ = text_differ
 
707
        self.old_label = old_label
 
708
        self.new_label = new_label
 
709
        self.path_encoding = path_encoding
 
710
        self.context_lines = context_lines
 
711
 
 
712
    def diff(self, old_path, new_path, old_kind, new_kind):
 
713
        """Compare two files in unified diff format
 
714
 
 
715
        :param old_path: Path of the file in the old tree
 
716
        :param new_path: Path of the file in the new tree
 
717
        :param old_kind: Old file-kind of the file
 
718
        :param new_kind: New file-kind of the file
 
719
        """
 
720
        if 'file' not in (old_kind, new_kind):
 
721
            return self.CANNOT_DIFF
 
722
        if old_kind == 'file':
 
723
            old_date = _patch_header_date(self.old_tree, old_path)
 
724
        elif old_kind is None:
 
725
            old_date = self.EPOCH_DATE
 
726
        else:
 
727
            return self.CANNOT_DIFF
 
728
        if new_kind == 'file':
 
729
            new_date = _patch_header_date(self.new_tree, new_path)
 
730
        elif new_kind is None:
 
731
            new_date = self.EPOCH_DATE
 
732
        else:
 
733
            return self.CANNOT_DIFF
 
734
        from_label = '%s%s\t%s' % (self.old_label, old_path,
 
735
                                   old_date)
 
736
        to_label = '%s%s\t%s' % (self.new_label, new_path,
 
737
                                 new_date)
 
738
        return self.diff_text(old_path, new_path, from_label, to_label)
 
739
 
 
740
    def diff_text(self, from_path, to_path, from_label, to_label):
 
741
        """Diff the content of given files in two trees
 
742
 
 
743
        :param from_path: The path in the from tree. If None,
 
744
            the file is not present in the from tree.
 
745
        :param to_path: The path in the to tree. This may refer
 
746
            to a different file from from_path.  If None,
 
747
            the file is not present in the to tree.
 
748
        """
 
749
        def _get_text(tree, path):
 
750
            if path is None:
 
751
                return []
 
752
            try:
 
753
                return tree.get_file_lines(path)
 
754
            except errors.NoSuchFile:
 
755
                return []
 
756
        try:
 
757
            from_text = _get_text(self.old_tree, from_path)
 
758
            to_text = _get_text(self.new_tree, to_path)
 
759
            self.text_differ(from_label, from_text, to_label, to_text,
 
760
                             self.to_file, path_encoding=self.path_encoding,
 
761
                             context_lines=self.context_lines)
 
762
        except errors.BinaryFile:
 
763
            self.to_file.write(
 
764
                ("Binary files %s and %s differ\n" %
 
765
                 (from_label, to_label)).encode(self.path_encoding, 'replace'))
 
766
        return self.CHANGED
 
767
 
 
768
 
 
769
class DiffFromTool(DiffPath):
 
770
 
 
771
    def __init__(self, command_template, old_tree, new_tree, to_file,
 
772
                 path_encoding='utf-8'):
 
773
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
774
        self.command_template = command_template
 
775
        self._root = osutils.mkdtemp(prefix='brz-diff-')
 
776
 
 
777
    @classmethod
 
778
    def from_string(klass, command_string, old_tree, new_tree, to_file,
 
779
                    path_encoding='utf-8'):
 
780
        command_template = cmdline.split(command_string)
 
781
        if '@' not in command_string:
 
782
            command_template.extend(['@old_path', '@new_path'])
 
783
        return klass(command_template, old_tree, new_tree, to_file,
 
784
                     path_encoding)
 
785
 
 
786
    @classmethod
 
787
    def make_from_diff_tree(klass, command_string, external_diff_options=None):
 
788
        def from_diff_tree(diff_tree):
 
789
            full_command_string = [command_string]
 
790
            if external_diff_options is not None:
 
791
                full_command_string += ' ' + external_diff_options
 
792
            return klass.from_string(full_command_string, diff_tree.old_tree,
 
793
                                     diff_tree.new_tree, diff_tree.to_file)
 
794
        return from_diff_tree
 
795
 
 
796
    def _get_command(self, old_path, new_path):
 
797
        my_map = {'old_path': old_path, 'new_path': new_path}
 
798
        command = [AtTemplate(t).substitute(my_map) for t in
 
799
                   self.command_template]
 
800
        if sys.platform == 'win32':  # Popen doesn't accept unicode on win32
 
801
            command_encoded = []
 
802
            for c in command:
 
803
                if isinstance(c, text_type):
 
804
                    command_encoded.append(c.encode('mbcs'))
 
805
                else:
 
806
                    command_encoded.append(c)
 
807
            return command_encoded
 
808
        else:
 
809
            return command
 
810
 
 
811
    def _execute(self, old_path, new_path):
 
812
        command = self._get_command(old_path, new_path)
 
813
        try:
 
814
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
 
815
                                    cwd=self._root)
 
816
        except OSError as e:
 
817
            if e.errno == errno.ENOENT:
 
818
                raise errors.ExecutableMissing(command[0])
 
819
            else:
 
820
                raise
 
821
        self.to_file.write(proc.stdout.read())
 
822
        proc.stdout.close()
 
823
        return proc.wait()
 
824
 
 
825
    def _try_symlink_root(self, tree, prefix):
 
826
        if (getattr(tree, 'abspath', None) is None or
 
827
                not osutils.host_os_dereferences_symlinks()):
 
828
            return False
 
829
        try:
 
830
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
 
831
        except OSError as e:
 
832
            if e.errno != errno.EEXIST:
 
833
                raise
 
834
        return True
 
835
 
 
836
    @staticmethod
 
837
    def _fenc():
 
838
        """Returns safe encoding for passing file path to diff tool"""
 
839
        if sys.platform == 'win32':
 
840
            return 'mbcs'
 
841
        else:
 
842
            # Don't fallback to 'utf-8' because subprocess may not be able to
 
843
            # handle utf-8 correctly when locale is not utf-8.
 
844
            return sys.getfilesystemencoding() or 'ascii'
 
845
 
 
846
    def _is_safepath(self, path):
 
847
        """Return true if `path` may be able to pass to subprocess."""
 
848
        fenc = self._fenc()
 
849
        try:
 
850
            return path == path.encode(fenc).decode(fenc)
 
851
        except UnicodeError:
 
852
            return False
 
853
 
 
854
    def _safe_filename(self, prefix, relpath):
 
855
        """Replace unsafe character in `relpath` then join `self._root`,
 
856
        `prefix` and `relpath`."""
 
857
        fenc = self._fenc()
 
858
        # encoded_str.replace('?', '_') may break multibyte char.
 
859
        # So we should encode, decode, then replace(u'?', u'_')
 
860
        relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
 
861
        relpath_tmp = relpath_tmp.replace(u'?', u'_')
 
862
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
 
863
 
 
864
    def _write_file(self, relpath, tree, prefix, force_temp=False,
 
865
                    allow_write=False):
 
866
        if not force_temp and isinstance(tree, WorkingTree):
 
867
            full_path = tree.abspath(relpath)
 
868
            if self._is_safepath(full_path):
 
869
                return full_path
 
870
 
 
871
        full_path = self._safe_filename(prefix, relpath)
 
872
        if not force_temp and self._try_symlink_root(tree, prefix):
 
873
            return full_path
 
874
        parent_dir = osutils.dirname(full_path)
 
875
        try:
 
876
            os.makedirs(parent_dir)
 
877
        except OSError as e:
 
878
            if e.errno != errno.EEXIST:
 
879
                raise
 
880
        source = tree.get_file(relpath)
 
881
        try:
 
882
            with open(full_path, 'wb') as target:
 
883
                osutils.pumpfile(source, target)
 
884
        finally:
 
885
            source.close()
 
886
        try:
 
887
            mtime = tree.get_file_mtime(relpath)
 
888
        except FileTimestampUnavailable:
 
889
            pass
 
890
        else:
 
891
            os.utime(full_path, (mtime, mtime))
 
892
        if not allow_write:
 
893
            osutils.make_readonly(full_path)
 
894
        return full_path
 
895
 
 
896
    def _prepare_files(self, old_path, new_path, force_temp=False,
 
897
                       allow_write_new=False):
 
898
        old_disk_path = self._write_file(
 
899
            old_path, self.old_tree, 'old', force_temp)
 
900
        new_disk_path = self._write_file(
 
901
            new_path, self.new_tree, 'new', force_temp,
 
902
            allow_write=allow_write_new)
 
903
        return old_disk_path, new_disk_path
 
904
 
 
905
    def finish(self):
 
906
        try:
 
907
            osutils.rmtree(self._root)
 
908
        except OSError as e:
 
909
            if e.errno != errno.ENOENT:
 
910
                mutter("The temporary directory \"%s\" was not "
 
911
                       "cleanly removed: %s." % (self._root, e))
 
912
 
 
913
    def diff(self, old_path, new_path, old_kind, new_kind):
 
914
        if (old_kind, new_kind) != ('file', 'file'):
 
915
            return DiffPath.CANNOT_DIFF
 
916
        (old_disk_path, new_disk_path) = self._prepare_files(
 
917
            old_path, new_path)
 
918
        self._execute(old_disk_path, new_disk_path)
 
919
 
 
920
    def edit_file(self, old_path, new_path):
 
921
        """Use this tool to edit a file.
 
922
 
 
923
        A temporary copy will be edited, and the new contents will be
 
924
        returned.
 
925
 
 
926
        :return: The new contents of the file.
 
927
        """
 
928
        old_abs_path, new_abs_path = self._prepare_files(
 
929
            old_path, new_path, allow_write_new=True, force_temp=True)
 
930
        command = self._get_command(old_abs_path, new_abs_path)
 
931
        subprocess.call(command, cwd=self._root)
 
932
        with open(new_abs_path, 'rb') as new_file:
 
933
            return new_file.read()
 
934
 
 
935
 
 
936
class DiffTree(object):
 
937
    """Provides textual representations of the difference between two trees.
 
938
 
 
939
    A DiffTree examines two trees and where a file-id has altered
 
940
    between them, generates a textual representation of the difference.
 
941
    DiffTree uses a sequence of DiffPath objects which are each
 
942
    given the opportunity to handle a given altered fileid. The list
 
943
    of DiffPath objects can be extended globally by appending to
 
944
    DiffTree.diff_factories, or for a specific diff operation by
 
945
    supplying the extra_factories option to the appropriate method.
 
946
    """
 
947
 
 
948
    # list of factories that can provide instances of DiffPath objects
 
949
    # may be extended by plugins.
 
950
    diff_factories = [DiffSymlink.from_diff_tree,
 
951
                      DiffDirectory.from_diff_tree]
 
952
 
 
953
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
954
                 diff_text=None, extra_factories=None):
 
955
        """Constructor
 
956
 
 
957
        :param old_tree: Tree to show as old in the comparison
 
958
        :param new_tree: Tree to show as new in the comparison
 
959
        :param to_file: File to write comparision to
 
960
        :param path_encoding: Character encoding to write paths in
 
961
        :param diff_text: DiffPath-type object to use as a last resort for
 
962
            diffing text files.
 
963
        :param extra_factories: Factories of DiffPaths to try before any other
 
964
            DiffPaths"""
 
965
        if diff_text is None:
 
966
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
967
                                 '', '', internal_diff)
 
968
        self.old_tree = old_tree
 
969
        self.new_tree = new_tree
 
970
        self.to_file = to_file
 
971
        self.path_encoding = path_encoding
 
972
        self.differs = []
 
973
        if extra_factories is not None:
 
974
            self.differs.extend(f(self) for f in extra_factories)
 
975
        self.differs.extend(f(self) for f in self.diff_factories)
 
976
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
 
977
 
 
978
    @classmethod
 
979
    def from_trees_options(klass, old_tree, new_tree, to_file,
 
980
                           path_encoding, external_diff_options, old_label,
 
981
                           new_label, using, context_lines):
 
982
        """Factory for producing a DiffTree.
 
983
 
 
984
        Designed to accept options used by show_diff_trees.
 
985
 
 
986
        :param old_tree: The tree to show as old in the comparison
 
987
        :param new_tree: The tree to show as new in the comparison
 
988
        :param to_file: File to write comparisons to
 
989
        :param path_encoding: Character encoding to use for writing paths
 
990
        :param external_diff_options: If supplied, use the installed diff
 
991
            binary to perform file comparison, using supplied options.
 
992
        :param old_label: Prefix to use for old file labels
 
993
        :param new_label: Prefix to use for new file labels
 
994
        :param using: Commandline to use to invoke an external diff tool
 
995
        """
 
996
        if using is not None:
 
997
            extra_factories = [DiffFromTool.make_from_diff_tree(
 
998
                using, external_diff_options)]
 
999
        else:
 
1000
            extra_factories = []
 
1001
        if external_diff_options:
 
1002
            opts = external_diff_options.split()
 
1003
 
 
1004
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
 
1005
                """:param path_encoding: not used but required
 
1006
                        to match the signature of internal_diff.
 
1007
                """
 
1008
                external_diff(olab, olines, nlab, nlines, to_file, opts)
 
1009
        else:
 
1010
            diff_file = internal_diff
 
1011
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
1012
                             old_label, new_label, diff_file, context_lines=context_lines)
 
1013
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
 
1014
                     extra_factories)
 
1015
 
 
1016
    def show_diff(self, specific_files, extra_trees=None):
 
1017
        """Write tree diff to self.to_file
 
1018
 
 
1019
        :param specific_files: the specific files to compare (recursive)
 
1020
        :param extra_trees: extra trees to use for mapping paths to file_ids
 
1021
        """
 
1022
        try:
 
1023
            return self._show_diff(specific_files, extra_trees)
 
1024
        finally:
 
1025
            for differ in self.differs:
 
1026
                differ.finish()
 
1027
 
 
1028
    def _show_diff(self, specific_files, extra_trees):
 
1029
        # TODO: Generation of pseudo-diffs for added/deleted files could
 
1030
        # be usefully made into a much faster special case.
 
1031
        iterator = self.new_tree.iter_changes(self.old_tree,
 
1032
                                              specific_files=specific_files,
 
1033
                                              extra_trees=extra_trees,
 
1034
                                              require_versioned=True)
 
1035
        has_changes = 0
 
1036
 
 
1037
        def changes_key(change):
 
1038
            old_path, new_path = change[1]
 
1039
            path = new_path
 
1040
            if path is None:
 
1041
                path = old_path
 
1042
            return path
 
1043
 
 
1044
        def get_encoded_path(path):
 
1045
            if path is not None:
 
1046
                return path.encode(self.path_encoding, "replace")
 
1047
        for (file_id, paths, changed_content, versioned, parent, name, kind,
 
1048
             executable) in sorted(iterator, key=changes_key):
 
1049
            # The root does not get diffed, and items with no known kind (that
 
1050
            # is, missing) in both trees are skipped as well.
 
1051
            if parent == (None, None) or kind == (None, None):
 
1052
                continue
 
1053
            oldpath, newpath = paths
 
1054
            oldpath_encoded = get_encoded_path(paths[0])
 
1055
            newpath_encoded = get_encoded_path(paths[1])
 
1056
            old_present = (kind[0] is not None and versioned[0])
 
1057
            new_present = (kind[1] is not None and versioned[1])
 
1058
            renamed = (parent[0], name[0]) != (parent[1], name[1])
 
1059
 
 
1060
            properties_changed = []
 
1061
            properties_changed.extend(
 
1062
                get_executable_change(executable[0], executable[1]))
 
1063
 
 
1064
            if properties_changed:
 
1065
                prop_str = b" (properties changed: %s)" % (
 
1066
                    b", ".join(properties_changed),)
 
1067
            else:
 
1068
                prop_str = b""
 
1069
 
 
1070
            if (old_present, new_present) == (True, False):
 
1071
                self.to_file.write(b"=== removed %s '%s'\n" %
 
1072
                                   (kind[0].encode('ascii'), oldpath_encoded))
 
1073
                newpath = oldpath
 
1074
            elif (old_present, new_present) == (False, True):
 
1075
                self.to_file.write(b"=== added %s '%s'\n" %
 
1076
                                   (kind[1].encode('ascii'), newpath_encoded))
 
1077
                oldpath = newpath
 
1078
            elif renamed:
 
1079
                self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
 
1080
                                   (kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
 
1081
            else:
 
1082
                # if it was produced by iter_changes, it must be
 
1083
                # modified *somehow*, either content or execute bit.
 
1084
                self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
 
1085
                                                                  newpath_encoded, prop_str))
 
1086
            if changed_content:
 
1087
                self._diff(oldpath, newpath, kind[0], kind[1])
 
1088
                has_changes = 1
 
1089
            if renamed:
 
1090
                has_changes = 1
 
1091
        return has_changes
 
1092
 
 
1093
    def diff(self, old_path, new_path):
 
1094
        """Perform a diff of a single file
 
1095
 
 
1096
        :param old_path: The path of the file in the old tree
 
1097
        :param new_path: The path of the file in the new tree
 
1098
        """
 
1099
        if old_path is None:
 
1100
            old_kind = None
 
1101
        else:
 
1102
            old_kind = self.old_tree.kind(old_path)
 
1103
        if new_path is None:
 
1104
            new_kind = None
 
1105
        else:
 
1106
            new_kind = self.new_tree.kind(new_path)
 
1107
        self._diff(old_path, new_path, old_kind, new_kind)
 
1108
 
 
1109
    def _diff(self, old_path, new_path, old_kind, new_kind):
 
1110
        result = DiffPath._diff_many(
 
1111
            self.differs, old_path, new_path, old_kind, new_kind)
 
1112
        if result is DiffPath.CANNOT_DIFF:
 
1113
            error_path = new_path
 
1114
            if error_path is None:
 
1115
                error_path = old_path
 
1116
            raise errors.NoDiffFound(error_path)
 
1117
 
 
1118
 
 
1119
format_registry = Registry()
 
1120
format_registry.register('default', DiffTree)