375
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")
378
468
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
379
469
external_diff_options=None,
380
470
old_label='a/', new_label='b/',
472
path_encoding='utf8',
382
474
"""Show in text form the changes from one tree to another.
385
If set, include only changes to these files.
480
Include only changes to these files - None for all changes.
387
482
external_diff_options
388
483
If set, use an external GNU diff and pass these options.
391
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
393
492
old_tree.lock_read()
410
510
old_tree.unlock()
413
def _show_diff_trees(old_tree, new_tree, to_file,
414
specific_files, external_diff_options,
415
old_label='a/', new_label='b/', extra_trees=None):
417
# GNU Patch uses the epoch date to detect files that are being added
418
# or removed in a diff.
419
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
421
# TODO: Generation of pseudo-diffs for added/deleted files could
422
# be usefully made into a much faster special case.
424
if external_diff_options:
425
assert isinstance(external_diff_options, basestring)
426
opts = external_diff_options.split()
427
def diff_file(olab, olines, nlab, nlines, to_file):
428
external_diff(olab, olines, nlab, nlines, to_file, opts)
430
diff_file = internal_diff
432
delta = new_tree.changes_from(old_tree,
433
specific_files=specific_files,
434
extra_trees=extra_trees, require_versioned=True)
437
for path, file_id, kind in delta.removed:
439
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
440
old_name = '%s%s\t%s' % (old_label, path,
441
_patch_header_date(old_tree, file_id, path))
442
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
443
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
444
new_name, None, None, to_file)
445
for path, file_id, kind in delta.added:
447
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
448
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
449
new_name = '%s%s\t%s' % (new_label, path,
450
_patch_header_date(new_tree, file_id, path))
451
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
452
old_name, None, None, to_file,
454
for (old_path, new_path, file_id, kind,
455
text_modified, meta_modified) in delta.renamed:
457
prop_str = get_prop_change(meta_modified)
458
print >>to_file, '=== renamed %s %r => %r%s' % (
459
kind, old_path.encode('utf8'),
460
new_path.encode('utf8'), prop_str)
461
old_name = '%s%s\t%s' % (old_label, old_path,
462
_patch_header_date(old_tree, file_id,
464
new_name = '%s%s\t%s' % (new_label, new_path,
465
_patch_header_date(new_tree, file_id,
467
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
469
text_modified, kind, to_file, diff_file)
470
for path, file_id, kind, text_modified, meta_modified in delta.modified:
472
prop_str = get_prop_change(meta_modified)
473
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
474
# The file may be in a different location in the old tree (because
475
# the containing dir was renamed, but the file itself was not)
476
old_path = old_tree.id2path(file_id)
477
old_name = '%s%s\t%s' % (old_label, old_path,
478
_patch_header_date(old_tree, file_id, old_path))
479
new_name = '%s%s\t%s' % (new_label, path,
480
_patch_header_date(new_tree, file_id, path))
482
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
484
True, kind, to_file, diff_file)
489
513
def _patch_header_date(tree, file_id, path):
490
514
"""Returns a timestamp suitable for use in a patch header."""
491
515
mtime = tree.get_file_mtime(file_id, path)
524
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
525
new_path, new_tree, text_modified,
526
kind, to_file, diff_file):
528
new_entry = new_tree.inventory[file_id]
529
old_tree.inventory[file_id].diff(diff_file,
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)