summaryrefslogtreecommitdiffstatshomepage
path: root/ui-stats.c
blob: 2a0c17460b0894f22e81eea7eacf7693b8d99d54 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/bin/sh
# This script can be used to implement syntax highlighting in the cgit
# tree-view by refering to this file with the source-filter or repo.source-
# filter options in cgitrc.
#
# This script requires a shell supporting the ${var##pattern} syntax.
# It is supported by at least dash and bash, however busybox environments
# might have to use an external call to sed instead.
#
# Note: the highlight command (http://www.andre-simon.de/) uses css for syntax
# highlighting, so you'll probably want something like the following included
# in your css file:
#
# Style definition file generated by highlight 2.4.8, http://www.andre-simon.de/
#
# table.blob .num  { color:#2928ff; }
# table.blob .esc  { color:#ff00ff; }
# table.blob .str  { color:#ff0000; }
# table.blob .dstr { color:#818100; }
# table.blob .slc  { color:#838183; font-style:italic; }
# table.blob .com  { color:#838183; font-style:italic; }
# table.blob .dir  { color:#008200; }
# table.blob .sym  { color:#000000; }
# table.blob .kwa  { color:#000000; font-weight:bold; }
# table.blob .kwb  { color:#830000; }
# table.blob .kwc  { color:#000000; font-weight:bold; }
# table.blob .kwd  { color:#010181; }
#
#
# Style definition file generated by highlight 2.6.14, http://www.andre-simon.de/
#
# body.hl  { background-color:#ffffff; }
# pre.hl   { color:#000000; background-color:#ffffff; font-size:10pt; font-family:'Courier New';}
# .hl.num  { color:#2928ff; }
# .hl.esc  { color:#ff00ff; }
# .hl.str  { color:#ff0000; }
# .hl.dstr { color:#818100; }
# .hl.slc  { color:#838183; font-style:italic; }
# .hl.com  { color:#838183; font-style:italic; }
# .hl.dir  { color:#008200; }
# .hl.sym  { color:#000000; }
# .hl.line { color:#555555; }
# .hl.mark { background-color:#ffffbb;}
# .hl.kwa  { color:#000000; font-weight:bold; }
# .hl.kwb  { color:#830000; }
# .hl.kwc  { color:#000000; font-weight:bold; }
# .hl.kwd  { color:#010181; }
#
#
# Style definition file generated by highlight 3.8, http://www.andre-simon.de/
#
# body.hl { background-color:#e0eaee; }
# pre.hl  { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New';}
# .hl.num { color:#b07e00; }
# .hl.esc { color:#ff00ff; }
# .hl.str { color:#bf0303; }
# .hl.pps { color:#818100; }
# .hl.slc { color:#838183; font-style:italic; }
# .hl.com { color:#838183; font-style:italic; }
# .hl.ppc { color:#008200; }
# .hl.opt { color:#000000; }
# .hl.lin { color:#555555; }
# .hl.kwa { color:#000000; font-weight:bold; }
# .hl.kwb { color:#0057ae; }
# .hl.kwc { color:#000000; font-weight:bold; }
# .hl.kwd { color:#010181; }
#
#
# Style definition file generated by highlight 3.13, http://www.andre-simon.de/
#
# body.hl { background-color:#e0eaee; }
# pre.hl  { color:#000000; background-color:#e0eaee; font-size:10pt; font-family:'Courier New',monospace;}
# .hl.num { color:#b07e00; }pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
#include "cgit.h"
#include "html.h"
#include "ui-shared.h"
#include "ui-stats.h"

#ifdef NO_C99_FORMAT
#define SZ_FMT "%u"
#else
#define SZ_FMT "%zu"
#endif

#define MONTHS 6

struct authorstat {
	long total;
	struct string_list list;
};

#define DAY_SECS (60 * 60 * 24)
#define WEEK_SECS (DAY_SECS * 7)

static void trunc_week(struct tm *tm)
{
	time_t t = timegm(tm);
	t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
	gmtime_r(&t, tm);	
}

static void dec_week(struct tm *tm)
{
	time_t t = timegm(tm);
	t -= WEEK_SECS;
	gmtime_r(&t, tm);	
}

static void inc_week(struct tm *tm)
{
	time_t t = timegm(tm);
	t += WEEK_SECS;
	gmtime_r(&t, tm);	
}

static char *pretty_week(struct tm *tm)
{
	static char buf[10];

	strftime(buf, sizeof(buf), "W%V %G", tm);
	return buf;
}

static void trunc_month(struct tm *tm)
{
	tm->tm_mday = 1;
}

static void dec_month(struct tm *tm)
{
	tm->tm_mon--;
	if (tm->tm_mon < 0) {
		tm->tm_year--;
		tm->tm_mon = 11;
	}
}

static void inc_month(struct tm *tm)
{
	tm->tm_mon++;
	if (tm->tm_mon > 11) {
		tm->tm_year++;
		tm->tm_mon = 0;
	}
}

static char *pretty_month(struct tm *tm)
{
	static const char *months[] = {
		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
	};
	return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
}

static void trunc_quarter(struct tm *tm)
{
	trunc_month(tm);
	while(tm->tm_mon % 3 != 0)
		dec_month(tm);
}

static void dec_quarter(struct tm *tm)
{
	dec_month(tm);
	dec_month(tm);
	dec_month(tm);
}

static void inc_quarter(struct tm *tm)
{
	inc_month(tm);
	inc_month(tm);
	inc_month(tm);
}

static char *pretty_quarter(struct tm *tm)
{
	return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
}

static void trunc_year(struct tm *tm)
{
	trunc_month(tm);
	tm->tm_mon = 0;
}

static void dec_year(struct tm *tm)
{
	tm->tm_year--;
}

static void inc_year(struct tm *tm)
{
	tm->tm_year++;
}

static char *pretty_year(struct tm *tm)
{
	return fmt("%d", tm->tm_year + 1900);
}

struct cgit_period periods[] = {
	{'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
	{'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
	{'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
	{'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
};

/* Given a period code or name, return a period index (1, 2, 3 or 4)
 * and update the period pointer to the correcsponding struct.
 * If no matching code is found, return 0.
 */
int cgit_find_stats_period(const char *expr, struct cgit_period **period)
{
	int i;
	char code = '\0';

	if (!expr)
		return 0;

	if (strlen(expr) == 1)
		code = expr[0];

	for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
		if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
			if (period)
				*period = &periods[i];
			return i+1;
		}
	return 0;
}

const char *cgit_find_stats_periodname(int idx)
{
	if (idx > 0 && idx < 4)
		return periods[idx - 1].name;
	else
		return "";
}

static void add_commit(struct string_list *authors, struct commit *commit,
	struct cgit_period *period)
{
	struct commitinfo *info;
	struct string_list_item *author, *item;
	struct authorstat *authorstat;
	struct string_list *items;
	char *tmp;
	struct tm *date;
	time_t t;

	info = cgit_parse_commit(commit);
	tmp = xstrdup(info->author);
	author = string_list_insert(authors, tmp);
	if (!author->util)
		author->util = xcalloc(1, sizeof(struct authorstat));
	else
		free(tmp);
	authorstat = author->util;
	items = &authorstat->list;
	t = info->committer_date;
	date = gmtime(&t);
	period->trunc(date);
	tmp = xstrdup(period->pretty(date));
	item = string_list_insert(items, tmp);
	if (item->util)
		free(tmp);
	item->util++;
	authorstat->total++;
	cgit_free_commitinfo(info);
}

static int cmp_total_commits(const void *a1, const void *a2)
{
	const struct string_list_item *i1 = a1;
	const struct string_list_item *i2 = a2;
	const struct authorstat *auth1 = i1->util;
	const struct authorstat *auth2 = i2->util;

	return auth2->total - auth1->total;
}

/* Walk the commit DAG and collect number of commits per author per
 * timeperiod into a nested string_list collection.
 */
struct string_list collect_stats(struct cgit_context *ctx,
	struct cgit_period *period)
{
	struct string_list authors;
	struct rev_info rev;
	struct commit *commit;
	const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
	int argc = 3;
	time_t now;
	long i;
	struct tm *tm;
	char tmp[11];

	time(&now);
	tm = gmtime(&now);
	period->trunc(tm);
	for (i = 1; i < period->count; i++)
		period->dec(tm);
	strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
	argv[2] = xstrdup(fmt("--since=%s", tmp));
	if (ctx->qry.path) {
		argv[3] = "--";
		argv[4] = ctx->qry.path;
		argc += 2;
	}
	init_revisions(&rev, NULL);
	rev.abbrev = DEFAULT_ABBREV;
	rev.commit_format = CMIT_FMT_DEFAULT;
	rev.no_merges = 1;
	rev.verbose_header = 1;
	rev.show_root_diff = 0;
	setup_revisions(argc, argv, &rev, NULL);
	prepare_revision_walk(&rev);
	memset(&authors, 0, sizeof(authors));
	while ((commit = get_revision(&rev)) != NULL) {
		add_commit(&authors, commit, period);
		free(commit->buffer);
		free_commit_list(commit->parents);
	}
	return authors;
}

void print_combined_authorrow(struct string_list *authors, int from, int to,
	const char *name, const char *leftclass, const char *centerclass,
	const char *rightclass, struct cgit_period *period)
{
	struct string_list_item *author;
	struct authorstat *authorstat;
	struct string_list *items;
	struct string_list_item *date;
	time_t now;
	long i, j, total, subtotal;
	struct tm *tm;
	char *tmp;

	time(&now);
	tm = gmtime(&now);
	period->trunc(tm);
	for (i = 1; i < period->count; i++)
		period->dec(tm);

	total = 0;
	htmlf("<tr><td class='%s'>%s</td>", leftclass,
		fmt(name, to - from + 1));
	for (j = 0; j < period->count; j++) {
		tmp = period->pretty(tm);
		period->inc(tm);
		subtotal = 0;
		for (i = from; i <= to; i++) {
			author = &authors->items[i];
			authorstat = author->util;
			items = &authorstat->list;
			date = string_list_lookup(items, tmp);
			if (date)
				subtotal += (size_t)date->util;
		}
		htmlf("<td class='%s'>%ld</td>", centerclass, subtotal);
		total += subtotal;
	}
	htmlf("<td class='%s'>%ld</td></tr>", rightclass, total);
}

void print_authors(struct string_list *authors, int top,
		   struct cgit_period *period)
{
	struct string_list_item *author;
	struct authorstat *authorstat;
	struct string_list *items;
	struct string_list_item *date;
	time_t now;
	long i, j, total;
	struct tm *tm;
	char *tmp;

	time(&now);
	tm = gmtime(&now);
	period->trunc(tm);
	for (i = 1; i < period->count; i++)
		period->dec(tm);

	html("<table class='stats'><tr><th>Author</th>");
	for (j = 0; j < period->count; j++) {
		tmp = period->pretty(tm);
		htmlf("<th>%s</th>", tmp);
		period->inc(tm);
	}
	html("<th>Total</th></tr>\n");

	if (top <= 0 || top > authors->nr)
		top = authors->nr;

	for (i = 0; i < top; i++) {
		author = &authors->items[i];
		html("<tr><td class='left'>");
		html_txt(author->string);
		html("</td>");
		authorstat = author->util;
		items = &authorstat->list;
		total = 0;
		for (j = 0; j < period->count; j++)
			period->dec(tm);
		for (j = 0; j < period->count; j++) {
			tmp = period->pretty(tm);
			period->inc(tm);
			date = string_list_lookup(items, tmp);
			if (!date)
				html("<td>0</td>");
			else {
				htmlf("<td>"SZ_FMT"</td>", (size_t)date->util);
				total += (size_t)date->util;
			}
		}
		htmlf("<td class='sum'>%ld</td></tr>", total);
	}

	if (top < authors->nr)
		print_combined_authorrow(authors, top, authors->nr - 1,
			"Others (%ld)", "left", "", "sum", period);

	print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
		"total", "sum", "sum", period);
	html("</table>");
}

/* Create a sorted string_list with one entry per author. The util-field
 * for each author is another string_list which is used to calculate the
 * number of commits per time-interval.
 */
void cgit_show_stats(struct cgit_context *ctx)
{
	struct string_list authors;
	struct cgit_period *period;
	int top, i;
	const char *code = "w";

	if (ctx->qry.period)
		code = ctx->qry.period;

	i = cgit_find_stats_period(code, &period);
	if (!i) {
		cgit_print_error(fmt("Unknown statistics type: %c", code[0]));
		return;
	}
	if (i > ctx->repo->max_stats) {
		cgit_print_error(fmt("Statistics type disabled: %s",
				     period->name));
		return;
	}
	authors = collect_stats(ctx, period);
	qsort(authors.items, authors.nr, sizeof(struct string_list_item),
		cmp_total_commits);

	top = ctx->qry.ofs;
	if (!top)
		top = 10;
	htmlf("<h2>Commits per author per %s", period->name);
	if (ctx->qry.path) {
		html(" (path '");
		html_txt(ctx->qry.path);
		html("')");
	}
	html("</h2>");

	html("<form method='get' action='' style='float: right; text-align: right;'>");
	cgit_add_hidden_formfields(1, 0, "stats");
	if (ctx->repo->max_stats > 1) {
		html("Period: ");
		html("<select name='period' onchange='this.form.submit();'>");
		for (i = 0; i < ctx->repo->max_stats; i++)
			htmlf("<option value='%c'%s>%s</option>",
				periods[i].code,
				period == &periods[i] ? " selected" : "",
				periods[i].name);
		html("</select><br/><br/>");
	}
	html("Authors: ");
	html("");
	html("<select name='ofs' onchange='this.form.submit();'>");
	htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
	htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
	htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
	htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
	htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
	html("</select>");
	html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
	html("</form>");
	print_authors(&authors, top, period);
}