1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
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.
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.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
39
from bzrlib.symbol_versioning import (
43
from bzrlib.trace import mutter, warning
46
# TODO: Rather than building a changeset object, we should probably
47
# invoke callbacks on an object. That object can either accumulate a
48
# list, write them out directly, etc etc.
51
class _PrematchedMatcher(difflib.SequenceMatcher):
52
"""Allow SequenceMatcher operations to use predetermined blocks"""
54
def __init__(self, matching_blocks):
55
difflib.SequenceMatcher(self, None, None)
56
self.matching_blocks = matching_blocks
60
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
61
allow_binary=False, sequence_matcher=None,
62
path_encoding='utf8'):
63
# FIXME: difflib is wrong if there is no trailing newline.
64
# The syntax used by patch seems to be "\ No newline at
65
# end of file" following the last diff line from that
66
# file. This is not trivial to insert into the
67
# unified_diff output and it might be better to just fix
68
# or replace that function.
70
# In the meantime we at least make sure the patch isn't
74
# Special workaround for Python2.3, where difflib fails if
75
# both sequences are empty.
76
if not oldlines and not newlines:
79
if allow_binary is False:
80
textfile.check_text_lines(oldlines)
81
textfile.check_text_lines(newlines)
83
if sequence_matcher is None:
84
sequence_matcher = patiencediff.PatienceSequenceMatcher
85
ud = patiencediff.unified_diff(oldlines, newlines,
86
fromfile=old_filename.encode(path_encoding),
87
tofile=new_filename.encode(path_encoding),
88
sequencematcher=sequence_matcher)
91
if len(ud) == 0: # Identical contents, nothing to do
93
# work-around for difflib being too smart for its own good
94
# if /dev/null is "1,0", patch won't recognize it as /dev/null
96
ud[2] = ud[2].replace('-1,0', '-0,0')
98
ud[2] = ud[2].replace('+1,0', '+0,0')
99
# work around for difflib emitting random spaces after the label
100
ud[0] = ud[0][:-2] + '\n'
101
ud[1] = ud[1][:-2] + '\n'
105
if not line.endswith('\n'):
106
to_file.write("\n\\ No newline at end of file\n")
110
def _spawn_external_diff(diffcmd, capture_errors=True):
111
"""Spawn the externall diff process, and return the child handle.
113
:param diffcmd: The command list to spawn
114
:param capture_errors: Capture stderr as well as setting LANG=C
115
and LC_ALL=C. This lets us read and understand the output of diff,
116
and respond to any errors.
117
:return: A Popen object.
120
# construct minimal environment
122
path = os.environ.get('PATH')
125
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
128
stderr = subprocess.PIPE
134
pipe = subprocess.Popen(diffcmd,
135
stdin=subprocess.PIPE,
136
stdout=subprocess.PIPE,
140
if e.errno == errno.ENOENT:
141
raise errors.NoDiff(str(e))
147
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
149
"""Display a diff by calling out to the external diff program."""
150
# make sure our own output is properly ordered before the diff
153
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
154
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
155
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
156
newtmpf = os.fdopen(newtmp_fd, 'wb')
159
# TODO: perhaps a special case for comparing to or from the empty
160
# sequence; can just use /dev/null on Unix
162
# TODO: if either of the files being compared already exists as a
163
# regular named file (e.g. in the working directory) then we can
164
# compare directly to that, rather than copying it.
166
oldtmpf.writelines(oldlines)
167
newtmpf.writelines(newlines)
175
'--label', old_filename,
177
'--label', new_filename,
182
# diff only allows one style to be specified; they don't override.
183
# note that some of these take optargs, and the optargs can be
184
# directly appended to the options.
185
# this is only an approximate parser; it doesn't properly understand
187
for s in ['-c', '-u', '-C', '-U',
192
'-y', '--side-by-side',
204
diffcmd.extend(diff_opts)
206
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
207
out,err = pipe.communicate()
210
# internal_diff() adds a trailing newline, add one here for consistency
213
# 'diff' gives retcode == 2 for all sorts of errors
214
# one of those is 'Binary files differ'.
215
# Bad options could also be the problem.
216
# 'Binary files' is not a real error, so we suppress that error.
219
# Since we got here, we want to make sure to give an i18n error
220
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
221
out, err = pipe.communicate()
223
# Write out the new i18n diff response
224
to_file.write(out+'\n')
225
if pipe.returncode != 2:
226
raise errors.BzrError(
227
'external diff failed with exit code 2'
228
' when run with LANG=C and LC_ALL=C,'
229
' but not when run natively: %r' % (diffcmd,))
231
first_line = lang_c_out.split('\n', 1)[0]
232
# Starting with diffutils 2.8.4 the word "binary" was dropped.
233
m = re.match('^(binary )?files.*differ$', first_line, re.I)
235
raise errors.BzrError('external diff failed with exit code 2;'
236
' command: %r' % (diffcmd,))
238
# Binary files differ, just return
241
# If we got to here, we haven't written out the output of diff
245
# returns 1 if files differ; that's OK
247
msg = 'signal %d' % (-rc)
249
msg = 'exit code %d' % rc
251
raise errors.BzrError('external diff failed with %s; command: %r'
256
oldtmpf.close() # and delete
258
# Clean up. Warn in case the files couldn't be deleted
259
# (in case windows still holds the file open, but not
260
# if the files have already been deleted)
262
os.remove(old_abspath)
264
if e.errno not in (errno.ENOENT,):
265
warning('Failed to delete temporary file: %s %s',
268
os.remove(new_abspath)
270
if e.errno not in (errno.ENOENT,):
271
warning('Failed to delete temporary file: %s %s',
275
@deprecated_function(one_zero)
276
def diff_cmd_helper(tree, specific_files, external_diff_options,
277
old_revision_spec=None, new_revision_spec=None,
279
old_label='a/', new_label='b/'):
280
"""Helper for cmd_diff.
285
:param specific_files:
286
The specific files to compare, or None
288
:param external_diff_options:
289
If non-None, run an external diff, and pass it these options
291
:param old_revision_spec:
292
If None, use basis tree as old revision, otherwise use the tree for
293
the specified revision.
295
:param new_revision_spec:
296
If None, use working tree as new revision, otherwise use the tree for
297
the specified revision.
299
:param revision_specs:
300
Zero, one or two RevisionSpecs from the command line, saying what revisions
301
to compare. This can be passed as an alternative to the old_revision_spec
302
and new_revision_spec parameters.
304
The more general form is show_diff_trees(), where the caller
305
supplies any two trees.
308
# TODO: perhaps remove the old parameters old_revision_spec and
309
# new_revision_spec, since this is only really for use from cmd_diff and
310
# it now always passes through a sequence of revision_specs -- mbp
315
revision = spec.in_store(tree.branch)
317
revision = spec.in_store(None)
318
revision_id = revision.rev_id
319
branch = revision.branch
320
return branch.repository.revision_tree(revision_id)
322
if revision_specs is not None:
323
assert (old_revision_spec is None
324
and new_revision_spec is None)
325
if len(revision_specs) > 0:
326
old_revision_spec = revision_specs[0]
327
if len(revision_specs) > 1:
328
new_revision_spec = revision_specs[1]
330
if old_revision_spec is None:
331
old_tree = tree.basis_tree()
333
old_tree = spec_tree(old_revision_spec)
335
if (new_revision_spec is None
336
or new_revision_spec.spec is None):
339
new_tree = spec_tree(new_revision_spec)
341
if new_tree is not tree:
342
extra_trees = (tree,)
346
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
347
external_diff_options,
348
old_label=old_label, new_label=new_label,
349
extra_trees=extra_trees)
352
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
353
"""Get the trees and specific files to diff given a list of paths.
355
This method works out the trees to be diff'ed and the files of
356
interest within those trees.
359
the list of arguments passed to the diff command
360
:param revision_specs:
361
Zero, one or two RevisionSpecs from the diff command line,
362
saying what revisions to compare.
364
The url of the old branch or tree. If None, the tree to use is
365
taken from the first path, if any, or the current working tree.
367
The url of the new branch or tree. If None, the tree to use is
368
taken from the first path, if any, or the current working tree.
370
a tuple of (old_tree, new_tree, specific_files, extra_trees) where
371
extra_trees is a sequence of additional trees to search in for
374
# Get the old and new revision specs
375
old_revision_spec = None
376
new_revision_spec = None
377
if revision_specs is not None:
378
if len(revision_specs) > 0:
379
old_revision_spec = revision_specs[0]
381
old_url = old_revision_spec.get_branch()
382
if len(revision_specs) > 1:
383
new_revision_spec = revision_specs[1]
385
new_url = new_revision_spec.get_branch()
388
make_paths_wt_relative = True
389
if path_list is None or len(path_list) == 0:
390
# If no path is given, assume the current directory
391
default_location = u'.'
392
elif old_url is not None and new_url is not None:
393
other_paths = path_list
394
make_paths_wt_relative = False
396
default_location = path_list[0]
397
other_paths = path_list[1:]
399
# Get the old location
402
old_url = default_location
403
working_tree, branch, relpath = \
404
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
406
specific_files.append(relpath)
407
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
409
# Get the new location
411
new_url = default_location
412
if new_url != old_url:
413
working_tree, branch, relpath = \
414
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
416
specific_files.append(relpath)
417
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
418
basis_is_default=working_tree is None)
420
# Get the specific files (all files is None, no files is [])
421
if make_paths_wt_relative and working_tree is not None:
422
other_paths = _relative_paths_in_tree(working_tree, other_paths)
423
specific_files.extend(other_paths)
424
if len(specific_files) == 0:
425
specific_files = None
427
# Get extra trees that ought to be searched for file-ids
429
if working_tree is not None and working_tree not in (old_tree, new_tree):
430
extra_trees = (working_tree,)
431
return old_tree, new_tree, specific_files, extra_trees
434
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
435
if branch is None and tree is not None:
437
if spec is None or spec.spec is None:
440
return tree.basis_tree()
442
return branch.basis_tree()
445
revision = spec.in_store(branch)
446
revision_id = revision.rev_id
447
rev_branch = revision.branch
448
return rev_branch.repository.revision_tree(revision_id)
451
def _relative_paths_in_tree(tree, paths):
452
"""Get the relative paths within a working tree.
454
Each path may be either an absolute path or a path relative to the
455
current working directory.
458
for filename in paths:
460
result.append(tree.relpath(osutils.dereference_path(filename)))
461
except errors.PathNotChild:
462
raise errors.BzrCommandError("Files are in different branches")
466
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
467
external_diff_options=None,
468
old_label='a/', new_label='b/',
470
path_encoding='utf8'):
471
"""Show in text form the changes from one tree to another.
477
Include only changes to these files - None for all changes.
479
external_diff_options
480
If set, use an external GNU diff and pass these options.
483
If set, more Trees to use for looking up file ids
486
If set, the path will be encoded as specified, otherwise is supposed
491
if extra_trees is not None:
492
for tree in extra_trees:
496
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
498
external_diff_options,
499
old_label, new_label)
500
return differ.show_diff(specific_files, extra_trees)
503
if extra_trees is not None:
504
for tree in extra_trees:
510
def _patch_header_date(tree, file_id, path):
511
"""Returns a timestamp suitable for use in a patch header."""
512
mtime = tree.get_file_mtime(file_id, path)
513
assert mtime is not None, \
514
"got an mtime of None for file-id %s, path %s in tree %s" % (
516
return timestamp.format_patch_date(mtime)
519
def _raise_if_nonexistent(paths, old_tree, new_tree):
520
"""Complain if paths are not in either inventory or tree.
522
It's OK with the files exist in either tree's inventory, or
523
if they exist in the tree but are not versioned.
525
This can be used by operations such as bzr status that can accept
526
unknown or ignored files.
528
mutter("check paths: %r", paths)
531
s = old_tree.filter_unversioned_files(paths)
532
s = new_tree.filter_unversioned_files(s)
533
s = [path for path in s if not new_tree.has_filename(path)]
535
raise errors.PathsDoNotExist(sorted(s))
538
def get_prop_change(meta_modified):
540
return " (properties changed)"
545
class DiffPath(object):
546
"""Base type for command object that compare files"""
548
# The type or contents of the file were unsuitable for diffing
549
CANNOT_DIFF = 'CANNOT_DIFF'
550
# The file has changed in a semantic way
552
# The file content may have changed, but there is no semantic change
553
UNCHANGED = 'UNCHANGED'
555
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
558
:param old_tree: The tree to show as the old tree in the comparison
559
:param new_tree: The tree to show as new in the comparison
560
:param to_file: The file to write comparison data to
561
:param path_encoding: The character encoding to write paths in
563
self.old_tree = old_tree
564
self.new_tree = new_tree
565
self.to_file = to_file
566
self.path_encoding = path_encoding
569
def from_diff_tree(klass, diff_tree):
570
return klass(diff_tree.old_tree, diff_tree.new_tree,
571
diff_tree.to_file, diff_tree.path_encoding)
574
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
575
for file_differ in differs:
576
result = file_differ.diff(file_id, old_path, new_path, old_kind,
578
if result is not DiffPath.CANNOT_DIFF:
581
return DiffPath.CANNOT_DIFF
584
class DiffKindChange(object):
585
"""Special differ for file kind changes.
587
Represents kind change as deletion + creation. Uses the other differs
590
def __init__(self, differs):
591
self.differs = differs
594
def from_diff_tree(klass, diff_tree):
595
return klass(diff_tree.differs)
597
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
598
"""Perform comparison
600
:param file_id: The file_id of the file to compare
601
:param old_path: Path of the file in the old tree
602
:param new_path: Path of the file in the new tree
603
:param old_kind: Old file-kind of the file
604
:param new_kind: New file-kind of the file
606
if None in (old_kind, new_kind):
607
return DiffPath.CANNOT_DIFF
608
result = DiffPath._diff_many(self.differs, file_id, old_path,
609
new_path, old_kind, None)
610
if result is DiffPath.CANNOT_DIFF:
612
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
616
class DiffDirectory(DiffPath):
618
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
619
"""Perform comparison between two directories. (dummy)
622
if 'directory' not in (old_kind, new_kind):
623
return self.CANNOT_DIFF
624
if old_kind not in ('directory', None):
625
return self.CANNOT_DIFF
626
if new_kind not in ('directory', None):
627
return self.CANNOT_DIFF
631
class DiffSymlink(DiffPath):
633
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
634
"""Perform comparison between two symlinks
636
:param file_id: The file_id of the file to compare
637
:param old_path: Path of the file in the old tree
638
:param new_path: Path of the file in the new tree
639
:param old_kind: Old file-kind of the file
640
:param new_kind: New file-kind of the file
642
if 'symlink' not in (old_kind, new_kind):
643
return self.CANNOT_DIFF
644
if old_kind == 'symlink':
645
old_target = self.old_tree.get_symlink_target(file_id)
646
elif old_kind is None:
649
return self.CANNOT_DIFF
650
if new_kind == 'symlink':
651
new_target = self.new_tree.get_symlink_target(file_id)
652
elif new_kind is None:
655
return self.CANNOT_DIFF
656
return self.diff_symlink(old_target, new_target)
658
def diff_symlink(self, old_target, new_target):
659
if old_target is None:
660
self.to_file.write('=== target is %r\n' % new_target)
661
elif new_target is None:
662
self.to_file.write('=== target was %r\n' % old_target)
664
self.to_file.write('=== target changed %r => %r\n' %
665
(old_target, new_target))
669
class DiffText(DiffPath):
671
# GNU Patch uses the epoch date to detect files that are being added
672
# or removed in a diff.
673
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
675
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
676
old_label='', new_label='', text_differ=internal_diff):
677
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
678
self.text_differ = text_differ
679
self.old_label = old_label
680
self.new_label = new_label
681
self.path_encoding = path_encoding
683
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
684
"""Compare two files in unified diff format
686
:param file_id: The file_id of the file to compare
687
:param old_path: Path of the file in the old tree
688
:param new_path: Path of the file in the new tree
689
:param old_kind: Old file-kind of the file
690
:param new_kind: New file-kind of the file
692
if 'file' not in (old_kind, new_kind):
693
return self.CANNOT_DIFF
694
from_file_id = to_file_id = file_id
695
if old_kind == 'file':
696
old_date = _patch_header_date(self.old_tree, file_id, old_path)
697
elif old_kind is None:
698
old_date = self.EPOCH_DATE
701
return self.CANNOT_DIFF
702
if new_kind == 'file':
703
new_date = _patch_header_date(self.new_tree, file_id, new_path)
704
elif new_kind is None:
705
new_date = self.EPOCH_DATE
708
return self.CANNOT_DIFF
709
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
710
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
711
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
713
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
714
"""Diff the content of given files in two trees
716
:param from_file_id: The id of the file in the from tree. If None,
717
the file is not present in the from tree.
718
:param to_file_id: The id of the file in the to tree. This may refer
719
to a different file from from_file_id. If None,
720
the file is not present in the to tree.
722
def _get_text(tree, file_id):
723
if file_id is not None:
724
return tree.get_file(file_id).readlines()
728
from_text = _get_text(self.old_tree, from_file_id)
729
to_text = _get_text(self.new_tree, to_file_id)
730
self.text_differ(from_label, from_text, to_label, to_text,
732
except errors.BinaryFile:
734
("Binary files %s and %s differ\n" %
735
(from_label, to_label)).encode(self.path_encoding))
739
class DiffTree(object):
740
"""Provides textual representations of the difference between two trees.
742
A DiffTree examines two trees and where a file-id has altered
743
between them, generates a textual representation of the difference.
744
DiffTree uses a sequence of DiffPath objects which are each
745
given the opportunity to handle a given altered fileid. The list
746
of DiffPath objects can be extended globally by appending to
747
DiffTree.diff_factories, or for a specific diff operation by
748
supplying the extra_factories option to the appropriate method.
751
# list of factories that can provide instances of DiffPath objects
752
# may be extended by plugins.
753
diff_factories = [DiffSymlink.from_diff_tree,
754
DiffDirectory.from_diff_tree]
756
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
757
diff_text=None, extra_factories=None):
760
:param old_tree: Tree to show as old in the comparison
761
:param new_tree: Tree to show as new in the comparison
762
:param to_file: File to write comparision to
763
:param path_encoding: Character encoding to write paths in
764
:param diff_text: DiffPath-type object to use as a last resort for
766
:param extra_factories: Factories of DiffPaths to try before any other
768
if diff_text is None:
769
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
770
'', '', internal_diff)
771
self.old_tree = old_tree
772
self.new_tree = new_tree
773
self.to_file = to_file
774
self.path_encoding = path_encoding
776
if extra_factories is not None:
777
self.differs.extend(f(self) for f in extra_factories)
778
self.differs.extend(f(self) for f in self.diff_factories)
779
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
782
def from_trees_options(klass, old_tree, new_tree, to_file,
783
path_encoding, external_diff_options, old_label,
785
"""Factory for producing a DiffTree.
787
Designed to accept options used by show_diff_trees.
788
:param old_tree: The tree to show as old in the comparison
789
:param new_tree: The tree to show as new in the comparison
790
:param to_file: File to write comparisons to
791
:param path_encoding: Character encoding to use for writing paths
792
:param external_diff_options: If supplied, use the installed diff
793
binary to perform file comparison, using supplied options.
794
:param old_label: Prefix to use for old file labels
795
:param new_label: Prefix to use for new file labels
797
if external_diff_options:
798
assert isinstance(external_diff_options, basestring)
799
opts = external_diff_options.split()
800
def diff_file(olab, olines, nlab, nlines, to_file):
801
external_diff(olab, olines, nlab, nlines, to_file, opts)
803
diff_file = internal_diff
804
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
805
old_label, new_label, diff_file)
806
return klass(old_tree, new_tree, to_file, path_encoding, diff_text)
808
def show_diff(self, specific_files, extra_trees=None):
809
"""Write tree diff to self.to_file
811
:param sepecific_files: the specific files to compare (recursive)
812
:param extra_trees: extra trees to use for mapping paths to file_ids
814
# TODO: Generation of pseudo-diffs for added/deleted files could
815
# be usefully made into a much faster special case.
816
iterator = self.new_tree._iter_changes(self.old_tree,
817
specific_files=specific_files,
818
extra_trees=extra_trees,
819
require_versioned=True)
821
def changes_key(change):
822
old_path, new_path = change[1]
827
def get_encoded_path(path):
829
return path.encode(self.path_encoding, "replace")
830
for (file_id, paths, changed_content, versioned, parent, name, kind,
831
executable) in sorted(iterator, key=changes_key):
832
if parent == (None, None):
834
oldpath, newpath = paths
835
oldpath_encoded = get_encoded_path(paths[0])
836
newpath_encoded = get_encoded_path(paths[1])
837
old_present = (kind[0] is not None and versioned[0])
838
new_present = (kind[1] is not None and versioned[1])
839
renamed = (parent[0], name[0]) != (parent[1], name[1])
840
prop_str = get_prop_change(executable[0] != executable[1])
841
if (old_present, new_present) == (True, False):
842
self.to_file.write("=== removed %s '%s'\n" %
843
(kind[0], oldpath_encoded))
845
elif (old_present, new_present) == (False, True):
846
self.to_file.write("=== added %s '%s'\n" %
847
(kind[1], newpath_encoded))
850
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
851
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
853
# if it was produced by _iter_changes, it must be
854
# modified *somehow*, either content or execute bit.
855
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
856
newpath_encoded, prop_str))
858
self.diff(file_id, oldpath, newpath)
864
def diff(self, file_id, old_path, new_path):
865
"""Perform a diff of a single file
867
:param file_id: file-id of the file
868
:param old_path: The path of the file in the old tree
869
:param new_path: The path of the file in the new tree
872
old_kind = self.old_tree.kind(file_id)
873
except (errors.NoSuchId, errors.NoSuchFile):
876
new_kind = self.new_tree.kind(file_id)
877
except (errors.NoSuchId, errors.NoSuchFile):
880
result = DiffPath._diff_many(self.differs, file_id, old_path,
881
new_path, old_kind, new_kind)
882
if result is DiffPath.CANNOT_DIFF:
883
error_path = new_path
884
if error_path is None:
885
error_path = old_path
886
raise errors.NoDiffFound(error_path)