python3
[build.git] / module-log.py
1 #!/usr/bin/python3
2
3 import os
4 import sys
5 import re
6 from optparse import OptionParser
7
8 modules_map = {
9     'general': ['build', 'tests', ],
10     'server': ['Monitor', 'MyPLC', 'PLCAPI', 'PLCRT', 'PLCWWW', 'PLEWWW', 'www-register-wizard',
11                'PXEService', 'drupal', 'plcmdline', ],
12     'node': ['linux-2.6', 'util-vserver', 'util-vserver-pl', 'chopstix-L0',
13              'BootCD', 'BootManager', 'BootstrapFS', 'VserverReference',
14              'DistributedRateLimiting', 'Mom', 'PingOfDeath',
15              'NodeManager', 'NodeManager-optin', 'NodeManager-topo',
16              'NodeUpdate', 'CoDemux',
17              'nodeconfig', 'pl_sshd',
18              'libnl', 'pypcilib', 'pyplnet', ],
19     'wifi': ['madwifi', 'PlanetBridge', 'hostapd', ],
20     'emulation': ['dummynet_image', 'ipfw', ],
21     'netflow': ['fprobe-ulog', 'pf2gui', 'pf2monitor', 'pf2slice', 'iproute2', 'iptables', 'silk', ],
22     'sfa': ['sfa', 'xmlrspecs', 'pyopenssl', ],
23     'vsys': ['vsys', 'vsys-scripts', 'vsys-wrappers', 'inotify-tools'],
24     'deprecated': ['proper', 'libhttpd++', 'oombailout', 'ulogd', 'patchdep', 'pdelta',
25                    'sandbox', 'playground', 'infrastructure', 'util-python', 'vnetspec',
26                    ],
27 }
28
29 epoch = '{2007-07-01}'
30
31
32 class ModuleHistory:
33
34     def __init__(self, name, options):
35         self.name = name
36         self.options = options
37         self.user_commits = []
38         self.user_revs = {}
39         self.current_rev = None
40         self.current_user = None
41
42     valid = re.compile(r'\Ar[0-9]+ \|')
43     tagging = re.compile(r'\ATagging|\ASetting tag')
44
45     @staticmethod
46     def sort_key(u, c):
47         return c
48
49     def record(self, user, rev):
50         try:
51             self.user_revs[user].append(rev)
52         except:
53             self.user_revs[user] = [rev]
54         self.current_rev = rev
55         self.current_user = user
56
57     def ignore(self):
58         if (not self.current_user) or (not self.current_rev):
59             return
60         user_list = self.user_revs[self.current_user]
61         if len(user_list) >= 1 and user_list[-1] == self.current_rev:
62             user_list.pop()
63
64     def scan(self):
65         cmd = "svn log -r %s:%s http://svn.planet-lab.org/svn/%s " % (
66             self.options.fromv, self.options.tov, self.name)
67         if self.options.verbose:
68             print('running', cmd)
69         f = os.popen(cmd)
70         for line in f:
71             if not self.valid.match(line):
72                 # mostly ignore commit body, except for ignoring the current commit if -i is set
73                 if self.options.ignore_tags and self.tagging.match(line):
74                     # roll back these changes
75                     self.ignore()
76                 continue
77             fields = line.split('|')
78             fields = [field.strip() for field in fields]
79             [rev, user, ctime, size] = fields[:4]
80             self.record(user, rev)
81         # translate into a list of tuples
82         user_commits = [(user, len(revs))
83                         for (user, revs) in list(self.user_revs.items())]
84         user_commits.sort(key=self.sort_key)
85         self.user_commits = user_commits
86
87     def show(self):
88         if len(self.user_commits) == 0:
89             return
90         print('%s [%s-%s]' %
91               (self.name, self.options.fromv, self.options.tov), end=' ')
92         if self.options.ignore_tags:
93             print(' - Ignored tag commits')
94         else:
95             print('')
96         for (u, c) in self.user_commits:
97             print("\t", u, c)
98
99
100 class Aggregate:
101
102     def __init__(self, options):
103         # key=user, value=commits
104         self.options = options
105         self.user_commits_dict = {}
106         self.user_commits = []
107
108     def merge(self, modulehistory):
109         for (u, c) in modulehistory.user_commits:
110             try:
111                 self.user_commits_dict[u] += c
112             except:
113                 self.user_commits_dict[u] = c
114
115     def sort(self):
116         user_commits = [(u, c)
117                         for (u, c) in list(self.user_commits_dict.items())]
118         user_commits.sort(ModuleHistory.sort)
119         self.user_commits = user_commits
120
121     def show(self):
122         print('Overall', end=' ')
123         if self.options.ignore_tags:
124             print(' - Ignored tag commits')
125         else:
126             print('')
127         for (u, c) in self.user_commits:
128             print("\t", u, c)
129
130
131 class Modules:
132
133     def __init__(self, map):
134         self.map = map
135
136     def categories(self):
137         return list(self.map.keys())
138
139     def all_modules(self, categories=None):
140         if not categories:
141             categories = self.categories()
142         elif not isinstance(categories, list):
143             categories = [categories]
144         result = []
145         for category in categories:
146             result += self.map[category]
147         return result
148
149     def locate(self, keywords):
150         result = []
151         for kw in keywords:
152             if kw in self.map:
153                 result += self.map[kw]
154             else:
155                 result += [kw]
156         return result
157
158     def list(self, scope):
159         for (cat, mod_list) in list(self.map.items()):
160             for mod in mod_list:
161                 if mod in scope:
162                     print(cat, mod)
163
164
165 def main():
166     usage = "%prog [module_or_category ...]"
167     parser = OptionParser(usage=usage)
168     parser.add_option("-f", "--from", action="store", dest='fromv',
169                       default=epoch, help="The revision to start from, default %s" % epoch)
170     parser.add_option("-t", "--to", action="store", dest='tov',
171                       default='HEAD', help="The revision to end with, default HEAD")
172     parser.add_option("-n", "--no-aggregate", action='store_false', dest='aggregate', default=True,
173                       help='Do not aggregate over modules')
174     parser.add_option("-v", "--verbose", action='store_true', dest='verbose', default=False,
175                       help='Run in verbose/debug mode')
176     parser.add_option("-i", "--ignore-tags", action='store_true', dest='ignore_tags',
177                       help='ignore commits related to tagging')
178     parser.add_option("-l", "--list", action='store_true', dest='list_modules',
179                       help='list available modules and categories')
180     # pass this to the invoked shell if any
181     (options, args) = parser.parse_args()
182
183     map = Modules(modules_map)
184     if not args:
185         modules = map.all_modules()
186     else:
187         modules = map.locate(args)
188
189     if options.list_modules:
190         map.list(modules)
191         return
192
193     if len(modules) <= 1:
194         options.aggregate = False
195
196     aggregate = Aggregate(options)
197     for module in modules:
198         history = ModuleHistory(module, options)
199         history.scan()
200         history.show()
201         aggregate.merge(history)
202
203     if options.aggregate:
204         aggregate.sort()
205         aggregate.show()
206
207
208 if __name__ == '__main__':
209     main()