/* * The statistics page, which counts commits per author across a window of * recent weeks, months, quarters or years. The window sizes live in one table * here, which cgit.c also resolves the max-stats setting against so a * repository can refuse the coarser windows. The page 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 "shared.h" #include "ui-shared.h" #include "ui-stats.h" #define DEFAULT_AUTHOR_ROWS 10 /* * 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 dec_week(struct tm *tm) { time_t t = timegm(tm); t -= SECONDS_PER_WEEK; gmtime_r(&t, tm); } static void inc_week(struct tm *tm) { time_t t = timegm(tm); t += 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 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 cgit_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 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 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 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", 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 window_start(const struct cgit_period *period, struct tm *tm) { time_t now; int i; time(&now); gmtime_r(&now, tm); period->trunc(tm); for (i = 1; i < period->count; i++) period->dec(tm); } 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; uintptr_t *count; // 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 uninitialized 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); count = (uintptr_t *)&bucket->util; if (*count) free(label); (*count)++; stats->total++; } /* * Count the commits in the displayed window, returning a list of authors * whose util field holds a struct authorstat. Merge commits are left out, so * that pulling a branch in does not credit the merger with its commits. */ static struct string_list collect_stats(const struct cgit_period *period) { struct string_list authors; struct rev_info rev; struct commit *commit; // setup_revisions reads the entries after the double dash up to a // NULL, past the count, so the sentinel has to stay even when the // path fills the slot before it. const char *argv[] = {NULL, ctx.qry.head, NULL, NULL, NULL}; int argc = 2; time_t since; struct tm tm; window_start(period, &tm); since = timegm(&tm); if (ctx.qry.path) { argv[2] = "--"; argv[3] = ctx.qry.path; argc += 2; } repo_init_revisions(the_repository, &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 reads argv the way main does and ignores the first // entry, so the head to walk sits at argv[1]. setup_revisions(argc, argv, &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; prepare_revision_walk(&rev); memset(&authors, 0, sizeof(authors)); while ((commit = get_revision(&rev)) != NULL) { 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; } return authors; } 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->count; i++) { string_list_append(&labels, period->pretty(&tm)); period->inc(&tm); } 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_summary_row(struct string_list *authors, size_t from, size_t count, const char *label_format, const char *leftclass, const char *centerclass, const char *rightclass, const struct string_list *labels) { struct authorstat *stats; struct string_list_item *bucket; size_t i, column; long total, subtotal; total = 0; htmlf("
| Author | "); for (column = 0; column < labels->nr; column++) htmlf("%s | ", labels->items[column].string); html("Total | |
|---|---|---|---|
| "); html_txt(author->string); html(" | "); stats = author->util; total = 0; for (column = 0; column < labels->nr; column++) { const char *label = labels->items[column].string; bucket = string_list_lookup(&stats->periods, label); if (!bucket) html("0 | "); else { htmlf("%lu | ", (uintptr_t)bucket->util); total += (uintptr_t)bucket->util; } } htmlf("%ld |