89
93
if sequence_matcher is None:
90
94
sequence_matcher = patiencediff.PatienceSequenceMatcher
91
ud = unified_diff_bytes(
93
fromfile=old_label.encode(path_encoding, 'replace'),
94
tofile=new_label.encode(path_encoding, 'replace'),
95
n=context_lines, sequencematcher=sequence_matcher)
95
ud = patiencediff.unified_diff(oldlines, newlines,
96
fromfile=old_filename.encode(path_encoding, 'replace'),
97
tofile=new_filename.encode(path_encoding, 'replace'),
98
n=context_lines, sequencematcher=sequence_matcher)
98
if len(ud) == 0: # Identical contents, nothing to do
101
if len(ud) == 0: # Identical contents, nothing to do
100
103
# work-around for difflib being too smart for its own good
101
104
# if /dev/null is "1,0", patch won't recognize it as /dev/null
103
ud[2] = ud[2].replace(b'-1,0', b'-0,0')
106
ud[2] = ud[2].replace('-1,0', '-0,0')
104
107
elif not newlines:
105
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
108
ud[2] = ud[2].replace('+1,0', '+0,0')
108
111
to_file.write(line)
109
if not line.endswith(b'\n'):
110
to_file.write(b"\n\\ No newline at end of file\n")
114
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
115
tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
117
Compare two sequences of lines; generate the delta as a unified diff.
119
Unified diffs are a compact way of showing line changes and a few
120
lines of context. The number of context lines is set by 'n' which
123
By default, the diff control lines (those with ---, +++, or @@) are
124
created with a trailing newline. This is helpful so that inputs
125
created from file.readlines() result in diffs that are suitable for
126
file.writelines() since both the inputs and outputs have trailing
129
For inputs that do not have trailing newlines, set the lineterm
130
argument to "" so that the output will be uniformly newline free.
132
The unidiff format normally has a header for filenames and modification
133
times. Any or all of these may be specified using strings for
134
'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification
135
times are normally expressed in the format returned by time.ctime().
139
>>> for line in bytes_unified_diff(b'one two three four'.split(),
140
... b'zero one tree four'.split(), b'Original', b'Current',
141
... b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
144
--- Original Sat Jan 26 23:30:50 1991
145
+++ Current Fri Jun 06 10:20:52 2003
154
if sequencematcher is None:
155
sequencematcher = difflib.SequenceMatcher
158
fromfiledate = b'\t' + bytes(fromfiledate)
160
tofiledate = b'\t' + bytes(tofiledate)
163
for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
165
yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
166
yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
168
i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
169
yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
170
for tag, i1, i2, j1, j2 in group:
172
for line in a[i1:i2]:
175
if tag == 'replace' or tag == 'delete':
176
for line in a[i1:i2]:
178
if tag == 'replace' or tag == 'insert':
179
for line in b[j1:j2]:
112
if not line.endswith('\n'):
113
to_file.write("\n\\ No newline at end of file\n")
183
117
def _spawn_external_diff(diffcmd, capture_errors=True):
316
248
out, err = pipe.communicate()
318
250
# Write out the new i18n diff response
319
to_file.write(out + b'\n')
251
to_file.write(out+'\n')
320
252
if pipe.returncode != 2:
321
253
raise errors.BzrError(
322
'external diff failed with exit code 2'
323
' when run with LANG=C and LC_ALL=C,'
324
' but not when run natively: %r' % (diffcmd,))
254
'external diff failed with exit code 2'
255
' when run with LANG=C and LC_ALL=C,'
256
' but not when run natively: %r' % (diffcmd,))
326
first_line = lang_c_out.split(b'\n', 1)[0]
258
first_line = lang_c_out.split('\n', 1)[0]
327
259
# Starting with diffutils 2.8.4 the word "binary" was dropped.
328
m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
260
m = re.match('^(binary )?files.*differ$', first_line, re.I)
330
262
raise errors.BzrError('external diff failed with exit code 2;'
331
263
' command: %r' % (diffcmd,))
519
454
context = DEFAULT_CONTEXT_AMOUNT
520
455
if format_cls is None:
521
456
format_cls = DiffTree
522
with cleanup.ExitStack() as exit_stack:
523
exit_stack.enter_context(old_tree.lock_read())
457
with old_tree.lock_read():
524
458
if extra_trees is not None:
525
459
for tree in extra_trees:
526
exit_stack.enter_context(tree.lock_read())
527
exit_stack.enter_context(new_tree.lock_read())
528
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
530
external_diff_options,
531
old_label, new_label, using,
532
context_lines=context)
533
return differ.show_diff(specific_files, extra_trees)
536
def _patch_header_date(tree, path):
463
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
465
external_diff_options,
466
old_label, new_label, using,
467
context_lines=context)
468
return differ.show_diff(specific_files, extra_trees)
471
if extra_trees is not None:
472
for tree in extra_trees:
476
def _patch_header_date(tree, file_id, path):
537
477
"""Returns a timestamp suitable for use in a patch header."""
539
mtime = tree.get_file_mtime(path)
479
mtime = tree.get_file_mtime(path, file_id)
540
480
except FileTimestampUnavailable:
542
482
return timestamp.format_patch_date(mtime)
545
485
def get_executable_change(old_is_x, new_is_x):
546
descr = {True: b"+x", False: b"-x", None: b"??"}
486
descr = { True:"+x", False:"-x", None:"??" }
547
487
if old_is_x != new_is_x:
548
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
488
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
582
522
diff_tree.to_file, diff_tree.path_encoding)
585
def _diff_many(differs, old_path, new_path, old_kind, new_kind):
525
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
586
526
for file_differ in differs:
587
result = file_differ.diff(old_path, new_path, old_kind, new_kind)
527
result = file_differ.diff(file_id, old_path, new_path, old_kind,
588
529
if result is not DiffPath.CANNOT_DIFF:
619
560
if None in (old_kind, new_kind):
620
561
return DiffPath.CANNOT_DIFF
621
result = DiffPath._diff_many(
622
self.differs, old_path, new_path, old_kind, None)
562
result = DiffPath._diff_many(self.differs, file_id, old_path,
563
new_path, old_kind, None)
623
564
if result is DiffPath.CANNOT_DIFF:
625
return DiffPath._diff_many(
626
self.differs, old_path, new_path, None, new_kind)
629
class DiffTreeReference(DiffPath):
631
def diff(self, old_path, new_path, old_kind, new_kind):
632
"""Perform comparison between two tree references. (dummy)
635
if 'tree-reference' not in (old_kind, new_kind):
636
return self.CANNOT_DIFF
637
if old_kind not in ('tree-reference', None):
638
return self.CANNOT_DIFF
639
if new_kind not in ('tree-reference', None):
640
return self.CANNOT_DIFF
566
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
644
570
class DiffDirectory(DiffPath):
646
def diff(self, old_path, new_path, old_kind, new_kind):
572
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
647
573
"""Perform comparison between two directories. (dummy)
685
612
def diff_symlink(self, old_target, new_target):
686
613
if old_target is None:
687
self.to_file.write(b'=== target is \'%s\'\n' %
688
new_target.encode(self.path_encoding, 'replace'))
614
self.to_file.write('=== target is %r\n' % new_target)
689
615
elif new_target is None:
690
self.to_file.write(b'=== target was \'%s\'\n' %
691
old_target.encode(self.path_encoding, 'replace'))
616
self.to_file.write('=== target was %r\n' % old_target)
693
self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
694
(old_target.encode(self.path_encoding, 'replace'),
695
new_target.encode(self.path_encoding, 'replace')))
618
self.to_file.write('=== target changed %r => %r\n' %
619
(old_target, new_target))
696
620
return self.CHANGED
702
626
# or removed in a diff.
703
627
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
705
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
706
old_label='', new_label='', text_differ=internal_diff,
629
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
630
old_label='', new_label='', text_differ=internal_diff,
707
631
context_lines=DEFAULT_CONTEXT_AMOUNT):
708
632
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
709
633
self.text_differ = text_differ
723
648
if 'file' not in (old_kind, new_kind):
724
649
return self.CANNOT_DIFF
650
from_file_id = to_file_id = file_id
725
651
if old_kind == 'file':
726
old_date = _patch_header_date(self.old_tree, old_path)
652
old_date = _patch_header_date(self.old_tree, file_id, old_path)
727
653
elif old_kind is None:
728
654
old_date = self.EPOCH_DATE
730
657
return self.CANNOT_DIFF
731
658
if new_kind == 'file':
732
new_date = _patch_header_date(self.new_tree, new_path)
659
new_date = _patch_header_date(self.new_tree, file_id, new_path)
733
660
elif new_kind is None:
734
661
new_date = self.EPOCH_DATE
736
664
return self.CANNOT_DIFF
737
from_label = '%s%s\t%s' % (self.old_label, old_path,
739
to_label = '%s%s\t%s' % (self.new_label, new_path,
741
return self.diff_text(old_path, new_path, from_label, to_label)
665
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
666
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
667
return self.diff_text(old_path, new_path, from_label, to_label,
668
from_file_id, to_file_id)
743
def diff_text(self, from_path, to_path, from_label, to_label):
670
def diff_text(self, from_path, to_path, from_label, to_label,
671
from_file_id=None, to_file_id=None):
744
672
"""Diff the content of given files in two trees
746
674
:param from_path: The path in the from tree. If None,
748
676
:param to_path: The path in the to tree. This may refer
749
677
to a different file from from_path. If None,
750
678
the file is not present in the to tree.
679
:param from_file_id: The id of the file in the from tree or None if
681
:param to_file_id: The id of the file in the to tree or None if
752
def _get_text(tree, path):
756
return tree.get_file_lines(path)
757
except errors.NoSuchFile:
684
def _get_text(tree, file_id, path):
687
return tree.get_file_lines(path, file_id)
760
from_text = _get_text(self.old_tree, from_path)
761
to_text = _get_text(self.new_tree, to_path)
689
from_text = _get_text(self.old_tree, from_file_id, from_path)
690
to_text = _get_text(self.new_tree, to_file_id, to_path)
762
691
self.text_differ(from_label, from_text, to_label, to_text,
763
692
self.to_file, path_encoding=self.path_encoding,
764
693
context_lines=self.context_lines)
765
694
except errors.BinaryFile:
766
695
self.to_file.write(
767
("Binary files %s%s and %s%s differ\n" %
768
(self.old_label, from_path, self.new_label, to_path)).encode(self.path_encoding, 'replace'))
696
("Binary files %s and %s differ\n" %
697
(from_label, to_label)).encode(self.path_encoding, 'replace'))
769
698
return self.CHANGED
895
827
def _prepare_files(self, old_path, new_path, force_temp=False,
896
allow_write_new=False):
897
old_disk_path = self._write_file(
898
old_path, self.old_tree, 'old', force_temp)
899
new_disk_path = self._write_file(
900
new_path, self.new_tree, 'new', force_temp,
901
allow_write=allow_write_new)
828
allow_write_new=False, file_id=None):
829
old_disk_path = self._write_file(old_path, self.old_tree, 'old',
830
force_temp, file_id=file_id)
831
new_disk_path = self._write_file(new_path, self.new_tree, 'new',
832
force_temp, file_id=file_id,
833
allow_write=allow_write_new)
902
834
return old_disk_path, new_disk_path
904
836
def finish(self):
907
839
except OSError as e:
908
840
if e.errno != errno.ENOENT:
909
841
mutter("The temporary directory \"%s\" was not "
910
"cleanly removed: %s." % (self._root, e))
842
"cleanly removed: %s." % (self._root, e))
912
def diff(self, old_path, new_path, old_kind, new_kind):
844
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
913
845
if (old_kind, new_kind) != ('file', 'file'):
914
846
return DiffPath.CANNOT_DIFF
915
847
(old_disk_path, new_disk_path) = self._prepare_files(
848
old_path, new_path, file_id=file_id)
917
849
self._execute(old_disk_path, new_disk_path)
919
def edit_file(self, old_path, new_path):
851
def edit_file(self, old_path, new_path, file_id=None):
920
852
"""Use this tool to edit a file.
922
854
A temporary copy will be edited, and the new contents will be
857
:param file_id: The id of the file to edit.
925
858
:return: The new contents of the file.
927
860
old_abs_path, new_abs_path = self._prepare_files(
928
old_path, new_path, allow_write_new=True, force_temp=True)
861
old_path, new_path, allow_write_new=True, force_temp=True,
929
863
command = self._get_command(old_abs_path, new_abs_path)
930
864
subprocess.call(command, cwd=self._root)
931
865
with open(new_abs_path, 'rb') as new_file:
1029
960
# TODO: Generation of pseudo-diffs for added/deleted files could
1030
961
# be usefully made into a much faster special case.
1031
962
iterator = self.new_tree.iter_changes(self.old_tree,
1032
specific_files=specific_files,
1033
extra_trees=extra_trees,
1034
require_versioned=True)
963
specific_files=specific_files,
964
extra_trees=extra_trees,
965
require_versioned=True)
1037
967
def changes_key(change):
1038
old_path, new_path = change.path
968
old_path, new_path = change[1]
1040
970
if path is None:
1044
973
def get_encoded_path(path):
1045
974
if path is not None:
1046
975
return path.encode(self.path_encoding, "replace")
1047
for change in sorted(iterator, key=changes_key):
976
for (file_id, paths, changed_content, versioned, parent, name, kind,
977
executable) in sorted(iterator, key=changes_key):
1048
978
# The root does not get diffed, and items with no known kind (that
1049
979
# is, missing) in both trees are skipped as well.
1050
if change.parent_id == (None, None) or change.kind == (None, None):
1052
if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
1054
'Ignoring "%s" as symlinks are not '
1055
'supported on this filesystem.' % (change.path[0],))
1057
oldpath, newpath = change.path
1058
oldpath_encoded = get_encoded_path(change.path[0])
1059
newpath_encoded = get_encoded_path(change.path[1])
1060
old_present = (change.kind[0] is not None and change.versioned[0])
1061
new_present = (change.kind[1] is not None and change.versioned[1])
1062
executable = change.executable
1064
renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
980
if parent == (None, None) or kind == (None, None):
982
oldpath, newpath = paths
983
oldpath_encoded = get_encoded_path(paths[0])
984
newpath_encoded = get_encoded_path(paths[1])
985
old_present = (kind[0] is not None and versioned[0])
986
new_present = (kind[1] is not None and versioned[1])
987
renamed = (parent[0], name[0]) != (parent[1], name[1])
1066
989
properties_changed = []
1067
properties_changed.extend(
1068
get_executable_change(executable[0], executable[1]))
990
properties_changed.extend(get_executable_change(executable[0], executable[1]))
1070
992
if properties_changed:
1071
993
prop_str = b" (properties changed: %s)" % (
1072
b", ".join(properties_changed),)
994
b", ".join(properties_changed),)
1083
1005
oldpath = newpath
1085
1007
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1086
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1008
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1088
1010
# if it was produced by iter_changes, it must be
1089
1011
# modified *somehow*, either content or execute bit.
1090
1012
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1091
newpath_encoded, prop_str))
1092
if change.changed_content:
1093
self._diff(oldpath, newpath, kind[0], kind[1])
1013
newpath_encoded, prop_str))
1015
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
1094
1016
has_changes = 1
1096
1018
has_changes = 1
1097
1019
return has_changes
1099
def diff(self, old_path, new_path):
1021
def diff(self, file_id, old_path, new_path):
1100
1022
"""Perform a diff of a single file
1024
:param file_id: file-id of the file
1102
1025
:param old_path: The path of the file in the old tree
1103
1026
:param new_path: The path of the file in the new tree
1105
1028
if old_path is None:
1106
1029
old_kind = None
1108
old_kind = self.old_tree.kind(old_path)
1031
old_kind = self.old_tree.kind(old_path, file_id)
1109
1032
if new_path is None:
1110
1033
new_kind = None
1112
new_kind = self.new_tree.kind(new_path)
1113
self._diff(old_path, new_path, old_kind, new_kind)
1035
new_kind = self.new_tree.kind(new_path, file_id)
1036
self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1115
def _diff(self, old_path, new_path, old_kind, new_kind):
1116
result = DiffPath._diff_many(
1117
self.differs, old_path, new_path, old_kind, new_kind)
1038
def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
1039
result = DiffPath._diff_many(self.differs, file_id, old_path,
1040
new_path, old_kind, new_kind)
1118
1041
if result is DiffPath.CANNOT_DIFF:
1119
1042
error_path = new_path
1120
1043
if error_path is None: