1
# Copyright (C) 2004 - 2006 Aaron Bentley
1
# Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
2
2
# <aaron.bentley@utoronto.ca>
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
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)
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
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'
57
34
def get_patch_names(iter_lines):
59
36
line = iter_lines.next()
37
match = re.match(binary_files_re, line)
39
raise BinaryFiles(match.group(1), match.group(2))
60
40
if not line.startswith("--- "):
61
41
raise MalformedPatchHeader("No orig name", line)
93
73
return (pos, range)
96
76
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)
78
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
80
raise MalformedHunkHeader("Does not match format.", line)
101
(orig, mod) = line[3:-4].split(" ")
82
(orig, mod) = matches.group(1).split(" ")
83
except (ValueError, IndexError), e:
103
84
raise MalformedHunkHeader(str(e), line)
104
85
if not orig.startswith('-') or not mod.startswith('+'):
105
86
raise MalformedHunkHeader("Positions don't start with + or -.", line)
107
88
(orig_pos, orig_range) = parse_range(orig[1:])
108
89
(mod_pos, mod_range) = parse_range(mod[1:])
90
except (ValueError, IndexError), e:
110
91
raise MalformedHunkHeader(str(e), line)
111
92
if mod_range < 0 or orig_range < 0:
112
93
raise MalformedHunkHeader("Hunk range is negative", line)
113
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
94
tail = matches.group(3)
95
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
162
144
return InsertLine(line[1:])
163
145
elif line.startswith("-"):
164
146
return RemoveLine(line[1:])
168
148
raise MalformedLine("Unknown line type", line)
173
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
153
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
174
154
self.orig_pos = orig_pos
175
155
self.orig_range = orig_range
176
156
self.mod_pos = mod_pos
177
157
self.mod_range = mod_range
180
161
def get_header(self):
181
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
183
self.range_str(self.mod_pos,
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,
186
172
def range_str(self, pos, range):
187
173
"""Return a file range, special-casing for 1-line files.
256
class BinaryPatch(object):
258
257
def __init__(self, oldname, newname):
259
258
self.oldname = oldname
260
259
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)
263
271
def __str__(self):
264
ret = self.get_header()
272
ret = self.get_header()
265
273
ret += "".join([str(h) for h in self.hunks])
268
276
def get_header(self):
269
277
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
272
"""Return a string of patch statistics"""
279
def stats_values(self):
280
"""Calculate the number of inserts and removes."""
275
283
for hunk in self.hunks:
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):
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)
319
357
for line in iter_lines:
320
358
if line.startswith('=== ') or line.startswith('*** '):
322
360
if line.startswith('#'):
324
elif line.startswith('--- '):
325
if len(saved_lines) > 0:
363
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:
326
372
yield saved_lines
374
elif line.startswith('@@'):
375
hunk = hunk_from_header(line)
376
orig_range = hunk.orig_range
328
377
saved_lines.append(line)
329
378
if len(saved_lines) > 0:
330
379
yield saved_lines
353
def parse_patches(iter_lines):
354
iter_lines = iter_lines_handle_nl(iter_lines)
355
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
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)]
358
414
def difference_index(atext, btext):
378
434
"""Iterate through a series of lines with a patch applied.
379
435
This handles a single file, and does exact, not fuzzy patching.
381
if orig_lines is not None:
382
orig_lines = orig_lines.__iter__()
437
patch_lines = iter_lines_handle_nl(iter(patch_lines))
438
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.
384
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
385
get_patch_names(patch_lines)
387
for hunk in iter_hunks(patch_lines):
451
if orig_lines is not None:
452
orig_lines = iter(orig_lines)
388
454
while line_no < hunk.orig_pos:
389
455
orig_line = orig_lines.next()