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