14
14
# You should have received a copy of the GNU General Public License
15
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
from __future__ import absolute_import
27
binary_files_re = 'Binary files (.*) and (.*) differ\n'
30
class PatchSyntax(BzrError):
31
"""Base class for patch syntax errors."""
34
class BinaryFiles(BzrError):
36
_fmt = 'Binary files section encountered.'
38
def __init__(self, orig_name, mod_name):
39
self.orig_name = orig_name
40
self.mod_name = mod_name
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
class PatchSyntax(Exception):
20
def __init__(self, msg):
21
Exception.__init__(self, msg)
43
24
class MalformedPatchHeader(PatchSyntax):
45
_fmt = "Malformed patch header. %(desc)s\n%(line)r"
47
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)
52
40
class MalformedLine(PatchSyntax):
54
_fmt = "Malformed line. %(desc)s\n%(line)r"
56
41
def __init__(self, desc, line):
61
class PatchConflict(BzrError):
63
_fmt = ('Text contents mismatch at line %(line_no)d. Original has '
64
'"%(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):
66
49
def __init__(self, line_no, orig_line, patch_line):
67
self.line_no = line_no
68
self.orig_line = orig_line.rstrip('\n')
69
self.patch_line = patch_line.rstrip('\n')
72
class MalformedHunkHeader(PatchSyntax):
74
_fmt = "Malformed hunk header. %(desc)s\n%(line)r"
76
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)
81
57
def get_patch_names(iter_lines):
82
line = next(iter_lines)
84
match = re.match(binary_files_re, line)
86
raise BinaryFiles(match.group(1), match.group(2))
59
line = iter_lines.next()
87
60
if not line.startswith("--- "):
88
61
raise MalformedPatchHeader("No orig name", line)
119
92
range = int(range)
120
93
return (pos, range)
123
96
def hunk_from_header(line):
125
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
127
raise MalformedHunkHeader("Does not match format.", line)
97
if not line.startswith("@@") or not line.endswith("@@\n") \
99
raise MalformedHunkHeader("Does not start and end with @@.", line)
129
(orig, mod) = matches.group(1).split(" ")
130
except (ValueError, IndexError) as e:
101
(orig, mod) = line[3:-4].split(" ")
131
103
raise MalformedHunkHeader(str(e), line)
132
104
if not orig.startswith('-') or not mod.startswith('+'):
133
105
raise MalformedHunkHeader("Positions don't start with + or -.", line)
135
107
(orig_pos, orig_range) = parse_range(orig[1:])
136
108
(mod_pos, mod_range) = parse_range(mod[1:])
137
except (ValueError, IndexError) as e:
138
110
raise MalformedHunkHeader(str(e), line)
139
111
if mod_range < 0 or orig_range < 0:
140
112
raise MalformedHunkHeader("Hunk range is negative", line)
141
tail = matches.group(3)
142
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
113
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
191
162
return InsertLine(line[1:])
192
163
elif line.startswith("-"):
193
164
return RemoveLine(line[1:])
195
168
raise MalformedLine("Unknown line type", line)
200
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
173
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
201
174
self.orig_pos = orig_pos
202
175
self.orig_range = orig_range
203
176
self.mod_pos = mod_pos
204
177
self.mod_range = mod_range
208
180
def get_header(self):
209
if self.tail is None:
212
tail_str = ' ' + self.tail
213
return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
215
self.range_str(self.mod_pos,
181
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
183
self.range_str(self.mod_pos,
219
186
def range_str(self, pos, range):
220
187
"""Return a file range, special-casing for 1-line files.
279
241
if hunk is not None:
282
hunk = hunk_from_header(line)
283
except MalformedHunkHeader:
285
# If the line isn't a hunk header, then we've reached the end
286
# of this patch and there's "junk" at the end. Ignore the
287
# rest of this patch.
243
hunk = hunk_from_header(line)
292
246
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
293
hunk_line = parse_line(next(iter_lines))
247
hunk_line = parse_line(iter_lines.next())
294
248
hunk.lines.append(hunk_line)
295
249
if isinstance(hunk_line, (RemoveLine, ContextLine)):
303
class BinaryPatch(object):
304
258
def __init__(self, oldname, newname):
305
259
self.oldname = oldname
306
260
self.newname = newname
309
return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
312
class Patch(BinaryPatch):
314
def __init__(self, oldname, newname):
315
BinaryPatch.__init__(self, oldname, newname)
318
263
def __str__(self):
319
ret = self.get_header()
264
ret = self.get_header()
320
265
ret += "".join([str(h) for h in self.hunks])
323
268
def get_header(self):
324
269
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
326
def stats_values(self):
327
"""Calculate the number of inserts and removes."""
272
"""Return a string of patch statistics"""
330
275
for hunk in self.hunks:
364
305
if isinstance(line, ContextLine):
367
def parse_patch(iter_lines, allow_dirty=False):
369
:arg iter_lines: iterable of lines to parse
370
:kwarg allow_dirty: If True, allow the patch to have trailing junk.
373
iter_lines = iter_lines_handle_nl(iter_lines)
375
(orig_name, mod_name) = get_patch_names(iter_lines)
376
except BinaryFiles as e:
377
return BinaryPatch(e.orig_name, e.mod_name)
379
patch = Patch(orig_name, mod_name)
380
for hunk in iter_hunks(iter_lines, allow_dirty):
381
patch.hunks.append(hunk)
385
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
387
:arg iter_lines: iterable of lines to parse for patches
388
:kwarg allow_dirty: If True, allow comments and other non-patch text
389
before the first patch. Note that the algorithm here can only find
390
such text before any patches have been found. Comments after the
391
first patch are stripped away in iter_hunks() if it is also passed
392
allow_dirty=True. Default False.
394
### FIXME: Docstring is not quite true. We allow certain comments no
395
# matter what, If they startwith '===', '***', or '#' Someone should
396
# reexamine this logic and decide if we should include those in
397
# allow_dirty or restrict those to only being before the patch is found
398
# (as allow_dirty does).
399
regex = re.compile(binary_files_re)
309
def parse_patch(iter_lines):
310
(orig_name, mod_name) = get_patch_names(iter_lines)
311
patch = Patch(orig_name, mod_name)
312
for hunk in iter_hunks(iter_lines):
313
patch.hunks.append(hunk)
317
def iter_file_patch(iter_lines):
405
320
for line in iter_lines:
406
if line.startswith('=== '):
407
if len(saved_lines) > 0:
408
if keep_dirty and len(dirty_head) > 0:
409
yield {'saved_lines': saved_lines,
410
'dirty_head': dirty_head}
415
dirty_head.append(line)
417
if line.startswith('*** '):
321
if line.startswith('=== ') or line.startswith('*** '):
419
323
if line.startswith('#'):
421
325
elif orig_range > 0:
422
326
if line.startswith('-') or line.startswith(' '):
424
elif line.startswith('--- ') or regex.match(line):
425
if allow_dirty and beginning:
426
# Patches can have "junk" at the beginning
427
# Stripping junk from the end of patches is handled when we
430
elif len(saved_lines) > 0:
431
if keep_dirty and len(dirty_head) > 0:
432
yield {'saved_lines': saved_lines,
433
'dirty_head': dirty_head}
328
elif line.startswith('--- '):
329
if len(saved_lines) > 0:
438
332
elif line.startswith('@@'):
439
333
hunk = hunk_from_header(line)
440
334
orig_range = hunk.orig_range
441
335
saved_lines.append(line)
442
336
if len(saved_lines) > 0:
443
if keep_dirty and len(dirty_head) > 0:
444
yield {'saved_lines': saved_lines,
445
'dirty_head': dirty_head}
450
340
def iter_lines_handle_nl(iter_lines):
471
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
473
:arg iter_lines: iterable of lines to parse for patches
474
:kwarg allow_dirty: If True, allow text that's not part of the patch at
475
selected places. This includes comments before and after a patch
476
for instance. Default False.
477
:kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
481
for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
482
if 'dirty_head' in patch_lines:
483
patches.append({'patch': parse_patch(
484
patch_lines['saved_lines'], allow_dirty),
485
'dirty_head': patch_lines['dirty_head']})
487
patches.append(parse_patch(patch_lines, allow_dirty))
360
def parse_patches(iter_lines):
361
iter_lines = iter_lines_handle_nl(iter_lines)
362
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
491
365
def difference_index(atext, btext):
511
385
"""Iterate through a series of lines with a patch applied.
512
386
This handles a single file, and does exact, not fuzzy patching.
514
patch_lines = iter_lines_handle_nl(iter(patch_lines))
388
if orig_lines is not None:
389
orig_lines = orig_lines.__iter__()
391
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
515
392
get_patch_names(patch_lines)
516
return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
519
def iter_patched_from_hunks(orig_lines, hunks):
520
"""Iterate through a series of lines with a patch applied.
521
This handles a single file, and does exact, not fuzzy patching.
523
:param orig_lines: The unpatched lines.
524
:param hunks: An iterable of Hunk instances.
528
if orig_lines is not None:
529
orig_lines = iter(orig_lines)
394
for hunk in iter_hunks(patch_lines):
531
395
while line_no < hunk.orig_pos:
532
orig_line = next(orig_lines)
396
orig_line = orig_lines.next()
535
399
for hunk_line in hunk.lines:
537
401
if isinstance(hunk_line, InsertLine):
538
402
yield hunk_line.contents
539
403
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
540
orig_line = next(orig_lines)
404
orig_line = orig_lines.next()
541
405
if orig_line != hunk_line.contents:
542
406
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
543
407
if isinstance(hunk_line, ContextLine):
546
if not isinstance(hunk_line, RemoveLine):
547
raise AssertionError(hunk_line)
410
assert isinstance(hunk_line, RemoveLine)
549
412
if orig_lines is not None:
550
413
for line in orig_lines: