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
23
from .lazy_import import lazy_import
25
from brzlib.lazy_import import lazy_import
24
26
lazy_import(globals(), """
38
from breezy.workingtree import WorkingTree
39
from breezy.i18n import gettext
43
from brzlib.workingtree import WorkingTree
44
from brzlib.i18n import gettext
45
from .registry import (
47
from brzlib.registry import (
48
from .trace import mutter, note, warning
49
from .tree import FileTimestampUnavailable
50
from brzlib.trace import mutter, note, warning
52
52
DEFAULT_CONTEXT_AMOUNT = 3
54
class AtTemplate(string.Template):
55
"""Templating class that uses @ instead of $."""
55
60
# TODO: Rather than building a changeset object, we should probably
56
61
# invoke callbacks on an object. That object can either accumulate a
79
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:
82
93
if allow_binary is False:
83
94
textfile.check_text_lines(oldlines)
84
95
textfile.check_text_lines(newlines)
86
97
if sequence_matcher is None:
87
98
sequence_matcher = patiencediff.PatienceSequenceMatcher
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)
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)
95
if len(ud) == 0: # Identical contents, nothing to do
105
if len(ud) == 0: # Identical contents, nothing to do
97
107
# work-around for difflib being too smart for its own good
98
108
# if /dev/null is "1,0", patch won't recognize it as /dev/null
100
ud[2] = ud[2].replace(b'-1,0', b'-0,0')
110
ud[2] = ud[2].replace('-1,0', '-0,0')
101
111
elif not newlines:
102
ud[2] = ud[2].replace(b'+1,0', b'+0,0')
112
ud[2] = ud[2].replace('+1,0', '+0,0')
105
115
to_file.write(line)
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]:
116
if not line.endswith('\n'):
117
to_file.write("\n\\ No newline at end of file\n")
180
121
def _spawn_external_diff(diffcmd, capture_errors=True):
313
252
out, err = pipe.communicate()
315
254
# Write out the new i18n diff response
316
to_file.write(out + b'\n')
255
to_file.write(out+'\n')
317
256
if pipe.returncode != 2:
318
257
raise errors.BzrError(
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,))
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,))
323
first_line = lang_c_out.split(b'\n', 1)[0]
262
first_line = lang_c_out.split('\n', 1)[0]
324
263
# Starting with diffutils 2.8.4 the word "binary" was dropped.
325
m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
264
m = re.match('^(binary )?files.*differ$', first_line, re.I)
327
266
raise errors.BzrError('external diff failed with exit code 2;'
328
267
' command: %r' % (diffcmd,))
516
458
context = DEFAULT_CONTEXT_AMOUNT
517
459
if format_cls is None:
518
460
format_cls = DiffTree
519
with contextlib.ExitStack() as exit_stack:
520
exit_stack.enter_context(old_tree.lock_read())
521
463
if extra_trees is not None:
522
464
for tree in extra_trees:
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):
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):
534
484
"""Returns a timestamp suitable for use in a patch header."""
536
mtime = tree.get_file_mtime(path)
537
except FileTimestampUnavailable:
486
mtime = tree.get_file_mtime(file_id, path)
487
except errors.FileTimestampUnavailable:
539
489
return timestamp.format_patch_date(mtime)
542
492
def get_executable_change(old_is_x, new_is_x):
543
descr = {True: b"+x", False: b"-x", None: b"??"}
493
descr = { True:"+x", False:"-x", None:"??" }
544
494
if old_is_x != new_is_x:
545
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],)]
616
567
if None in (old_kind, new_kind):
617
568
return DiffPath.CANNOT_DIFF
618
result = DiffPath._diff_many(
619
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)
620
571
if result is DiffPath.CANNOT_DIFF:
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
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)
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%s and %s%s differ\n" %
765
(self.old_label, from_path, self.new_label, to_path)).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
889
833
osutils.make_readonly(full_path)
892
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,
893
837
allow_write_new=False):
894
old_disk_path = self._write_file(
895
old_path, self.old_tree, 'old', force_temp)
896
new_disk_path = self._write_file(
897
new_path, self.new_tree, 'new', force_temp,
898
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)
899
843
return old_disk_path, new_disk_path
901
845
def finish(self):
903
847
osutils.rmtree(self._root)
905
849
if e.errno != errno.ENOENT:
906
850
mutter("The temporary directory \"%s\" was not "
907
"cleanly removed: %s." % (self._root, e))
851
"cleanly removed: %s." % (self._root, e))
909
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):
910
854
if (old_kind, new_kind) != ('file', 'file'):
911
855
return DiffPath.CANNOT_DIFF
912
856
(old_disk_path, new_disk_path) = self._prepare_files(
857
file_id, old_path, new_path)
914
858
self._execute(old_disk_path, new_disk_path)
916
def edit_file(self, old_path, new_path):
860
def edit_file(self, file_id):
917
861
"""Use this tool to edit a file.
919
863
A temporary copy will be edited, and the new contents will be
866
:param file_id: The id of the file to edit.
922
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)
924
871
old_abs_path, new_abs_path = self._prepare_files(
925
old_path, new_path, allow_write_new=True, force_temp=True)
872
file_id, old_path, new_path,
873
allow_write_new=True,
926
875
command = self._get_command(old_abs_path, new_abs_path)
927
876
subprocess.call(command, cwd=self._root)
928
with open(new_abs_path, 'rb') as new_file:
877
new_file = open(new_abs_path, 'rb')
929
879
return new_file.read()
932
884
class DiffTree(object):
1026
975
# TODO: Generation of pseudo-diffs for added/deleted files could
1027
976
# be usefully made into a much faster special case.
1028
977
iterator = self.new_tree.iter_changes(self.old_tree,
1029
specific_files=specific_files,
1030
extra_trees=extra_trees,
1031
require_versioned=True)
978
specific_files=specific_files,
979
extra_trees=extra_trees,
980
require_versioned=True)
1034
982
def changes_key(change):
1035
old_path, new_path = change.path
983
old_path, new_path = change[1]
1037
985
if path is None:
1041
988
def get_encoded_path(path):
1042
989
if path is not None:
1043
990
return path.encode(self.path_encoding, "replace")
1044
for change in sorted(iterator, key=changes_key):
991
for (file_id, paths, changed_content, versioned, parent, name, kind,
992
executable) in sorted(iterator, key=changes_key):
1045
993
# The root does not get diffed, and items with no known kind (that
1046
994
# is, missing) in both trees are skipped as well.
1047
if change.parent_id == (None, None) or change.kind == (None, None):
1049
if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
1051
'Ignoring "%s" as symlinks are not '
1052
'supported on this filesystem.' % (change.path[0],))
1054
oldpath, newpath = change.path
1055
oldpath_encoded = get_encoded_path(change.path[0])
1056
newpath_encoded = get_encoded_path(change.path[1])
1057
old_present = (change.kind[0] is not None and change.versioned[0])
1058
new_present = (change.kind[1] is not None and change.versioned[1])
1059
executable = change.executable
1061
renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
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])
1063
1004
properties_changed = []
1064
properties_changed.extend(
1065
get_executable_change(executable[0], executable[1]))
1005
properties_changed.extend(get_executable_change(executable[0], executable[1]))
1067
1007
if properties_changed:
1068
prop_str = b" (properties changed: %s)" % (
1069
b", ".join(properties_changed),)
1008
prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1073
1012
if (old_present, new_present) == (True, False):
1074
self.to_file.write(b"=== removed %s '%s'\n" %
1075
(kind[0].encode('ascii'), oldpath_encoded))
1013
self.to_file.write("=== removed %s '%s'\n" %
1014
(kind[0], oldpath_encoded))
1076
1015
newpath = oldpath
1077
1016
elif (old_present, new_present) == (False, True):
1078
self.to_file.write(b"=== added %s '%s'\n" %
1079
(kind[1].encode('ascii'), newpath_encoded))
1017
self.to_file.write("=== added %s '%s'\n" %
1018
(kind[1], newpath_encoded))
1080
1019
oldpath = newpath
1082
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1083
(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))
1085
1024
# if it was produced by iter_changes, it must be
1086
1025
# modified *somehow*, either content or execute bit.
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])
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])
1091
1030
has_changes = 1
1093
1032
has_changes = 1
1094
1033
return has_changes
1096
def diff(self, old_path, new_path):
1035
def diff(self, file_id, old_path, new_path):
1097
1036
"""Perform a diff of a single file
1038
:param file_id: file-id of the file
1099
1039
:param old_path: The path of the file in the old tree
1100
1040
:param new_path: The path of the file in the new tree
1102
if old_path is None:
1043
old_kind = self.old_tree.kind(file_id)
1044
except (errors.NoSuchId, errors.NoSuchFile):
1103
1045
old_kind = None
1105
old_kind = self.old_tree.kind(old_path)
1106
if new_path is None:
1047
new_kind = self.new_tree.kind(file_id)
1048
except (errors.NoSuchId, errors.NoSuchFile):
1107
1049
new_kind = None
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)
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)
1115
1056
if result is DiffPath.CANNOT_DIFF:
1116
1057
error_path = new_path
1117
1058
if error_path is None: