Workaround for linux-2.6 repository.
[gitstats.git] / gitstats
bloba939713080e792c89d9044e584f7f364c5776534
1 #!/usr/bin/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 def getoutput(cmd, quiet = False):
16 if not quiet:
17 print '>> %s' % cmd
18 output = commands.getoutput(cmd)
19 return output
21 def getkeyssortedbyvalues(dict):
22 return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
24 # TODO getdictkeyssortedbyvaluekey(dict, key) - eg. dict['author'] = { 'commits' : 512 } - ...key(dict, 'commits')
26 class DataCollector:
27 """Manages data collection from a revision control repository."""
28 def __init__(self):
29 self.stamp_created = time.time()
30 pass
33 # This should be the main function to extract data from the repository.
34 def collect(self, dir):
35 self.dir = dir
38 # : get a dictionary of author
39 def getAuthorInfo(self, author):
40 return None
42 def getActivityByDayOfWeek(self):
43 return {}
45 def getActivityByHourOfDay(self):
46 return {}
49 # Get a list of authors
50 def getAuthors(self):
51 return []
53 def getFirstCommitDate(self):
54 return datetime.datetime.now()
56 def getLastCommitDate(self):
57 return datetime.datetime.now()
59 def getStampCreated(self):
60 return self.stamp_created
62 def getTags(self):
63 return []
65 def getTotalAuthors(self):
66 return -1
68 def getTotalCommits(self):
69 return -1
71 def getTotalFiles(self):
72 return -1
74 def getTotalLOC(self):
75 return -1
77 class GitDataCollector(DataCollector):
78 def collect(self, dir):
79 DataCollector.collect(self, dir)
81 self.total_authors = int(getoutput('git-log |git-shortlog -s |wc -l'))
82 self.total_commits = int(getoutput('git-rev-list HEAD |wc -l'))
83 self.total_files = int(getoutput('git-ls-files |wc -l'))
84 #self.total_lines = int(getoutput('git-ls-files -z |xargs -0 cat |wc -l'))
86 self.activity_by_hour_of_day = {} # hour -> commits
87 self.activity_by_day_of_week = {} # day -> commits
88 self.activity_by_month_of_year = {} # month [1-12] -> commits
89 self.activity_by_hour_of_week = {} # weekday -> hour -> commits
91 self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp}
93 # author of the month
94 self.author_of_month = {} # month -> author -> commits
95 self.author_of_year = {} # year -> author -> commits
96 self.commits_by_month = {} # month -> commits
97 self.commits_by_year = {} # year -> commits
98 self.first_commit_stamp = 0
99 self.last_commit_stamp = 0
101 # tags
102 self.tags = {}
103 lines = getoutput('git-show-ref --tags').split('\n')
104 for line in lines:
105 if len(line) == 0:
106 continue
107 (hash, tag) = line.split(' ')
108 tag = tag.replace('refs/tags/', '')
109 output = getoutput('git-log "%s" --pretty=format:"%%at %%an" -n 1' % hash)
110 if len(output) > 0:
111 parts = output.split(' ')
112 stamp = 0
113 try:
114 stamp = int(parts[0])
115 except ValueError:
116 stamp = 0
117 self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d') }
118 pass
120 # Collect revision statistics
121 # Outputs "<stamp> <author>"
122 lines = getoutput('git-rev-list --pretty=format:"%at %an" HEAD |grep -v ^commit').split('\n')
123 for line in lines:
124 # linux-2.6 says "<unknown>" for one line O_o
125 parts = line.split(' ')
126 author = ''
127 try:
128 stamp = int(parts[0])
129 except ValueError:
130 stamp = 0
131 if len(parts) > 1:
132 author = ' '.join(parts[1:])
133 date = datetime.datetime.fromtimestamp(float(stamp))
135 # First and last commit stamp
136 if self.last_commit_stamp == 0:
137 self.last_commit_stamp = stamp
138 self.first_commit_stamp = stamp
140 # activity
141 # hour
142 hour = date.hour
143 if hour in self.activity_by_hour_of_day:
144 self.activity_by_hour_of_day[hour] += 1
145 else:
146 self.activity_by_hour_of_day[hour] = 1
148 # day of week
149 day = date.weekday()
150 if day in self.activity_by_day_of_week:
151 self.activity_by_day_of_week[day] += 1
152 else:
153 self.activity_by_day_of_week[day] = 1
155 # hour of week
156 if day not in self.activity_by_hour_of_week:
157 self.activity_by_hour_of_week[day] = {}
158 if hour not in self.activity_by_hour_of_week[day]:
159 self.activity_by_hour_of_week[day][hour] = 1
160 else:
161 self.activity_by_hour_of_week[day][hour] += 1
163 # month of year
164 month = date.month
165 if month in self.activity_by_month_of_year:
166 self.activity_by_month_of_year[month] += 1
167 else:
168 self.activity_by_month_of_year[month] = 1
170 # author stats
171 if author not in self.authors:
172 self.authors[author] = {}
173 # TODO commits
174 if 'last_commit_stamp' not in self.authors[author]:
175 self.authors[author]['last_commit_stamp'] = stamp
176 self.authors[author]['first_commit_stamp'] = stamp
177 if 'commits' in self.authors[author]:
178 self.authors[author]['commits'] += 1
179 else:
180 self.authors[author]['commits'] = 1
182 # author of the month/year
183 yymm = datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m')
184 if yymm in self.author_of_month:
185 if author in self.author_of_month[yymm]:
186 self.author_of_month[yymm][author] += 1
187 else:
188 self.author_of_month[yymm][author] = 1
189 else:
190 self.author_of_month[yymm] = {}
191 self.author_of_month[yymm][author] = 1
192 if yymm in self.commits_by_month:
193 self.commits_by_month[yymm] += 1
194 else:
195 self.commits_by_month[yymm] = 1
197 yy = datetime.datetime.fromtimestamp(stamp).year
198 if yy in self.author_of_year:
199 if author in self.author_of_year[yy]:
200 self.author_of_year[yy][author] += 1
201 else:
202 self.author_of_year[yy][author] = 1
203 else:
204 self.author_of_year[yy] = {}
205 self.author_of_year[yy][author] = 1
206 if yy in self.commits_by_year:
207 self.commits_by_year[yy] += 1
208 else:
209 self.commits_by_year[yy] = 1
211 # TODO Optimize this, it's the worst bottleneck
212 # outputs "<stamp> <files>" for each revision
213 self.files_by_stamp = {} # stamp -> files
214 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')
215 for line in lines:
216 parts = line.split(' ')
217 if len(parts) != 2:
218 continue
219 (stamp, files) = parts[0:2]
220 try:
221 self.files_by_stamp[int(stamp)] = int(files)
222 except ValueError:
223 print 'Warning: failed to parse line "%s"' % line
225 # extensions
226 self.extensions = {} # extension -> files, lines
227 lines = getoutput('git-ls-files').split('\n')
228 for line in lines:
229 base = os.path.basename(line)
230 if base.find('.') == -1:
231 ext = ''
232 else:
233 ext = base[(base.rfind('.') + 1):]
235 if ext not in self.extensions:
236 self.extensions[ext] = {'files': 0, 'lines': 0}
238 self.extensions[ext]['files'] += 1
239 try:
240 # FIXME filenames with spaces or special characters are broken
241 self.extensions[ext]['lines'] += int(getoutput('wc -l < %s' % line, quiet = True))
242 except:
243 print 'Warning: Could not count lines for file "%s"' % line
245 # line statistics
246 # outputs:
247 # N files changed, N insertions (+), N deletions(-)
248 # <stamp> <author>
249 self.changes_by_date = {} # stamp -> { files, ins, del }
250 lines = getoutput('git-log --shortstat --pretty=format:"%at %an" |tac').split('\n')
251 files = 0; inserted = 0; deleted = 0; total_lines = 0
252 for line in lines:
253 if len(line) == 0:
254 continue
256 # <stamp> <author>
257 if line.find(',') == -1:
258 pos = line.find(' ')
259 (stamp, author) = (int(line[:pos]), line[pos+1:])
260 self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
261 else:
262 numbers = re.findall('\d+', line)
263 if len(numbers) == 3:
264 (files, inserted, deleted) = map(lambda el : int(el), numbers)
265 total_lines += inserted
266 total_lines -= deleted
267 else:
268 print 'Warning: failed to handle line "%s"' % line
269 (files, inserted, deleted) = (0, 0, 0)
270 #self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
271 self.total_lines = total_lines
273 def getActivityByDayOfWeek(self):
274 return self.activity_by_day_of_week
276 def getActivityByHourOfDay(self):
277 return self.activity_by_hour_of_day
279 def getAuthorInfo(self, author):
280 a = self.authors[author]
282 commits = a['commits']
283 commits_frac = (100 * float(commits)) / self.getTotalCommits()
284 date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp'])
285 date_last = datetime.datetime.fromtimestamp(a['last_commit_stamp'])
286 delta = date_last - date_first
288 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 }
289 return res
291 def getAuthors(self):
292 return self.authors.keys()
294 def getFirstCommitDate(self):
295 return datetime.datetime.fromtimestamp(self.first_commit_stamp)
297 def getLastCommitDate(self):
298 return datetime.datetime.fromtimestamp(self.last_commit_stamp)
300 def getTags(self):
301 lines = getoutput('git-show-ref --tags |cut -d/ -f3')
302 return lines.split('\n')
304 def getTagDate(self, tag):
305 return self.revToDate('tags/' + tag)
307 def getTotalAuthors(self):
308 return self.total_authors
310 def getTotalCommits(self):
311 return self.total_commits
313 def getTotalFiles(self):
314 return self.total_files
316 def getTotalLOC(self):
317 return self.total_lines
319 def revToDate(self, rev):
320 stamp = int(getoutput('git-log --pretty=format:%%at "%s" -n 1' % rev))
321 return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d')
323 class ReportCreator:
324 """Creates the actual report based on given data."""
325 def __init__(self):
326 pass
328 def create(self, data, path):
329 self.data = data
330 self.path = path
332 def html_linkify(text):
333 return text.lower().replace(' ', '_')
335 def html_header(level, text):
336 name = html_linkify(text)
337 return '\n<h%d><a href="#%s" name="%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
339 class HTMLReportCreator(ReportCreator):
340 def create(self, data, path):
341 ReportCreator.create(self, data, path)
343 # TODO copy the CSS if it does not exist
344 if not os.path.exists(path + '/gitstats.css'):
345 #shutil.copyfile('')
346 pass
348 f = open(path + "/index.html", 'w')
349 format = '%Y-%m-%d %H:%m:%S'
350 self.printHeader(f)
352 f.write('<h1>GitStats</h1>')
354 self.printNav(f)
356 f.write('<dl>');
357 f.write('<dt>Generated</dt><dd>%s (in %d seconds)</dd>' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated()));
358 f.write('<dt>Report Period</dt><dd>%s to %s</dd>' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format)))
359 f.write('<dt>Total Files</dt><dd>%s</dd>' % data.getTotalFiles())
360 f.write('<dt>Total Lines of Code</dt><dd>%s</dd>' % data.getTotalLOC())
361 f.write('<dt>Total Commits</dt><dd>%s</dd>' % data.getTotalCommits())
362 f.write('<dt>Authors</dt><dd>%s</dd>' % data.getTotalAuthors())
363 f.write('</dl>');
365 f.write('</body>\n</html>');
366 f.close()
369 # Activity
370 f = open(path + '/activity.html', 'w')
371 self.printHeader(f)
372 f.write('<h1>Activity</h1>')
373 self.printNav(f)
375 #f.write('<h2>Last 30 days</h2>')
377 #f.write('<h2>Last 12 months</h2>')
379 # Hour of Day
380 f.write(html_header(2, 'Hour of Day'))
381 hour_of_day = data.getActivityByHourOfDay()
382 f.write('<table><tr><th>Hour</th>')
383 for i in range(1, 25):
384 f.write('<th>%d</th>' % i)
385 f.write('</tr>\n<tr><th>Commits</th>')
386 fp = open(path + '/hour_of_day.dat', 'w')
387 for i in range(0, 24):
388 if i in hour_of_day:
389 f.write('<td>%d</td>' % hour_of_day[i])
390 fp.write('%d %d\n' % (i, hour_of_day[i]))
391 else:
392 f.write('<td>0</td>')
393 fp.write('%d 0\n' % i)
394 fp.close()
395 f.write('</tr>\n<tr><th>%</th>')
396 totalcommits = data.getTotalCommits()
397 for i in range(0, 24):
398 if i in hour_of_day:
399 f.write('<td>%.2f</td>' % ((100.0 * hour_of_day[i]) / totalcommits))
400 else:
401 f.write('<td>0.00</td>')
402 f.write('</tr></table>')
403 f.write('<img src="hour_of_day.png" alt="Hour of Day" />')
404 fg = open(path + '/hour_of_day.dat', 'w')
405 for i in range(0, 24):
406 if i in hour_of_day:
407 fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
408 else:
409 fg.write('%d 0\n' % (i + 1))
410 fg.close()
412 # Day of Week
413 f.write(html_header(2, 'Day of Week'))
414 day_of_week = data.getActivityByDayOfWeek()
415 f.write('<div class="vtable"><table>')
416 f.write('<tr><th>Day</th><th>Total (%)</th></tr>')
417 fp = open(path + '/day_of_week.dat', 'w')
418 for d in range(0, 7):
419 commits = 0
420 if d in day_of_week:
421 commits = day_of_week[d]
422 fp.write('%d %d\n' % (d + 1, commits))
423 f.write('<tr>')
424 f.write('<th>%d</th>' % (d + 1))
425 if d in day_of_week:
426 f.write('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits))
427 else:
428 f.write('<td>0</td>')
429 f.write('</tr>')
430 f.write('</table></div>')
431 f.write('<img src="day_of_week.png" alt="Day of Week" />')
432 fp.close()
434 # Hour of Week
435 f.write(html_header(2, 'Hour of Week'))
436 f.write('<table>')
438 f.write('<tr><th>Weekday</th>')
439 for hour in range(0, 24):
440 f.write('<th>%d</th>' % (hour + 1))
441 f.write('</tr>')
443 for weekday in range(0, 7):
444 f.write('<tr><th>%d</th>' % (weekday + 1))
445 for hour in range(0, 24):
446 try:
447 commits = data.activity_by_hour_of_week[weekday][hour]
448 except KeyError:
449 commits = 0
450 if commits != 0:
451 f.write('<td>%d</td>' % commits)
452 else:
453 f.write('<td></td>')
454 f.write('</tr>')
456 f.write('</table>')
458 # Month of Year
459 f.write(html_header(2, 'Month of Year'))
460 f.write('<div class="vtable"><table>')
461 f.write('<tr><th>Month</th><th>Commits (%)</th></tr>')
462 fp = open (path + '/month_of_year.dat', 'w')
463 for mm in range(1, 13):
464 commits = 0
465 if mm in data.activity_by_month_of_year:
466 commits = data.activity_by_month_of_year[mm]
467 f.write('<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalCommits()))
468 fp.write('%d %d\n' % (mm, commits))
469 fp.close()
470 f.write('</table></div>')
471 f.write('<img src="month_of_year.png" alt="Month of Year" />')
473 # Commits by year/month
474 f.write(html_header(2, 'Commits by year/month'))
475 f.write('<div class="vtable"><table><tr><th>Month</th><th>Commits</th></tr>')
476 for yymm in reversed(sorted(data.commits_by_month.keys())):
477 f.write('<tr><td>%s</td><td>%d</td></tr>' % (yymm, data.commits_by_month[yymm]))
478 f.write('</table></div>')
479 f.write('<img src="commits_by_year_month.png" alt="Commits by year/month" />')
480 fg = open(path + '/commits_by_year_month.dat', 'w')
481 for yymm in sorted(data.commits_by_month.keys()):
482 fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
483 fg.close()
485 # Commits by year
486 f.write(html_header(2, 'Commits by Year'))
487 f.write('<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th></tr>')
488 for yy in reversed(sorted(data.commits_by_year.keys())):
489 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()))
490 f.write('</table></div>')
491 f.write('<img src="commits_by_year.png" alt="Commits by Year" />')
492 fg = open(path + '/commits_by_year.dat', 'w')
493 for yy in sorted(data.commits_by_year.keys()):
494 fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
495 fg.close()
497 f.write('</body></html>')
498 f.close()
501 # Authors
502 f = open(path + '/authors.html', 'w')
503 self.printHeader(f)
505 f.write('<h1>Authors</h1>')
506 self.printNav(f)
508 # Authors :: List of authors
509 f.write(html_header(2, 'List of Authors'))
511 f.write('<table class="authors">')
512 f.write('<tr><th>Author</th><th>Commits (%)</th><th>First commit</th><th>Last commit</th><th>Age</th></tr>')
513 for author in sorted(data.getAuthors()):
514 info = data.getAuthorInfo(author)
515 f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%s</td><td>%s</td><td>%s</td></tr>' % (author, info['commits'], info['commits_frac'], info['date_first'], info['date_last'], info['timedelta']))
516 f.write('</table>')
518 # Authors :: Author of Month
519 f.write(html_header(2, 'Author of Month'))
520 f.write('<table>')
521 f.write('<tr><th>Month</th><th>Author</th><th>Commits (%)</th></tr>')
522 for yymm in reversed(sorted(data.author_of_month.keys())):
523 authordict = data.author_of_month[yymm]
524 authors = getkeyssortedbyvalues(authordict)
525 authors.reverse()
526 commits = data.author_of_month[yymm][authors[0]]
527 f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td></tr>' % (yymm, authors[0], commits, (100 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm]))
529 f.write('</table>')
531 f.write(html_header(2, 'Author of Year'))
532 f.write('<table><tr><th>Year</th><th>Author</th><th>Commits (%)</th></tr>')
533 for yy in reversed(sorted(data.author_of_year.keys())):
534 authordict = data.author_of_year[yy]
535 authors = getkeyssortedbyvalues(authordict)
536 authors.reverse()
537 commits = data.author_of_year[yy][authors[0]]
538 f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td></tr>' % (yy, authors[0], commits, (100 * commits) / data.commits_by_year[yy], data.commits_by_year[yy]))
539 f.write('</table>')
541 f.write('</body></html>')
542 f.close()
545 # Files
546 f = open(path + '/files.html', 'w')
547 self.printHeader(f)
548 f.write('<h1>Files</h1>')
549 self.printNav(f)
551 f.write('<dl>\n')
552 f.write('<dt>Total files</dt><dd>%d</dd>' % data.getTotalFiles())
553 f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
554 f.write('<dt>Average file size</dt><dd>%.2f bytes</dd>' % ((100.0 * data.getTotalLOC()) / data.getTotalFiles()))
555 f.write('</dl>\n')
557 # Files :: File count by date
558 f.write(html_header(2, 'File count by date'))
560 fg = open(path + '/files_by_date.dat', 'w')
561 for stamp in sorted(data.files_by_stamp.keys()):
562 fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
563 fg.close()
565 f.write('<img src="files_by_date.png" alt="Files by Date" />')
567 #f.write('<h2>Average file size by date</h2>')
569 # Files :: Extensions
570 f.write(html_header(2, 'Extensions'))
571 f.write('<table><tr><th>Extension</th><th>Files (%)</th><th>Lines (%)</th><th>Lines/file</th></tr>')
572 for ext in sorted(data.extensions.keys()):
573 files = data.extensions[ext]['files']
574 lines = data.extensions[ext]['lines']
575 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))
576 f.write('</table>')
578 f.write('</body></html>')
579 f.close()
582 # Lines
583 f = open(path + '/lines.html', 'w')
584 self.printHeader(f)
585 f.write('<h1>Lines</h1>')
586 self.printNav(f)
588 f.write('<dl>\n')
589 f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
590 f.write('</dl>\n')
592 f.write(html_header(2, 'Lines of Code'))
593 f.write('<img src="lines_of_code.png" />')
595 fg = open(path + '/lines_of_code.dat', 'w')
596 for stamp in sorted(data.changes_by_date.keys()):
597 fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines']))
598 fg.close()
600 f.write('</body></html>')
601 f.close()
604 # tags.html
605 f = open(path + '/tags.html', 'w')
606 self.printHeader(f)
607 f.write('<h1>Tags</h1>')
608 self.printNav(f)
610 f.write('<dl>')
611 f.write('<dt>Total tags</dt><dd>%d</dd>' % len(data.tags))
612 if len(data.tags) > 0:
613 f.write('<dt>Average commits per tag</dt><dd>%.2f</dd>' % (data.getTotalCommits() / len(data.tags)))
614 f.write('</dl>')
616 f.write('<table>')
617 f.write('<tr><th>Name</th><th>Date</th></tr>')
618 # sort the tags by date desc
619 tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
620 for tag in tags_sorted_by_date_desc:
621 f.write('<tr><td>%s</td><td>%s</td></tr>' % (tag, data.tags[tag]['date']))
622 f.write('</table>')
624 f.write('</body></html>')
625 f.close()
627 self.createGraphs(path)
628 pass
630 def createGraphs(self, path):
631 print 'Generating graphs...'
633 # hour of day
634 f = open(path + '/hour_of_day.plot', 'w')
635 f.write(GNUPLOT_COMMON)
636 f.write(
638 set output 'hour_of_day.png'
639 unset key
640 set xrange [0.5:24.5]
641 set xtics 4
642 set ylabel "Commits"
643 plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
644 """)
645 f.close()
647 # day of week
648 f = open(path + '/day_of_week.plot', 'w')
649 f.write(GNUPLOT_COMMON)
650 f.write(
652 set output 'day_of_week.png'
653 unset key
654 set xrange [0.5:7.5]
655 set xtics 1
656 set ylabel "Commits"
657 plot 'day_of_week.dat' using 1:2:(0.5) w boxes fs solid
658 """)
659 f.close()
661 # Month of Year
662 f = open(path + '/month_of_year.plot', 'w')
663 f.write(GNUPLOT_COMMON)
664 f.write(
666 set output 'month_of_year.png'
667 unset key
668 set xrange [0.5:12.5]
669 set xtics 1
670 set ylabel "Commits"
671 plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid
672 """)
673 f.close()
675 # commits_by_year_month
676 f = open(path + '/commits_by_year_month.plot', 'w')
677 f.write(GNUPLOT_COMMON)
678 f.write(
680 set output 'commits_by_year_month.png'
681 unset key
682 set xdata time
683 set timefmt "%Y-%m"
684 set format x "%Y-%m"
685 set xtics rotate by 90 15768000
686 set ylabel "Commits"
687 plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
688 """)
689 f.close()
691 # commits_by_year
692 f = open(path + '/commits_by_year.plot', 'w')
693 f.write(GNUPLOT_COMMON)
694 f.write(
696 set output 'commits_by_year.png'
697 unset key
698 set xtics 1
699 set ylabel "Commits"
700 plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
701 """)
702 f.close()
704 # Files by date
705 f = open(path + '/files_by_date.plot', 'w')
706 f.write(GNUPLOT_COMMON)
707 f.write(
709 set output 'files_by_date.png'
710 unset key
711 set xdata time
712 set timefmt "%Y-%m-%d"
713 set format x "%Y-%m-%d"
714 set ylabel "Files"
715 set xtics rotate by 90
716 plot 'files_by_date.dat' using 1:2 smooth csplines
717 """)
718 f.close()
720 # Lines of Code
721 f = open(path + '/lines_of_code.plot', 'w')
722 f.write(GNUPLOT_COMMON)
723 f.write(
725 set output 'lines_of_code.png'
726 unset key
727 set xdata time
728 set timefmt "%s"
729 set format x "%Y-%m-%d"
730 set ylabel "Lines"
731 set xtics rotate by 90
732 plot 'lines_of_code.dat' using 1:2 w lines
733 """)
734 f.close()
736 os.chdir(path)
737 files = glob.glob(path + '/*.plot')
738 for f in files:
739 print '>> gnuplot %s' % os.path.basename(f)
740 os.system('gnuplot %s' % f)
742 def printHeader(self, f):
743 f.write(
744 """<?xml version="1.0" encoding="UTF-8"?>
745 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
746 <html xmlns="http://www.w3.org/1999/xhtml">
747 <head>
748 <title>GitStats</title>
749 <link rel="stylesheet" href="gitstats.css" type="text/css" />
750 <meta name="generator" content="GitStats" />
751 </head>
752 <body>
753 """)
755 def printNav(self, f):
756 f.write("""
757 <div class="nav">
758 <ul>
759 <li><a href="index.html">General</a></li>
760 <li><a href="activity.html">Activity</a></li>
761 <li><a href="authors.html">Authors</a></li>
762 <li><a href="files.html">Files</a></li>
763 <li><a href="lines.html">Lines</a></li>
764 <li><a href="tags.html">Tags</a></li>
765 </ul>
766 </div>
767 """)
770 usage = """
771 Usage: gitstats [options] <gitpath> <outputpath>
773 Options:
776 if len(sys.argv) < 3:
777 print usage
778 sys.exit(0)
780 gitpath = sys.argv[1]
781 outputpath = os.path.abspath(sys.argv[2])
783 try:
784 os.makedirs(outputpath)
785 except OSError:
786 pass
787 if not os.path.isdir(outputpath):
788 print 'FATAL: Output path is not a directory or does not exist'
789 sys.exit(1)
791 print 'Git path: %s' % gitpath
792 print 'Output path: %s' % outputpath
794 os.chdir(gitpath)
796 print 'Collecting data...'
797 data = GitDataCollector()
798 data.collect(gitpath)
800 print 'Generating report...'
801 report = HTMLReportCreator()
802 report.create(data, outputpath)