patchupdate.py: Improve patch system and remove dependencies, when the order of patches doesn't matter.

This commit is contained in:
Sebastian Lackner
2014-07-25 16:39:08 +02:00
parent 80e51dcad6
commit e827cc078f
9 changed files with 212 additions and 233 deletions

View File

@@ -5,50 +5,20 @@ import itertools
import difflib
import subprocess
import hashlib
import collections
class GeneralPatchError(RuntimeError):
pass
class PatchParserError(GeneralPatchError):
class PatchParserError(RuntimeError):
"""Unable to parse patch file - either an unimplemented feature, or corrupted patch."""
pass
class PatchApplyError(GeneralPatchError):
class PatchApplyError(RuntimeError):
"""Failed to apply/merge patch."""
pass
class Patch(object):
def __init__(self):
self.extracted_patch = None
def is_empty(self):
raise NotImplementedError("is_empty not implemented.")
def read_chunks(self):
raise NotImplementedError("read_chunks not implemented.")
def read_hunks(self, reverse=False):
raise NotImplementedError("read_hunks not implemented.")
def extract(self):
"""Create a temporary file containing the extracted patch."""
if not self.extracted_patch:
self.extracted_patch = tempfile.NamedTemporaryFile()
for chunk in self.read_chunks():
self.extracted_patch.write(chunk)
self.extracted_patch.flush()
return self.extracted_patch
def hash(self):
m = hashlib.md5()
for srcpos, srchunk, dsthunk in self.read_hunks():
m.update(hashlib.md5("\n".join(srchunk)).digest())
m.update(hashlib.md5("\n".join(dsthunk)).digest())
return m.digest()
class FilePatch(Patch):
class PatchObject(object):
def __init__(self, filename):
super(FilePatch, self).__init__()
self.extracted_patch = None
self.unique_hash = None
self.filename = filename
self.offset_begin = None
@@ -62,15 +32,13 @@ class FilePatch(Patch):
self.newsha1 = None
self.newmode = None
self.hunks = [] # offset, srcpos, srclines, dstpos, dstlines
self.isbinary = False
self.binary_patch_offset = None
self.binary_patch_type = None
self.binary_patch_size = None
def is_empty(self):
return len(self.hunks) == 0
def is_binary(self):
return self.isbinary
def read_chunks(self):
"""Iterates over arbitrary sized chunks of this patch."""
@@ -84,155 +52,73 @@ class FilePatch(Patch):
yield buf
i -= len(buf)
def read_hunks(self, reverse=False):
"""Iterates over hunks contained in this patch, reverse exchanges source and destination."""
if self.isbinary:
raise GeneralPatchError("Command not allowed for binary patches.")
def extract(self):
"""Create a temporary file containing the extracted patch."""
if not self.extracted_patch:
self.extracted_patch = tempfile.NamedTemporaryFile()
for chunk in self.read_chunks():
self.extracted_patch.write(chunk)
self.extracted_patch.flush()
return self.extracted_patch
with open(self.filename) as fp:
for offset, srcpos, srclines, dstpos, dstlines in self.hunks:
fp.seek(offset)
srchunk = []
dsthunk = []
while srclines > 0 or dstlines > 0:
line = fp.readline()
if line == "":
raise PatchParserError("Truncated patch.")
line = line.rstrip("\r\n")
if line.startswith(" "):
if srclines == 0 or dstlines == 0:
raise PatchParserError("Corrupted patch.")
srchunk.append(line[1:])
dsthunk.append(line[1:])
srclines -= 1
dstlines -= 1
elif line.startswith("-"):
if srclines == 0:
raise PatchParserError("Corrupted patch.")
srchunk.append(line[1:])
srclines -= 1
elif line.startswith("+"):
if dstlines == 0:
raise PatchParserError("Corrupted patch.")
dsthunk.append(line[1:])
dstlines -= 1
elif line.startswith("\\ "):
pass # ignore
else:
raise PatchParserError("Unexpected line in hunk.")
if reverse:
yield dstpos, dsthunk, srchunk
else:
yield srcpos, srchunk, dsthunk
class MemoryPatch(Patch):
def __init__(self):
super(MemoryPatch, self).__init__()
self.oldname = None
self.newname = None
self.modified_file = None
self.hunks = [] # srcpos, srclines, dstpos, dstlines, lines
def is_empty(self):
return len(self.hunks) == 0
def read_chunks(self):
"""Iterates over arbitrary sized chunks of this patch."""
if self.oldname is None or self.newname is None:
raise GeneralPatchError("Patch doesn't have old or new name.")
yield "diff --git a/%s b/%s\n" % (self.oldname, self.newname)
yield "--- a/%s\n" % self.oldname
yield "+++ b/%s\n" % self.newname
for srcpos, srclines, dstpos, dstlines, hunk in self.hunks:
yield "@@ -%d,%d +%d,%d @@\n" % (srcpos+1, srclines, dstpos+1, dstlines)
for mode, line in hunk:
if mode == 0:
yield " %s\n" % line
elif mode == 1:
yield "+%s\n" % line
elif mode == 2:
yield "-%s\n" % line
def read_hunks(self, reverse=False):
"""Iterates over hunks contained in this patch, reverse exchanges source and destination."""
for srcpos, srclines, dstpos, dstlines, hunk in self.hunks:
srchunk = []
dsthunk = []
for mode, line in hunk:
if mode == 0:
srchunk.append(line)
dsthunk.append(line)
elif mode == 1:
dsthunk.append(line)
elif mode == 2:
srchunk.append(line)
assert srclines == len(srchunk)
assert dstlines == len(dsthunk)
if reverse:
yield dstpos, dsthunk, srchunk
else:
yield srcpos, srchunk, dsthunk
class FileReader(object):
def __init__(self, filename):
self.filename = filename
self.fp = open(self.filename)
self.peeked = None
def close(self):
self.fp.close()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
def seek(self, pos):
"""Change the file cursor position."""
self.fp.seek(pos)
self.peeked = None
def tell(self):
"""Return the current file cursor position."""
if self.peeked is None:
return self.fp.tell()
return self.peeked[0]
def peek(self):
"""Read one line without changing the file cursor."""
if self.peeked is None:
pos = self.fp.tell()
tmp = self.fp.readline()
if len(tmp) == 0: return None
self.peeked = (pos, tmp)
return self.peeked[1]
def read(self):
"""Read one line from the file, and move the file cursor to the next line."""
if self.peeked is None:
tmp = self.fp.readline()
if len(tmp) == 0: return None
return tmp
tmp, self.peeked = self.peeked, None
return tmp[1]
def hash(self):
"""Hash the content of the patch."""
if not self.unique_hash:
m = hashlib.sha256()
for chunk in self.read_chunks():
m.update(chunk)
self.unique_hash = m.digest()
return self.unique_hash
def read_patch(filename):
"""Iterates over all patches contained in a file, and returns FilePatch objects."""
"""Iterates over all patches contained in a file, and returns PatchObject objects."""
class _FileReader(object):
def __init__(self, filename):
self.filename = filename
self.fp = open(self.filename)
self.peeked = None
def close(self):
self.fp.close()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
def seek(self, pos):
"""Change the file cursor position."""
self.fp.seek(pos)
self.peeked = None
def tell(self):
"""Return the current file cursor position."""
if self.peeked is None:
return self.fp.tell()
return self.peeked[0]
def peek(self):
"""Read one line without changing the file cursor."""
if self.peeked is None:
pos = self.fp.tell()
tmp = self.fp.readline()
if len(tmp) == 0: return None
self.peeked = (pos, tmp)
return self.peeked[1]
def read(self):
"""Read one line from the file, and move the file cursor to the next line."""
if self.peeked is None:
tmp = self.fp.readline()
if len(tmp) == 0: return None
return tmp
tmp, self.peeked = self.peeked, None
return tmp[1]
def _read_single_patch(fp, oldname=None, newname=None):
patch = FilePatch(fp.filename)
patch = PatchObject(fp.filename)
patch.offset_begin = fp.tell()
patch.oldname = oldname
patch.newname = newname
@@ -313,7 +199,6 @@ def read_patch(filename):
raise PatchParserError("Empty hunk doesn't make sense.")
assert fp.read() == line
patch.hunks.append(( fp.tell(), srcpos, srclines, dstpos, dstlines ))
while srclines > 0 or dstlines > 0:
line = fp.read()
if line is None:
@@ -370,7 +255,7 @@ def read_patch(filename):
patch.offset_end = fp.tell()
return patch
with FileReader(filename) as fp:
with _FileReader(filename) as fp:
while True:
line = fp.peek()
if line is None:
@@ -386,34 +271,34 @@ def read_patch(filename):
else:
assert fp.read() == line
def apply_patch(lines, patch, reverse=False, fuzz=2):
def apply_patch(content, patches, reverse=False, fuzz=2):
"""Apply a patch with optional fuzz - uses the commandline 'patch' utility."""
if patch.is_empty():
return lines
if not isinstance(patches, collections.Sequence):
patches = [patches]
contentfile = tempfile.NamedTemporaryFile(delete=False)
try:
for line in lines:
contentfile.write("%s\n" % line)
contentfile.write(content)
contentfile.close()
patchfile = patch.extract()
cmdline = ["patch", "--batch", "-r", "-"] # "--silent" ?
if reverse: cmdline.append("--reverse")
if fuzz != 2: cmdline.append("--fuzz=%d" % fuzz)
cmdline += [contentfile.name, patchfile.name]
for patch in patches:
exitcode = subprocess.call(cmdline)
if exitcode != 0:
raise PatchApplyError("Failed to apply patch (exitcode %d)." % exitcode)
patchfile = patch.extract()
cmdline = ["patch", "--batch", "--silent", "-r", "-"]
if reverse: cmdline.append("--reverse")
if fuzz != 2: cmdline.append("--fuzz=%d" % fuzz)
cmdline += [contentfile.name, patchfile.name]
exitcode = subprocess.call(cmdline)
if exitcode != 0:
raise PatchApplyError("Failed to apply patch (exitcode %d)." % exitcode)
with open(contentfile.name) as fp:
lines = fp.read().split("\n")
if lines[-1] == "": lines.pop()
content = fp.read()
finally:
os.unlink(contentfile.name)
return lines
return content