|
|
@@ -1,9 +1,10 @@
|
|
1
|
|
-#!/usr/bin/env python2
|
|
|
1
|
+#!/usr/bin/env python3
|
|
2
|
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
|
|
6
|
6
|
import glob
|
|
|
7
|
+import json
|
|
7
|
8
|
import os
|
|
8
|
9
|
import pickle
|
|
9
|
10
|
import platform
|
|
|
@@ -13,6 +14,7 @@ import subprocess
|
|
13
|
14
|
import sys
|
|
14
|
15
|
import time
|
|
15
|
16
|
import zlib
|
|
|
17
|
+import multiprocessing
|
|
16
|
18
|
|
|
17
|
19
|
if sys.version_info < (2, 6):
|
|
18
|
20
|
print >> sys.stderr, "Python 2.6 or higher is required for gitstats"
|
|
|
@@ -25,6 +27,7 @@ os.environ['LC_ALL'] = 'C'
|
|
25
|
27
|
GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n'
|
|
26
|
28
|
ON_LINUX = (platform.system() == 'Linux')
|
|
27
|
29
|
WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
|
|
|
30
|
+JSONFILE = 'gitstats.json'
|
|
28
|
31
|
|
|
29
|
32
|
exectime_internal = 0.0
|
|
30
|
33
|
exectime_external = 0.0
|
|
|
@@ -46,36 +49,46 @@ conf = {
|
|
46
|
49
|
'commit_end': 'HEAD',
|
|
47
|
50
|
'linear_linestats': 1,
|
|
48
|
51
|
'project_name': '',
|
|
49
|
|
- 'processes': 8,
|
|
50
|
|
- 'start_date': ''
|
|
|
52
|
+ 'processes': multiprocessing.cpu_count(),
|
|
|
53
|
+ 'start_date': '',
|
|
|
54
|
+ 'end_date': '',
|
|
|
55
|
+ 'excluded_authors': [],
|
|
|
56
|
+ 'excluded_prefixes': []
|
|
51
|
57
|
}
|
|
52
|
58
|
|
|
53
|
59
|
def getpipeoutput(cmds, quiet = False):
|
|
54
|
60
|
global exectime_external
|
|
55
|
61
|
start = time.time()
|
|
56
|
62
|
if not quiet and ON_LINUX and os.isatty(1):
|
|
57
|
|
- print '>> ' + ' | '.join(cmds),
|
|
|
63
|
+ print('>> ' + ' | '.join(cmds))
|
|
58
|
64
|
sys.stdout.flush()
|
|
59
|
65
|
p = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
|
|
60
|
66
|
processes=[p]
|
|
61
|
67
|
for x in cmds[1:]:
|
|
62
|
68
|
p = subprocess.Popen(x, stdin = p.stdout, stdout = subprocess.PIPE, shell = True)
|
|
63
|
69
|
processes.append(p)
|
|
64
|
|
- output = p.communicate()[0]
|
|
|
70
|
+ output = (p.communicate()[0]).decode("utf-8")
|
|
65
|
71
|
for p in processes:
|
|
66
|
72
|
p.wait()
|
|
67
|
73
|
end = time.time()
|
|
68
|
74
|
if not quiet:
|
|
69
|
75
|
if ON_LINUX and os.isatty(1):
|
|
70
|
|
- print '\r',
|
|
71
|
|
- print '[%.5f] >> %s' % (end - start, ' | '.join(cmds))
|
|
|
76
|
+ print('\r')
|
|
|
77
|
+ print('[%.5f] >> %s' % (end - start, ' | '.join(cmds)))
|
|
72
|
78
|
exectime_external += (end - start)
|
|
73
|
79
|
return output.rstrip('\n')
|
|
74
|
80
|
|
|
75
|
81
|
def getlogrange(defaultrange = 'HEAD', end_only = True):
|
|
76
|
82
|
commit_range = getcommitrange(defaultrange, end_only)
|
|
|
83
|
+ datesel = ''
|
|
77
|
84
|
if len(conf['start_date']) > 0:
|
|
78
|
|
- return '--since="%s" "%s"' % (conf['start_date'], commit_range)
|
|
|
85
|
+ datesel = '--since="%s" %s' % (conf['start_date'], datesel)
|
|
|
86
|
+ if len(conf['end_date']) > 0:
|
|
|
87
|
+ datesel = '--until="%s" %s' % (conf['end_date'], datesel)
|
|
|
88
|
+
|
|
|
89
|
+ if (len(datesel) > 0):
|
|
|
90
|
+ commit_range = '%s "%s"' % (datesel, commit_range)
|
|
|
91
|
+
|
|
79
|
92
|
return commit_range
|
|
80
|
93
|
|
|
81
|
94
|
def getcommitrange(defaultrange = 'HEAD', end_only = False):
|
|
|
@@ -86,11 +99,11 @@ def getcommitrange(defaultrange = 'HEAD', end_only = False):
|
|
86
|
99
|
return defaultrange
|
|
87
|
100
|
|
|
88
|
101
|
def getkeyssortedbyvalues(dict):
|
|
89
|
|
- return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
|
|
|
102
|
+ return list(map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items()))))
|
|
90
|
103
|
|
|
91
|
104
|
# dict['author'] = { 'commits': 512 } - ...key(dict, 'commits')
|
|
92
|
105
|
def getkeyssortedbyvaluekey(d, key):
|
|
93
|
|
- return map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys())))
|
|
|
106
|
+ return list(map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys()))))
|
|
94
|
107
|
|
|
95
|
108
|
def getstatsummarycounts(line):
|
|
96
|
109
|
numbers = re.findall('\d+', line)
|
|
|
@@ -147,6 +160,14 @@ class DataCollector:
|
|
147
|
160
|
self.activity_by_hour_of_week_busiest = 0
|
|
148
|
161
|
self.activity_by_year_week = {} # yy_wNN -> commits
|
|
149
|
162
|
self.activity_by_year_week_peak = 0
|
|
|
163
|
+ self.lineactivity_by_hour_of_day = {} # hour -> commits
|
|
|
164
|
+ self.lineactivity_by_day_of_week = {} # day -> commits
|
|
|
165
|
+ self.lineactivity_by_month_of_year = {} # month [1-12] -> commits
|
|
|
166
|
+ self.lineactivity_by_hour_of_week = {} # weekday -> hour -> commits
|
|
|
167
|
+ self.lineactivity_by_hour_of_day_busiest = 0
|
|
|
168
|
+ self.lineactivity_by_hour_of_week_busiest = 0
|
|
|
169
|
+ self.lineactivity_by_year_week = {} # yy_wNN -> commits
|
|
|
170
|
+ self.lineactivity_by_year_week_peak = 0
|
|
150
|
171
|
|
|
151
|
172
|
self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp, last_active_day, active_days, lines_added, lines_removed}
|
|
152
|
173
|
|
|
|
@@ -207,7 +228,7 @@ class DataCollector:
|
|
207
|
228
|
def loadCache(self, cachefile):
|
|
208
|
229
|
if not os.path.exists(cachefile):
|
|
209
|
230
|
return
|
|
210
|
|
- print 'Loading cache...'
|
|
|
231
|
+ print('Loading cache...')
|
|
211
|
232
|
f = open(cachefile, 'rb')
|
|
212
|
233
|
try:
|
|
213
|
234
|
self.cache = pickle.loads(zlib.decompress(f.read()))
|
|
|
@@ -232,6 +253,12 @@ class DataCollector:
|
|
232
|
253
|
|
|
233
|
254
|
def getActivityByHourOfDay(self):
|
|
234
|
255
|
return {}
|
|
|
256
|
+
|
|
|
257
|
+ def getLineActivityByDayOfWeek(self):
|
|
|
258
|
+ return {}
|
|
|
259
|
+
|
|
|
260
|
+ def getLineActivityByHourOfDay(self):
|
|
|
261
|
+ return {}
|
|
235
|
262
|
|
|
236
|
263
|
# : get a dictionary of domains
|
|
237
|
264
|
def getDomainInfo(self, domain):
|
|
|
@@ -263,13 +290,16 @@ class DataCollector:
|
|
263
|
290
|
def getTotalFiles(self):
|
|
264
|
291
|
return -1
|
|
265
|
292
|
|
|
|
293
|
+ def getTotalLines(self):
|
|
|
294
|
+ return -1
|
|
|
295
|
+
|
|
266
|
296
|
def getTotalLOC(self):
|
|
267
|
297
|
return -1
|
|
268
|
298
|
|
|
269
|
299
|
##
|
|
270
|
300
|
# Save cacheable data
|
|
271
|
301
|
def saveCache(self, cachefile):
|
|
272
|
|
- print 'Saving cache...'
|
|
|
302
|
+ print('Saving cache...')
|
|
273
|
303
|
tempfile = cachefile + '.tmp'
|
|
274
|
304
|
f = open(tempfile, 'wb')
|
|
275
|
305
|
#pickle.dump(self.cache, f)
|
|
|
@@ -308,7 +338,7 @@ class GitDataCollector(DataCollector):
|
|
308
|
338
|
self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), 'commits': 0, 'authors': {} }
|
|
309
|
339
|
|
|
310
|
340
|
# collect info on tags, starting from latest
|
|
311
|
|
- tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), self.tags.items()))))
|
|
|
341
|
+ tags_sorted_by_date_desc = list(map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), self.tags.items())))))
|
|
312
|
342
|
prev = None
|
|
313
|
343
|
for tag in reversed(tags_sorted_by_date_desc):
|
|
314
|
344
|
cmd = 'git shortlog -s "%s"' % tag
|
|
|
@@ -322,6 +352,8 @@ class GitDataCollector(DataCollector):
|
|
322
|
352
|
parts = re.split('\s+', line, 2)
|
|
323
|
353
|
commits = int(parts[1])
|
|
324
|
354
|
author = parts[2]
|
|
|
355
|
+ if author in conf["excluded_authors"]:
|
|
|
356
|
+ continue
|
|
325
|
357
|
self.tags[tag]['commits'] += commits
|
|
326
|
358
|
self.tags[tag]['authors'][author] = commits
|
|
327
|
359
|
|
|
|
@@ -338,6 +370,8 @@ class GitDataCollector(DataCollector):
|
|
338
|
370
|
timezone = parts[3]
|
|
339
|
371
|
author, mail = parts[4].split('<', 1)
|
|
340
|
372
|
author = author.rstrip()
|
|
|
373
|
+ if author in conf["excluded_authors"]:
|
|
|
374
|
+ continue
|
|
341
|
375
|
mail = mail.rstrip('>')
|
|
342
|
376
|
domain = '?'
|
|
343
|
377
|
if mail.find('@') != -1:
|
|
|
@@ -434,14 +468,18 @@ class GitDataCollector(DataCollector):
|
|
434
|
468
|
self.commits_by_timezone[timezone] = self.commits_by_timezone.get(timezone, 0) + 1
|
|
435
|
469
|
|
|
436
|
470
|
# outputs "<stamp> <files>" for each revision
|
|
437
|
|
- revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T" %s' % getlogrange('HEAD'), 'grep -v ^commit']).strip().split('\n')
|
|
|
471
|
+ revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T %%an" %s' % getlogrange('HEAD'), 'grep -v ^commit']).strip().split('\n')
|
|
438
|
472
|
lines = []
|
|
439
|
473
|
revs_to_read = []
|
|
440
|
474
|
time_rev_count = []
|
|
441
|
475
|
#Look up rev in cache and take info from cache if found
|
|
442
|
476
|
#If not append rev to list of rev to read from repo
|
|
443
|
477
|
for revline in revlines:
|
|
444
|
|
- time, rev = revline.split(' ')
|
|
|
478
|
+ _revline = revline.split(' ')
|
|
|
479
|
+ time, rev = _revline[:2]
|
|
|
480
|
+ author = ' '.join(_revline[2:])
|
|
|
481
|
+ if author in conf["excluded_authors"]:
|
|
|
482
|
+ continue
|
|
445
|
483
|
#if cache empty then add time and rev to list of new rev's
|
|
446
|
484
|
#otherwise try to read needed info from cache
|
|
447
|
485
|
if 'files_in_tree' not in self.cache.keys():
|
|
|
@@ -474,7 +512,7 @@ class GitDataCollector(DataCollector):
|
|
474
|
512
|
try:
|
|
475
|
513
|
self.files_by_stamp[int(stamp)] = int(files)
|
|
476
|
514
|
except ValueError:
|
|
477
|
|
- print 'Warning: failed to parse line "%s"' % line
|
|
|
515
|
+ print('Warning: failed to parse line "%s"' % line)
|
|
478
|
516
|
|
|
479
|
517
|
# extensions and size of files
|
|
480
|
518
|
lines = getpipeoutput(['git ls-tree -r -l -z %s' % getcommitrange('HEAD', end_only = True)]).split('\000')
|
|
|
@@ -489,6 +527,14 @@ class GitDataCollector(DataCollector):
|
|
489
|
527
|
blob_id = parts[2]
|
|
490
|
528
|
size = int(parts[3])
|
|
491
|
529
|
fullpath = parts[4]
|
|
|
530
|
+ exclude = False
|
|
|
531
|
+ for path in conf["excluded_prefixes"]:
|
|
|
532
|
+ if fullpath.startswith(path):
|
|
|
533
|
+ exclude = True
|
|
|
534
|
+ break
|
|
|
535
|
+
|
|
|
536
|
+ if exclude:
|
|
|
537
|
+ continue
|
|
492
|
538
|
|
|
493
|
539
|
self.total_size += size
|
|
494
|
540
|
self.total_files += 1
|
|
|
@@ -540,6 +586,7 @@ class GitDataCollector(DataCollector):
|
|
540
|
586
|
lines.reverse()
|
|
541
|
587
|
files = 0; inserted = 0; deleted = 0; total_lines = 0
|
|
542
|
588
|
author = None
|
|
|
589
|
+ last_line = ""
|
|
543
|
590
|
for line in lines:
|
|
544
|
591
|
if len(line) == 0:
|
|
545
|
592
|
continue
|
|
|
@@ -550,35 +597,72 @@ class GitDataCollector(DataCollector):
|
|
550
|
597
|
if pos != -1:
|
|
551
|
598
|
try:
|
|
552
|
599
|
(stamp, author) = (int(line[:pos]), line[pos+1:])
|
|
553
|
|
- self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
|
|
554
|
|
-
|
|
555
|
|
- date = datetime.datetime.fromtimestamp(stamp)
|
|
556
|
|
- yymm = date.strftime('%Y-%m')
|
|
557
|
|
- self.lines_added_by_month[yymm] = self.lines_added_by_month.get(yymm, 0) + inserted
|
|
558
|
|
- self.lines_removed_by_month[yymm] = self.lines_removed_by_month.get(yymm, 0) + deleted
|
|
559
|
|
-
|
|
560
|
|
- yy = date.year
|
|
561
|
|
- self.lines_added_by_year[yy] = self.lines_added_by_year.get(yy,0) + inserted
|
|
562
|
|
- self.lines_removed_by_year[yy] = self.lines_removed_by_year.get(yy, 0) + deleted
|
|
563
|
|
-
|
|
564
|
|
- files, inserted, deleted = 0, 0, 0
|
|
|
600
|
+ if author not in conf["excluded_authors"]:
|
|
|
601
|
+ self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
|
|
|
602
|
+
|
|
|
603
|
+ date = datetime.datetime.fromtimestamp(stamp)
|
|
|
604
|
+ yymm = date.strftime('%Y-%m')
|
|
|
605
|
+ self.lines_added_by_month[yymm] = self.lines_added_by_month.get(yymm, 0) + inserted
|
|
|
606
|
+ self.lines_removed_by_month[yymm] = self.lines_removed_by_month.get(yymm, 0) + deleted
|
|
|
607
|
+
|
|
|
608
|
+ yy = date.year
|
|
|
609
|
+ self.lines_added_by_year[yy] = self.lines_added_by_year.get(yy,0) + inserted
|
|
|
610
|
+ self.lines_removed_by_year[yy] = self.lines_removed_by_year.get(yy, 0) + deleted
|
|
|
611
|
+
|
|
|
612
|
+ # lineactivity
|
|
|
613
|
+ # hour
|
|
|
614
|
+ hour = date.hour
|
|
|
615
|
+ self.lineactivity_by_hour_of_day[hour] = self.lineactivity_by_hour_of_day.get(hour, 0) + inserted + deleted
|
|
|
616
|
+ # most active hour?
|
|
|
617
|
+ if self.lineactivity_by_hour_of_day[hour] > self.lineactivity_by_hour_of_day_busiest:
|
|
|
618
|
+ self.lineactivity_by_hour_of_day_busiest = self.lineactivity_by_hour_of_day[hour]
|
|
|
619
|
+
|
|
|
620
|
+ # day of week
|
|
|
621
|
+ day = date.weekday()
|
|
|
622
|
+ self.lineactivity_by_day_of_week[day] = self.lineactivity_by_day_of_week.get(day, 0) + inserted + deleted
|
|
|
623
|
+
|
|
|
624
|
+ # domain stats
|
|
|
625
|
+ #if domain not in self.domains:
|
|
|
626
|
+ #self.domains[domain] = {}
|
|
|
627
|
+ # lines
|
|
|
628
|
+ #self.domains[domain]['lines'] = self.domains[domain].get('lines', 0) + 1
|
|
|
629
|
+
|
|
|
630
|
+ # hour of week
|
|
|
631
|
+ if day not in self.lineactivity_by_hour_of_week:
|
|
|
632
|
+ self.lineactivity_by_hour_of_week[day] = {}
|
|
|
633
|
+ self.lineactivity_by_hour_of_week[day][hour] = self.lineactivity_by_hour_of_week[day].get(hour, 0) + inserted + deleted
|
|
|
634
|
+ # most active hour?
|
|
|
635
|
+ if self.lineactivity_by_hour_of_week[day][hour] > self.lineactivity_by_hour_of_week_busiest:
|
|
|
636
|
+ self.lineactivity_by_hour_of_week_busiest = self.lineactivity_by_hour_of_week[day][hour]
|
|
|
637
|
+
|
|
|
638
|
+ # month of year
|
|
|
639
|
+ month = date.month
|
|
|
640
|
+ self.lineactivity_by_month_of_year[month] = self.lineactivity_by_month_of_year.get(month, 0) + inserted + deleted
|
|
|
641
|
+
|
|
|
642
|
+ # yearly/weekly activity
|
|
|
643
|
+ yyw = date.strftime('%Y-%W')
|
|
|
644
|
+ self.lineactivity_by_year_week[yyw] = self.lineactivity_by_year_week.get(yyw, 0) + inserted + deleted
|
|
|
645
|
+ if self.lineactivity_by_year_week_peak < self.lineactivity_by_year_week[yyw]:
|
|
|
646
|
+ self.lineactivity_by_year_week_peak = self.lineactivity_by_year_week[yyw]
|
|
|
647
|
+
|
|
|
648
|
+ files, inserted, deleted = 0, 0, 0
|
|
|
649
|
+
|
|
|
650
|
+ numbers = getstatsummarycounts(last_line)
|
|
|
651
|
+ if len(numbers) == 3:
|
|
|
652
|
+ (files, inserted, deleted) = map(lambda el : int(el), numbers)
|
|
|
653
|
+ total_lines += inserted
|
|
|
654
|
+ total_lines -= deleted
|
|
|
655
|
+ self.total_lines_added += inserted
|
|
|
656
|
+ self.total_lines_removed += deleted
|
|
|
657
|
+ else:
|
|
|
658
|
+ print('Warning: failed to handle line "%s"' % line)
|
|
|
659
|
+ (files, inserted, deleted) = (0, 0, 0)
|
|
565
|
660
|
except ValueError:
|
|
566
|
|
- print 'Warning: unexpected line "%s"' % line
|
|
|
661
|
+ print('Warning: unexpected line "%s"' % line)
|
|
567
|
662
|
else:
|
|
568
|
|
- print 'Warning: unexpected line "%s"' % line
|
|
|
663
|
+ print('Warning: unexpected line "%s"' % line)
|
|
569
|
664
|
else:
|
|
570
|
|
- numbers = getstatsummarycounts(line)
|
|
571
|
|
-
|
|
572
|
|
- if len(numbers) == 3:
|
|
573
|
|
- (files, inserted, deleted) = map(lambda el : int(el), numbers)
|
|
574
|
|
- total_lines += inserted
|
|
575
|
|
- total_lines -= deleted
|
|
576
|
|
- self.total_lines_added += inserted
|
|
577
|
|
- self.total_lines_removed += deleted
|
|
578
|
|
-
|
|
579
|
|
- else:
|
|
580
|
|
- print 'Warning: failed to handle line "%s"' % line
|
|
581
|
|
- (files, inserted, deleted) = (0, 0, 0)
|
|
|
665
|
+ last_line = line
|
|
582
|
666
|
#self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
|
|
583
|
667
|
self.total_lines += total_lines
|
|
584
|
668
|
|
|
|
@@ -606,32 +690,33 @@ class GitDataCollector(DataCollector):
|
|
606
|
690
|
try:
|
|
607
|
691
|
oldstamp = stamp
|
|
608
|
692
|
(stamp, author) = (int(line[:pos]), line[pos+1:])
|
|
609
|
|
- if oldstamp > stamp:
|
|
610
|
|
- # clock skew, keep old timestamp to avoid having ugly graph
|
|
611
|
|
- stamp = oldstamp
|
|
612
|
|
- if author not in self.authors:
|
|
613
|
|
- self.authors[author] = { 'lines_added' : 0, 'lines_removed' : 0, 'commits' : 0}
|
|
614
|
|
- self.authors[author]['commits'] = self.authors[author].get('commits', 0) + 1
|
|
615
|
|
- self.authors[author]['lines_added'] = self.authors[author].get('lines_added', 0) + inserted
|
|
616
|
|
- self.authors[author]['lines_removed'] = self.authors[author].get('lines_removed', 0) + deleted
|
|
617
|
|
- if stamp not in self.changes_by_date_by_author:
|
|
618
|
|
- self.changes_by_date_by_author[stamp] = {}
|
|
619
|
|
- if author not in self.changes_by_date_by_author[stamp]:
|
|
620
|
|
- self.changes_by_date_by_author[stamp][author] = {}
|
|
621
|
|
- self.changes_by_date_by_author[stamp][author]['lines_added'] = self.authors[author]['lines_added']
|
|
622
|
|
- self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits']
|
|
623
|
|
- files, inserted, deleted = 0, 0, 0
|
|
|
693
|
+ if author not in conf["excluded_authors"]:
|
|
|
694
|
+ if oldstamp > stamp:
|
|
|
695
|
+ # clock skew, keep old timestamp to avoid having ugly graph
|
|
|
696
|
+ stamp = oldstamp
|
|
|
697
|
+ if author not in self.authors:
|
|
|
698
|
+ self.authors[author] = { 'lines_added' : 0, 'lines_removed' : 0, 'commits' : 0}
|
|
|
699
|
+ self.authors[author]['commits'] = self.authors[author].get('commits', 0) + 1
|
|
|
700
|
+ self.authors[author]['lines_added'] = self.authors[author].get('lines_added', 0) + inserted
|
|
|
701
|
+ self.authors[author]['lines_removed'] = self.authors[author].get('lines_removed', 0) + deleted
|
|
|
702
|
+ if stamp not in self.changes_by_date_by_author:
|
|
|
703
|
+ self.changes_by_date_by_author[stamp] = {}
|
|
|
704
|
+ if author not in self.changes_by_date_by_author[stamp]:
|
|
|
705
|
+ self.changes_by_date_by_author[stamp][author] = {}
|
|
|
706
|
+ self.changes_by_date_by_author[stamp][author]['lines_added'] = self.authors[author]['lines_added']
|
|
|
707
|
+ self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits']
|
|
|
708
|
+ files, inserted, deleted = 0, 0, 0
|
|
624
|
709
|
except ValueError:
|
|
625
|
|
- print 'Warning: unexpected line "%s"' % line
|
|
|
710
|
+ print('Warning: unexpected line "%s"' % line)
|
|
626
|
711
|
else:
|
|
627
|
|
- print 'Warning: unexpected line "%s"' % line
|
|
|
712
|
+ print('Warning: unexpected line "%s"' % line)
|
|
628
|
713
|
else:
|
|
629
|
714
|
numbers = getstatsummarycounts(line);
|
|
630
|
715
|
|
|
631
|
716
|
if len(numbers) == 3:
|
|
632
|
|
- (files, inserted, deleted) = map(lambda el : int(el), numbers)
|
|
|
717
|
+ (files, inserted, deleted) = list(map(lambda el : int(el), numbers))
|
|
633
|
718
|
else:
|
|
634
|
|
- print 'Warning: failed to handle line "%s"' % line
|
|
|
719
|
+ print('Warning: failed to handle line "%s"' % line)
|
|
635
|
720
|
(files, inserted, deleted) = (0, 0, 0)
|
|
636
|
721
|
|
|
637
|
722
|
def refine(self):
|
|
|
@@ -662,6 +747,12 @@ class GitDataCollector(DataCollector):
|
|
662
|
747
|
|
|
663
|
748
|
def getActivityByHourOfDay(self):
|
|
664
|
749
|
return self.activity_by_hour_of_day
|
|
|
750
|
+
|
|
|
751
|
+ def getLineActivityByDayOfWeek(self):
|
|
|
752
|
+ return self.lineactivity_by_day_of_week
|
|
|
753
|
+
|
|
|
754
|
+ def getLineActivityByHourOfDay(self):
|
|
|
755
|
+ return self.lineactivity_by_hour_of_day
|
|
665
|
756
|
|
|
666
|
757
|
def getAuthorInfo(self, author):
|
|
667
|
758
|
return self.authors[author]
|
|
|
@@ -704,6 +795,9 @@ class GitDataCollector(DataCollector):
|
|
704
|
795
|
|
|
705
|
796
|
def getTotalLOC(self):
|
|
706
|
797
|
return self.total_lines
|
|
|
798
|
+
|
|
|
799
|
+ def getTotalLines(self):
|
|
|
800
|
+ return self.total_lines_added + self.total_lines_removed
|
|
707
|
801
|
|
|
708
|
802
|
def getTotalSize(self):
|
|
709
|
803
|
return self.total_size
|
|
|
@@ -728,6 +822,24 @@ def html_header(level, text):
|
|
728
|
822
|
name = html_linkify(text)
|
|
729
|
823
|
return '\n<h%d id="%s"><a href="#%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
|
|
730
|
824
|
|
|
|
825
|
+
|
|
|
826
|
+class GitDataCollectorJSONEncoder(json.JSONEncoder):
|
|
|
827
|
+ def default(self, obj):
|
|
|
828
|
+ if isinstance(obj, set):
|
|
|
829
|
+ return list(obj)
|
|
|
830
|
+ if isinstance(obj, datetime.timedelta):
|
|
|
831
|
+ return str(obj)
|
|
|
832
|
+ if isinstance(obj, GitDataCollector):
|
|
|
833
|
+ return obj.__dict__
|
|
|
834
|
+ # Let the base class default method raise the TypeError
|
|
|
835
|
+ return json.JSONEncoder.default(self, obj)
|
|
|
836
|
+
|
|
|
837
|
+class JSONReportCreator(ReportCreator):
|
|
|
838
|
+ def create(self, data, filename):
|
|
|
839
|
+ f = open(filename, 'w')
|
|
|
840
|
+ json.dump(data, f, indent=True,
|
|
|
841
|
+ cls=GitDataCollectorJSONEncoder)
|
|
|
842
|
+ f.close()
|
|
731
|
843
|
class HTMLReportCreator(ReportCreator):
|
|
732
|
844
|
def create(self, data, path):
|
|
733
|
845
|
ReportCreator.create(self, data, path)
|
|
|
@@ -744,7 +856,7 @@ class HTMLReportCreator(ReportCreator):
|
|
744
|
856
|
shutil.copyfile(src, path + '/' + file)
|
|
745
|
857
|
break
|
|
746
|
858
|
else:
|
|
747
|
|
- print 'Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs)
|
|
|
859
|
+ print('Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs))
|
|
748
|
860
|
|
|
749
|
861
|
f = open(path + "/index.html", 'w')
|
|
750
|
862
|
format = '%Y-%m-%d %H:%M:%S'
|
|
|
@@ -754,7 +866,7 @@ class HTMLReportCreator(ReportCreator):
|
|
754
|
866
|
|
|
755
|
867
|
self.printNav(f)
|
|
756
|
868
|
|
|
757
|
|
- f.write('<dl>')
|
|
|
869
|
+ f.write('<div><div class="card-body"><dl>')
|
|
758
|
870
|
f.write('<dt>Project name</dt><dd>%s</dd>' % (data.projectname))
|
|
759
|
871
|
f.write('<dt>Generated</dt><dd>%s (in %d seconds)</dd>' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated()))
|
|
760
|
872
|
f.write('<dt>Generator</dt><dd><a href="http://gitstats.sourceforge.net/">GitStats</a> (version %s), %s, %s</dd>' % (getversion(), getgitversion(), getgnuplotversion()))
|
|
|
@@ -764,7 +876,7 @@ class HTMLReportCreator(ReportCreator):
|
|
764
|
876
|
f.write('<dt>Total Lines of Code</dt><dd>%s (%d added, %d removed)</dd>' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed))
|
|
765
|
877
|
f.write('<dt>Total Commits</dt><dd>%s (average %.1f commits per active day, %.1f per all days)</dd>' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays()))
|
|
766
|
878
|
f.write('<dt>Authors</dt><dd>%s (average %.1f commits per author)</dd>' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors()))
|
|
767
|
|
- f.write('</dl>')
|
|
|
879
|
+ f.write('</dl></div></div>')
|
|
768
|
880
|
|
|
769
|
881
|
f.write('</body>\n</html>')
|
|
770
|
882
|
f.close()
|
|
|
@@ -795,7 +907,7 @@ class HTMLReportCreator(ReportCreator):
|
|
795
|
907
|
stampcur -= deltaweek
|
|
796
|
908
|
|
|
797
|
909
|
# top row: commits & bar
|
|
798
|
|
- f.write('<table class="noborders"><tr>')
|
|
|
910
|
+ f.write('<div><div class="card-body"><table class="noborders"><tr>')
|
|
799
|
911
|
for i in range(0, WEEKS):
|
|
800
|
912
|
commits = 0
|
|
801
|
913
|
if weeks[i] in data.activity_by_year_week:
|
|
|
@@ -811,12 +923,12 @@ class HTMLReportCreator(ReportCreator):
|
|
811
|
923
|
f.write('</tr><tr>')
|
|
812
|
924
|
for i in range(0, WEEKS):
|
|
813
|
925
|
f.write('<td>%s</td>' % (WEEKS - i))
|
|
814
|
|
- f.write('</tr></table>')
|
|
|
926
|
+ f.write('</tr></table></div></div>')
|
|
815
|
927
|
|
|
816
|
928
|
# Hour of Day
|
|
817
|
929
|
f.write(html_header(2, 'Hour of Day'))
|
|
818
|
930
|
hour_of_day = data.getActivityByHourOfDay()
|
|
819
|
|
- f.write('<table><tr><th>Hour</th>')
|
|
|
931
|
+ f.write('<div><div class="card-body"><table><tr><th>Hour</th>')
|
|
820
|
932
|
for i in range(0, 24):
|
|
821
|
933
|
f.write('<th>%d</th>' % i)
|
|
822
|
934
|
f.write('</tr>\n<tr><th>Commits</th>')
|
|
|
@@ -895,7 +1007,7 @@ class HTMLReportCreator(ReportCreator):
|
|
895
|
1007
|
f.write('<td></td>')
|
|
896
|
1008
|
f.write('</tr>')
|
|
897
|
1009
|
|
|
898
|
|
- f.write('</table>')
|
|
|
1010
|
+ f.write('</table></div></div>')
|
|
899
|
1011
|
|
|
900
|
1012
|
# Month of Year
|
|
901
|
1013
|
f.write(html_header(2, 'Month of Year'))
|
|
|
@@ -938,7 +1050,7 @@ class HTMLReportCreator(ReportCreator):
|
|
938
|
1050
|
|
|
939
|
1051
|
# Commits by timezone
|
|
940
|
1052
|
f.write(html_header(2, 'Commits by Timezone'))
|
|
941
|
|
- f.write('<table><tr>')
|
|
|
1053
|
+ f.write('<div><div class="card-body"><table><tr>')
|
|
942
|
1054
|
f.write('<th>Timezone</th><th>Commits</th>')
|
|
943
|
1055
|
f.write('</tr>')
|
|
944
|
1056
|
max_commits_on_tz = max(data.commits_by_timezone.values())
|
|
|
@@ -946,7 +1058,7 @@ class HTMLReportCreator(ReportCreator):
|
|
946
|
1058
|
commits = data.commits_by_timezone[i]
|
|
947
|
1059
|
r = 127 + int((float(commits) / max_commits_on_tz) * 128)
|
|
948
|
1060
|
f.write('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
|
|
949
|
|
- f.write('</table>')
|
|
|
1061
|
+ f.write('</table></div>')
|
|
950
|
1062
|
|
|
951
|
1063
|
f.write('</body></html>')
|
|
952
|
1064
|
f.close()
|
|
|
@@ -962,12 +1074,12 @@ class HTMLReportCreator(ReportCreator):
|
|
962
|
1074
|
# Authors :: List of authors
|
|
963
|
1075
|
f.write(html_header(2, 'List of Authors'))
|
|
964
|
1076
|
|
|
965
|
|
- f.write('<table class="authors sortable" id="authors">')
|
|
|
1077
|
+ f.write('<div><div class="card-body"><table class="authors sortable" id="authors">')
|
|
966
|
1078
|
f.write('<tr><th>Author</th><th>Commits (%)</th><th>+ lines</th><th>- lines</th><th>First commit</th><th>Last commit</th><th class="unsortable">Age</th><th>Active days</th><th># by commits</th></tr>')
|
|
967
|
1079
|
for author in data.getAuthors(conf['max_authors']):
|
|
968
|
1080
|
info = data.getAuthorInfo(author)
|
|
969
|
1081
|
f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d</td><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%d</td><td>%d</td></tr>' % (author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'], info['date_first'], info['date_last'], info['timedelta'], len(info['active_days']), info['place_by_commits']))
|
|
970
|
|
- f.write('</table>')
|
|
|
1082
|
+ f.write('</table></div></div>')
|
|
971
|
1083
|
|
|
972
|
1084
|
allauthors = data.getAuthors()
|
|
973
|
1085
|
if len(allauthors) > conf['max_authors']:
|
|
|
@@ -1134,6 +1246,173 @@ class HTMLReportCreator(ReportCreator):
|
|
1134
|
1246
|
fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines']))
|
|
1135
|
1247
|
fg.close()
|
|
1136
|
1248
|
|
|
|
1249
|
+ # Weekly activity
|
|
|
1250
|
+ WEEKS = 32
|
|
|
1251
|
+ f.write(html_header(2, 'Weekly activity'))
|
|
|
1252
|
+ f.write('<p>Last %d weeks</p>' % WEEKS)
|
|
|
1253
|
+
|
|
|
1254
|
+ # generate weeks to show (previous N weeks from now)
|
|
|
1255
|
+ now = datetime.datetime.now()
|
|
|
1256
|
+ deltaweek = datetime.timedelta(7)
|
|
|
1257
|
+ weeks = []
|
|
|
1258
|
+ stampcur = now
|
|
|
1259
|
+ for i in range(0, WEEKS):
|
|
|
1260
|
+ weeks.insert(0, stampcur.strftime('%Y-%W'))
|
|
|
1261
|
+ stampcur -= deltaweek
|
|
|
1262
|
+
|
|
|
1263
|
+ # top row: commits & bar
|
|
|
1264
|
+ f.write('<table class="noborders"><tr>')
|
|
|
1265
|
+ for i in range(0, WEEKS):
|
|
|
1266
|
+ commits = 0
|
|
|
1267
|
+ if weeks[i] in data.lineactivity_by_year_week:
|
|
|
1268
|
+ commits = data.lineactivity_by_year_week[weeks[i]]
|
|
|
1269
|
+
|
|
|
1270
|
+ percentage = 0
|
|
|
1271
|
+ if weeks[i] in data.lineactivity_by_year_week:
|
|
|
1272
|
+ percentage = float(data.lineactivity_by_year_week[weeks[i]]) / data.lineactivity_by_year_week_peak
|
|
|
1273
|
+ height = max(1, int(200 * percentage))
|
|
|
1274
|
+ f.write('<td style="text-align: center; vertical-align: bottom">%d<div style="display: block; background-color: red; width: 20px; height: %dpx"></div></td>' % (commits, height))
|
|
|
1275
|
+
|
|
|
1276
|
+ # bottom row: year/week
|
|
|
1277
|
+ f.write('</tr><tr>')
|
|
|
1278
|
+ for i in range(0, WEEKS):
|
|
|
1279
|
+ f.write('<td>%s</td>' % (WEEKS - i))
|
|
|
1280
|
+ f.write('</tr></table>')
|
|
|
1281
|
+
|
|
|
1282
|
+ # Hour of Day
|
|
|
1283
|
+ f.write(html_header(2, 'Hour of Day'))
|
|
|
1284
|
+ hour_of_day = data.getLineActivityByHourOfDay()
|
|
|
1285
|
+ f.write('<table><tr><th>Hour</th>')
|
|
|
1286
|
+ for i in range(0, 24):
|
|
|
1287
|
+ f.write('<th>%d</th>' % i)
|
|
|
1288
|
+ f.write('</tr>\n<tr><th>Lines</th>')
|
|
|
1289
|
+ fp = open(path + '/line_hour_of_day.dat', 'w')
|
|
|
1290
|
+ for i in range(0, 24):
|
|
|
1291
|
+ if i in hour_of_day:
|
|
|
1292
|
+ r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128)
|
|
|
1293
|
+ f.write('<td style="background-color: rgb(%d, 0, 0)">%d</td>' % (r, hour_of_day[i]))
|
|
|
1294
|
+ fp.write('%d %d\n' % (i, hour_of_day[i]))
|
|
|
1295
|
+ else:
|
|
|
1296
|
+ f.write('<td>0</td>')
|
|
|
1297
|
+ fp.write('%d 0\n' % i)
|
|
|
1298
|
+ fp.close()
|
|
|
1299
|
+ f.write('</tr>\n<tr><th>%</th>')
|
|
|
1300
|
+ totallines = data.getTotalLines()
|
|
|
1301
|
+ for i in range(0, 24):
|
|
|
1302
|
+ if i in hour_of_day:
|
|
|
1303
|
+ r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128)
|
|
|
1304
|
+ f.write('<td style="background-color: rgb(%d, 0, 0)">%.2f</td>' % (r, (100.0 * hour_of_day[i]) / totallines))
|
|
|
1305
|
+ else:
|
|
|
1306
|
+ f.write('<td>0.00</td>')
|
|
|
1307
|
+ f.write('</tr></table>')
|
|
|
1308
|
+ f.write('<img src="line_hour_of_day.png" alt="Hour of Day" />')
|
|
|
1309
|
+ fg = open(path + '/line_hour_of_day.dat', 'w')
|
|
|
1310
|
+ for i in range(0, 24):
|
|
|
1311
|
+ if i in hour_of_day:
|
|
|
1312
|
+ fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
|
|
|
1313
|
+ else:
|
|
|
1314
|
+ fg.write('%d 0\n' % (i + 1))
|
|
|
1315
|
+ fg.close()
|
|
|
1316
|
+
|
|
|
1317
|
+ # Day of Week
|
|
|
1318
|
+ f.write(html_header(2, 'Day of Week'))
|
|
|
1319
|
+ day_of_week = data.getLineActivityByDayOfWeek()
|
|
|
1320
|
+ f.write('<div class="vtable"><table>')
|
|
|
1321
|
+ f.write('<tr><th>Day</th><th>Total (%)</th></tr>')
|
|
|
1322
|
+ fp = open(path + '/line_day_of_week.dat', 'w')
|
|
|
1323
|
+ for d in range(0, 7):
|
|
|
1324
|
+ commits = 0
|
|
|
1325
|
+ if d in day_of_week:
|
|
|
1326
|
+ commits = day_of_week[d]
|
|
|
1327
|
+ fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits))
|
|
|
1328
|
+ f.write('<tr>')
|
|
|
1329
|
+ f.write('<th>%s</th>' % (WEEKDAYS[d]))
|
|
|
1330
|
+ if d in day_of_week:
|
|
|
1331
|
+ f.write('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totallines))
|
|
|
1332
|
+ else:
|
|
|
1333
|
+ f.write('<td>0</td>')
|
|
|
1334
|
+ f.write('</tr>')
|
|
|
1335
|
+ f.write('</table></div>')
|
|
|
1336
|
+ f.write('<img src="line_day_of_week.png" alt="Day of Week" />')
|
|
|
1337
|
+ fp.close()
|
|
|
1338
|
+
|
|
|
1339
|
+ # Hour of Week
|
|
|
1340
|
+ f.write(html_header(2, 'Hour of Week'))
|
|
|
1341
|
+ f.write('<table>')
|
|
|
1342
|
+
|
|
|
1343
|
+ f.write('<tr><th>Weekday</th>')
|
|
|
1344
|
+ for hour in range(0, 24):
|
|
|
1345
|
+ f.write('<th>%d</th>' % (hour))
|
|
|
1346
|
+ f.write('</tr>')
|
|
|
1347
|
+
|
|
|
1348
|
+ for weekday in range(0, 7):
|
|
|
1349
|
+ f.write('<tr><th>%s</th>' % (WEEKDAYS[weekday]))
|
|
|
1350
|
+ for hour in range(0, 24):
|
|
|
1351
|
+ try:
|
|
|
1352
|
+ commits = data.lineactivity_by_hour_of_week[weekday][hour]
|
|
|
1353
|
+ except KeyError:
|
|
|
1354
|
+ commits = 0
|
|
|
1355
|
+ if commits != 0:
|
|
|
1356
|
+ f.write('<td')
|
|
|
1357
|
+ r = 127 + int((float(commits) / data.lineactivity_by_hour_of_week_busiest) * 128)
|
|
|
1358
|
+ f.write(' style="background-color: rgb(%d, 0, 0)"' % r)
|
|
|
1359
|
+ f.write('>%d</td>' % commits)
|
|
|
1360
|
+ else:
|
|
|
1361
|
+ f.write('<td></td>')
|
|
|
1362
|
+ f.write('</tr>')
|
|
|
1363
|
+
|
|
|
1364
|
+ f.write('</table>')
|
|
|
1365
|
+
|
|
|
1366
|
+ # Month of Year
|
|
|
1367
|
+ f.write(html_header(2, 'Month of Year'))
|
|
|
1368
|
+ f.write('<div class="vtable"><table>')
|
|
|
1369
|
+ f.write('<tr><th>Month</th><th>Lines (%)</th></tr>')
|
|
|
1370
|
+ fp = open (path + '/line_month_of_year.dat', 'w')
|
|
|
1371
|
+ for mm in range(1, 13):
|
|
|
1372
|
+ commits = 0
|
|
|
1373
|
+ if mm in data.lineactivity_by_month_of_year:
|
|
|
1374
|
+ commits = data.lineactivity_by_month_of_year[mm]
|
|
|
1375
|
+ f.write('<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalLines()))
|
|
|
1376
|
+ fp.write('%d %d\n' % (mm, commits))
|
|
|
1377
|
+ fp.close()
|
|
|
1378
|
+ f.write('</table></div>')
|
|
|
1379
|
+ f.write('<img src="line_month_of_year.png" alt="Month of Year" />')
|
|
|
1380
|
+
|
|
|
1381
|
+ # Lines by year/month
|
|
|
1382
|
+ f.write(html_header(2, 'Lines by year/month'))
|
|
|
1383
|
+ f.write('<div class="vtable"><table><tr><th>Month</th><th>Commits</th><th>Lines added</th><th>Lines removed</th></tr>')
|
|
|
1384
|
+ for yymm in reversed(sorted(data.commits_by_month.keys())):
|
|
|
1385
|
+ 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)))
|
|
|
1386
|
+ f.write('</table></div>')
|
|
|
1387
|
+ f.write('<img src="line_commits_by_year_month.png" alt="Commits by year/month" />')
|
|
|
1388
|
+ fg = open(path + '/line_commits_by_year_month.dat', 'w')
|
|
|
1389
|
+ for yymm in sorted(data.commits_by_month.keys()):
|
|
|
1390
|
+ fg.write('%s %s\n' % (yymm, data.lines_added_by_month.get(yymm, 0) + data.lines_removed_by_month.get(yymm, 0)))
|
|
|
1391
|
+ fg.close()
|
|
|
1392
|
+
|
|
|
1393
|
+ # Lines by year
|
|
|
1394
|
+ f.write(html_header(2, 'Lines by Year'))
|
|
|
1395
|
+ f.write('<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th><th>Lines added</th><th>Lines removed</th></tr>')
|
|
|
1396
|
+ for yy in reversed(sorted(data.commits_by_year.keys())):
|
|
|
1397
|
+ 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)))
|
|
|
1398
|
+ f.write('</table></div>')
|
|
|
1399
|
+ f.write('<img src="line_commits_by_year.png" alt="Commits by Year" />')
|
|
|
1400
|
+ fg = open(path + '/line_commits_by_year.dat', 'w')
|
|
|
1401
|
+ for yy in sorted(data.commits_by_year.keys()):
|
|
|
1402
|
+ fg.write('%d %d\n' % (yy, data.lines_added_by_year.get(yy,0) + data.lines_removed_by_year.get(yy,0)))
|
|
|
1403
|
+ fg.close()
|
|
|
1404
|
+
|
|
|
1405
|
+ # Commits by timezone
|
|
|
1406
|
+ f.write(html_header(2, 'Commits by Timezone'))
|
|
|
1407
|
+ f.write('<table><tr>')
|
|
|
1408
|
+ f.write('<th>Timezone</th><th>Commits</th>')
|
|
|
1409
|
+ max_commits_on_tz = max(data.commits_by_timezone.values())
|
|
|
1410
|
+ for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)):
|
|
|
1411
|
+ commits = data.commits_by_timezone[i]
|
|
|
1412
|
+ r = 127 + int((float(commits) / max_commits_on_tz) * 128)
|
|
|
1413
|
+ f.write('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
|
|
|
1414
|
+ f.write('</tr></table>')
|
|
|
1415
|
+
|
|
1137
|
1416
|
f.write('</body></html>')
|
|
1138
|
1417
|
f.close()
|
|
1139
|
1418
|
|
|
|
@@ -1153,7 +1432,7 @@ class HTMLReportCreator(ReportCreator):
|
|
1153
|
1432
|
f.write('<table class="tags">')
|
|
1154
|
1433
|
f.write('<tr><th>Name</th><th>Date</th><th>Commits</th><th>Authors</th></tr>')
|
|
1155
|
1434
|
# sort the tags by date desc
|
|
1156
|
|
- tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
|
|
|
1435
|
+ tags_sorted_by_date_desc = list(map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items())))))
|
|
1157
|
1436
|
for tag in tags_sorted_by_date_desc:
|
|
1158
|
1437
|
authorinfo = []
|
|
1159
|
1438
|
self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors'])
|
|
|
@@ -1168,7 +1447,7 @@ class HTMLReportCreator(ReportCreator):
|
|
1168
|
1447
|
self.createGraphs(path)
|
|
1169
|
1448
|
|
|
1170
|
1449
|
def createGraphs(self, path):
|
|
1171
|
|
- print 'Generating graphs...'
|
|
|
1450
|
+ print('Generating graphs...')
|
|
1172
|
1451
|
|
|
1173
|
1452
|
# hour of day
|
|
1174
|
1453
|
f = open(path + '/hour_of_day.plot', 'w')
|
|
|
@@ -1336,6 +1615,99 @@ plot """
|
|
1336
|
1615
|
|
|
1337
|
1616
|
f.close()
|
|
1338
|
1617
|
|
|
|
1618
|
+ # hour of day
|
|
|
1619
|
+ f = open(path + '/line_hour_of_day.plot', 'w')
|
|
|
1620
|
+ f.write(GNUPLOT_COMMON)
|
|
|
1621
|
+ f.write(
|
|
|
1622
|
+"""
|
|
|
1623
|
+set output 'line_hour_of_day.png'
|
|
|
1624
|
+unset key
|
|
|
1625
|
+set xrange [0.5:24.5]
|
|
|
1626
|
+set xtics 4
|
|
|
1627
|
+set grid y
|
|
|
1628
|
+set ylabel "Lines"
|
|
|
1629
|
+plot 'line_hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
1630
|
+""")
|
|
|
1631
|
+ f.close()
|
|
|
1632
|
+
|
|
|
1633
|
+ # day of week
|
|
|
1634
|
+ f = open(path + '/line_day_of_week.plot', 'w')
|
|
|
1635
|
+ f.write(GNUPLOT_COMMON)
|
|
|
1636
|
+ f.write(
|
|
|
1637
|
+"""
|
|
|
1638
|
+set output 'line_day_of_week.png'
|
|
|
1639
|
+unset key
|
|
|
1640
|
+set xrange [0.5:7.5]
|
|
|
1641
|
+set xtics 1
|
|
|
1642
|
+set grid y
|
|
|
1643
|
+set ylabel "Lines"
|
|
|
1644
|
+plot 'line_day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid
|
|
|
1645
|
+""")
|
|
|
1646
|
+ f.close()
|
|
|
1647
|
+
|
|
|
1648
|
+ # Domains
|
|
|
1649
|
+# f = open(path + '/domains.plot', 'w')
|
|
|
1650
|
+# f.write(GNUPLOT_COMMON)
|
|
|
1651
|
+# f.write(
|
|
|
1652
|
+#"""
|
|
|
1653
|
+#set output 'domains.png'
|
|
|
1654
|
+#unset key
|
|
|
1655
|
+#unset xtics
|
|
|
1656
|
+#set yrange [0:]
|
|
|
1657
|
+#set grid y
|
|
|
1658
|
+#set ylabel "Commits"
|
|
|
1659
|
+#plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1
|
|
|
1660
|
+#""")
|
|
|
1661
|
+# f.close()
|
|
|
1662
|
+
|
|
|
1663
|
+ # Month of Year
|
|
|
1664
|
+ f = open(path + '/line_month_of_year.plot', 'w')
|
|
|
1665
|
+ f.write(GNUPLOT_COMMON)
|
|
|
1666
|
+ f.write(
|
|
|
1667
|
+"""
|
|
|
1668
|
+set output 'line_month_of_year.png'
|
|
|
1669
|
+unset key
|
|
|
1670
|
+set xrange [0.5:12.5]
|
|
|
1671
|
+set xtics 1
|
|
|
1672
|
+set grid y
|
|
|
1673
|
+set ylabel "Lines"
|
|
|
1674
|
+plot 'line_month_of_year.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
1675
|
+""")
|
|
|
1676
|
+ f.close()
|
|
|
1677
|
+
|
|
|
1678
|
+ # commits_by_year_month
|
|
|
1679
|
+ f = open(path + '/line_commits_by_year_month.plot', 'w')
|
|
|
1680
|
+ f.write(GNUPLOT_COMMON)
|
|
|
1681
|
+ f.write(
|
|
|
1682
|
+"""
|
|
|
1683
|
+set output 'line_commits_by_year_month.png'
|
|
|
1684
|
+unset key
|
|
|
1685
|
+set xdata time
|
|
|
1686
|
+set timefmt "%Y-%m"
|
|
|
1687
|
+set format x "%Y-%m"
|
|
|
1688
|
+set xtics rotate
|
|
|
1689
|
+set bmargin 5
|
|
|
1690
|
+set grid y
|
|
|
1691
|
+set ylabel "Lines"
|
|
|
1692
|
+plot 'line_commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
1693
|
+""")
|
|
|
1694
|
+ f.close()
|
|
|
1695
|
+
|
|
|
1696
|
+ # commits_by_year
|
|
|
1697
|
+ f = open(path + '/line_commits_by_year.plot', 'w')
|
|
|
1698
|
+ f.write(GNUPLOT_COMMON)
|
|
|
1699
|
+ f.write(
|
|
|
1700
|
+"""
|
|
|
1701
|
+set output 'line_commits_by_year.png'
|
|
|
1702
|
+unset key
|
|
|
1703
|
+set xtics 1 rotate
|
|
|
1704
|
+set grid y
|
|
|
1705
|
+set ylabel "Lines"
|
|
|
1706
|
+set yrange [0:]
|
|
|
1707
|
+plot 'line_commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
1708
|
+""")
|
|
|
1709
|
+ f.close()
|
|
|
1710
|
+
|
|
1339
|
1711
|
# Commits per author
|
|
1340
|
1712
|
f = open(path + '/commits_by_author.plot', 'w')
|
|
1341
|
1713
|
f.write(GNUPLOT_COMMON)
|
|
|
@@ -1370,7 +1742,7 @@ plot """
|
|
1370
|
1742
|
for f in files:
|
|
1371
|
1743
|
out = getpipeoutput([gnuplot_cmd + ' "%s"' % f])
|
|
1372
|
1744
|
if len(out) > 0:
|
|
1373
|
|
- print out
|
|
|
1745
|
+ print(out)
|
|
1374
|
1746
|
|
|
1375
|
1747
|
def printHeader(self, f, title = ''):
|
|
1376
|
1748
|
f.write(
|
|
|
@@ -1379,8 +1751,8 @@ plot """
|
|
1379
|
1751
|
<head>
|
|
1380
|
1752
|
<meta charset="UTF-8">
|
|
1381
|
1753
|
<title>GitStats - %s</title>
|
|
1382
|
|
- <link rel="stylesheet" href="%s" type="text/css">
|
|
1383
|
|
- <meta name="generator" content="GitStats %s">
|
|
|
1754
|
+ <link rel="stylesheet" href="%s" type="text/css" />
|
|
|
1755
|
+ <meta name="generator" content="GitStats %s" />
|
|
1384
|
1756
|
<script type="text/javascript" src="sortable.js"></script>
|
|
1385
|
1757
|
</head>
|
|
1386
|
1758
|
<body>
|
|
|
@@ -1401,7 +1773,7 @@ plot """
|
|
1401
|
1773
|
""")
|
|
1402
|
1774
|
|
|
1403
|
1775
|
def usage():
|
|
1404
|
|
- print """
|
|
|
1776
|
+ text = """
|
|
1405
|
1777
|
Usage: gitstats [options] <gitpath..> <outputpath>
|
|
1406
|
1778
|
|
|
1407
|
1779
|
Options:
|
|
|
@@ -1412,6 +1784,7 @@ Default config values:
|
|
1412
|
1784
|
|
|
1413
|
1785
|
Please see the manual page for more details.
|
|
1414
|
1786
|
""" % conf
|
|
|
1787
|
+ print(text)
|
|
1415
|
1788
|
|
|
1416
|
1789
|
|
|
1417
|
1790
|
class GitStats:
|
|
|
@@ -1424,6 +1797,8 @@ class GitStats:
|
|
1424
|
1797
|
raise KeyError('no such key "%s" in config' % key)
|
|
1425
|
1798
|
if isinstance(conf[key], int):
|
|
1426
|
1799
|
conf[key] = int(value)
|
|
|
1800
|
+ elif isinstance(conf[key], list):
|
|
|
1801
|
+ conf[key].append(value)
|
|
1427
|
1802
|
else:
|
|
1428
|
1803
|
conf[key] = value
|
|
1429
|
1804
|
elif o in ('-h', '--help'):
|
|
|
@@ -1442,48 +1817,53 @@ class GitStats:
|
|
1442
|
1817
|
except OSError:
|
|
1443
|
1818
|
pass
|
|
1444
|
1819
|
if not os.path.isdir(outputpath):
|
|
1445
|
|
- print 'FATAL: Output path is not a directory or does not exist'
|
|
|
1820
|
+ print('FATAL: Output path is not a directory or does not exist')
|
|
1446
|
1821
|
sys.exit(1)
|
|
1447
|
1822
|
|
|
1448
|
1823
|
if not getgnuplotversion():
|
|
1449
|
|
- print 'gnuplot not found'
|
|
|
1824
|
+ print('gnuplot not found')
|
|
1450
|
1825
|
sys.exit(1)
|
|
1451
|
1826
|
|
|
1452
|
|
- print 'Output path: %s' % outputpath
|
|
|
1827
|
+ print('Output path: %s' % outputpath)
|
|
1453
|
1828
|
cachefile = os.path.join(outputpath, 'gitstats.cache')
|
|
1454
|
1829
|
|
|
1455
|
1830
|
data = GitDataCollector()
|
|
1456
|
1831
|
data.loadCache(cachefile)
|
|
1457
|
1832
|
|
|
1458
|
1833
|
for gitpath in args[0:-1]:
|
|
1459
|
|
- print 'Git path: %s' % gitpath
|
|
|
1834
|
+ print('Git path: %s' % gitpath)
|
|
1460
|
1835
|
|
|
1461
|
1836
|
prevdir = os.getcwd()
|
|
1462
|
1837
|
os.chdir(gitpath)
|
|
1463
|
1838
|
|
|
1464
|
|
- print 'Collecting data...'
|
|
|
1839
|
+ print('Collecting data...')
|
|
1465
|
1840
|
data.collect(gitpath)
|
|
1466
|
1841
|
|
|
1467
|
1842
|
os.chdir(prevdir)
|
|
1468
|
1843
|
|
|
1469
|
|
- print 'Refining data...'
|
|
|
1844
|
+ print('Refining data...')
|
|
1470
|
1845
|
data.saveCache(cachefile)
|
|
1471
|
1846
|
data.refine()
|
|
1472
|
1847
|
|
|
1473
|
1848
|
os.chdir(rundir)
|
|
1474
|
1849
|
|
|
1475
|
|
- print 'Generating report...'
|
|
|
1850
|
+ print('Generating HTML report...')
|
|
1476
|
1851
|
report = HTMLReportCreator()
|
|
1477
|
1852
|
report.create(data, outputpath)
|
|
1478
|
1853
|
|
|
|
1854
|
+ print('Generating JSON report...')
|
|
|
1855
|
+ report = JSONReportCreator()
|
|
|
1856
|
+ report.create(data, os.path.join(outputpath, JSONFILE))
|
|
|
1857
|
+
|
|
1479
|
1858
|
time_end = time.time()
|
|
1480
|
1859
|
exectime_internal = time_end - time_start
|
|
1481
|
|
- print 'Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal)
|
|
|
1860
|
+ print('Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal))
|
|
1482
|
1861
|
if sys.stdin.isatty():
|
|
1483
|
|
- print 'You may now run:'
|
|
1484
|
|
- print
|
|
1485
|
|
- print ' sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''")
|
|
1486
|
|
- print
|
|
|
1862
|
+ print('You may now run:')
|
|
|
1863
|
+ print()
|
|
|
1864
|
+ print(' sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''"))
|
|
|
1865
|
+ print(' sensible-notepad \'%s\'' % os.path.join(outputpath, JSONFILE).replace("'", "'\\''"))
|
|
|
1866
|
+ print()
|
|
1487
|
1867
|
|
|
1488
|
1868
|
if __name__=='__main__':
|
|
1489
|
1869
|
g = GitStats()
|