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
23
from bzrlib.lazy_import import lazy_import
24
lazy_import(globals(), """
41
from bzrlib.symbol_versioning import (
45
from bzrlib.trace import mutter, warning
48
# TODO: Rather than building a changeset object, we should probably
49
# invoke callbacks on an object. That object can either accumulate a
50
# list, write them out directly, etc etc.
53
class _PrematchedMatcher(difflib.SequenceMatcher):
54
"""Allow SequenceMatcher operations to use predetermined blocks"""
56
def __init__(self, matching_blocks):
57
difflib.SequenceMatcher(self, None, None)
58
self.matching_blocks = matching_blocks
62
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
63
allow_binary=False, sequence_matcher=None,
64
path_encoding='utf8'):
65
# FIXME: difflib is wrong if there is no trailing newline.
66
# The syntax used by patch seems to be "\ No newline at
67
# end of file" following the last diff line from that
68
# file. This is not trivial to insert into the
69
# unified_diff output and it might be better to just fix
70
# or replace that function.
72
# In the meantime we at least make sure the patch isn't
76
# Special workaround for Python2.3, where difflib fails if
77
# both sequences are empty.
78
if not oldlines and not newlines:
81
if allow_binary is False:
82
textfile.check_text_lines(oldlines)
83
textfile.check_text_lines(newlines)
85
if sequence_matcher is None:
86
sequence_matcher = patiencediff.PatienceSequenceMatcher
87
ud = patiencediff.unified_diff(oldlines, newlines,
88
fromfile=old_filename.encode(path_encoding),
89
tofile=new_filename.encode(path_encoding),
90
sequencematcher=sequence_matcher)
93
if len(ud) == 0: # Identical contents, nothing to do
95
# work-around for difflib being too smart for its own good
96
# if /dev/null is "1,0", patch won't recognize it as /dev/null
98
ud[2] = ud[2].replace('-1,0', '-0,0')
100
ud[2] = ud[2].replace('+1,0', '+0,0')
101
# work around for difflib emitting random spaces after the label
102
ud[0] = ud[0][:-2] + '\n'
103
ud[1] = ud[1][:-2] + '\n'
107
if not line.endswith('\n'):
108
to_file.write("\n\\ No newline at end of file\n")
112
def _spawn_external_diff(diffcmd, capture_errors=True):
113
"""Spawn the externall diff process, and return the child handle.
115
:param diffcmd: The command list to spawn
116
:param capture_errors: Capture stderr as well as setting LANG=C
117
and LC_ALL=C. This lets us read and understand the output of diff,
118
and respond to any errors.
119
:return: A Popen object.
122
# construct minimal environment
124
path = os.environ.get('PATH')
127
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
130
stderr = subprocess.PIPE
136
pipe = subprocess.Popen(diffcmd,
137
stdin=subprocess.PIPE,
138
stdout=subprocess.PIPE,
142
if e.errno == errno.ENOENT:
143
raise errors.NoDiff(str(e))
149
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
151
"""Display a diff by calling out to the external diff program."""
152
# make sure our own output is properly ordered before the diff
155
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
156
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
157
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
158
newtmpf = os.fdopen(newtmp_fd, 'wb')
161
# TODO: perhaps a special case for comparing to or from the empty
162
# sequence; can just use /dev/null on Unix
164
# TODO: if either of the files being compared already exists as a
165
# regular named file (e.g. in the working directory) then we can
166
# compare directly to that, rather than copying it.
168
oldtmpf.writelines(oldlines)
169
newtmpf.writelines(newlines)
177
'--label', old_filename,
179
'--label', new_filename,
184
# diff only allows one style to be specified; they don't override.
185
# note that some of these take optargs, and the optargs can be
186
# directly appended to the options.
187
# this is only an approximate parser; it doesn't properly understand
189
for s in ['-c', '-u', '-C', '-U',
194
'-y', '--side-by-side',
206
diffcmd.extend(diff_opts)
208
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
209
out,err = pipe.communicate()
212
# internal_diff() adds a trailing newline, add one here for consistency
215
# 'diff' gives retcode == 2 for all sorts of errors
216
# one of those is 'Binary files differ'.
217
# Bad options could also be the problem.
218
# 'Binary files' is not a real error, so we suppress that error.
221
# Since we got here, we want to make sure to give an i18n error
222
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
223
out, err = pipe.communicate()
225
# Write out the new i18n diff response
226
to_file.write(out+'\n')
227
if pipe.returncode != 2:
228
raise errors.BzrError(
229
'external diff failed with exit code 2'
230
' when run with LANG=C and LC_ALL=C,'
231
' but not when run natively: %r' % (diffcmd,))
233
first_line = lang_c_out.split('\n', 1)[0]
234
# Starting with diffutils 2.8.4 the word "binary" was dropped.
235
m = re.match('^(binary )?files.*differ$', first_line, re.I)
237
raise errors.BzrError('external diff failed with exit code 2;'
238
' command: %r' % (diffcmd,))
240
# Binary files differ, just return
243
# If we got to here, we haven't written out the output of diff
247
# returns 1 if files differ; that's OK
249
msg = 'signal %d' % (-rc)
251
msg = 'exit code %d' % rc
253
raise errors.BzrError('external diff failed with %s; command: %r'
258
oldtmpf.close() # and delete
260
# Clean up. Warn in case the files couldn't be deleted
261
# (in case windows still holds the file open, but not
262
# if the files have already been deleted)
264
os.remove(old_abspath)
266
if e.errno not in (errno.ENOENT,):
267
warning('Failed to delete temporary file: %s %s',
270
os.remove(new_abspath)
272
if e.errno not in (errno.ENOENT,):
273
warning('Failed to delete temporary file: %s %s',
277
@deprecated_function(one_zero)
278
def diff_cmd_helper(tree, specific_files, external_diff_options,
279
old_revision_spec=None, new_revision_spec=None,
281
old_label='a/', new_label='b/'):
282
"""Helper for cmd_diff.
287
:param specific_files:
288
The specific files to compare, or None
290
:param external_diff_options:
291
If non-None, run an external diff, and pass it these options
293
:param old_revision_spec:
294
If None, use basis tree as old revision, otherwise use the tree for
295
the specified revision.
297
:param new_revision_spec:
298
If None, use working tree as new revision, otherwise use the tree for
299
the specified revision.
301
:param revision_specs:
302
Zero, one or two RevisionSpecs from the command line, saying what revisions
303
to compare. This can be passed as an alternative to the old_revision_spec
304
and new_revision_spec parameters.
306
The more general form is show_diff_trees(), where the caller
307
supplies any two trees.
310
# TODO: perhaps remove the old parameters old_revision_spec and
311
# new_revision_spec, since this is only really for use from cmd_diff and
312
# it now always passes through a sequence of revision_specs -- mbp
317
revision = spec.in_store(tree.branch)
319
revision = spec.in_store(None)
320
revision_id = revision.rev_id
321
branch = revision.branch
322
return branch.repository.revision_tree(revision_id)
324
if revision_specs is not None:
325
assert (old_revision_spec is None
326
and new_revision_spec is None)
327
if len(revision_specs) > 0:
328
old_revision_spec = revision_specs[0]
329
if len(revision_specs) > 1:
330
new_revision_spec = revision_specs[1]
332
if old_revision_spec is None:
333
old_tree = tree.basis_tree()
335
old_tree = spec_tree(old_revision_spec)
337
if (new_revision_spec is None
338
or new_revision_spec.spec is None):
341
new_tree = spec_tree(new_revision_spec)
343
if new_tree is not tree:
344
extra_trees = (tree,)
348
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
349
external_diff_options,
350
old_label=old_label, new_label=new_label,
351
extra_trees=extra_trees)
354
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
355
"""Get the trees and specific files to diff given a list of paths.
357
This method works out the trees to be diff'ed and the files of
358
interest within those trees.
361
the list of arguments passed to the diff command
362
:param revision_specs:
363
Zero, one or two RevisionSpecs from the diff command line,
364
saying what revisions to compare.
366
The url of the old branch or tree. If None, the tree to use is
367
taken from the first path, if any, or the current working tree.
369
The url of the new branch or tree. If None, the tree to use is
370
taken from the first path, if any, or the current working tree.
372
a tuple of (old_tree, new_tree, specific_files, extra_trees) where
373
extra_trees is a sequence of additional trees to search in for
376
# Get the old and new revision specs
377
old_revision_spec = None
378
new_revision_spec = None
379
if revision_specs is not None:
380
if len(revision_specs) > 0:
381
old_revision_spec = revision_specs[0]
383
old_url = old_revision_spec.get_branch()
384
if len(revision_specs) > 1:
385
new_revision_spec = revision_specs[1]
387
new_url = new_revision_spec.get_branch()
390
make_paths_wt_relative = True
391
if path_list is None or len(path_list) == 0:
392
# If no path is given, assume the current directory
393
default_location = u'.'
394
elif old_url is not None and new_url is not None:
395
other_paths = path_list
396
make_paths_wt_relative = False
398
default_location = path_list[0]
399
other_paths = path_list[1:]
401
# Get the old location
404
old_url = default_location
405
working_tree, branch, relpath = \
406
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
408
specific_files.append(relpath)
409
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
411
# Get the new location
413
new_url = default_location
414
if new_url != old_url:
415
working_tree, branch, relpath = \
416
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
418
specific_files.append(relpath)
419
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
420
basis_is_default=working_tree is None)
422
# Get the specific files (all files is None, no files is [])
423
if make_paths_wt_relative and working_tree is not None:
424
other_paths = _relative_paths_in_tree(working_tree, other_paths)
425
specific_files.extend(other_paths)
426
if len(specific_files) == 0:
427
specific_files = None
429
# Get extra trees that ought to be searched for file-ids
431
if working_tree is not None and working_tree not in (old_tree, new_tree):
432
extra_trees = (working_tree,)
433
return old_tree, new_tree, specific_files, extra_trees
436
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
437
if branch is None and tree is not None:
439
if spec is None or spec.spec is None:
442
return tree.basis_tree()
444
return branch.basis_tree()
447
revision = spec.in_store(branch)
448
revision_id = revision.rev_id
449
rev_branch = revision.branch
450
return rev_branch.repository.revision_tree(revision_id)
453
def _relative_paths_in_tree(tree, paths):
454
"""Get the relative paths within a working tree.
456
Each path may be either an absolute path or a path relative to the
457
current working directory.
460
for filename in paths:
462
result.append(tree.relpath(osutils.dereference_path(filename)))
463
except errors.PathNotChild:
464
raise errors.BzrCommandError("Files are in different branches")
468
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
469
external_diff_options=None,
470
old_label='a/', new_label='b/',
472
path_encoding='utf8',
474
"""Show in text form the changes from one tree to another.
480
Include only changes to these files - None for all changes.
482
external_diff_options
483
If set, use an external GNU diff and pass these options.
486
If set, more Trees to use for looking up file ids
489
If set, the path will be encoded as specified, otherwise is supposed
494
if extra_trees is not None:
495
for tree in extra_trees:
499
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
501
external_diff_options,
502
old_label, new_label, using)
503
return differ.show_diff(specific_files, extra_trees)
506
if extra_trees is not None:
507
for tree in extra_trees:
513
def _patch_header_date(tree, file_id, path):
514
"""Returns a timestamp suitable for use in a patch header."""
515
mtime = tree.get_file_mtime(file_id, path)
516
assert mtime is not None, \
517
"got an mtime of None for file-id %s, path %s in tree %s" % (
519
return timestamp.format_patch_date(mtime)
522
def _raise_if_nonexistent(paths, old_tree, new_tree):
523
"""Complain if paths are not in either inventory or tree.
525
It's OK with the files exist in either tree's inventory, or
526
if they exist in the tree but are not versioned.
528
This can be used by operations such as bzr status that can accept
529
unknown or ignored files.
531
mutter("check paths: %r", paths)
534
s = old_tree.filter_unversioned_files(paths)
535
s = new_tree.filter_unversioned_files(s)
536
s = [path for path in s if not new_tree.has_filename(path)]
538
raise errors.PathsDoNotExist(sorted(s))
541
def get_prop_change(meta_modified):
543
return " (properties changed)"
548
class DiffPath(object):
549
"""Base type for command object that compare files"""
551
# The type or contents of the file were unsuitable for diffing
552
CANNOT_DIFF = 'CANNOT_DIFF'
553
# The file has changed in a semantic way
555
# The file content may have changed, but there is no semantic change
556
UNCHANGED = 'UNCHANGED'
558
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
561
:param old_tree: The tree to show as the old tree in the comparison
562
:param new_tree: The tree to show as new in the comparison
563
:param to_file: The file to write comparison data to
564
:param path_encoding: The character encoding to write paths in
566
self.old_tree = old_tree
567
self.new_tree = new_tree
568
self.to_file = to_file
569
self.path_encoding = path_encoding
575
def from_diff_tree(klass, diff_tree):
576
return klass(diff_tree.old_tree, diff_tree.new_tree,
577
diff_tree.to_file, diff_tree.path_encoding)
580
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
581
for file_differ in differs:
582
result = file_differ.diff(file_id, old_path, new_path, old_kind,
584
if result is not DiffPath.CANNOT_DIFF:
587
return DiffPath.CANNOT_DIFF
590
class DiffKindChange(object):
591
"""Special differ for file kind changes.
593
Represents kind change as deletion + creation. Uses the other differs
596
def __init__(self, differs):
597
self.differs = differs
603
def from_diff_tree(klass, diff_tree):
604
return klass(diff_tree.differs)
606
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
607
"""Perform comparison
609
:param file_id: The file_id of the file to compare
610
:param old_path: Path of the file in the old tree
611
:param new_path: Path of the file in the new tree
612
:param old_kind: Old file-kind of the file
613
:param new_kind: New file-kind of the file
615
if None in (old_kind, new_kind):
616
return DiffPath.CANNOT_DIFF
617
result = DiffPath._diff_many(self.differs, file_id, old_path,
618
new_path, old_kind, None)
619
if result is DiffPath.CANNOT_DIFF:
621
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
625
class DiffDirectory(DiffPath):
627
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
628
"""Perform comparison between two directories. (dummy)
631
if 'directory' not in (old_kind, new_kind):
632
return self.CANNOT_DIFF
633
if old_kind not in ('directory', None):
634
return self.CANNOT_DIFF
635
if new_kind not in ('directory', None):
636
return self.CANNOT_DIFF
640
class DiffSymlink(DiffPath):
642
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
643
"""Perform comparison between two symlinks
645
:param file_id: The file_id of the file to compare
646
:param old_path: Path of the file in the old tree
647
:param new_path: Path of the file in the new tree
648
:param old_kind: Old file-kind of the file
649
:param new_kind: New file-kind of the file
651
if 'symlink' not in (old_kind, new_kind):
652
return self.CANNOT_DIFF
653
if old_kind == 'symlink':
654
old_target = self.old_tree.get_symlink_target(file_id)
655
elif old_kind is None:
658
return self.CANNOT_DIFF
659
if new_kind == 'symlink':
660
new_target = self.new_tree.get_symlink_target(file_id)
661
elif new_kind is None:
664
return self.CANNOT_DIFF
665
return self.diff_symlink(old_target, new_target)
667
def diff_symlink(self, old_target, new_target):
668
if old_target is None:
669
self.to_file.write('=== target is %r\n' % new_target)
670
elif new_target is None:
671
self.to_file.write('=== target was %r\n' % old_target)
673
self.to_file.write('=== target changed %r => %r\n' %
674
(old_target, new_target))
678
class DiffText(DiffPath):
680
# GNU Patch uses the epoch date to detect files that are being added
681
# or removed in a diff.
682
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
684
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
685
old_label='', new_label='', text_differ=internal_diff):
686
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
687
self.text_differ = text_differ
688
self.old_label = old_label
689
self.new_label = new_label
690
self.path_encoding = path_encoding
692
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
693
"""Compare two files in unified diff format
695
:param file_id: The file_id of the file to compare
696
:param old_path: Path of the file in the old tree
697
:param new_path: Path of the file in the new tree
698
:param old_kind: Old file-kind of the file
699
:param new_kind: New file-kind of the file
701
if 'file' not in (old_kind, new_kind):
702
return self.CANNOT_DIFF
703
from_file_id = to_file_id = file_id
704
if old_kind == 'file':
705
old_date = _patch_header_date(self.old_tree, file_id, old_path)
706
elif old_kind is None:
707
old_date = self.EPOCH_DATE
710
return self.CANNOT_DIFF
711
if new_kind == 'file':
712
new_date = _patch_header_date(self.new_tree, file_id, new_path)
713
elif new_kind is None:
714
new_date = self.EPOCH_DATE
717
return self.CANNOT_DIFF
718
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
719
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
720
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
722
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
723
"""Diff the content of given files in two trees
725
:param from_file_id: The id of the file in the from tree. If None,
726
the file is not present in the from tree.
727
:param to_file_id: The id of the file in the to tree. This may refer
728
to a different file from from_file_id. If None,
729
the file is not present in the to tree.
731
def _get_text(tree, file_id):
732
if file_id is not None:
733
return tree.get_file(file_id).readlines()
737
from_text = _get_text(self.old_tree, from_file_id)
738
to_text = _get_text(self.new_tree, to_file_id)
739
self.text_differ(from_label, from_text, to_label, to_text,
741
except errors.BinaryFile:
743
("Binary files %s and %s differ\n" %
744
(from_label, to_label)).encode(self.path_encoding))
748
class DiffFromTool(DiffPath):
750
def __init__(self, command_template, old_tree, new_tree, to_file,
751
path_encoding='utf-8'):
752
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
753
self.command_template = command_template
754
self._root = tempfile.mkdtemp(prefix='bzr-diff-')
757
def from_string(klass, command_string, old_tree, new_tree, to_file,
758
path_encoding='utf-8'):
759
command_template = commands.shlex_split_unicode(command_string)
760
command_template.extend(['%(old_path)s', '%(new_path)s'])
761
return klass(command_template, old_tree, new_tree, to_file,
765
def make_from_diff_tree(klass, command_string):
766
def from_diff_tree(diff_tree):
767
return klass.from_string(command_string, diff_tree.old_tree,
768
diff_tree.new_tree, diff_tree.to_file)
769
return from_diff_tree
771
def _get_command(self, old_path, new_path):
772
my_map = {'old_path': old_path, 'new_path': new_path}
773
return [t % my_map for t in self.command_template]
775
def _execute(self, old_path, new_path):
776
proc = subprocess.Popen(self._get_command(old_path, new_path),
777
stdout=subprocess.PIPE, cwd=self._root)
778
self.to_file.write(proc.stdout.read())
781
def _write_file(self, file_id, tree, prefix, old_path):
782
full_old_path = osutils.pathjoin(self._root, prefix, old_path)
783
parent_dir = osutils.dirname(full_old_path)
785
os.makedirs(parent_dir)
787
if e.errno != errno.EEXIST:
789
source = tree.get_file(file_id)
791
target = open(full_old_path, 'wb')
793
osutils.pumpfile(source, target)
800
def _prepare_files(self, file_id, old_path, new_path):
801
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
803
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
805
return old_disk_path, new_disk_path
808
shutil.rmtree(self._root)
810
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
811
if (old_kind, new_kind) != ('file', 'file'):
812
return DiffPath.CANNOT_DIFF
813
self._prepare_files(file_id, old_path, new_path)
814
self._execute(osutils.pathjoin('old', old_path),
815
osutils.pathjoin('new', new_path))
818
class DiffTree(object):
819
"""Provides textual representations of the difference between two trees.
821
A DiffTree examines two trees and where a file-id has altered
822
between them, generates a textual representation of the difference.
823
DiffTree uses a sequence of DiffPath objects which are each
824
given the opportunity to handle a given altered fileid. The list
825
of DiffPath objects can be extended globally by appending to
826
DiffTree.diff_factories, or for a specific diff operation by
827
supplying the extra_factories option to the appropriate method.
830
# list of factories that can provide instances of DiffPath objects
831
# may be extended by plugins.
832
diff_factories = [DiffSymlink.from_diff_tree,
833
DiffDirectory.from_diff_tree]
835
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
836
diff_text=None, extra_factories=None):
839
:param old_tree: Tree to show as old in the comparison
840
:param new_tree: Tree to show as new in the comparison
841
:param to_file: File to write comparision to
842
:param path_encoding: Character encoding to write paths in
843
:param diff_text: DiffPath-type object to use as a last resort for
845
:param extra_factories: Factories of DiffPaths to try before any other
847
if diff_text is None:
848
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
849
'', '', internal_diff)
850
self.old_tree = old_tree
851
self.new_tree = new_tree
852
self.to_file = to_file
853
self.path_encoding = path_encoding
855
if extra_factories is not None:
856
self.differs.extend(f(self) for f in extra_factories)
857
self.differs.extend(f(self) for f in self.diff_factories)
858
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
861
def from_trees_options(klass, old_tree, new_tree, to_file,
862
path_encoding, external_diff_options, old_label,
864
"""Factory for producing a DiffTree.
866
Designed to accept options used by show_diff_trees.
867
:param old_tree: The tree to show as old in the comparison
868
:param new_tree: The tree to show as new in the comparison
869
:param to_file: File to write comparisons to
870
:param path_encoding: Character encoding to use for writing paths
871
:param external_diff_options: If supplied, use the installed diff
872
binary to perform file comparison, using supplied options.
873
:param old_label: Prefix to use for old file labels
874
:param new_label: Prefix to use for new file labels
875
:param using: Commandline to use to invoke an external diff tool
877
if using is not None:
878
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
881
if external_diff_options:
882
assert isinstance(external_diff_options, basestring)
883
opts = external_diff_options.split()
884
def diff_file(olab, olines, nlab, nlines, to_file):
885
external_diff(olab, olines, nlab, nlines, to_file, opts)
887
diff_file = internal_diff
888
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
889
old_label, new_label, diff_file)
890
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
893
def show_diff(self, specific_files, extra_trees=None):
894
"""Write tree diff to self.to_file
896
:param sepecific_files: the specific files to compare (recursive)
897
:param extra_trees: extra trees to use for mapping paths to file_ids
900
return self._show_diff(specific_files, extra_trees)
902
for differ in self.differs:
905
def _show_diff(self, specific_files, extra_trees):
906
# TODO: Generation of pseudo-diffs for added/deleted files could
907
# be usefully made into a much faster special case.
908
iterator = self.new_tree._iter_changes(self.old_tree,
909
specific_files=specific_files,
910
extra_trees=extra_trees,
911
require_versioned=True)
913
def changes_key(change):
914
old_path, new_path = change[1]
919
def get_encoded_path(path):
921
return path.encode(self.path_encoding, "replace")
922
for (file_id, paths, changed_content, versioned, parent, name, kind,
923
executable) in sorted(iterator, key=changes_key):
924
if parent == (None, None):
926
oldpath, newpath = paths
927
oldpath_encoded = get_encoded_path(paths[0])
928
newpath_encoded = get_encoded_path(paths[1])
929
old_present = (kind[0] is not None and versioned[0])
930
new_present = (kind[1] is not None and versioned[1])
931
renamed = (parent[0], name[0]) != (parent[1], name[1])
932
prop_str = get_prop_change(executable[0] != executable[1])
933
if (old_present, new_present) == (True, False):
934
self.to_file.write("=== removed %s '%s'\n" %
935
(kind[0], oldpath_encoded))
937
elif (old_present, new_present) == (False, True):
938
self.to_file.write("=== added %s '%s'\n" %
939
(kind[1], newpath_encoded))
942
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
943
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
945
# if it was produced by _iter_changes, it must be
946
# modified *somehow*, either content or execute bit.
947
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
948
newpath_encoded, prop_str))
950
self.diff(file_id, oldpath, newpath)
956
def diff(self, file_id, old_path, new_path):
957
"""Perform a diff of a single file
959
:param file_id: file-id of the file
960
:param old_path: The path of the file in the old tree
961
:param new_path: The path of the file in the new tree
964
old_kind = self.old_tree.kind(file_id)
965
except (errors.NoSuchId, errors.NoSuchFile):
968
new_kind = self.new_tree.kind(file_id)
969
except (errors.NoSuchId, errors.NoSuchFile):
972
result = DiffPath._diff_many(self.differs, file_id, old_path,
973
new_path, old_kind, new_kind)
974
if result is DiffPath.CANNOT_DIFF:
975
error_path = new_path
976
if error_path is None:
977
error_path = old_path
978
raise errors.NoDiffFound(error_path)