summaryrefslogtreecommitdiffstatshomepage
path: root/ui-repolist.c
blob: 4c86543673855e40adf43a33d32bfcf1297f899e (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
/* ui-repolist.c: functions for generating the repolist page
 *
 * Copyright (C) 2006 Lars Hjemli
 *
 * Licensed under GNU General Public License v2
 *   (see COPYING for full license text)
 */

#include "cgit.h"
#include <time.h>


time_t read_agefile(char *path)
{
	FILE *f;
	static char buf[64], buf2[64];

	if (!(f = fopen(path, "r")))
		return -1;
	fgets(buf, sizeof(buf), f);
	fclose(f);
	if (parse_date(buf, buf2, sizeof(buf2)))
		return strtoul(buf2, NULL, 10);
	else
		return 0;
}

static void print_modtime(struct repoinfo *repo)
{
	char *path;
	struct stat s;

	path = fmt("%s/%s", repo->path, cgit_agefile);
	if (stat(path, &s) == 0) {
		cgit_print_age(read_agefile(path), -1, NULL);
		return;
	}

	path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch);
	if (stat(path, &s) != 0)
		return;
	cgit_print_age(s.st_mtime, -1, NULL);
}

void cgit_print_repolist(struct cacheitem *item)
{
	int i, columns = 4;
	char *last_group = NULL;

	if (cgit_enable_index_links)
		columns++;

	cgit_print_docstart(cgit_root_title, item);
	cgit_print_pageheader(cgit_root_title, 0);

	html("<table class='list nowrap'>");
	if (cgit_index_header) {
		htmlf("<tr class='nohover'><td colspan='%d' class='include-block'>",
		      columns);
		html_include(cgit_index_header);
		html("</td></tr>");
	}
	html("<tr class='nohover'>"
	     "<th class='left'>Name</th>"
	     "<th class='left'>Description</th>"
	     "<th class='left'>Owner</th>"
	     "<th class='left'>Idle</th>");
	if (cgit_enable_index_links)
		html("<th>Links</th>");
	html("</tr>\n");

	for (i=0; i<cgit_repolist.count; i++) {
		cgit_repo = &cgit_repolist.repos[i];
		if ((last_group == NULL && cgit_repo->group != NULL) ||
		    (last_group != NULL && cgit_repo->group == NULL) ||
		    (last_group != NULL && cgit_repo->group != NULL &&
		     strcmp(cgit_repo->group, last_group))) {
			htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>",
			      columns);
			html_txt(cgit_repo->group);
			html("</td></tr>");
			last_group = cgit_repo->group;
		}
		htmlf("<tr><td class='%s'>",
		      cgit_repo->group ? "sublevel-repo" : "toplevel-repo");
		html_link_open(cgit_repourl(cgit_repo->url), NULL, NULL);
		html_txt(cgit_repo->name);
		html_link_close();
		html("</td><td>");
		html_ntxt(cgit_max_repodesc_len, cgit_repo->desc);
		html("</td><td>");
		html_txt(cgit_repo->owner);
		html("</td><td>");
		print_modtime(cgit_repo);
		html("</td>");
		if (cgit_enable_index_links) {
			html("<td>");
			html_link_open(cgit_repourl(cgit_repo->url),
				       NULL, "button");
			html("summary</a>");
			cgit_log_link("log", NULL, "button", NULL, NULL, NULL, 0);
			cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
			html("</td>");
		}
		html("</tr>\n");
	}
	html("</table>");
	cgit_print_docend();
}
">= 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. */ static 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.max_parents = 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; } static 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); } static 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("Unknown statistics type: %c", code[0]); return; } if (i > ctx->repo->max_stats) { cgit_print_error("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; html("<div class='cgit-panel'>"); html("<b>stat options</b>"); html("<form method='get' action=''>"); cgit_add_hidden_formfields(1, 0, "stats"); html("<table><tr><td colspan='2'/></tr>"); if (ctx->repo->max_stats > 1) { html("<tr><td class='label'>Period:</td>"); html("<td class='ctrl'><select name='period' onchange='this.form.submit();'>"); for (i = 0; i < ctx->repo->max_stats; i++) html_option(fmt("%c", periods[i].code), periods[i].name, fmt("%c", period->code)); html("</select></td></tr>"); } html("<tr><td class='label'>Authors:</td>"); html("<td class='ctrl'><select name='ofs' onchange='this.form.submit();'>"); html_intoption(10, "10", top); html_intoption(25, "25", top); html_intoption(50, "50", top); html_intoption(100, "100", top); html_intoption(-1, "all", top); html("</select></td></tr>"); html("<tr><td/><td class='ctrl'>"); html("<noscript><input type='submit' value='Reload'/></noscript>"); html("</td></tr></table>"); html("</form>"); html("</div>"); htmlf("<h2>Commits per author per %s", period->name); if (ctx->qry.path) { html(" (path '"); html_txt(ctx->qry.path); html("')"); } html("</h2>"); print_authors(&authors, top, period); }