diff options
Diffstat (limited to 'source/ui-stats.c')
| -rw-r--r-- | source/ui-stats.c | 570 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1 file changed, 297 insertions, 273 deletions
diff --git a/source/ui-stats.c b/source/ui-stats.c index 20e9a30..5436907 100644 --- a/source/ui-stats.c +++ b/source/ui-stats.c @@ -1,44 +1,89 @@ -/* ui-stats.c: generate stats view - * - * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> - * - * Licensed under GNU General Public License v2 - * (see LICENSE.txt for full license text) +/* + * The statistics page, which counts commits per author across a window of + * recent weeks, months, quarters or years and breaks the tip of the branch + * down by the language its files are written in. 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. Neither half of the page reads a + * diff or the contents of a blob, so the whole thing costs about one rev-list + * plus one tree read. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" -#include "ui-stats.h" #include "html.h" +#include "parsing.h" +#include "shared.h" #include "ui-shared.h" +#include "ui-stats.h" + +#define DEFAULT_AUTHOR_ROWS 10 +#define MAX_LANGUAGE_ROWS 6 +static const struct { + const char *ext; + const char *label; +} lang_map[] = { + {"c", "C"}, {"h", "C"}, + {"cpp", "C++"}, {"cc", "C++"}, {"cxx", "C++"}, + {"hpp", "C++"}, {"hh", "C++"}, + {"js", "JavaScript"}, {"mjs", "JavaScript"}, + {"ts", "TypeScript"}, {"tsx", "TypeScript"}, + {"py", "Python"}, {"lua", "Lua"}, + {"sh", "Shell"}, {"bash", "Shell"}, + {"go", "Go"}, {"rs", "Rust"}, {"zig", "Zig"}, + {"java", "Java"}, {"kt", "Kotlin"}, {"cs", "C#"}, {"swift", "Swift"}, + {"rb", "Ruby"}, {"pl", "Perl"}, {"pm", "Perl"}, {"php", "PHP"}, + {"hs", "Haskell"}, {"el", "Lisp"}, {"ml", "OCaml"}, + {"css", "CSS"}, {"scss", "CSS"}, + {"html", "HTML"}, {"htm", "HTML"}, {"xml", "XML"}, + {"md", "Markdown"}, {"rst", "Text"}, {"txt", "Text"}, + {"json", "JSON"}, {"yml", "YAML"}, {"yaml", "YAML"}, {"toml", "TOML"}, + {"mk", "Make"}, {"tex", "TeX"}, {"sql", "SQL"}, {"vim", "Vimscript"}, +}; + +/* + * 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 list; + struct string_list periods; }; -#define DAY_SECS (60 * 60 * 24) -#define WEEK_SECS (DAY_SECS * 7) +/* + * Bytes at the tip of the branch per language. langs is keyed by the language + * name, again with the count living in the util field. + */ +struct lang_sizes { + struct string_list langs; + unsigned long total; +}; static void trunc_week(struct tm *tm) { time_t t = timegm(tm); - t -= ((tm->tm_wday + 6) % 7) * DAY_SECS; + + // 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 -= WEEK_SECS; + + t -= SECONDS_PER_WEEK; gmtime_r(&t, tm); } static void inc_week(struct tm *tm) { time_t t = timegm(tm); - t += WEEK_SECS; + + t += SECONDS_PER_WEEK; gmtime_r(&t, tm); } @@ -46,7 +91,11 @@ static char *pretty_week(struct tm *tm) { static char buf[10]; - strftime(buf, sizeof(buf), "W%V %G", tm); + // 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; } @@ -129,6 +178,10 @@ 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}, @@ -136,95 +189,58 @@ static const struct cgit_period periods[] = { {'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, const struct cgit_period **period) +static void window_start(const struct cgit_period *period, struct tm *tm) { - 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; -} + time_t now; + int i; -const char *cgit_find_stats_periodname(int idx) -{ - if (idx > 0 && idx <= (int)ARRAY_SIZE(periods)) - return periods[idx - 1].name; - else - return ""; + 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) + const struct cgit_period *period) { - struct string_list_item *author, *item; - struct authorstat *authorstat; - struct string_list *items; - char *tmp; + struct string_list_item *author, *bucket; + struct authorstat *stats; + char *name, *label; struct tm date; - time_t t; - uintptr_t *counter; + time_t when; + uintptr_t *count; - /* A commit can lack an author header, so fall back rather than - * xstrdup(NULL). */ - tmp = xstrdup(info->author ? info->author : "(unknown)"); - author = string_list_insert(authors, tmp); + // 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(tmp); - authorstat = author->util; - items = &authorstat->list; - t = info->committer_date; + 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. Drop such a commit from the stats. - if (!gmtime_r(&t, &date)) + // represent, which would leave date uninitialized and later index the + // month table out of bounds. + if (!gmtime_r(&when, &date)) return; period->trunc(&date); - tmp = xstrdup(period->pretty(&date)); - item = string_list_insert(items, tmp); - counter = (uintptr_t *)&item->util; - if (*counter) - free(tmp); - (*counter)++; + label = xstrdup(period->pretty(&date)); + bucket = string_list_insert(&stats->periods, label); + count = (uintptr_t *)&bucket->util; + if (*count) + free(label); + (*count)++; - authorstat->total++; + stats->total++; } -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 the sign only, since a long difference truncated to int could - // flip and leave the comparator inconsistent. - if (auth2->total > auth1->total) - return 1; - if (auth2->total < auth1->total) - return -1; - return 0; -} - -/* Walk the commit DAG once for the configured window. One walk, no - * diffs, so the cost class matches rev-list and the page cache absorbs - * repeat views. Merge commits are skipped. +/* + * 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) { @@ -233,15 +249,10 @@ static struct string_list collect_stats(const struct cgit_period *period) struct commit *commit; const char *argv[] = {NULL, ctx.qry.head, NULL, NULL}; int argc = 2; - time_t now, since; - long i; + time_t since; struct tm tm; - time(&now); - gmtime_r(&now, &tm); - period->trunc(&tm); - for (i = 1; i < period->count; i++) - period->dec(&tm); + window_start(period, &tm); since = timegm(&tm); if (ctx.qry.path) { @@ -255,10 +266,12 @@ static struct string_list collect_stats(const struct cgit_period *period) 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 in-process check - // below still bounds the period edge exactly. + // 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)); @@ -275,24 +288,34 @@ static struct string_list collect_stats(const struct cgit_period *period) 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. Every table below - * walks the same period, and each step of that walk converts a struct tm to a - * time_t and back, so the labels are worked out once and shared. It also frees - * the callers from pretty() handing back a buffer it reuses. + * 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; - time_t now; int i; - time(&now); - gmtime_r(&now, &tm); - period->trunc(&tm); - for (i = 1; i < period->count; i++) - period->dec(&tm); + window_start(period, &tm); for (i = 0; i < period->count; i++) { string_list_append(&labels, period->pretty(&tm)); period->inc(&tm); @@ -300,34 +323,33 @@ static struct string_list build_period_labels(const struct cgit_period *period) return labels; } -/* Sum one row across a run of authors. Taking a count rather than an end index - * keeps an empty author list from describing a run that wraps. */ -static void print_combined_authorrow(struct string_list *authors, size_t from, - size_t count, const char *name, - const char *leftclass, - const char *centerclass, - const char *rightclass, - const struct string_list *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 string_list_item *author; - struct authorstat *authorstat; - struct string_list *items; - struct string_list_item *date; - size_t i, j; + struct authorstat *stats; + struct string_list_item *bucket; + size_t i, column; long total, subtotal; total = 0; htmlf("<tr><td class='%s'>%s</td>", leftclass, - cgit_fmt(name, (long)count)); - for (j = 0; j < labels->nr; j++) { + cgit_fmt(label_format, (long)count)); + for (column = 0; column < labels->nr; column++) { + const char *label = labels->items[column].string; + subtotal = 0; for (i = from; i < from + count; i++) { - author = &authors->items[i]; - authorstat = author->util; - items = &authorstat->list; - date = string_list_lookup(items, labels->items[j].string); - if (date) - subtotal += (uintptr_t)date->util; + stats = authors->items[i].util; + bucket = string_list_lookup(&stats->periods, label); + if (bucket) + subtotal += (uintptr_t)bucket->util; } htmlf("<td class='%s'>%ld</td>", centerclass, subtotal); total += subtotal; @@ -335,91 +357,75 @@ static void print_combined_authorrow(struct string_list *authors, size_t from, htmlf("<td class='%s'>%ld</td></tr>", rightclass, total); } -static void print_authors(struct string_list *authors, int requested_top, +static void print_authors(struct string_list *authors, int max_rows, const struct string_list *labels) { - struct string_list_item *author; - struct authorstat *authorstat; - struct string_list *items; - struct string_list_item *date; - size_t i, j, top; + struct string_list_item *author, *bucket; + struct authorstat *stats; + size_t i, column, rows; long total; html("<table class='stats'><tr><th>Author</th>"); - for (j = 0; j < labels->nr; j++) - htmlf("<th>%s</th>", labels->items[j].string); + for (column = 0; column < labels->nr; column++) + htmlf("<th>%s</th>", labels->items[column].string); html("<th>Total</th></tr>\n"); - // The author count arrives through ofs, which carries -1 for "all". - top = (requested_top <= 0 || (size_t)requested_top > authors->nr) - ? authors->nr : (size_t)requested_top; + // 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 < top; i++) { + for (i = 0; i < rows; i++) { author = &authors->items[i]; html("<tr><td class='left'>"); html_txt(author->string); html("</td>"); - authorstat = author->util; - items = &authorstat->list; + stats = author->util; total = 0; - for (j = 0; j < labels->nr; j++) { - date = string_list_lookup(items, labels->items[j].string); - if (!date) + for (column = 0; column < labels->nr; column++) { + const char *label = labels->items[column].string; + + bucket = string_list_lookup(&stats->periods, label); + if (!bucket) html("<td>0</td>"); else { - htmlf("<td>%lu</td>", (uintptr_t)date->util); - total += (uintptr_t)date->util; + htmlf("<td>%lu</td>", (uintptr_t)bucket->util); + total += (uintptr_t)bucket->util; } } htmlf("<td class='sum'>%ld</td></tr>", total); } - if (top < authors->nr) - print_combined_authorrow(authors, top, authors->nr - top, + if (rows < authors->nr) + print_summary_row(authors, rows, authors->nr - rows, "Others (%ld)", "left", "", "sum", labels); - print_combined_authorrow(authors, 0, authors->nr, "Total", + print_summary_row(authors, 0, authors->nr, "Total", "total", "sum", "sum", labels); html("</table>"); } -/* Bytes at HEAD per language, judged by file extension. One recursive - * tree read, sizes come from object headers without loading content. */ -static const struct { +static const char *language_of(const char *pathname) +{ const char *ext; - const char *label; -} lang_map[] = { - {"c", "C"}, {"h", "C"}, - {"cpp", "C++"}, {"cc", "C++"}, {"cxx", "C++"}, - {"hpp", "C++"}, {"hh", "C++"}, - {"js", "JavaScript"}, {"mjs", "JavaScript"}, - {"ts", "TypeScript"}, {"tsx", "TypeScript"}, - {"py", "Python"}, {"lua", "Lua"}, - {"sh", "Shell"}, {"bash", "Shell"}, - {"go", "Go"}, {"rs", "Rust"}, {"zig", "Zig"}, - {"java", "Java"}, {"kt", "Kotlin"}, {"cs", "C#"}, {"swift", "Swift"}, - {"rb", "Ruby"}, {"pl", "Perl"}, {"pm", "Perl"}, {"php", "PHP"}, - {"hs", "Haskell"}, {"el", "Lisp"}, {"ml", "OCaml"}, - {"css", "CSS"}, {"scss", "CSS"}, - {"html", "HTML"}, {"htm", "HTML"}, {"xml", "XML"}, - {"md", "Markdown"}, {"rst", "Text"}, {"txt", "Text"}, - {"json", "JSON"}, {"yml", "YAML"}, {"yaml", "YAML"}, {"toml", "TOML"}, - {"mk", "Make"}, {"tex", "TeX"}, {"sql", "SQL"}, {"vim", "Vimscript"}, -}; + size_t i; -struct lang_walk_ctx { - struct string_list langs; - unsigned long total; -}; + ext = strrchr(pathname, '.'); + if (ext && ext != pathname && ext[1]) { + for (i = 0; i < ARRAY_SIZE(lang_map); i++) + if (!strcasecmp(ext + 1, lang_map[i].ext)) + return lang_map[i].label; + } else if (!strcmp(pathname, "Makefile")) { + return "Make"; + } + return "Other"; +} -static int lang_walk_cb(const struct object_id *oid, struct strbuf *base, - const char *pathname, unsigned mode, void *cbdata) +static int add_blob_size(const struct object_id *oid, struct strbuf *base, + const char *pathname, unsigned mode, void *data) { - struct lang_walk_ctx *lw = cbdata; - struct string_list_item *item; - const char *ext, *label = NULL; + struct lang_sizes *sizes = data; + struct string_list_item *lang; unsigned long size; - int i; if (S_ISDIR(mode)) return READ_TREE_RECURSIVE; @@ -429,35 +435,26 @@ static int lang_walk_cb(const struct object_id *oid, struct strbuf *base, || !size) return 0; - ext = strrchr(pathname, '.'); - if (ext && ext != pathname && ext[1]) { - for (i = 0; i < (int)ARRAY_SIZE(lang_map); i++) - if (!strcasecmp(ext + 1, lang_map[i].ext)) { - label = lang_map[i].label; - break; - } - } else if (!strcmp(pathname, "Makefile")) { - label = "Make"; - } - if (!label) - label = "Other"; - item = string_list_insert(&lw->langs, label); - item->util = (void *)((uintptr_t)item->util + size); - lw->total += size; + lang = string_list_insert(&sizes->langs, language_of(pathname)); + lang->util = (void *)((uintptr_t)lang->util + size); + sizes->total += size; return 0; } -static int cmp_lang_bytes(const void *a1, const void *a2) +static int cmp_lang_bytes(const void *a, const void *b) { - const struct string_list_item *i1 = a1; - const struct string_list_item *i2 = a2; - uintptr_t b1 = (uintptr_t)i1->util; - uintptr_t b2 = (uintptr_t)i2->util; + const struct string_list_item *first = a; + const struct string_list_item *second = b; + uintptr_t first_bytes = (uintptr_t)first->util; + uintptr_t second_bytes = (uintptr_t)second->util; - return b1 < b2 ? 1 : b1 > b2 ? -1 : 0; + return first_bytes < second_bytes ? 1 : + first_bytes > second_bytes ? -1 : 0; } -static void summarize_tree(struct lang_walk_ctx *lw) +// Leaves the list sorted largest first, which is the order the rows print in +// and what lets the caller stop naming languages once it reaches the tail. +static void measure_languages(struct lang_sizes *sizes) { struct pathspec paths = { .nr = 0 }; struct object_id oid; @@ -470,9 +467,9 @@ static void summarize_tree(struct lang_walk_ctx *lw) return; read_tree(the_repository, repo_get_commit_tree(the_repository, commit), - &paths, lang_walk_cb, lw); - qsort(lw->langs.items, lw->langs.nr, sizeof(struct string_list_item), - cmp_lang_bytes); + &paths, add_blob_size, sizes); + qsort(sizes->langs.items, sizes->langs.nr, + sizeof(struct string_list_item), cmp_lang_bytes); } static void print_language_row(const char *label, unsigned long bytes, @@ -489,103 +486,63 @@ static void print_language_row(const char *label, unsigned long bytes, strbuf_release(&size); } -/* Bordered like the commits-per-author table. */ -static void print_languages(struct lang_walk_ctx *lw) +static void print_languages(const struct lang_sizes *sizes) { unsigned long other; size_t i; int shown; - if (!lw->total) + if (!sizes->total) return; - /* Show up to six named rows, everything else folds into an - * Other row at the end. */ + // The Other row is printed last whatever its size, so everything past + // the named languages is summed into it up front. other = 0; shown = 0; - for (i = 0; i < lw->langs.nr; i++) { - if (shown < 6 && strcmp(lw->langs.items[i].string, "Other")) { + for (i = 0; i < sizes->langs.nr; i++) { + if (shown < MAX_LANGUAGE_ROWS && + strcmp(sizes->langs.items[i].string, "Other")) { shown++; continue; } - other += (uintptr_t)lw->langs.items[i].util; + other += (uintptr_t)sizes->langs.items[i].util; } html("<h2>Languages</h2>"); html("<table class='stats'>"); html("<tr><th>Language</th><th>Size</th><th>Share</th></tr>\n"); shown = 0; - for (i = 0; i < lw->langs.nr && shown < 6; i++) { - if (!strcmp(lw->langs.items[i].string, "Other")) + for (i = 0; i < sizes->langs.nr && shown < MAX_LANGUAGE_ROWS; i++) { + if (!strcmp(sizes->langs.items[i].string, "Other")) continue; - print_language_row(lw->langs.items[i].string, - (uintptr_t)lw->langs.items[i].util, - lw->total); + print_language_row(sizes->langs.items[i].string, + (uintptr_t)sizes->langs.items[i].util, + sizes->total); shown++; } if (other) - print_language_row("Other", other, lw->total); + print_language_row("Other", other, sizes->total); 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(void) +static void print_options_form(const struct cgit_period *period, int top) { - struct string_list authors; - struct lang_walk_ctx lw; - const struct cgit_period *period; - int top, i; - const char *code = "w"; + int choices, i; - if (ctx.qry.period) - code = ctx.qry.period; - - i = cgit_find_stats_period(code, &period); - if (!i) { - cgit_print_error_page(404, "Not found", - "Unknown statistics type: %c", code[0]); - return; - } - if (ctx.repo->max_stats && i > ctx.repo->max_stats) { - cgit_print_error_page(400, "Bad request", - "Statistics type disabled: %s", period->name); - return; - } - /* Walk the tree before the history walk. Releasing commit memory - * during that walk resets each commit's slab index, and a later - * lookup through the commit graph would then read another - * commit's slot and walk the wrong tree. */ - memset(&lw, 0, sizeof(lw)); - summarize_tree(&lw); - - authors = collect_stats(period); - qsort(authors.items, authors.nr, sizeof(struct string_list_item), - cmp_total_commits); - - top = ctx.qry.ofs; - if (!top) - top = 10; - - cgit_print_layout_start(); - - /* The options panel floats right of the page top, the same spot - * the diff controls occupy on the diff pages. */ html("<div class='cgit-panel'>"); html("<b>stat options</b>"); html("<form method='get'>"); cgit_add_hidden_formfields(1, 0, "stats"); html("<table><tr><td colspan='2'/></tr>"); if (!ctx.repo->max_stats || ctx.repo->max_stats > 1) { - int nperiods = ctx.repo->max_stats ? + choices = ctx.repo->max_stats ? ctx.repo->max_stats : (int)ARRAY_SIZE(periods); html("<tr><td class='label'>Period:</td>"); html("<td class='ctrl'><select name='period' data-autosubmit='1'>"); - for (i = 0; i < nperiods; i++) + for (i = 0; i < choices; i++) html_option(cgit_fmt("%c", periods[i].code), - periods[i].name, cgit_fmt("%c", period->code)); + periods[i].name, + cgit_fmt("%c", period->code)); html("</select></td></tr>"); } html("<tr><td class='label'>Authors:</td>"); @@ -601,6 +558,75 @@ void cgit_show_stats(void) html("</td></tr></table>"); html("</form>"); html("</div>"); +} + +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, labels; + struct lang_sizes sizes; + 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 (ctx.repo->max_stats && period_index > ctx.repo->max_stats) { + cgit_print_error_page(400, "Bad request", + "Statistics type disabled: %s", period->name); + return; + } + // The tree has to be measured before the history walk. That walk + // releases each commit as it goes, which resets the commit's slab + // index, so a lookup afterwards would read another commit's slot and + // walk the wrong tree. + memset(&sizes, 0, sizeof(sizes)); + measure_languages(&sizes); + + authors = collect_stats(period); + qsort(authors.items, authors.nr, sizeof(struct string_list_item), + cmp_total_commits); + + top = ctx.qry.ofs; + if (!top) + top = DEFAULT_AUTHOR_ROWS; + + cgit_print_layout_start(); + print_options_form(period, top); htmlf("<h2>Commits per author per %s", period->name); if (ctx.qry.path) { @@ -609,15 +635,13 @@ void cgit_show_stats(void) html("')"); } html("</h2>"); - { - struct string_list labels = build_period_labels(period); - print_authors(&authors, top, &labels); - string_list_clear(&labels, 0); - } - print_languages(&lw); - string_list_clear(&lw.langs, 0); + labels = build_period_labels(period); + print_authors(&authors, top, &labels); + string_list_clear(&labels, 0); + + print_languages(&sizes); + string_list_clear(&sizes.langs, 0); cgit_print_layout_end(); } - |
