Blob Blame History Raw
#!/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]))