Diff options
Diffstat (limited to '')
-rw-r--r--source/ui-stats.c186
1 file changed, 75 insertions, 111 deletions
diff --git a/source/ui-stats.c b/source/ui-stats.c
index e4dcf8e..ee5b76b 100644
--- a/source/ui-stats.c
+++ b/source/ui-stats.c
@@ -1,9 +1,7 @@
/*
- * 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.
+ * 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
@@ -11,12 +9,29 @@
#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
+// 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.
@@ -35,19 +50,11 @@ static void trunc_week(struct tm *tm)
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)
+static void step_week(struct tm *tm, int n)
{
time_t t = timegm(tm);
- t += SECONDS_PER_WEEK;
+ t += n * SECONDS_PER_WEEK;
gmtime_r(&t, tm);
}
@@ -68,21 +75,15 @@ static void trunc_month(struct tm *tm)
tm->tm_mday = 1;
}
-static void dec_month(struct tm *tm)
+static void step_month(struct tm *tm, int n)
{
- tm->tm_mon--;
+ tm->tm_mon += n;
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_mon += 12;
+ } else if (tm->tm_mon > 11) {
tm->tm_year++;
- tm->tm_mon = 0;
+ tm->tm_mon -= 12;
}
}
@@ -98,22 +99,12 @@ static char *pretty_month(struct tm *tm)
static void trunc_quarter(struct tm *tm)
{
trunc_month(tm);
- while (tm->tm_mon % 3 != 0)
- dec_month(tm);
+ tm->tm_mon -= tm->tm_mon % 3;
}
-static void dec_quarter(struct tm *tm)
+static void step_quarter(struct tm *tm, int n)
{
- 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);
+ step_month(tm, 3 * n);
}
static char *pretty_quarter(struct tm *tm)
@@ -127,14 +118,9 @@ static void trunc_year(struct tm *tm)
tm->tm_mon = 0;
}
-static void dec_year(struct tm *tm)
-{
- tm->tm_year--;
-}
-
-static void inc_year(struct tm *tm)
+static void step_year(struct tm *tm, int n)
{
- tm->tm_year++;
+ tm->tm_year += n;
}
static char *pretty_year(struct tm *tm)
@@ -145,10 +131,10 @@ static char *pretty_year(struct tm *tm)
// 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", 4, trunc_week, dec_week, inc_week, pretty_week},
- {'m', "month", 4, trunc_month, dec_month, inc_month, pretty_month},
- {'q', "quarter", 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
- {'y', "year", 4, trunc_year, dec_year, inc_year, pretty_year},
+ {'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)
@@ -162,8 +148,12 @@ static void window_start(const struct cgit_period *period, struct tm *tm)
time(&now);
gmtime_r(&now, tm);
period->trunc(tm);
- for (i = 1; i < period->count; i++)
- period->dec(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,
@@ -186,13 +176,12 @@ static void add_commit(struct string_list *authors, struct commitinfo *info,
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
+ // 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));
- // The pointer field holds the count itself.
bucket = string_list_insert(&stats->periods, label);
if (bucket->util)
free(label);
@@ -211,38 +200,25 @@ static int collect_stats(const struct cgit_period *period, struct string_list *a
{
struct rev_info rev;
struct commit *commit;
- // setup_revisions reads these the way main does, skipping the first
- // entry, and reads past the count up to a NULL after the double dash,
- // 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;
+ struct strvec argv = STRVEC_INIT;
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;
- }
+ 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.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);
+ setup_revisions_from_strvec(&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;
- // A failed setup leaves the walk holding freed commits, so it must
- // not be read from.
- if (prepare_revision_walk(&rev)) {
- cgit_print_error_page(500, "Internal Server Error", "Unable to read the history");
+ if (cgit_prepare_walk(&rev)) {
+ strvec_clear(&argv);
return -1;
}
while ((commit = get_revision(&rev))) {
@@ -255,6 +231,8 @@ static int collect_stats(const struct cgit_period *period, struct string_list *a
release_commit_memory(the_repository->parsed_objects, commit);
commit->parents = NULL;
}
+ release_revisions(&rev);
+ strvec_clear(&argv);
return 0;
}
@@ -286,9 +264,9 @@ static struct string_list build_period_labels(const struct cgit_period *period)
int i;
window_start(period, &tm);
- for (i = 0; i < period->count; i++) {
+ for (i = 0; i < PERIOD_COLUMNS; i++) {
string_list_append(&labels, period->pretty(&tm));
- period->inc(&tm);
+ period->step(&tm, 1);
}
return labels;
}
@@ -297,9 +275,9 @@ static struct string_list build_period_labels(const struct cgit_period *period)
* 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)
+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;
@@ -307,7 +285,9 @@ static void print_summary_row(struct string_list *authors, size_t from, size_t c
long total, subtotal;
total = 0;
- htmlf("<tr><td class='%s'>%s</td>", leftclass, cgit_fmt(label_format, (long)count));
+ htmlf("<tr><td class='%s'>", nameclass);
+ html_txt(name);
+ html("</td>");
for (column = 0; column < labels->nr; column++) {
const char *label = labels->items[column].string;
@@ -318,18 +298,18 @@ static void print_summary_row(struct string_list *authors, size_t from, size_t c
if (bucket)
subtotal += (uintptr_t)bucket->util;
}
- htmlf("<td class='%s'>%ld</td>", centerclass, subtotal);
+ if (cellclass)
+ htmlf("<td class='%s'>%ld</td>", cellclass, subtotal);
+ else
+ htmlf("<td>%ld</td>", subtotal);
total += subtotal;
}
- htmlf("<td class='%s'>%ld</td></tr>\n", rightclass, total);
+ htmlf("<td class='sum'>%ld</td></tr>\n", total);
}
static void print_authors(struct string_list *authors, int max_rows, const struct string_list *labels)
{
- struct string_list_item *author, *bucket;
- struct authorstat *stats;
size_t i, column, rows;
- long total;
html("<table class='stats'>\n<tr><th scope='col'>Author</th>");
for (column = 0; column < labels->nr; column++)
@@ -339,32 +319,14 @@ static void print_authors(struct string_list *authors, int max_rows, const struc
// 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++) {
- author = &authors->items[i];
- html("<tr><td class='left'>");
- html_txt(author->string);
- html("</td>");
- 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("<td>0</td>");
- else {
- htmlf("<td>%ju</td>", (uintmax_t)(uintptr_t)bucket->util);
- total += (uintptr_t)bucket->util;
- }
- }
- htmlf("<td class='sum'>%ld</td></tr>\n", total);
- }
+ for (i = 0; i < rows; i++)
+ print_row(authors, i, 1, authors->items[i].string, "left", NULL, labels);
if (rows < authors->nr)
- print_summary_row(authors, rows, authors->nr - rows, "Others (%ld)", "left", "", "sum",
- labels);
+ print_row(authors, rows, authors->nr - rows,
+ cgit_fmt("Others (%ld)", (long)(authors->nr - rows)), "left", NULL, labels);
- print_summary_row(authors, 0, authors->nr, "Total", "total", "sum", "sum", labels);
+ print_row(authors, 0, authors->nr, "Total", "total", "sum", labels);
html("</table>\n");
}
@@ -375,7 +337,7 @@ static void print_options_form(const struct cgit_period *period, int top)
html("<div class='cgit-panel'>\n");
html("<strong>Stat options</strong>");
html("<form method='get' aria-label='Stat options'>");
- cgit_add_hidden_formfields(1, 0, "stats");
+ cgit_add_hidden_formfields(1, 0);
html("<div class='fields'>");
if (ctx.repo->max_stats > 1) {
choices = ctx.repo->max_stats;
@@ -448,9 +410,11 @@ void cgit_show_stats(void)
cgit_print_error_page(400, "Bad Request", "Statistics type disabled: %s", period->name);
return;
}
+ if (cgit_refuse_pathspec_magic(ctx.qry.path))
+ return;
if (collect_stats(period, &authors))
return;
- cgit_qsort(authors.items, authors.nr, sizeof(struct string_list_item), cmp_total_commits);
+ QSORT(authors.items, authors.nr, cmp_total_commits);
top = ctx.qry.ofs;
if (!top)