List of Authors: added "# by commits" column.
[gitstats.git] / gitstats
blob90a70fabc2a5b36e410ba199feaf27aef85e1dba
1 #!/usr/bin/env python
2 # Copyright (c) 2007 Heikki Hokkanen <hoxu@users.sf.net>
3 # GPLv2
4 import commands
5 import datetime
6 import glob
7 import os
8 import re
9 import shutil
10 import sys
11 import time
13 GNUPLOT_COMMON = 'set terminal png transparent\nset size 0.5,0.5\n'
15 exectime_internal = 0.0
16 exectime_external = 0.0
17 time_start = time.time()
19 def getoutput(cmd, quiet = False):
20 global exectime_external
21 start = time.time()
22 if not quiet:
23 print '>> %s' % cmd,
24 sys.stdout.flush()
25 output = commands.getoutput(cmd)
26 end = time.time()
27 if not quiet:
28 print '\r[%.5f] >> %s' % (end - start, cmd)
29 exectime_external += (end - start)
30 return output
32 def getkeyssortedbyvalues(dict):
33 return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
35 # dict['author'] = { 'commits': 512 } - ...key(dict, 'commits')
36 def getkeyssortedbyvaluekey(d, key):
37 return map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys())))
39 class DataCollector:
40 """Manages data collection from a revision control repository."""
41 def __init__(self):
42 self.stamp_created = time.time()
43 pass
46 # This should be the main function to extract data from the repository.
47 def collect(self, dir):
48 self.dir = dir
49 self.projectname = os.path.basename(os.path.abspath(dir))
52 # : get a dictionary of author
53 def getAuthorInfo(self, author):
54 return None
56 def getActivityByDayOfWeek(self):
57 return {}
59 def getActivityByHourOfDay(self):
60 return {}
63 # Get a list of authors
64 def getAuthors(self):
65 return []
67 def getFirstCommitDate(self):
68 return datetime.datetime.now()
70 def getLastCommitDate(self):
71 return datetime.datetime.now()
73 def getStampCreated(self):
74 return self.stamp_created
76 def getTags(self):
77 return []
79 def getTotalAuthors(self):
80 return -1
82 def getTotalCommits(self):
83 return -1
85 def getTotalFiles(self):
86 return -1
88 def getTotalLOC(self):
89 return -1
91 class GitDataCollector(DataCollector):
92 def collect(self, dir):
93 DataCollector.collect(self, dir)
95 self.total_authors = int(getoutput('git-log |git-shortlog -s |wc -l'))
96 #self.total_lines = int(getoutput('git-ls-files -z |xargs -0 cat |wc -l'))
98 self.activity_by_hour_of_day = {} # hour -> commits
99 self.activity_by_day_of_week = {} # day -> commits
100 self.activity_by_month_of_year = {} # month [1-12] -> commits
101 self.activity_by_hour_of_week = {} # weekday -> hour -> commits
103 self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp}
105 # author of the month
106 self.author_of_month = {} # month -> author -> commits
107 self.author_of_year = {} # year -> author -> commits
108 self.commits_by_month = {} # month -> commits
109 self.commits_by_year = {} # year -> commits
110 self.first_commit_stamp = 0
111 self.last_commit_stamp = 0
113 # tags
114 self.tags = {}
115 lines = getoutput('git-show-ref --tags').split('\n')
116 for line in lines:
117 if len(line) == 0:
118 continue
119 (hash, tag) = line.split(' ')
120 tag = tag.replace('refs/tags/', '')
121 output = getoutput('git-log "%s" --pretty=format:"%%at %%an" -n 1' % hash)
122 if len(output) > 0:
123 parts = output.split(' ')
124 stamp = 0
125 try:
126 stamp = int(parts[0])
127 except ValueError:
128 stamp = 0
129 self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d') }
130 pass
132 # Collect revision statistics
133 # Outputs "<stamp> <author>"
134 lines = getoutput('git-rev-list --pretty=format:"%at %an" HEAD |grep -v ^commit').split('\n')
135 for line in lines:
136 # linux-2.6 says "<unknown>" for one line O_o
137 parts = line.split(' ')
138 author = ''
139 try:
140 stamp = int(parts[0])
141 except ValueError:
142 stamp = 0
143 if len(parts) > 1:
144 author = ' '.join(parts[1:])
145 date = datetime.datetime.fromtimestamp(float(stamp))
147 # First and last commit stamp
148 if self.last_commit_stamp == 0:
149 self.last_commit_stamp = stamp
150 self.first_commit_stamp = stamp
152 # activity
153 # hour
154 hour = date.hour
155 if hour in self.activity_by_hour_of_day:
156 self.activity_by_hour_of_day[hour] += 1
157 else:
158 self.activity_by_hour_of_day[hour] = 1
160 # day of week
161 day = date.weekday()
162 if day in self.activity_by_day_of_week:
163 self.activity_by_day_of_week[day] += 1
164 else:
165 self.activity_by_day_of_week[day] = 1
167 # hour of week
168 if day not in self.activity_by_hour_of_week:
169 self.activity_by_hour_of_week[day] = {}
170 if hour not in self.activity_by_hour_of_week[day]:
171 self.activity_by_hour_of_week[day][hour] = 1
172 else:
173 self.activity_by_hour_of_week[day][hour] += 1
175 # month of year
176 month = date.month
177 if month in self.activity_by_month_of_year:
178 self.activity_by_month_of_year[month] += 1
179 else:
180 self.activity_by_month_of_year[month] = 1
182 # author stats
183 if author not in self.authors:
184 self.authors[author] = {}
185 # TODO commits
186 if 'last_commit_stamp' not in self.authors[author]:
187 self.authors[author]['last_commit_stamp'] = stamp
188 self.authors[author]['first_commit_stamp'] = stamp
189 if 'commits' in self.authors[author]:
190 self.authors[author]['commits'] += 1
191 else:
192 self.authors[author]['commits'] = 1
194 # author of the month/year
195 yymm = datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m')
196 if yymm in self.author_of_month:
197 if author in self.author_of_month[yymm]:
198 self.author_of_month[yymm][author] += 1
199 else:
200 self.author_of_month[yymm][author] = 1
201 else:
202 self.author_of_month[yymm] = {}
203 self.author_of_month[yymm][author] = 1
204 if yymm in self.commits_by_month:
205 self.commits_by_month[yymm] += 1
206 else:
207 self.commits_by_month[yymm] = 1
209 yy = datetime.datetime.fromtimestamp(stamp).year
210 if yy in self.author_of_year:
211 if author in self.author_of_year[yy]:
212 self.author_of_year[yy][author] += 1
213 else:
214 self.author_of_year[yy][author] = 1
215 else:
216 self.author_of_year[yy] = {}
217 self.author_of_year[yy][author] = 1
218 if yy in self.commits_by_year:
219 self.commits_by_year[yy] += 1
220 else:
221 self.commits_by_year[yy] = 1
223 # TODO Optimize this, it's the worst bottleneck
224 # outputs "<stamp> <files>" for each revision
225 self.files_by_stamp = {} # stamp -> files
226 lines = getoutput('git-rev-list --pretty=format:"%at %H" HEAD |grep -v ^commit |while read line; do set $line; echo "$1 $(git-ls-tree -r "$2" |wc -l)"; done').split('\n')
227 self.total_commits = len(lines)
228 for line in lines:
229 parts = line.split(' ')
230 if len(parts) != 2:
231 continue
232 (stamp, files) = parts[0:2]
233 try:
234 self.files_by_stamp[int(stamp)] = int(files)
235 except ValueError:
236 print 'Warning: failed to parse line "%s"' % line
238 # extensions
239 self.extensions = {} # extension -> files, lines
240 lines = getoutput('git-ls-files').split('\n')
241 self.total_files = len(lines)
242 for line in lines:
243 base = os.path.basename(line)
244 if base.find('.') == -1:
245 ext = ''
246 else:
247 ext = base[(base.rfind('.') + 1):]
249 if ext not in self.extensions:
250 self.extensions[ext] = {'files': 0, 'lines': 0}
252 self.extensions[ext]['files'] += 1
253 try:
254 # Escaping could probably be improved here
255 self.extensions[ext]['lines'] += int(getoutput('wc -l < %s' % re.sub(r'(\W)', r'\\\1', line), quiet = True))
256 except:
257 print 'Warning: Could not count lines for file "%s"' % line
259 # line statistics
260 # outputs:
261 # N files changed, N insertions (+), N deletions(-)
262 # <stamp> <author>
263 self.changes_by_date = {} # stamp -> { files, ins, del }
264 lines = getoutput('git-log --shortstat --pretty=format:"%at %an"').split('\n')
265 lines.reverse()
266 files = 0; inserted = 0; deleted = 0; total_lines = 0
267 for line in lines:
268 if len(line) == 0:
269 continue
271 # <stamp> <author>
272 if line.find('files changed,') == -1:
273 pos = line.find(' ')
274 if pos != -1:
275 (stamp, author) = (int(line[:pos]), line[pos+1:])
276 self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
277 else:
278 print 'Warning: unexpected line "%s"' % line
279 else:
280 numbers = re.findall('\d+', line)
281 if len(numbers) == 3:
282 (files, inserted, deleted) = map(lambda el : int(el), numbers)
283 total_lines += inserted
284 total_lines -= deleted
285 else:
286 print 'Warning: failed to handle line "%s"' % line
287 (files, inserted, deleted) = (0, 0, 0)
288 #self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
289 self.total_lines = total_lines
291 def getActivityByDayOfWeek(self):
292 return self.activity_by_day_of_week
294 def getActivityByHourOfDay(self):
295 return self.activity_by_hour_of_day
297 def getAuthorInfo(self, author):
298 a = self.authors[author]
300 commits = a['commits']
301 commits_frac = (100 * float(commits)) / self.getTotalCommits()
302 date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp'])
303 date_last = datetime.datetime.fromtimestamp(a['last_commit_stamp'])
304 delta = date_last - date_first
306 res = { 'commits': commits, 'commits_frac': commits_frac, 'date_first': date_first.strftime('%Y-%m-%d'), 'date_last': date_last.strftime('%Y-%m-%d'), 'timedelta' : delta }
307 return res
309 def getAuthors(self):
310 return self.authors.keys()
312 def getFirstCommitDate(self):
313 return datetime.datetime.fromtimestamp(self.first_commit_stamp)
315 def getLastCommitDate(self):
316 return datetime.datetime.fromtimestamp(self.last_commit_stamp)
318 def getTags(self):
319 lines = getoutput('git-show-ref --tags |cut -d/ -f3')
320 return lines.split('\n')
322 def getTagDate(self, tag):
323 return self.revToDate('tags/' + tag)
325 def getTotalAuthors(self):
326 return self.total_authors
328 def getTotalCommits(self):
329 return self.total_commits
331 def getTotalFiles(self):
332 return self.total_files
334 def getTotalLOC(self):
335 return self.total_lines
337 def revToDate(self, rev):
338 stamp = int(getoutput('git-log --pretty=format:%%at "%s" -n 1' % rev))
339 return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d')
341 class ReportCreator:
342 """Creates the actual report based on given data."""
343 def __init__(self):
344 pass
346 def create(self, data, path):
347 self.data = data
348 self.path = path
350 def html_linkify(text):
351 return text.lower().replace(' ', '_')
353 def html_header(level, text):
354 name = html_linkify(text)
355 return '\n<h%d><a href="#%s" name="%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
357 class HTMLReportCreator(ReportCreator):
358 def create(self, data, path):
359 ReportCreator.create(self, data, path)
360 self.title = data.projectname
362 # TODO copy the CSS if it does not exist
363 if not os.path.exists(path + '/gitstats.css'):
364 shutil.copyfile('gitstats.css', path + '/gitstats.css')
365 pass
367 f = open(path + "/index.html", 'w')
368 format = '%Y-%m-%d %H:%m:%S'
369 self.printHeader(f)
371 f.write('<h1>GitStats - %s</h1>' % data.projectname)
373 self.printNav(f)
375 f.write('<dl>');
376 f.write('<dt>Project name</dt><dd>%s</dd>' % (data.projectname))
377 f.write('<dt>Generated</dt><dd>%s (in %d seconds)</dd>' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated()));
378 f.write('<dt>Report Period</dt><dd>%s to %s</dd>' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format)))
379 f.write('<dt>Total Files</dt><dd>%s</dd>' % data.getTotalFiles())
380 f.write('<dt>Total Lines of Code</dt><dd>%s</dd>' % data.getTotalLOC())
381 f.write('<dt>Total Commits</dt><dd>%s</dd>' % data.getTotalCommits())
382 f.write('<dt>Authors</dt><dd>%s</dd>' % data.getTotalAuthors())
383 f.write('</dl>');
385 f.write('</body>\n</html>');
386 f.close()
389 # Activity
390 f = open(path + '/activity.html', 'w')
391 self.printHeader(f)
392 f.write('<h1>Activity</h1>')
393 self.printNav(f)
395 #f.write('<h2>Last 30 days</h2>')
397 #f.write('<h2>Last 12 months</h2>')
399 # Hour of Day
400 f.write(html_header(2, 'Hour of Day'))
401 hour_of_day = data.getActivityByHourOfDay()
402 f.write('<table><tr><th>Hour</th>')
403 for i in range(1, 25):
404 f.write('<th>%d</th>' % i)
405 f.write('</tr>\n<tr><th>Commits</th>')
406 fp = open(path + '/hour_of_day.dat', 'w')
407 for i in range(0, 24):
408 if i in hour_of_day:
409 f.write('<td>%d</td>' % hour_of_day[i])
410 fp.write('%d %d\n' % (i, hour_of_day[i]))
411 else:
412 f.write('<td>0</td>')
413 fp.write('%d 0\n' % i)
414 fp.close()
415 f.write('</tr>\n<tr><th>%</th>')
416 totalcommits = data.getTotalCommits()
417 for i in range(0, 24):
418 if i in hour_of_day:
419 f.write('<td>%.2f</td>' % ((100.0 * hour_of_day[i]) / totalcommits))
420 else:
421 f.write('<td>0.00</td>')
422 f.write('</tr></table>')
423 f.write('<img src="hour_of_day.png" alt="Hour of Day" />')
424 fg = open(path + '/hour_of_day.dat', 'w')
425 for i in range(0, 24):
426 if i in hour_of_day:
427 fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
428 else:
429 fg.write('%d 0\n' % (i + 1))
430 fg.close()
432 # Day of Week
433 f.write(html_header(2, 'Day of Week'))
434 day_of_week = data.getActivityByDayOfWeek()
435 f.write('<div class="vtable"><table>')
436 f.write('<tr><th>Day</th><th>Total (%)</th></tr>')
437 fp = open(path + '/day_of_week.dat', 'w')
438 for d in range(0, 7):
439 commits = 0
440 if d in day_of_week:
441 commits = day_of_week[d]
442 fp.write('%d %d\n' % (d + 1, commits))
443 f.write('<tr>')
444 f.write('<th>%d</th>' % (d + 1))
445 if d in day_of_week:
446 f.write('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits))
447 else:
448 f.write('<td>0</td>')
449 f.write('</tr>')
450 f.write('</table></div>')
451 f.write('<img src="day_of_week.png" alt="Day of Week" />')
452 fp.close()
454 # Hour of Week
455 f.write(html_header(2, 'Hour of Week'))
456 f.write('<table>')
458 f.write('<tr><th>Weekday</th>')
459 for hour in range(0, 24):
460 f.write('<th>%d</th>' % (hour + 1))
461 f.write('</tr>')
463 for weekday in range(0, 7):
464 f.write('<tr><th>%d</th>' % (weekday + 1))
465 for hour in range(0, 24):
466 try:
467 commits = data.activity_by_hour_of_week[weekday][hour]
468 except KeyError:
469 commits = 0
470 if commits != 0:
471 f.write('<td>%d</td>' % commits)
472 else:
473 f.write('<td></td>')
474 f.write('</tr>')
476 f.write('</table>')
478 # Month of Year
479 f.write(html_header(2, 'Month of Year'))
480 f.write('<div class="vtable"><table>')
481 f.write('<tr><th>Month</th><th>Commits (%)</th></tr>')
482 fp = open (path + '/month_of_year.dat', 'w')
483 for mm in range(1, 13):
484 commits = 0
485 if mm in data.activity_by_month_of_year:
486 commits = data.activity_by_month_of_year[mm]
487 f.write('<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalCommits()))
488 fp.write('%d %d\n' % (mm, commits))
489 fp.close()
490 f.write('</table></div>')
491 f.write('<img src="month_of_year.png" alt="Month of Year" />')
493 # Commits by year/month
494 f.write(html_header(2, 'Commits by year/month'))
495 f.write('<div class="vtable"><table><tr><th>Month</th><th>Commits</th></tr>')
496 for yymm in reversed(sorted(data.commits_by_month.keys())):
497 f.write('<tr><td>%s</td><td>%d</td></tr>' % (yymm, data.commits_by_month[yymm]))
498 f.write('</table></div>')
499 f.write('<img src="commits_by_year_month.png" alt="Commits by year/month" />')
500 fg = open(path + '/commits_by_year_month.dat', 'w')
501 for yymm in sorted(data.commits_by_month.keys()):
502 fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
503 fg.close()
505 # Commits by year
506 f.write(html_header(2, 'Commits by Year'))
507 f.write('<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th></tr>')
508 for yy in reversed(sorted(data.commits_by_year.keys())):
509 f.write('<tr><td>%s</td><td>%d (%.2f%%)</td></tr>' % (yy, data.commits_by_year[yy], (100.0 * data.commits_by_year[yy]) / data.getTotalCommits()))
510 f.write('</table></div>')
511 f.write('<img src="commits_by_year.png" alt="Commits by Year" />')
512 fg = open(path + '/commits_by_year.dat', 'w')
513 for yy in sorted(data.commits_by_year.keys()):
514 fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
515 fg.close()
517 f.write('</body></html>')
518 f.close()
521 # Authors
522 f = open(path + '/authors.html', 'w')
523 self.printHeader(f)
525 f.write('<h1>Authors</h1>')
526 self.printNav(f)
528 # Authors :: List of authors
529 f.write(html_header(2, 'List of Authors'))
531 f.write('<table class="authors">')
532 f.write('<tr><th>Author</th><th>Commits (%)</th><th>First commit</th><th>Last commit</th><th>Age</th><th># by commits</th></tr>')
533 authors_by_commits = getkeyssortedbyvaluekey(data.authors, 'commits')
534 authors_by_commits.reverse() # most first
535 for author in sorted(data.getAuthors()):
536 info = data.getAuthorInfo(author)
537 f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%s</td><td>%s</td><td>%s</td><td>%d</td></tr>' % (author, info['commits'], info['commits_frac'], info['date_first'], info['date_last'], info['timedelta'], authors_by_commits.index(author) + 1))
538 f.write('</table>')
540 # Authors :: Author of Month
541 f.write(html_header(2, 'Author of Month'))
542 f.write('<table>')
543 f.write('<tr><th>Month</th><th>Author</th><th>Commits (%)</th><th>Next top 5</th></tr>')
544 for yymm in reversed(sorted(data.author_of_month.keys())):
545 authordict = data.author_of_month[yymm]
546 authors = getkeyssortedbyvalues(authordict)
547 authors.reverse()
548 commits = data.author_of_month[yymm][authors[0]]
549 next = ', '.join(authors[1:5])
550 f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td></tr>' % (yymm, authors[0], commits, (100 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm], next))
552 f.write('</table>')
554 f.write(html_header(2, 'Author of Year'))
555 f.write('<table><tr><th>Year</th><th>Author</th><th>Commits (%)</th><th>Next top 5</th></tr>')
556 for yy in reversed(sorted(data.author_of_year.keys())):
557 authordict = data.author_of_year[yy]
558 authors = getkeyssortedbyvalues(authordict)
559 authors.reverse()
560 commits = data.author_of_year[yy][authors[0]]
561 next = ', '.join(authors[1:5])
562 f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td></tr>' % (yy, authors[0], commits, (100 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next))
563 f.write('</table>')
565 f.write('</body></html>')
566 f.close()
569 # Files
570 f = open(path + '/files.html', 'w')
571 self.printHeader(f)
572 f.write('<h1>Files</h1>')
573 self.printNav(f)
575 f.write('<dl>\n')
576 f.write('<dt>Total files</dt><dd>%d</dd>' % data.getTotalFiles())
577 f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
578 f.write('<dt>Average file size</dt><dd>%.2f bytes</dd>' % ((100.0 * data.getTotalLOC()) / data.getTotalFiles()))
579 f.write('</dl>\n')
581 # Files :: File count by date
582 f.write(html_header(2, 'File count by date'))
584 fg = open(path + '/files_by_date.dat', 'w')
585 for stamp in sorted(data.files_by_stamp.keys()):
586 fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
587 fg.close()
589 f.write('<img src="files_by_date.png" alt="Files by Date" />')
591 #f.write('<h2>Average file size by date</h2>')
593 # Files :: Extensions
594 f.write(html_header(2, 'Extensions'))
595 f.write('<table><tr><th>Extension</th><th>Files (%)</th><th>Lines (%)</th><th>Lines/file</th></tr>')
596 for ext in sorted(data.extensions.keys()):
597 files = data.extensions[ext]['files']
598 lines = data.extensions[ext]['lines']
599 f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d (%.2f%%)</td><td>%d</td></tr>' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, (100.0 * lines) / data.getTotalLOC(), lines / files))
600 f.write('</table>')
602 f.write('</body></html>')
603 f.close()
606 # Lines
607 f = open(path + '/lines.html', 'w')
608 self.printHeader(f)
609 f.write('<h1>Lines</h1>')
610 self.printNav(f)
612 f.write('<dl>\n')
613 f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
614 f.write('</dl>\n')
616 f.write(html_header(2, 'Lines of Code'))
617 f.write('<img src="lines_of_code.png" />')
619 fg = open(path + '/lines_of_code.dat', 'w')
620 for stamp in sorted(data.changes_by_date.keys()):
621 fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines']))
622 fg.close()
624 f.write('</body></html>')
625 f.close()
628 # tags.html
629 f = open(path + '/tags.html', 'w')
630 self.printHeader(f)
631 f.write('<h1>Tags</h1>')
632 self.printNav(f)
634 f.write('<dl>')
635 f.write('<dt>Total tags</dt><dd>%d</dd>' % len(data.tags))
636 if len(data.tags) > 0:
637 f.write('<dt>Average commits per tag</dt><dd>%.2f</dd>' % (data.getTotalCommits() / len(data.tags)))
638 f.write('</dl>')
640 f.write('<table>')
641 f.write('<tr><th>Name</th><th>Date</th></tr>')
642 # sort the tags by date desc
643 tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
644 for tag in tags_sorted_by_date_desc:
645 f.write('<tr><td>%s</td><td>%s</td></tr>' % (tag, data.tags[tag]['date']))
646 f.write('</table>')
648 f.write('</body></html>')
649 f.close()
651 self.createGraphs(path)
652 pass
654 def createGraphs(self, path):
655 print 'Generating graphs...'
657 # hour of day
658 f = open(path + '/hour_of_day.plot', 'w')
659 f.write(GNUPLOT_COMMON)
660 f.write(
662 set output 'hour_of_day.png'
663 unset key
664 set xrange [0.5:24.5]
665 set xtics 4
666 set ylabel "Commits"
667 plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
668 """)
669 f.close()
671 # day of week
672 f = open(path + '/day_of_week.plot', 'w')
673 f.write(GNUPLOT_COMMON)
674 f.write(
676 set output 'day_of_week.png'
677 unset key
678 set xrange [0.5:7.5]
679 set xtics 1
680 set ylabel "Commits"
681 plot 'day_of_week.dat' using 1:2:(0.5) w boxes fs solid
682 """)
683 f.close()
685 # Month of Year
686 f = open(path + '/month_of_year.plot', 'w')
687 f.write(GNUPLOT_COMMON)
688 f.write(
690 set output 'month_of_year.png'
691 unset key
692 set xrange [0.5:12.5]
693 set xtics 1
694 set ylabel "Commits"
695 plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid
696 """)
697 f.close()
699 # commits_by_year_month
700 f = open(path + '/commits_by_year_month.plot', 'w')
701 f.write(GNUPLOT_COMMON)
702 f.write(
704 set output 'commits_by_year_month.png'
705 unset key
706 set xdata time
707 set timefmt "%Y-%m"
708 set format x "%Y-%m"
709 set xtics rotate by 90 15768000
710 set bmargin 5
711 set ylabel "Commits"
712 plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
713 """)
714 f.close()
716 # commits_by_year
717 f = open(path + '/commits_by_year.plot', 'w')
718 f.write(GNUPLOT_COMMON)
719 f.write(
721 set output 'commits_by_year.png'
722 unset key
723 set xtics 1
724 set ylabel "Commits"
725 plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
726 """)
727 f.close()
729 # Files by date
730 f = open(path + '/files_by_date.plot', 'w')
731 f.write(GNUPLOT_COMMON)
732 f.write(
734 set output 'files_by_date.png'
735 unset key
736 set xdata time
737 set timefmt "%Y-%m-%d"
738 set format x "%Y-%m-%d"
739 set ylabel "Files"
740 set xtics rotate by 90
741 set bmargin 6
742 plot 'files_by_date.dat' using 1:2 smooth csplines
743 """)
744 f.close()
746 # Lines of Code
747 f = open(path + '/lines_of_code.plot', 'w')
748 f.write(GNUPLOT_COMMON)
749 f.write(
751 set output 'lines_of_code.png'
752 unset key
753 set xdata time
754 set timefmt "%s"
755 set format x "%Y-%m-%d"
756 set ylabel "Lines"
757 set xtics rotate by 90
758 set bmargin 6
759 plot 'lines_of_code.dat' using 1:2 w lines
760 """)
761 f.close()
763 os.chdir(path)
764 files = glob.glob(path + '/*.plot')
765 for f in files:
766 out = getoutput('gnuplot %s' % f)
767 if len(out) > 0:
768 print out
770 def printHeader(self, f, title = ''):
771 f.write(
772 """<?xml version="1.0" encoding="UTF-8"?>
773 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
774 <html xmlns="http://www.w3.org/1999/xhtml">
775 <head>
776 <title>GitStats - %s</title>
777 <link rel="stylesheet" href="gitstats.css" type="text/css" />
778 <meta name="generator" content="GitStats" />
779 </head>
780 <body>
781 """ % self.title)
783 def printNav(self, f):
784 f.write("""
785 <div class="nav">
786 <ul>
787 <li><a href="index.html">General</a></li>
788 <li><a href="activity.html">Activity</a></li>
789 <li><a href="authors.html">Authors</a></li>
790 <li><a href="files.html">Files</a></li>
791 <li><a href="lines.html">Lines</a></li>
792 <li><a href="tags.html">Tags</a></li>
793 </ul>
794 </div>
795 """)
798 usage = """
799 Usage: gitstats [options] <gitpath> <outputpath>
801 Options:
804 if len(sys.argv) < 3:
805 print usage
806 sys.exit(0)
808 gitpath = sys.argv[1]
809 outputpath = os.path.abspath(sys.argv[2])
810 rundir = os.getcwd()
812 try:
813 os.makedirs(outputpath)
814 except OSError:
815 pass
816 if not os.path.isdir(outputpath):
817 print 'FATAL: Output path is not a directory or does not exist'
818 sys.exit(1)
820 print 'Git path: %s' % gitpath
821 print 'Output path: %s' % outputpath
823 os.chdir(gitpath)
825 print 'Collecting data...'
826 data = GitDataCollector()
827 data.collect(gitpath)
829 os.chdir(rundir)
831 print 'Generating report...'
832 report = HTMLReportCreator()
833 report.create(data, outputpath)
835 time_end = time.time()
836 exectime_internal = time_end - time_start
837 print 'Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal)