all repos — cgit @ eca95229acdc3c7c27fdcc1319f5c96da9e3b538

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