#!/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]))