#!/usr/bin/env python # vim: set fileencoding=UTF-8: # Copyright 2013 Red Hat, Inc. # Author: Jan Pokorný # Licensed under MIT license import sys import re import getopt from os import environ, sep from os.path import join as path_join from subprocess import Popen, PIPE from threading import Thread # svn checkout http://python-patch.googlecode.com/svn/trunk/ python-patch # pushd python-patch # patch <../python-patch-allow-for-externalizing-hunk-again.patch # popd # ln -s python-patch/patch . import patch re_h = re.compile( r'^Hunk \#(\d+) succeeded at (\d+)(?: with fuzz (\d+))? \(offset (-?\d+) lines?\)\.$', re.MULTILINE ) use_threading = True #use_threading = False def adjustment(ret, stderrdata): print >>sys.stderr, "ret = {0}\nstderrdata = {1}".format(ret, stderrdata) for match in re_h.finditer(stderrdata): print >>sys.stderr, match.groups() ret = int(match.groups()[3]) break else: ret = 0 return ret def hunk_worker(hunk, header, tres, i, cmd, **kwargs): proc = Popen(cmd, **kwargs) str_hunk = str(hunk) partial_patch = header + '\n' + str_hunk _, stderrdata = proc.communicate(partial_patch) delta = adjustment(proc.wait(), stderrdata) tres[i] = delta def proceed(opts, args): if not args: args.append('-') expr_dict = {} popped = 0 rename = True for i, (o, a) in enumerate(opts[:]): if o == '--expr': expr = opts.pop(i - popped)[1] popped += 1 try: for filespec in expr.split('::'): f, rest = filespec.split(':', 1) f_spec = expr_dict.setdefault(f, []) f_spec.extend([map(int, x.split('/')) for x in rest.split(':')]) except: print >>sys.stderr, "invalid --expr specification" return 1 elif o == '-S': opts.pop(i - popped)[1] rename = False elif o == '-s': opts.pop(i - popped)[1] rename = True cmd_args = reduce(lambda a, x: a + list(x), opts, []) #print >>sys.stderr, "cmdargs: {0}, opts: {1}".format(cmd_args, opts) null = open('/dev/null', 'a') cmd = (lambda *a: a)('/usr/bin/patch', '-o-', *cmd_args) e = dict(LC='C') for name, value in environ.iteritems(): if name in ('TMPDIR', 'TMP', 'TEMP'): e[name] = value kwargs = dict(stdin=PIPE, stdout=null, stderr=PIPE, env=e) for arg in args: arg = arg if arg != '-' else '/dev/stdin' ps = patch.fromfile(arg) if not ps: print >>sys.stderr, "Bad patch file: {0}".format(arg) continue for p in ps.items: header, source, target = p.header, p.source, p.target if rename: source = path_join('a', *(source.split(sep)[1:])) target = path_join('b', *(target.split(sep)[1:])) header = map(lambda x: x.replace(' ' + p.source, ' ' + source) \ .replace(' ' + p.target, ' ' + target), header) header = '{0}--- {1}\n+++ {2}'.format('\n'.join(header), source, target) print header tres = [0 for hunk in p.hunks] if expr_dict: for f in expr_dict.keys(): if p.source[-len(f):] == f: for hunk, delta in expr_dict[f]: tres[hunk - 1] = delta break ts = [len(expr_dict) == 0 for hunk in p.hunks] misorder = [0 for hunk in p.hunks] tmax = 10 avg = 0.0 cnt = 0 avg_limit = 50.0 len_ts = len(ts) while any(ts): # crazy per-single-hunk matching logic (to get more control?) # not used if nonempty --expr specified # ts[i] = False if not to continue with that or bool(x) == True # otherwise (None is a temporary local state!) for i in xrange(len_ts): if not ts[i]: continue # XXX: should take total number of per-file-hunks into # account (start with 3+ if allowed?) if not use_threading: hunk_worker(p.hunks[i], header, tres, i, cmd, **kwargs) ts[i] = None continue if i >= tmax: blockers = filter(lambda (x, y): bool(y), enumerate(ts[:i])) while len(blockers) >= tmax: for ii, it in blockers: it.join(0.001) if not it.is_alive(): ts[ii] = None break else: continue break t = Thread(target=hunk_worker, args=(p.hunks[i], header, tres, i, cmd), kwargs=kwargs) ts[i] = t t.start() for i in xrange(len_ts): t = ts[i] if t and use_threading: t.join() ts[i] = None if ts[i] is None: avg = (avg * cnt + tres[i]) cnt += 1 avg /= cnt ts[i] = False for i in xrange(1, len_ts): hunk = p.hunks[i] if hunk.startsrc < p.hunks[i-1].startsrc: #+ p.hunks[i-1].linessrc: # "misordered hunks" guard #print >>sys.stderr, "Critical adjustment: {0}".format(i+1) if misorder[i-1] > 2: tres[i] = int(avg) else: misorder[i-1] += 1 delta = int(hunk.startsrc - (p.hunks[i-1].startsrc + misorder[i-1] * p.hunks[i-1].linessrc)) p.hunks[i-1].startsrc += delta if p.hunks[i-1].startsrc < 0: p.hunks[i-1].startsrc = 0 else: p.hunks[i-1].starttgt += delta if i < len_ts + 1: delta = -delta delta /= 2 hunk.startsrc += delta hunk.starttgt += delta for ii in xrange(i-1, len_ts): ts[ii] = True break else: misorder[i-1] = 0 ts[i] = ts[i] or abs(tres[i] - avg) > avg_limit ts[i] = ts[i] or abs(tres[i] - tres[i-1]) > (avg_limit/2) if i < len_ts - 1: ts[i] = ts[i] or abs(tres[i] - tres[i+1]) > (avg_limit/2) if ts[i]: delta = int(tres[i] + (tres[i] - avg) / 1.25) hunk.startsrc += delta hunk.starttgt += delta else: continue avg_limit *= 1.25 check_and_print_hunk(len_ts - 1, 999999, p.hunks, tres, misorder) null.close() def check_and_print_hunk(i, test, hunks, tres, misorder): """non-tail recursion to eval offset (hi->lo) + dump hunks (lo->hi) if ok""" # tail version seems not to be conveniently possible if i < 0: return delta = tres[i] tres[i] = 0 if misorder[i] < 2: hunk = hunks[i] if delta: hunk.startsrc += delta hunk.starttgt += delta str_hunk = str(hunk) check_and_print_hunk(i-1, min(hunk.startsrc, test), hunks, tres, misorder) if test <= hunk.startsrc: print >>sys.stderr, "Duplicate: {0}".format(str_hunk) else: print str_hunk, return if __name__ == '__main__': # see PATCH(1) opts, args = getopt.getopt(sys.argv[1:], 'p:f:' + 'sSh', ['strip=', 'fuzz='] + ['expr=']) if ('-h', '') in opts: print >>sys.stderr, "Usage: {0} [-{{sS}}] [--expr=EXPR] [PATCHOPTS] [PATCH]" \ .format(sys.argv[0]) print >>sys.stderr, " where EXPR is: FILE1:HUNK1/OFFSET1[:...][::FILE2:...]" print >>sys.stderr, " currently supported PATCHOPTS: [-{{p|-f}}] + counterparts" print >>sys.stderr, " and the PATCH can also be piped to stdin" print >>sys.stderr, "Options of internal use (can shadow the uncommon patch ones):" print >>sys.stderr, "--expr EXPR offset task specified by EXPR, see above" print >>sys.stderr, "-s paths in patch follows git diff (default)" print >>sys.stderr, "-S suppress paths standardization as per above" print >>sys.stderr, "-h this help screen" sys.exit(0) sys.exit(proceed(opts, args))