|
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+import datetime
|
|
|
2
|
+import glob
|
|
|
3
|
+import pkg_resources
|
|
|
4
|
+import os
|
|
|
5
|
+import shutil
|
|
|
6
|
+import time
|
|
|
7
|
+
|
|
|
8
|
+from .reportcreator import ReportCreator
|
|
|
9
|
+from .miscfuncs import getgitversion, getgnuplotversion, getkeyssortedbyvaluekey, getkeyssortedbyvalues, getpipeoutput, \
|
|
|
10
|
+ getversion, gnuplot_cmd
|
|
|
11
|
+
|
|
|
12
|
+GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n'
|
|
|
13
|
+WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
|
|
|
14
|
+
|
|
|
15
|
+def html_linkify(text):
|
|
|
16
|
+ return text.lower().replace(' ', '_')
|
|
|
17
|
+
|
|
|
18
|
+
|
|
|
19
|
+def html_header(level, text):
|
|
|
20
|
+ name = html_linkify(text)
|
|
|
21
|
+ return '\n<h%d id="%s"><a href="#%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
|
|
|
22
|
+
|
|
|
23
|
+
|
|
|
24
|
+class HTMLReportCreator(ReportCreator):
|
|
|
25
|
+ def __init(self, conf):
|
|
|
26
|
+ super(HTMLReportCreator, self).__init__(conf)
|
|
|
27
|
+
|
|
|
28
|
+ def create(self, data, path):
|
|
|
29
|
+ super(HTMLReportCreator, self).create(data, path)
|
|
|
30
|
+ self.title = data.projectname
|
|
|
31
|
+
|
|
|
32
|
+ # copy static files. Looks in the binary directory, ../share/gitstats and /usr/share/gitstats
|
|
|
33
|
+ resources = (self.conf['style'], 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif')
|
|
|
34
|
+ for resource in (self.conf['style'], 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif'):
|
|
|
35
|
+ resource_file = pkg_resources.resource_filename('gitstats', os.path.join('resources', resource))
|
|
|
36
|
+ if os.path.exists(resource_file):
|
|
|
37
|
+ shutil.copyfile(resource_file, os.path.join(path, resource))
|
|
|
38
|
+ else:
|
|
|
39
|
+ print(f'Warning: "{resource}" not found, so not copied')
|
|
|
40
|
+
|
|
|
41
|
+ f = open(path + "/index.html", 'w')
|
|
|
42
|
+ format = '%Y-%m-%d %H:%M:%S'
|
|
|
43
|
+ self.printHeader(f)
|
|
|
44
|
+
|
|
|
45
|
+ f.write('<h1>GitStats - %s</h1>' % data.projectname)
|
|
|
46
|
+
|
|
|
47
|
+ self.printNav(f)
|
|
|
48
|
+
|
|
|
49
|
+ f.write('<dl>')
|
|
|
50
|
+ f.write('<dt>Project name</dt><dd>%s</dd>' % (data.projectname))
|
|
|
51
|
+ f.write('<dt>Generated</dt><dd>%s (in %d seconds)</dd>' % (
|
|
|
52
|
+ datetime.datetime.now().strftime(format), time.time() - data.getStampCreated()))
|
|
|
53
|
+ f.write(
|
|
|
54
|
+ '<dt>Generator</dt><dd><a href="https://github.com/hoxu/gitstats">GitStats</a> (version %s), %s, %s</dd>' % (
|
|
|
55
|
+ getversion(), getgitversion(), getgnuplotversion()))
|
|
|
56
|
+ f.write('<dt>Report Period</dt><dd>%s to %s</dd>' % (
|
|
|
57
|
+ data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format)))
|
|
|
58
|
+ f.write('<dt>Age</dt><dd>%d days, %d active days (%3.2f%%)</dd>' % (
|
|
|
59
|
+ data.getCommitDeltaDays(), len(data.getActiveDays()),
|
|
|
60
|
+ (100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays())))
|
|
|
61
|
+ f.write('<dt>Total Files</dt><dd>%s</dd>' % data.getTotalFiles())
|
|
|
62
|
+ f.write('<dt>Total Lines of Code</dt><dd>%s (%d added, %d removed)</dd>' % (
|
|
|
63
|
+ data.getTotalLOC(), data.total_lines_added, data.total_lines_removed))
|
|
|
64
|
+ f.write('<dt>Total Commits</dt><dd>%s (average %.1f commits per active day, %.1f per all days)</dd>' % (
|
|
|
65
|
+ data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()),
|
|
|
66
|
+ float(data.getTotalCommits()) / data.getCommitDeltaDays()))
|
|
|
67
|
+ f.write('<dt>Authors</dt><dd>%s (average %.1f commits per author)</dd>' % (
|
|
|
68
|
+ data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors()))
|
|
|
69
|
+ f.write('</dl>')
|
|
|
70
|
+
|
|
|
71
|
+ f.write('</body>\n</html>')
|
|
|
72
|
+ f.close()
|
|
|
73
|
+
|
|
|
74
|
+ ###
|
|
|
75
|
+ # Activity
|
|
|
76
|
+ f = open(path + '/activity.html', 'w')
|
|
|
77
|
+ self.printHeader(f)
|
|
|
78
|
+ f.write('<h1>Activity</h1>')
|
|
|
79
|
+ self.printNav(f)
|
|
|
80
|
+
|
|
|
81
|
+ # f.write('<h2>Last 30 days</h2>')
|
|
|
82
|
+
|
|
|
83
|
+ # f.write('<h2>Last 12 months</h2>')
|
|
|
84
|
+
|
|
|
85
|
+ # Weekly activity
|
|
|
86
|
+ WEEKS = 32
|
|
|
87
|
+ f.write(html_header(2, 'Weekly activity'))
|
|
|
88
|
+ f.write('<p>Last %d weeks</p>' % WEEKS)
|
|
|
89
|
+
|
|
|
90
|
+ # generate weeks to show (previous N weeks from now)
|
|
|
91
|
+ now = datetime.datetime.now()
|
|
|
92
|
+ deltaweek = datetime.timedelta(7)
|
|
|
93
|
+ weeks = []
|
|
|
94
|
+ stampcur = now
|
|
|
95
|
+ for i in range(0, WEEKS):
|
|
|
96
|
+ weeks.insert(0, stampcur.strftime('%Y-%W'))
|
|
|
97
|
+ stampcur -= deltaweek
|
|
|
98
|
+
|
|
|
99
|
+ # top row: commits & bar
|
|
|
100
|
+ f.write('<table class="noborders"><tr>')
|
|
|
101
|
+ for i in range(0, WEEKS):
|
|
|
102
|
+ commits = 0
|
|
|
103
|
+ if weeks[i] in data.activity_by_year_week:
|
|
|
104
|
+ commits = data.activity_by_year_week[weeks[i]]
|
|
|
105
|
+
|
|
|
106
|
+ percentage = 0
|
|
|
107
|
+ if weeks[i] in data.activity_by_year_week:
|
|
|
108
|
+ percentage = float(data.activity_by_year_week[weeks[i]]) / data.activity_by_year_week_peak
|
|
|
109
|
+ height = max(1, int(200 * percentage))
|
|
|
110
|
+ f.write(
|
|
|
111
|
+ '<td style="text-align: center; vertical-align: bottom">%d<div style="display: block; background-color: red; width: 20px; height: %dpx"></div></td>' % (
|
|
|
112
|
+ commits, height))
|
|
|
113
|
+
|
|
|
114
|
+ # bottom row: year/week
|
|
|
115
|
+ f.write('</tr><tr>')
|
|
|
116
|
+ for i in range(0, WEEKS):
|
|
|
117
|
+ f.write('<td>%s</td>' % (WEEKS - i))
|
|
|
118
|
+ f.write('</tr></table>')
|
|
|
119
|
+
|
|
|
120
|
+ # Hour of Day
|
|
|
121
|
+ f.write(html_header(2, 'Hour of Day'))
|
|
|
122
|
+ hour_of_day = data.getActivityByHourOfDay()
|
|
|
123
|
+ f.write('<table><tr><th>Hour</th>')
|
|
|
124
|
+ for i in range(0, 24):
|
|
|
125
|
+ f.write('<th>%d</th>' % i)
|
|
|
126
|
+ f.write('</tr>\n<tr><th>Commits</th>')
|
|
|
127
|
+ fp = open(path + '/hour_of_day.dat', 'w')
|
|
|
128
|
+ for i in range(0, 24):
|
|
|
129
|
+ if i in hour_of_day:
|
|
|
130
|
+ r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128)
|
|
|
131
|
+ f.write('<td style="background-color: rgb(%d, 0, 0)">%d</td>' % (r, hour_of_day[i]))
|
|
|
132
|
+ fp.write('%d %d\n' % (i, hour_of_day[i]))
|
|
|
133
|
+ else:
|
|
|
134
|
+ f.write('<td>0</td>')
|
|
|
135
|
+ fp.write('%d 0\n' % i)
|
|
|
136
|
+ fp.close()
|
|
|
137
|
+ f.write('</tr>\n<tr><th>%</th>')
|
|
|
138
|
+ totalcommits = data.getTotalCommits()
|
|
|
139
|
+ for i in range(0, 24):
|
|
|
140
|
+ if i in hour_of_day:
|
|
|
141
|
+ r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128)
|
|
|
142
|
+ f.write('<td style="background-color: rgb(%d, 0, 0)">%.2f</td>' % (
|
|
|
143
|
+ r, (100.0 * hour_of_day[i]) / totalcommits))
|
|
|
144
|
+ else:
|
|
|
145
|
+ f.write('<td>0.00</td>')
|
|
|
146
|
+ f.write('</tr></table>')
|
|
|
147
|
+ f.write('<img src="hour_of_day.png" alt="Hour of Day">')
|
|
|
148
|
+ fg = open(path + '/hour_of_day.dat', 'w')
|
|
|
149
|
+ for i in range(0, 24):
|
|
|
150
|
+ if i in hour_of_day:
|
|
|
151
|
+ fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
|
|
|
152
|
+ else:
|
|
|
153
|
+ fg.write('%d 0\n' % (i + 1))
|
|
|
154
|
+ fg.close()
|
|
|
155
|
+
|
|
|
156
|
+ # Day of Week
|
|
|
157
|
+ f.write(html_header(2, 'Day of Week'))
|
|
|
158
|
+ day_of_week = data.getActivityByDayOfWeek()
|
|
|
159
|
+ f.write('<div class="vtable"><table>')
|
|
|
160
|
+ f.write('<tr><th>Day</th><th>Total (%)</th></tr>')
|
|
|
161
|
+ fp = open(path + '/day_of_week.dat', 'w')
|
|
|
162
|
+ for d in range(0, 7):
|
|
|
163
|
+ commits = 0
|
|
|
164
|
+ if d in day_of_week:
|
|
|
165
|
+ commits = day_of_week[d]
|
|
|
166
|
+ fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits))
|
|
|
167
|
+ f.write('<tr>')
|
|
|
168
|
+ f.write('<th>%s</th>' % (WEEKDAYS[d]))
|
|
|
169
|
+ if d in day_of_week:
|
|
|
170
|
+ f.write('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits))
|
|
|
171
|
+ else:
|
|
|
172
|
+ f.write('<td>0</td>')
|
|
|
173
|
+ f.write('</tr>')
|
|
|
174
|
+ f.write('</table></div>')
|
|
|
175
|
+ f.write('<img src="day_of_week.png" alt="Day of Week">')
|
|
|
176
|
+ fp.close()
|
|
|
177
|
+
|
|
|
178
|
+ # Hour of Week
|
|
|
179
|
+ f.write(html_header(2, 'Hour of Week'))
|
|
|
180
|
+ f.write('<table>')
|
|
|
181
|
+
|
|
|
182
|
+ f.write('<tr><th>Weekday</th>')
|
|
|
183
|
+ for hour in range(0, 24):
|
|
|
184
|
+ f.write('<th>%d</th>' % (hour))
|
|
|
185
|
+ f.write('</tr>')
|
|
|
186
|
+
|
|
|
187
|
+ for weekday in range(0, 7):
|
|
|
188
|
+ f.write('<tr><th>%s</th>' % (WEEKDAYS[weekday]))
|
|
|
189
|
+ for hour in range(0, 24):
|
|
|
190
|
+ try:
|
|
|
191
|
+ commits = data.activity_by_hour_of_week[weekday][hour]
|
|
|
192
|
+ except KeyError:
|
|
|
193
|
+ commits = 0
|
|
|
194
|
+ if commits != 0:
|
|
|
195
|
+ f.write('<td')
|
|
|
196
|
+ r = 127 + int((float(commits) / data.activity_by_hour_of_week_busiest) * 128)
|
|
|
197
|
+ f.write(' style="background-color: rgb(%d, 0, 0)"' % r)
|
|
|
198
|
+ f.write('>%d</td>' % commits)
|
|
|
199
|
+ else:
|
|
|
200
|
+ f.write('<td></td>')
|
|
|
201
|
+ f.write('</tr>')
|
|
|
202
|
+
|
|
|
203
|
+ f.write('</table>')
|
|
|
204
|
+
|
|
|
205
|
+ # Month of Year
|
|
|
206
|
+ f.write(html_header(2, 'Month of Year'))
|
|
|
207
|
+ f.write('<div class="vtable"><table>')
|
|
|
208
|
+ f.write('<tr><th>Month</th><th>Commits (%)</th></tr>')
|
|
|
209
|
+ fp = open(path + '/month_of_year.dat', 'w')
|
|
|
210
|
+ for mm in range(1, 13):
|
|
|
211
|
+ commits = 0
|
|
|
212
|
+ if mm in data.activity_by_month_of_year:
|
|
|
213
|
+ commits = data.activity_by_month_of_year[mm]
|
|
|
214
|
+ f.write(
|
|
|
215
|
+ '<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalCommits()))
|
|
|
216
|
+ fp.write('%d %d\n' % (mm, commits))
|
|
|
217
|
+ fp.close()
|
|
|
218
|
+ f.write('</table></div>')
|
|
|
219
|
+ f.write('<img src="month_of_year.png" alt="Month of Year">')
|
|
|
220
|
+
|
|
|
221
|
+ # Commits by year/month
|
|
|
222
|
+ f.write(html_header(2, 'Commits by year/month'))
|
|
|
223
|
+ f.write(
|
|
|
224
|
+ '<div class="vtable"><table><tr><th>Month</th><th>Commits</th><th>Lines added</th><th>Lines removed</th></tr>')
|
|
|
225
|
+ for yymm in reversed(sorted(data.commits_by_month.keys())):
|
|
|
226
|
+ f.write('<tr><td>%s</td><td>%d</td><td>%d</td><td>%d</td></tr>' % (
|
|
|
227
|
+ yymm, data.commits_by_month.get(yymm, 0), data.lines_added_by_month.get(yymm, 0),
|
|
|
228
|
+ data.lines_removed_by_month.get(yymm, 0)))
|
|
|
229
|
+ f.write('</table></div>')
|
|
|
230
|
+ f.write('<img src="commits_by_year_month.png" alt="Commits by year/month">')
|
|
|
231
|
+ fg = open(path + '/commits_by_year_month.dat', 'w')
|
|
|
232
|
+ for yymm in sorted(data.commits_by_month.keys()):
|
|
|
233
|
+ fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
|
|
|
234
|
+ fg.close()
|
|
|
235
|
+
|
|
|
236
|
+ # Commits by year
|
|
|
237
|
+ f.write(html_header(2, 'Commits by Year'))
|
|
|
238
|
+ f.write(
|
|
|
239
|
+ '<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th><th>Lines added</th><th>Lines removed</th></tr>')
|
|
|
240
|
+ for yy in reversed(sorted(data.commits_by_year.keys())):
|
|
|
241
|
+ f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d</td><td>%d</td></tr>' % (
|
|
|
242
|
+ yy, data.commits_by_year.get(yy, 0), (100.0 * data.commits_by_year.get(yy, 0)) / data.getTotalCommits(),
|
|
|
243
|
+ data.lines_added_by_year.get(yy, 0), data.lines_removed_by_year.get(yy, 0)))
|
|
|
244
|
+ f.write('</table></div>')
|
|
|
245
|
+ f.write('<img src="commits_by_year.png" alt="Commits by Year">')
|
|
|
246
|
+ fg = open(path + '/commits_by_year.dat', 'w')
|
|
|
247
|
+ for yy in sorted(data.commits_by_year.keys()):
|
|
|
248
|
+ fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
|
|
|
249
|
+ fg.close()
|
|
|
250
|
+
|
|
|
251
|
+ # Commits by timezone
|
|
|
252
|
+ f.write(html_header(2, 'Commits by Timezone'))
|
|
|
253
|
+ f.write('<table><tr>')
|
|
|
254
|
+ f.write('<th>Timezone</th><th>Commits</th>')
|
|
|
255
|
+ f.write('</tr>')
|
|
|
256
|
+ max_commits_on_tz = max(data.commits_by_timezone.values())
|
|
|
257
|
+ for i in sorted(data.commits_by_timezone.keys(), key=lambda n: int(n)):
|
|
|
258
|
+ commits = data.commits_by_timezone[i]
|
|
|
259
|
+ r = 127 + int((float(commits) / max_commits_on_tz) * 128)
|
|
|
260
|
+ f.write('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
|
|
|
261
|
+ f.write('</table>')
|
|
|
262
|
+
|
|
|
263
|
+ f.write('</body></html>')
|
|
|
264
|
+ f.close()
|
|
|
265
|
+
|
|
|
266
|
+ ###
|
|
|
267
|
+ # Authors
|
|
|
268
|
+ f = open(path + '/authors.html', 'w')
|
|
|
269
|
+ self.printHeader(f)
|
|
|
270
|
+
|
|
|
271
|
+ f.write('<h1>Authors</h1>')
|
|
|
272
|
+ self.printNav(f)
|
|
|
273
|
+
|
|
|
274
|
+ # Authors :: List of authors
|
|
|
275
|
+ f.write(html_header(2, 'List of Authors'))
|
|
|
276
|
+
|
|
|
277
|
+ f.write('<table class="authors sortable" id="authors">')
|
|
|
278
|
+ f.write(
|
|
|
279
|
+ '<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>')
|
|
|
280
|
+ for author in data.getAuthors(self.conf['max_authors']):
|
|
|
281
|
+ info = data.getAuthorInfo(author)
|
|
|
282
|
+ f.write(
|
|
|
283
|
+ '<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>' % (
|
|
|
284
|
+ author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'],
|
|
|
285
|
+ info['date_first'], info['date_last'], info['timedelta'], len(info['active_days']),
|
|
|
286
|
+ info['place_by_commits']))
|
|
|
287
|
+ f.write('</table>')
|
|
|
288
|
+
|
|
|
289
|
+ allauthors = data.getAuthors()
|
|
|
290
|
+ if len(allauthors) > self.conf['max_authors']:
|
|
|
291
|
+ rest = allauthors[self.conf['max_authors']:]
|
|
|
292
|
+ f.write('<p class="moreauthors">These didn\'t make it to the top: %s</p>' % ', '.join(rest))
|
|
|
293
|
+
|
|
|
294
|
+ f.write(html_header(2, 'Cumulated Added Lines of Code per Author'))
|
|
|
295
|
+ f.write('<img src="lines_of_code_by_author.png" alt="Lines of code per Author">')
|
|
|
296
|
+ if len(allauthors) > self.conf['max_authors']:
|
|
|
297
|
+ f.write('<p class="moreauthors">Only top %d authors shown</p>' % self.conf['max_authors'])
|
|
|
298
|
+
|
|
|
299
|
+ f.write(html_header(2, 'Commits per Author'))
|
|
|
300
|
+ f.write('<img src="commits_by_author.png" alt="Commits per Author">')
|
|
|
301
|
+ if len(allauthors) > self.conf['max_authors']:
|
|
|
302
|
+ f.write('<p class="moreauthors">Only top %d authors shown</p>' % self.conf['max_authors'])
|
|
|
303
|
+
|
|
|
304
|
+ fgl = open(path + '/lines_of_code_by_author.dat', 'w')
|
|
|
305
|
+ fgc = open(path + '/commits_by_author.dat', 'w')
|
|
|
306
|
+
|
|
|
307
|
+ lines_by_authors = {} # cumulated added lines by
|
|
|
308
|
+ # author. to save memory,
|
|
|
309
|
+ # changes_by_date_by_author[stamp][author] is defined
|
|
|
310
|
+ # only at points where author commits.
|
|
|
311
|
+ # lines_by_authors allows us to generate all the
|
|
|
312
|
+ # points in the .dat file.
|
|
|
313
|
+
|
|
|
314
|
+ # Don't rely on getAuthors to give the same order each
|
|
|
315
|
+ # time. Be robust and keep the list in a variable.
|
|
|
316
|
+ commits_by_authors = {} # cumulated added lines by
|
|
|
317
|
+
|
|
|
318
|
+ self.authors_to_plot = data.getAuthors(self.conf['max_authors'])
|
|
|
319
|
+ for author in self.authors_to_plot:
|
|
|
320
|
+ lines_by_authors[author] = 0
|
|
|
321
|
+ commits_by_authors[author] = 0
|
|
|
322
|
+ for stamp in sorted(data.changes_by_date_by_author.keys()):
|
|
|
323
|
+ fgl.write('%d' % stamp)
|
|
|
324
|
+ fgc.write('%d' % stamp)
|
|
|
325
|
+ for author in self.authors_to_plot:
|
|
|
326
|
+ if author in data.changes_by_date_by_author[stamp].keys():
|
|
|
327
|
+ lines_by_authors[author] = data.changes_by_date_by_author[stamp][author]['lines_added']
|
|
|
328
|
+ commits_by_authors[author] = data.changes_by_date_by_author[stamp][author]['commits']
|
|
|
329
|
+ fgl.write(' %d' % lines_by_authors[author])
|
|
|
330
|
+ fgc.write(' %d' % commits_by_authors[author])
|
|
|
331
|
+ fgl.write('\n')
|
|
|
332
|
+ fgc.write('\n')
|
|
|
333
|
+ fgl.close()
|
|
|
334
|
+ fgc.close()
|
|
|
335
|
+
|
|
|
336
|
+ # Authors :: Author of Month
|
|
|
337
|
+ f.write(html_header(2, 'Author of Month'))
|
|
|
338
|
+ f.write('<table class="sortable" id="aom">')
|
|
|
339
|
+ f.write(
|
|
|
340
|
+ '<tr><th>Month</th><th>Author</th><th>Commits (%%)</th><th class="unsortable">Next top %d</th><th>Number of authors</th></tr>' %
|
|
|
341
|
+ self.conf['authors_top'])
|
|
|
342
|
+ for yymm in reversed(sorted(data.author_of_month.keys())):
|
|
|
343
|
+ authordict = data.author_of_month[yymm]
|
|
|
344
|
+ authors = getkeyssortedbyvalues(authordict)
|
|
|
345
|
+ authors.reverse()
|
|
|
346
|
+ commits = data.author_of_month[yymm][authors[0]]
|
|
|
347
|
+ next = ', '.join(authors[1:self.conf['authors_top'] + 1])
|
|
|
348
|
+ f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td><td>%d</td></tr>' % (
|
|
|
349
|
+ yymm, authors[0], commits, (100.0 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm],
|
|
|
350
|
+ next, len(authors)))
|
|
|
351
|
+
|
|
|
352
|
+ f.write('</table>')
|
|
|
353
|
+
|
|
|
354
|
+ f.write(html_header(2, 'Author of Year'))
|
|
|
355
|
+ f.write(
|
|
|
356
|
+ '<table class="sortable" id="aoy"><tr><th>Year</th><th>Author</th><th>Commits (%%)</th><th class="unsortable">Next top %d</th><th>Number of authors</th></tr>' %
|
|
|
357
|
+ self.conf['authors_top'])
|
|
|
358
|
+ for yy in reversed(sorted(data.author_of_year.keys())):
|
|
|
359
|
+ authordict = data.author_of_year[yy]
|
|
|
360
|
+ authors = getkeyssortedbyvalues(authordict)
|
|
|
361
|
+ authors.reverse()
|
|
|
362
|
+ commits = data.author_of_year[yy][authors[0]]
|
|
|
363
|
+ next = ', '.join(authors[1:self.conf['authors_top'] + 1])
|
|
|
364
|
+ f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td><td>%d</td></tr>' % (
|
|
|
365
|
+ yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next,
|
|
|
366
|
+ len(authors)))
|
|
|
367
|
+ f.write('</table>')
|
|
|
368
|
+
|
|
|
369
|
+ # Domains
|
|
|
370
|
+ f.write(html_header(2, 'Commits by Domains'))
|
|
|
371
|
+ domains_by_commits = getkeyssortedbyvaluekey(data.domains, 'commits')
|
|
|
372
|
+ domains_by_commits.reverse() # most first
|
|
|
373
|
+ f.write('<div class="vtable"><table>')
|
|
|
374
|
+ f.write('<tr><th>Domains</th><th>Total (%)</th></tr>')
|
|
|
375
|
+ fp = open(path + '/domains.dat', 'w')
|
|
|
376
|
+ n = 0
|
|
|
377
|
+ for domain in domains_by_commits:
|
|
|
378
|
+ if n == self.conf['max_domains']:
|
|
|
379
|
+ break
|
|
|
380
|
+ commits = 0
|
|
|
381
|
+ n += 1
|
|
|
382
|
+ info = data.getDomainInfo(domain)
|
|
|
383
|
+ fp.write('%s %d %d\n' % (domain, n, info['commits']))
|
|
|
384
|
+ f.write('<tr><th>%s</th><td>%d (%.2f%%)</td></tr>' % (
|
|
|
385
|
+ domain, info['commits'], (100.0 * info['commits'] / totalcommits)))
|
|
|
386
|
+ f.write('</table></div>')
|
|
|
387
|
+ f.write('<img src="domains.png" alt="Commits by Domains">')
|
|
|
388
|
+ fp.close()
|
|
|
389
|
+
|
|
|
390
|
+ f.write('</body></html>')
|
|
|
391
|
+ f.close()
|
|
|
392
|
+
|
|
|
393
|
+ ###
|
|
|
394
|
+ # Files
|
|
|
395
|
+ f = open(path + '/files.html', 'w')
|
|
|
396
|
+ self.printHeader(f)
|
|
|
397
|
+ f.write('<h1>Files</h1>')
|
|
|
398
|
+ self.printNav(f)
|
|
|
399
|
+
|
|
|
400
|
+ f.write('<dl>\n')
|
|
|
401
|
+ f.write('<dt>Total files</dt><dd>%d</dd>' % data.getTotalFiles())
|
|
|
402
|
+ f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
|
|
|
403
|
+ try:
|
|
|
404
|
+ f.write(
|
|
|
405
|
+ '<dt>Average file size</dt><dd>%.2f bytes</dd>' % (float(data.getTotalSize()) / data.getTotalFiles()))
|
|
|
406
|
+ except ZeroDivisionError:
|
|
|
407
|
+ pass
|
|
|
408
|
+ f.write('</dl>\n')
|
|
|
409
|
+
|
|
|
410
|
+ # Files :: File count by date
|
|
|
411
|
+ f.write(html_header(2, 'File count by date'))
|
|
|
412
|
+
|
|
|
413
|
+ # use set to get rid of duplicate/unnecessary entries
|
|
|
414
|
+ files_by_date = set()
|
|
|
415
|
+ for stamp in sorted(data.files_by_stamp.keys()):
|
|
|
416
|
+ files_by_date.add(
|
|
|
417
|
+ '%s %d' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
|
|
|
418
|
+
|
|
|
419
|
+ fg = open(path + '/files_by_date.dat', 'w')
|
|
|
420
|
+ for line in sorted(list(files_by_date)):
|
|
|
421
|
+ fg.write('%s\n' % line)
|
|
|
422
|
+ # for stamp in sorted(data.files_by_stamp.keys()):
|
|
|
423
|
+ # fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
|
|
|
424
|
+ fg.close()
|
|
|
425
|
+
|
|
|
426
|
+ f.write('<img src="files_by_date.png" alt="Files by Date">')
|
|
|
427
|
+
|
|
|
428
|
+ # f.write('<h2>Average file size by date</h2>')
|
|
|
429
|
+
|
|
|
430
|
+ # Files :: Extensions
|
|
|
431
|
+ f.write(html_header(2, 'Extensions'))
|
|
|
432
|
+ f.write(
|
|
|
433
|
+ '<table class="sortable" id="ext"><tr><th>Extension</th><th>Files (%)</th><th>Lines (%)</th><th>Lines/file</th></tr>')
|
|
|
434
|
+ for ext in sorted(data.extensions.keys()):
|
|
|
435
|
+ files = data.extensions[ext]['files']
|
|
|
436
|
+ lines = data.extensions[ext]['lines']
|
|
|
437
|
+ try:
|
|
|
438
|
+ loc_percentage = (100.0 * lines) / data.getTotalLOC()
|
|
|
439
|
+ except ZeroDivisionError:
|
|
|
440
|
+ loc_percentage = 0
|
|
|
441
|
+ f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d (%.2f%%)</td><td>%d</td></tr>' % (
|
|
|
442
|
+ ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files))
|
|
|
443
|
+ f.write('</table>')
|
|
|
444
|
+
|
|
|
445
|
+ f.write('</body></html>')
|
|
|
446
|
+ f.close()
|
|
|
447
|
+
|
|
|
448
|
+ ###
|
|
|
449
|
+ # Lines
|
|
|
450
|
+ f = open(path + '/lines.html', 'w')
|
|
|
451
|
+ self.printHeader(f)
|
|
|
452
|
+ f.write('<h1>Lines</h1>')
|
|
|
453
|
+ self.printNav(f)
|
|
|
454
|
+
|
|
|
455
|
+ f.write('<dl>\n')
|
|
|
456
|
+ f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
|
|
|
457
|
+ f.write('</dl>\n')
|
|
|
458
|
+
|
|
|
459
|
+ f.write(html_header(2, 'Lines of Code'))
|
|
|
460
|
+ f.write('<img src="lines_of_code.png" alt="Lines of Code">')
|
|
|
461
|
+
|
|
|
462
|
+ fg = open(path + '/lines_of_code.dat', 'w')
|
|
|
463
|
+ for stamp in sorted(data.changes_by_date.keys()):
|
|
|
464
|
+ fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines']))
|
|
|
465
|
+ fg.close()
|
|
|
466
|
+
|
|
|
467
|
+ f.write('</body></html>')
|
|
|
468
|
+ f.close()
|
|
|
469
|
+
|
|
|
470
|
+ ###
|
|
|
471
|
+ # tags.html
|
|
|
472
|
+ f = open(path + '/tags.html', 'w')
|
|
|
473
|
+ self.printHeader(f)
|
|
|
474
|
+ f.write('<h1>Tags</h1>')
|
|
|
475
|
+ self.printNav(f)
|
|
|
476
|
+
|
|
|
477
|
+ f.write('<dl>')
|
|
|
478
|
+ f.write('<dt>Total tags</dt><dd>%d</dd>' % len(data.tags))
|
|
|
479
|
+ if len(data.tags) > 0:
|
|
|
480
|
+ f.write('<dt>Average commits per tag</dt><dd>%.2f</dd>' % (1.0 * data.getTotalCommits() / len(data.tags)))
|
|
|
481
|
+ f.write('</dl>')
|
|
|
482
|
+
|
|
|
483
|
+ f.write('<table class="tags">')
|
|
|
484
|
+ f.write('<tr><th>Name</th><th>Date</th><th>Commits</th><th>Authors</th></tr>')
|
|
|
485
|
+ # sort the tags by date desc
|
|
|
486
|
+ tags_sorted_by_date_desc = map(lambda el: el[1],
|
|
|
487
|
+ reversed(sorted(map(lambda el: (el[1]['date'], el[0]), data.tags.items()))))
|
|
|
488
|
+ for tag in tags_sorted_by_date_desc:
|
|
|
489
|
+ authorinfo = []
|
|
|
490
|
+ self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors'])
|
|
|
491
|
+ for i in reversed(self.authors_by_commits):
|
|
|
492
|
+ authorinfo.append('%s (%d)' % (i, data.tags[tag]['authors'][i]))
|
|
|
493
|
+ f.write('<tr><td>%s</td><td>%s</td><td>%d</td><td>%s</td></tr>' % (
|
|
|
494
|
+ tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo)))
|
|
|
495
|
+ f.write('</table>')
|
|
|
496
|
+
|
|
|
497
|
+ f.write('</body></html>')
|
|
|
498
|
+ f.close()
|
|
|
499
|
+
|
|
|
500
|
+ self.createGraphs(path)
|
|
|
501
|
+
|
|
|
502
|
+ def createGraphs(self, path):
|
|
|
503
|
+ print('Generating graphs...')
|
|
|
504
|
+
|
|
|
505
|
+ # hour of day
|
|
|
506
|
+ f = open(path + '/hour_of_day.plot', 'w')
|
|
|
507
|
+ f.write(GNUPLOT_COMMON)
|
|
|
508
|
+ f.write(
|
|
|
509
|
+ """
|
|
|
510
|
+ set output 'hour_of_day.png'
|
|
|
511
|
+ unset key
|
|
|
512
|
+ set xrange [0.5:24.5]
|
|
|
513
|
+ set yrange [0:]
|
|
|
514
|
+ set xtics 4
|
|
|
515
|
+ set grid y
|
|
|
516
|
+ set ylabel "Commits"
|
|
|
517
|
+ plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
518
|
+ """)
|
|
|
519
|
+ f.close()
|
|
|
520
|
+
|
|
|
521
|
+ # day of week
|
|
|
522
|
+ f = open(path + '/day_of_week.plot', 'w')
|
|
|
523
|
+ f.write(GNUPLOT_COMMON)
|
|
|
524
|
+ f.write(
|
|
|
525
|
+ """
|
|
|
526
|
+ set output 'day_of_week.png'
|
|
|
527
|
+ unset key
|
|
|
528
|
+ set xrange [0.5:7.5]
|
|
|
529
|
+ set yrange [0:]
|
|
|
530
|
+ set xtics 1
|
|
|
531
|
+ set grid y
|
|
|
532
|
+ set ylabel "Commits"
|
|
|
533
|
+ plot 'day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid
|
|
|
534
|
+ """)
|
|
|
535
|
+ f.close()
|
|
|
536
|
+
|
|
|
537
|
+ # Domains
|
|
|
538
|
+ f = open(path + '/domains.plot', 'w')
|
|
|
539
|
+ f.write(GNUPLOT_COMMON)
|
|
|
540
|
+ f.write(
|
|
|
541
|
+ """
|
|
|
542
|
+ set output 'domains.png'
|
|
|
543
|
+ unset key
|
|
|
544
|
+ unset xtics
|
|
|
545
|
+ set yrange [0:]
|
|
|
546
|
+ set grid y
|
|
|
547
|
+ set ylabel "Commits"
|
|
|
548
|
+ plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1
|
|
|
549
|
+ """)
|
|
|
550
|
+ f.close()
|
|
|
551
|
+
|
|
|
552
|
+ # Month of Year
|
|
|
553
|
+ f = open(path + '/month_of_year.plot', 'w')
|
|
|
554
|
+ f.write(GNUPLOT_COMMON)
|
|
|
555
|
+ f.write(
|
|
|
556
|
+ """
|
|
|
557
|
+ set output 'month_of_year.png'
|
|
|
558
|
+ unset key
|
|
|
559
|
+ set xrange [0.5:12.5]
|
|
|
560
|
+ set yrange [0:]
|
|
|
561
|
+ set xtics 1
|
|
|
562
|
+ set grid y
|
|
|
563
|
+ set ylabel "Commits"
|
|
|
564
|
+ plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
565
|
+ """)
|
|
|
566
|
+ f.close()
|
|
|
567
|
+
|
|
|
568
|
+ # commits_by_year_month
|
|
|
569
|
+ f = open(path + '/commits_by_year_month.plot', 'w')
|
|
|
570
|
+ f.write(GNUPLOT_COMMON)
|
|
|
571
|
+ f.write(
|
|
|
572
|
+ """
|
|
|
573
|
+ set output 'commits_by_year_month.png'
|
|
|
574
|
+ unset key
|
|
|
575
|
+ set yrange [0:]
|
|
|
576
|
+ set xdata time
|
|
|
577
|
+ set timefmt "%Y-%m"
|
|
|
578
|
+ set format x "%Y-%m"
|
|
|
579
|
+ set xtics rotate
|
|
|
580
|
+ set bmargin 5
|
|
|
581
|
+ set grid y
|
|
|
582
|
+ set ylabel "Commits"
|
|
|
583
|
+ plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
584
|
+ """)
|
|
|
585
|
+ f.close()
|
|
|
586
|
+
|
|
|
587
|
+ # commits_by_year
|
|
|
588
|
+ f = open(path + '/commits_by_year.plot', 'w')
|
|
|
589
|
+ f.write(GNUPLOT_COMMON)
|
|
|
590
|
+ f.write(
|
|
|
591
|
+ """
|
|
|
592
|
+ set output 'commits_by_year.png'
|
|
|
593
|
+ unset key
|
|
|
594
|
+ set yrange [0:]
|
|
|
595
|
+ set xtics 1 rotate
|
|
|
596
|
+ set grid y
|
|
|
597
|
+ set ylabel "Commits"
|
|
|
598
|
+ set yrange [0:]
|
|
|
599
|
+ plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
|
|
|
600
|
+ """)
|
|
|
601
|
+ f.close()
|
|
|
602
|
+
|
|
|
603
|
+ # Files by date
|
|
|
604
|
+ f = open(path + '/files_by_date.plot', 'w')
|
|
|
605
|
+ f.write(GNUPLOT_COMMON)
|
|
|
606
|
+ f.write(
|
|
|
607
|
+ """
|
|
|
608
|
+ set output 'files_by_date.png'
|
|
|
609
|
+ unset key
|
|
|
610
|
+ set yrange [0:]
|
|
|
611
|
+ set xdata time
|
|
|
612
|
+ set timefmt "%Y-%m-%d"
|
|
|
613
|
+ set format x "%Y-%m-%d"
|
|
|
614
|
+ set grid y
|
|
|
615
|
+ set ylabel "Files"
|
|
|
616
|
+ set xtics rotate
|
|
|
617
|
+ set ytics autofreq
|
|
|
618
|
+ set bmargin 6
|
|
|
619
|
+ plot 'files_by_date.dat' using 1:2 w steps
|
|
|
620
|
+ """)
|
|
|
621
|
+ f.close()
|
|
|
622
|
+
|
|
|
623
|
+ # Lines of Code
|
|
|
624
|
+ f = open(path + '/lines_of_code.plot', 'w')
|
|
|
625
|
+ f.write(GNUPLOT_COMMON)
|
|
|
626
|
+ f.write(
|
|
|
627
|
+ """
|
|
|
628
|
+ set output 'lines_of_code.png'
|
|
|
629
|
+ unset key
|
|
|
630
|
+ set yrange [0:]
|
|
|
631
|
+ set xdata time
|
|
|
632
|
+ set timefmt "%s"
|
|
|
633
|
+ set format x "%Y-%m-%d"
|
|
|
634
|
+ set grid y
|
|
|
635
|
+ set ylabel "Lines"
|
|
|
636
|
+ set xtics rotate
|
|
|
637
|
+ set bmargin 6
|
|
|
638
|
+ plot 'lines_of_code.dat' using 1:2 w lines
|
|
|
639
|
+ """)
|
|
|
640
|
+ f.close()
|
|
|
641
|
+
|
|
|
642
|
+ # Lines of Code Added per author
|
|
|
643
|
+ f = open(path + '/lines_of_code_by_author.plot', 'w')
|
|
|
644
|
+ f.write(GNUPLOT_COMMON)
|
|
|
645
|
+ f.write(
|
|
|
646
|
+ """
|
|
|
647
|
+ set terminal png transparent size 640,480
|
|
|
648
|
+ set output 'lines_of_code_by_author.png'
|
|
|
649
|
+ set key left top
|
|
|
650
|
+ set yrange [0:]
|
|
|
651
|
+ set xdata time
|
|
|
652
|
+ set timefmt "%s"
|
|
|
653
|
+ set format x "%Y-%m-%d"
|
|
|
654
|
+ set grid y
|
|
|
655
|
+ set ylabel "Lines"
|
|
|
656
|
+ set xtics rotate
|
|
|
657
|
+ set bmargin 6
|
|
|
658
|
+ plot """
|
|
|
659
|
+ )
|
|
|
660
|
+ i = 1
|
|
|
661
|
+ plots = []
|
|
|
662
|
+ for a in self.authors_to_plot:
|
|
|
663
|
+ i = i + 1
|
|
|
664
|
+ author = a.replace("\"", "\\\"").replace("`", "")
|
|
|
665
|
+ plots.append("""'lines_of_code_by_author.dat' using 1:%d title "%s" w lines""" % (i, author))
|
|
|
666
|
+ f.write(", ".join(plots))
|
|
|
667
|
+ f.write('\n')
|
|
|
668
|
+
|
|
|
669
|
+ f.close()
|
|
|
670
|
+
|
|
|
671
|
+ # Commits per author
|
|
|
672
|
+ f = open(path + '/commits_by_author.plot', 'w')
|
|
|
673
|
+ f.write(GNUPLOT_COMMON)
|
|
|
674
|
+ f.write(
|
|
|
675
|
+ """
|
|
|
676
|
+ set terminal png transparent size 640,480
|
|
|
677
|
+ set output 'commits_by_author.png'
|
|
|
678
|
+ set key left top
|
|
|
679
|
+ set yrange [0:]
|
|
|
680
|
+ set xdata time
|
|
|
681
|
+ set timefmt "%s"
|
|
|
682
|
+ set format x "%Y-%m-%d"
|
|
|
683
|
+ set grid y
|
|
|
684
|
+ set ylabel "Commits"
|
|
|
685
|
+ set xtics rotate
|
|
|
686
|
+ set bmargin 6
|
|
|
687
|
+ plot """
|
|
|
688
|
+ )
|
|
|
689
|
+ i = 1
|
|
|
690
|
+ plots = []
|
|
|
691
|
+ for a in self.authors_to_plot:
|
|
|
692
|
+ i = i + 1
|
|
|
693
|
+ author = a.replace("\"", "\\\"").replace("`", "")
|
|
|
694
|
+ plots.append("""'commits_by_author.dat' using 1:%d title "%s" w lines""" % (i, author))
|
|
|
695
|
+ f.write(", ".join(plots))
|
|
|
696
|
+ f.write('\n')
|
|
|
697
|
+
|
|
|
698
|
+ f.close()
|
|
|
699
|
+
|
|
|
700
|
+ os.chdir(path)
|
|
|
701
|
+ files = glob.glob(path + '/*.plot')
|
|
|
702
|
+ for f in files:
|
|
|
703
|
+ out = getpipeoutput([gnuplot_cmd + ' "%s"' % f])
|
|
|
704
|
+ if len(out) > 0:
|
|
|
705
|
+ print(out)
|
|
|
706
|
+
|
|
|
707
|
+ def printHeader(self, f, title=''):
|
|
|
708
|
+ f.write(
|
|
|
709
|
+ """<!DOCTYPE html>
|
|
|
710
|
+ <html>
|
|
|
711
|
+ <head>
|
|
|
712
|
+ <meta charset="UTF-8">
|
|
|
713
|
+ <title>GitStats - %s</title>
|
|
|
714
|
+ <link rel="stylesheet" href="%s" type="text/css">
|
|
|
715
|
+ <meta name="generator" content="GitStats %s">
|
|
|
716
|
+ <script type="text/javascript" src="sortable.js"></script>
|
|
|
717
|
+ </head>
|
|
|
718
|
+ <body>
|
|
|
719
|
+ """ % (self.title, self.conf['style'], getversion()))
|
|
|
720
|
+
|
|
|
721
|
+ def printNav(self, f):
|
|
|
722
|
+ f.write("""
|
|
|
723
|
+<div class="nav">
|
|
|
724
|
+<ul>
|
|
|
725
|
+<li><a href="index.html">General</a></li>
|
|
|
726
|
+<li><a href="activity.html">Activity</a></li>
|
|
|
727
|
+<li><a href="authors.html">Authors</a></li>
|
|
|
728
|
+<li><a href="files.html">Files</a></li>
|
|
|
729
|
+<li><a href="lines.html">Lines</a></li>
|
|
|
730
|
+<li><a href="tags.html">Tags</a></li>
|
|
|
731
|
+</ul>
|
|
|
732
|
+</div>
|
|
|
733
|
+""")
|