25
from .lazy_import import lazy_import
25
from bzrlib.lazy_import import lazy_import
26
26
lazy_import(globals(), """
42
from breezy.workingtree import WorkingTree
43
from breezy.i18n import gettext
43
from bzrlib.workingtree import WorkingTree
44
from bzrlib.i18n import gettext
46
from .registry import (
47
from bzrlib.registry import (
49
from .sixish import text_type
50
from .trace import mutter, note, warning
51
from .tree import FileTimestampUnavailable
50
from bzrlib.trace import mutter, note, warning
54
52
DEFAULT_CONTEXT_AMOUNT = 3
57
54
class AtTemplate(string.Template):
58
55
"""Templating class that uses @ instead of $."""
87
84
# In the meantime we at least make sure the patch isn't
88
# Special workaround for Python2.3, where difflib fails if
89
# both sequences are empty.
90
if not oldlines and not newlines:
90
93
if allow_binary is False:
91
94
textfile.check_text_lines(oldlines)
92
95
textfile.check_text_lines(newlines)
94
97
if sequence_matcher is None:
95
98
sequence_matcher = patiencediff.PatienceSequenceMatcher
96
ud = unified_diff_bytes(
98
fromfile=old_label.encode(path_encoding, 'replace'),
99
tofile=new_label.encode(path_encoding, 'replace'),
100
n=context_lines, sequencematcher=sequence_matcher)
99
ud = patiencediff.unified_diff(oldlines, newlines,
100
fromfile=old_filename.encode(path_encoding, 'replace'),
101
tofile=new_filename.encode(path_encoding, 'replace'),
102
n=context_lines, sequencematcher=sequence_matcher)
103
if len(ud) == 0: # Identical contents, nothing to do
105
if len(ud) == 0: # Identical contents, nothing to do
105
107
# work-around for difflib being too smart for its own good
106
108
# if /dev/null is "1,0", patch won't recognize it as /dev/null
108
ud[2] = ud[2].replace(b'-1,0', b'-0,0')
110
ud[2] = ud[2].replace('-1,0', '-0,0')
109
111
elif not newlines:
110
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
112
ud[2] = ud[2].replace('+1,0', '+0,0')
113
115
to_file.write(line)
114
if not line.endswith(b'\n'):
115
to_file.write(b"\n\\ No newline at end of file\n")
119
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
120
tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
122
Compare two sequences of lines; generate the delta as a unified diff.
124
Unified diffs are a compact way of showing line changes and a few
125
lines of context. The number of context lines is set by 'n' which
128
By default, the diff control lines (those with ---, +++, or @@) are
129
created with a trailing newline. This is helpful so that inputs
130
created from file.readlines() result in diffs that are suitable for
131
file.writelines() since both the inputs and outputs have trailing
134
For inputs that do not have trailing newlines, set the lineterm
135
argument to "" so that the output will be uniformly newline free.
137
The unidiff format normally has a header for filenames and modification
138
times. Any or all of these may be specified using strings for
139
'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification
140
times are normally expressed in the format returned by time.ctime().
144
>>> for line in bytes_unified_diff(b'one two three four'.split(),
145
... b'zero one tree four'.split(), b'Original', b'Current',
146
... b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
149
--- Original Sat Jan 26 23:30:50 1991
150
+++ Current Fri Jun 06 10:20:52 2003
159
if sequencematcher is None:
160
sequencematcher = difflib.SequenceMatcher
163
fromfiledate = b'\t' + bytes(fromfiledate)
165
tofiledate = b'\t' + bytes(tofiledate)
168
for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
170
yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
171
yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
173
i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
174
yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
175
for tag, i1, i2, j1, j2 in group:
177
for line in a[i1:i2]:
180
if tag == 'replace' or tag == 'delete':
181
for line in a[i1:i2]:
183
if tag == 'replace' or tag == 'insert':
184
for line in b[j1:j2]:
116
if not line.endswith('\n'):
117
to_file.write("\n\\ No newline at end of file\n")
188
121
def _spawn_external_diff(diffcmd, capture_errors=True):
261
def external_diff(old_label, oldlines, new_label, newlines, to_file,
192
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
263
194
"""Display a diff by calling out to the external diff program."""
264
195
# make sure our own output is properly ordered before the diff
267
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='brz-diff-old-')
268
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='brz-diff-new-')
198
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
199
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
269
200
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
270
201
newtmpf = os.fdopen(newtmp_fd, 'wb')
321
252
out, err = pipe.communicate()
323
254
# Write out the new i18n diff response
324
to_file.write(out + b'\n')
255
to_file.write(out+'\n')
325
256
if pipe.returncode != 2:
326
257
raise errors.BzrError(
327
'external diff failed with exit code 2'
328
' when run with LANG=C and LC_ALL=C,'
329
' but not when run natively: %r' % (diffcmd,))
258
'external diff failed with exit code 2'
259
' when run with LANG=C and LC_ALL=C,'
260
' but not when run natively: %r' % (diffcmd,))
331
first_line = lang_c_out.split(b'\n', 1)[0]
262
first_line = lang_c_out.split('\n', 1)[0]
332
263
# Starting with diffutils 2.8.4 the word "binary" was dropped.
333
m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
264
m = re.match('^(binary )?files.*differ$', first_line, re.I)
335
266
raise errors.BzrError('external diff failed with exit code 2;'
336
267
' command: %r' % (diffcmd,))
543
476
if extra_trees is not None:
544
477
for tree in extra_trees:
548
def _patch_header_date(tree, path):
483
def _patch_header_date(tree, file_id, path):
549
484
"""Returns a timestamp suitable for use in a patch header."""
551
mtime = tree.get_file_mtime(path)
552
except FileTimestampUnavailable:
486
mtime = tree.get_file_mtime(file_id, path)
487
except errors.FileTimestampUnavailable:
554
489
return timestamp.format_patch_date(mtime)
557
492
def get_executable_change(old_is_x, new_is_x):
558
descr = {True: b"+x", False: b"-x", None: b"??"}
493
descr = { True:"+x", False:"-x", None:"??" }
559
494
if old_is_x != new_is_x:
560
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
495
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
631
567
if None in (old_kind, new_kind):
632
568
return DiffPath.CANNOT_DIFF
633
result = DiffPath._diff_many(
634
self.differs, old_path, new_path, old_kind, None)
569
result = DiffPath._diff_many(self.differs, file_id, old_path,
570
new_path, old_kind, None)
635
571
if result is DiffPath.CANNOT_DIFF:
637
return DiffPath._diff_many(
638
self.differs, old_path, new_path, None, new_kind)
573
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
641
577
class DiffDirectory(DiffPath):
643
def diff(self, old_path, new_path, old_kind, new_kind):
579
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
644
580
"""Perform comparison between two directories. (dummy)
682
619
def diff_symlink(self, old_target, new_target):
683
620
if old_target is None:
684
self.to_file.write(b'=== target is \'%s\'\n' %
685
new_target.encode(self.path_encoding, 'replace'))
621
self.to_file.write('=== target is %r\n' % new_target)
686
622
elif new_target is None:
687
self.to_file.write(b'=== target was \'%s\'\n' %
688
old_target.encode(self.path_encoding, 'replace'))
623
self.to_file.write('=== target was %r\n' % old_target)
690
self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
691
(old_target.encode(self.path_encoding, 'replace'),
692
new_target.encode(self.path_encoding, 'replace')))
625
self.to_file.write('=== target changed %r => %r\n' %
626
(old_target, new_target))
693
627
return self.CHANGED
699
633
# or removed in a diff.
700
634
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
702
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
703
old_label='', new_label='', text_differ=internal_diff,
636
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
637
old_label='', new_label='', text_differ=internal_diff,
704
638
context_lines=DEFAULT_CONTEXT_AMOUNT):
705
639
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
706
640
self.text_differ = text_differ
720
655
if 'file' not in (old_kind, new_kind):
721
656
return self.CANNOT_DIFF
657
from_file_id = to_file_id = file_id
722
658
if old_kind == 'file':
723
old_date = _patch_header_date(self.old_tree, old_path)
659
old_date = _patch_header_date(self.old_tree, file_id, old_path)
724
660
elif old_kind is None:
725
661
old_date = self.EPOCH_DATE
727
664
return self.CANNOT_DIFF
728
665
if new_kind == 'file':
729
new_date = _patch_header_date(self.new_tree, new_path)
666
new_date = _patch_header_date(self.new_tree, file_id, new_path)
730
667
elif new_kind is None:
731
668
new_date = self.EPOCH_DATE
733
671
return self.CANNOT_DIFF
734
from_label = '%s%s\t%s' % (self.old_label, old_path,
736
to_label = '%s%s\t%s' % (self.new_label, new_path,
738
return self.diff_text(old_path, new_path, from_label, to_label)
672
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
673
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
674
return self.diff_text(from_file_id, to_file_id, from_label, to_label,
740
def diff_text(self, from_path, to_path, from_label, to_label):
677
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
678
from_path=None, to_path=None):
741
679
"""Diff the content of given files in two trees
743
:param from_path: The path in the from tree. If None,
681
:param from_file_id: The id of the file in the from tree. If None,
744
682
the file is not present in the from tree.
745
:param to_path: The path in the to tree. This may refer
746
to a different file from from_path. If None,
683
:param to_file_id: The id of the file in the to tree. This may refer
684
to a different file from from_file_id. If None,
747
685
the file is not present in the to tree.
686
:param from_path: The path in the from tree or None if unknown.
687
:param to_path: The path in the to tree or None if unknown.
749
def _get_text(tree, path):
753
return tree.get_file_lines(path)
754
except errors.NoSuchFile:
689
def _get_text(tree, file_id, path):
690
if file_id is not None:
691
return tree.get_file_lines(file_id, path)
757
from_text = _get_text(self.old_tree, from_path)
758
to_text = _get_text(self.new_tree, to_path)
695
from_text = _get_text(self.old_tree, from_file_id, from_path)
696
to_text = _get_text(self.new_tree, to_file_id, to_path)
759
697
self.text_differ(from_label, from_text, to_label, to_text,
760
698
self.to_file, path_encoding=self.path_encoding,
761
699
context_lines=self.context_lines)
762
700
except errors.BinaryFile:
763
701
self.to_file.write(
764
("Binary files %s and %s differ\n" %
765
(from_label, to_label)).encode(self.path_encoding, 'replace'))
702
("Binary files %s and %s differ\n" %
703
(from_label, to_label)).encode(self.path_encoding,'replace'))
766
704
return self.CHANGED
893
833
osutils.make_readonly(full_path)
896
def _prepare_files(self, old_path, new_path, force_temp=False,
836
def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
897
837
allow_write_new=False):
898
old_disk_path = self._write_file(
899
old_path, self.old_tree, 'old', force_temp)
900
new_disk_path = self._write_file(
901
new_path, self.new_tree, 'new', force_temp,
902
allow_write=allow_write_new)
838
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
839
old_path, force_temp)
840
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
841
new_path, force_temp,
842
allow_write=allow_write_new)
903
843
return old_disk_path, new_disk_path
905
845
def finish(self):
907
847
osutils.rmtree(self._root)
909
849
if e.errno != errno.ENOENT:
910
850
mutter("The temporary directory \"%s\" was not "
911
"cleanly removed: %s." % (self._root, e))
851
"cleanly removed: %s." % (self._root, e))
913
def diff(self, old_path, new_path, old_kind, new_kind):
853
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
914
854
if (old_kind, new_kind) != ('file', 'file'):
915
855
return DiffPath.CANNOT_DIFF
916
856
(old_disk_path, new_disk_path) = self._prepare_files(
857
file_id, old_path, new_path)
918
858
self._execute(old_disk_path, new_disk_path)
920
def edit_file(self, old_path, new_path):
860
def edit_file(self, file_id):
921
861
"""Use this tool to edit a file.
923
863
A temporary copy will be edited, and the new contents will be
866
:param file_id: The id of the file to edit.
926
867
:return: The new contents of the file.
869
old_path = self.old_tree.id2path(file_id)
870
new_path = self.new_tree.id2path(file_id)
928
871
old_abs_path, new_abs_path = self._prepare_files(
929
old_path, new_path, allow_write_new=True, force_temp=True)
872
file_id, old_path, new_path,
873
allow_write_new=True,
930
875
command = self._get_command(old_abs_path, new_abs_path)
931
876
subprocess.call(command, cwd=self._root)
932
with open(new_abs_path, 'rb') as new_file:
877
new_file = open(new_abs_path, 'rb')
933
879
return new_file.read()
936
884
class DiffTree(object):
1058
1002
renamed = (parent[0], name[0]) != (parent[1], name[1])
1060
1004
properties_changed = []
1061
properties_changed.extend(
1062
get_executable_change(executable[0], executable[1]))
1005
properties_changed.extend(get_executable_change(executable[0], executable[1]))
1064
1007
if properties_changed:
1065
prop_str = b" (properties changed: %s)" % (
1066
b", ".join(properties_changed),)
1008
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1070
1012
if (old_present, new_present) == (True, False):
1071
self.to_file.write(b"=== removed %s '%s'\n" %
1072
(kind[0].encode('ascii'), oldpath_encoded))
1013
self.to_file.write("=== removed %s '%s'\n" %
1014
(kind[0], oldpath_encoded))
1073
1015
newpath = oldpath
1074
1016
elif (old_present, new_present) == (False, True):
1075
self.to_file.write(b"=== added %s '%s'\n" %
1076
(kind[1].encode('ascii'), newpath_encoded))
1017
self.to_file.write("=== added %s '%s'\n" %
1018
(kind[1], newpath_encoded))
1077
1019
oldpath = newpath
1079
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1080
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1021
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
1022
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1082
1024
# if it was produced by iter_changes, it must be
1083
1025
# modified *somehow*, either content or execute bit.
1084
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1085
newpath_encoded, prop_str))
1026
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
1027
newpath_encoded, prop_str))
1086
1028
if changed_content:
1087
self._diff(oldpath, newpath, kind[0], kind[1])
1029
self._diff(file_id, oldpath, newpath, kind[0], kind[1])
1088
1030
has_changes = 1
1090
1032
has_changes = 1
1091
1033
return has_changes
1093
def diff(self, old_path, new_path):
1035
def diff(self, file_id, old_path, new_path):
1094
1036
"""Perform a diff of a single file
1038
:param file_id: file-id of the file
1096
1039
:param old_path: The path of the file in the old tree
1097
1040
:param new_path: The path of the file in the new tree
1099
if old_path is None:
1043
old_kind = self.old_tree.kind(file_id)
1044
except (errors.NoSuchId, errors.NoSuchFile):
1100
1045
old_kind = None
1102
old_kind = self.old_tree.kind(old_path)
1103
if new_path is None:
1047
new_kind = self.new_tree.kind(file_id)
1048
except (errors.NoSuchId, errors.NoSuchFile):
1104
1049
new_kind = None
1106
new_kind = self.new_tree.kind(new_path)
1107
self._diff(old_path, new_path, old_kind, new_kind)
1109
def _diff(self, old_path, new_path, old_kind, new_kind):
1110
result = DiffPath._diff_many(
1111
self.differs, old_path, new_path, old_kind, new_kind)
1050
self._diff(file_id, old_path, new_path, old_kind, new_kind)
1053
def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
1054
result = DiffPath._diff_many(self.differs, file_id, old_path,
1055
new_path, old_kind, new_kind)
1112
1056
if result is DiffPath.CANNOT_DIFF:
1113
1057
error_path = new_path
1114
1058
if error_path is None: