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 (
46
from bzrlib.trace import mutter, warning
49
# TODO: Rather than building a changeset object, we should probably
50
# invoke callbacks on an object. That object can either accumulate a
51
# list, write them out directly, etc etc.
54
class _PrematchedMatcher(difflib.SequenceMatcher):
55
"""Allow SequenceMatcher operations to use predetermined blocks"""
57
def __init__(self, matching_blocks):
58
difflib.SequenceMatcher(self, None, None)
59
self.matching_blocks = matching_blocks
63
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
64
allow_binary=False, sequence_matcher=None,
65
path_encoding='utf8'):
66
# FIXME: difflib is wrong if there is no trailing newline.
67
# The syntax used by patch seems to be "\ No newline at
68
# end of file" following the last diff line from that
69
# file. This is not trivial to insert into the
70
# unified_diff output and it might be better to just fix
71
# or replace that function.
73
# In the meantime we at least make sure the patch isn't
77
# Special workaround for Python2.3, where difflib fails if
78
# both sequences are empty.
79
if not oldlines and not newlines:
82
if allow_binary is False:
83
textfile.check_text_lines(oldlines)
84
textfile.check_text_lines(newlines)
86
if sequence_matcher is None:
87
sequence_matcher = patiencediff.PatienceSequenceMatcher
88
ud = patiencediff.unified_diff(oldlines, newlines,
89
fromfile=old_filename.encode(path_encoding),
90
tofile=new_filename.encode(path_encoding),
91
sequencematcher=sequence_matcher)
94
if len(ud) == 0: # Identical contents, nothing to do
96
# work-around for difflib being too smart for its own good
97
# if /dev/null is "1,0", patch won't recognize it as /dev/null
99
ud[2] = ud[2].replace('-1,0', '-0,0')
101
ud[2] = ud[2].replace('+1,0', '+0,0')
102
# work around for difflib emitting random spaces after the label
103
ud[0] = ud[0][:-2] + '\n'
104
ud[1] = ud[1][:-2] + '\n'
108
if not line.endswith('\n'):
109
to_file.write("\n\\ No newline at end of file\n")
113
def _spawn_external_diff(diffcmd, capture_errors=True):
114
"""Spawn the externall diff process, and return the child handle.
116
:param diffcmd: The command list to spawn
117
:param capture_errors: Capture stderr as well as setting LANG=C
118
and LC_ALL=C. This lets us read and understand the output of diff,
119
and respond to any errors.
120
:return: A Popen object.
123
# construct minimal environment
125
path = os.environ.get('PATH')
128
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
131
stderr = subprocess.PIPE
137
pipe = subprocess.Popen(diffcmd,
138
stdin=subprocess.PIPE,
139
stdout=subprocess.PIPE,
143
if e.errno == errno.ENOENT:
144
raise errors.NoDiff(str(e))
150
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
152
"""Display a diff by calling out to the external diff program."""
153
# make sure our own output is properly ordered before the diff
156
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
157
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
158
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
159
newtmpf = os.fdopen(newtmp_fd, 'wb')
162
# TODO: perhaps a special case for comparing to or from the empty
163
# sequence; can just use /dev/null on Unix
165
# TODO: if either of the files being compared already exists as a
166
# regular named file (e.g. in the working directory) then we can
167
# compare directly to that, rather than copying it.
169
oldtmpf.writelines(oldlines)
170
newtmpf.writelines(newlines)
178
'--label', old_filename,
180
'--label', new_filename,
185
# diff only allows one style to be specified; they don't override.
186
# note that some of these take optargs, and the optargs can be
187
# directly appended to the options.
188
# this is only an approximate parser; it doesn't properly understand
190
for s in ['-c', '-u', '-C', '-U',
195
'-y', '--side-by-side',
207
diffcmd.extend(diff_opts)
209
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
210
out,err = pipe.communicate()
213
# internal_diff() adds a trailing newline, add one here for consistency
216
# 'diff' gives retcode == 2 for all sorts of errors
217
# one of those is 'Binary files differ'.
218
# Bad options could also be the problem.
219
# 'Binary files' is not a real error, so we suppress that error.
222
# Since we got here, we want to make sure to give an i18n error
223
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
224
out, err = pipe.communicate()
226
# Write out the new i18n diff response
227
to_file.write(out+'\n')
228
if pipe.returncode != 2:
229
raise errors.BzrError(
230
'external diff failed with exit code 2'
231
' when run with LANG=C and LC_ALL=C,'
232
' but not when run natively: %r' % (diffcmd,))
234
first_line = lang_c_out.split('\n', 1)[0]
235
# Starting with diffutils 2.8.4 the word "binary" was dropped.
236
m = re.match('^(binary )?files.*differ$', first_line, re.I)
238
raise errors.BzrError('external diff failed with exit code 2;'
239
' command: %r' % (diffcmd,))
241
# Binary files differ, just return
244
# If we got to here, we haven't written out the output of diff
248
# returns 1 if files differ; that's OK
250
msg = 'signal %d' % (-rc)
252
msg = 'exit code %d' % rc
254
raise errors.BzrError('external diff failed with %s; command: %r'
259
oldtmpf.close() # and delete
261
# Clean up. Warn in case the files couldn't be deleted
262
# (in case windows still holds the file open, but not
263
# if the files have already been deleted)
265
os.remove(old_abspath)
267
if e.errno not in (errno.ENOENT,):
268
warning('Failed to delete temporary file: %s %s',
271
os.remove(new_abspath)
273
if e.errno not in (errno.ENOENT,):
274
warning('Failed to delete temporary file: %s %s',
278
@deprecated_function(one_zero)
279
def diff_cmd_helper(tree, specific_files, external_diff_options,
280
old_revision_spec=None, new_revision_spec=None,
282
old_label='a/', new_label='b/'):
283
"""Helper for cmd_diff.
288
:param specific_files:
289
The specific files to compare, or None
291
:param external_diff_options:
292
If non-None, run an external diff, and pass it these options
294
:param old_revision_spec:
295
If None, use basis tree as old revision, otherwise use the tree for
296
the specified revision.
298
:param new_revision_spec:
299
If None, use working tree as new revision, otherwise use the tree for
300
the specified revision.
302
:param revision_specs:
303
Zero, one or two RevisionSpecs from the command line, saying what revisions
304
to compare. This can be passed as an alternative to the old_revision_spec
305
and new_revision_spec parameters.
307
The more general form is show_diff_trees(), where the caller
308
supplies any two trees.
311
# TODO: perhaps remove the old parameters old_revision_spec and
312
# new_revision_spec, since this is only really for use from cmd_diff and
313
# it now always passes through a sequence of revision_specs -- mbp
318
revision = spec.in_store(tree.branch)
320
revision = spec.in_store(None)
321
revision_id = revision.rev_id
322
branch = revision.branch
323
return branch.repository.revision_tree(revision_id)
325
if revision_specs is not None:
326
if len(revision_specs) > 0:
327
old_revision_spec = revision_specs[0]
328
if len(revision_specs) > 1:
329
new_revision_spec = revision_specs[1]
331
if old_revision_spec is None:
332
old_tree = tree.basis_tree()
334
old_tree = spec_tree(old_revision_spec)
336
if (new_revision_spec is None
337
or new_revision_spec.spec is None):
340
new_tree = spec_tree(new_revision_spec)
342
if new_tree is not tree:
343
extra_trees = (tree,)
347
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
348
external_diff_options,
349
old_label=old_label, new_label=new_label,
350
extra_trees=extra_trees)
353
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
354
"""Get the trees and specific files to diff given a list of paths.
356
This method works out the trees to be diff'ed and the files of
357
interest within those trees.
360
the list of arguments passed to the diff command
361
:param revision_specs:
362
Zero, one or two RevisionSpecs from the diff command line,
363
saying what revisions to compare.
365
The url of the old branch or tree. If None, the tree to use is
366
taken from the first path, if any, or the current working tree.
368
The url of the new branch or tree. If None, the tree to use is
369
taken from the first path, if any, or the current working tree.
371
a tuple of (old_tree, new_tree, specific_files, extra_trees) where
372
extra_trees is a sequence of additional trees to search in for
375
# Get the old and new revision specs
376
old_revision_spec = None
377
new_revision_spec = None
378
if revision_specs is not None:
379
if len(revision_specs) > 0:
380
old_revision_spec = revision_specs[0]
382
old_url = old_revision_spec.get_branch()
383
if len(revision_specs) > 1:
384
new_revision_spec = revision_specs[1]
386
new_url = new_revision_spec.get_branch()
389
make_paths_wt_relative = True
390
consider_relpath = True
391
if path_list is None or len(path_list) == 0:
392
# If no path is given, the current working tree is used
393
default_location = u'.'
394
consider_relpath = False
395
elif old_url is not None and new_url is not None:
396
other_paths = path_list
397
make_paths_wt_relative = False
399
default_location = path_list[0]
400
other_paths = path_list[1:]
402
# Get the old location
405
old_url = default_location
406
working_tree, branch, relpath = \
407
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
408
if consider_relpath and relpath != '':
409
specific_files.append(relpath)
410
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
412
# Get the new location
414
new_url = default_location
415
if new_url != old_url:
416
working_tree, branch, relpath = \
417
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
418
if consider_relpath and relpath != '':
419
specific_files.append(relpath)
420
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
421
basis_is_default=working_tree is None)
423
# Get the specific files (all files is None, no files is [])
424
if make_paths_wt_relative and working_tree is not None:
425
other_paths = _relative_paths_in_tree(working_tree, other_paths)
426
specific_files.extend(other_paths)
427
if len(specific_files) == 0:
428
specific_files = None
430
# Get extra trees that ought to be searched for file-ids
432
if working_tree is not None and working_tree not in (old_tree, new_tree):
433
extra_trees = (working_tree,)
434
return old_tree, new_tree, specific_files, extra_trees
437
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
438
if branch is None and tree is not None:
440
if spec is None or spec.spec is None:
443
return tree.basis_tree()
445
return branch.basis_tree()
448
revision = spec.in_store(branch)
449
revision_id = revision.rev_id
450
rev_branch = revision.branch
451
return rev_branch.repository.revision_tree(revision_id)
454
def _relative_paths_in_tree(tree, paths):
455
"""Get the relative paths within a working tree.
457
Each path may be either an absolute path or a path relative to the
458
current working directory.
461
for filename in paths:
463
result.append(tree.relpath(osutils.dereference_path(filename)))
464
except errors.PathNotChild:
465
raise errors.BzrCommandError("Files are in different branches")
469
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
470
external_diff_options=None,
471
old_label='a/', new_label='b/',
473
path_encoding='utf8',
475
"""Show in text form the changes from one tree to another.
481
Include only changes to these files - None for all changes.
483
external_diff_options
484
If set, use an external GNU diff and pass these options.
487
If set, more Trees to use for looking up file ids
490
If set, the path will be encoded as specified, otherwise is supposed
495
if extra_trees is not None:
496
for tree in extra_trees:
500
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
502
external_diff_options,
503
old_label, new_label, using)
504
return differ.show_diff(specific_files, extra_trees)
507
if extra_trees is not None:
508
for tree in extra_trees:
514
def _patch_header_date(tree, file_id, path):
515
"""Returns a timestamp suitable for use in a patch header."""
516
mtime = tree.get_file_mtime(file_id, path)
517
return timestamp.format_patch_date(mtime)
520
def _raise_if_nonexistent(paths, old_tree, new_tree):
521
"""Complain if paths are not in either inventory or tree.
523
It's OK with the files exist in either tree's inventory, or
524
if they exist in the tree but are not versioned.
526
This can be used by operations such as bzr status that can accept
527
unknown or ignored files.
529
mutter("check paths: %r", paths)
532
s = old_tree.filter_unversioned_files(paths)
533
s = new_tree.filter_unversioned_files(s)
534
s = [path for path in s if not new_tree.has_filename(path)]
536
raise errors.PathsDoNotExist(sorted(s))
539
@deprecated_function(one_three)
540
def get_prop_change(meta_modified):
542
return " (properties changed)"
546
def get_executable_change(old_is_x, new_is_x):
547
descr = { True:"+x", False:"-x", None:"??" }
548
if old_is_x != new_is_x:
549
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
554
class DiffPath(object):
555
"""Base type for command object that compare files"""
557
# The type or contents of the file were unsuitable for diffing
558
CANNOT_DIFF = 'CANNOT_DIFF'
559
# The file has changed in a semantic way
561
# The file content may have changed, but there is no semantic change
562
UNCHANGED = 'UNCHANGED'
564
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
567
:param old_tree: The tree to show as the old tree in the comparison
568
:param new_tree: The tree to show as new in the comparison
569
:param to_file: The file to write comparison data to
570
:param path_encoding: The character encoding to write paths in
572
self.old_tree = old_tree
573
self.new_tree = new_tree
574
self.to_file = to_file
575
self.path_encoding = path_encoding
581
def from_diff_tree(klass, diff_tree):
582
return klass(diff_tree.old_tree, diff_tree.new_tree,
583
diff_tree.to_file, diff_tree.path_encoding)
586
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
587
for file_differ in differs:
588
result = file_differ.diff(file_id, old_path, new_path, old_kind,
590
if result is not DiffPath.CANNOT_DIFF:
593
return DiffPath.CANNOT_DIFF
596
class DiffKindChange(object):
597
"""Special differ for file kind changes.
599
Represents kind change as deletion + creation. Uses the other differs
602
def __init__(self, differs):
603
self.differs = differs
609
def from_diff_tree(klass, diff_tree):
610
return klass(diff_tree.differs)
612
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
613
"""Perform comparison
615
:param file_id: The file_id of the file to compare
616
:param old_path: Path of the file in the old tree
617
:param new_path: Path of the file in the new tree
618
:param old_kind: Old file-kind of the file
619
:param new_kind: New file-kind of the file
621
if None in (old_kind, new_kind):
622
return DiffPath.CANNOT_DIFF
623
result = DiffPath._diff_many(self.differs, file_id, old_path,
624
new_path, old_kind, None)
625
if result is DiffPath.CANNOT_DIFF:
627
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
631
class DiffDirectory(DiffPath):
633
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
634
"""Perform comparison between two directories. (dummy)
637
if 'directory' not in (old_kind, new_kind):
638
return self.CANNOT_DIFF
639
if old_kind not in ('directory', None):
640
return self.CANNOT_DIFF
641
if new_kind not in ('directory', None):
642
return self.CANNOT_DIFF
646
class DiffSymlink(DiffPath):
648
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
649
"""Perform comparison between two symlinks
651
:param file_id: The file_id of the file to compare
652
:param old_path: Path of the file in the old tree
653
:param new_path: Path of the file in the new tree
654
:param old_kind: Old file-kind of the file
655
:param new_kind: New file-kind of the file
657
if 'symlink' not in (old_kind, new_kind):
658
return self.CANNOT_DIFF
659
if old_kind == 'symlink':
660
old_target = self.old_tree.get_symlink_target(file_id)
661
elif old_kind is None:
664
return self.CANNOT_DIFF
665
if new_kind == 'symlink':
666
new_target = self.new_tree.get_symlink_target(file_id)
667
elif new_kind is None:
670
return self.CANNOT_DIFF
671
return self.diff_symlink(old_target, new_target)
673
def diff_symlink(self, old_target, new_target):
674
if old_target is None:
675
self.to_file.write('=== target is %r\n' % new_target)
676
elif new_target is None:
677
self.to_file.write('=== target was %r\n' % old_target)
679
self.to_file.write('=== target changed %r => %r\n' %
680
(old_target, new_target))
684
class DiffText(DiffPath):
686
# GNU Patch uses the epoch date to detect files that are being added
687
# or removed in a diff.
688
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
690
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
691
old_label='', new_label='', text_differ=internal_diff):
692
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
693
self.text_differ = text_differ
694
self.old_label = old_label
695
self.new_label = new_label
696
self.path_encoding = path_encoding
698
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
699
"""Compare two files in unified diff format
701
:param file_id: The file_id of the file to compare
702
:param old_path: Path of the file in the old tree
703
:param new_path: Path of the file in the new tree
704
:param old_kind: Old file-kind of the file
705
:param new_kind: New file-kind of the file
707
if 'file' not in (old_kind, new_kind):
708
return self.CANNOT_DIFF
709
from_file_id = to_file_id = file_id
710
if old_kind == 'file':
711
old_date = _patch_header_date(self.old_tree, file_id, old_path)
712
elif old_kind is None:
713
old_date = self.EPOCH_DATE
716
return self.CANNOT_DIFF
717
if new_kind == 'file':
718
new_date = _patch_header_date(self.new_tree, file_id, new_path)
719
elif new_kind is None:
720
new_date = self.EPOCH_DATE
723
return self.CANNOT_DIFF
724
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
725
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
726
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
728
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
729
"""Diff the content of given files in two trees
731
:param from_file_id: The id of the file in the from tree. If None,
732
the file is not present in the from tree.
733
:param to_file_id: The id of the file in the to tree. This may refer
734
to a different file from from_file_id. If None,
735
the file is not present in the to tree.
737
def _get_text(tree, file_id):
738
if file_id is not None:
739
return tree.get_file(file_id).readlines()
743
from_text = _get_text(self.old_tree, from_file_id)
744
to_text = _get_text(self.new_tree, to_file_id)
745
self.text_differ(from_label, from_text, to_label, to_text,
747
except errors.BinaryFile:
749
("Binary files %s and %s differ\n" %
750
(from_label, to_label)).encode(self.path_encoding))
754
class DiffFromTool(DiffPath):
756
def __init__(self, command_template, old_tree, new_tree, to_file,
757
path_encoding='utf-8'):
758
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
759
self.command_template = command_template
760
self._root = tempfile.mkdtemp(prefix='bzr-diff-')
763
def from_string(klass, command_string, old_tree, new_tree, to_file,
764
path_encoding='utf-8'):
765
command_template = commands.shlex_split_unicode(command_string)
766
command_template.extend(['%(old_path)s', '%(new_path)s'])
767
return klass(command_template, old_tree, new_tree, to_file,
771
def make_from_diff_tree(klass, command_string):
772
def from_diff_tree(diff_tree):
773
return klass.from_string(command_string, diff_tree.old_tree,
774
diff_tree.new_tree, diff_tree.to_file)
775
return from_diff_tree
777
def _get_command(self, old_path, new_path):
778
my_map = {'old_path': old_path, 'new_path': new_path}
779
return [t % my_map for t in self.command_template]
781
def _execute(self, old_path, new_path):
782
command = self._get_command(old_path, new_path)
784
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
787
if e.errno == errno.ENOENT:
788
raise errors.ExecutableMissing(command[0])
791
self.to_file.write(proc.stdout.read())
794
def _try_symlink_root(self, tree, prefix):
795
if not (getattr(tree, 'abspath', None) is not None
796
and osutils.has_symlinks()):
799
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
801
if e.errno != errno.EEXIST:
805
def _write_file(self, file_id, tree, prefix, relpath):
806
full_path = osutils.pathjoin(self._root, prefix, relpath)
807
if self._try_symlink_root(tree, prefix):
809
parent_dir = osutils.dirname(full_path)
811
os.makedirs(parent_dir)
813
if e.errno != errno.EEXIST:
815
source = tree.get_file(file_id, relpath)
817
target = open(full_path, 'wb')
819
osutils.pumpfile(source, target)
824
osutils.make_readonly(full_path)
825
mtime = tree.get_file_mtime(file_id)
826
os.utime(full_path, (mtime, mtime))
829
def _prepare_files(self, file_id, old_path, new_path):
830
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
832
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
834
return old_disk_path, new_disk_path
837
osutils.rmtree(self._root)
839
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
840
if (old_kind, new_kind) != ('file', 'file'):
841
return DiffPath.CANNOT_DIFF
842
self._prepare_files(file_id, old_path, new_path)
843
self._execute(osutils.pathjoin('old', old_path),
844
osutils.pathjoin('new', new_path))
847
class DiffTree(object):
848
"""Provides textual representations of the difference between two trees.
850
A DiffTree examines two trees and where a file-id has altered
851
between them, generates a textual representation of the difference.
852
DiffTree uses a sequence of DiffPath objects which are each
853
given the opportunity to handle a given altered fileid. The list
854
of DiffPath objects can be extended globally by appending to
855
DiffTree.diff_factories, or for a specific diff operation by
856
supplying the extra_factories option to the appropriate method.
859
# list of factories that can provide instances of DiffPath objects
860
# may be extended by plugins.
861
diff_factories = [DiffSymlink.from_diff_tree,
862
DiffDirectory.from_diff_tree]
864
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
865
diff_text=None, extra_factories=None):
868
:param old_tree: Tree to show as old in the comparison
869
:param new_tree: Tree to show as new in the comparison
870
:param to_file: File to write comparision to
871
:param path_encoding: Character encoding to write paths in
872
:param diff_text: DiffPath-type object to use as a last resort for
874
:param extra_factories: Factories of DiffPaths to try before any other
876
if diff_text is None:
877
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
878
'', '', internal_diff)
879
self.old_tree = old_tree
880
self.new_tree = new_tree
881
self.to_file = to_file
882
self.path_encoding = path_encoding
884
if extra_factories is not None:
885
self.differs.extend(f(self) for f in extra_factories)
886
self.differs.extend(f(self) for f in self.diff_factories)
887
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
890
def from_trees_options(klass, old_tree, new_tree, to_file,
891
path_encoding, external_diff_options, old_label,
893
"""Factory for producing a DiffTree.
895
Designed to accept options used by show_diff_trees.
896
:param old_tree: The tree to show as old in the comparison
897
:param new_tree: The tree to show as new in the comparison
898
:param to_file: File to write comparisons to
899
:param path_encoding: Character encoding to use for writing paths
900
:param external_diff_options: If supplied, use the installed diff
901
binary to perform file comparison, using supplied options.
902
:param old_label: Prefix to use for old file labels
903
:param new_label: Prefix to use for new file labels
904
:param using: Commandline to use to invoke an external diff tool
906
if using is not None:
907
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
910
if external_diff_options:
911
opts = external_diff_options.split()
912
def diff_file(olab, olines, nlab, nlines, to_file):
913
external_diff(olab, olines, nlab, nlines, to_file, opts)
915
diff_file = internal_diff
916
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
917
old_label, new_label, diff_file)
918
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
921
def show_diff(self, specific_files, extra_trees=None):
922
"""Write tree diff to self.to_file
924
:param sepecific_files: the specific files to compare (recursive)
925
:param extra_trees: extra trees to use for mapping paths to file_ids
928
return self._show_diff(specific_files, extra_trees)
930
for differ in self.differs:
933
def _show_diff(self, specific_files, extra_trees):
934
# TODO: Generation of pseudo-diffs for added/deleted files could
935
# be usefully made into a much faster special case.
936
iterator = self.new_tree.iter_changes(self.old_tree,
937
specific_files=specific_files,
938
extra_trees=extra_trees,
939
require_versioned=True)
941
def changes_key(change):
942
old_path, new_path = change[1]
947
def get_encoded_path(path):
949
return path.encode(self.path_encoding, "replace")
950
for (file_id, paths, changed_content, versioned, parent, name, kind,
951
executable) in sorted(iterator, key=changes_key):
952
if parent == (None, None):
954
oldpath, newpath = paths
955
oldpath_encoded = get_encoded_path(paths[0])
956
newpath_encoded = get_encoded_path(paths[1])
957
old_present = (kind[0] is not None and versioned[0])
958
new_present = (kind[1] is not None and versioned[1])
959
renamed = (parent[0], name[0]) != (parent[1], name[1])
961
properties_changed = []
962
properties_changed.extend(get_executable_change(executable[0], executable[1]))
964
if properties_changed:
965
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
969
if (old_present, new_present) == (True, False):
970
self.to_file.write("=== removed %s '%s'\n" %
971
(kind[0], oldpath_encoded))
973
elif (old_present, new_present) == (False, True):
974
self.to_file.write("=== added %s '%s'\n" %
975
(kind[1], newpath_encoded))
978
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
979
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
981
# if it was produced by iter_changes, it must be
982
# modified *somehow*, either content or execute bit.
983
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
984
newpath_encoded, prop_str))
986
self.diff(file_id, oldpath, newpath)
992
def diff(self, file_id, old_path, new_path):
993
"""Perform a diff of a single file
995
:param file_id: file-id of the file
996
:param old_path: The path of the file in the old tree
997
:param new_path: The path of the file in the new tree
1000
old_kind = self.old_tree.kind(file_id)
1001
except (errors.NoSuchId, errors.NoSuchFile):
1004
new_kind = self.new_tree.kind(file_id)
1005
except (errors.NoSuchId, errors.NoSuchFile):
1008
result = DiffPath._diff_many(self.differs, file_id, old_path,
1009
new_path, old_kind, new_kind)
1010
if result is DiffPath.CANNOT_DIFF:
1011
error_path = new_path
1012
if error_path is None:
1013
error_path = old_path
1014
raise errors.NoDiffFound(error_path)