~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-05-04 12:10:51 UTC
  • mfrom: (5819.1.4 777007-developer-doc)
  • Revision ID: pqm@pqm.ubuntu.com-20110504121051-aovlsmqiivjmc4fc
(jelmer) Small fixes to developer documentation. (Jonathan Riddell)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
 
1
# Copyright (C) 2005-2011 Canonical Ltd.
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
 
17
import difflib
17
18
import os
18
19
import re
 
20
import string
19
21
import sys
20
22
 
21
23
from bzrlib.lazy_import import lazy_import
23
25
import errno
24
26
import subprocess
25
27
import tempfile
26
 
import time
27
28
 
28
29
from bzrlib import (
 
30
    bzrdir,
 
31
    cmdline,
 
32
    cleanup,
29
33
    errors,
30
34
    osutils,
31
35
    patiencediff,
32
36
    textfile,
 
37
    timestamp,
 
38
    views,
33
39
    )
 
40
 
 
41
from bzrlib.workingtree import WorkingTree
34
42
""")
35
43
 
36
 
# compatability - plugins import compare_trees from diff!!!
37
 
# deprecated as of 0.10
38
 
from bzrlib.delta import compare_trees
 
44
from bzrlib.registry import (
 
45
    Registry,
 
46
    )
39
47
from bzrlib.symbol_versioning import (
40
 
        deprecated_function,
41
 
        zero_eight,
42
 
        )
43
 
from bzrlib.trace import mutter, warning
 
48
    deprecated_function,
 
49
    deprecated_in,
 
50
    )
 
51
from bzrlib.trace import mutter, note, warning
 
52
 
 
53
 
 
54
class AtTemplate(string.Template):
 
55
    """Templating class that uses @ instead of $."""
 
56
 
 
57
    delimiter = '@'
44
58
 
45
59
 
46
60
# TODO: Rather than building a changeset object, we should probably
47
61
# invoke callbacks on an object.  That object can either accumulate a
48
62
# list, write them out directly, etc etc.
49
63
 
 
64
 
 
65
class _PrematchedMatcher(difflib.SequenceMatcher):
 
66
    """Allow SequenceMatcher operations to use predetermined blocks"""
 
67
 
 
68
    def __init__(self, matching_blocks):
 
69
        difflib.SequenceMatcher(self, None, None)
 
70
        self.matching_blocks = matching_blocks
 
71
        self.opcodes = None
 
72
 
 
73
 
50
74
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
51
75
                  allow_binary=False, sequence_matcher=None,
52
76
                  path_encoding='utf8'):
65
89
    # both sequences are empty.
66
90
    if not oldlines and not newlines:
67
91
        return
68
 
    
 
92
 
69
93
    if allow_binary is False:
70
94
        textfile.check_text_lines(oldlines)
71
95
        textfile.check_text_lines(newlines)
73
97
    if sequence_matcher is None:
74
98
        sequence_matcher = patiencediff.PatienceSequenceMatcher
75
99
    ud = patiencediff.unified_diff(oldlines, newlines,
76
 
                      fromfile=old_filename.encode(path_encoding),
77
 
                      tofile=new_filename.encode(path_encoding),
 
100
                      fromfile=old_filename.encode(path_encoding, 'replace'),
 
101
                      tofile=new_filename.encode(path_encoding, 'replace'),
78
102
                      sequencematcher=sequence_matcher)
79
103
 
80
104
    ud = list(ud)
 
105
    if len(ud) == 0: # Identical contents, nothing to do
 
106
        return
81
107
    # work-around for difflib being too smart for its own good
82
108
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
83
109
    if not oldlines:
84
110
        ud[2] = ud[2].replace('-1,0', '-0,0')
85
111
    elif not newlines:
86
112
        ud[2] = ud[2].replace('+1,0', '+0,0')
87
 
    # work around for difflib emitting random spaces after the label
88
 
    ud[0] = ud[0][:-2] + '\n'
89
 
    ud[1] = ud[1][:-2] + '\n'
90
113
 
91
114
    for line in ud:
92
115
        to_file.write(line)
93
116
        if not line.endswith('\n'):
94
117
            to_file.write("\n\\ No newline at end of file\n")
95
 
    print >>to_file
96
 
 
97
 
 
98
 
def _set_lang_C():
99
 
    """Set the env vars LANG=C and LC_ALL=C."""
100
 
    osutils.set_or_unset_env('LANG', 'C')
101
 
    osutils.set_or_unset_env('LC_ALL', 'C')
102
 
    osutils.set_or_unset_env('LC_CTYPE', None)
103
 
    osutils.set_or_unset_env('LANGUAGE', None)
 
118
    to_file.write('\n')
104
119
 
105
120
 
106
121
def _spawn_external_diff(diffcmd, capture_errors=True):
113
128
    :return: A Popen object.
114
129
    """
115
130
    if capture_errors:
116
 
        if sys.platform == 'win32':
117
 
            # Win32 doesn't support preexec_fn, but that is
118
 
            # okay, because it doesn't support LANG and LC_ALL either.
119
 
            preexec_fn = None
120
 
        else:
121
 
            preexec_fn = _set_lang_C
 
131
        # construct minimal environment
 
132
        env = {}
 
133
        path = os.environ.get('PATH')
 
134
        if path is not None:
 
135
            env['PATH'] = path
 
136
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
 
137
        env['LANG'] = 'C'
 
138
        env['LC_ALL'] = 'C'
122
139
        stderr = subprocess.PIPE
123
140
    else:
124
 
        preexec_fn = None
 
141
        env = None
125
142
        stderr = None
126
143
 
127
144
    try:
129
146
                                stdin=subprocess.PIPE,
130
147
                                stdout=subprocess.PIPE,
131
148
                                stderr=stderr,
132
 
                                preexec_fn=preexec_fn)
 
149
                                env=env)
133
150
    except OSError, e:
134
151
        if e.errno == errno.ENOENT:
135
152
            raise errors.NoDiff(str(e))
165
182
 
166
183
        if not diff_opts:
167
184
            diff_opts = []
 
185
        if sys.platform == 'win32':
 
186
            # Popen doesn't do the proper encoding for external commands
 
187
            # Since we are dealing with an ANSI api, use mbcs encoding
 
188
            old_filename = old_filename.encode('mbcs')
 
189
            new_filename = new_filename.encode('mbcs')
168
190
        diffcmd = ['diff',
169
191
                   '--label', old_filename,
170
192
                   old_abspath,
193
215
            break
194
216
        else:
195
217
            diffcmd.append('-u')
196
 
                  
 
218
 
197
219
        if diff_opts:
198
220
            diffcmd.extend(diff_opts)
199
221
 
200
222
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
201
223
        out,err = pipe.communicate()
202
224
        rc = pipe.returncode
203
 
        
 
225
 
204
226
        # internal_diff() adds a trailing newline, add one here for consistency
205
227
        out += '\n'
206
228
        if rc == 2:
241
263
                msg = 'signal %d' % (-rc)
242
264
            else:
243
265
                msg = 'exit code %d' % rc
244
 
                
245
 
            raise errors.BzrError('external diff failed with %s; command: %r' 
 
266
 
 
267
            raise errors.BzrError('external diff failed with %s; command: %r'
246
268
                                  % (rc, diffcmd))
247
269
 
248
270
 
266
288
                        new_abspath, e)
267
289
 
268
290
 
269
 
@deprecated_function(zero_eight)
270
 
def show_diff(b, from_spec, specific_files, external_diff_options=None,
271
 
              revision2=None, output=None, b2=None):
272
 
    """Shortcut for showing the diff to the working tree.
273
 
 
274
 
    Please use show_diff_trees instead.
275
 
 
276
 
    b
277
 
        Branch.
278
 
 
279
 
    revision
280
 
        None for 'basis tree', or otherwise the old revision to compare against.
281
 
    
282
 
    The more general form is show_diff_trees(), where the caller
283
 
    supplies any two trees.
284
 
    """
285
 
    if output is None:
286
 
        output = sys.stdout
287
 
 
288
 
    if from_spec is None:
289
 
        old_tree = b.bzrdir.open_workingtree()
290
 
        if b2 is None:
291
 
            old_tree = old_tree = old_tree.basis_tree()
292
 
    else:
293
 
        old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
294
 
 
295
 
    if revision2 is None:
296
 
        if b2 is None:
297
 
            new_tree = b.bzrdir.open_workingtree()
298
 
        else:
299
 
            new_tree = b2.bzrdir.open_workingtree()
300
 
    else:
301
 
        new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
302
 
 
303
 
    return show_diff_trees(old_tree, new_tree, output, specific_files,
304
 
                           external_diff_options)
305
 
 
306
 
 
307
 
def diff_cmd_helper(tree, specific_files, external_diff_options, 
308
 
                    old_revision_spec=None, new_revision_spec=None,
309
 
                    old_label='a/', new_label='b/'):
310
 
    """Helper for cmd_diff.
311
 
 
312
 
   tree 
313
 
        A WorkingTree
314
 
 
315
 
    specific_files
316
 
        The specific files to compare, or None
317
 
 
318
 
    external_diff_options
319
 
        If non-None, run an external diff, and pass it these options
320
 
 
321
 
    old_revision_spec
322
 
        If None, use basis tree as old revision, otherwise use the tree for
323
 
        the specified revision. 
324
 
 
325
 
    new_revision_spec
326
 
        If None, use working tree as new revision, otherwise use the tree for
327
 
        the specified revision.
328
 
    
329
 
    The more general form is show_diff_trees(), where the caller
330
 
    supplies any two trees.
331
 
    """
332
 
    def spec_tree(spec):
333
 
        if tree:
334
 
            revision = spec.in_store(tree.branch)
335
 
        else:
336
 
            revision = spec.in_store(None)
337
 
        revision_id = revision.rev_id
338
 
        branch = revision.branch
339
 
        return branch.repository.revision_tree(revision_id)
340
 
    if old_revision_spec is None:
341
 
        old_tree = tree.basis_tree()
342
 
    else:
343
 
        old_tree = spec_tree(old_revision_spec)
344
 
 
345
 
    if new_revision_spec is None:
346
 
        new_tree = tree
347
 
    else:
348
 
        new_tree = spec_tree(new_revision_spec)
349
 
    if new_tree is not tree:
350
 
        extra_trees = (tree,)
351
 
    else:
352
 
        extra_trees = None
353
 
 
354
 
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
355
 
                           external_diff_options,
356
 
                           old_label=old_label, new_label=new_label,
357
 
                           extra_trees=extra_trees)
 
291
@deprecated_function(deprecated_in((2, 2, 0)))
 
292
def get_trees_and_branches_to_diff(path_list, revision_specs, old_url, new_url,
 
293
                                   apply_view=True):
 
294
    """Get the trees and specific files to diff given a list of paths.
 
295
 
 
296
    This method works out the trees to be diff'ed and the files of
 
297
    interest within those trees.
 
298
 
 
299
    :param path_list:
 
300
        the list of arguments passed to the diff command
 
301
    :param revision_specs:
 
302
        Zero, one or two RevisionSpecs from the diff command line,
 
303
        saying what revisions to compare.
 
304
    :param old_url:
 
305
        The url of the old branch or tree. If None, the tree to use is
 
306
        taken from the first path, if any, or the current working tree.
 
307
    :param new_url:
 
308
        The url of the new branch or tree. If None, the tree to use is
 
309
        taken from the first path, if any, or the current working tree.
 
310
    :param apply_view:
 
311
        if True and a view is set, apply the view or check that the paths
 
312
        are within it
 
313
    :returns:
 
314
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
315
        specific_files, extra_trees) where extra_trees is a sequence of
 
316
        additional trees to search in for file-ids.  The trees and branches
 
317
        are not locked.
 
318
    """
 
319
    op = cleanup.OperationWithCleanups(get_trees_and_branches_to_diff_locked)
 
320
    return op.run_simple(path_list, revision_specs, old_url, new_url,
 
321
            op.add_cleanup, apply_view=apply_view)
 
322
    
 
323
 
 
324
def get_trees_and_branches_to_diff_locked(
 
325
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
326
    """Get the trees and specific files to diff given a list of paths.
 
327
 
 
328
    This method works out the trees to be diff'ed and the files of
 
329
    interest within those trees.
 
330
 
 
331
    :param path_list:
 
332
        the list of arguments passed to the diff command
 
333
    :param revision_specs:
 
334
        Zero, one or two RevisionSpecs from the diff command line,
 
335
        saying what revisions to compare.
 
336
    :param old_url:
 
337
        The url of the old branch or tree. If None, the tree to use is
 
338
        taken from the first path, if any, or the current working tree.
 
339
    :param new_url:
 
340
        The url of the new branch or tree. If None, the tree to use is
 
341
        taken from the first path, if any, or the current working tree.
 
342
    :param add_cleanup:
 
343
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
344
        will register cleanups that must be run to unlock the trees, etc.
 
345
    :param apply_view:
 
346
        if True and a view is set, apply the view or check that the paths
 
347
        are within it
 
348
    :returns:
 
349
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
350
        specific_files, extra_trees) where extra_trees is a sequence of
 
351
        additional trees to search in for file-ids.  The trees and branches
 
352
        will be read-locked until the cleanups registered via the add_cleanup
 
353
        param are run.
 
354
    """
 
355
    # Get the old and new revision specs
 
356
    old_revision_spec = None
 
357
    new_revision_spec = None
 
358
    if revision_specs is not None:
 
359
        if len(revision_specs) > 0:
 
360
            old_revision_spec = revision_specs[0]
 
361
            if old_url is None:
 
362
                old_url = old_revision_spec.get_branch()
 
363
        if len(revision_specs) > 1:
 
364
            new_revision_spec = revision_specs[1]
 
365
            if new_url is None:
 
366
                new_url = new_revision_spec.get_branch()
 
367
 
 
368
    other_paths = []
 
369
    make_paths_wt_relative = True
 
370
    consider_relpath = True
 
371
    if path_list is None or len(path_list) == 0:
 
372
        # If no path is given, the current working tree is used
 
373
        default_location = u'.'
 
374
        consider_relpath = False
 
375
    elif old_url is not None and new_url is not None:
 
376
        other_paths = path_list
 
377
        make_paths_wt_relative = False
 
378
    else:
 
379
        default_location = path_list[0]
 
380
        other_paths = path_list[1:]
 
381
 
 
382
    def lock_tree_or_branch(wt, br):
 
383
        if wt is not None:
 
384
            wt.lock_read()
 
385
            add_cleanup(wt.unlock)
 
386
        elif br is not None:
 
387
            br.lock_read()
 
388
            add_cleanup(br.unlock)
 
389
 
 
390
    # Get the old location
 
391
    specific_files = []
 
392
    if old_url is None:
 
393
        old_url = default_location
 
394
    working_tree, branch, relpath = \
 
395
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
 
396
    lock_tree_or_branch(working_tree, branch)
 
397
    if consider_relpath and relpath != '':
 
398
        if working_tree is not None and apply_view:
 
399
            views.check_path_in_view(working_tree, relpath)
 
400
        specific_files.append(relpath)
 
401
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
 
402
    old_branch = branch
 
403
 
 
404
    # Get the new location
 
405
    if new_url is None:
 
406
        new_url = default_location
 
407
    if new_url != old_url:
 
408
        working_tree, branch, relpath = \
 
409
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
 
410
        lock_tree_or_branch(working_tree, branch)
 
411
        if consider_relpath and relpath != '':
 
412
            if working_tree is not None and apply_view:
 
413
                views.check_path_in_view(working_tree, relpath)
 
414
            specific_files.append(relpath)
 
415
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
 
416
        basis_is_default=working_tree is None)
 
417
    new_branch = branch
 
418
 
 
419
    # Get the specific files (all files is None, no files is [])
 
420
    if make_paths_wt_relative and working_tree is not None:
 
421
        other_paths = working_tree.safe_relpath_files(
 
422
            other_paths,
 
423
            apply_view=apply_view)
 
424
    specific_files.extend(other_paths)
 
425
    if len(specific_files) == 0:
 
426
        specific_files = None
 
427
        if (working_tree is not None and working_tree.supports_views()
 
428
            and apply_view):
 
429
            view_files = working_tree.views.lookup_view()
 
430
            if view_files:
 
431
                specific_files = view_files
 
432
                view_str = views.view_display_str(view_files)
 
433
                note("*** Ignoring files outside view. View is %s" % view_str)
 
434
 
 
435
    # Get extra trees that ought to be searched for file-ids
 
436
    extra_trees = None
 
437
    if working_tree is not None and working_tree not in (old_tree, new_tree):
 
438
        extra_trees = (working_tree,)
 
439
    return old_tree, new_tree, old_branch, new_branch, specific_files, extra_trees
 
440
 
 
441
 
 
442
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
 
443
    if branch is None and tree is not None:
 
444
        branch = tree.branch
 
445
    if spec is None or spec.spec is None:
 
446
        if basis_is_default:
 
447
            if tree is not None:
 
448
                return tree.basis_tree()
 
449
            else:
 
450
                return branch.basis_tree()
 
451
        else:
 
452
            return tree
 
453
    return spec.as_tree(branch)
358
454
 
359
455
 
360
456
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
361
457
                    external_diff_options=None,
362
458
                    old_label='a/', new_label='b/',
363
 
                    extra_trees=None):
 
459
                    extra_trees=None,
 
460
                    path_encoding='utf8',
 
461
                    using=None,
 
462
                    format_cls=None):
364
463
    """Show in text form the changes from one tree to another.
365
464
 
366
 
    to_files
367
 
        If set, include only changes to these files.
368
 
 
369
 
    external_diff_options
370
 
        If set, use an external GNU diff and pass these options.
371
 
 
372
 
    extra_trees
373
 
        If set, more Trees to use for looking up file ids
 
465
    :param to_file: The output stream.
 
466
    :param specific_files:Include only changes to these files - None for all
 
467
        changes.
 
468
    :param external_diff_options: If set, use an external GNU diff and pass 
 
469
        these options.
 
470
    :param extra_trees: If set, more Trees to use for looking up file ids
 
471
    :param path_encoding: If set, the path will be encoded as specified, 
 
472
        otherwise is supposed to be utf8
 
473
    :param format_cls: Formatter class (DiffTree subclass)
374
474
    """
 
475
    if format_cls is None:
 
476
        format_cls = DiffTree
375
477
    old_tree.lock_read()
376
478
    try:
 
479
        if extra_trees is not None:
 
480
            for tree in extra_trees:
 
481
                tree.lock_read()
377
482
        new_tree.lock_read()
378
483
        try:
379
 
            return _show_diff_trees(old_tree, new_tree, to_file,
380
 
                                    specific_files, external_diff_options,
381
 
                                    old_label=old_label, new_label=new_label,
382
 
                                    extra_trees=extra_trees)
 
484
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
485
                                                   path_encoding,
 
486
                                                   external_diff_options,
 
487
                                                   old_label, new_label, using)
 
488
            return differ.show_diff(specific_files, extra_trees)
383
489
        finally:
384
490
            new_tree.unlock()
 
491
            if extra_trees is not None:
 
492
                for tree in extra_trees:
 
493
                    tree.unlock()
385
494
    finally:
386
495
        old_tree.unlock()
387
496
 
388
497
 
389
 
def _show_diff_trees(old_tree, new_tree, to_file,
390
 
                     specific_files, external_diff_options, 
391
 
                     old_label='a/', new_label='b/', extra_trees=None):
 
498
def _patch_header_date(tree, file_id, path):
 
499
    """Returns a timestamp suitable for use in a patch header."""
 
500
    try:
 
501
        mtime = tree.get_file_mtime(file_id, path)
 
502
    except errors.FileTimestampUnavailable:
 
503
        mtime = 0
 
504
    return timestamp.format_patch_date(mtime)
 
505
 
 
506
 
 
507
def get_executable_change(old_is_x, new_is_x):
 
508
    descr = { True:"+x", False:"-x", None:"??" }
 
509
    if old_is_x != new_is_x:
 
510
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
 
511
    else:
 
512
        return []
 
513
 
 
514
 
 
515
class DiffPath(object):
 
516
    """Base type for command object that compare files"""
 
517
 
 
518
    # The type or contents of the file were unsuitable for diffing
 
519
    CANNOT_DIFF = 'CANNOT_DIFF'
 
520
    # The file has changed in a semantic way
 
521
    CHANGED = 'CHANGED'
 
522
    # The file content may have changed, but there is no semantic change
 
523
    UNCHANGED = 'UNCHANGED'
 
524
 
 
525
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
 
526
        """Constructor.
 
527
 
 
528
        :param old_tree: The tree to show as the old tree in the comparison
 
529
        :param new_tree: The tree to show as new in the comparison
 
530
        :param to_file: The file to write comparison data to
 
531
        :param path_encoding: The character encoding to write paths in
 
532
        """
 
533
        self.old_tree = old_tree
 
534
        self.new_tree = new_tree
 
535
        self.to_file = to_file
 
536
        self.path_encoding = path_encoding
 
537
 
 
538
    def finish(self):
 
539
        pass
 
540
 
 
541
    @classmethod
 
542
    def from_diff_tree(klass, diff_tree):
 
543
        return klass(diff_tree.old_tree, diff_tree.new_tree,
 
544
                     diff_tree.to_file, diff_tree.path_encoding)
 
545
 
 
546
    @staticmethod
 
547
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
 
548
        for file_differ in differs:
 
549
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
 
550
                                      new_kind)
 
551
            if result is not DiffPath.CANNOT_DIFF:
 
552
                return result
 
553
        else:
 
554
            return DiffPath.CANNOT_DIFF
 
555
 
 
556
 
 
557
class DiffKindChange(object):
 
558
    """Special differ for file kind changes.
 
559
 
 
560
    Represents kind change as deletion + creation.  Uses the other differs
 
561
    to do this.
 
562
    """
 
563
    def __init__(self, differs):
 
564
        self.differs = differs
 
565
 
 
566
    def finish(self):
 
567
        pass
 
568
 
 
569
    @classmethod
 
570
    def from_diff_tree(klass, diff_tree):
 
571
        return klass(diff_tree.differs)
 
572
 
 
573
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
574
        """Perform comparison
 
575
 
 
576
        :param file_id: The file_id of the file to compare
 
577
        :param old_path: Path of the file in the old tree
 
578
        :param new_path: Path of the file in the new tree
 
579
        :param old_kind: Old file-kind of the file
 
580
        :param new_kind: New file-kind of the file
 
581
        """
 
582
        if None in (old_kind, new_kind):
 
583
            return DiffPath.CANNOT_DIFF
 
584
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
585
                                       new_path, old_kind, None)
 
586
        if result is DiffPath.CANNOT_DIFF:
 
587
            return result
 
588
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
 
589
                                     None, new_kind)
 
590
 
 
591
 
 
592
class DiffDirectory(DiffPath):
 
593
 
 
594
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
595
        """Perform comparison between two directories.  (dummy)
 
596
 
 
597
        """
 
598
        if 'directory' not in (old_kind, new_kind):
 
599
            return self.CANNOT_DIFF
 
600
        if old_kind not in ('directory', None):
 
601
            return self.CANNOT_DIFF
 
602
        if new_kind not in ('directory', None):
 
603
            return self.CANNOT_DIFF
 
604
        return self.CHANGED
 
605
 
 
606
 
 
607
class DiffSymlink(DiffPath):
 
608
 
 
609
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
610
        """Perform comparison between two symlinks
 
611
 
 
612
        :param file_id: The file_id of the file to compare
 
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 'symlink' not in (old_kind, new_kind):
 
619
            return self.CANNOT_DIFF
 
620
        if old_kind == 'symlink':
 
621
            old_target = self.old_tree.get_symlink_target(file_id)
 
622
        elif old_kind is None:
 
623
            old_target = None
 
624
        else:
 
625
            return self.CANNOT_DIFF
 
626
        if new_kind == 'symlink':
 
627
            new_target = self.new_tree.get_symlink_target(file_id)
 
628
        elif new_kind is None:
 
629
            new_target = None
 
630
        else:
 
631
            return self.CANNOT_DIFF
 
632
        return self.diff_symlink(old_target, new_target)
 
633
 
 
634
    def diff_symlink(self, old_target, new_target):
 
635
        if old_target is None:
 
636
            self.to_file.write('=== target is %r\n' % new_target)
 
637
        elif new_target is None:
 
638
            self.to_file.write('=== target was %r\n' % old_target)
 
639
        else:
 
640
            self.to_file.write('=== target changed %r => %r\n' %
 
641
                              (old_target, new_target))
 
642
        return self.CHANGED
 
643
 
 
644
 
 
645
class DiffText(DiffPath):
392
646
 
393
647
    # GNU Patch uses the epoch date to detect files that are being added
394
648
    # or removed in a diff.
395
649
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
396
650
 
397
 
    # TODO: Generation of pseudo-diffs for added/deleted files could
398
 
    # be usefully made into a much faster special case.
399
 
 
400
 
    if external_diff_options:
401
 
        assert isinstance(external_diff_options, basestring)
402
 
        opts = external_diff_options.split()
403
 
        def diff_file(olab, olines, nlab, nlines, to_file):
404
 
            external_diff(olab, olines, nlab, nlines, to_file, opts)
405
 
    else:
406
 
        diff_file = internal_diff
407
 
    
408
 
    delta = new_tree.changes_from(old_tree,
409
 
        specific_files=specific_files,
410
 
        extra_trees=extra_trees, require_versioned=True)
411
 
 
412
 
    has_changes = 0
413
 
    for path, file_id, kind in delta.removed:
414
 
        has_changes = 1
415
 
        print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
416
 
        old_name = '%s%s\t%s' % (old_label, path,
417
 
                                 _patch_header_date(old_tree, file_id, path))
418
 
        new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
419
 
        old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
420
 
                                         new_name, None, None, to_file)
421
 
    for path, file_id, kind in delta.added:
422
 
        has_changes = 1
423
 
        print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
424
 
        old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
425
 
        new_name = '%s%s\t%s' % (new_label, path,
426
 
                                 _patch_header_date(new_tree, file_id, path))
427
 
        new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
428
 
                                         old_name, None, None, to_file, 
429
 
                                         reverse=True)
430
 
    for (old_path, new_path, file_id, kind,
431
 
         text_modified, meta_modified) in delta.renamed:
432
 
        has_changes = 1
433
 
        prop_str = get_prop_change(meta_modified)
434
 
        print >>to_file, '=== renamed %s %r => %r%s' % (
435
 
                    kind, old_path.encode('utf8'),
436
 
                    new_path.encode('utf8'), prop_str)
437
 
        old_name = '%s%s\t%s' % (old_label, old_path,
438
 
                                 _patch_header_date(old_tree, file_id,
439
 
                                                    old_path))
440
 
        new_name = '%s%s\t%s' % (new_label, new_path,
441
 
                                 _patch_header_date(new_tree, file_id,
442
 
                                                    new_path))
443
 
        _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
444
 
                                    new_name, new_tree,
445
 
                                    text_modified, kind, to_file, diff_file)
446
 
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
447
 
        has_changes = 1
448
 
        prop_str = get_prop_change(meta_modified)
449
 
        print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
450
 
        old_name = '%s%s\t%s' % (old_label, path,
451
 
                                 _patch_header_date(old_tree, file_id, path))
452
 
        new_name = '%s%s\t%s' % (new_label, path,
453
 
                                 _patch_header_date(new_tree, file_id, path))
454
 
        if text_modified:
455
 
            _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
456
 
                                        new_name, new_tree,
457
 
                                        True, kind, to_file, diff_file)
458
 
 
459
 
    return has_changes
460
 
 
461
 
 
462
 
def _patch_header_date(tree, file_id, path):
463
 
    """Returns a timestamp suitable for use in a patch header."""
464
 
    tm = time.gmtime(tree.get_file_mtime(file_id, path))
465
 
    return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
466
 
 
467
 
 
468
 
def _raise_if_nonexistent(paths, old_tree, new_tree):
469
 
    """Complain if paths are not in either inventory or tree.
470
 
 
471
 
    It's OK with the files exist in either tree's inventory, or 
472
 
    if they exist in the tree but are not versioned.
473
 
    
474
 
    This can be used by operations such as bzr status that can accept
475
 
    unknown or ignored files.
 
651
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
652
                 old_label='', new_label='', text_differ=internal_diff):
 
653
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
654
        self.text_differ = text_differ
 
655
        self.old_label = old_label
 
656
        self.new_label = new_label
 
657
        self.path_encoding = path_encoding
 
658
 
 
659
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
660
        """Compare two files in unified diff format
 
661
 
 
662
        :param file_id: The file_id of the file to compare
 
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 'file' not in (old_kind, new_kind):
 
669
            return self.CANNOT_DIFF
 
670
        from_file_id = to_file_id = file_id
 
671
        if old_kind == 'file':
 
672
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
 
673
        elif old_kind is None:
 
674
            old_date = self.EPOCH_DATE
 
675
            from_file_id = None
 
676
        else:
 
677
            return self.CANNOT_DIFF
 
678
        if new_kind == 'file':
 
679
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
 
680
        elif new_kind is None:
 
681
            new_date = self.EPOCH_DATE
 
682
            to_file_id = None
 
683
        else:
 
684
            return self.CANNOT_DIFF
 
685
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
 
686
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
 
687
        return self.diff_text(from_file_id, to_file_id, from_label, to_label,
 
688
            old_path, new_path)
 
689
 
 
690
    def diff_text(self, from_file_id, to_file_id, from_label, to_label,
 
691
        from_path=None, to_path=None):
 
692
        """Diff the content of given files in two trees
 
693
 
 
694
        :param from_file_id: The id of the file in the from tree.  If None,
 
695
            the file is not present in the from tree.
 
696
        :param to_file_id: The id of the file in the to tree.  This may refer
 
697
            to a different file from from_file_id.  If None,
 
698
            the file is not present in the to tree.
 
699
        :param from_path: The path in the from tree or None if unknown.
 
700
        :param to_path: The path in the to tree or None if unknown.
 
701
        """
 
702
        def _get_text(tree, file_id, path):
 
703
            if file_id is not None:
 
704
                return tree.get_file_lines(file_id, path)
 
705
            else:
 
706
                return []
 
707
        try:
 
708
            from_text = _get_text(self.old_tree, from_file_id, from_path)
 
709
            to_text = _get_text(self.new_tree, to_file_id, to_path)
 
710
            self.text_differ(from_label, from_text, to_label, to_text,
 
711
                             self.to_file, path_encoding=self.path_encoding)
 
712
        except errors.BinaryFile:
 
713
            self.to_file.write(
 
714
                  ("Binary files %s and %s differ\n" %
 
715
                  (from_label, to_label)).encode(self.path_encoding,'replace'))
 
716
        return self.CHANGED
 
717
 
 
718
 
 
719
class DiffFromTool(DiffPath):
 
720
 
 
721
    def __init__(self, command_template, old_tree, new_tree, to_file,
 
722
                 path_encoding='utf-8'):
 
723
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
724
        self.command_template = command_template
 
725
        self._root = osutils.mkdtemp(prefix='bzr-diff-')
 
726
 
 
727
    @classmethod
 
728
    def from_string(klass, command_string, old_tree, new_tree, to_file,
 
729
                    path_encoding='utf-8'):
 
730
        command_template = cmdline.split(command_string)
 
731
        if '@' not in command_string:
 
732
            command_template.extend(['@old_path', '@new_path'])
 
733
        return klass(command_template, old_tree, new_tree, to_file,
 
734
                     path_encoding)
 
735
 
 
736
    @classmethod
 
737
    def make_from_diff_tree(klass, command_string, external_diff_options=None):
 
738
        def from_diff_tree(diff_tree):
 
739
            full_command_string = [command_string]
 
740
            if external_diff_options is not None:
 
741
                full_command_string += ' ' + external_diff_options
 
742
            return klass.from_string(full_command_string, diff_tree.old_tree,
 
743
                                     diff_tree.new_tree, diff_tree.to_file)
 
744
        return from_diff_tree
 
745
 
 
746
    def _get_command(self, old_path, new_path):
 
747
        my_map = {'old_path': old_path, 'new_path': new_path}
 
748
        return [AtTemplate(t).substitute(my_map) for t in
 
749
                self.command_template]
 
750
 
 
751
    def _execute(self, old_path, new_path):
 
752
        command = self._get_command(old_path, new_path)
 
753
        try:
 
754
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
 
755
                                    cwd=self._root)
 
756
        except OSError, e:
 
757
            if e.errno == errno.ENOENT:
 
758
                raise errors.ExecutableMissing(command[0])
 
759
            else:
 
760
                raise
 
761
        self.to_file.write(proc.stdout.read())
 
762
        return proc.wait()
 
763
 
 
764
    def _try_symlink_root(self, tree, prefix):
 
765
        if (getattr(tree, 'abspath', None) is None
 
766
            or not osutils.host_os_dereferences_symlinks()):
 
767
            return False
 
768
        try:
 
769
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
 
770
        except OSError, e:
 
771
            if e.errno != errno.EEXIST:
 
772
                raise
 
773
        return True
 
774
 
 
775
    def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
 
776
                    allow_write=False):
 
777
        if not force_temp and isinstance(tree, WorkingTree):
 
778
            return tree.abspath(tree.id2path(file_id))
 
779
        
 
780
        full_path = osutils.pathjoin(self._root, prefix, relpath)
 
781
        if not force_temp and self._try_symlink_root(tree, prefix):
 
782
            return full_path
 
783
        parent_dir = osutils.dirname(full_path)
 
784
        try:
 
785
            os.makedirs(parent_dir)
 
786
        except OSError, e:
 
787
            if e.errno != errno.EEXIST:
 
788
                raise
 
789
        source = tree.get_file(file_id, relpath)
 
790
        try:
 
791
            target = open(full_path, 'wb')
 
792
            try:
 
793
                osutils.pumpfile(source, target)
 
794
            finally:
 
795
                target.close()
 
796
        finally:
 
797
            source.close()
 
798
        try:
 
799
            mtime = tree.get_file_mtime(file_id)
 
800
        except errors.FileTimestampUnavailable:
 
801
            pass
 
802
        else:
 
803
            os.utime(full_path, (mtime, mtime))
 
804
        if not allow_write:
 
805
            osutils.make_readonly(full_path)
 
806
        return full_path
 
807
 
 
808
    def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
 
809
                       allow_write_new=False):
 
810
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
 
811
                                         old_path, force_temp)
 
812
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
 
813
                                         new_path, force_temp,
 
814
                                         allow_write=allow_write_new)
 
815
        return old_disk_path, new_disk_path
 
816
 
 
817
    def finish(self):
 
818
        try:
 
819
            osutils.rmtree(self._root)
 
820
        except OSError, e:
 
821
            if e.errno != errno.ENOENT:
 
822
                mutter("The temporary directory \"%s\" was not "
 
823
                        "cleanly removed: %s." % (self._root, e))
 
824
 
 
825
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
826
        if (old_kind, new_kind) != ('file', 'file'):
 
827
            return DiffPath.CANNOT_DIFF
 
828
        (old_disk_path, new_disk_path) = self._prepare_files(
 
829
                                                file_id, old_path, new_path)
 
830
        self._execute(old_disk_path, new_disk_path)
 
831
 
 
832
    def edit_file(self, file_id):
 
833
        """Use this tool to edit a file.
 
834
 
 
835
        A temporary copy will be edited, and the new contents will be
 
836
        returned.
 
837
 
 
838
        :param file_id: The id of the file to edit.
 
839
        :return: The new contents of the file.
 
840
        """
 
841
        old_path = self.old_tree.id2path(file_id)
 
842
        new_path = self.new_tree.id2path(file_id)
 
843
        new_abs_path = self._prepare_files(file_id, old_path, new_path,
 
844
                                           allow_write_new=True,
 
845
                                           force_temp=True)[1]
 
846
        command = self._get_command(osutils.pathjoin('old', old_path),
 
847
                                    osutils.pathjoin('new', new_path))
 
848
        subprocess.call(command, cwd=self._root)
 
849
        new_file = open(new_abs_path, 'r')
 
850
        try:
 
851
            return new_file.read()
 
852
        finally:
 
853
            new_file.close()
 
854
 
 
855
 
 
856
class DiffTree(object):
 
857
    """Provides textual representations of the difference between two trees.
 
858
 
 
859
    A DiffTree examines two trees and where a file-id has altered
 
860
    between them, generates a textual representation of the difference.
 
861
    DiffTree uses a sequence of DiffPath objects which are each
 
862
    given the opportunity to handle a given altered fileid. The list
 
863
    of DiffPath objects can be extended globally by appending to
 
864
    DiffTree.diff_factories, or for a specific diff operation by
 
865
    supplying the extra_factories option to the appropriate method.
476
866
    """
477
 
    mutter("check paths: %r", paths)
478
 
    if not paths:
479
 
        return
480
 
    s = old_tree.filter_unversioned_files(paths)
481
 
    s = new_tree.filter_unversioned_files(s)
482
 
    s = [path for path in s if not new_tree.has_filename(path)]
483
 
    if s:
484
 
        raise errors.PathsDoNotExist(sorted(s))
485
 
 
486
 
 
487
 
def get_prop_change(meta_modified):
488
 
    if meta_modified:
489
 
        return " (properties changed)"
490
 
    else:
491
 
        return  ""
492
 
 
493
 
 
494
 
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
495
 
                                new_path, new_tree, text_modified,
496
 
                                kind, to_file, diff_file):
497
 
    if text_modified:
498
 
        new_entry = new_tree.inventory[file_id]
499
 
        old_tree.inventory[file_id].diff(diff_file,
500
 
                                         old_path, old_tree,
501
 
                                         new_path, new_entry, 
502
 
                                         new_tree, to_file)
 
867
 
 
868
    # list of factories that can provide instances of DiffPath objects
 
869
    # may be extended by plugins.
 
870
    diff_factories = [DiffSymlink.from_diff_tree,
 
871
                      DiffDirectory.from_diff_tree]
 
872
 
 
873
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
874
                 diff_text=None, extra_factories=None):
 
875
        """Constructor
 
876
 
 
877
        :param old_tree: Tree to show as old in the comparison
 
878
        :param new_tree: Tree to show as new in the comparison
 
879
        :param to_file: File to write comparision to
 
880
        :param path_encoding: Character encoding to write paths in
 
881
        :param diff_text: DiffPath-type object to use as a last resort for
 
882
            diffing text files.
 
883
        :param extra_factories: Factories of DiffPaths to try before any other
 
884
            DiffPaths"""
 
885
        if diff_text is None:
 
886
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
887
                                 '', '',  internal_diff)
 
888
        self.old_tree = old_tree
 
889
        self.new_tree = new_tree
 
890
        self.to_file = to_file
 
891
        self.path_encoding = path_encoding
 
892
        self.differs = []
 
893
        if extra_factories is not None:
 
894
            self.differs.extend(f(self) for f in extra_factories)
 
895
        self.differs.extend(f(self) for f in self.diff_factories)
 
896
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
 
897
 
 
898
    @classmethod
 
899
    def from_trees_options(klass, old_tree, new_tree, to_file,
 
900
                           path_encoding, external_diff_options, old_label,
 
901
                           new_label, using):
 
902
        """Factory for producing a DiffTree.
 
903
 
 
904
        Designed to accept options used by show_diff_trees.
 
905
        :param old_tree: The tree to show as old in the comparison
 
906
        :param new_tree: The tree to show as new in the comparison
 
907
        :param to_file: File to write comparisons to
 
908
        :param path_encoding: Character encoding to use for writing paths
 
909
        :param external_diff_options: If supplied, use the installed diff
 
910
            binary to perform file comparison, using supplied options.
 
911
        :param old_label: Prefix to use for old file labels
 
912
        :param new_label: Prefix to use for new file labels
 
913
        :param using: Commandline to use to invoke an external diff tool
 
914
        """
 
915
        if using is not None:
 
916
            extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
 
917
        else:
 
918
            extra_factories = []
 
919
        if external_diff_options:
 
920
            opts = external_diff_options.split()
 
921
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None):
 
922
                """:param path_encoding: not used but required
 
923
                        to match the signature of internal_diff.
 
924
                """
 
925
                external_diff(olab, olines, nlab, nlines, to_file, opts)
 
926
        else:
 
927
            diff_file = internal_diff
 
928
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
929
                             old_label, new_label, diff_file)
 
930
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
 
931
                     extra_factories)
 
932
 
 
933
    def show_diff(self, specific_files, extra_trees=None):
 
934
        """Write tree diff to self.to_file
 
935
 
 
936
        :param specific_files: the specific files to compare (recursive)
 
937
        :param extra_trees: extra trees to use for mapping paths to file_ids
 
938
        """
 
939
        try:
 
940
            return self._show_diff(specific_files, extra_trees)
 
941
        finally:
 
942
            for differ in self.differs:
 
943
                differ.finish()
 
944
 
 
945
    def _show_diff(self, specific_files, extra_trees):
 
946
        # TODO: Generation of pseudo-diffs for added/deleted files could
 
947
        # be usefully made into a much faster special case.
 
948
        iterator = self.new_tree.iter_changes(self.old_tree,
 
949
                                               specific_files=specific_files,
 
950
                                               extra_trees=extra_trees,
 
951
                                               require_versioned=True)
 
952
        has_changes = 0
 
953
        def changes_key(change):
 
954
            old_path, new_path = change[1]
 
955
            path = new_path
 
956
            if path is None:
 
957
                path = old_path
 
958
            return path
 
959
        def get_encoded_path(path):
 
960
            if path is not None:
 
961
                return path.encode(self.path_encoding, "replace")
 
962
        for (file_id, paths, changed_content, versioned, parent, name, kind,
 
963
             executable) in sorted(iterator, key=changes_key):
 
964
            # The root does not get diffed, and items with no known kind (that
 
965
            # is, missing) in both trees are skipped as well.
 
966
            if parent == (None, None) or kind == (None, None):
 
967
                continue
 
968
            oldpath, newpath = paths
 
969
            oldpath_encoded = get_encoded_path(paths[0])
 
970
            newpath_encoded = get_encoded_path(paths[1])
 
971
            old_present = (kind[0] is not None and versioned[0])
 
972
            new_present = (kind[1] is not None and versioned[1])
 
973
            renamed = (parent[0], name[0]) != (parent[1], name[1])
 
974
 
 
975
            properties_changed = []
 
976
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
 
977
 
 
978
            if properties_changed:
 
979
                prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
 
980
            else:
 
981
                prop_str = ""
 
982
 
 
983
            if (old_present, new_present) == (True, False):
 
984
                self.to_file.write("=== removed %s '%s'\n" %
 
985
                                   (kind[0], oldpath_encoded))
 
986
                newpath = oldpath
 
987
            elif (old_present, new_present) == (False, True):
 
988
                self.to_file.write("=== added %s '%s'\n" %
 
989
                                   (kind[1], newpath_encoded))
 
990
                oldpath = newpath
 
991
            elif renamed:
 
992
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
 
993
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
 
994
            else:
 
995
                # if it was produced by iter_changes, it must be
 
996
                # modified *somehow*, either content or execute bit.
 
997
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
 
998
                                   newpath_encoded, prop_str))
 
999
            if changed_content:
 
1000
                self._diff(file_id, oldpath, newpath, kind[0], kind[1])
 
1001
                has_changes = 1
 
1002
            if renamed:
 
1003
                has_changes = 1
 
1004
        return has_changes
 
1005
 
 
1006
    def diff(self, file_id, old_path, new_path):
 
1007
        """Perform a diff of a single file
 
1008
 
 
1009
        :param file_id: file-id of the file
 
1010
        :param old_path: The path of the file in the old tree
 
1011
        :param new_path: The path of the file in the new tree
 
1012
        """
 
1013
        try:
 
1014
            old_kind = self.old_tree.kind(file_id)
 
1015
        except (errors.NoSuchId, errors.NoSuchFile):
 
1016
            old_kind = None
 
1017
        try:
 
1018
            new_kind = self.new_tree.kind(file_id)
 
1019
        except (errors.NoSuchId, errors.NoSuchFile):
 
1020
            new_kind = None
 
1021
        self._diff(file_id, old_path, new_path, old_kind, new_kind)
 
1022
 
 
1023
 
 
1024
    def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
1025
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
1026
                                       new_path, old_kind, new_kind)
 
1027
        if result is DiffPath.CANNOT_DIFF:
 
1028
            error_path = new_path
 
1029
            if error_path is None:
 
1030
                error_path = old_path
 
1031
            raise errors.NoDiffFound(error_path)
 
1032
 
 
1033
 
 
1034
format_registry = Registry()
 
1035
format_registry.register('default', DiffTree)