|
|
@@ -1,5 +1,5 @@
|
|
1
|
|
-#!/usr/bin/env python
|
|
2
|
|
-# Copyright (c) 2007-2012 Heikki Hokkanen <hoxu@users.sf.net> & others (see doc/author.txt)
|
|
|
1
|
+#!/usr/bin/env python2
|
|
|
2
|
+# Copyright (c) 2007-2014 Heikki Hokkanen <hoxu@users.sf.net> & others (see doc/AUTHOR)
|
|
3
|
3
|
# GPLv2 / GPLv3
|
|
4
|
4
|
import datetime
|
|
5
|
5
|
import getopt
|
|
|
@@ -14,6 +14,12 @@ import sys
|
|
14
|
14
|
import time
|
|
15
|
15
|
import zlib
|
|
16
|
16
|
|
|
|
17
|
+if sys.version_info < (2, 6):
|
|
|
18
|
+ print >> sys.stderr, "Python 2.6 or higher is required for gitstats"
|
|
|
19
|
+ sys.exit(1)
|
|
|
20
|
+
|
|
|
21
|
+from multiprocessing import Pool
|
|
|
22
|
+
|
|
17
|
23
|
os.environ['LC_ALL'] = 'C'
|
|
18
|
24
|
|
|
19
|
25
|
GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n'
|
|
|
@@ -40,7 +46,8 @@ conf = {
|
|
40
|
46
|
'commit_end': 'HEAD',
|
|
41
|
47
|
'linear_linestats': 1,
|
|
42
|
48
|
'project_name': '',
|
|
43
|
|
- 'merge_authors': {}
|
|
|
49
|
+ 'processes': 8,
|
|
|
50
|
+ 'start_date': ''
|
|
44
|
51
|
}
|
|
45
|
52
|
|
|
46
|
53
|
def getpipeoutput(cmds, quiet = False):
|
|
|
@@ -49,12 +56,14 @@ def getpipeoutput(cmds, quiet = False):
|
|
49
|
56
|
if not quiet and ON_LINUX and os.isatty(1):
|
|
50
|
57
|
print '>> ' + ' | '.join(cmds),
|
|
51
|
58
|
sys.stdout.flush()
|
|
52
|
|
- p0 = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
|
|
53
|
|
- p = p0
|
|
|
59
|
+ p = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
|
|
|
60
|
+ processes=[p]
|
|
54
|
61
|
for x in cmds[1:]:
|
|
55
|
|
- p = subprocess.Popen(x, stdin = p0.stdout, stdout = subprocess.PIPE, shell = True)
|
|
56
|
|
- p0 = p
|
|
|
62
|
+ p = subprocess.Popen(x, stdin = p.stdout, stdout = subprocess.PIPE, shell = True)
|
|
|
63
|
+ processes.append(p)
|
|
57
|
64
|
output = p.communicate()[0]
|
|
|
65
|
+ for p in processes:
|
|
|
66
|
+ p.wait()
|
|
58
|
67
|
end = time.time()
|
|
59
|
68
|
if not quiet:
|
|
60
|
69
|
if ON_LINUX and os.isatty(1):
|
|
|
@@ -63,6 +72,12 @@ def getpipeoutput(cmds, quiet = False):
|
|
63
|
72
|
exectime_external += (end - start)
|
|
64
|
73
|
return output.rstrip('\n')
|
|
65
|
74
|
|
|
|
75
|
+def getlogrange(defaultrange = 'HEAD', end_only = True):
|
|
|
76
|
+ commit_range = getcommitrange(defaultrange, end_only)
|
|
|
77
|
+ if len(conf['start_date']) > 0:
|
|
|
78
|
+ return '--since="%s" "%s"' % (conf['start_date'], commit_range)
|
|
|
79
|
+ return commit_range
|
|
|
80
|
+
|
|
66
|
81
|
def getcommitrange(defaultrange = 'HEAD', end_only = False):
|
|
67
|
82
|
if len(conf['commit_end']) > 0:
|
|
68
|
83
|
if end_only or len(conf['commit_begin']) == 0:
|
|
|
@@ -104,6 +119,20 @@ def getgitversion():
|
|
104
|
119
|
def getgnuplotversion():
|
|
105
|
120
|
return getpipeoutput(['%s --version' % gnuplot_cmd]).split('\n')[0]
|
|
106
|
121
|
|
|
|
122
|
+def getnumoffilesfromrev(time_rev):
|
|
|
123
|
+ """
|
|
|
124
|
+ Get number of files changed in commit
|
|
|
125
|
+ """
|
|
|
126
|
+ time, rev = time_rev
|
|
|
127
|
+ return (int(time), rev, int(getpipeoutput(['git ls-tree -r --name-only "%s"' % rev, 'wc -l']).split('\n')[0]))
|
|
|
128
|
+
|
|
|
129
|
+def getnumoflinesinblob(ext_blob):
|
|
|
130
|
+ """
|
|
|
131
|
+ Get number of lines in blob
|
|
|
132
|
+ """
|
|
|
133
|
+ ext, blob_id = ext_blob
|
|
|
134
|
+ return (ext, blob_id, int(getpipeoutput(['git cat-file blob %s' % blob_id, 'wc -l']).split()[0]))
|
|
|
135
|
+
|
|
107
|
136
|
class DataCollector:
|
|
108
|
137
|
"""Manages data collection from a revision control repository."""
|
|
109
|
138
|
def __init__(self):
|
|
|
@@ -257,7 +286,7 @@ class GitDataCollector(DataCollector):
|
|
257
|
286
|
def collect(self, dir):
|
|
258
|
287
|
DataCollector.collect(self, dir)
|
|
259
|
288
|
|
|
260
|
|
- self.total_authors += int(getpipeoutput(['git shortlog -s %s' % getcommitrange(), 'wc -l']))
|
|
|
289
|
+ self.total_authors += int(getpipeoutput(['git shortlog -s %s' % getlogrange(), 'wc -l']))
|
|
261
|
290
|
#self.total_lines = int(getoutput('git-ls-files -z |xargs -0 cat |wc -l'))
|
|
262
|
291
|
|
|
263
|
292
|
# tags
|
|
|
@@ -293,14 +322,12 @@ class GitDataCollector(DataCollector):
|
|
293
|
322
|
parts = re.split('\s+', line, 2)
|
|
294
|
323
|
commits = int(parts[1])
|
|
295
|
324
|
author = parts[2]
|
|
296
|
|
- if author in conf['merge_authors']:
|
|
297
|
|
- author = conf['merge_authors'][author]
|
|
298
|
325
|
self.tags[tag]['commits'] += commits
|
|
299
|
326
|
self.tags[tag]['authors'][author] = commits
|
|
300
|
327
|
|
|
301
|
328
|
# Collect revision statistics
|
|
302
|
329
|
# Outputs "<stamp> <date> <time> <timezone> <author> '<' <mail> '>'"
|
|
303
|
|
- lines = getpipeoutput(['git rev-list --pretty=format:"%%at %%ai %%aN <%%aE>" %s' % getcommitrange('HEAD'), 'grep -v ^commit']).split('\n')
|
|
|
330
|
+ lines = getpipeoutput(['git rev-list --pretty=format:"%%at %%ai %%aN <%%aE>" %s' % getlogrange('HEAD'), 'grep -v ^commit']).split('\n')
|
|
304
|
331
|
for line in lines:
|
|
305
|
332
|
parts = line.split(' ', 4)
|
|
306
|
333
|
author = ''
|
|
|
@@ -311,8 +338,6 @@ class GitDataCollector(DataCollector):
|
|
311
|
338
|
timezone = parts[3]
|
|
312
|
339
|
author, mail = parts[4].split('<', 1)
|
|
313
|
340
|
author = author.rstrip()
|
|
314
|
|
- if author in conf['merge_authors']:
|
|
315
|
|
- author = conf['merge_authors'][author]
|
|
316
|
341
|
mail = mail.rstrip('>')
|
|
317
|
342
|
domain = '?'
|
|
318
|
343
|
if mail.find('@') != -1:
|
|
|
@@ -408,14 +433,37 @@ class GitDataCollector(DataCollector):
|
|
408
|
433
|
# timezone
|
|
409
|
434
|
self.commits_by_timezone[timezone] = self.commits_by_timezone.get(timezone, 0) + 1
|
|
410
|
435
|
|
|
411
|
|
- # TODO Optimize this, it's the worst bottleneck
|
|
412
|
436
|
# outputs "<stamp> <files>" for each revision
|
|
413
|
|
- revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T" %s' % getcommitrange('HEAD'), 'grep -v ^commit']).strip().split('\n')
|
|
|
437
|
+ revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T" %s' % getlogrange('HEAD'), 'grep -v ^commit']).strip().split('\n')
|
|
414
|
438
|
lines = []
|
|
|
439
|
+ revs_to_read = []
|
|
|
440
|
+ time_rev_count = []
|
|
|
441
|
+ #Look up rev in cache and take info from cache if found
|
|
|
442
|
+ #If not append rev to list of rev to read from repo
|
|
415
|
443
|
for revline in revlines:
|
|
416
|
444
|
time, rev = revline.split(' ')
|
|
417
|
|
- linecount = self.getFilesInCommit(rev)
|
|
418
|
|
- lines.append('%d %d' % (int(time), linecount))
|
|
|
445
|
+ #if cache empty then add time and rev to list of new rev's
|
|
|
446
|
+ #otherwise try to read needed info from cache
|
|
|
447
|
+ if 'files_in_tree' not in self.cache.keys():
|
|
|
448
|
+ revs_to_read.append((time,rev))
|
|
|
449
|
+ continue
|
|
|
450
|
+ if rev in self.cache['files_in_tree'].keys():
|
|
|
451
|
+ lines.append('%d %d' % (int(time), self.cache['files_in_tree'][rev]))
|
|
|
452
|
+ else:
|
|
|
453
|
+ revs_to_read.append((time,rev))
|
|
|
454
|
+
|
|
|
455
|
+ #Read revisions from repo
|
|
|
456
|
+ pool = Pool(processes=conf['processes'])
|
|
|
457
|
+ time_rev_count = pool.map(getnumoffilesfromrev, revs_to_read)
|
|
|
458
|
+ pool.terminate()
|
|
|
459
|
+ pool.join()
|
|
|
460
|
+
|
|
|
461
|
+ #Update cache with new revisions and append then to general list
|
|
|
462
|
+ for (time, rev, count) in time_rev_count:
|
|
|
463
|
+ if 'files_in_tree' not in self.cache:
|
|
|
464
|
+ self.cache['files_in_tree'] = {}
|
|
|
465
|
+ self.cache['files_in_tree'][rev] = count
|
|
|
466
|
+ lines.append('%d %d' % (int(time), count))
|
|
419
|
467
|
|
|
420
|
468
|
self.total_commits += len(lines)
|
|
421
|
469
|
for line in lines:
|
|
|
@@ -430,14 +478,15 @@ class GitDataCollector(DataCollector):
|
|
430
|
478
|
|
|
431
|
479
|
# extensions and size of files
|
|
432
|
480
|
lines = getpipeoutput(['git ls-tree -r -l -z %s' % getcommitrange('HEAD', end_only = True)]).split('\000')
|
|
|
481
|
+ blobs_to_read = []
|
|
433
|
482
|
for line in lines:
|
|
434
|
483
|
if len(line) == 0:
|
|
435
|
484
|
continue
|
|
436
|
|
- parts = re.split('\s+', line, 5)
|
|
|
485
|
+ parts = re.split('\s+', line, 4)
|
|
437
|
486
|
if parts[0] == '160000' and parts[3] == '-':
|
|
438
|
487
|
# skip submodules
|
|
439
|
488
|
continue
|
|
440
|
|
- sha1 = parts[2]
|
|
|
489
|
+ blob_id = parts[2]
|
|
441
|
490
|
size = int(parts[3])
|
|
442
|
491
|
fullpath = parts[4]
|
|
443
|
492
|
|
|
|
@@ -451,15 +500,31 @@ class GitDataCollector(DataCollector):
|
|
451
|
500
|
ext = filename[(filename.rfind('.') + 1):]
|
|
452
|
501
|
if len(ext) > conf['max_ext_length']:
|
|
453
|
502
|
ext = ''
|
|
454
|
|
-
|
|
455
|
503
|
if ext not in self.extensions:
|
|
456
|
504
|
self.extensions[ext] = {'files': 0, 'lines': 0}
|
|
457
|
|
-
|
|
458
|
505
|
self.extensions[ext]['files'] += 1
|
|
459
|
|
- try:
|
|
460
|
|
- self.extensions[ext]['lines'] += self.getLinesInBlob(sha1)
|
|
461
|
|
- except:
|
|
462
|
|
- print 'Warning: Could not count lines for file "%s"' % line
|
|
|
506
|
+ #if cache empty then add ext and blob id to list of new blob's
|
|
|
507
|
+ #otherwise try to read needed info from cache
|
|
|
508
|
+ if 'lines_in_blob' not in self.cache.keys():
|
|
|
509
|
+ blobs_to_read.append((ext,blob_id))
|
|
|
510
|
+ continue
|
|
|
511
|
+ if blob_id in self.cache['lines_in_blob'].keys():
|
|
|
512
|
+ self.extensions[ext]['lines'] += self.cache['lines_in_blob'][blob_id]
|
|
|
513
|
+ else:
|
|
|
514
|
+ blobs_to_read.append((ext,blob_id))
|
|
|
515
|
+
|
|
|
516
|
+ #Get info abount line count for new blob's that wasn't found in cache
|
|
|
517
|
+ pool = Pool(processes=conf['processes'])
|
|
|
518
|
+ ext_blob_linecount = pool.map(getnumoflinesinblob, blobs_to_read)
|
|
|
519
|
+ pool.terminate()
|
|
|
520
|
+ pool.join()
|
|
|
521
|
+
|
|
|
522
|
+ #Update cache and write down info about number of number of lines
|
|
|
523
|
+ for (ext, blob_id, linecount) in ext_blob_linecount:
|
|
|
524
|
+ if 'lines_in_blob' not in self.cache:
|
|
|
525
|
+ self.cache['lines_in_blob'] = {}
|
|
|
526
|
+ self.cache['lines_in_blob'][blob_id] = linecount
|
|
|
527
|
+ self.extensions[ext]['lines'] += self.cache['lines_in_blob'][blob_id]
|
|
463
|
528
|
|
|
464
|
529
|
# line statistics
|
|
465
|
530
|
# outputs:
|
|
|
@@ -471,7 +536,7 @@ class GitDataCollector(DataCollector):
|
|
471
|
536
|
extra = ''
|
|
472
|
537
|
if conf['linear_linestats']:
|
|
473
|
538
|
extra = '--first-parent -m'
|
|
474
|
|
- lines = getpipeoutput(['git log --shortstat %s --pretty=format:"%%at %%aN" %s' % (extra, getcommitrange('HEAD'))]).split('\n')
|
|
|
539
|
+ lines = getpipeoutput(['git log --shortstat %s --pretty=format:"%%at %%aN" %s' % (extra, getlogrange('HEAD'))]).split('\n')
|
|
475
|
540
|
lines.reverse()
|
|
476
|
541
|
files = 0; inserted = 0; deleted = 0; total_lines = 0
|
|
477
|
542
|
author = None
|
|
|
@@ -485,8 +550,6 @@ class GitDataCollector(DataCollector):
|
|
485
|
550
|
if pos != -1:
|
|
486
|
551
|
try:
|
|
487
|
552
|
(stamp, author) = (int(line[:pos]), line[pos+1:])
|
|
488
|
|
- if author in conf['merge_authors']:
|
|
489
|
|
- author = conf['merge_authors'][author]
|
|
490
|
553
|
self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
|
|
491
|
554
|
|
|
492
|
555
|
date = datetime.datetime.fromtimestamp(stamp)
|
|
|
@@ -517,7 +580,7 @@ class GitDataCollector(DataCollector):
|
|
517
|
580
|
print 'Warning: failed to handle line "%s"' % line
|
|
518
|
581
|
(files, inserted, deleted) = (0, 0, 0)
|
|
519
|
582
|
#self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
|
|
520
|
|
- self.total_lines = total_lines
|
|
|
583
|
+ self.total_lines += total_lines
|
|
521
|
584
|
|
|
522
|
585
|
# Per-author statistics
|
|
523
|
586
|
|
|
|
@@ -527,7 +590,7 @@ class GitDataCollector(DataCollector):
|
|
527
|
590
|
# Similar to the above, but never use --first-parent
|
|
528
|
591
|
# (we need to walk through every commit to know who
|
|
529
|
592
|
# committed what, not just through mainline)
|
|
530
|
|
- lines = getpipeoutput(['git log --shortstat --date-order --pretty=format:"%%at %%aN" %s' % (getcommitrange('HEAD'))]).split('\n')
|
|
|
593
|
+ lines = getpipeoutput(['git log --shortstat --date-order --pretty=format:"%%at %%aN" %s' % (getlogrange('HEAD'))]).split('\n')
|
|
531
|
594
|
lines.reverse()
|
|
532
|
595
|
files = 0; inserted = 0; deleted = 0
|
|
533
|
596
|
author = None
|
|
|
@@ -543,8 +606,6 @@ class GitDataCollector(DataCollector):
|
|
543
|
606
|
try:
|
|
544
|
607
|
oldstamp = stamp
|
|
545
|
608
|
(stamp, author) = (int(line[:pos]), line[pos+1:])
|
|
546
|
|
- if author in conf['merge_authors']:
|
|
547
|
|
- author = conf['merge_authors'][author]
|
|
548
|
609
|
if oldstamp > stamp:
|
|
549
|
610
|
# clock skew, keep old timestamp to avoid having ugly graph
|
|
550
|
611
|
stamp = oldstamp
|
|
|
@@ -619,33 +680,12 @@ class GitDataCollector(DataCollector):
|
|
619
|
680
|
def getDomains(self):
|
|
620
|
681
|
return self.domains.keys()
|
|
621
|
682
|
|
|
622
|
|
- def getFilesInCommit(self, rev):
|
|
623
|
|
- try:
|
|
624
|
|
- res = self.cache['files_in_tree'][rev]
|
|
625
|
|
- except:
|
|
626
|
|
- res = int(getpipeoutput(['git ls-tree -r --name-only "%s"' % rev, 'wc -l']).split('\n')[0])
|
|
627
|
|
- if 'files_in_tree' not in self.cache:
|
|
628
|
|
- self.cache['files_in_tree'] = {}
|
|
629
|
|
- self.cache['files_in_tree'][rev] = res
|
|
630
|
|
-
|
|
631
|
|
- return res
|
|
632
|
|
-
|
|
633
|
683
|
def getFirstCommitDate(self):
|
|
634
|
684
|
return datetime.datetime.fromtimestamp(self.first_commit_stamp)
|
|
635
|
685
|
|
|
636
|
686
|
def getLastCommitDate(self):
|
|
637
|
687
|
return datetime.datetime.fromtimestamp(self.last_commit_stamp)
|
|
638
|
688
|
|
|
639
|
|
- def getLinesInBlob(self, sha1):
|
|
640
|
|
- try:
|
|
641
|
|
- res = self.cache['lines_in_blob'][sha1]
|
|
642
|
|
- except:
|
|
643
|
|
- res = int(getpipeoutput(['git cat-file blob %s' % sha1, 'wc -l']).split()[0])
|
|
644
|
|
- if 'lines_in_blob' not in self.cache:
|
|
645
|
|
- self.cache['lines_in_blob'] = {}
|
|
646
|
|
- self.cache['lines_in_blob'][sha1] = res
|
|
647
|
|
- return res
|
|
648
|
|
-
|
|
649
|
689
|
def getTags(self):
|
|
650
|
690
|
lines = getpipeoutput(['git show-ref --tags', 'cut -d/ -f3'])
|
|
651
|
691
|
return lines.split('\n')
|
|
|
@@ -686,7 +726,7 @@ def html_linkify(text):
|
|
686
|
726
|
|
|
687
|
727
|
def html_header(level, text):
|
|
688
|
728
|
name = html_linkify(text)
|
|
689
|
|
- return '\n<h%d><a href="#%s" name="%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
|
|
|
729
|
+ return '\n<h%d id="%s"><a href="#%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
|
|
690
|
730
|
|
|
691
|
731
|
class HTMLReportCreator(ReportCreator):
|
|
692
|
732
|
def create(self, data, path):
|
|
|
@@ -827,7 +867,7 @@ class HTMLReportCreator(ReportCreator):
|
|
827
|
867
|
f.write('<td>0</td>')
|
|
828
|
868
|
f.write('</tr>')
|
|
829
|
869
|
f.write('</table></div>')
|
|
830
|
|
- f.write('<img src="day_of_week.png" alt="Day of Week" />')
|
|
|
870
|
+ f.write('<img src="day_of_week.png" alt="Day of Week">')
|
|
831
|
871
|
fp.close()
|
|
832
|
872
|
|
|
833
|
873
|
# Hour of Week
|
|
|
@@ -870,7 +910,7 @@ class HTMLReportCreator(ReportCreator):
|
|
870
|
910
|
fp.write('%d %d\n' % (mm, commits))
|
|
871
|
911
|
fp.close()
|
|
872
|
912
|
f.write('</table></div>')
|
|
873
|
|
- f.write('<img src="month_of_year.png" alt="Month of Year" />')
|
|
|
913
|
+ f.write('<img src="month_of_year.png" alt="Month of Year">')
|
|
874
|
914
|
|
|
875
|
915
|
# Commits by year/month
|
|
876
|
916
|
f.write(html_header(2, 'Commits by year/month'))
|
|
|
@@ -878,7 +918,7 @@ class HTMLReportCreator(ReportCreator):
|
|
878
|
918
|
for yymm in reversed(sorted(data.commits_by_month.keys())):
|
|
879
|
919
|
f.write('<tr><td>%s</td><td>%d</td><td>%d</td><td>%d</td></tr>' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0)))
|
|
880
|
920
|
f.write('</table></div>')
|
|
881
|
|
- f.write('<img src="commits_by_year_month.png" alt="Commits by year/month" />')
|
|
|
921
|
+ f.write('<img src="commits_by_year_month.png" alt="Commits by year/month">')
|
|
882
|
922
|
fg = open(path + '/commits_by_year_month.dat', 'w')
|
|
883
|
923
|
for yymm in sorted(data.commits_by_month.keys()):
|
|
884
|
924
|
fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
|
|
|
@@ -890,7 +930,7 @@ class HTMLReportCreator(ReportCreator):
|
|
890
|
930
|
for yy in reversed(sorted(data.commits_by_year.keys())):
|
|
891
|
931
|
f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d</td><td>%d</td></tr>' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0)))
|
|
892
|
932
|
f.write('</table></div>')
|
|
893
|
|
- f.write('<img src="commits_by_year.png" alt="Commits by Year" />')
|
|
|
933
|
+ f.write('<img src="commits_by_year.png" alt="Commits by Year">')
|
|
894
|
934
|
fg = open(path + '/commits_by_year.dat', 'w')
|
|
895
|
935
|
for yy in sorted(data.commits_by_year.keys()):
|
|
896
|
936
|
fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
|
|
|
@@ -900,13 +940,13 @@ class HTMLReportCreator(ReportCreator):
|
|
900
|
940
|
f.write(html_header(2, 'Commits by Timezone'))
|
|
901
|
941
|
f.write('<div><div class="card-body"><table><tr>')
|
|
902
|
942
|
f.write('<th>Timezone</th><th>Commits</th>')
|
|
|
943
|
+ f.write('</tr>')
|
|
903
|
944
|
max_commits_on_tz = max(data.commits_by_timezone.values())
|
|
904
|
945
|
for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)):
|
|
905
|
946
|
commits = data.commits_by_timezone[i]
|
|
906
|
947
|
r = 127 + int((float(commits) / max_commits_on_tz) * 128)
|
|
907
|
948
|
f.write('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
|
|
908
|
949
|
f.write('</tr></table></div>')
|
|
909
|
|
-
|
|
910
|
950
|
f.write('</body></html>')
|
|
911
|
951
|
f.close()
|
|
912
|
952
|
|
|
|
@@ -934,12 +974,12 @@ class HTMLReportCreator(ReportCreator):
|
|
934
|
974
|
f.write('<p class="moreauthors">These didn\'t make it to the top: %s</p>' % ', '.join(rest))
|
|
935
|
975
|
|
|
936
|
976
|
f.write(html_header(2, 'Cumulated Added Lines of Code per Author'))
|
|
937
|
|
- f.write('<img src="lines_of_code_by_author.png" alt="Lines of code per Author" />')
|
|
|
977
|
+ f.write('<img src="lines_of_code_by_author.png" alt="Lines of code per Author">')
|
|
938
|
978
|
if len(allauthors) > conf['max_authors']:
|
|
939
|
979
|
f.write('<p class="moreauthors">Only top %d authors shown</p>' % conf['max_authors'])
|
|
940
|
980
|
|
|
941
|
981
|
f.write(html_header(2, 'Commits per Author'))
|
|
942
|
|
- f.write('<img src="commits_by_author.png" alt="Commits per Author" />')
|
|
|
982
|
+ f.write('<img src="commits_by_author.png" alt="Commits per Author">')
|
|
943
|
983
|
if len(allauthors) > conf['max_authors']:
|
|
944
|
984
|
f.write('<p class="moreauthors">Only top %d authors shown</p>' % conf['max_authors'])
|
|
945
|
985
|
|
|
|
@@ -1017,7 +1057,7 @@ class HTMLReportCreator(ReportCreator):
|
|
1017
|
1057
|
fp.write('%s %d %d\n' % (domain, n , info['commits']))
|
|
1018
|
1058
|
f.write('<tr><th>%s</th><td>%d (%.2f%%)</td></tr>' % (domain, info['commits'], (100.0 * info['commits'] / totalcommits)))
|
|
1019
|
1059
|
f.write('</table></div>')
|
|
1020
|
|
- f.write('<img src="domains.png" alt="Commits by Domains" />')
|
|
|
1060
|
+ f.write('<img src="domains.png" alt="Commits by Domains">')
|
|
1021
|
1061
|
fp.close()
|
|
1022
|
1062
|
|
|
1023
|
1063
|
f.write('</body></html>')
|
|
|
@@ -1054,7 +1094,7 @@ class HTMLReportCreator(ReportCreator):
|
|
1054
|
1094
|
# fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
|
|
1055
|
1095
|
fg.close()
|
|
1056
|
1096
|
|
|
1057
|
|
- f.write('<img src="files_by_date.png" alt="Files by Date" />')
|
|
|
1097
|
+ f.write('<img src="files_by_date.png" alt="Files by Date">')
|
|
1058
|
1098
|
|
|
1059
|
1099
|
#f.write('<h2>Average file size by date</h2>')
|
|
1060
|
1100
|
|
|
|
@@ -1086,7 +1126,7 @@ class HTMLReportCreator(ReportCreator):
|
|
1086
|
1126
|
f.write('</dl></div>\n')
|
|
1087
|
1127
|
|
|
1088
|
1128
|
f.write(html_header(2, 'Lines of Code'))
|
|
1089
|
|
- f.write('<img src="lines_of_code.png" />')
|
|
|
1129
|
+ f.write('<img src="lines_of_code.png" alt="Lines of Code">')
|
|
1090
|
1130
|
|
|
1091
|
1131
|
fg = open(path + '/lines_of_code.dat', 'w')
|
|
1092
|
1132
|
for stamp in sorted(data.changes_by_date.keys()):
|
|
|
@@ -1137,6 +1177,7 @@ class HTMLReportCreator(ReportCreator):
|
|
1137
|
1177
|
set output 'hour_of_day.png'
|
|
1138
|
1178
|
unset key
|
|
1139
|
1179
|
set xrange [0.5:24.5]
|
|
|
1180
|
+set yrange [0:]
|
|
1140
|
1181
|
set xtics 4
|
|
1141
|
1182
|
set grid y
|
|
1142
|
1183
|
set ylabel "Commits"
|
|
|
@@ -1152,6 +1193,7 @@ plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
|
|
1152
|
1193
|
set output 'day_of_week.png'
|
|
1153
|
1194
|
unset key
|
|
1154
|
1195
|
set xrange [0.5:7.5]
|
|
|
1196
|
+set yrange [0:]
|
|
1155
|
1197
|
set xtics 1
|
|
1156
|
1198
|
set grid y
|
|
1157
|
1199
|
set ylabel "Commits"
|
|
|
@@ -1182,6 +1224,7 @@ plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labe
|
|
1182
|
1224
|
set output 'month_of_year.png'
|
|
1183
|
1225
|
unset key
|
|
1184
|
1226
|
set xrange [0.5:12.5]
|
|
|
1227
|
+set yrange [0:]
|
|
1185
|
1228
|
set xtics 1
|
|
1186
|
1229
|
set grid y
|
|
1187
|
1230
|
set ylabel "Commits"
|
|
|
@@ -1196,6 +1239,7 @@ plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid
|
|
1196
|
1239
|
"""
|
|
1197
|
1240
|
set output 'commits_by_year_month.png'
|
|
1198
|
1241
|
unset key
|
|
|
1242
|
+set yrange [0:]
|
|
1199
|
1243
|
set xdata time
|
|
1200
|
1244
|
set timefmt "%Y-%m"
|
|
1201
|
1245
|
set format x "%Y-%m"
|
|
|
@@ -1214,6 +1258,7 @@ plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
|
|
1214
|
1258
|
"""
|
|
1215
|
1259
|
set output 'commits_by_year.png'
|
|
1216
|
1260
|
unset key
|
|
|
1261
|
+set yrange [0:]
|
|
1217
|
1262
|
set xtics 1 rotate
|
|
1218
|
1263
|
set grid y
|
|
1219
|
1264
|
set ylabel "Commits"
|
|
|
@@ -1229,6 +1274,7 @@ plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
|
|
1229
|
1274
|
"""
|
|
1230
|
1275
|
set output 'files_by_date.png'
|
|
1231
|
1276
|
unset key
|
|
|
1277
|
+set yrange [0:]
|
|
1232
|
1278
|
set xdata time
|
|
1233
|
1279
|
set timefmt "%Y-%m-%d"
|
|
1234
|
1280
|
set format x "%Y-%m-%d"
|
|
|
@@ -1248,6 +1294,7 @@ plot 'files_by_date.dat' using 1:2 w steps
|
|
1248
|
1294
|
"""
|
|
1249
|
1295
|
set output 'lines_of_code.png'
|
|
1250
|
1296
|
unset key
|
|
|
1297
|
+set yrange [0:]
|
|
1251
|
1298
|
set xdata time
|
|
1252
|
1299
|
set timefmt "%s"
|
|
1253
|
1300
|
set format x "%Y-%m-%d"
|
|
|
@@ -1267,6 +1314,7 @@ plot 'lines_of_code.dat' using 1:2 w lines
|
|
1267
|
1314
|
set terminal png transparent size 640,480
|
|
1268
|
1315
|
set output 'lines_of_code_by_author.png'
|
|
1269
|
1316
|
set key left top
|
|
|
1317
|
+set yrange [0:]
|
|
1270
|
1318
|
set xdata time
|
|
1271
|
1319
|
set timefmt "%s"
|
|
1272
|
1320
|
set format x "%Y-%m-%d"
|
|
|
@@ -1280,7 +1328,8 @@ plot """
|
|
1280
|
1328
|
plots = []
|
|
1281
|
1329
|
for a in self.authors_to_plot:
|
|
1282
|
1330
|
i = i + 1
|
|
1283
|
|
- plots.append("""'lines_of_code_by_author.dat' using 1:%d title "%s" w lines""" % (i, a.replace("\"", "\\\"")))
|
|
|
1331
|
+ author = a.replace("\"", "\\\"").replace("`", "")
|
|
|
1332
|
+ plots.append("""'lines_of_code_by_author.dat' using 1:%d title "%s" w lines""" % (i, author))
|
|
1284
|
1333
|
f.write(", ".join(plots))
|
|
1285
|
1334
|
f.write('\n')
|
|
1286
|
1335
|
|
|
|
@@ -1294,6 +1343,7 @@ plot """
|
|
1294
|
1343
|
set terminal png transparent size 640,480
|
|
1295
|
1344
|
set output 'commits_by_author.png'
|
|
1296
|
1345
|
set key left top
|
|
|
1346
|
+set yrange [0:]
|
|
1297
|
1347
|
set xdata time
|
|
1298
|
1348
|
set timefmt "%s"
|
|
1299
|
1349
|
set format x "%Y-%m-%d"
|
|
|
@@ -1307,7 +1357,8 @@ plot """
|
|
1307
|
1357
|
plots = []
|
|
1308
|
1358
|
for a in self.authors_to_plot:
|
|
1309
|
1359
|
i = i + 1
|
|
1310
|
|
- plots.append("""'commits_by_author.dat' using 1:%d title "%s" w lines""" % (i, a.replace("\"", "\\\"")))
|
|
|
1360
|
+ author = a.replace("\"", "\\\"").replace("`", "")
|
|
|
1361
|
+ plots.append("""'commits_by_author.dat' using 1:%d title "%s" w lines""" % (i, author))
|
|
1311
|
1362
|
f.write(", ".join(plots))
|
|
1312
|
1363
|
f.write('\n')
|
|
1313
|
1364
|
|
|
|
@@ -1322,10 +1373,10 @@ plot """
|
|
1322
|
1373
|
|
|
1323
|
1374
|
def printHeader(self, f, title = ''):
|
|
1324
|
1375
|
f.write(
|
|
1325
|
|
-"""<?xml version="1.0" encoding="UTF-8"?>
|
|
1326
|
|
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1327
|
|
-<html xmlns="http://www.w3.org/1999/xhtml">
|
|
|
1376
|
+"""<!DOCTYPE html>
|
|
|
1377
|
+<html>
|
|
1328
|
1378
|
<head>
|
|
|
1379
|
+ <meta charset="UTF-8">
|
|
1329
|
1380
|
<title>GitStats - %s</title>
|
|
1330
|
1381
|
<link rel="stylesheet" href="%s" type="text/css" />
|
|
1331
|
1382
|
<link rel="stylesheet" href="bootstrap.css" type="text/css" />
|
|
|
@@ -1350,10 +1401,23 @@ plot """
|
|
1350
|
1401
|
</nav>
|
|
1351
|
1402
|
""")
|
|
1352
|
1403
|
|
|
|
1404
|
+def usage():
|
|
|
1405
|
+ print """
|
|
|
1406
|
+Usage: gitstats [options] <gitpath..> <outputpath>
|
|
|
1407
|
+
|
|
|
1408
|
+Options:
|
|
|
1409
|
+-c key=value Override configuration value
|
|
|
1410
|
+
|
|
|
1411
|
+Default config values:
|
|
|
1412
|
+%s
|
|
|
1413
|
+
|
|
|
1414
|
+Please see the manual page for more details.
|
|
|
1415
|
+""" % conf
|
|
|
1416
|
+
|
|
1353
|
1417
|
|
|
1354
|
1418
|
class GitStats:
|
|
1355
|
1419
|
def run(self, args_orig):
|
|
1356
|
|
- optlist, args = getopt.getopt(args_orig, 'c:')
|
|
|
1420
|
+ optlist, args = getopt.getopt(args_orig, 'hc:', ["help"])
|
|
1357
|
1421
|
for o,v in optlist:
|
|
1358
|
1422
|
if o == '-c':
|
|
1359
|
1423
|
key, value = v.split('=', 1)
|
|
|
@@ -1361,22 +1425,14 @@ class GitStats:
|
|
1361
|
1425
|
raise KeyError('no such key "%s" in config' % key)
|
|
1362
|
1426
|
if isinstance(conf[key], int):
|
|
1363
|
1427
|
conf[key] = int(value)
|
|
1364
|
|
- elif isinstance(conf[key], dict):
|
|
1365
|
|
- kk,vv = value.split(',', 1)
|
|
1366
|
|
- conf[key][kk] = vv
|
|
1367
|
1428
|
else:
|
|
1368
|
1429
|
conf[key] = value
|
|
|
1430
|
+ elif o in ('-h', '--help'):
|
|
|
1431
|
+ usage()
|
|
|
1432
|
+ sys.exit()
|
|
1369
|
1433
|
|
|
1370
|
1434
|
if len(args) < 2:
|
|
1371
|
|
- print """
|
|
1372
|
|
-Usage: gitstats [options] <gitpath..> <outputpath>
|
|
1373
|
|
-
|
|
1374
|
|
-Options:
|
|
1375
|
|
--c key=value Override configuration value
|
|
1376
|
|
-
|
|
1377
|
|
-Default config values:
|
|
1378
|
|
-%s
|
|
1379
|
|
-""" % conf
|
|
|
1435
|
+ usage()
|
|
1380
|
1436
|
sys.exit(0)
|
|
1381
|
1437
|
|
|
1382
|
1438
|
outputpath = os.path.abspath(args[-1])
|
|
|
@@ -1403,11 +1459,14 @@ Default config values:
|
|
1403
|
1459
|
for gitpath in args[0:-1]:
|
|
1404
|
1460
|
print 'Git path: %s' % gitpath
|
|
1405
|
1461
|
|
|
|
1462
|
+ prevdir = os.getcwd()
|
|
1406
|
1463
|
os.chdir(gitpath)
|
|
1407
|
1464
|
|
|
1408
|
1465
|
print 'Collecting data...'
|
|
1409
|
1466
|
data.collect(gitpath)
|
|
1410
|
1467
|
|
|
|
1468
|
+ os.chdir(prevdir)
|
|
|
1469
|
+
|
|
1411
|
1470
|
print 'Refining data...'
|
|
1412
|
1471
|
data.saveCache(cachefile)
|
|
1413
|
1472
|
data.refine()
|