]> git.cameronkatri.com Git - cgit.git/commitdiff
Add a 'stats' page to each repo
authorLars Hjemli <hjemli@gmail.com>
Sat, 6 Dec 2008 16:38:19 +0000 (17:38 +0100)
committerLars Hjemli <hjemli@gmail.com>
Sat, 6 Dec 2008 16:38:19 +0000 (17:38 +0100)
This new page, which is disabled by default, can be used to print some
statistics about the number of commits per period in the repository,
where period can be either weeks, months, quarters or years.

The function can be activated globally by setting 'enable-stats=1' in
cgitrc and disabled for individual repos by setting 'repo.enable-stats=0'.

Signed-off-by: Lars Hjemli <hjemli@gmail.com>
Makefile
cgit.c
cgit.css
cgit.h
cgitrc.5.txt
cmd.c
shared.c
ui-shared.c
ui-stats.c [new file with mode: 0644]
ui-stats.h [new file with mode: 0644]

index 561af76f31ff038d31fa56d30c77956673625b17..f426f985982f20dff008c4254e23ebf0afc17a98 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -68,6 +68,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
diff --git a/cgit.c b/cgit.c
index c82587b68b8cc5169f4265eaad98fbc938aa0b8e..22b6d7cb09d0bc1085aba5f14efdfac6e3b3bfb9 100644 (file)
--- a/cgit.c
+++ b/cgit.c
@@ -54,6 +54,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, "enable-stats"))
+               ctx.cfg.enable_stats = atoi(value);
        else if (!strcmp(name, "cache-size"))
                ctx.cfg.cache_size = atoi(value);
        else if (!strcmp(name, "cache-root"))
@@ -112,6 +114,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.enable-stats"))
+               ctx.repo->enable_stats = ctx.cfg.enable_stats && atoi(value);
        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) {
@@ -154,6 +158,8 @@ static void querystring_cb(const char *name, const char *value)
                ctx.qry.name = xstrdup(value);
        } else if (!strcmp(name, "mimetype")) {
                ctx.qry.mimetype = xstrdup(value);
+       } else if (!strcmp(name, "period")) {
+               ctx.qry.period = xstrdup(value);
        }
 }
 
index a37d21816cdbe213e1efb6b084ecece66aa4f0f3..ef30fbf899803c2a102875c1dfb3d25dd4f55760 100644 (file)
--- a/cgit.css
+++ b/cgit.css
@@ -456,3 +456,80 @@ div.footer {
        font-size: 80%;
        color: #ccc;
 }
+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;
+}
+
diff --git a/cgit.h b/cgit.h
index 91db98aa900061046fa342935f78bd82a89b665b..85045c4a98285607e962e59f34788584a7f39ab9 100644 (file)
--- a/cgit.h
+++ b/cgit.h
@@ -61,6 +61,7 @@ struct cgit_repo {
        int snapshots;
        int enable_log_filecount;
        int enable_log_linecount;
+       int enable_stats;
 };
 
 struct cgit_repolist {
@@ -119,6 +120,7 @@ struct cgit_query {
        char *name;
        char *mimetype;
        char *url;
+       char *period;
        int   ofs;
        int nohead;
 };
@@ -151,6 +153,7 @@ struct cgit_config {
        int enable_index_links;
        int enable_log_filecount;
        int enable_log_linecount;
+       int enable_stats;
        int local_time;
        int max_repo_count;
        int max_commit_count;
index 7887b02ce60ca7701b0a3d5d4db9aac9a2136325..60d3ea492d3f8a923a3102235d872b93f1c98664 100644 (file)
@@ -74,6 +74,10 @@ enable-log-linecount
        and removed lines for each commit on the repository log page. Default
        value: "0".
 
+enable-stats
+       Globally enable/disable statistics for each repository. Default
+       value: "0".
+
 favicon
        Url used as link to a shortcut icon for cgit. If specified, it is
        suggested to use the value "/favicon.ico" since certain browsers will
@@ -218,6 +222,10 @@ repo.enable-log-linecount
        A flag which can be used to disable the global setting
        `enable-log-linecount'. Default value: none.
 
+repo.enable-stats
+       A flag which can be used to disable the global setting
+       `enable-stats'. Default value: none.
+
 repo.name
        The value to show as repository name. Default value: <repo.url>.
 
diff --git a/cmd.c b/cmd.c
index 5b3c14c557e0b5aa56a95263cdbd559c2460de04..744bf84db246c7f7e4090bde978a62a94e0a1ce2 100644 (file)
--- a/cmd.c
+++ b/cmd.c
@@ -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"
@@ -109,6 +110,14 @@ static void snapshot_fn(struct cgit_context *ctx)
                            ctx->repo->snapshots, ctx->qry.nohead);
 }
 
+static void stats_fn(struct cgit_context *ctx)
+{
+       if (ctx->repo->enable_stats)
+               cgit_show_stats(ctx);
+       else
+               cgit_print_error("Stats disabled for this repo");
+}
+
 static void summary_fn(struct cgit_context *ctx)
 {
        cgit_print_summary();
@@ -145,6 +154,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),
index f5875e4273d55d6380dedd3310ebac08a9b636d7..37333f0a4018b89c80d8a064c42e7965794ee8d2 100644 (file)
--- a/shared.c
+++ b/shared.c
@@ -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->enable_stats = ctx.cfg.enable_stats;
        ret->module_link = ctx.cfg.module_link;
        ret->readme = NULL;
        return ret;
index 224e5f3b2f3da88d89a0ea3034b9182cf7775cd7..0e688a0141a4a055a35774856aa26d1d8e8c4675 100644 (file)
@@ -641,6 +641,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->enable_stats)
+                       reporevlink("stats", "stats", NULL, hc(cmd, "stats"),
+                                   ctx->qry.head, NULL, NULL);
                if (ctx->repo->readme)
                        reporevlink("about", "about", NULL,
                                    hc(cmd, "about"), ctx->qry.head, NULL,
diff --git a/ui-stats.c b/ui-stats.c
new file mode 100644 (file)
index 0000000..9150840
--- /dev/null
@@ -0,0 +1,380 @@
+#include "cgit.h"
+#include "html.h"
+#include <string-list.h>
+
+#define MONTHS 6
+
+struct 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);
+};
+
+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 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 add_commit(struct string_list *authors, struct commit *commit,
+       struct 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 Period *period)
+{
+       struct string_list authors;
+       struct rev_info rev;
+       struct commit *commit;
+       const char *argv[] = {NULL, ctx->qry.head, NULL, NULL};
+       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));
+       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(3, 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 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 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 Period *period;
+       int top, i;
+
+       period = &periods[0];
+       if (ctx->qry.period) {
+               for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
+                       if (periods[i].code == ctx->qry.period[0]) {
+                               period = &periods[i];
+                               break;
+                       }
+       }
+       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</h2>", period->name);
+
+       html("<form method='get' action='.' style='float: right; text-align: right;'>");
+       if (strcmp(ctx->qry.head, ctx->repo->defbranch))
+               htmlf("<input type='hidden' name='h' value='%s'/>", ctx->qry.head);
+       html("Period: ");
+       html("<select name='period' onchange='this.form.submit();'>");
+       for (i = 0; i < sizeof(periods) / sizeof(periods[0]); 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>&nbsp;&nbsp;<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 (file)
index 0000000..f1d744c
--- /dev/null
@@ -0,0 +1,8 @@
+#ifndef UI_STATS_H
+#define UI_STATS_H
+
+#include "cgit.h"
+
+extern void cgit_show_stats(struct cgit_context *ctx);
+
+#endif /* UI_STATS_H */