278
296
patch.hunks.append(hunk)
281
def parse_patches(lines):
284
return [ parse_patch(lines.__iter__()) ]
288
iter_lines = lines.__iter__()
300
def iter_file_patch(iter_lines):
292
try: line = iter_lines.next()
293
except StopIteration:
294
patches.extend(parse(saved_lines))
302
for line in iter_lines:
297
303
if line.startswith('*** '):
298
patches.extend(parse(saved_lines))
305
if line.startswith('==='):
307
elif line.startswith('--- '):
308
if len(saved_lines) > 0:
301
elif line.startswith('--- ') and len(saved_lines) > 1:
302
patches.extend(parse(saved_lines))
303
saved_lines = [ line ]
306
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