--- /dev/null
+#!/usr/bin/python
+# -*- mode:python; var: python-guess-indent:false; python-indent:4; -*-
+
+import os
+import sys
+import re
+from optparse import OptionParser
+
+modules_map = {
+ 'general' : [ 'build', 'tests', ],
+ 'server' : ['Monitor', 'MyPLC', 'PLCAPI', 'PLCRT', 'PLCWWW', 'PLEWWW', 'www-register-wizard',
+ 'PXEService', 'drupal', 'plcmdline',],
+ 'node': [ 'linux-2.6', 'util-vserver', 'util-vserver-pl', 'chopstix-L0',
+ 'BootCD', 'BootManager', 'BootstrapFS', 'VserverReference',
+ 'DistributedRateLimiting', 'Mom', 'PingOfDeath',
+ 'NodeManager', 'NodeManager-optin', 'NodeManager-topo',
+ 'NodeUpdate', 'CoDemux',
+ 'nodeconfig', 'pl_sshd',
+ 'libnl', 'pypcilib', 'pyplnet',],
+ 'wifi' : ['madwifi', 'PlanetBridge', 'wifiadmin', 'hostapd',],
+ 'emulation': ['dummynet_image', 'ipfw', ],
+ 'netflow' : [ 'fprobe-ulog', 'pf2gui', 'pf2monitor', 'pf2slice', 'iproute2', 'iptables', 'silk',],
+ 'sfa' : [ 'geniwrapper', 'xmlrspecs', 'pyopenssl', ],
+ 'vsys' : ['vsys', 'vsys-scripts', 'vsys-wrappers', 'inotify-tools'],
+ 'deprecated' : [ 'proper', 'libhttpd++', 'oombailout', 'ulogd', 'patchdep', 'pdelta',
+ 'sandbox', 'playground', 'infrastructure', 'util-python', 'vnetspec',
+ ],
+ }
+
+epoch='{2007-07-01}'
+
+class ModuleHistory:
+
+ def __init__ (self, name, options):
+ self.name=name
+ self.options=options
+ self.user_commits=[]
+ self.user_revs={}
+ self.current_rev=None
+ self.current_user=None
+
+ valid=re.compile('\Ar[0-9]+ \|')
+ tagging=re.compile('\ATagging|\ASetting tag')
+
+ @staticmethod
+ def sort ( (u1,c1), (u2,c2) ): return c2-c1
+
+ def record(self,user,rev):
+ try:
+ self.user_revs[user].append(rev)
+ except:
+ self.user_revs[user] = [rev]
+ self.current_rev=rev
+ self.current_user=user
+
+ def ignore(self):
+ if (not self.current_user) or (not self.current_rev): return
+ user_list=self.user_revs[self.current_user]
+ if len(user_list) >= 1 and user_list[-1] == self.current_rev:
+ user_list.pop()
+
+ def scan (self):
+ cmd = "svn log -r %s:%s http://svn.planet-lab.org/svn/%s " % (self.options.fromv,self.options.tov,self.name)
+ if self.options.verbose:
+ print 'running',cmd
+ f = os.popen(cmd)
+ for line in f:
+ if not self.valid.match(line):
+ # mostly ignore commit body, except for ignoring the current commit if -i is set
+ if self.options.ignore_tags and self.tagging.match(line):
+ # roll back these changes
+ self.ignore()
+ continue
+ fields = line.split('|')
+ fields = [field.strip() for field in fields]
+ [rev,user,ctime,size] = fields[:4]
+ self.record(user,rev)
+ # translate into a list of tuples
+ user_commits = [ (user,len(revs)) for (user,revs) in self.user_revs.items() ]
+ user_commits.sort(self.sort)
+ self.user_commits=user_commits
+
+ def show(self):
+ if len(self.user_commits) ==0: return
+ print '%s [%s-%s]'%(self.name,self.options.fromv,self.options.tov),
+ if self.options.ignore_tags:
+ print ' - Ignored tag commits'
+ else:
+ print ''
+ for (u,c) in self.user_commits:
+ print "\t",u,c
+
+class Aggregate:
+
+ def __init__ (self,options):
+ # key=user, value=commits
+ self.options=options
+ self.user_commits_dict={}
+ self.user_commits=[]
+
+ def merge (self, modulehistory):
+ for (u,c) in modulehistory.user_commits:
+ try:
+ self.user_commits_dict[u] += c
+ except:
+ self.user_commits_dict[u] = c
+
+ def sort (self):
+ user_commits = [ (u,c) for (u,c) in self.user_commits_dict.items() ]
+ user_commits.sort(ModuleHistory.sort)
+ self.user_commits=user_commits
+
+ def show(self):
+ print 'Overall',
+ if self.options.ignore_tags:
+ print ' - Ignored tag commits'
+ else:
+ print ''
+ for (u,c) in self.user_commits:
+ print "\t",u,c
+
+class Modules:
+
+ def __init__ (self,map):
+ self.map=map
+
+ def categories(self):
+ return self.map.keys()
+
+ def all_modules(self,categories=None):
+ if not categories: categories=self.categories()
+ elif not isinstance(categories,list): categories=[categories]
+ result=[]
+ for category in categories:
+ result += self.map[category]
+ return result
+
+ def locate (self,keywords):
+ result=[]
+ for kw in keywords:
+ if self.map.has_key(kw):
+ result += self.map[kw]
+ else:
+ result += [kw]
+ return result
+
+ def list(self,scope):
+ for (cat,mod_list) in self.map.items():
+ for mod in mod_list:
+ if mod in scope:
+ print cat,mod
+
+def main ():
+ usage="%prog [module_or_category ...]"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-f", "--from", action = "store", dest='fromv',
+ default = epoch, help = "The revision to start from, default %s"%epoch)
+ parser.add_option("-t", "--to", action = "store", dest='tov',
+ default = 'HEAD', help = "The revision to end with, default HEAD")
+ parser.add_option("-n","--no-aggregate", action='store_false',dest='aggregate',default=True,
+ help='Do not aggregate over modules')
+ parser.add_option("-v","--verbose", action='store_true',dest='verbose',default=False,
+ help='Run in verbose/debug mode')
+ parser.add_option("-i","--ignore-tags",action='store_true',dest='ignore_tags',
+ help='ignore commits related to tagging')
+ parser.add_option("-l","--list",action='store_true',dest='list_modules',
+ help='list available modules and categories')
+ # pass this to the invoked shell if any
+ (options, args) = parser.parse_args()
+
+ map=Modules(modules_map)
+ if not args:
+ modules=map.all_modules()
+ else:
+ modules=map.locate(args)
+
+ if options.list_modules:
+ map.list(modules)
+ return
+
+ if len(modules) <=1:
+ options.aggregate=False
+
+ aggregate = Aggregate(options)
+ for module in modules:
+ history=ModuleHistory(module,options)
+ history.scan()
+ history.show()
+ aggregate.merge(history)
+
+ if options.aggregate:
+ aggregate.sort()
+ aggregate.show()
+
+if __name__ == '__main__':
+ main()