351
375
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
consider_relpath = True
392
if path_list is None or len(path_list) == 0:
393
# If no path is given, the current working tree is used
394
default_location = u'.'
395
consider_relpath = False
396
elif old_url is not None and new_url is not None:
397
other_paths = path_list
398
make_paths_wt_relative = False
400
default_location = path_list[0]
401
other_paths = path_list[1:]
403
# Get the old location
406
old_url = default_location
407
working_tree, branch, relpath = \
408
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
409
if consider_relpath and relpath != '':
410
specific_files.append(relpath)
411
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
413
# Get the new location
415
new_url = default_location
416
if new_url != old_url:
417
working_tree, branch, relpath = \
418
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
419
if consider_relpath and relpath != '':
420
specific_files.append(relpath)
421
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
422
basis_is_default=working_tree is None)
424
# Get the specific files (all files is None, no files is [])
425
if make_paths_wt_relative and working_tree is not None:
426
other_paths = _relative_paths_in_tree(working_tree, other_paths)
427
specific_files.extend(other_paths)
428
if len(specific_files) == 0:
429
specific_files = None
431
# Get extra trees that ought to be searched for file-ids
433
if working_tree is not None and working_tree not in (old_tree, new_tree):
434
extra_trees = (working_tree,)
435
return old_tree, new_tree, specific_files, extra_trees
438
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
439
if branch is None and tree is not None:
441
if spec is None or spec.spec is None:
444
return tree.basis_tree()
446
return branch.basis_tree()
449
revision = spec.in_store(branch)
450
revision_id = revision.rev_id
451
rev_branch = revision.branch
452
return rev_branch.repository.revision_tree(revision_id)
455
def _relative_paths_in_tree(tree, paths):
456
"""Get the relative paths within a working tree.
458
Each path may be either an absolute path or a path relative to the
459
current working directory.
462
for filename in paths:
464
result.append(tree.relpath(osutils.dereference_path(filename)))
465
except errors.PathNotChild:
466
raise errors.BzrCommandError("Files are in different branches")
470
378
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
471
379
external_diff_options=None,
472
380
old_label='a/', new_label='b/',
474
path_encoding='utf8',
476
382
"""Show in text form the changes from one tree to another.
482
Include only changes to these files - None for all changes.
385
If set, include only changes to these files.
484
387
external_diff_options
485
388
If set, use an external GNU diff and pass these options.
488
391
If set, more Trees to use for looking up file ids
491
If set, the path will be encoded as specified, otherwise is supposed
494
393
old_tree.lock_read()
512
410
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 '%s'" % (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 '%s'" % (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 '%s' => %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 '%s'%s" % (kind, path.encode('utf8'),
475
# The file may be in a different location in the old tree (because
476
# the containing dir was renamed, but the file itself was not)
477
old_path = old_tree.id2path(file_id)
478
old_name = '%s%s\t%s' % (old_label, old_path,
479
_patch_header_date(old_tree, file_id, old_path))
480
new_name = '%s%s\t%s' % (new_label, path,
481
_patch_header_date(new_tree, file_id, path))
483
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
485
True, kind, to_file, diff_file)
515
490
def _patch_header_date(tree, file_id, path):
516
491
"""Returns a timestamp suitable for use in a patch header."""
517
492
mtime = tree.get_file_mtime(file_id, path)
550
class DiffPath(object):
551
"""Base type for command object that compare files"""
553
# The type or contents of the file were unsuitable for diffing
554
CANNOT_DIFF = 'CANNOT_DIFF'
555
# The file has changed in a semantic way
557
# The file content may have changed, but there is no semantic change
558
UNCHANGED = 'UNCHANGED'
560
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
563
:param old_tree: The tree to show as the old tree in the comparison
564
:param new_tree: The tree to show as new in the comparison
565
:param to_file: The file to write comparison data to
566
:param path_encoding: The character encoding to write paths in
568
self.old_tree = old_tree
569
self.new_tree = new_tree
570
self.to_file = to_file
571
self.path_encoding = path_encoding
577
def from_diff_tree(klass, diff_tree):
578
return klass(diff_tree.old_tree, diff_tree.new_tree,
579
diff_tree.to_file, diff_tree.path_encoding)
582
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
583
for file_differ in differs:
584
result = file_differ.diff(file_id, old_path, new_path, old_kind,
586
if result is not DiffPath.CANNOT_DIFF:
589
return DiffPath.CANNOT_DIFF
592
class DiffKindChange(object):
593
"""Special differ for file kind changes.
595
Represents kind change as deletion + creation. Uses the other differs
598
def __init__(self, differs):
599
self.differs = differs
605
def from_diff_tree(klass, diff_tree):
606
return klass(diff_tree.differs)
608
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
609
"""Perform comparison
611
:param file_id: The file_id of the file to compare
612
:param old_path: Path of the file in the old tree
613
:param new_path: Path of the file in the new tree
614
:param old_kind: Old file-kind of the file
615
:param new_kind: New file-kind of the file
617
if None in (old_kind, new_kind):
618
return DiffPath.CANNOT_DIFF
619
result = DiffPath._diff_many(self.differs, file_id, old_path,
620
new_path, old_kind, None)
621
if result is DiffPath.CANNOT_DIFF:
623
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
627
class DiffDirectory(DiffPath):
629
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
630
"""Perform comparison between two directories. (dummy)
633
if 'directory' not in (old_kind, new_kind):
634
return self.CANNOT_DIFF
635
if old_kind not in ('directory', None):
636
return self.CANNOT_DIFF
637
if new_kind not in ('directory', None):
638
return self.CANNOT_DIFF
642
class DiffSymlink(DiffPath):
644
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
645
"""Perform comparison between two symlinks
647
:param file_id: The file_id of the file to compare
648
:param old_path: Path of the file in the old tree
649
:param new_path: Path of the file in the new tree
650
:param old_kind: Old file-kind of the file
651
:param new_kind: New file-kind of the file
653
if 'symlink' not in (old_kind, new_kind):
654
return self.CANNOT_DIFF
655
if old_kind == 'symlink':
656
old_target = self.old_tree.get_symlink_target(file_id)
657
elif old_kind is None:
660
return self.CANNOT_DIFF
661
if new_kind == 'symlink':
662
new_target = self.new_tree.get_symlink_target(file_id)
663
elif new_kind is None:
666
return self.CANNOT_DIFF
667
return self.diff_symlink(old_target, new_target)
669
def diff_symlink(self, old_target, new_target):
670
if old_target is None:
671
self.to_file.write('=== target is %r\n' % new_target)
672
elif new_target is None:
673
self.to_file.write('=== target was %r\n' % old_target)
675
self.to_file.write('=== target changed %r => %r\n' %
676
(old_target, new_target))
680
class DiffText(DiffPath):
682
# GNU Patch uses the epoch date to detect files that are being added
683
# or removed in a diff.
684
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
686
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
687
old_label='', new_label='', text_differ=internal_diff):
688
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
689
self.text_differ = text_differ
690
self.old_label = old_label
691
self.new_label = new_label
692
self.path_encoding = path_encoding
694
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
695
"""Compare two files in unified diff format
697
:param file_id: The file_id of the file to compare
698
:param old_path: Path of the file in the old tree
699
:param new_path: Path of the file in the new tree
700
:param old_kind: Old file-kind of the file
701
:param new_kind: New file-kind of the file
703
if 'file' not in (old_kind, new_kind):
704
return self.CANNOT_DIFF
705
from_file_id = to_file_id = file_id
706
if old_kind == 'file':
707
old_date = _patch_header_date(self.old_tree, file_id, old_path)
708
elif old_kind is None:
709
old_date = self.EPOCH_DATE
712
return self.CANNOT_DIFF
713
if new_kind == 'file':
714
new_date = _patch_header_date(self.new_tree, file_id, new_path)
715
elif new_kind is None:
716
new_date = self.EPOCH_DATE
719
return self.CANNOT_DIFF
720
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
721
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
722
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
724
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
725
"""Diff the content of given files in two trees
727
:param from_file_id: The id of the file in the from tree. If None,
728
the file is not present in the from tree.
729
:param to_file_id: The id of the file in the to tree. This may refer
730
to a different file from from_file_id. If None,
731
the file is not present in the to tree.
733
def _get_text(tree, file_id):
734
if file_id is not None:
735
return tree.get_file(file_id).readlines()
739
from_text = _get_text(self.old_tree, from_file_id)
740
to_text = _get_text(self.new_tree, to_file_id)
741
self.text_differ(from_label, from_text, to_label, to_text,
743
except errors.BinaryFile:
745
("Binary files %s and %s differ\n" %
746
(from_label, to_label)).encode(self.path_encoding))
750
class DiffFromTool(DiffPath):
752
def __init__(self, command_template, old_tree, new_tree, to_file,
753
path_encoding='utf-8'):
754
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
755
self.command_template = command_template
756
self._root = tempfile.mkdtemp(prefix='bzr-diff-')
759
def from_string(klass, command_string, old_tree, new_tree, to_file,
760
path_encoding='utf-8'):
761
command_template = commands.shlex_split_unicode(command_string)
762
command_template.extend(['%(old_path)s', '%(new_path)s'])
763
return klass(command_template, old_tree, new_tree, to_file,
767
def make_from_diff_tree(klass, command_string):
768
def from_diff_tree(diff_tree):
769
return klass.from_string(command_string, diff_tree.old_tree,
770
diff_tree.new_tree, diff_tree.to_file)
771
return from_diff_tree
773
def _get_command(self, old_path, new_path):
774
my_map = {'old_path': old_path, 'new_path': new_path}
775
return [t % my_map for t in self.command_template]
777
def _execute(self, old_path, new_path):
778
command = self._get_command(old_path, new_path)
780
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
783
if e.errno == errno.ENOENT:
784
raise errors.ExecutableMissing(command[0])
787
self.to_file.write(proc.stdout.read())
790
def _try_symlink_root(self, tree, prefix):
791
if not (getattr(tree, 'abspath', None) is not None
792
and osutils.has_symlinks()):
795
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
797
if e.errno != errno.EEXIST:
801
def _write_file(self, file_id, tree, prefix, relpath):
802
full_path = osutils.pathjoin(self._root, prefix, relpath)
803
if self._try_symlink_root(tree, prefix):
805
parent_dir = osutils.dirname(full_path)
807
os.makedirs(parent_dir)
809
if e.errno != errno.EEXIST:
811
source = tree.get_file(file_id, relpath)
813
target = open(full_path, 'wb')
815
osutils.pumpfile(source, target)
820
osutils.make_readonly(full_path)
821
mtime = tree.get_file_mtime(file_id)
822
os.utime(full_path, (mtime, mtime))
825
def _prepare_files(self, file_id, old_path, new_path):
826
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
828
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
830
return old_disk_path, new_disk_path
833
osutils.rmtree(self._root)
835
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
836
if (old_kind, new_kind) != ('file', 'file'):
837
return DiffPath.CANNOT_DIFF
838
self._prepare_files(file_id, old_path, new_path)
839
self._execute(osutils.pathjoin('old', old_path),
840
osutils.pathjoin('new', new_path))
843
class DiffTree(object):
844
"""Provides textual representations of the difference between two trees.
846
A DiffTree examines two trees and where a file-id has altered
847
between them, generates a textual representation of the difference.
848
DiffTree uses a sequence of DiffPath objects which are each
849
given the opportunity to handle a given altered fileid. The list
850
of DiffPath objects can be extended globally by appending to
851
DiffTree.diff_factories, or for a specific diff operation by
852
supplying the extra_factories option to the appropriate method.
855
# list of factories that can provide instances of DiffPath objects
856
# may be extended by plugins.
857
diff_factories = [DiffSymlink.from_diff_tree,
858
DiffDirectory.from_diff_tree]
860
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
861
diff_text=None, extra_factories=None):
864
:param old_tree: Tree to show as old in the comparison
865
:param new_tree: Tree to show as new in the comparison
866
:param to_file: File to write comparision to
867
:param path_encoding: Character encoding to write paths in
868
:param diff_text: DiffPath-type object to use as a last resort for
870
:param extra_factories: Factories of DiffPaths to try before any other
872
if diff_text is None:
873
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
874
'', '', internal_diff)
875
self.old_tree = old_tree
876
self.new_tree = new_tree
877
self.to_file = to_file
878
self.path_encoding = path_encoding
880
if extra_factories is not None:
881
self.differs.extend(f(self) for f in extra_factories)
882
self.differs.extend(f(self) for f in self.diff_factories)
883
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
886
def from_trees_options(klass, old_tree, new_tree, to_file,
887
path_encoding, external_diff_options, old_label,
889
"""Factory for producing a DiffTree.
891
Designed to accept options used by show_diff_trees.
892
:param old_tree: The tree to show as old in the comparison
893
:param new_tree: The tree to show as new in the comparison
894
:param to_file: File to write comparisons to
895
:param path_encoding: Character encoding to use for writing paths
896
:param external_diff_options: If supplied, use the installed diff
897
binary to perform file comparison, using supplied options.
898
:param old_label: Prefix to use for old file labels
899
:param new_label: Prefix to use for new file labels
900
:param using: Commandline to use to invoke an external diff tool
902
if using is not None:
903
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
906
if external_diff_options:
907
assert isinstance(external_diff_options, basestring)
908
opts = external_diff_options.split()
909
def diff_file(olab, olines, nlab, nlines, to_file):
910
external_diff(olab, olines, nlab, nlines, to_file, opts)
912
diff_file = internal_diff
913
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
914
old_label, new_label, diff_file)
915
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
918
def show_diff(self, specific_files, extra_trees=None):
919
"""Write tree diff to self.to_file
921
:param sepecific_files: the specific files to compare (recursive)
922
:param extra_trees: extra trees to use for mapping paths to file_ids
925
return self._show_diff(specific_files, extra_trees)
927
for differ in self.differs:
930
def _show_diff(self, specific_files, extra_trees):
931
# TODO: Generation of pseudo-diffs for added/deleted files could
932
# be usefully made into a much faster special case.
933
iterator = self.new_tree.iter_changes(self.old_tree,
934
specific_files=specific_files,
935
extra_trees=extra_trees,
936
require_versioned=True)
938
def changes_key(change):
939
old_path, new_path = change[1]
944
def get_encoded_path(path):
946
return path.encode(self.path_encoding, "replace")
947
for (file_id, paths, changed_content, versioned, parent, name, kind,
948
executable) in sorted(iterator, key=changes_key):
949
if parent == (None, None):
951
oldpath, newpath = paths
952
oldpath_encoded = get_encoded_path(paths[0])
953
newpath_encoded = get_encoded_path(paths[1])
954
old_present = (kind[0] is not None and versioned[0])
955
new_present = (kind[1] is not None and versioned[1])
956
renamed = (parent[0], name[0]) != (parent[1], name[1])
957
prop_str = get_prop_change(executable[0] != executable[1])
958
if (old_present, new_present) == (True, False):
959
self.to_file.write("=== removed %s '%s'\n" %
960
(kind[0], oldpath_encoded))
962
elif (old_present, new_present) == (False, True):
963
self.to_file.write("=== added %s '%s'\n" %
964
(kind[1], newpath_encoded))
967
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
968
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
970
# if it was produced by iter_changes, it must be
971
# modified *somehow*, either content or execute bit.
972
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
973
newpath_encoded, prop_str))
975
self.diff(file_id, oldpath, newpath)
981
def diff(self, file_id, old_path, new_path):
982
"""Perform a diff of a single file
984
:param file_id: file-id of the file
985
:param old_path: The path of the file in the old tree
986
:param new_path: The path of the file in the new tree
989
old_kind = self.old_tree.kind(file_id)
990
except (errors.NoSuchId, errors.NoSuchFile):
993
new_kind = self.new_tree.kind(file_id)
994
except (errors.NoSuchId, errors.NoSuchFile):
997
result = DiffPath._diff_many(self.differs, file_id, old_path,
998
new_path, old_kind, new_kind)
999
if result is DiffPath.CANNOT_DIFF:
1000
error_path = new_path
1001
if error_path is None:
1002
error_path = old_path
1003
raise errors.NoDiffFound(error_path)
525
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
526
new_path, new_tree, text_modified,
527
kind, to_file, diff_file):
529
new_entry = new_tree.inventory[file_id]
530
old_tree.inventory[file_id].diff(diff_file,