diff --git a/scripts/patch-report b/scripts/patch-report new file mode 100755 index 0000000..694ca6d --- /dev/null +++ b/scripts/patch-report @@ -0,0 +1,239 @@ +#!/usr/bin/env python +# +# prepares a report on the patches in a series file that do not yet have +# mainline tags. +# +# common usage: patch-report -s summary -r report < series +# +# -s specifies a file where a summary of patches per user is printed +# -r is a report with details about each patch +# -t does guards style tagging in the report of patches that can be reversed +# -f tries to reverse with rej. This is very slow. +# -u prints only details on patches corresponding to a specific user. +# this is everything before the @ sign in the email. +# +# This can try to reverse all the untagged patches, use -p source-dir to +# specify the directory where it should try to reverse things. + +# +# + +import os, sys, select, popen2, re +from optparse import OptionParser + +# counts the number of times we find a given username. We only +# consider emails from @suse and @novell, and don't include the domain +# name in the counting (so mason@suse.de and mason@suse.com are the same) +def countuser(s, users, foundusers, emailre): + found = [] + for m in emailre.finditer(s): + u = m.group(1) + addr = m.group(2) + if not addr.startswith('@suse') and not addr.startswith('@novell'): + continue + if u in foundusers: + continue + foundusers[u] = 1 + n = users.get(u, 0) + users[u] = n + 1 + found.append(m.group(0)) + return found + + +parser = OptionParser(usage="usage: %prog [options]") +parser.add_option("-u", "--user", help="find a specific user", default="") +parser.add_option("-r", "--report-file", help="report output file", default="") +parser.add_option("-v", "--verbose", help="verbose", action="store_true", + default=False) +parser.add_option("-t", "--tag", help="tag patches in report", + action="store_true", default=False) +parser.add_option("-f", "--rej", help="try using rej", action="store_true", + default=False) +parser.add_option("-p", "--patch-dir", help="try to reverse patches in path", + default="") +parser.add_option("-s", "--summary-file", help="summary output file", + default="") +(options, args) = parser.parse_args() + +# start a two way connection with xargs patch-tag. +pipeout, pipein = popen2.popen2("xargs patch-tag -p From -p Signed-off-by -p Acked-by -p Patch-mainline -a Patch-mainline=empty -p Subject") +readers = [ sys.stdin, pipeout ] +writers = [ pipein ] +writeq = [] +stdindone = False +patches = {} + +# records details about all the patches +patchinfo = {} + +# keeps the output in the same order as the series read over stdin +patchorder = [] +# a count of the patches found for each user +users = {} +emailre = re.compile(r"([\w\.]+)(@[\w\.]+)") + +goodwords = [ '2.6', 'yes', 'obsolete', 'never' ] +badwords = ['-mm', 'no', 'empty' ] + +# ugly select loop to talk with patch-tag +while readers: + (r, w, x) = select.select(readers, writers, []) + for f in r: + if f == sys.stdin: + l = f.readline() + if l: + writeq.append(l) + else: + del readers[0] + stdindone = True + if len(writeq) == 0: + pipein.close() + writers = [] + elif f == pipeout: + line = f.readline().rstrip() + l = line.split() + if l: + # the format is: + # file: Patch-mainline: data + # data may be empty + p = l[0].rstrip(':') + if len(l) <= 1: + continue + htype = l[1].rstrip(':') + + if len(l) < 3: + t = 'empty' + else: + t = " ".join(l[2:]) + patchinfo.setdefault(p, {}).setdefault(htype, []).append(t) + if htype != 'Patch-mainline': + continue + good = False + t = t.lower() + for x in goodwords: + if x in t: + good = True + break + for x in badwords: + # For example, 2.6.16-mm2 is bad + if x in t: + good = False + break + if not good: + patches[p] = t + patchorder.append(p) + else: + del readers[0] + + if w and writeq: + w[0].write(writeq[0]) + del writeq[0] + if stdindone and len(writeq) == 0: + pipein.close() + writers = [] + +if options.report_file: + try: + outf = file(options.report_file, "w") + except IOError: + sys.stderr.write("unable to open %s for writing\n" % + options.report_file); + sys.exit(1) +else: + outf = sys.stdout + +# optionally try to figure out which patches we can reverse +if options.patch_dir: + for i in xrange(len(patchorder)-1, -1, -1): + p = patchorder[i] + fuzz = 0 + failed = 0 + files = 0 + reject_files = 0 + # we want to be smart about counting the failed hunks + patchf = os.popen("patch -f -p1 -d%s -R < '%s'" % + (options.patch_dir, p)) + for l in patchf: + if options.verbose: + sys.stderr.write(l) + l = l.rstrip('\r\n') + if l[:14] == 'patching file ': + files += 1 + elif l.find('saving rejects to file') >= 0: + reject_files += 1 + elif l.find('FAILED at') >= 0: + failed += 1 + elif l.find('with fuzz') >= 0: + fuzz += 1 + + patcherr = patchf.close() + if failed == 0 and patcherr == None: + if fuzz: + patchinfo[p]['Reverse'] = 'fuzzy' + else: + patchinfo[p]['Reverse'] = 'yes' + else: + str = "hunks failed %d fuzzy %d files %d reject files %d" % (failed, + fuzz, files, reject_files) + if options.rej: + patchpath = os.path.abspath(p) + ret = os.system("cd '%s' ; rej -R -a --dry-run -F -M -p 1 '%s'" + % (options.patch_dir, patchpath)) + if ret == 0: + str = "rej resolved " + str + patchinfo[p]['Reverse'] = str + +for p in patchorder: + h = patchinfo[p] + foundusers = {} + if options.user: + good = False + for x in ['Acked-by', 'Signed-off-by', 'From']: + if options.user in "".join(h.get(x, [])): + good = True + break + if not good: + continue + tag = "+nag " + # always print From and Subject. Only print acked-by or signed-off-by + # if it is a suse/novell person + if 'From' in h: + l = " ".join(h['From']) + outf.write("# From: %s\n" % l) + countuser(l, users, foundusers, emailre) + if 'Subject' in h: + outf.write("# Subject: %s\n" % " ".join(h['Subject'])) + for x in ['Acked-by', 'Signed-off-by']: + if x in h: + l = " ".join(h[x]) + found = countuser(l, users, foundusers, emailre) + if found: + outf.write("# %s: %s\n" % (x, " ".join(found))) + if 'Reverse' in h: + t = h['Reverse'] + if t == 'yes': + tag = "+reverse " + elif t == 'fuzzy': + tag = "+reverse-fuzzy " + outf.write("# Reverse: %s\n" % h['Reverse']) + if options.tag: + p = tag + p + outf.write("%s\n\n" % (p)) + +if options.summary_file: + try: + outf = file(options.summary_file, "w") + except IOError: + sys.stderr.write("unable to open %s for writing\n" % + options.report_file); + sys.exit(1) +else: + outf = sys.stdout +userk = users.keys() +userk.sort() +outf.write("Total untagged patches: %d\n" % len(patchorder)) +for u in userk: + if options.user and options.user not in u: + continue + outf.write("%s: %d\n" % (u, users[u])) +