14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
25
23
from .lazy_import import lazy_import
26
24
lazy_import(globals(), """
31
30
from breezy import (
94
86
if sequence_matcher is None:
95
87
sequence_matcher = patiencediff.PatienceSequenceMatcher
96
ud = patiencediff.unified_diff_bytes(oldlines, newlines,
97
fromfile=old_label.encode(
98
path_encoding, 'replace'),
99
tofile=new_label.encode(
100
path_encoding, 'replace'),
101
n=context_lines, sequencematcher=sequence_matcher)
88
ud = unified_diff_bytes(
90
fromfile=old_label.encode(path_encoding, 'replace'),
91
tofile=new_label.encode(path_encoding, 'replace'),
92
n=context_lines, sequencematcher=sequence_matcher)
104
95
if len(ud) == 0: # Identical contents, nothing to do
117
108
to_file.write(b'\n')
111
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
112
tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
114
Compare two sequences of lines; generate the delta as a unified diff.
116
Unified diffs are a compact way of showing line changes and a few
117
lines of context. The number of context lines is set by 'n' which
120
By default, the diff control lines (those with ---, +++, or @@) are
121
created with a trailing newline. This is helpful so that inputs
122
created from file.readlines() result in diffs that are suitable for
123
file.writelines() since both the inputs and outputs have trailing
126
For inputs that do not have trailing newlines, set the lineterm
127
argument to "" so that the output will be uniformly newline free.
129
The unidiff format normally has a header for filenames and modification
130
times. Any or all of these may be specified using strings for
131
'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification
132
times are normally expressed in the format returned by time.ctime().
136
>>> for line in bytes_unified_diff(b'one two three four'.split(),
137
... b'zero one tree four'.split(), b'Original', b'Current',
138
... b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
141
--- Original Sat Jan 26 23:30:50 1991
142
+++ Current Fri Jun 06 10:20:52 2003
151
if sequencematcher is None:
152
sequencematcher = difflib.SequenceMatcher
155
fromfiledate = b'\t' + bytes(fromfiledate)
157
tofiledate = b'\t' + bytes(tofiledate)
160
for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
162
yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
163
yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
165
i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
166
yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
167
for tag, i1, i2, j1, j2 in group:
169
for line in a[i1:i2]:
172
if tag == 'replace' or tag == 'delete':
173
for line in a[i1:i2]:
175
if tag == 'replace' or tag == 'insert':
176
for line in b[j1:j2]:
120
180
def _spawn_external_diff(diffcmd, capture_errors=True):
121
181
"""Spawn the external diff process, and return the child handle.
304
364
def get_trees_and_branches_to_diff_locked(
305
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
365
path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
306
366
"""Get the trees and specific files to diff given a list of paths.
308
368
This method works out the trees to be diff'ed and the files of
320
380
The url of the new branch or tree. If None, the tree to use is
321
381
taken from the first path, if any, or the current working tree.
323
a callable like Command.add_cleanup. get_trees_and_branches_to_diff
383
an ExitStack object. get_trees_and_branches_to_diff
324
384
will register cleanups that must be run to unlock the trees, etc.
325
385
:param apply_view:
326
386
if True and a view is set, apply the view or check that the paths
329
389
a tuple of (old_tree, new_tree, old_branch, new_branch,
330
390
specific_files, extra_trees) where extra_trees is a sequence of
331
391
additional trees to search in for file-ids. The trees and branches
332
will be read-locked until the cleanups registered via the add_cleanup
392
will be read-locked until the cleanups registered via the exit_stack
335
395
# Get the old and new revision specs
458
516
context = DEFAULT_CONTEXT_AMOUNT
459
517
if format_cls is None:
460
518
format_cls = DiffTree
461
with old_tree.lock_read():
519
with contextlib.ExitStack() as exit_stack:
520
exit_stack.enter_context(old_tree.lock_read())
462
521
if extra_trees is not None:
463
522
for tree in extra_trees:
467
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
469
external_diff_options,
470
old_label, new_label, using,
471
context_lines=context)
472
return differ.show_diff(specific_files, extra_trees)
475
if extra_trees is not None:
476
for tree in extra_trees:
480
def _patch_header_date(tree, file_id, path):
523
exit_stack.enter_context(tree.lock_read())
524
exit_stack.enter_context(new_tree.lock_read())
525
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
527
external_diff_options,
528
old_label, new_label, using,
529
context_lines=context)
530
return differ.show_diff(specific_files, extra_trees)
533
def _patch_header_date(tree, path):
481
534
"""Returns a timestamp suitable for use in a patch header."""
483
536
mtime = tree.get_file_mtime(path)
526
579
diff_tree.to_file, diff_tree.path_encoding)
529
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
582
def _diff_many(differs, old_path, new_path, old_kind, new_kind):
530
583
for file_differ in differs:
531
result = file_differ.diff(file_id, old_path, new_path, old_kind,
584
result = file_differ.diff(old_path, new_path, old_kind, new_kind)
533
585
if result is not DiffPath.CANNOT_DIFF:
553
605
def from_diff_tree(klass, diff_tree):
554
606
return klass(diff_tree.differs)
556
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
608
def diff(self, old_path, new_path, old_kind, new_kind):
557
609
"""Perform comparison
559
:param file_id: The file_id of the file to compare
560
611
:param old_path: Path of the file in the old tree
561
612
:param new_path: Path of the file in the new tree
562
613
:param old_kind: Old file-kind of the file
565
616
if None in (old_kind, new_kind):
566
617
return DiffPath.CANNOT_DIFF
567
result = DiffPath._diff_many(self.differs, file_id, old_path,
568
new_path, old_kind, None)
618
result = DiffPath._diff_many(
619
self.differs, old_path, new_path, old_kind, None)
569
620
if result is DiffPath.CANNOT_DIFF:
571
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
622
return DiffPath._diff_many(
623
self.differs, old_path, new_path, None, new_kind)
626
class DiffTreeReference(DiffPath):
628
def diff(self, old_path, new_path, old_kind, new_kind):
629
"""Perform comparison between two tree references. (dummy)
632
if 'tree-reference' not in (old_kind, new_kind):
633
return self.CANNOT_DIFF
634
if old_kind not in ('tree-reference', None):
635
return self.CANNOT_DIFF
636
if new_kind not in ('tree-reference', None):
637
return self.CANNOT_DIFF
575
641
class DiffDirectory(DiffPath):
577
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
643
def diff(self, old_path, new_path, old_kind, new_kind):
578
644
"""Perform comparison between two directories. (dummy)
590
656
class DiffSymlink(DiffPath):
592
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
658
def diff(self, old_path, new_path, old_kind, new_kind):
593
659
"""Perform comparison between two symlinks
595
:param file_id: The file_id of the file to compare
596
661
:param old_path: Path of the file in the old tree
597
662
:param new_path: Path of the file in the new tree
598
663
:param old_kind: Old file-kind of the file
644
709
self.path_encoding = path_encoding
645
710
self.context_lines = context_lines
647
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
712
def diff(self, old_path, new_path, old_kind, new_kind):
648
713
"""Compare two files in unified diff format
650
:param file_id: The file_id of the file to compare
651
715
:param old_path: Path of the file in the old tree
652
716
:param new_path: Path of the file in the new tree
653
717
:param old_kind: Old file-kind of the file
656
720
if 'file' not in (old_kind, new_kind):
657
721
return self.CANNOT_DIFF
658
from_file_id = to_file_id = file_id
659
722
if old_kind == 'file':
660
old_date = _patch_header_date(self.old_tree, file_id, old_path)
723
old_date = _patch_header_date(self.old_tree, old_path)
661
724
elif old_kind is None:
662
725
old_date = self.EPOCH_DATE
665
727
return self.CANNOT_DIFF
666
728
if new_kind == 'file':
667
new_date = _patch_header_date(self.new_tree, file_id, new_path)
729
new_date = _patch_header_date(self.new_tree, new_path)
668
730
elif new_kind is None:
669
731
new_date = self.EPOCH_DATE
672
733
return self.CANNOT_DIFF
673
from_label = '%s%s\t%s' % (self.old_label, old_path,
675
to_label = '%s%s\t%s' % (self.new_label, new_path,
677
return self.diff_text(old_path, new_path, from_label, to_label,
678
from_file_id, to_file_id)
734
from_label = '%s%s\t%s' % (
735
self.old_label, old_path or new_path, old_date)
736
to_label = '%s%s\t%s' % (
737
self.new_label, new_path or old_path, new_date)
738
return self.diff_text(old_path, new_path, from_label, to_label)
680
def diff_text(self, from_path, to_path, from_label, to_label,
681
from_file_id=None, to_file_id=None):
740
def diff_text(self, from_path, to_path, from_label, to_label):
682
741
"""Diff the content of given files in two trees
684
743
:param from_path: The path in the from tree. If None,
686
745
:param to_path: The path in the to tree. This may refer
687
746
to a different file from from_path. If None,
688
747
the file is not present in the to tree.
689
:param from_file_id: The id of the file in the from tree or None if
691
:param to_file_id: The id of the file in the to tree or None if
694
def _get_text(tree, file_id, path):
697
return tree.get_file_lines(path)
749
def _get_text(tree, path):
753
return tree.get_file_lines(path)
754
except errors.NoSuchFile:
699
from_text = _get_text(self.old_tree, from_file_id, from_path)
700
to_text = _get_text(self.new_tree, to_file_id, to_path)
757
from_text = _get_text(self.old_tree, from_path)
758
to_text = _get_text(self.new_tree, to_path)
701
759
self.text_differ(from_label, from_text, to_label, to_text,
702
760
self.to_file, path_encoding=self.path_encoding,
703
761
context_lines=self.context_lines)
704
762
except errors.BinaryFile:
705
763
self.to_file.write(
706
("Binary files %s and %s differ\n" %
707
(from_label, to_label)).encode(self.path_encoding, 'replace'))
764
("Binary files %s%s and %s%s differ\n" %
765
(self.old_label, from_path or to_path,
766
self.new_label, to_path or from_path)
767
).encode(self.path_encoding, 'replace'))
708
768
return self.CHANGED
717
777
self._root = osutils.mkdtemp(prefix='brz-diff-')
720
def from_string(klass, command_string, old_tree, new_tree, to_file,
780
def from_string(klass, command_template, old_tree, new_tree, to_file,
721
781
path_encoding='utf-8'):
722
command_template = cmdline.split(command_string)
723
if '@' not in command_string:
724
command_template.extend(['@old_path', '@new_path'])
725
782
return klass(command_template, old_tree, new_tree, to_file,
738
795
def _get_command(self, old_path, new_path):
739
796
my_map = {'old_path': old_path, 'new_path': new_path}
740
command = [AtTemplate(t).substitute(my_map) for t in
797
command = [t.format(**my_map) for t in
741
798
self.command_template]
799
if command == self.command_template:
800
command += [old_path, new_path]
742
801
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
743
802
command_encoded = []
744
803
for c in command:
745
if isinstance(c, text_type):
804
if isinstance(c, str):
746
805
command_encoded.append(c.encode('mbcs'))
748
807
command_encoded.append(c)
804
863
return osutils.pathjoin(self._root, prefix, relpath_tmp)
806
865
def _write_file(self, relpath, tree, prefix, force_temp=False,
807
allow_write=False, file_id=None):
808
867
if not force_temp and isinstance(tree, WorkingTree):
809
868
full_path = tree.abspath(relpath)
810
869
if self._is_safepath(full_path):
819
878
except OSError as e:
820
879
if e.errno != errno.EEXIST:
822
source = tree.get_file(relpath)
824
with open(full_path, 'wb') as target:
825
osutils.pumpfile(source, target)
881
with tree.get_file(relpath) as source, \
882
open(full_path, 'wb') as target:
883
osutils.pumpfile(source, target)
829
885
mtime = tree.get_file_mtime(relpath)
830
886
except FileTimestampUnavailable:
838
894
def _prepare_files(self, old_path, new_path, force_temp=False,
839
allow_write_new=False, file_id=None):
840
old_disk_path = self._write_file(old_path, self.old_tree, 'old',
841
force_temp, file_id=file_id)
842
new_disk_path = self._write_file(new_path, self.new_tree, 'new',
843
force_temp, file_id=file_id,
844
allow_write=allow_write_new)
895
allow_write_new=False):
896
old_disk_path = self._write_file(
897
old_path, self.old_tree, 'old', force_temp)
898
new_disk_path = self._write_file(
899
new_path, self.new_tree, 'new', force_temp,
900
allow_write=allow_write_new)
845
901
return old_disk_path, new_disk_path
847
903
def finish(self):
852
908
mutter("The temporary directory \"%s\" was not "
853
909
"cleanly removed: %s." % (self._root, e))
855
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
911
def diff(self, old_path, new_path, old_kind, new_kind):
856
912
if (old_kind, new_kind) != ('file', 'file'):
857
913
return DiffPath.CANNOT_DIFF
858
914
(old_disk_path, new_disk_path) = self._prepare_files(
859
old_path, new_path, file_id=file_id)
860
916
self._execute(old_disk_path, new_disk_path)
862
def edit_file(self, old_path, new_path, file_id=None):
918
def edit_file(self, old_path, new_path):
863
919
"""Use this tool to edit a file.
865
921
A temporary copy will be edited, and the new contents will be
868
:param file_id: The id of the file to edit.
869
924
:return: The new contents of the file.
871
926
old_abs_path, new_abs_path = self._prepare_files(
872
old_path, new_path, allow_write_new=True, force_temp=True,
927
old_path, new_path, allow_write_new=True, force_temp=True)
874
928
command = self._get_command(old_abs_path, new_abs_path)
875
929
subprocess.call(command, cwd=self._root)
876
930
with open(new_abs_path, 'rb') as new_file:
892
946
# list of factories that can provide instances of DiffPath objects
893
947
# may be extended by plugins.
894
948
diff_factories = [DiffSymlink.from_diff_tree,
895
DiffDirectory.from_diff_tree]
949
DiffDirectory.from_diff_tree,
950
DiffTreeReference.from_diff_tree]
897
952
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
898
953
diff_text=None, extra_factories=None):
988
1043
def get_encoded_path(path):
989
1044
if path is not None:
990
1045
return path.encode(self.path_encoding, "replace")
991
for (file_id, paths, changed_content, versioned, parent, name, kind,
992
executable) in sorted(iterator, key=changes_key):
1046
for change in sorted(iterator, key=changes_key):
993
1047
# The root does not get diffed, and items with no known kind (that
994
1048
# is, missing) in both trees are skipped as well.
995
if parent == (None, None) or kind == (None, None):
997
oldpath, newpath = paths
998
oldpath_encoded = get_encoded_path(paths[0])
999
newpath_encoded = get_encoded_path(paths[1])
1000
old_present = (kind[0] is not None and versioned[0])
1001
new_present = (kind[1] is not None and versioned[1])
1002
renamed = (parent[0], name[0]) != (parent[1], name[1])
1049
if change.parent_id == (None, None) or change.kind == (None, None):
1051
if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
1053
'Ignoring "%s" as symlinks are not '
1054
'supported on this filesystem.' % (change.path[0],))
1056
oldpath, newpath = change.path
1057
oldpath_encoded = get_encoded_path(oldpath)
1058
newpath_encoded = get_encoded_path(newpath)
1059
old_present = (change.kind[0] is not None and change.versioned[0])
1060
new_present = (change.kind[1] is not None and change.versioned[1])
1061
executable = change.executable
1063
renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
1004
1065
properties_changed = []
1005
1066
properties_changed.extend(
1014
1075
if (old_present, new_present) == (True, False):
1015
1076
self.to_file.write(b"=== removed %s '%s'\n" %
1016
1077
(kind[0].encode('ascii'), oldpath_encoded))
1018
1078
elif (old_present, new_present) == (False, True):
1019
1079
self.to_file.write(b"=== added %s '%s'\n" %
1020
1080
(kind[1].encode('ascii'), newpath_encoded))
1023
1082
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1024
1083
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1027
1086
# modified *somehow*, either content or execute bit.
1028
1087
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1029
1088
newpath_encoded, prop_str))
1031
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
1089
if change.changed_content:
1090
self._diff(oldpath, newpath, kind[0], kind[1])
1032
1091
has_changes = 1
1034
1093
has_changes = 1
1035
1094
return has_changes
1037
def diff(self, file_id, old_path, new_path):
1096
def diff(self, old_path, new_path):
1038
1097
"""Perform a diff of a single file
1040
:param file_id: file-id of the file
1041
1099
:param old_path: The path of the file in the old tree
1042
1100
:param new_path: The path of the file in the new tree
1049
1107
new_kind = None
1051
1109
new_kind = self.new_tree.kind(new_path)
1052
self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1110
self._diff(old_path, new_path, old_kind, new_kind)
1054
def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
1055
result = DiffPath._diff_many(self.differs, file_id, old_path,
1056
new_path, old_kind, new_kind)
1112
def _diff(self, old_path, new_path, old_kind, new_kind):
1113
result = DiffPath._diff_many(
1114
self.differs, old_path, new_path, old_kind, new_kind)
1057
1115
if result is DiffPath.CANNOT_DIFF:
1058
1116
error_path = new_path
1059
1117
if error_path is None: