summary refs log tree commit diff
diff options
context:
space:
mode:
authorLars Hjemli <hjemli@gmail.com>2008-12-06 17:38:19 +0100
committerLars Hjemli <hjemli@gmail.com>2008-12-06 17:38:19 +0100
commitf86a23ff537258d36bf8f1876fa7a4bede6673d8 (patch)
tree8328d415416058cdc5b0fd2c6564ddcab5766c7a
parent140012d7a8e51df5a9f9c556696778b86ade4fc9 (diff)
Add a 'stats' page to each repo
This new page, which is disabled by default, can be used to print some
statistics about the number of commits per period in the repository,
where period can be either weeks, months, quarters or years.

The function can be activated globally by setting 'enable-stats=1' in
cgitrc and disabled for individual repos by setting 'repo.enable-stats=0'.

Signed-off-by: Lars Hjemli <hjemli@gmail.com>
-rw-r--r--Makefile1
-rw-r--r--cgit.c6
-rw-r--r--cgit.css77
-rw-r--r--cgit.h3
-rw-r--r--cgitrc.5.txt8
-rw-r--r--cmd.c10
-rw-r--r--shared.c1
-rw-r--r--ui-shared.c3
-rw-r--r--ui-stats.c380
-rw-r--r--ui-stats.h8
10 files changed, 497 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index 561af76..f426f98 100644
--- a/Makefile
+++ b/Makefile
@@ -68,6 +68,7 @@ OBJECTS += ui-refs.o
 OBJECTS += ui-repolist.o
 OBJECTS += ui-shared.o
 OBJECTS += ui-snapshot.o
+OBJECTS += ui-stats.o
 OBJECTS += ui-summary.o
 OBJECTS += ui-tag.o
 OBJECTS += ui-tree.o
diff --git a/cgit.c b/cgit.c
index c82587b..22b6d7c 100644
--- a/cgit.c
+++ b/cgit.c
@@ -54,6 +54,8 @@ void config_cb(const char *name, const char *value)
 		ctx.cfg.enable_log_filecount = atoi(value);
 	else if (!strcmp(name, "enable-log-linecount"))
 		ctx.cfg.enable_log_linecount = atoi(value);
+	else if (!strcmp(name, "enable-stats"))
+		ctx.cfg.enable_stats = atoi(value);
 	else if (!strcmp(name, "cache-size"))
 		ctx.cfg.cache_size = atoi(value);
 	else if (!strcmp(name, "cache-root"))
@@ -112,6 +114,8 @@ void config_cb(const char *name, const char *value)
 		ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
 	else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
 		ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
+	else if (ctx.repo && !strcmp(name, "repo.enable-stats"))
+		ctx.repo->enable_stats = ctx.cfg.enable_stats && atoi(value);
 	else if (ctx.repo && !strcmp(name, "repo.module-link"))
 		ctx.repo->module_link= xstrdup(value);
 	else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
@@ -154,6 +158,8 @@ static void querystring_cb(const char *name, const char *value)
 		ctx.qry.name = xstrdup(value);
 	} else if (!strcmp(name, "mimetype")) {
 		ctx.qry.mimetype = xstrdup(value);
+	} else if (!strcmp(name, "period")) {
+		ctx.qry.period = xstrdup(value);
 	}
 }
 
diff --git a/cgit.css b/cgit.css
index a37d218..ef30fbf 100644
--- a/cgit.css
+++ b/cgit.css
@@ -456,3 +456,80 @@ div.footer {
 	font-size: 80%;
 	color: #ccc;
 }
+table.stats {
+	border: solid 1px black;
+	border-collapse: collapse;
+}
+
+table.stats th {
+	text-align: left;
+	padding: 1px 0.5em;
+	background-color: #eee;
+	border: solid 1px black;
+}
+
+table.stats td {
+	text-align: right;
+	padding: 1px 0.5em;
+	border: solid 1px black;
+}
+
+table.stats td.total {
+	font-weight: bold;
+	text-align: left;
+}
+
+table.stats td.sum {
+	color: #c00;
+	font-weight: bold;
+/*	background-color: #eee; */
+}
+
+table.stats td.left {
+	text-align: left;
+}
+
+table.vgraph {
+	border-collapse: separate;
+	border: solid 1px black;
+	height: 200px;
+}
+
+table.vgraph th {
+	background-color: #eee;
+	font-weight: bold;
+	border: solid 1px white;
+	padding: 1px 0.5em;
+}
+
+table.vgraph td {
+	vertical-align: bottom;
+	padding: 0px 10px;
+}
+
+table.vgraph div.bar {
+	background-color: #eee;
+}
+
+table.hgraph {
+	border: solid 1px black;
+	width: 800px;
+}
+
+table.hgraph th {
+	background-color: #eee;
+	font-weight: bold;
+	border: solid 1px black;
+	padding: 1px 0.5em;
+}
+
+table.hgraph td {
+	vertical-align: center;
+	padding: 2px 2px;
+}
+
+table.hgraph div.bar {
+	background-color: #eee;
+	height: 1em;
+}
+
diff --git a/cgit.h b/cgit.h
index 91db98a..85045c4 100644
--- a/cgit.h
+++ b/cgit.h
@@ -61,6 +61,7 @@ struct cgit_repo {
 	int snapshots;
 	int enable_log_filecount;
 	int enable_log_linecount;
+	int enable_stats;
 };
 
 struct cgit_repolist {
@@ -119,6 +120,7 @@ struct cgit_query {
 	char *name;
 	char *mimetype;
 	char *url;
+	char *period;
 	int   ofs;
 	int nohead;
 };
@@ -151,6 +153,7 @@ struct cgit_config {
 	int enable_index_links;
 	int enable_log_filecount;
 	int enable_log_linecount;
+	int enable_stats;
 	int local_time;
 	int max_repo_count;
 	int max_commit_count;
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 7887b02..60d3ea4 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -74,6 +74,10 @@ enable-log-linecount
 	and removed lines for each commit on the repository log page. Default
 	value: "0".
 
+enable-stats
+	Globally enable/disable statistics for each repository. Default
+	value: "0".
+
 favicon
 	Url used as link to a shortcut icon for cgit. If specified, it is
 	suggested to use the value "/favicon.ico" since certain browsers will
@@ -218,6 +222,10 @@ repo.enable-log-linecount
 	A flag which can be used to disable the global setting
 	`enable-log-linecount'. Default value: none.
 
+repo.enable-stats
+	A flag which can be used to disable the global setting
+	`enable-stats'. Default value: none.
+
 repo.name
 	The value to show as repository name. Default value: <repo.url>.
 
diff --git a/cmd.c b/cmd.c
index 5b3c14c..744bf84 100644
--- a/cmd.c
+++ b/cmd.c
@@ -21,6 +21,7 @@
 #include "ui-refs.h"
 #include "ui-repolist.h"
 #include "ui-snapshot.h"
+#include "ui-stats.h"
 #include "ui-summary.h"
 #include "ui-tag.h"
 #include "ui-tree.h"
@@ -109,6 +110,14 @@ static void snapshot_fn(struct cgit_context *ctx)
 			    ctx->repo->snapshots, ctx->qry.nohead);
 }
 
+static void stats_fn(struct cgit_context *ctx)
+{
+	if (ctx->repo->enable_stats)
+		cgit_show_stats(ctx);
+	else
+		cgit_print_error("Stats disabled for this repo");
+}
+
 static void summary_fn(struct cgit_context *ctx)
 {
 	cgit_print_summary();
@@ -145,6 +154,7 @@ struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
 		def_cmd(refs, 1, 1),
 		def_cmd(repolist, 0, 0),
 		def_cmd(snapshot, 1, 0),
+		def_cmd(stats, 1, 1),
 		def_cmd(summary, 1, 1),
 		def_cmd(tag, 1, 1),
 		def_cmd(tree, 1, 1),
diff --git a/shared.c b/shared.c
index f5875e4..37333f0 100644
--- a/shared.c
+++ b/shared.c
@@ -58,6 +58,7 @@ struct cgit_repo *cgit_add_repo(const char *url)
 	ret->snapshots = ctx.cfg.snapshots;
 	ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
 	ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
+	ret->enable_stats = ctx.cfg.enable_stats;
 	ret->module_link = ctx.cfg.module_link;
 	ret->readme = NULL;
 	return ret;
diff --git a/ui-shared.c b/ui-shared.c
index 224e5f3..0e688a0 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -641,6 +641,9 @@ void cgit_print_pageheader(struct cgit_context *ctx)
 				 ctx->qry.head, ctx->qry.sha1);
 		cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
 			       ctx->qry.sha1, ctx->qry.sha2, NULL);
+		if (ctx->repo->enable_stats)
+			reporevlink("stats", "stats", NULL, hc(cmd, "stats"),
+				    ctx->qry.head, NULL, NULL);
 		if (ctx->repo->readme)
 			reporevlink("about", "about", NULL,
 				    hc(cmd, "about"), ctx->qry.head, NULL,
diff --git a/ui-stats.c b/ui-stats.c
new file mode 100644
index 0000000..9150840
--- /dev/null
+++ b/ui-stats.c
@@ -0,0 +1,380 @@
+#include "cgit.h"
+#include "html.h"
+#include <string-list.h>
+
+#define MONTHS 6
+
+struct Period {
+	const char code;
+	const char *name;
+	int max_periods;
+	int count;
+
+	/* Convert a tm value to the first day in the period */
+	void (*trunc)(struct tm *tm);
+
+	/* Update tm value to start of next/previous period */
+	void (*dec)(struct tm *tm);
+	void (*inc)(struct tm *tm);
+
+	/* Pretty-print a tm value */
+	char *(*pretty)(struct tm *tm);
+};
+
+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 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},
+};
+
+static void add_commit(struct string_list *authors, struct commit *commit,
+	struct 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(tmp, authors);
+	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(tmp, items);
+	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 Period *period)
+{
+	struct string_list authors;
+	struct rev_info rev;
+	struct commit *commit;
+	const char *argv[] = {NULL, ctx->qry.head, NULL, NULL};
+	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));
+	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(3, 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 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(tmp, items);
+			if (date)
+				subtotal += (size_t)date->util;
+		}
+		htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
+		total += subtotal;
+	}
+	htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
+}
+
+void print_authors(struct string_list *authors, int top, struct 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(tmp, items);
+			if (!date)
+				html("<td>0</td>");
+			else {
+				htmlf("<td>%d</td>", date->util);
+				total += (size_t)date->util;
+			}
+		}
+		htmlf("<td class='sum'>%d</td></tr>", total);
+	}
+
+	if (top < authors->nr)
+		print_combined_authorrow(authors, top, authors->nr - 1,
+			"Others (%d)", "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 Period *period;
+	int top, i;
+
+	period = &periods[0];
+	if (ctx->qry.period) {
+		for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
+			if (periods[i].code == ctx->qry.period[0]) {
+				period = &periods[i];
+				break;
+			}
+	}
+	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</h2>", period->name);
+
+	html("<form method='get' action='.' style='float: right; text-align: right;'>");
+	if (strcmp(ctx->qry.head, ctx->repo->defbranch))
+		htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head);
+	html("Period: ");
+	html("<select name='period' onchange='this.form.submit();'>");
+	for (i = 0; i < sizeof(periods) / sizeof(periods[0]); 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);
+}
+
diff --git a/ui-stats.h b/ui-stats.h
new file mode 100644
index 0000000..f1d744c
--- /dev/null
+++ b/ui-stats.h
@@ -0,0 +1,8 @@
+#ifndef UI_STATS_H
+#define UI_STATS_H
+
+#include "cgit.h"
+
+extern void cgit_show_stats(struct cgit_context *ctx);
+
+#endif /* UI_STATS_H */