all repos — cgit @ 97b3d252629a8a3b9d356c2532dec7611438e4b9

a hyperfast web frontend for git written in c

ui-stats.c (view raw)

  1#include <string-list.h>
  2
  3#include "cgit.h"
  4#include "html.h"
  5#include "ui-shared.h"
  6#include "ui-stats.h"
  7
  8#define MONTHS 6
  9
 10struct authorstat {
 11	long total;
 12	struct string_list list;
 13};
 14
 15#define DAY_SECS (60 * 60 * 24)
 16#define WEEK_SECS (DAY_SECS * 7)
 17
 18static void trunc_week(struct tm *tm)
 19{
 20	time_t t = timegm(tm);
 21	t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
 22	gmtime_r(&t, tm);	
 23}
 24
 25static void dec_week(struct tm *tm)
 26{
 27	time_t t = timegm(tm);
 28	t -= WEEK_SECS;
 29	gmtime_r(&t, tm);	
 30}
 31
 32static void inc_week(struct tm *tm)
 33{
 34	time_t t = timegm(tm);
 35	t += WEEK_SECS;
 36	gmtime_r(&t, tm);	
 37}
 38
 39static char *pretty_week(struct tm *tm)
 40{
 41	static char buf[10];
 42
 43	strftime(buf, sizeof(buf), "W%V %G", tm);
 44	return buf;
 45}
 46
 47static void trunc_month(struct tm *tm)
 48{
 49	tm->tm_mday = 1;
 50}
 51
 52static void dec_month(struct tm *tm)
 53{
 54	tm->tm_mon--;
 55	if (tm->tm_mon < 0) {
 56		tm->tm_year--;
 57		tm->tm_mon = 11;
 58	}
 59}
 60
 61static void inc_month(struct tm *tm)
 62{
 63	tm->tm_mon++;
 64	if (tm->tm_mon > 11) {
 65		tm->tm_year++;
 66		tm->tm_mon = 0;
 67	}
 68}
 69
 70static char *pretty_month(struct tm *tm)
 71{
 72	static const char *months[] = {
 73		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
 74		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
 75	};
 76	return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
 77}
 78
 79static void trunc_quarter(struct tm *tm)
 80{
 81	trunc_month(tm);
 82	while(tm->tm_mon % 3 != 0)
 83		dec_month(tm);
 84}
 85
 86static void dec_quarter(struct tm *tm)
 87{
 88	dec_month(tm);
 89	dec_month(tm);
 90	dec_month(tm);
 91}
 92
 93static void inc_quarter(struct tm *tm)
 94{
 95	inc_month(tm);
 96	inc_month(tm);
 97	inc_month(tm);
 98}
 99
100static char *pretty_quarter(struct tm *tm)
101{
102	return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
103}
104
105static void trunc_year(struct tm *tm)
106{
107	trunc_month(tm);
108	tm->tm_mon = 0;
109}
110
111static void dec_year(struct tm *tm)
112{
113	tm->tm_year--;
114}
115
116static void inc_year(struct tm *tm)
117{
118	tm->tm_year++;
119}
120
121static char *pretty_year(struct tm *tm)
122{
123	return fmt("%d", tm->tm_year + 1900);
124}
125
126struct cgit_period periods[] = {
127	{'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
128	{'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
129	{'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
130	{'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
131};
132
133/* Given a period code or name, return a period index (1, 2, 3 or 4)
134 * and update the period pointer to the correcsponding struct.
135 * If no matching code is found, return 0.
136 */
137int cgit_find_stats_period(const char *expr, struct cgit_period **period)
138{
139	int i;
140	char code = '\0';
141
142	if (!expr)
143		return 0;
144
145	if (strlen(expr) == 1)
146		code = expr[0];
147
148	for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
149		if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
150			if (period)
151				*period = &periods[i];
152			return i+1;
153		}
154	return 0;
155}
156
157static void add_commit(struct string_list *authors, struct commit *commit,
158	struct cgit_period *period)
159{
160	struct commitinfo *info;
161	struct string_list_item *author, *item;
162	struct authorstat *authorstat;
163	struct string_list *items;
164	char *tmp;
165	struct tm *date;
166	time_t t;
167
168	info = cgit_parse_commit(commit);
169	tmp = xstrdup(info->author);
170	author = string_list_insert(tmp, authors);
171	if (!author->util)
172		author->util = xcalloc(1, sizeof(struct authorstat));
173	else
174		free(tmp);
175	authorstat = author->util;
176	items = &authorstat->list;
177	t = info->committer_date;
178	date = gmtime(&t);
179	period->trunc(date);
180	tmp = xstrdup(period->pretty(date));
181	item = string_list_insert(tmp, items);
182	if (item->util)
183		free(tmp);
184	item->util++;
185	authorstat->total++;
186	cgit_free_commitinfo(info);
187}
188
189static int cmp_total_commits(const void *a1, const void *a2)
190{
191	const struct string_list_item *i1 = a1;
192	const struct string_list_item *i2 = a2;
193	const struct authorstat *auth1 = i1->util;
194	const struct authorstat *auth2 = i2->util;
195
196	return auth2->total - auth1->total;
197}
198
199/* Walk the commit DAG and collect number of commits per author per
200 * timeperiod into a nested string_list collection.
201 */
202struct string_list collect_stats(struct cgit_context *ctx,
203	struct cgit_period *period)
204{
205	struct string_list authors;
206	struct rev_info rev;
207	struct commit *commit;
208	const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
209	int argc = 3;
210	time_t now;
211	long i;
212	struct tm *tm;
213	char tmp[11];
214
215	time(&now);
216	tm = gmtime(&now);
217	period->trunc(tm);
218	for (i = 1; i < period->count; i++)
219		period->dec(tm);
220	strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
221	argv[2] = xstrdup(fmt("--since=%s", tmp));
222	if (ctx->qry.path) {
223		argv[3] = "--";
224		argv[4] = ctx->qry.path;
225		argc += 2;
226	}
227	init_revisions(&rev, NULL);
228	rev.abbrev = DEFAULT_ABBREV;
229	rev.commit_format = CMIT_FMT_DEFAULT;
230	rev.no_merges = 1;
231	rev.verbose_header = 1;
232	rev.show_root_diff = 0;
233	setup_revisions(argc, argv, &rev, NULL);
234	prepare_revision_walk(&rev);
235	memset(&authors, 0, sizeof(authors));
236	while ((commit = get_revision(&rev)) != NULL) {
237		add_commit(&authors, commit, period);
238		free(commit->buffer);
239		free_commit_list(commit->parents);
240	}
241	return authors;
242}
243
244void print_combined_authorrow(struct string_list *authors, int from, int to,
245	const char *name, const char *leftclass, const char *centerclass,
246	const char *rightclass, struct cgit_period *period)
247{
248	struct string_list_item *author;
249	struct authorstat *authorstat;
250	struct string_list *items;
251	struct string_list_item *date;
252	time_t now;
253	long i, j, total, subtotal;
254	struct tm *tm;
255	char *tmp;
256
257	time(&now);
258	tm = gmtime(&now);
259	period->trunc(tm);
260	for (i = 1; i < period->count; i++)
261		period->dec(tm);
262
263	total = 0;
264	htmlf("<tr><td class='%s'>%s</td>", leftclass,
265		fmt(name, to - from + 1));
266	for (j = 0; j < period->count; j++) {
267		tmp = period->pretty(tm);
268		period->inc(tm);
269		subtotal = 0;
270		for (i = from; i <= to; i++) {
271			author = &authors->items[i];
272			authorstat = author->util;
273			items = &authorstat->list;
274			date = string_list_lookup(tmp, items);
275			if (date)
276				subtotal += (size_t)date->util;
277		}
278		htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
279		total += subtotal;
280	}
281	htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
282}
283
284void print_authors(struct string_list *authors, int top,
285		   struct cgit_period *period)
286{
287	struct string_list_item *author;
288	struct authorstat *authorstat;
289	struct string_list *items;
290	struct string_list_item *date;
291	time_t now;
292	long i, j, total;
293	struct tm *tm;
294	char *tmp;
295
296	time(&now);
297	tm = gmtime(&now);
298	period->trunc(tm);
299	for (i = 1; i < period->count; i++)
300		period->dec(tm);
301
302	html("<table class='stats'><tr><th>Author</th>");
303	for (j = 0; j < period->count; j++) {
304		tmp = period->pretty(tm);
305		htmlf("<th>%s</th>", tmp);
306		period->inc(tm);
307	}
308	html("<th>Total</th></tr>\n");
309
310	if (top <= 0 || top > authors->nr)
311		top = authors->nr;
312
313	for (i = 0; i < top; i++) {
314		author = &authors->items[i];
315		html("<tr><td class='left'>");
316		html_txt(author->string);
317		html("</td>");
318		authorstat = author->util;
319		items = &authorstat->list;
320		total = 0;
321		for (j = 0; j < period->count; j++)
322			period->dec(tm);
323		for (j = 0; j < period->count; j++) {
324			tmp = period->pretty(tm);
325			period->inc(tm);
326			date = string_list_lookup(tmp, items);
327			if (!date)
328				html("<td>0</td>");
329			else {
330				htmlf("<td>%d</td>", date->util);
331				total += (size_t)date->util;
332			}
333		}
334		htmlf("<td class='sum'>%d</td></tr>", total);
335	}
336
337	if (top < authors->nr)
338		print_combined_authorrow(authors, top, authors->nr - 1,
339			"Others (%d)", "left", "", "sum", period);
340
341	print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
342		"total", "sum", "sum", period);
343	html("</table>");
344}
345
346/* Create a sorted string_list with one entry per author. The util-field
347 * for each author is another string_list which is used to calculate the
348 * number of commits per time-interval.
349 */
350void cgit_show_stats(struct cgit_context *ctx)
351{
352	struct string_list authors;
353	struct cgit_period *period;
354	int top, i;
355	const char *code = "w";
356
357	if (ctx->qry.period)
358		code = ctx->qry.period;
359
360	i = cgit_find_stats_period(code, &period);
361	if (!i) {
362		cgit_print_error(fmt("Unknown statistics type: %c", code));
363		return;
364	}
365	if (i > ctx->repo->max_stats) {
366		cgit_print_error(fmt("Statistics type disabled: %s",
367				     period->name));
368		return;
369	}
370	authors = collect_stats(ctx, period);
371	qsort(authors.items, authors.nr, sizeof(struct string_list_item),
372		cmp_total_commits);
373
374	top = ctx->qry.ofs;
375	if (!top)
376		top = 10;
377	htmlf("<h2>Commits per author per %s", period->name);
378	if (ctx->qry.path) {
379		html(" (path '");
380		html_txt(ctx->qry.path);
381		html("')");
382	}
383	html("</h2>");
384
385	html("<form method='get' action='' style='float: right; text-align: right;'>");
386	cgit_add_hidden_formfields(1, 0, "stats");
387	if (ctx->repo->max_stats > 1) {
388		html("Period: ");
389		html("<select name='period' onchange='this.form.submit();'>");
390		for (i = 0; i < ctx->repo->max_stats; i++)
391			htmlf("<option value='%c'%s>%s</option>",
392				periods[i].code,
393				period == &periods[i] ? " selected" : "",
394				periods[i].name);
395		html("</select><br/><br/>");
396	}
397	html("Authors: ");
398	html("");
399	html("<select name='ofs' onchange='this.form.submit();'>");
400	htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
401	htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
402	htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
403	htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
404	htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
405	html("</select>");
406	html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
407	html("</form>");
408	print_authors(&authors, top, period);
409}
410