/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-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

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