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., 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)
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'
57
35
def get_patch_names(iter_lines):
36
line = iter_lines.next()
59
line = iter_lines.next()
38
match = re.match(binary_files_re, line)
40
raise BinaryFiles(match.group(1), match.group(2))
60
41
if not line.startswith("--- "):
61
42
raise MalformedPatchHeader("No orig name", line)
93
74
return (pos, range)
96
77
def hunk_from_header(line):
97
if not line.startswith("@@") or not line.endswith("@@\n") \
99
raise MalformedHunkHeader("Does not start and end with @@.", line)
79
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
81
raise MalformedHunkHeader("Does not match format.", line)
101
(orig, mod) = line[3:-4].split(" ")
83
(orig, mod) = matches.group(1).split(" ")
84
except (ValueError, IndexError), e:
103
85
raise MalformedHunkHeader(str(e), line)
104
86
if not orig.startswith('-') or not mod.startswith('+'):
105
87
raise MalformedHunkHeader("Positions don't start with + or -.", line)
107
89
(orig_pos, orig_range) = parse_range(orig[1:])
108
90
(mod_pos, mod_range) = parse_range(mod[1:])
91
except (ValueError, IndexError), e:
110
92
raise MalformedHunkHeader(str(e), line)
111
93
if mod_range < 0 or orig_range < 0:
112
94
raise MalformedHunkHeader("Hunk range is negative", line)
113
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
95
tail = matches.group(3)
96
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
162
145
return InsertLine(line[1:])
163
146
elif line.startswith("-"):
164
147
return RemoveLine(line[1:])
168
149
raise MalformedLine("Unknown line type", line)
173
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
154
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
174
155
self.orig_pos = orig_pos
175
156
self.orig_range = orig_range
176
157
self.mod_pos = mod_pos
177
158
self.mod_range = mod_range
180
162
def get_header(self):
181
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
183
self.range_str(self.mod_pos,
163
if self.tail is None:
166
tail_str = ' ' + self.tail
167
return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
169
self.range_str(self.mod_pos,
186
173
def range_str(self, pos, range):
187
174
"""Return a file range, special-casing for 1-line files.
257
class BinaryPatch(object):
258
258
def __init__(self, oldname, newname):
259
259
self.oldname = oldname
260
260
self.newname = newname
263
return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
266
class Patch(BinaryPatch):
268
def __init__(self, oldname, newname):
269
BinaryPatch.__init__(self, oldname, newname)
263
272
def __str__(self):
264
ret = self.get_header()
273
ret = self.get_header()
265
274
ret += "".join([str(h) for h in self.hunks])
268
277
def get_header(self):
269
278
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
272
"""Return a string of patch statistics"""
280
def stats_values(self):
281
"""Calculate the number of inserts and removes."""
275
284
for hunk in self.hunks:
279
288
elif isinstance(line, RemoveLine):
290
return (inserts, removes, len(self.hunks))
293
"""Return a string of patch statistics"""
281
294
return "%i inserts, %i removes in %i hunks" % \
282
(inserts, removes, len(self.hunks))
284
297
def pos_in_mod(self, position):
285
298
newpos = position
305
318
if isinstance(line, ContextLine):
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):
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, keep_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)
320
359
for line in iter_lines:
321
if line.startswith('=== ') or line.startswith('*** '):
360
if line.startswith('=== '):
361
if len(saved_lines) > 0:
362
if keep_dirty and len(dirty_head) > 0:
363
yield {'saved_lines': saved_lines,
364
'dirty_head': dirty_head}
369
dirty_head.append(line)
371
if line.startswith('*** '):
323
373
if line.startswith('#'):
325
375
elif orig_range > 0:
326
376
if line.startswith('-') or line.startswith(' '):
328
elif line.startswith('--- '):
329
if len(saved_lines) > 0:
378
elif line.startswith('--- ') or regex.match(line):
379
if allow_dirty and beginning:
380
# Patches can have "junk" at the beginning
381
# Stripping junk from the end of patches is handled when we
384
elif len(saved_lines) > 0:
385
if keep_dirty and len(dirty_head) > 0:
386
yield {'saved_lines': saved_lines,
387
'dirty_head': dirty_head}
332
392
elif line.startswith('@@'):
333
393
hunk = hunk_from_header(line)
334
394
orig_range = hunk.orig_range
335
395
saved_lines.append(line)
336
396
if len(saved_lines) > 0:
397
if keep_dirty and len(dirty_head) > 0:
398
yield {'saved_lines': saved_lines,
399
'dirty_head': dirty_head}
340
404
def iter_lines_handle_nl(iter_lines):
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)]
425
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
427
:arg iter_lines: iterable of lines to parse for patches
428
:kwarg allow_dirty: If True, allow text that's not part of the patch at
429
selected places. This includes comments before and after a patch
430
for instance. Default False.
431
:kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
435
for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
436
if 'dirty_head' in patch_lines:
437
patches.append({'patch': parse_patch(
438
patch_lines['saved_lines'], allow_dirty),
439
'dirty_head': patch_lines['dirty_head']})
441
patches.append(parse_patch(patch_lines, allow_dirty))
365
445
def difference_index(atext, btext):
385
465
"""Iterate through a series of lines with a patch applied.
386
466
This handles a single file, and does exact, not fuzzy patching.
388
if orig_lines is not None:
389
orig_lines = orig_lines.__iter__()
468
patch_lines = iter_lines_handle_nl(iter(patch_lines))
469
get_patch_names(patch_lines)
470
return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
473
def iter_patched_from_hunks(orig_lines, hunks):
474
"""Iterate through a series of lines with a patch applied.
475
This handles a single file, and does exact, not fuzzy patching.
477
:param orig_lines: The unpatched lines.
478
:param hunks: An iterable of Hunk instances.
391
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
392
get_patch_names(patch_lines)
394
for hunk in iter_hunks(patch_lines):
482
if orig_lines is not None:
483
orig_lines = iter(orig_lines)
395
485
while line_no < hunk.orig_pos:
396
486
orig_line = orig_lines.next()