15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
26
binary_files_re = b'Binary files (.*) and (.*) differ\n'
29
class PatchSyntax(BzrError):
30
"""Base class for patch syntax errors."""
33
class BinaryFiles(BzrError):
35
_fmt = 'Binary files section encountered.'
37
def __init__(self, orig_name, mod_name):
38
self.orig_name = orig_name
39
self.mod_name = mod_name
19
class PatchSyntax(Exception):
20
def __init__(self, msg):
21
Exception.__init__(self, msg)
42
24
class MalformedPatchHeader(PatchSyntax):
44
_fmt = "Malformed patch header. %(desc)s\n%(line)r"
46
def __init__(self, desc, line):
25
def __init__(self, desc, line):
28
msg = "Malformed patch header. %s\n%r" % (self.desc, self.line)
29
PatchSyntax.__init__(self, msg)
32
class MalformedHunkHeader(PatchSyntax):
33
def __init__(self, desc, line):
36
msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line)
37
PatchSyntax.__init__(self, msg)
51
40
class MalformedLine(PatchSyntax):
53
_fmt = "Malformed line. %(desc)s\n%(line)r"
55
41
def __init__(self, desc, line):
60
class PatchConflict(BzrError):
62
_fmt = ('Text contents mismatch at line %(line_no)d. Original has '
63
'"%(orig_line)s", but patch says it should be "%(patch_line)s"')
44
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
45
PatchSyntax.__init__(self, msg)
48
class PatchConflict(Exception):
65
49
def __init__(self, line_no, orig_line, patch_line):
66
self.line_no = line_no
67
self.orig_line = orig_line.rstrip('\n')
68
self.patch_line = patch_line.rstrip('\n')
71
class MalformedHunkHeader(PatchSyntax):
73
_fmt = "Malformed hunk header. %(desc)s\n%(line)r"
75
def __init__(self, desc, line):
50
orig = orig_line.rstrip('\n')
51
patch = str(patch_line).rstrip('\n')
52
msg = 'Text contents mismatch at line %d. Original has "%s",'\
53
' but patch says it should be "%s"' % (line_no, orig, patch)
54
Exception.__init__(self, msg)
80
57
def get_patch_names(iter_lines):
81
line = next(iter_lines)
83
match = re.match(binary_files_re, line)
85
raise BinaryFiles(match.group(1), match.group(2))
86
if not line.startswith(b"--- "):
59
line = iter_lines.next()
60
if not line.startswith("--- "):
87
61
raise MalformedPatchHeader("No orig name", line)
89
orig_name = line[4:].rstrip(b"\n")
63
orig_name = line[4:].rstrip("\n")
90
64
except StopIteration:
91
65
raise MalformedPatchHeader("No orig line", "")
93
line = next(iter_lines)
94
if not line.startswith(b"+++ "):
67
line = iter_lines.next()
68
if not line.startswith("+++ "):
95
69
raise PatchSyntax("No mod name")
97
mod_name = line[4:].rstrip(b"\n")
71
mod_name = line[4:].rstrip("\n")
98
72
except StopIteration:
99
73
raise MalformedPatchHeader("No mod line", "")
100
74
return (orig_name, mod_name)
122
96
def hunk_from_header(line):
124
matches = re.match(br'\@\@ ([^@]*) \@\@( (.*))?\n', line)
98
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
125
99
if matches is None:
126
100
raise MalformedHunkHeader("Does not match format.", line)
128
(orig, mod) = matches.group(1).split(b" ")
129
except (ValueError, IndexError) as e:
102
(orig, mod) = matches.group(1).split(" ")
103
except (ValueError, IndexError), e:
130
104
raise MalformedHunkHeader(str(e), line)
131
if not orig.startswith(b'-') or not mod.startswith(b'+'):
105
if not orig.startswith('-') or not mod.startswith('+'):
132
106
raise MalformedHunkHeader("Positions don't start with + or -.", line)
134
108
(orig_pos, orig_range) = parse_range(orig[1:])
135
109
(mod_pos, mod_range) = parse_range(mod[1:])
136
except (ValueError, IndexError) as e:
110
except (ValueError, IndexError), e:
137
111
raise MalformedHunkHeader(str(e), line)
138
112
if mod_range < 0 or orig_range < 0:
139
113
raise MalformedHunkHeader("Hunk range is negative", line)
141
115
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
144
class HunkLine(object):
146
119
def __init__(self, contents):
147
120
self.contents = contents
149
122
def get_str(self, leadchar):
150
if self.contents == b"\n" and leadchar == b" " and False:
152
if not self.contents.endswith(b'\n'):
153
terminator = b'\n' + NO_NL
123
if self.contents == "\n" and leadchar == " " and False:
125
if not self.contents.endswith('\n'):
126
terminator = '\n' + NO_NL
156
129
return leadchar + self.contents + terminator
159
raise NotImplementedError
162
132
class ContextLine(HunkLine):
164
133
def __init__(self, contents):
165
134
HunkLine.__init__(self, contents)
168
return self.get_str(b" ")
137
return self.get_str(" ")
171
140
class InsertLine(HunkLine):
172
141
def __init__(self, contents):
173
142
HunkLine.__init__(self, contents)
176
return self.get_str(b"+")
145
return self.get_str("+")
179
148
class RemoveLine(HunkLine):
180
149
def __init__(self, contents):
181
150
HunkLine.__init__(self, contents)
184
return self.get_str(b"-")
187
NO_NL = b'\\ No newline at end of file\n'
188
__pychecker__ = "no-returnvalues"
153
return self.get_str("-")
155
NO_NL = '\\ No newline at end of file\n'
156
__pychecker__="no-returnvalues"
191
158
def parse_line(line):
192
if line.startswith(b"\n"):
159
if line.startswith("\n"):
193
160
return ContextLine(line)
194
elif line.startswith(b" "):
161
elif line.startswith(" "):
195
162
return ContextLine(line[1:])
196
elif line.startswith(b"+"):
163
elif line.startswith("+"):
197
164
return InsertLine(line[1:])
198
elif line.startswith(b"-"):
165
elif line.startswith("-"):
199
166
return RemoveLine(line[1:])
201
168
raise MalformedLine("Unknown line type", line)
209
173
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
210
174
self.orig_pos = orig_pos
211
175
self.orig_range = orig_range
235
199
:return: a string in the format 1,4 except when range == pos == 1
240
return b"%i,%i" % (pos, range)
204
return "%i,%i" % (pos, range)
243
207
lines = [self.get_header()]
244
208
for line in self.lines:
245
lines.append(line.as_bytes())
246
return b"".join(lines)
209
lines.append(str(line))
210
return "".join(lines)
250
212
def shift_to_mod(self, pos):
251
if pos < self.orig_pos - 1:
213
if pos < self.orig_pos-1:
253
elif pos > self.orig_pos + self.orig_range:
215
elif pos > self.orig_pos+self.orig_range:
254
216
return self.mod_range - self.orig_range
256
218
return self.shift_to_mod_lines(pos)
258
220
def shift_to_mod_lines(self, pos):
259
position = self.orig_pos - 1
221
position = self.orig_pos-1
261
223
for line in self.lines:
262
224
if isinstance(line, InsertLine):
276
def iter_hunks(iter_lines, allow_dirty=False):
278
:arg iter_lines: iterable of lines to parse for hunks
279
:kwarg allow_dirty: If True, when we encounter something that is not
280
a hunk header when we're looking for one, assume the rest of the lines
281
are not part of the patch (comments or other junk). Default False
238
def iter_hunks(iter_lines):
284
240
for line in iter_lines:
286
242
if hunk is not None:
290
246
if hunk is not None:
293
hunk = hunk_from_header(line)
294
except MalformedHunkHeader:
296
# If the line isn't a hunk header, then we've reached the end
297
# of this patch and there's "junk" at the end. Ignore the
298
# rest of this patch.
248
hunk = hunk_from_header(line)
303
251
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
304
hunk_line = parse_line(next(iter_lines))
252
hunk_line = parse_line(iter_lines.next())
305
253
hunk.lines.append(hunk_line)
306
254
if isinstance(hunk_line, (RemoveLine, ContextLine)):
314
class BinaryPatch(object):
316
263
def __init__(self, oldname, newname):
317
264
self.oldname = oldname
318
265
self.newname = newname
321
return b'Binary files %s and %s differ\n' % (self.oldname, self.newname)
324
class Patch(BinaryPatch):
326
def __init__(self, oldname, newname):
327
BinaryPatch.__init__(self, oldname, newname)
331
269
ret = self.get_header()
332
ret += b"".join([h.as_bytes() for h in self.hunks])
270
ret += "".join([str(h) for h in self.hunks])
335
273
def get_header(self):
336
return b"--- %s\n+++ %s\n" % (self.oldname, self.newname)
274
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
338
276
def stats_values(self):
339
277
"""Calculate the number of inserts and removes."""
380
def parse_patch(iter_lines, allow_dirty=False):
382
:arg iter_lines: iterable of lines to parse
383
:kwarg allow_dirty: If True, allow the patch to have trailing junk.
318
def parse_patch(iter_lines):
386
319
iter_lines = iter_lines_handle_nl(iter_lines)
388
(orig_name, mod_name) = get_patch_names(iter_lines)
389
except BinaryFiles as e:
390
return BinaryPatch(e.orig_name, e.mod_name)
392
patch = Patch(orig_name, mod_name)
393
for hunk in iter_hunks(iter_lines, allow_dirty):
394
patch.hunks.append(hunk)
398
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
400
:arg iter_lines: iterable of lines to parse for patches
401
:kwarg allow_dirty: If True, allow comments and other non-patch text
402
before the first patch. Note that the algorithm here can only find
403
such text before any patches have been found. Comments after the
404
first patch are stripped away in iter_hunks() if it is also passed
405
allow_dirty=True. Default False.
407
# FIXME: Docstring is not quite true. We allow certain comments no
408
# matter what, If they startwith '===', '***', or '#' Someone should
409
# reexamine this logic and decide if we should include those in
410
# allow_dirty or restrict those to only being before the patch is found
411
# (as allow_dirty does).
412
regex = re.compile(binary_files_re)
320
(orig_name, mod_name) = get_patch_names(iter_lines)
321
patch = Patch(orig_name, mod_name)
322
for hunk in iter_hunks(iter_lines):
323
patch.hunks.append(hunk)
327
def iter_file_patch(iter_lines):
418
330
for line in iter_lines:
419
if line.startswith(b'=== '):
420
if len(saved_lines) > 0:
421
if keep_dirty and len(dirty_head) > 0:
422
yield {'saved_lines': saved_lines,
423
'dirty_head': dirty_head}
428
dirty_head.append(line)
430
if line.startswith(b'*** '):
432
if line.startswith(b'#'):
331
if line.startswith('=== ') or line.startswith('*** '):
333
if line.startswith('#'):
434
335
elif orig_range > 0:
435
if line.startswith(b'-') or line.startswith(b' '):
336
if line.startswith('-') or line.startswith(' '):
437
elif line.startswith(b'--- ') or regex.match(line):
438
if allow_dirty and beginning:
439
# Patches can have "junk" at the beginning
440
# Stripping junk from the end of patches is handled when we
443
elif len(saved_lines) > 0:
444
if keep_dirty and len(dirty_head) > 0:
445
yield {'saved_lines': saved_lines,
446
'dirty_head': dirty_head}
338
elif line.startswith('--- '):
339
if len(saved_lines) > 0:
451
elif line.startswith(b'@@'):
342
elif line.startswith('@@'):
452
343
hunk = hunk_from_header(line)
453
344
orig_range = hunk.orig_range
454
345
saved_lines.append(line)
455
346
if len(saved_lines) > 0:
456
if keep_dirty and len(dirty_head) > 0:
457
yield {'saved_lines': saved_lines,
458
'dirty_head': dirty_head}
463
350
def iter_lines_handle_nl(iter_lines):
484
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
486
:arg iter_lines: iterable of lines to parse for patches
487
:kwarg allow_dirty: If True, allow text that's not part of the patch at
488
selected places. This includes comments before and after a patch
489
for instance. Default False.
490
:kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
493
for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
494
if 'dirty_head' in patch_lines:
495
yield ({'patch': parse_patch(patch_lines['saved_lines'], allow_dirty),
496
'dirty_head': patch_lines['dirty_head']})
498
yield parse_patch(patch_lines, allow_dirty)
371
def parse_patches(iter_lines):
372
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
501
375
def difference_index(atext, btext):
539
413
orig_lines = iter(orig_lines)
540
414
for hunk in hunks:
541
415
while line_no < hunk.orig_pos:
542
orig_line = next(orig_lines)
416
orig_line = orig_lines.next()
545
419
for hunk_line in hunk.lines:
546
seen_patch.append(hunk_line.contents)
420
seen_patch.append(str(hunk_line))
547
421
if isinstance(hunk_line, InsertLine):
548
422
yield hunk_line.contents
549
423
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
550
orig_line = next(orig_lines)
424
orig_line = orig_lines.next()
551
425
if orig_line != hunk_line.contents:
552
raise PatchConflict(line_no, orig_line,
553
b''.join(seen_patch))
426
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
554
427
if isinstance(hunk_line, ContextLine):
560
433
if orig_lines is not None:
561
434
for line in orig_lines:
565
def apply_patches(tt, patches, prefix=1):
566
"""Apply patches to a TreeTransform.
568
:param tt: TreeTransform instance
569
:param patches: List of patches
570
:param prefix: Number leading path segments to strip
573
return '/'.join(p.split('/')[1:])
575
from breezy.bzr.generate_ids import gen_file_id
576
# TODO(jelmer): Extract and set mode
577
for patch in patches:
578
if patch.oldname == b'/dev/null':
582
oldname = strip_prefix(patch.oldname.decode())
583
trans_id = tt.trans_id_tree_path(oldname)
584
orig_contents = tt._tree.get_file_text(oldname)
585
tt.delete_contents(trans_id)
587
if patch.newname != b'/dev/null':
588
newname = strip_prefix(patch.newname.decode())
589
new_contents = iter_patched_from_hunks(
590
orig_contents.splitlines(True), patch.hunks)
592
parts = os.path.split(newname)
594
for part in parts[1:-1]:
595
trans_id = tt.new_directory(part, trans_id)
597
parts[-1], trans_id, new_contents,
598
file_id=gen_file_id(newname))
600
tt.create_file(new_contents, trans_id)
603
class AppliedPatches(object):
604
"""Context that provides access to a tree with patches applied.
607
def __init__(self, tree, patches, prefix=1):
609
self.patches = patches
613
self._tt = self.tree.preview_transform()
614
apply_patches(self._tt, self.patches, prefix=self.prefix)
615
return self._tt.get_preview_tree()
617
def __exit__(self, exc_type, exc_value, exc_tb):