summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Corbet <corbet@lwn.net>2013-07-23 14:02:26 -0600
committerJonathan Corbet <corbet@lwn.net>2013-07-23 14:02:26 -0600
commit4a30d22c6bf780b714b6fdfe46249c9ac1e8c049 (patch)
treea075b87eaaf954403caa5a920899951037a8a585
parent500b460c4683faeeefa6bf50afcc6a4f018b3b5d (diff)
A simple tool for generating changelog stats.
-rwxr-xr-xchangelogs273
1 files changed, 273 insertions, 0 deletions
diff --git a/changelogs b/changelogs
new file mode 100755
index 0000000..b0e2232
--- /dev/null
+++ b/changelogs
@@ -0,0 +1,273 @@
+#!/usr/bin/pypy
+# -*- python -*-
+#
+# Go munging through changelogs for interesting info.
+#
+# git log <whatever> | changelogs
+#
+from patterns import patterns
+import re, sys
+
+
+SavedLine = ''
+
+def getline(input):
+ global SavedLine
+ if SavedLine:
+ ret = SavedLine
+ SavedLine = ''
+ return ret
+ l = input.readline()
+ if l:
+ return l.rstrip()
+ return None
+
+def SaveLine(line):
+ global SavedLine
+ SavedLine = line
+
+#
+# A simple state machine based on where we are in the patch. The
+# first stuff we get is the header.
+#
+S_HEADER = 0
+#
+# Then comes the single-line description.
+#
+S_DESC = 1
+#
+# ...the full changelog...
+#
+S_CHANGELOG = 2
+#
+# ...and the tag section.
+#
+S_TAGS = 3
+S_DONE = 4
+
+#
+# The functions to handle each of these states.
+#
+def get_header(patch, line, input):
+ if line == '':
+ return S_DESC
+ m = patterns['author'].match(line)
+ if m:
+ patch.author = m.group(1)
+ return S_HEADER
+
+def get_desc(patch, line, input):
+ if not line:
+ print 'Missing desc in', patch.commit
+ return S_CHANGELOG
+ patch.desc = line
+ #print 'desc', patch.desc
+ line = getline(input)
+ if line:
+ print 'Weird post-desc line in', patch.commit
+ return S_CHANGELOG
+
+tagline = re.compile(r'^\s+(([-a-z]+-by)|cc):.*@.*$', re.I)
+def get_changelog(patch, line, input):
+ #print 'cl', line
+ if not line:
+ if patch.templog:
+ patch.changelog += patch.templog
+ patch.templog = ''
+ if patterns['commit'].match(line):
+ # No changelog at all - usually a Linus tag
+ SaveLine(line)
+ return S_DONE
+ elif tagline.match(line):
+ if patch.templog:
+ patch.changelog += patch.templog
+ return get_tag(patch, line, input)
+ else:
+ patch.templog += line + '\n'
+ return S_CHANGELOG
+
+def get_tag(patch, line, input):
+ #
+ # Some people put blank lines in the middle of tags.
+ #
+ #print 'tag', line
+ if not line:
+ return S_TAGS
+ #
+ # A new commit line says we've gone too far.
+ #
+ if patterns['commit'].match(line):
+ SaveLine(line)
+ return S_DONE
+ m = patterns['signed-off-by'].match(line)
+ if m:
+ patch.signoffs.append(m.group(2))
+ else:
+ #
+ # Look for other tags indicating that somebody at least
+ # looked at the patch.
+ #
+ for tag in ('acked-by', 'reviewed-by', 'tested-by'):
+ if patterns[tag].match(line):
+ patch.othertags += 1
+ break
+ return S_TAGS
+
+grabbers = [ get_header, get_desc, get_changelog, get_tag ]
+
+
+#
+# A variant on the gitdm patch class.
+#
+class patch:
+ def __init__(self, commit):
+ self.commit = commit
+ self.desc = ''
+ self.changelog = ''
+ self.templog = ''
+ self.author = ''
+ self.signoffs = [ ]
+ self.othertags = 0
+
+def grabpatch(input):
+ #
+ # If it's not a patch something is screwy.
+ #
+ line = getline(input)
+ if line is None:
+ return None
+ m = patterns['commit'].match(line)
+ if not m:
+ print 'noncommit', line
+ return None
+ p = patch(m.group(1))
+ state = S_HEADER
+ #
+ # Crank through the patch.
+ #
+ while state != S_DONE:
+ line = getline(input)
+ if line is None:
+ if state != S_TAGS:
+ print 'Ran out of patch'
+ return p
+ state = grabbers[state](p, line, input)
+ return p
+
+#
+# Stats gathering.
+#
+EmptyCulprits = { }
+SingleSSCulprits = { }
+NoSOBCulprits = { }
+
+def LogEmptyCulprit(culprit):
+ try:
+ EmptyCulprits[culprit] += 1
+ except KeyError:
+ EmptyCulprits[culprit] = 1
+
+def LogSSCulprit(culprit):
+ try:
+ SingleSSCulprits[culprit] += 1
+ except KeyError:
+ SingleSSCulprits[culprit] = 1
+
+def LogNoSOBCulprit(culprit):
+ try:
+ NoSOBCulprits[culprit] += 1
+ except KeyError:
+ NoSOBCulprits[culprit] = 1
+
+def SortedCulprits(culprits):
+ def compare(c1, c2):
+ return culprits[c2] - culprits[c1]
+ names = culprits.keys()
+ names.sort(compare)
+ return names
+
+def PrintCulprits(culprits, sorted):
+ for name in sorted:
+ print '\t%30s: %d' % (name, culprits[name])
+#
+# Patch logging
+#
+SSPatches = { }
+EmptyCLPatches = { }
+NoSOBPatches = { }
+
+def LogSSPatch(p):
+ LogSSCulprit(p.author)
+ try:
+ SSPatches[p.author].append(p)
+ except KeyError:
+ SSPatches[p.author] = [p]
+
+def LogECLPatch(p):
+ LogEmptyCulprit(p.author)
+ try:
+ EmptyCLPatches[p.author].append(p)
+ except KeyError:
+ EmptyCLPatches[p.author] = [p]
+
+def LogNoSOB(p):
+ LogNoSOBCulprit(p.author)
+ try:
+ NoSOBPatches[p.author].append(p)
+ except KeyError:
+ NoSOBPatches[p.author] = [p]
+
+LinusURL = 'http://git.kernel.org/linus/'
+def WritePatches(names, patches, file):
+ out = open(file, 'w')
+ for name in names:
+ out.write('<h4>%s</h4>\n<ul>\n' % name)
+ for p in patches[name]:
+ out.write('\t<li><a href="%s%s"><tt>%s</tt></a> %s\n' % (LinusURL,
+ p.commit,
+ p.commit,
+ p.desc))
+ out.write('</ul>\n\n')
+ out.close()
+
+#
+# Main program.
+#
+Npatches = 0
+NemptyCL = 0
+
+Nsinglesob = 0
+Nnosob = 0
+
+p = grabpatch(sys.stdin)
+while p:
+ #print p.commit, len(p.changelog)
+ Npatches += 1
+ if len(p.changelog) == 0:
+ NemptyCL += 1
+ LogECLPatch(p)
+ if len(p.signoffs) == 0:
+ Nnosob += 1
+ LogNoSOB(p)
+ elif len(p.signoffs) == 1 and p.othertags == 0:
+ Nsinglesob += 1
+ LogSSPatch(p)
+ p = grabpatch(sys.stdin)
+
+print '%d patches, %d w/o changelog' % (Npatches, NemptyCL)
+print ' %d w/o signoff, %d w/1 signoff, %d SS culprits' % (Nnosob, Nsinglesob,
+ len(SingleSSCulprits))
+print '\nMost single signoffs:'
+sorted = SortedCulprits(SingleSSCulprits)[:20]
+PrintCulprits(SingleSSCulprits, sorted)
+WritePatches(sorted, SSPatches, 'sspatches.html')
+
+print '\nMost empty changelogs:'
+sorted = SortedCulprits(EmptyCulprits)[:20]
+PrintCulprits(EmptyCulprits, sorted)
+WritePatches(sorted, EmptyCLPatches, 'emptypatches.html')
+
+print '\nNoSOB:'
+sorted = SortedCulprits(NoSOBCulprits)
+PrintCulprits(NoSOBCulprits, sorted)
+WritePatches(sorted, NoSOBPatches, 'nosobpatches.html')