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
20
from bzrlib.errors import (
32
binary_files_re = 'Binary files (.*) and (.*) differ\n'
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)
24
class MalformedPatchHeader(PatchSyntax):
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)
40
class MalformedLine(PatchSyntax):
41
def __init__(self, desc, line):
44
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
45
PatchSyntax.__init__(self, msg)
48
class PatchConflict(Exception):
49
def __init__(self, line_no, orig_line, patch_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)
34
57
def get_patch_names(iter_lines):
36
59
line = iter_lines.next()
37
match = re.match(binary_files_re, line)
39
raise BinaryFiles(match.group(1), match.group(2))
40
60
if not line.startswith("--- "):
41
61
raise MalformedPatchHeader("No orig name", line)
73
93
return (pos, range)
76
96
def hunk_from_header(line):
78
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
80
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)
82
(orig, mod) = matches.group(1).split(" ")
83
except (ValueError, IndexError), e:
101
(orig, mod) = line[3:-4].split(" ")
84
103
raise MalformedHunkHeader(str(e), line)
85
104
if not orig.startswith('-') or not mod.startswith('+'):
86
105
raise MalformedHunkHeader("Positions don't start with + or -.", line)
88
107
(orig_pos, orig_range) = parse_range(orig[1:])
89
108
(mod_pos, mod_range) = parse_range(mod[1:])
90
except (ValueError, IndexError), e:
91
110
raise MalformedHunkHeader(str(e), line)
92
111
if mod_range < 0 or orig_range < 0:
93
112
raise MalformedHunkHeader("Hunk range is negative", line)
94
tail = matches.group(3)
95
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
113
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
144
162
return InsertLine(line[1:])
145
163
elif line.startswith("-"):
146
164
return RemoveLine(line[1:])
148
168
raise MalformedLine("Unknown line type", line)
153
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):
154
174
self.orig_pos = orig_pos
155
175
self.orig_range = orig_range
156
176
self.mod_pos = mod_pos
157
177
self.mod_range = mod_range
161
180
def get_header(self):
162
if self.tail is None:
165
tail_str = ' ' + self.tail
166
return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
168
self.range_str(self.mod_pos,
181
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
183
self.range_str(self.mod_pos,
172
186
def range_str(self, pos, range):
173
187
"""Return a file range, special-casing for 1-line files.
218
def iter_hunks(iter_lines, allow_dirty=False):
220
:arg iter_lines: iterable of lines to parse for hunks
221
:kwarg allow_dirty: If True, when we encounter something that is not
222
a hunk header when we're looking for one, assume the rest of the lines
223
are not part of the patch (comments or other junk). Default False
233
def iter_hunks(iter_lines):
226
235
for line in iter_lines:
232
241
if hunk is not None:
235
hunk = hunk_from_header(line)
236
except MalformedHunkHeader:
238
# If the line isn't a hunk header, then we've reached the end
239
# of this patch and there's "junk" at the end. Ignore the
240
# rest of this patch.
243
hunk = hunk_from_header(line)
245
246
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
256
class BinaryPatch(object):
257
258
def __init__(self, oldname, newname):
258
259
self.oldname = oldname
259
260
self.newname = newname
262
return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
265
class Patch(BinaryPatch):
267
def __init__(self, oldname, newname):
268
BinaryPatch.__init__(self, oldname, newname)
271
263
def __str__(self):
272
ret = self.get_header()
264
ret = self.get_header()
273
265
ret += "".join([str(h) for h in self.hunks])
276
268
def get_header(self):
277
269
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
279
def stats_values(self):
280
"""Calculate the number of inserts and removes."""
272
"""Return a string of patch statistics"""
283
275
for hunk in self.hunks:
287
279
elif isinstance(line, RemoveLine):
289
return (inserts, removes, len(self.hunks))
292
"""Return a string of patch statistics"""
293
281
return "%i inserts, %i removes in %i hunks" % \
282
(inserts, removes, len(self.hunks))
296
284
def pos_in_mod(self, position):
297
285
newpos = position
321
def parse_patch(iter_lines, allow_dirty=False):
323
:arg iter_lines: iterable of lines to parse
324
:kwarg allow_dirty: If True, allow the patch to have trailing junk.
327
iter_lines = iter_lines_handle_nl(iter_lines)
329
(orig_name, mod_name) = get_patch_names(iter_lines)
330
except BinaryFiles, e:
331
return BinaryPatch(e.orig_name, e.mod_name)
333
patch = Patch(orig_name, mod_name)
334
for hunk in iter_hunks(iter_lines, allow_dirty):
335
patch.hunks.append(hunk)
339
def iter_file_patch(iter_lines, allow_dirty=False):
341
:arg iter_lines: iterable of lines to parse for patches
342
:kwarg allow_dirty: If True, allow comments and other non-patch text
343
before the first patch. Note that the algorithm here can only find
344
such text before any patches have been found. Comments after the
345
first patch are stripped away in iter_hunks() if it is also passed
346
allow_dirty=True. Default False.
348
### FIXME: Docstring is not quite true. We allow certain comments no
349
# matter what, If they startwith '===', '***', or '#' Someone should
350
# reexamine this logic and decide if we should include those in
351
# allow_dirty or restrict those to only being before the patch is found
352
# (as allow_dirty does).
353
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):
357
320
for line in iter_lines:
358
321
if line.startswith('=== ') or line.startswith('*** '):
362
325
elif orig_range > 0:
363
326
if line.startswith('-') or line.startswith(' '):
365
elif line.startswith('--- ') or regex.match(line):
366
if allow_dirty and beginning:
367
# Patches can have "junk" at the beginning
368
# Stripping junk from the end of patches is handled when we
371
elif len(saved_lines) > 0:
328
elif line.startswith('--- '):
329
if len(saved_lines) > 0:
372
330
yield saved_lines
374
332
elif line.startswith('@@'):
403
def parse_patches(iter_lines, allow_dirty=False):
405
:arg iter_lines: iterable of lines to parse for patches
406
:kwarg allow_dirty: If True, allow text that's not part of the patch at
407
selected places. This includes comments before and after a patch
408
for instance. Default False.
410
return [parse_patch(f.__iter__(), allow_dirty) for f in
411
iter_file_patch(iter_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)]
414
365
def difference_index(atext, btext):
434
385
"""Iterate through a series of lines with a patch applied.
435
386
This handles a single file, and does exact, not fuzzy patching.
437
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__())
438
392
get_patch_names(patch_lines)
439
return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
442
def iter_patched_from_hunks(orig_lines, hunks):
443
"""Iterate through a series of lines with a patch applied.
444
This handles a single file, and does exact, not fuzzy patching.
446
:param orig_lines: The unpatched lines.
447
:param hunks: An iterable of Hunk instances.
451
if orig_lines is not None:
452
orig_lines = iter(orig_lines)
394
for hunk in iter_hunks(patch_lines):
454
395
while line_no < hunk.orig_pos:
455
396
orig_line = orig_lines.next()