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
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
from __future__ import absolute_import
27
binary_files_re = b'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.'
20
binary_files_re = 'Binary files (.*) and (.*) differ\n'
23
class BinaryFiles(Exception):
38
25
def __init__(self, orig_name, mod_name):
39
26
self.orig_name = orig_name
40
27
self.mod_name = mod_name
28
Exception.__init__(self, 'Binary files section encountered.')
31
class PatchSyntax(Exception):
32
def __init__(self, msg):
33
Exception.__init__(self, msg)
43
36
class MalformedPatchHeader(PatchSyntax):
45
_fmt = "Malformed patch header. %(desc)s\n%(line)r"
47
def __init__(self, desc, line):
37
def __init__(self, desc, line):
40
msg = "Malformed patch header. %s\n%r" % (self.desc, self.line)
41
PatchSyntax.__init__(self, msg)
44
class MalformedHunkHeader(PatchSyntax):
45
def __init__(self, desc, line):
48
msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line)
49
PatchSyntax.__init__(self, msg)
52
52
class MalformedLine(PatchSyntax):
54
_fmt = "Malformed line. %(desc)s\n%(line)r"
56
53
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"')
56
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
57
PatchSyntax.__init__(self, msg)
60
class PatchConflict(Exception):
66
61
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):
62
orig = orig_line.rstrip('\n')
63
patch = str(patch_line).rstrip('\n')
64
msg = 'Text contents mismatch at line %d. Original has "%s",'\
65
' but patch says it should be "%s"' % (line_no, orig, patch)
66
Exception.__init__(self, msg)
81
69
def get_patch_names(iter_lines):
82
line = next(iter_lines)
71
line = iter_lines.next()
84
72
match = re.match(binary_files_re, line)
85
73
if match is not None:
86
74
raise BinaryFiles(match.group(1), match.group(2))
87
if not line.startswith(b"--- "):
75
if not line.startswith("--- "):
88
76
raise MalformedPatchHeader("No orig name", line)
90
orig_name = line[4:].rstrip(b"\n")
78
orig_name = line[4:].rstrip("\n")
91
79
except StopIteration:
92
80
raise MalformedPatchHeader("No orig line", "")
94
line = next(iter_lines)
95
if not line.startswith(b"+++ "):
82
line = iter_lines.next()
83
if not line.startswith("+++ "):
96
84
raise PatchSyntax("No mod name")
98
mod_name = line[4:].rstrip(b"\n")
86
mod_name = line[4:].rstrip("\n")
99
87
except StopIteration:
100
88
raise MalformedPatchHeader("No mod line", "")
101
89
return (orig_name, mod_name)
123
111
def hunk_from_header(line):
125
matches = re.match(br'\@\@ ([^@]*) \@\@( (.*))?\n', line)
113
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
126
114
if matches is None:
127
115
raise MalformedHunkHeader("Does not match format.", line)
129
(orig, mod) = matches.group(1).split(b" ")
130
except (ValueError, IndexError) as e:
117
(orig, mod) = matches.group(1).split(" ")
118
except (ValueError, IndexError), e:
131
119
raise MalformedHunkHeader(str(e), line)
132
if not orig.startswith(b'-') or not mod.startswith(b'+'):
120
if not orig.startswith('-') or not mod.startswith('+'):
133
121
raise MalformedHunkHeader("Positions don't start with + or -.", line)
135
123
(orig_pos, orig_range) = parse_range(orig[1:])
136
124
(mod_pos, mod_range) = parse_range(mod[1:])
137
except (ValueError, IndexError) as e:
125
except (ValueError, IndexError), e:
138
126
raise MalformedHunkHeader(str(e), line)
139
127
if mod_range < 0 or orig_range < 0:
140
128
raise MalformedHunkHeader("Hunk range is negative", line)
142
130
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
145
class HunkLine(object):
147
134
def __init__(self, contents):
148
135
self.contents = contents
150
137
def get_str(self, leadchar):
151
if self.contents == b"\n" and leadchar == b" " and False:
153
if not self.contents.endswith(b'\n'):
154
terminator = b'\n' + NO_NL
138
if self.contents == "\n" and leadchar == " " and False:
140
if not self.contents.endswith('\n'):
141
terminator = '\n' + NO_NL
157
144
return leadchar + self.contents + terminator
160
raise NotImplementedError
163
147
class ContextLine(HunkLine):
165
148
def __init__(self, contents):
166
149
HunkLine.__init__(self, contents)
169
return self.get_str(b" ")
152
return self.get_str(" ")
172
155
class InsertLine(HunkLine):
173
156
def __init__(self, contents):
174
157
HunkLine.__init__(self, contents)
177
return self.get_str(b"+")
160
return self.get_str("+")
180
163
class RemoveLine(HunkLine):
181
164
def __init__(self, contents):
182
165
HunkLine.__init__(self, contents)
185
return self.get_str(b"-")
188
NO_NL = b'\\ No newline at end of file\n'
189
__pychecker__ = "no-returnvalues"
168
return self.get_str("-")
170
NO_NL = '\\ No newline at end of file\n'
171
__pychecker__="no-returnvalues"
192
173
def parse_line(line):
193
if line.startswith(b"\n"):
174
if line.startswith("\n"):
194
175
return ContextLine(line)
195
elif line.startswith(b" "):
176
elif line.startswith(" "):
196
177
return ContextLine(line[1:])
197
elif line.startswith(b"+"):
178
elif line.startswith("+"):
198
179
return InsertLine(line[1:])
199
elif line.startswith(b"-"):
180
elif line.startswith("-"):
200
181
return RemoveLine(line[1:])
202
183
raise MalformedLine("Unknown line type", line)
210
188
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
211
189
self.orig_pos = orig_pos
212
190
self.orig_range = orig_range
236
214
:return: a string in the format 1,4 except when range == pos == 1
241
return b"%i,%i" % (pos, range)
219
return "%i,%i" % (pos, range)
244
222
lines = [self.get_header()]
245
223
for line in self.lines:
246
lines.append(line.as_bytes())
247
return b"".join(lines)
224
lines.append(str(line))
225
return "".join(lines)
251
227
def shift_to_mod(self, pos):
252
if pos < self.orig_pos - 1:
228
if pos < self.orig_pos-1:
254
elif pos > self.orig_pos + self.orig_range:
230
elif pos > self.orig_pos+self.orig_range:
255
231
return self.mod_range - self.orig_range
257
233
return self.shift_to_mod_lines(pos)
259
235
def shift_to_mod_lines(self, pos):
260
position = self.orig_pos - 1
236
position = self.orig_pos-1
262
238
for line in self.lines:
263
239
if isinstance(line, InsertLine):
277
def iter_hunks(iter_lines, allow_dirty=False):
279
:arg iter_lines: iterable of lines to parse for hunks
280
:kwarg allow_dirty: If True, when we encounter something that is not
281
a hunk header when we're looking for one, assume the rest of the lines
282
are not part of the patch (comments or other junk). Default False
253
def iter_hunks(iter_lines):
285
255
for line in iter_lines:
287
257
if hunk is not None:
291
261
if hunk is not None:
294
hunk = hunk_from_header(line)
295
except MalformedHunkHeader:
297
# If the line isn't a hunk header, then we've reached the end
298
# of this patch and there's "junk" at the end. Ignore the
299
# rest of this patch.
263
hunk = hunk_from_header(line)
304
266
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
305
hunk_line = parse_line(next(iter_lines))
267
hunk_line = parse_line(iter_lines.next())
306
268
hunk.lines.append(hunk_line)
307
269
if isinstance(hunk_line, (RemoveLine, ContextLine)):
381
def parse_patch(iter_lines, allow_dirty=False):
383
:arg iter_lines: iterable of lines to parse
384
:kwarg allow_dirty: If True, allow the patch to have trailing junk.
342
def parse_patch(iter_lines):
387
343
iter_lines = iter_lines_handle_nl(iter_lines)
389
345
(orig_name, mod_name) = get_patch_names(iter_lines)
390
except BinaryFiles as e:
346
except BinaryFiles, e:
391
347
return BinaryPatch(e.orig_name, e.mod_name)
393
349
patch = Patch(orig_name, mod_name)
394
for hunk in iter_hunks(iter_lines, allow_dirty):
350
for hunk in iter_hunks(iter_lines):
395
351
patch.hunks.append(hunk)
399
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
401
:arg iter_lines: iterable of lines to parse for patches
402
:kwarg allow_dirty: If True, allow comments and other non-patch text
403
before the first patch. Note that the algorithm here can only find
404
such text before any patches have been found. Comments after the
405
first patch are stripped away in iter_hunks() if it is also passed
406
allow_dirty=True. Default False.
408
# FIXME: Docstring is not quite true. We allow certain comments no
409
# matter what, If they startwith '===', '***', or '#' Someone should
410
# reexamine this logic and decide if we should include those in
411
# allow_dirty or restrict those to only being before the patch is found
412
# (as allow_dirty does).
355
def iter_file_patch(iter_lines):
413
356
regex = re.compile(binary_files_re)
419
359
for line in iter_lines:
420
if line.startswith(b'=== '):
421
if len(saved_lines) > 0:
422
if keep_dirty and len(dirty_head) > 0:
423
yield {'saved_lines': saved_lines,
424
'dirty_head': dirty_head}
429
dirty_head.append(line)
431
if line.startswith(b'*** '):
433
if line.startswith(b'#'):
360
if line.startswith('=== ') or line.startswith('*** '):
362
if line.startswith('#'):
435
364
elif orig_range > 0:
436
if line.startswith(b'-') or line.startswith(b' '):
365
if line.startswith('-') or line.startswith(' '):
438
elif line.startswith(b'--- ') or regex.match(line):
439
if allow_dirty and beginning:
440
# Patches can have "junk" at the beginning
441
# Stripping junk from the end of patches is handled when we
444
elif len(saved_lines) > 0:
445
if keep_dirty and len(dirty_head) > 0:
446
yield {'saved_lines': saved_lines,
447
'dirty_head': dirty_head}
367
elif line.startswith('--- ') or regex.match(line):
368
if len(saved_lines) > 0:
452
elif line.startswith(b'@@'):
371
elif line.startswith('@@'):
453
372
hunk = hunk_from_header(line)
454
373
orig_range = hunk.orig_range
455
374
saved_lines.append(line)
456
375
if len(saved_lines) > 0:
457
if keep_dirty and len(dirty_head) > 0:
458
yield {'saved_lines': saved_lines,
459
'dirty_head': dirty_head}
464
379
def iter_lines_handle_nl(iter_lines):
485
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
487
:arg iter_lines: iterable of lines to parse for patches
488
:kwarg allow_dirty: If True, allow text that's not part of the patch at
489
selected places. This includes comments before and after a patch
490
for instance. Default False.
491
:kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
494
for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
495
if 'dirty_head' in patch_lines:
496
yield ({'patch': parse_patch(patch_lines['saved_lines'], allow_dirty),
497
'dirty_head': patch_lines['dirty_head']})
499
yield parse_patch(patch_lines, allow_dirty)
400
def parse_patches(iter_lines):
401
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
502
404
def difference_index(atext, btext):