diff options
author | Lars Hjemli <hjemli@gmail.com> | 2009-01-27 20:16:37 +0100 |
---|---|---|
committer | Lars Hjemli <hjemli@gmail.com> | 2009-01-27 20:16:37 +0100 |
commit | 7710178e45dee61e85ea77c4221309ce8c086f95 (patch) | |
tree | 281c5251777308f18c05d323183b28470445f4bc | |
parent | e78186dcb63ec67a38dddfcd8f91d2108583320b (diff) | |
parent | b54ef9749c083afd86573112fad3b3ed8ee2d0e4 (diff) | |
download | cgit-7710178e45dee61e85ea77c4221309ce8c086f95.tar.gz cgit-7710178e45dee61e85ea77c4221309ce8c086f95.tar.bz2 cgit-7710178e45dee61e85ea77c4221309ce8c086f95.zip |
Merge branch 'lh/stats'
Conflicts:
cgit.c
cgit.css
cgit.h
ui-tree.c
Signed-off-by: Lars Hjemli <hjemli@gmail.com>
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | cgit.c | 8 | ||||
-rw-r--r-- | cgit.css | 76 | ||||
-rw-r--r-- | cgit.h | 3 | ||||
-rw-r--r-- | cgitrc.5.txt | 17 | ||||
-rw-r--r-- | cmd.c | 7 | ||||
-rw-r--r-- | shared.c | 1 | ||||
-rw-r--r-- | ui-shared.c | 15 | ||||
-rw-r--r-- | ui-shared.h | 5 | ||||
-rw-r--r-- | ui-stats.c | 410 | ||||
-rw-r--r-- | ui-stats.h | 27 | ||||
-rw-r--r-- | ui-tree.c | 3 |
12 files changed, 569 insertions, 4 deletions
@@ -90,6 +90,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 @@ -12,6 +12,7 @@ #include "configfile.h" #include "html.h" #include "ui-shared.h" +#include "ui-stats.h" #include "scan-tree.h" const char *cgit_version = CGIT_VERSION; @@ -54,6 +55,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, "max-stats")) + ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); else if (!strcmp(name, "cache-size")) ctx.cfg.cache_size = atoi(value); else if (!strcmp(name, "cache-root")) @@ -112,6 +115,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.max-stats")) + ctx.repo->max_stats = cgit_find_stats_period(value, NULL); 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) { @@ -158,6 +163,8 @@ static void querystring_cb(const char *name, const char *value) ctx.qry.sort = xstrdup(value); } else if (!strcmp(name, "showmsg")) { ctx.qry.showmsg = atoi(value); + } else if (!strcmp(name, "period")) { + ctx.qry.period = xstrdup(value); } } @@ -181,6 +188,7 @@ static void prepare_context(struct cgit_context *ctx) ctx->cfg.max_lock_attempts = 5; ctx->cfg.max_msg_len = 80; ctx->cfg.max_repodesc_len = 80; + ctx->cfg.max_stats = 0; ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; ctx->cfg.renamelimit = -1; ctx->cfg.robots = "index, nofollow"; @@ -495,3 +495,79 @@ a.deco { background-color: #ff8888; border: solid 1px #770000; } +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; +} @@ -61,6 +61,7 @@ struct cgit_repo { int snapshots; int enable_log_filecount; int enable_log_linecount; + int max_stats; time_t mtime; }; @@ -120,6 +121,7 @@ struct cgit_query { char *name; char *mimetype; char *url; + char *period; int ofs; int nohead; char *sort; @@ -160,6 +162,7 @@ struct cgit_config { int max_lock_attempts; int max_msg_len; int max_repodesc_len; + int max_stats; int nocache; int renamelimit; int snapshots; diff --git a/cgitrc.5.txt b/cgitrc.5.txt index ab9ab66..09f56a6 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -129,6 +129,11 @@ max-repodesc-length Specifies the maximum number of repo description characters to display on the repository index page. Default value: "80". +max-stats + Set the default maximum statistics period. Valid values are "week", + "month", "quarter" and "year". If unspecified, statistics are + disabled. Default value: none. See also: "repo.max-stats". + module-link Text which will be used as the formatstring for a hyperlink when a submodule is printed in a directory listing. The arguments for the @@ -218,6 +223,11 @@ repo.enable-log-linecount A flag which can be used to disable the global setting `enable-log-linecount'. Default value: none. +repo.max-stats + Override the default maximum statistics period. Valid values are equal + to the values specified for the global "max-stats" setting. Default + value: none. + repo.name The value to show as repository name. Default value: <repo.url>. @@ -276,6 +286,10 @@ favicon=/favicon.ico logo=/img/mylogo.png +# Enable statistics per week, month and quarter +max-stats=quarter + + # Set the title and heading of the repository index page root-title=foobar.com git repositories @@ -348,6 +362,9 @@ repo.snapshots=0 # Disable line-counts for this repo repo.enable-log-linecount=0 +# Restrict the max statistics period for this repo +repo.max-stats=month + BUGS ---- @@ -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" @@ -108,6 +109,11 @@ static void snapshot_fn(struct cgit_context *ctx) ctx->repo->snapshots, ctx->qry.nohead); } +static void stats_fn(struct cgit_context *ctx) +{ + cgit_show_stats(ctx); +} + static void summary_fn(struct cgit_context *ctx) { cgit_print_summary(); @@ -144,6 +150,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), @@ -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->max_stats = ctx.cfg.max_stats; ret->module_link = ctx.cfg.module_link; ret->readme = NULL; ret->mtime = -1; diff --git a/ui-shared.c b/ui-shared.c index fba1ba6..4f28512 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -369,6 +369,12 @@ void cgit_patch_link(char *name, char *title, char *class, char *head, reporevlink("patch", name, title, class, head, rev, NULL); } +void cgit_stats_link(char *name, char *title, char *class, char *head, + char *path) +{ + reporevlink("stats", name, title, class, head, NULL, path); +} + void cgit_object_link(struct object *obj) { char *page, *shortrev, *fullrev, *name; @@ -557,7 +563,7 @@ int print_archive_ref(const char *refname, const unsigned char *sha1, return 0; } -void add_hidden_formfields(int incl_head, int incl_search, char *page) +void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page) { char *url; @@ -619,7 +625,7 @@ void cgit_print_pageheader(struct cgit_context *ctx) cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); html("</td><td class='form'>"); html("<form method='get' action=''>\n"); - add_hidden_formfields(0, 1, ctx->qry.page); + cgit_add_hidden_formfields(0, 1, ctx->qry.page); html("<select name='h' onchange='this.form.submit();'>\n"); for_each_branch_ref(print_branch_option, ctx->qry.head); html("</select> "); @@ -656,6 +662,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->max_stats) + cgit_stats_link("stats", NULL, hc(cmd, "stats"), + ctx->qry.head, NULL); if (ctx->repo->readme) reporevlink("about", "about", NULL, hc(cmd, "about"), ctx->qry.head, NULL, @@ -666,7 +675,7 @@ void cgit_print_pageheader(struct cgit_context *ctx) html_url_path(cgit_fileurl(ctx->qry.repo, "log", ctx->qry.path, NULL)); html("'>\n"); - add_hidden_formfields(1, 0, "log"); + cgit_add_hidden_formfields(1, 0, "log"); html("<select name='qt'>\n"); html_option("grep", "log msg", ctx->qry.grep); html_option("author", "author", ctx->qry.grep); diff --git a/ui-shared.h b/ui-shared.h index 2ab53ae..5a3821f 100644 --- a/ui-shared.h +++ b/ui-shared.h @@ -30,6 +30,8 @@ extern void cgit_snapshot_link(char *name, char *title, char *class, char *head, char *rev, char *archivename); extern void cgit_diff_link(char *name, char *title, char *class, char *head, char *new_rev, char *old_rev, char *path); +extern void cgit_stats_link(char *name, char *title, char *class, char *head, + char *path); extern void cgit_object_link(struct object *obj); extern void cgit_print_error(char *msg); @@ -42,5 +44,6 @@ extern void cgit_print_pageheader(struct cgit_context *ctx); extern void cgit_print_filemode(unsigned short mode); extern void cgit_print_snapshot_links(const char *repo, const char *head, const char *hex, int snapshots); - +extern void cgit_add_hidden_formfields(int incl_head, int incl_search, + char *page); #endif /* UI_SHARED_H */ diff --git a/ui-stats.c b/ui-stats.c new file mode 100644 index 0000000..9fc06d3 --- /dev/null +++ b/ui-stats.c @@ -0,0 +1,410 @@ +#include <string-list.h> + +#include "cgit.h" +#include "html.h" +#include "ui-shared.h" +#include "ui-stats.h" + +#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; +} + +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(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 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(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 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(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 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)); + 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> <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..4f13dba --- /dev/null +++ b/ui-stats.h @@ -0,0 +1,27 @@ +#ifndef UI_STATS_H +#define UI_STATS_H + +#include "cgit.h" + +struct cgit_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); +}; + +extern int cgit_find_stats_period(const char *expr, struct cgit_period **period); + +extern void cgit_show_stats(struct cgit_context *ctx); + +#endif /* UI_STATS_H */ @@ -109,6 +109,9 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen, html("<td>"); cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, fullpath, 0, NULL, NULL, ctx.qry.showmsg); + if (ctx.repo->max_stats) + cgit_stats_link("stats", NULL, "button", ctx.qry.head, + fullpath); html("</td></tr>\n"); free(name); return 0; |