/* * The statistics page counts each author's commits in the most recent periods * of the size asked for. It reads no diff and no blob contents, so the whole * thing costs about one rev-list. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" #include "html.h" #include "parsing.h" #include "ui-shared.h" #include "ui-stats.h" #define DEFAULT_AUTHOR_ROWS 10 // How many periods the page shows side by side. #define PERIOD_COLUMNS 4 struct cgit_period { const char code; const char *name; // Move a tm back to the first day of the period it falls in. void (*trunc)(struct tm *tm); // Step a truncated tm n whole periods, back when n is negative. void (*step)(struct tm *tm, int n); // Label the period a tm falls in. The buffer is reused, so a caller // that keeps a label copies it. char *(*pretty)(struct tm *tm); }; // One author's share of the window. periods is keyed by the label of a period, // with the commit count stored in the util field itself rather than behind // another allocation. struct authorstat { long total; struct string_list periods; }; static void trunc_week(struct tm *tm) { time_t t = timegm(tm); // tm_wday counts from Sunday, while the label comes from %V and %G, // which number the ISO weeks that start on Monday. t -= ((tm->tm_wday + 6) % 7) * SECONDS_PER_DAY; gmtime_r(&t, tm); } static void step_week(struct tm *tm, int n) { time_t t = timegm(tm); t += n * SECONDS_PER_WEEK; gmtime_r(&t, tm); } static char *pretty_week(struct tm *tm) { static char buf[10]; // A year of five digits or more does not fit, and strftime then leaves // the buffer with contents the standard says nothing about, so empty it // rather than return whatever the last week left. if (!strftime(buf, sizeof(buf), "W%V %G", tm)) buf[0] = '\0'; return buf; } static void trunc_month(struct tm *tm) { tm->tm_mday = 1; } static void step_month(struct tm *tm, int n) { tm->tm_mon += n; if (tm->tm_mon < 0) { tm->tm_year--; tm->tm_mon += 12; } else if (tm->tm_mon > 11) { tm->tm_year++; tm->tm_mon -= 12; } } static char *pretty_month(struct tm *tm) { static const char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; return cgit_fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900); } static void trunc_quarter(struct tm *tm) { trunc_month(tm); tm->tm_mon -= tm->tm_mon % 3; } static void step_quarter(struct tm *tm, int n) { step_month(tm, 3 * n); } static char *pretty_quarter(struct tm *tm) { return cgit_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 step_year(struct tm *tm, int n) { tm->tm_year += n; } static char *pretty_year(struct tm *tm) { return cgit_fmt("%d", tm->tm_year + 1900); } // The order runs from the finest window to the coarsest, because a repository // caps the page by storing an index into this table as its max-stats. static const struct cgit_period periods[] = { {'w', "week", trunc_week, step_week, pretty_week}, {'m', "month", trunc_month, step_month, pretty_month}, {'q', "quarter", trunc_quarter, step_quarter, pretty_quarter}, {'y', "year", trunc_year, step_year, pretty_year}, }; static void window_start(const struct cgit_period *period, struct tm *tm) { // Read once, so the buckets and the column labels agree on the window // even when the clock crosses a period boundary between them. static time_t now; int i; if (!now) time(&now); gmtime_r(&now, tm); period->trunc(tm); // The window opens at midnight, not at the time of day it is now. tm->tm_hour = 0; tm->tm_min = 0; tm->tm_sec = 0; for (i = 1; i < PERIOD_COLUMNS; i++) period->step(tm, -1); } static void add_commit(struct string_list *authors, struct commitinfo *info, const struct cgit_period *period) { struct string_list_item *author, *bucket; struct authorstat *stats; char *name, *label; struct tm date; time_t when; // A commit can lack an author header, so fall back rather than // handing xstrdup a NULL. name = xstrdup(info->author ? info->author : "(unknown)"); author = string_list_insert(authors, name); if (!author->util) author->util = xcalloc(1, sizeof(struct authorstat)); else free(name); stats = author->util; when = info->committer_date; // A crafted commit can carry a date outside the range gmtime_r can // represent, which would leave date uninitialised and later index the // month table out of bounds. if (!gmtime_r(&when, &date)) return; period->trunc(&date); label = xstrdup(period->pretty(&date)); bucket = string_list_insert(&stats->periods, label); if (bucket->util) free(label); bucket->util = (void *)((uintptr_t)bucket->util + 1); stats->total++; } /* * Count the commits in the displayed window into authors, whose util fields * hold a struct authorstat, and return non-zero once an error page has * answered instead. Merge commits are left out, so that pulling a branch in * does not credit the merger with its commits. */ static int collect_stats(const struct cgit_period *period, struct string_list *authors) { struct rev_info rev; struct commit *commit; struct strvec argv = STRVEC_INIT; time_t since; struct tm tm; window_start(period, &tm); since = timegm(&tm); strvec_pushl(&argv, "stats", ctx.qry.head, NULL); if (ctx.qry.path) strvec_pushl(&argv, "--", ctx.qry.path, NULL); repo_init_revisions(the_repository, &rev, NULL); rev.max_parents = 1; setup_revisions(argv.nr, argv.v, &rev, NULL); // Prune the walk to the displayed window instead of traversing the // whole history and discarding older commits. The check below still // bounds the period edge exactly. rev.max_age = since; if (cgit_prepare_walk(&rev)) { strvec_clear(&argv); return -1; } while ((commit = get_revision(&rev))) { struct commitinfo *info = cgit_parse_commit(commit); if ((time_t)info->committer_date >= since) add_commit(authors, info, period); cgit_free_commitinfo(info); release_commit_memory(the_repository->parsed_objects, commit); commit->parents = NULL; } strvec_clear(&argv); return 0; } static int cmp_total_commits(const void *a, const void *b) { const struct string_list_item *first = a; const struct string_list_item *second = b; const struct authorstat *first_stats = first->util; const struct authorstat *second_stats = second->util; // Report the sign only, since a long difference truncated to int // could flip and leave the comparator inconsistent. if (second_stats->total > first_stats->total) return 1; if (second_stats->total < first_stats->total) return -1; return 0; } /* * The column labels for the displayed window, oldest first. pretty hands back * a buffer it goes on to reuse, so the labels are copied here once and shared * by every table below. */ static struct string_list build_period_labels(const struct cgit_period *period) { struct string_list labels = STRING_LIST_INIT_DUP; struct tm tm; int i; window_start(period, &tm); for (i = 0; i < PERIOD_COLUMNS; i++) { string_list_append(&labels, period->pretty(&tm)); period->step(&tm, 1); } return labels; } /* * The run of authors to sum is given as a start and a count rather than as * two indices, so that an empty author list cannot describe a run that wraps. */ static void print_row(struct string_list *authors, size_t from, size_t count, const char *name, const char *nameclass, const char *cellclass, const struct string_list *labels) { struct authorstat *stats; struct string_list_item *bucket; size_t i, column; long total, subtotal; total = 0; htmlf("", nameclass); html_txt(name); html(""); for (column = 0; column < labels->nr; column++) { const char *label = labels->items[column].string; subtotal = 0; for (i = from; i < from + count; i++) { stats = authors->items[i].util; bucket = string_list_lookup(&stats->periods, label); if (bucket) subtotal += (uintptr_t)bucket->util; } if (cellclass) htmlf("%ld", cellclass, subtotal); else htmlf("%ld", subtotal); total += subtotal; } htmlf("%ld\n", total); } static void print_authors(struct string_list *authors, int max_rows, const struct string_list *labels) { size_t i, column, rows; html("\n"); for (column = 0; column < labels->nr; column++) htmlf("", labels->items[column].string); html("\n"); // The row count arrives through ofs, which carries -1 for "all". rows = (max_rows <= 0 || (size_t)max_rows > authors->nr) ? authors->nr : (size_t)max_rows; for (i = 0; i < rows; i++) print_row(authors, i, 1, authors->items[i].string, "left", NULL, labels); if (rows < authors->nr) print_row(authors, rows, authors->nr - rows, cgit_fmt("Others (%ld)", (long)(authors->nr - rows)), "left", NULL, labels); print_row(authors, 0, authors->nr, "Total", "total", "sum", labels); html("
Author%sTotal
\n"); } static void print_options_form(const struct cgit_period *period, int top) { int choices, i; html("
\n"); html("Stat options"); html("
"); cgit_add_hidden_formfields(1, 0); html("
"); if (ctx.repo->max_stats > 1) { choices = ctx.repo->max_stats; html(""); html("\n"); } html(""); html("\n"); html(""); html("
"); html("
\n"); html("
\n"); } int cgit_find_stats_period(const char *expr, const struct cgit_period **period) { size_t i; char code = '\0'; if (!expr) return 0; if (strlen(expr) == 1) code = expr[0]; for (i = 0; i < ARRAY_SIZE(periods); 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 <= (int)ARRAY_SIZE(periods)) return periods[idx - 1].name; else return ""; } void cgit_show_stats(void) { struct string_list authors = STRING_LIST_INIT_NODUP; struct string_list labels; const struct cgit_period *period; int top, period_index; const char *code = "w"; if (ctx.qry.period) code = ctx.qry.period; period_index = cgit_find_stats_period(code, &period); if (!period_index) { cgit_print_error_page(404, "Not Found", "Unknown statistics type: %c", code[0]); return; } if (period_index > ctx.repo->max_stats) { cgit_print_error_page(400, "Bad Request", "Statistics type disabled: %s", period->name); return; } // A leading colon makes git read the path as a pathspec with magic, // which can end the request inside git. if (ctx.qry.path && ctx.qry.path[0] == ':') { cgit_print_error_page(400, "Bad Request", "Invalid path"); return; } if (collect_stats(period, &authors)) return; QSORT(authors.items, authors.nr, cmp_total_commits); top = ctx.qry.ofs; if (!top) top = DEFAULT_AUTHOR_ROWS; cgit_print_layout_start(); print_options_form(period, top); htmlf("

Commits per author per %s", period->name); if (ctx.qry.path) { html(" (path '"); html_txt(ctx.qry.path); html("')"); } html("

\n"); labels = build_period_labels(period); print_authors(&authors, top, &labels); string_list_clear(&labels, 0); cgit_print_layout_end(); }