/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-29 13:16:26 UTC
  • mto: This revision was merged to the branch mainline in revision 7376.
  • Revision ID: jelmer@jelmer.uk-20190629131626-qioafloyemhdbm4w
Remove Tree.get_root_id() in favour of Tree.path2id('').

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 DiffTreeReference(DiffPath):
 
642
 
 
643
    def diff(self, old_path, new_path, old_kind, new_kind):
 
644
        """Perform comparison between two tree references.  (dummy)
 
645
 
 
646
        """
 
647
        if 'tree-reference' not in (old_kind, new_kind):
 
648
            return self.CANNOT_DIFF
 
649
        if old_kind not in ('tree-reference', None):
 
650
            return self.CANNOT_DIFF
 
651
        if new_kind not in ('tree-reference', None):
 
652
            return self.CANNOT_DIFF
 
653
        return self.CHANGED
 
654
 
 
655
 
 
656
class DiffDirectory(DiffPath):
 
657
 
 
658
    def diff(self, old_path, new_path, old_kind, new_kind):
 
659
        """Perform comparison between two directories.  (dummy)
 
660
 
 
661
        """
 
662
        if 'directory' not in (old_kind, new_kind):
 
663
            return self.CANNOT_DIFF
 
664
        if old_kind not in ('directory', None):
 
665
            return self.CANNOT_DIFF
 
666
        if new_kind not in ('directory', None):
 
667
            return self.CANNOT_DIFF
 
668
        return self.CHANGED
 
669
 
 
670
 
 
671
class DiffSymlink(DiffPath):
 
672
 
 
673
    def diff(self, old_path, new_path, old_kind, new_kind):
 
674
        """Perform comparison between two symlinks
 
675
 
 
676
        :param old_path: Path of the file in the old tree
 
677
        :param new_path: Path of the file in the new tree
 
678
        :param old_kind: Old file-kind of the file
 
679
        :param new_kind: New file-kind of the file
 
680
        """
 
681
        if 'symlink' not in (old_kind, new_kind):
 
682
            return self.CANNOT_DIFF
 
683
        if old_kind == 'symlink':
 
684
            old_target = self.old_tree.get_symlink_target(old_path)
 
685
        elif old_kind is None:
 
686
            old_target = None
 
687
        else:
 
688
            return self.CANNOT_DIFF
 
689
        if new_kind == 'symlink':
 
690
            new_target = self.new_tree.get_symlink_target(new_path)
 
691
        elif new_kind is None:
 
692
            new_target = None
 
693
        else:
 
694
            return self.CANNOT_DIFF
 
695
        return self.diff_symlink(old_target, new_target)
 
696
 
 
697
    def diff_symlink(self, old_target, new_target):
 
698
        if old_target is None:
 
699
            self.to_file.write(b'=== target is \'%s\'\n' %
 
700
                               new_target.encode(self.path_encoding, 'replace'))
 
701
        elif new_target is None:
 
702
            self.to_file.write(b'=== target was \'%s\'\n' %
 
703
                               old_target.encode(self.path_encoding, 'replace'))
 
704
        else:
 
705
            self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
 
706
                               (old_target.encode(self.path_encoding, 'replace'),
 
707
                                new_target.encode(self.path_encoding, 'replace')))
 
708
        return self.CHANGED
 
709
 
 
710
 
 
711
class DiffText(DiffPath):
 
712
 
 
713
    # GNU Patch uses the epoch date to detect files that are being added
 
714
    # or removed in a diff.
 
715
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
 
716
 
 
717
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
718
                 old_label='', new_label='', text_differ=internal_diff,
 
719
                 context_lines=DEFAULT_CONTEXT_AMOUNT):
 
720
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
721
        self.text_differ = text_differ
 
722
        self.old_label = old_label
 
723
        self.new_label = new_label
 
724
        self.path_encoding = path_encoding
 
725
        self.context_lines = context_lines
 
726
 
 
727
    def diff(self, old_path, new_path, old_kind, new_kind):
 
728
        """Compare two files in unified diff format
 
729
 
 
730
        :param old_path: Path of the file in the old tree
 
731
        :param new_path: Path of the file in the new tree
 
732
        :param old_kind: Old file-kind of the file
 
733
        :param new_kind: New file-kind of the file
 
734
        """
 
735
        if 'file' not in (old_kind, new_kind):
 
736
            return self.CANNOT_DIFF
 
737
        if old_kind == 'file':
 
738
            old_date = _patch_header_date(self.old_tree, old_path)
 
739
        elif old_kind is None:
 
740
            old_date = self.EPOCH_DATE
 
741
        else:
 
742
            return self.CANNOT_DIFF
 
743
        if new_kind == 'file':
 
744
            new_date = _patch_header_date(self.new_tree, new_path)
 
745
        elif new_kind is None:
 
746
            new_date = self.EPOCH_DATE
 
747
        else:
 
748
            return self.CANNOT_DIFF
 
749
        from_label = '%s%s\t%s' % (self.old_label, old_path,
 
750
                                   old_date)
 
751
        to_label = '%s%s\t%s' % (self.new_label, new_path,
 
752
                                 new_date)
 
753
        return self.diff_text(old_path, new_path, from_label, to_label)
 
754
 
 
755
    def diff_text(self, from_path, to_path, from_label, to_label):
 
756
        """Diff the content of given files in two trees
 
757
 
 
758
        :param from_path: The path in the from tree. If None,
 
759
            the file is not present in the from tree.
 
760
        :param to_path: The path in the to tree. This may refer
 
761
            to a different file from from_path.  If None,
 
762
            the file is not present in the to tree.
 
763
        """
 
764
        def _get_text(tree, path):
 
765
            if path is None:
 
766
                return []
 
767
            try:
 
768
                return tree.get_file_lines(path)
 
769
            except errors.NoSuchFile:
 
770
                return []
 
771
        try:
 
772
            from_text = _get_text(self.old_tree, from_path)
 
773
            to_text = _get_text(self.new_tree, to_path)
 
774
            self.text_differ(from_label, from_text, to_label, to_text,
 
775
                             self.to_file, path_encoding=self.path_encoding,
 
776
                             context_lines=self.context_lines)
 
777
        except errors.BinaryFile:
 
778
            self.to_file.write(
 
779
                ("Binary files %s and %s differ\n" %
 
780
                 (from_label, to_label)).encode(self.path_encoding, 'replace'))
 
781
        return self.CHANGED
 
782
 
 
783
 
 
784
class DiffFromTool(DiffPath):
 
785
 
 
786
    def __init__(self, command_template, old_tree, new_tree, to_file,
 
787
                 path_encoding='utf-8'):
 
788
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
789
        self.command_template = command_template
 
790
        self._root = osutils.mkdtemp(prefix='brz-diff-')
 
791
 
 
792
    @classmethod
 
793
    def from_string(klass, command_string, old_tree, new_tree, to_file,
 
794
                    path_encoding='utf-8'):
 
795
        command_template = cmdline.split(command_string)
 
796
        if '@' not in command_string:
 
797
            command_template.extend(['@old_path', '@new_path'])
 
798
        return klass(command_template, old_tree, new_tree, to_file,
 
799
                     path_encoding)
 
800
 
 
801
    @classmethod
 
802
    def make_from_diff_tree(klass, command_string, external_diff_options=None):
 
803
        def from_diff_tree(diff_tree):
 
804
            full_command_string = [command_string]
 
805
            if external_diff_options is not None:
 
806
                full_command_string += ' ' + external_diff_options
 
807
            return klass.from_string(full_command_string, diff_tree.old_tree,
 
808
                                     diff_tree.new_tree, diff_tree.to_file)
 
809
        return from_diff_tree
 
810
 
 
811
    def _get_command(self, old_path, new_path):
 
812
        my_map = {'old_path': old_path, 'new_path': new_path}
 
813
        command = [AtTemplate(t).substitute(my_map) for t in
 
814
                   self.command_template]
 
815
        if sys.platform == 'win32':  # Popen doesn't accept unicode on win32
 
816
            command_encoded = []
 
817
            for c in command:
 
818
                if isinstance(c, text_type):
 
819
                    command_encoded.append(c.encode('mbcs'))
 
820
                else:
 
821
                    command_encoded.append(c)
 
822
            return command_encoded
 
823
        else:
 
824
            return command
 
825
 
 
826
    def _execute(self, old_path, new_path):
 
827
        command = self._get_command(old_path, new_path)
 
828
        try:
 
829
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
 
830
                                    cwd=self._root)
 
831
        except OSError as e:
 
832
            if e.errno == errno.ENOENT:
 
833
                raise errors.ExecutableMissing(command[0])
 
834
            else:
 
835
                raise
 
836
        self.to_file.write(proc.stdout.read())
 
837
        proc.stdout.close()
 
838
        return proc.wait()
 
839
 
 
840
    def _try_symlink_root(self, tree, prefix):
 
841
        if (getattr(tree, 'abspath', None) is None or
 
842
                not osutils.host_os_dereferences_symlinks()):
 
843
            return False
 
844
        try:
 
845
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
 
846
        except OSError as e:
 
847
            if e.errno != errno.EEXIST:
 
848
                raise
 
849
        return True
 
850
 
 
851
    @staticmethod
 
852
    def _fenc():
 
853
        """Returns safe encoding for passing file path to diff tool"""
 
854
        if sys.platform == 'win32':
 
855
            return 'mbcs'
 
856
        else:
 
857
            # Don't fallback to 'utf-8' because subprocess may not be able to
 
858
            # handle utf-8 correctly when locale is not utf-8.
 
859
            return sys.getfilesystemencoding() or 'ascii'
 
860
 
 
861
    def _is_safepath(self, path):
 
862
        """Return true if `path` may be able to pass to subprocess."""
 
863
        fenc = self._fenc()
 
864
        try:
 
865
            return path == path.encode(fenc).decode(fenc)
 
866
        except UnicodeError:
 
867
            return False
 
868
 
 
869
    def _safe_filename(self, prefix, relpath):
 
870
        """Replace unsafe character in `relpath` then join `self._root`,
 
871
        `prefix` and `relpath`."""
 
872
        fenc = self._fenc()
 
873
        # encoded_str.replace('?', '_') may break multibyte char.
 
874
        # So we should encode, decode, then replace(u'?', u'_')
 
875
        relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
 
876
        relpath_tmp = relpath_tmp.replace(u'?', u'_')
 
877
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
 
878
 
 
879
    def _write_file(self, relpath, tree, prefix, force_temp=False,
 
880
                    allow_write=False):
 
881
        if not force_temp and isinstance(tree, WorkingTree):
 
882
            full_path = tree.abspath(relpath)
 
883
            if self._is_safepath(full_path):
 
884
                return full_path
 
885
 
 
886
        full_path = self._safe_filename(prefix, relpath)
 
887
        if not force_temp and self._try_symlink_root(tree, prefix):
 
888
            return full_path
 
889
        parent_dir = osutils.dirname(full_path)
 
890
        try:
 
891
            os.makedirs(parent_dir)
 
892
        except OSError as e:
 
893
            if e.errno != errno.EEXIST:
 
894
                raise
 
895
        source = tree.get_file(relpath)
 
896
        try:
 
897
            with open(full_path, 'wb') as target:
 
898
                osutils.pumpfile(source, target)
 
899
        finally:
 
900
            source.close()
 
901
        try:
 
902
            mtime = tree.get_file_mtime(relpath)
 
903
        except FileTimestampUnavailable:
 
904
            pass
 
905
        else:
 
906
            os.utime(full_path, (mtime, mtime))
 
907
        if not allow_write:
 
908
            osutils.make_readonly(full_path)
 
909
        return full_path
 
910
 
 
911
    def _prepare_files(self, old_path, new_path, force_temp=False,
 
912
                       allow_write_new=False):
 
913
        old_disk_path = self._write_file(
 
914
            old_path, self.old_tree, 'old', force_temp)
 
915
        new_disk_path = self._write_file(
 
916
            new_path, self.new_tree, 'new', force_temp,
 
917
            allow_write=allow_write_new)
 
918
        return old_disk_path, new_disk_path
 
919
 
 
920
    def finish(self):
 
921
        try:
 
922
            osutils.rmtree(self._root)
 
923
        except OSError as e:
 
924
            if e.errno != errno.ENOENT:
 
925
                mutter("The temporary directory \"%s\" was not "
 
926
                       "cleanly removed: %s." % (self._root, e))
 
927
 
 
928
    def diff(self, old_path, new_path, old_kind, new_kind):
 
929
        if (old_kind, new_kind) != ('file', 'file'):
 
930
            return DiffPath.CANNOT_DIFF
 
931
        (old_disk_path, new_disk_path) = self._prepare_files(
 
932
            old_path, new_path)
 
933
        self._execute(old_disk_path, new_disk_path)
 
934
 
 
935
    def edit_file(self, old_path, new_path):
 
936
        """Use this tool to edit a file.
 
937
 
 
938
        A temporary copy will be edited, and the new contents will be
 
939
        returned.
 
940
 
 
941
        :return: The new contents of the file.
 
942
        """
 
943
        old_abs_path, new_abs_path = self._prepare_files(
 
944
            old_path, new_path, allow_write_new=True, force_temp=True)
 
945
        command = self._get_command(old_abs_path, new_abs_path)
 
946
        subprocess.call(command, cwd=self._root)
 
947
        with open(new_abs_path, 'rb') as new_file:
 
948
            return new_file.read()
 
949
 
 
950
 
 
951
class DiffTree(object):
 
952
    """Provides textual representations of the difference between two trees.
 
953
 
 
954
    A DiffTree examines two trees and where a file-id has altered
 
955
    between them, generates a textual representation of the difference.
 
956
    DiffTree uses a sequence of DiffPath objects which are each
 
957
    given the opportunity to handle a given altered fileid. The list
 
958
    of DiffPath objects can be extended globally by appending to
 
959
    DiffTree.diff_factories, or for a specific diff operation by
 
960
    supplying the extra_factories option to the appropriate method.
 
961
    """
 
962
 
 
963
    # list of factories that can provide instances of DiffPath objects
 
964
    # may be extended by plugins.
 
965
    diff_factories = [DiffSymlink.from_diff_tree,
 
966
                      DiffDirectory.from_diff_tree,
 
967
                      DiffTreeReference.from_diff_tree]
 
968
 
 
969
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
970
                 diff_text=None, extra_factories=None):
 
971
        """Constructor
 
972
 
 
973
        :param old_tree: Tree to show as old in the comparison
 
974
        :param new_tree: Tree to show as new in the comparison
 
975
        :param to_file: File to write comparision to
 
976
        :param path_encoding: Character encoding to write paths in
 
977
        :param diff_text: DiffPath-type object to use as a last resort for
 
978
            diffing text files.
 
979
        :param extra_factories: Factories of DiffPaths to try before any other
 
980
            DiffPaths"""
 
981
        if diff_text is None:
 
982
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
983
                                 '', '', internal_diff)
 
984
        self.old_tree = old_tree
 
985
        self.new_tree = new_tree
 
986
        self.to_file = to_file
 
987
        self.path_encoding = path_encoding
 
988
        self.differs = []
 
989
        if extra_factories is not None:
 
990
            self.differs.extend(f(self) for f in extra_factories)
 
991
        self.differs.extend(f(self) for f in self.diff_factories)
 
992
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
 
993
 
 
994
    @classmethod
 
995
    def from_trees_options(klass, old_tree, new_tree, to_file,
 
996
                           path_encoding, external_diff_options, old_label,
 
997
                           new_label, using, context_lines):
 
998
        """Factory for producing a DiffTree.
 
999
 
 
1000
        Designed to accept options used by show_diff_trees.
 
1001
 
 
1002
        :param old_tree: The tree to show as old in the comparison
 
1003
        :param new_tree: The tree to show as new in the comparison
 
1004
        :param to_file: File to write comparisons to
 
1005
        :param path_encoding: Character encoding to use for writing paths
 
1006
        :param external_diff_options: If supplied, use the installed diff
 
1007
            binary to perform file comparison, using supplied options.
 
1008
        :param old_label: Prefix to use for old file labels
 
1009
        :param new_label: Prefix to use for new file labels
 
1010
        :param using: Commandline to use to invoke an external diff tool
 
1011
        """
 
1012
        if using is not None:
 
1013
            extra_factories = [DiffFromTool.make_from_diff_tree(
 
1014
                using, external_diff_options)]
 
1015
        else:
 
1016
            extra_factories = []
 
1017
        if external_diff_options:
 
1018
            opts = external_diff_options.split()
 
1019
 
 
1020
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
 
1021
                """:param path_encoding: not used but required
 
1022
                        to match the signature of internal_diff.
 
1023
                """
 
1024
                external_diff(olab, olines, nlab, nlines, to_file, opts)
 
1025
        else:
 
1026
            diff_file = internal_diff
 
1027
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
1028
                             old_label, new_label, diff_file, context_lines=context_lines)
 
1029
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
 
1030
                     extra_factories)
 
1031
 
 
1032
    def show_diff(self, specific_files, extra_trees=None):
 
1033
        """Write tree diff to self.to_file
 
1034
 
 
1035
        :param specific_files: the specific files to compare (recursive)
 
1036
        :param extra_trees: extra trees to use for mapping paths to file_ids
 
1037
        """
 
1038
        try:
 
1039
            return self._show_diff(specific_files, extra_trees)
 
1040
        finally:
 
1041
            for differ in self.differs:
 
1042
                differ.finish()
 
1043
 
 
1044
    def _show_diff(self, specific_files, extra_trees):
 
1045
        # TODO: Generation of pseudo-diffs for added/deleted files could
 
1046
        # be usefully made into a much faster special case.
 
1047
        iterator = self.new_tree.iter_changes(self.old_tree,
 
1048
                                              specific_files=specific_files,
 
1049
                                              extra_trees=extra_trees,
 
1050
                                              require_versioned=True)
 
1051
        has_changes = 0
 
1052
 
 
1053
        def changes_key(change):
 
1054
            old_path, new_path = change[1]
 
1055
            path = new_path
 
1056
            if path is None:
 
1057
                path = old_path
 
1058
            return path
 
1059
 
 
1060
        def get_encoded_path(path):
 
1061
            if path is not None:
 
1062
                return path.encode(self.path_encoding, "replace")
 
1063
        for change in sorted(iterator, key=changes_key):
 
1064
            # The root does not get diffed, and items with no known kind (that
 
1065
            # is, missing) in both trees are skipped as well.
 
1066
            if change.parent_id == (None, None) or change.kind == (None, None):
 
1067
                continue
 
1068
            if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
 
1069
                warning(
 
1070
                    'Ignoring "%s" as symlinks are not '
 
1071
                    'supported on this filesystem.' % (change.path[0],))
 
1072
                continue
 
1073
            oldpath, newpath = change.path
 
1074
            oldpath_encoded = get_encoded_path(change.path[0])
 
1075
            newpath_encoded = get_encoded_path(change.path[1])
 
1076
            old_present = (change.kind[0] is not None and change.versioned[0])
 
1077
            new_present = (change.kind[1] is not None and change.versioned[1])
 
1078
            executable = change.executable
 
1079
            kind = change.kind
 
1080
            renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
 
1081
 
 
1082
            properties_changed = []
 
1083
            properties_changed.extend(
 
1084
                get_executable_change(executable[0], executable[1]))
 
1085
 
 
1086
            if properties_changed:
 
1087
                prop_str = b" (properties changed: %s)" % (
 
1088
                    b", ".join(properties_changed),)
 
1089
            else:
 
1090
                prop_str = b""
 
1091
 
 
1092
            if (old_present, new_present) == (True, False):
 
1093
                self.to_file.write(b"=== removed %s '%s'\n" %
 
1094
                                   (kind[0].encode('ascii'), oldpath_encoded))
 
1095
                newpath = oldpath
 
1096
            elif (old_present, new_present) == (False, True):
 
1097
                self.to_file.write(b"=== added %s '%s'\n" %
 
1098
                                   (kind[1].encode('ascii'), newpath_encoded))
 
1099
                oldpath = newpath
 
1100
            elif renamed:
 
1101
                self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
 
1102
                                   (kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
 
1103
            else:
 
1104
                # if it was produced by iter_changes, it must be
 
1105
                # modified *somehow*, either content or execute bit.
 
1106
                self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
 
1107
                                                                  newpath_encoded, prop_str))
 
1108
            if change.changed_content:
 
1109
                self._diff(oldpath, newpath, kind[0], kind[1])
 
1110
                has_changes = 1
 
1111
            if renamed:
 
1112
                has_changes = 1
 
1113
        return has_changes
 
1114
 
 
1115
    def diff(self, old_path, new_path):
 
1116
        """Perform a diff of a single file
 
1117
 
 
1118
        :param old_path: The path of the file in the old tree
 
1119
        :param new_path: The path of the file in the new tree
 
1120
        """
 
1121
        if old_path is None:
 
1122
            old_kind = None
 
1123
        else:
 
1124
            old_kind = self.old_tree.kind(old_path)
 
1125
        if new_path is None:
 
1126
            new_kind = None
 
1127
        else:
 
1128
            new_kind = self.new_tree.kind(new_path)
 
1129
        self._diff(old_path, new_path, old_kind, new_kind)
 
1130
 
 
1131
    def _diff(self, old_path, new_path, old_kind, new_kind):
 
1132
        result = DiffPath._diff_many(
 
1133
            self.differs, old_path, new_path, old_kind, new_kind)
 
1134
        if result is DiffPath.CANNOT_DIFF:
 
1135
            error_path = new_path
 
1136
            if error_path is None:
 
1137
                error_path = old_path
 
1138
            raise errors.NoDiffFound(error_path)
 
1139
 
 
1140
 
 
1141
format_registry = Registry()
 
1142
format_registry.register('default', DiffTree)