1
# Copyright (C) 2004, 2005 Aaron Bentley
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
18
class PatchSyntax(Exception):
19
def __init__(self, msg):
20
Exception.__init__(self, msg)
23
class MalformedPatchHeader(PatchSyntax):
24
def __init__(self, desc, line):
27
msg = "Malformed patch header. %s\n%r" % (self.desc, self.line)
28
PatchSyntax.__init__(self, msg)
30
class MalformedHunkHeader(PatchSyntax):
31
def __init__(self, desc, line):
34
msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line)
35
PatchSyntax.__init__(self, msg)
37
class MalformedLine(PatchSyntax):
38
def __init__(self, desc, line):
41
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
42
PatchSyntax.__init__(self, msg)
44
def get_patch_names(iter_lines):
46
line = iter_lines.next()
47
if not line.startswith("--- "):
48
raise MalformedPatchHeader("No orig name", line)
50
orig_name = line[4:].rstrip("\n")
52
raise MalformedPatchHeader("No orig line", "")
54
line = iter_lines.next()
55
if not line.startswith("+++ "):
56
raise PatchSyntax("No mod name")
58
mod_name = line[4:].rstrip("\n")
60
raise MalformedPatchHeader("No mod line", "")
61
return (orig_name, mod_name)
63
def parse_range(textrange):
64
"""Parse a patch range, handling the "1" special-case
66
:param textrange: The text to parse
68
:return: the position and range, as a tuple
71
tmp = textrange.split(',')
82
def hunk_from_header(line):
83
if not line.startswith("@@") or not line.endswith("@@\n") \
85
raise MalformedHunkHeader("Does not start and end with @@.", line)
87
(orig, mod) = line[3:-4].split(" ")
89
raise MalformedHunkHeader(str(e), line)
90
if not orig.startswith('-') or not mod.startswith('+'):
91
raise MalformedHunkHeader("Positions don't start with + or -.", line)
93
(orig_pos, orig_range) = parse_range(orig[1:])
94
(mod_pos, mod_range) = parse_range(mod[1:])
96
raise MalformedHunkHeader(str(e), line)
97
if mod_range < 0 or orig_range < 0:
98
raise MalformedHunkHeader("Hunk range is negative", line)
99
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
103
def __init__(self, contents):
104
self.contents = contents
106
def get_str(self, leadchar):
107
if self.contents == "\n" and leadchar == " " and False:
109
if not self.contents.endswith('\n'):
110
terminator = '\n' + NO_NL
113
return leadchar + self.contents + terminator
116
class ContextLine(HunkLine):
117
def __init__(self, contents):
118
HunkLine.__init__(self, contents)
121
return self.get_str(" ")
124
class InsertLine(HunkLine):
125
def __init__(self, contents):
126
HunkLine.__init__(self, contents)
129
return self.get_str("+")
132
class RemoveLine(HunkLine):
133
def __init__(self, contents):
134
HunkLine.__init__(self, contents)
137
return self.get_str("-")
139
NO_NL = '\\ No newline at end of file\n'
140
__pychecker__="no-returnvalues"
142
def parse_line(line):
143
if line.startswith("\n"):
144
return ContextLine(line)
145
elif line.startswith(" "):
146
return ContextLine(line[1:])
147
elif line.startswith("+"):
148
return InsertLine(line[1:])
149
elif line.startswith("-"):
150
return RemoveLine(line[1:])
154
raise MalformedLine("Unknown line type", line)
159
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
160
self.orig_pos = orig_pos
161
self.orig_range = orig_range
162
self.mod_pos = mod_pos
163
self.mod_range = mod_range
166
def get_header(self):
167
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
169
self.range_str(self.mod_pos,
172
def range_str(self, pos, range):
173
"""Return a file range, special-casing for 1-line files.
175
:param pos: The position in the file
177
:range: The range in the file
179
:return: a string in the format 1,4 except when range == pos == 1
184
return "%i,%i" % (pos, range)
187
lines = [self.get_header()]
188
for line in self.lines:
189
lines.append(str(line))
190
return "".join(lines)
192
def shift_to_mod(self, pos):
193
if pos < self.orig_pos-1:
195
elif pos > self.orig_pos+self.orig_range:
196
return self.mod_range - self.orig_range
198
return self.shift_to_mod_lines(pos)
200
def shift_to_mod_lines(self, pos):
201
assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
202
position = self.orig_pos-1
204
for line in self.lines:
205
if isinstance(line, InsertLine):
207
elif isinstance(line, RemoveLine):
212
elif isinstance(line, ContextLine):
218
def iter_hunks(iter_lines):
220
for line in iter_lines:
228
hunk = hunk_from_header(line)
231
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
232
hunk_line = parse_line(iter_lines.next())
233
hunk.lines.append(hunk_line)
234
if isinstance(hunk_line, (RemoveLine, ContextLine)):
236
if isinstance(hunk_line, (InsertLine, ContextLine)):
242
def __init__(self, oldname, newname):
243
self.oldname = oldname
244
self.newname = newname
248
ret = self.get_header()
249
ret += "".join([str(h) for h in self.hunks])
252
def get_header(self):
253
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
256
"""Return a string of patch statistics"""
259
for hunk in self.hunks:
260
for line in hunk.lines:
261
if isinstance(line, InsertLine):
263
elif isinstance(line, RemoveLine):
265
return "%i inserts, %i removes in %i hunks" % \
266
(inserts, removes, len(self.hunks))
268
def pos_in_mod(self, position):
270
for hunk in self.hunks:
271
shift = hunk.shift_to_mod(position)
277
def iter_inserted(self):
278
"""Iteraties through inserted lines
280
:return: Pair of line number, line
281
:rtype: iterator of (int, InsertLine)
283
for hunk in self.hunks:
284
pos = hunk.mod_pos - 1;
285
for line in hunk.lines:
286
if isinstance(line, InsertLine):
289
if isinstance(line, ContextLine):
292
def parse_patch(iter_lines):
293
(orig_name, mod_name) = get_patch_names(iter_lines)
294
patch = Patch(orig_name, mod_name)
295
for hunk in iter_hunks(iter_lines):
296
patch.hunks.append(hunk)
300
def iter_file_patch(iter_lines):
302
for line in iter_lines:
303
if line.startswith('*** '):
305
if line.startswith('==='):
307
elif line.startswith('--- '):
308
if len(saved_lines) > 0:
311
saved_lines.append(line)
312
if len(saved_lines) > 0:
316
def iter_lines_handle_nl(iter_lines):
318
Iterates through lines, ensuring that lines that originally had no
319
terminating \n are produced without one. This transformation may be
320
applied at any point up until hunk line parsing, and is safe to apply
324
for line in iter_lines:
326
assert last_line.endswith('\n')
327
last_line = last_line[:-1]
329
if last_line is not None:
332
if last_line is not None:
336
def parse_patches(iter_lines):
337
iter_lines = iter_lines_handle_nl(iter_lines)
338
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
341
def difference_index(atext, btext):
342
"""Find the indext of the first character that differs betweeen two texts
344
:param atext: The first text
346
:param btext: The second text
348
:return: The index, or None if there are no differences within the range
349
:rtype: int or NoneType
352
if len(btext) < length:
354
for i in range(length):
355
if atext[i] != btext[i]:
359
class PatchConflict(Exception):
360
def __init__(self, line_no, orig_line, patch_line):
361
orig = orig_line.rstrip('\n')
362
patch = str(patch_line).rstrip('\n')
363
msg = 'Text contents mismatch at line %d. Original has "%s",'\
364
' but patch says it should be "%s"' % (line_no, orig, patch)
365
Exception.__init__(self, msg)
368
def iter_patched(orig_lines, patch_lines):
369
"""Iterate through a series of lines with a patch applied.
370
This handles a single file, and does exact, not fuzzy patching.
372
if orig_lines is not None:
373
orig_lines = orig_lines.__iter__()
375
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
376
get_patch_names(patch_lines)
378
for hunk in iter_hunks(patch_lines):
379
while line_no < hunk.orig_pos:
380
orig_line = orig_lines.next()
383
for hunk_line in hunk.lines:
384
seen_patch.append(str(hunk_line))
385
if isinstance(hunk_line, InsertLine):
386
yield hunk_line.contents
387
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
388
orig_line = orig_lines.next()
389
if orig_line != hunk_line.contents:
390
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
391
if isinstance(hunk_line, ContextLine):
394
assert isinstance(hunk_line, RemoveLine)
399
class PatchesTester(unittest.TestCase):
400
def datafile(self, filename):
401
data_path = os.path.join(os.path.dirname(__file__), "testdata",
403
return file(data_path, "rb")
405
def testValidPatchHeader(self):
406
"""Parse a valid patch header"""
407
lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n')
408
(orig, mod) = get_patch_names(lines.__iter__())
409
assert(orig == "orig/commands.py")
410
assert(mod == "mod/dommands.py")
412
def testInvalidPatchHeader(self):
413
"""Parse an invalid patch header"""
414
lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n')
415
self.assertRaises(MalformedPatchHeader, get_patch_names,
418
def testValidHunkHeader(self):
419
"""Parse a valid hunk header"""
420
header = "@@ -34,11 +50,6 @@\n"
421
hunk = hunk_from_header(header);
422
assert (hunk.orig_pos == 34)
423
assert (hunk.orig_range == 11)
424
assert (hunk.mod_pos == 50)
425
assert (hunk.mod_range == 6)
426
assert (str(hunk) == header)
428
def testValidHunkHeader2(self):
429
"""Parse a tricky, valid hunk header"""
430
header = "@@ -1 +0,0 @@\n"
431
hunk = hunk_from_header(header);
432
assert (hunk.orig_pos == 1)
433
assert (hunk.orig_range == 1)
434
assert (hunk.mod_pos == 0)
435
assert (hunk.mod_range == 0)
436
assert (str(hunk) == header)
438
def makeMalformed(self, header):
439
self.assertRaises(MalformedHunkHeader, hunk_from_header, header)
441
def testInvalidHeader(self):
442
"""Parse an invalid hunk header"""
443
self.makeMalformed(" -34,11 +50,6 \n")
444
self.makeMalformed("@@ +50,6 -34,11 @@\n")
445
self.makeMalformed("@@ -34,11 +50,6 @@")
446
self.makeMalformed("@@ -34.5,11 +50,6 @@\n")
447
self.makeMalformed("@@-34,11 +50,6@@\n")
448
self.makeMalformed("@@ 34,11 50,6 @@\n")
449
self.makeMalformed("@@ -34,11 @@\n")
450
self.makeMalformed("@@ -34,11 +50,6.5 @@\n")
451
self.makeMalformed("@@ -34,11 +50,-6 @@\n")
453
def lineThing(self,text, type):
454
line = parse_line(text)
455
assert(isinstance(line, type))
456
assert(str(line)==text)
458
def makeMalformedLine(self, text):
459
self.assertRaises(MalformedLine, parse_line, text)
461
def testValidLine(self):
462
"""Parse a valid hunk line"""
463
self.lineThing(" hello\n", ContextLine)
464
self.lineThing("+hello\n", InsertLine)
465
self.lineThing("-hello\n", RemoveLine)
467
def testMalformedLine(self):
468
"""Parse invalid valid hunk lines"""
469
self.makeMalformedLine("hello\n")
471
def compare_parsed(self, patchtext):
472
lines = patchtext.splitlines(True)
473
patch = parse_patch(lines.__iter__())
475
i = difference_index(patchtext, pstr)
477
print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
478
self.assertEqual (patchtext, str(patch))
481
"""Test parsing a whole patch"""
482
patchtext = """--- orig/commands.py
484
@@ -1337,7 +1337,8 @@
486
def set_title(self, command=None):
488
- version = self.tree.tree_version.nonarch
489
+ version = pylon.alias_or_version(self.tree.tree_version, self.tree,
492
version = "[no version]"
494
@@ -1983,7 +1984,11 @@
496
if len(new_merges) > 0:
497
if cmdutil.prompt("Log for merge"):
498
- mergestuff = cmdutil.log_for_merge(tree, comp_version)
499
+ if cmdutil.prompt("changelog for merge"):
500
+ mergestuff = "Patches applied:\\n"
501
+ mergestuff += pylon.changelog_for_merge(new_merges)
503
+ mergestuff = cmdutil.log_for_merge(tree, comp_version)
504
log.description += mergestuff
508
self.compare_parsed(patchtext)
511
"""Handle patches missing half the position, range tuple"""
513
"""--- orig/__init__.py
516
__docformat__ = "restructuredtext en"
517
+__doc__ = An alternate Arch commandline interface
519
self.compare_parsed(patchtext)
523
def testLineLookup(self):
525
"""Make sure we can accurately look up mod line from orig"""
526
patch = parse_patch(self.datafile("diff"))
527
orig = list(self.datafile("orig"))
528
mod = list(self.datafile("mod"))
530
for i in range(len(orig)):
531
mod_pos = patch.pos_in_mod(i)
533
removals.append(orig[i])
535
assert(mod[mod_pos]==orig[i])
536
rem_iter = removals.__iter__()
537
for hunk in patch.hunks:
538
for line in hunk.lines:
539
if isinstance(line, RemoveLine):
540
next = rem_iter.next()
541
if line.contents != next:
542
sys.stdout.write(" orig:%spatch:%s" % (next,
544
assert(line.contents == next)
545
self.assertRaises(StopIteration, rem_iter.next)
547
def testFirstLineRenumber(self):
548
"""Make sure we handle lines at the beginning of the hunk"""
549
patch = parse_patch(self.datafile("insert_top.patch"))
550
assert (patch.pos_in_mod(0)==1)
553
patchesTestSuite = unittest.makeSuite(PatchesTester,'test')
554
runner = unittest.TextTestRunner(verbosity=0)
555
return runner.run(patchesTestSuite)
558
if __name__ == "__main__":
560
# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683