344
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")
347
466
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
348
467
external_diff_options=None,
349
468
old_label='a/', new_label='b/',
385
507
old_tree.unlock()
388
def _show_diff_trees(old_tree, new_tree, to_file,
389
specific_files, external_diff_options, path_encoding,
390
old_label='a/', new_label='b/', extra_trees=None):
392
# GNU Patch uses the epoch date to detect files that are being added
393
# or removed in a diff.
394
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
396
# TODO: Generation of pseudo-diffs for added/deleted files could
397
# be usefully made into a much faster special case.
399
if external_diff_options:
400
assert isinstance(external_diff_options, basestring)
401
opts = external_diff_options.split()
402
def diff_file(olab, olines, nlab, nlines, to_file):
403
external_diff(olab, olines, nlab, nlines, to_file, opts)
405
diff_file = internal_diff
407
delta = new_tree.changes_from(old_tree,
408
specific_files=specific_files,
409
extra_trees=extra_trees, require_versioned=True)
412
for path, file_id, kind in delta.removed:
414
path_encoded = path.encode(path_encoding, "replace")
415
to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
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:
423
path_encoded = path.encode(path_encoding, "replace")
424
to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
425
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
426
new_name = '%s%s\t%s' % (new_label, path,
427
_patch_header_date(new_tree, file_id, path))
428
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
429
old_name, None, None, to_file,
431
for (old_path, new_path, file_id, kind,
432
text_modified, meta_modified) in delta.renamed:
434
prop_str = get_prop_change(meta_modified)
435
oldpath_encoded = old_path.encode(path_encoding, "replace")
436
newpath_encoded = new_path.encode(path_encoding, "replace")
437
to_file.write("=== renamed %s '%s' => '%s'%s\n" % (kind,
438
oldpath_encoded, newpath_encoded, prop_str))
439
old_name = '%s%s\t%s' % (old_label, old_path,
440
_patch_header_date(old_tree, file_id,
442
new_name = '%s%s\t%s' % (new_label, new_path,
443
_patch_header_date(new_tree, file_id,
445
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
447
text_modified, kind, to_file, diff_file)
448
for path, file_id, kind, text_modified, meta_modified in delta.modified:
450
prop_str = get_prop_change(meta_modified)
451
path_encoded = path.encode(path_encoding, "replace")
452
to_file.write("=== modified %s '%s'%s\n" % (kind,
453
path_encoded, prop_str))
454
# The file may be in a different location in the old tree (because
455
# the containing dir was renamed, but the file itself was not)
456
old_path = old_tree.id2path(file_id)
457
old_name = '%s%s\t%s' % (old_label, old_path,
458
_patch_header_date(old_tree, file_id, old_path))
459
new_name = '%s%s\t%s' % (new_label, path,
460
_patch_header_date(new_tree, file_id, path))
462
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
464
True, kind, to_file, diff_file)
469
510
def _patch_header_date(tree, file_id, path):
470
511
"""Returns a timestamp suitable for use in a patch header."""
471
512
mtime = tree.get_file_mtime(file_id, path)
504
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
505
new_path, new_tree, text_modified,
506
kind, to_file, diff_file):
508
new_entry = new_tree.inventory[file_id]
509
old_tree.inventory[file_id].diff(diff_file,
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.
817
delta = self.new_tree.changes_from(self.old_tree,
818
specific_files=specific_files,
819
extra_trees=extra_trees, require_versioned=True)
822
for path, file_id, kind in delta.removed:
824
path_encoded = path.encode(self.path_encoding, "replace")
825
self.to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
826
self.diff(file_id, path, path)
828
for path, file_id, kind in delta.added:
830
path_encoded = path.encode(self.path_encoding, "replace")
831
self.to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
832
self.diff(file_id, path, path)
833
for (old_path, new_path, file_id, kind,
834
text_modified, meta_modified) in delta.renamed:
836
prop_str = get_prop_change(meta_modified)
837
oldpath_encoded = old_path.encode(self.path_encoding, "replace")
838
newpath_encoded = new_path.encode(self.path_encoding, "replace")
839
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" % (kind,
840
oldpath_encoded, newpath_encoded, prop_str))
842
self.diff(file_id, old_path, new_path)
843
for path, file_id, kind, text_modified, meta_modified in\
846
prop_str = get_prop_change(meta_modified)
847
path_encoded = path.encode(self.path_encoding, "replace")
848
self.to_file.write("=== modified %s '%s'%s\n" % (kind,
849
path_encoded, prop_str))
850
# The file may be in a different location in the old tree (because
851
# the containing dir was renamed, but the file itself was not)
853
old_path = self.old_tree.id2path(file_id)
854
self.diff(file_id, old_path, path)
857
def diff(self, file_id, old_path, new_path):
858
"""Perform a diff of a single file
860
:param file_id: file-id of the file
861
:param old_path: The path of the file in the old tree
862
:param new_path: The path of the file in the new tree
865
old_kind = self.old_tree.kind(file_id)
866
except (errors.NoSuchId, errors.NoSuchFile):
869
new_kind = self.new_tree.kind(file_id)
870
except (errors.NoSuchId, errors.NoSuchFile):
873
result = DiffPath._diff_many(self.differs, file_id, old_path,
874
new_path, old_kind, new_kind)
875
if result is DiffPath.CANNOT_DIFF:
876
error_path = new_path
877
if error_path is None:
878
error_path = old_path
879
raise errors.NoDiffFound(error_path)