/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: 2020-02-19 23:18:42 UTC
  • mto: (7490.3.4 work)
  • mto: This revision was merged to the branch mainline in revision 7495.
  • Revision ID: jelmer@jelmer.uk-20200219231842-agwjh2db66cpajqg
Consistent return values.

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