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
from bzrlib.lazy_import import lazy_import
23
from .lazy_import import lazy_import
26
24
lazy_import(globals(), """
43
from bzrlib.workingtree import WorkingTree
44
from bzrlib.i18n import gettext
38
from breezy.workingtree import WorkingTree
39
from breezy.i18n import gettext
47
from bzrlib.registry import (
45
from .registry import (
50
from bzrlib.trace import mutter, note, warning
48
from .trace import mutter, note, warning
49
from .tree import FileTimestampUnavailable
52
52
DEFAULT_CONTEXT_AMOUNT = 3
54
class AtTemplate(string.Template):
55
"""Templating class that uses @ instead of $."""
60
55
# TODO: Rather than building a changeset object, we should probably
61
56
# invoke callbacks on an object. That object can either accumulate a
84
79
# 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:
93
82
if allow_binary is False:
94
83
textfile.check_text_lines(oldlines)
95
84
textfile.check_text_lines(newlines)
97
86
if sequence_matcher is None:
98
87
sequence_matcher = patiencediff.PatienceSequenceMatcher
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)
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)
105
if len(ud) == 0: # Identical contents, nothing to do
95
if len(ud) == 0: # Identical contents, nothing to do
107
97
# work-around for difflib being too smart for its own good
108
98
# if /dev/null is "1,0", patch won't recognize it as /dev/null
110
ud[2] = ud[2].replace('-1,0', '-0,0')
100
ud[2] = ud[2].replace(b'-1,0', b'-0,0')
111
101
elif not newlines:
112
ud[2] = ud[2].replace('+1,0', '+0,0')
102
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
115
105
to_file.write(line)
116
if not line.endswith('\n'):
117
to_file.write("\n\\ No newline at end of file\n")
106
if not line.endswith(b'\n'):
107
to_file.write(b"\n\\ No newline at end of file\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]:
121
180
def _spawn_external_diff(diffcmd, capture_errors=True):
252
313
out, err = pipe.communicate()
254
315
# Write out the new i18n diff response
255
to_file.write(out+'\n')
316
to_file.write(out + b'\n')
256
317
if pipe.returncode != 2:
257
318
raise errors.BzrError(
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,))
319
'external diff failed with exit code 2'
320
' when run with LANG=C and LC_ALL=C,'
321
' but not when run natively: %r' % (diffcmd,))
262
first_line = lang_c_out.split('\n', 1)[0]
323
first_line = lang_c_out.split(b'\n', 1)[0]
263
324
# Starting with diffutils 2.8.4 the word "binary" was dropped.
264
m = re.match('^(binary )?files.*differ$', first_line, re.I)
325
m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
266
327
raise errors.BzrError('external diff failed with exit code 2;'
267
328
' command: %r' % (diffcmd,))
458
516
context = DEFAULT_CONTEXT_AMOUNT
459
517
if format_cls is None:
460
518
format_cls = DiffTree
519
with contextlib.ExitStack() as exit_stack:
520
exit_stack.enter_context(old_tree.lock_read())
463
521
if extra_trees is not None:
464
522
for tree in extra_trees:
468
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
470
external_diff_options,
471
old_label, new_label, using,
472
context_lines=context)
473
return differ.show_diff(specific_files, extra_trees)
476
if extra_trees is not None:
477
for tree in extra_trees:
483
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):
484
534
"""Returns a timestamp suitable for use in a patch header."""
486
mtime = tree.get_file_mtime(file_id, path)
487
except errors.FileTimestampUnavailable:
536
mtime = tree.get_file_mtime(path)
537
except FileTimestampUnavailable:
489
539
return timestamp.format_patch_date(mtime)
492
542
def get_executable_change(old_is_x, new_is_x):
493
descr = { True:"+x", False:"-x", None:"??" }
543
descr = {True: b"+x", False: b"-x", None: b"??"}
494
544
if old_is_x != new_is_x:
495
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
545
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
567
616
if None in (old_kind, new_kind):
568
617
return DiffPath.CANNOT_DIFF
569
result = DiffPath._diff_many(self.differs, file_id, old_path,
570
new_path, old_kind, None)
618
result = DiffPath._diff_many(
619
self.differs, old_path, new_path, old_kind, None)
571
620
if result is DiffPath.CANNOT_DIFF:
573
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
577
641
class DiffDirectory(DiffPath):
579
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):
580
644
"""Perform comparison between two directories. (dummy)
655
720
if 'file' not in (old_kind, new_kind):
656
721
return self.CANNOT_DIFF
657
from_file_id = to_file_id = file_id
658
722
if old_kind == 'file':
659
old_date = _patch_header_date(self.old_tree, file_id, old_path)
723
old_date = _patch_header_date(self.old_tree, old_path)
660
724
elif old_kind is None:
661
725
old_date = self.EPOCH_DATE
664
727
return self.CANNOT_DIFF
665
728
if new_kind == 'file':
666
new_date = _patch_header_date(self.new_tree, file_id, new_path)
729
new_date = _patch_header_date(self.new_tree, new_path)
667
730
elif new_kind is None:
668
731
new_date = self.EPOCH_DATE
671
733
return self.CANNOT_DIFF
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,
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)
677
def diff_text(self, from_file_id, to_file_id, from_label, to_label,
678
from_path=None, to_path=None):
740
def diff_text(self, from_path, to_path, from_label, to_label):
679
741
"""Diff the content of given files in two trees
681
:param from_file_id: The id of the file in the from tree. If None,
743
:param from_path: The path in the from tree. If None,
682
744
the file is not present in the from tree.
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,
745
:param to_path: The path in the to tree. This may refer
746
to a different file from from_path. If None,
685
747
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.
689
def _get_text(tree, file_id, path):
690
if file_id is not None:
691
return tree.get_file_lines(file_id, path)
749
def _get_text(tree, path):
753
return tree.get_file_lines(path)
754
except errors.NoSuchFile:
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)
757
from_text = _get_text(self.old_tree, from_path)
758
to_text = _get_text(self.new_tree, to_path)
697
759
self.text_differ(from_label, from_text, to_label, to_text,
698
760
self.to_file, path_encoding=self.path_encoding,
699
761
context_lines=self.context_lines)
700
762
except errors.BinaryFile:
701
763
self.to_file.write(
702
("Binary files %s and %s differ\n" %
703
(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'))
704
768
return self.CHANGED
833
891
osutils.make_readonly(full_path)
836
def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
894
def _prepare_files(self, old_path, new_path, force_temp=False,
837
895
allow_write_new=False):
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)
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)
843
901
return old_disk_path, new_disk_path
845
903
def finish(self):
847
905
osutils.rmtree(self._root)
849
907
if e.errno != errno.ENOENT:
850
908
mutter("The temporary directory \"%s\" was not "
851
"cleanly removed: %s." % (self._root, e))
909
"cleanly removed: %s." % (self._root, e))
853
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):
854
912
if (old_kind, new_kind) != ('file', 'file'):
855
913
return DiffPath.CANNOT_DIFF
856
914
(old_disk_path, new_disk_path) = self._prepare_files(
857
file_id, old_path, new_path)
858
916
self._execute(old_disk_path, new_disk_path)
860
def edit_file(self, file_id):
918
def edit_file(self, old_path, new_path):
861
919
"""Use this tool to edit a file.
863
921
A temporary copy will be edited, and the new contents will be
866
:param file_id: The id of the file to edit.
867
924
: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)
871
926
old_abs_path, new_abs_path = self._prepare_files(
872
file_id, old_path, new_path,
873
allow_write_new=True,
927
old_path, new_path, allow_write_new=True, force_temp=True)
875
928
command = self._get_command(old_abs_path, new_abs_path)
876
929
subprocess.call(command, cwd=self._root)
877
new_file = open(new_abs_path, 'rb')
930
with open(new_abs_path, 'rb') as new_file:
879
931
return new_file.read()
884
934
class DiffTree(object):
975
1028
# TODO: Generation of pseudo-diffs for added/deleted files could
976
1029
# be usefully made into a much faster special case.
977
1030
iterator = self.new_tree.iter_changes(self.old_tree,
978
specific_files=specific_files,
979
extra_trees=extra_trees,
980
require_versioned=True)
1031
specific_files=specific_files,
1032
extra_trees=extra_trees,
1033
require_versioned=True)
982
1036
def changes_key(change):
983
old_path, new_path = change[1]
1037
old_path, new_path = change.path
985
1039
if path is 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
properties_changed.extend(get_executable_change(executable[0], executable[1]))
1066
properties_changed.extend(
1067
get_executable_change(executable[0], executable[1]))
1007
1069
if properties_changed:
1008
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1070
prop_str = b" (properties changed: %s)" % (
1071
b", ".join(properties_changed),)
1012
1075
if (old_present, new_present) == (True, False):
1013
self.to_file.write("=== removed %s '%s'\n" %
1014
(kind[0], oldpath_encoded))
1076
self.to_file.write(b"=== removed %s '%s'\n" %
1077
(kind[0].encode('ascii'), oldpath_encoded))
1016
1078
elif (old_present, new_present) == (False, True):
1017
self.to_file.write("=== added %s '%s'\n" %
1018
(kind[1], newpath_encoded))
1079
self.to_file.write(b"=== added %s '%s'\n" %
1080
(kind[1].encode('ascii'), newpath_encoded))
1021
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
1022
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
1082
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1083
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1024
1085
# if it was produced by iter_changes, it must be
1025
1086
# modified *somehow*, either content or execute bit.
1026
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
1027
newpath_encoded, prop_str))
1029
self._diff(file_id, oldpath, newpath, kind[0], kind[1])
1087
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1088
newpath_encoded, prop_str))
1089
if change.changed_content:
1090
self._diff(oldpath, newpath, kind[0], kind[1])
1030
1091
has_changes = 1
1032
1093
has_changes = 1
1033
1094
return has_changes
1035
def diff(self, file_id, old_path, new_path):
1096
def diff(self, old_path, new_path):
1036
1097
"""Perform a diff of a single file
1038
:param file_id: file-id of the file
1039
1099
:param old_path: The path of the file in the old tree
1040
1100
:param new_path: The path of the file in the new tree
1043
old_kind = self.old_tree.kind(file_id)
1044
except (errors.NoSuchId, errors.NoSuchFile):
1102
if old_path is None:
1045
1103
old_kind = None
1047
new_kind = self.new_tree.kind(file_id)
1048
except (errors.NoSuchId, errors.NoSuchFile):
1105
old_kind = self.old_tree.kind(old_path)
1106
if new_path is None:
1049
1107
new_kind = None
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)
1109
new_kind = self.new_tree.kind(new_path)
1110
self._diff(old_path, 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)
1056
1115
if result is DiffPath.CANNOT_DIFF:
1057
1116
error_path = new_path
1058
1117
if error_path is None: