summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--Makefile1
-rw-r--r--cgit.c8
-rw-r--r--cgit.css76
-rw-r--r--cgit.h3
-rw-r--r--cgitrc.5.txt17
-rw-r--r--cmd.c7
-rw-r--r--shared.c1
-rw-r--r--ui-shared.c15
-rw-r--r--ui-shared.h5
-rw-r--r--ui-stats.c410
-rw-r--r--ui-stats.h27
-rw-r--r--ui-tree.c3
12 files changed, 569 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index 7793c0b..a52285e 100644
--- a/Makefile
+++ b/Makefile
@@ -87,12 +87,13 @@ OBJECTS += ui-log.o
87OBJECTS += ui-patch.o 87OBJECTS += ui-patch.o
88OBJECTS += ui-plain.o 88OBJECTS += ui-plain.o
89OBJECTS += ui-refs.o 89OBJECTS += ui-refs.o
90OBJECTS += ui-repolist.o 90OBJECTS += ui-repolist.o
91OBJECTS += ui-shared.o 91OBJECTS += ui-shared.o
92OBJECTS += ui-snapshot.o 92OBJECTS += ui-snapshot.o
93OBJECTS += ui-stats.o
93OBJECTS += ui-summary.o 94OBJECTS += ui-summary.o
94OBJECTS += ui-tag.o 95OBJECTS += ui-tag.o
95OBJECTS += ui-tree.o 96OBJECTS += ui-tree.o
96 97
97ifdef NEEDS_LIBICONV 98ifdef NEEDS_LIBICONV
98 EXTLIBS += -liconv 99 EXTLIBS += -liconv
diff --git a/cgit.c b/cgit.c
index f35f605..608cab6 100644
--- a/cgit.c
+++ b/cgit.c
@@ -9,12 +9,13 @@
9#include "cgit.h" 9#include "cgit.h"
10#include "cache.h" 10#include "cache.h"
11#include "cmd.h" 11#include "cmd.h"
12#include "configfile.h" 12#include "configfile.h"
13#include "html.h" 13#include "html.h"
14#include "ui-shared.h" 14#include "ui-shared.h"
15#include "ui-stats.h"
15#include "scan-tree.h" 16#include "scan-tree.h"
16 17
17const char *cgit_version = CGIT_VERSION; 18const char *cgit_version = CGIT_VERSION;
18 19
19void config_cb(const char *name, const char *value) 20void config_cb(const char *name, const char *value)
20{ 21{
@@ -51,12 +52,14 @@ void config_cb(const char *name, const char *value)
51 else if (!strcmp(name, "enable-index-links")) 52 else if (!strcmp(name, "enable-index-links"))
52 ctx.cfg.enable_index_links = atoi(value); 53 ctx.cfg.enable_index_links = atoi(value);
53 else if (!strcmp(name, "enable-log-filecount")) 54 else if (!strcmp(name, "enable-log-filecount"))
54 ctx.cfg.enable_log_filecount = atoi(value); 55 ctx.cfg.enable_log_filecount = atoi(value);
55 else if (!strcmp(name, "enable-log-linecount")) 56 else if (!strcmp(name, "enable-log-linecount"))
56 ctx.cfg.enable_log_linecount = atoi(value); 57 ctx.cfg.enable_log_linecount = atoi(value);
58 else if (!strcmp(name, "max-stats"))
59 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
57 else if (!strcmp(name, "cache-size")) 60 else if (!strcmp(name, "cache-size"))
58 ctx.cfg.cache_size = atoi(value); 61 ctx.cfg.cache_size = atoi(value);
59 else if (!strcmp(name, "cache-root")) 62 else if (!strcmp(name, "cache-root"))
60 ctx.cfg.cache_root = xstrdup(value); 63 ctx.cfg.cache_root = xstrdup(value);
61 else if (!strcmp(name, "cache-root-ttl")) 64 else if (!strcmp(name, "cache-root-ttl"))
62 ctx.cfg.cache_root_ttl = atoi(value); 65 ctx.cfg.cache_root_ttl = atoi(value);
@@ -109,12 +112,14 @@ void config_cb(const char *name, const char *value)
109 else if (ctx.repo && !strcmp(name, "repo.snapshots")) 112 else if (ctx.repo && !strcmp(name, "repo.snapshots"))
110 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */ 113 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
111 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount")) 114 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
112 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); 115 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
113 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) 116 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
114 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); 117 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
118 else if (ctx.repo && !strcmp(name, "repo.max-stats"))
119 ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
115 else if (ctx.repo && !strcmp(name, "repo.module-link")) 120 else if (ctx.repo && !strcmp(name, "repo.module-link"))
116 ctx.repo->module_link= xstrdup(value); 121 ctx.repo->module_link= xstrdup(value);
117 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { 122 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
118 if (*value == '/') 123 if (*value == '/')
119 ctx.repo->readme = xstrdup(value); 124 ctx.repo->readme = xstrdup(value);
120 else 125 else
@@ -155,12 +160,14 @@ static void querystring_cb(const char *name, const char *value)
155 } else if (!strcmp(name, "mimetype")) { 160 } else if (!strcmp(name, "mimetype")) {
156 ctx.qry.mimetype = xstrdup(value); 161 ctx.qry.mimetype = xstrdup(value);
157 } else if (!strcmp(name, "s")){ 162 } else if (!strcmp(name, "s")){
158 ctx.qry.sort = xstrdup(value); 163 ctx.qry.sort = xstrdup(value);
159 } else if (!strcmp(name, "showmsg")) { 164 } else if (!strcmp(name, "showmsg")) {
160 ctx.qry.showmsg = atoi(value); 165 ctx.qry.showmsg = atoi(value);
166 } else if (!strcmp(name, "period")) {
167 ctx.qry.period = xstrdup(value);
161 } 168 }
162} 169}
163 170
164static void prepare_context(struct cgit_context *ctx) 171static void prepare_context(struct cgit_context *ctx)
165{ 172{
166 memset(ctx, 0, sizeof(ctx)); 173 memset(ctx, 0, sizeof(ctx));
@@ -178,12 +185,13 @@ static void prepare_context(struct cgit_context *ctx)
178 ctx->cfg.local_time = 0; 185 ctx->cfg.local_time = 0;
179 ctx->cfg.max_repo_count = 50; 186 ctx->cfg.max_repo_count = 50;
180 ctx->cfg.max_commit_count = 50; 187 ctx->cfg.max_commit_count = 50;
181 ctx->cfg.max_lock_attempts = 5; 188 ctx->cfg.max_lock_attempts = 5;
182 ctx->cfg.max_msg_len = 80; 189 ctx->cfg.max_msg_len = 80;
183 ctx->cfg.max_repodesc_len = 80; 190 ctx->cfg.max_repodesc_len = 80;
191 ctx->cfg.max_stats = 0;
184 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 192 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
185 ctx->cfg.renamelimit = -1; 193 ctx->cfg.renamelimit = -1;
186 ctx->cfg.robots = "index, nofollow"; 194 ctx->cfg.robots = "index, nofollow";
187 ctx->cfg.root_title = "Git repository browser"; 195 ctx->cfg.root_title = "Git repository browser";
188 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 196 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
189 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 197 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
diff --git a/cgit.css b/cgit.css
index f19446d..e8214de 100644
--- a/cgit.css
+++ b/cgit.css
@@ -492,6 +492,82 @@ a.remote-deco {
492a.deco { 492a.deco {
493 margin: 0px 0.5em; 493 margin: 0px 0.5em;
494 padding: 0px 0.25em; 494 padding: 0px 0.25em;
495 background-color: #ff8888; 495 background-color: #ff8888;
496 border: solid 1px #770000; 496 border: solid 1px #770000;
497} 497}
498table.stats {
499 border: solid 1px black;
500 border-collapse: collapse;
501}
502
503table.stats th {
504 text-align: left;
505 padding: 1px 0.5em;
506 background-color: #eee;
507 border: solid 1px black;
508}
509
510table.stats td {
511 text-align: right;
512 padding: 1px 0.5em;
513 border: solid 1px black;
514}
515
516table.stats td.total {
517 font-weight: bold;
518 text-align: left;
519}
520
521table.stats td.sum {
522 color: #c00;
523 font-weight: bold;
524 /*background-color: #eee; */
525}
526
527table.stats td.left {
528 text-align: left;
529}
530
531table.vgraph {
532 border-collapse: separate;
533 border: solid 1px black;
534 height: 200px;
535}
536
537table.vgraph th {
538 background-color: #eee;
539 font-weight: bold;
540 border: solid 1px white;
541 padding: 1px 0.5em;
542}
543
544table.vgraph td {
545 vertical-align: bottom;
546 padding: 0px 10px;
547}
548
549table.vgraph div.bar {
550 background-color: #eee;
551}
552
553table.hgraph {
554 border: solid 1px black;
555 width: 800px;
556}
557
558table.hgraph th {
559 background-color: #eee;
560 font-weight: bold;
561 border: solid 1px black;
562 padding: 1px 0.5em;
563}
564
565table.hgraph td {
566 vertical-align: center;
567 padding: 2px 2px;
568}
569
570table.hgraph div.bar {
571 background-color: #eee;
572 height: 1em;
573}
diff --git a/cgit.h b/cgit.h
index cb2f176..4fe94c6 100644
--- a/cgit.h
+++ b/cgit.h
@@ -58,12 +58,13 @@ struct cgit_repo {
58 char *module_link; 58 char *module_link;
59 char *readme; 59 char *readme;
60 char *clone_url; 60 char *clone_url;
61 int snapshots; 61 int snapshots;
62 int enable_log_filecount; 62 int enable_log_filecount;
63 int enable_log_linecount; 63 int enable_log_linecount;
64 int max_stats;
64 time_t mtime; 65 time_t mtime;
65}; 66};
66 67
67struct cgit_repolist { 68struct cgit_repolist {
68 int length; 69 int length;
69 int count; 70 int count;
@@ -117,12 +118,13 @@ struct cgit_query {
117 char *sha1; 118 char *sha1;
118 char *sha2; 119 char *sha2;
119 char *path; 120 char *path;
120 char *name; 121 char *name;
121 char *mimetype; 122 char *mimetype;
122 char *url; 123 char *url;
124 char *period;
123 int ofs; 125 int ofs;
124 int nohead; 126 int nohead;
125 char *sort; 127 char *sort;
126 int showmsg; 128 int showmsg;
127}; 129};
128 130
@@ -157,12 +159,13 @@ struct cgit_config {
157 int local_time; 159 int local_time;
158 int max_repo_count; 160 int max_repo_count;
159 int max_commit_count; 161 int max_commit_count;
160 int max_lock_attempts; 162 int max_lock_attempts;
161 int max_msg_len; 163 int max_msg_len;
162 int max_repodesc_len; 164 int max_repodesc_len;
165 int max_stats;
163 int nocache; 166 int nocache;
164 int renamelimit; 167 int renamelimit;
165 int snapshots; 168 int snapshots;
166 int summary_branches; 169 int summary_branches;
167 int summary_log; 170 int summary_log;
168 int summary_tags; 171 int summary_tags;
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index ab9ab66..09f56a6 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -126,12 +126,17 @@ max-repo-count
126 index page. Default value: "50". 126 index page. Default value: "50".
127 127
128max-repodesc-length 128max-repodesc-length
129 Specifies the maximum number of repo description characters to display 129 Specifies the maximum number of repo description characters to display
130 on the repository index page. Default value: "80". 130 on the repository index page. Default value: "80".
131 131
132max-stats
133 Set the default maximum statistics period. Valid values are "week",
134 "month", "quarter" and "year". If unspecified, statistics are
135 disabled. Default value: none. See also: "repo.max-stats".
136
132module-link 137module-link
133 Text which will be used as the formatstring for a hyperlink when a 138 Text which will be used as the formatstring for a hyperlink when a
134 submodule is printed in a directory listing. The arguments for the 139 submodule is printed in a directory listing. The arguments for the
135 formatstring are the path and SHA1 of the submodule commit. Default 140 formatstring are the path and SHA1 of the submodule commit. Default
136 value: "./?repo=%s&page=commit&id=%s" 141 value: "./?repo=%s&page=commit&id=%s"
137 142
@@ -215,12 +220,17 @@ repo.enable-log-filecount
215 `enable-log-filecount'. Default value: none. 220 `enable-log-filecount'. Default value: none.
216 221
217repo.enable-log-linecount 222repo.enable-log-linecount
218 A flag which can be used to disable the global setting 223 A flag which can be used to disable the global setting
219 `enable-log-linecount'. Default value: none. 224 `enable-log-linecount'. Default value: none.
220 225
226repo.max-stats
227 Override the default maximum statistics period. Valid values are equal
228 to the values specified for the global "max-stats" setting. Default
229 value: none.
230
221repo.name 231repo.name
222 The value to show as repository name. Default value: <repo.url>. 232 The value to show as repository name. Default value: <repo.url>.
223 233
224repo.owner 234repo.owner
225 A value used to identify the owner of the repository. Default value: 235 A value used to identify the owner of the repository. Default value:
226 none. 236 none.
@@ -273,12 +283,16 @@ favicon=/favicon.ico
273 283
274 284
275# Use a custom logo 285# Use a custom logo
276logo=/img/mylogo.png 286logo=/img/mylogo.png
277 287
278 288
289# Enable statistics per week, month and quarter
290max-stats=quarter
291
292
279# Set the title and heading of the repository index page 293# Set the title and heading of the repository index page
280root-title=foobar.com git repositories 294root-title=foobar.com git repositories
281 295
282 296
283# Set a subheading for the repository index page 297# Set a subheading for the repository index page
284root-desc=tracking the foobar development 298root-desc=tracking the foobar development
@@ -345,12 +359,15 @@ repo.desc=the kernel
345# Disable adhoc downloads of this repo 359# Disable adhoc downloads of this repo
346repo.snapshots=0 360repo.snapshots=0
347 361
348# Disable line-counts for this repo 362# Disable line-counts for this repo
349repo.enable-log-linecount=0 363repo.enable-log-linecount=0
350 364
365# Restrict the max statistics period for this repo
366repo.max-stats=month
367
351 368
352BUGS 369BUGS
353---- 370----
354Comments currently cannot appear on the same line as a setting; the comment 371Comments currently cannot appear on the same line as a setting; the comment
355will be included as part of the value. E.g. this line: 372will be included as part of the value. E.g. this line:
356 373
diff --git a/cmd.c b/cmd.c
index 8914fa5..cf97da7 100644
--- a/cmd.c
+++ b/cmd.c
@@ -18,12 +18,13 @@
18#include "ui-log.h" 18#include "ui-log.h"
19#include "ui-patch.h" 19#include "ui-patch.h"
20#include "ui-plain.h" 20#include "ui-plain.h"
21#include "ui-refs.h" 21#include "ui-refs.h"
22#include "ui-repolist.h" 22#include "ui-repolist.h"
23#include "ui-snapshot.h" 23#include "ui-snapshot.h"
24#include "ui-stats.h"
24#include "ui-summary.h" 25#include "ui-summary.h"
25#include "ui-tag.h" 26#include "ui-tag.h"
26#include "ui-tree.h" 27#include "ui-tree.h"
27 28
28static void HEAD_fn(struct cgit_context *ctx) 29static void HEAD_fn(struct cgit_context *ctx)
29{ 30{
@@ -105,12 +106,17 @@ static void refs_fn(struct cgit_context *ctx)
105static void snapshot_fn(struct cgit_context *ctx) 106static void snapshot_fn(struct cgit_context *ctx)
106{ 107{
107 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, ctx->qry.path, 108 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, ctx->qry.path,
108 ctx->repo->snapshots, ctx->qry.nohead); 109 ctx->repo->snapshots, ctx->qry.nohead);
109} 110}
110 111
112static void stats_fn(struct cgit_context *ctx)
113{
114 cgit_show_stats(ctx);
115}
116
111static void summary_fn(struct cgit_context *ctx) 117static void summary_fn(struct cgit_context *ctx)
112{ 118{
113 cgit_print_summary(); 119 cgit_print_summary();
114} 120}
115 121
116static void tag_fn(struct cgit_context *ctx) 122static void tag_fn(struct cgit_context *ctx)
@@ -141,12 +147,13 @@ struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
141 def_cmd(objects, 1, 0), 147 def_cmd(objects, 1, 0),
142 def_cmd(patch, 1, 0), 148 def_cmd(patch, 1, 0),
143 def_cmd(plain, 1, 0), 149 def_cmd(plain, 1, 0),
144 def_cmd(refs, 1, 1), 150 def_cmd(refs, 1, 1),
145 def_cmd(repolist, 0, 0), 151 def_cmd(repolist, 0, 0),
146 def_cmd(snapshot, 1, 0), 152 def_cmd(snapshot, 1, 0),
153 def_cmd(stats, 1, 1),
147 def_cmd(summary, 1, 1), 154 def_cmd(summary, 1, 1),
148 def_cmd(tag, 1, 1), 155 def_cmd(tag, 1, 1),
149 def_cmd(tree, 1, 1), 156 def_cmd(tree, 1, 1),
150 }; 157 };
151 int i; 158 int i;
152 159
diff --git a/shared.c b/shared.c
index a764c4d..578a544 100644
--- a/shared.c
+++ b/shared.c
@@ -55,12 +55,13 @@ struct cgit_repo *cgit_add_repo(const char *url)
55 ret->owner = NULL; 55 ret->owner = NULL;
56 ret->group = ctx.cfg.repo_group; 56 ret->group = ctx.cfg.repo_group;
57 ret->defbranch = "master"; 57 ret->defbranch = "master";
58 ret->snapshots = ctx.cfg.snapshots; 58 ret->snapshots = ctx.cfg.snapshots;
59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
61 ret->max_stats = ctx.cfg.max_stats;
61 ret->module_link = ctx.cfg.module_link; 62 ret->module_link = ctx.cfg.module_link;
62 ret->readme = NULL; 63 ret->readme = NULL;
63 ret->mtime = -1; 64 ret->mtime = -1;
64 return ret; 65 return ret;
65} 66}
66 67
diff --git a/ui-shared.c b/ui-shared.c
index fba1ba6..4f28512 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -366,12 +366,18 @@ void cgit_diff_link(char *name, char *title, char *class, char *head,
366void cgit_patch_link(char *name, char *title, char *class, char *head, 366void cgit_patch_link(char *name, char *title, char *class, char *head,
367 char *rev) 367 char *rev)
368{ 368{
369 reporevlink("patch", name, title, class, head, rev, NULL); 369 reporevlink("patch", name, title, class, head, rev, NULL);
370} 370}
371 371
372void cgit_stats_link(char *name, char *title, char *class, char *head,
373 char *path)
374{
375 reporevlink("stats", name, title, class, head, NULL, path);
376}
377
372void cgit_object_link(struct object *obj) 378void cgit_object_link(struct object *obj)
373{ 379{
374 char *page, *shortrev, *fullrev, *name; 380 char *page, *shortrev, *fullrev, *name;
375 381
376 fullrev = sha1_to_hex(obj->sha1); 382 fullrev = sha1_to_hex(obj->sha1);
377 shortrev = xstrdup(fullrev); 383 shortrev = xstrdup(fullrev);
@@ -554,13 +560,13 @@ int print_archive_ref(const char *refname, const unsigned char *sha1,
554 html_link_open(url, NULL, "menu"); 560 html_link_open(url, NULL, "menu");
555 html_txt(strlpart(buf, 20)); 561 html_txt(strlpart(buf, 20));
556 html_link_close(); 562 html_link_close();
557 return 0; 563 return 0;
558} 564}
559 565
560void add_hidden_formfields(int incl_head, int incl_search, char *page) 566void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page)
561{ 567{
562 char *url; 568 char *url;
563 569
564 if (!ctx.cfg.virtual_root) { 570 if (!ctx.cfg.virtual_root) {
565 url = fmt("%s/%s", ctx.qry.repo, page); 571 url = fmt("%s/%s", ctx.qry.repo, page);
566 if (ctx.qry.path) 572 if (ctx.qry.path)
@@ -616,13 +622,13 @@ void cgit_print_pageheader(struct cgit_context *ctx)
616 if (ctx->repo) { 622 if (ctx->repo) {
617 cgit_index_link("index", NULL, NULL, NULL, 0); 623 cgit_index_link("index", NULL, NULL, NULL, 0);
618 html(" : "); 624 html(" : ");
619 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 625 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
620 html("</td><td class='form'>"); 626 html("</td><td class='form'>");
621 html("<form method='get' action=''>\n"); 627 html("<form method='get' action=''>\n");
622 add_hidden_formfields(0, 1, ctx->qry.page); 628 cgit_add_hidden_formfields(0, 1, ctx->qry.page);
623 html("<select name='h' onchange='this.form.submit();'>\n"); 629 html("<select name='h' onchange='this.form.submit();'>\n");
624 for_each_branch_ref(print_branch_option, ctx->qry.head); 630 for_each_branch_ref(print_branch_option, ctx->qry.head);
625 html("</select> "); 631 html("</select> ");
626 html("<input type='submit' name='' value='switch'/>"); 632 html("<input type='submit' name='' value='switch'/>");
627 html("</form>"); 633 html("</form>");
628 } else 634 } else
@@ -653,23 +659,26 @@ void cgit_print_pageheader(struct cgit_context *ctx)
653 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 659 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
654 ctx->qry.sha1, NULL); 660 ctx->qry.sha1, NULL);
655 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 661 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
656 ctx->qry.head, ctx->qry.sha1); 662 ctx->qry.head, ctx->qry.sha1);
657 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 663 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
658 ctx->qry.sha1, ctx->qry.sha2, NULL); 664 ctx->qry.sha1, ctx->qry.sha2, NULL);
665 if (ctx->repo->max_stats)
666 cgit_stats_link("stats", NULL, hc(cmd, "stats"),
667 ctx->qry.head, NULL);
659 if (ctx->repo->readme) 668 if (ctx->repo->readme)
660 reporevlink("about", "about", NULL, 669 reporevlink("about", "about", NULL,
661 hc(cmd, "about"), ctx->qry.head, NULL, 670 hc(cmd, "about"), ctx->qry.head, NULL,
662 NULL); 671 NULL);
663 html("</td><td class='form'>"); 672 html("</td><td class='form'>");
664 html("<form class='right' method='get' action='"); 673 html("<form class='right' method='get' action='");
665 if (ctx->cfg.virtual_root) 674 if (ctx->cfg.virtual_root)
666 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 675 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
667 ctx->qry.path, NULL)); 676 ctx->qry.path, NULL));
668 html("'>\n"); 677 html("'>\n");
669 add_hidden_formfields(1, 0, "log"); 678 cgit_add_hidden_formfields(1, 0, "log");
670 html("<select name='qt'>\n"); 679 html("<select name='qt'>\n");
671 html_option("grep", "log msg", ctx->qry.grep); 680 html_option("grep", "log msg", ctx->qry.grep);
672 html_option("author", "author", ctx->qry.grep); 681 html_option("author", "author", ctx->qry.grep);
673 html_option("committer", "committer", ctx->qry.grep); 682 html_option("committer", "committer", ctx->qry.grep);
674 html("</select>\n"); 683 html("</select>\n");
675 html("<input class='txt' type='text' size='10' name='q' value='"); 684 html("<input class='txt' type='text' size='10' name='q' value='");
diff --git a/ui-shared.h b/ui-shared.h
index 2ab53ae..5a3821f 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -27,20 +27,23 @@ extern void cgit_patch_link(char *name, char *title, char *class, char *head,
27extern void cgit_refs_link(char *name, char *title, char *class, char *head, 27extern void cgit_refs_link(char *name, char *title, char *class, char *head,
28 char *rev, char *path); 28 char *rev, char *path);
29extern void cgit_snapshot_link(char *name, char *title, char *class, 29extern void cgit_snapshot_link(char *name, char *title, char *class,
30 char *head, char *rev, char *archivename); 30 char *head, char *rev, char *archivename);
31extern void cgit_diff_link(char *name, char *title, char *class, char *head, 31extern void cgit_diff_link(char *name, char *title, char *class, char *head,
32 char *new_rev, char *old_rev, char *path); 32 char *new_rev, char *old_rev, char *path);
33extern void cgit_stats_link(char *name, char *title, char *class, char *head,
34 char *path);
33extern void cgit_object_link(struct object *obj); 35extern void cgit_object_link(struct object *obj);
34 36
35extern void cgit_print_error(char *msg); 37extern void cgit_print_error(char *msg);
36extern void cgit_print_date(time_t secs, char *format, int local_time); 38extern void cgit_print_date(time_t secs, char *format, int local_time);
37extern void cgit_print_age(time_t t, time_t max_relative, char *format); 39extern void cgit_print_age(time_t t, time_t max_relative, char *format);
38extern void cgit_print_http_headers(struct cgit_context *ctx); 40extern void cgit_print_http_headers(struct cgit_context *ctx);
39extern void cgit_print_docstart(struct cgit_context *ctx); 41extern void cgit_print_docstart(struct cgit_context *ctx);
40extern void cgit_print_docend(); 42extern void cgit_print_docend();
41extern void cgit_print_pageheader(struct cgit_context *ctx); 43extern void cgit_print_pageheader(struct cgit_context *ctx);
42extern void cgit_print_filemode(unsigned short mode); 44extern void cgit_print_filemode(unsigned short mode);
43extern void cgit_print_snapshot_links(const char *repo, const char *head, 45extern void cgit_print_snapshot_links(const char *repo, const char *head,
44 const char *hex, int snapshots); 46 const char *hex, int snapshots);
45 47extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
48 char *page);
46#endif /* UI_SHARED_H */ 49#endif /* UI_SHARED_H */
diff --git a/ui-stats.c b/ui-stats.c
new file mode 100644
index 0000000..9fc06d3
--- a/dev/null
+++ b/ui-stats.c
@@ -0,0 +1,410 @@
1#include <string-list.h>
2
3#include "cgit.h"
4#include "html.h"
5#include "ui-shared.h"
6#include "ui-stats.h"
7
8#define MONTHS 6
9
10struct authorstat {
11 long total;
12 struct string_list list;
13};
14
15#define DAY_SECS (60 * 60 * 24)
16#define WEEK_SECS (DAY_SECS * 7)
17
18static void trunc_week(struct tm *tm)
19{
20 time_t t = timegm(tm);
21 t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
22 gmtime_r(&t, tm);
23}
24
25static void dec_week(struct tm *tm)
26{
27 time_t t = timegm(tm);
28 t -= WEEK_SECS;
29 gmtime_r(&t, tm);
30}
31
32static void inc_week(struct tm *tm)
33{
34 time_t t = timegm(tm);
35 t += WEEK_SECS;
36 gmtime_r(&t, tm);
37}
38
39static char *pretty_week(struct tm *tm)
40{
41 static char buf[10];
42
43 strftime(buf, sizeof(buf), "W%V %G", tm);
44 return buf;
45}
46
47static void trunc_month(struct tm *tm)
48{
49 tm->tm_mday = 1;
50}
51
52static void dec_month(struct tm *tm)
53{
54 tm->tm_mon--;
55 if (tm->tm_mon < 0) {
56 tm->tm_year--;
57 tm->tm_mon = 11;
58 }
59}
60
61static void inc_month(struct tm *tm)
62{
63 tm->tm_mon++;
64 if (tm->tm_mon > 11) {
65 tm->tm_year++;
66 tm->tm_mon = 0;
67 }
68}
69
70static char *pretty_month(struct tm *tm)
71{
72 static const char *months[] = {
73 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
74 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
75 };
76 return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
77}
78
79static void trunc_quarter(struct tm *tm)
80{
81 trunc_month(tm);
82 while(tm->tm_mon % 3 != 0)
83 dec_month(tm);
84}
85
86static void dec_quarter(struct tm *tm)
87{
88 dec_month(tm);
89 dec_month(tm);
90 dec_month(tm);
91}
92
93static void inc_quarter(struct tm *tm)
94{
95 inc_month(tm);
96 inc_month(tm);
97 inc_month(tm);
98}
99
100static char *pretty_quarter(struct tm *tm)
101{
102 return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
103}
104
105static void trunc_year(struct tm *tm)
106{
107 trunc_month(tm);
108 tm->tm_mon = 0;
109}
110
111static void dec_year(struct tm *tm)
112{
113 tm->tm_year--;
114}
115
116static void inc_year(struct tm *tm)
117{
118 tm->tm_year++;
119}
120
121static char *pretty_year(struct tm *tm)
122{
123 return fmt("%d", tm->tm_year + 1900);
124}
125
126struct cgit_period periods[] = {
127 {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
128 {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
129 {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
130 {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
131};
132
133/* Given a period code or name, return a period index (1, 2, 3 or 4)
134 * and update the period pointer to the correcsponding struct.
135 * If no matching code is found, return 0.
136 */
137int cgit_find_stats_period(const char *expr, struct cgit_period **period)
138{
139 int i;
140 char code = '\0';
141
142 if (!expr)
143 return 0;
144
145 if (strlen(expr) == 1)
146 code = expr[0];
147
148 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
149 if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
150 if (period)
151 *period = &periods[i];
152 return i+1;
153 }
154 return 0;
155}
156
157static void add_commit(struct string_list *authors, struct commit *commit,
158 struct cgit_period *period)
159{
160 struct commitinfo *info;
161 struct string_list_item *author, *item;
162 struct authorstat *authorstat;
163 struct string_list *items;
164 char *tmp;
165 struct tm *date;
166 time_t t;
167
168 info = cgit_parse_commit(commit);
169 tmp = xstrdup(info->author);
170 author = string_list_insert(tmp, authors);
171 if (!author->util)
172 author->util = xcalloc(1, sizeof(struct authorstat));
173 else
174 free(tmp);
175 authorstat = author->util;
176 items = &authorstat->list;
177 t = info->committer_date;
178 date = gmtime(&t);
179 period->trunc(date);
180 tmp = xstrdup(period->pretty(date));
181 item = string_list_insert(tmp, items);
182 if (item->util)
183 free(tmp);
184 item->util++;
185 authorstat->total++;
186 cgit_free_commitinfo(info);
187}
188
189static int cmp_total_commits(const void *a1, const void *a2)
190{
191 const struct string_list_item *i1 = a1;
192 const struct string_list_item *i2 = a2;
193 const struct authorstat *auth1 = i1->util;
194 const struct authorstat *auth2 = i2->util;
195
196 return auth2->total - auth1->total;
197}
198
199/* Walk the commit DAG and collect number of commits per author per
200 * timeperiod into a nested string_list collection.
201 */
202struct string_list collect_stats(struct cgit_context *ctx,
203 struct cgit_period *period)
204{
205 struct string_list authors;
206 struct rev_info rev;
207 struct commit *commit;
208 const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
209 int argc = 3;
210 time_t now;
211 long i;
212 struct tm *tm;
213 char tmp[11];
214
215 time(&now);
216 tm = gmtime(&now);
217 period->trunc(tm);
218 for (i = 1; i < period->count; i++)
219 period->dec(tm);
220 strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
221 argv[2] = xstrdup(fmt("--since=%s", tmp));
222 if (ctx->qry.path) {
223 argv[3] = "--";
224 argv[4] = ctx->qry.path;
225 argc += 2;
226 }
227 init_revisions(&rev, NULL);
228 rev.abbrev = DEFAULT_ABBREV;
229 rev.commit_format = CMIT_FMT_DEFAULT;
230 rev.no_merges = 1;
231 rev.verbose_header = 1;
232 rev.show_root_diff = 0;
233 setup_revisions(argc, argv, &rev, NULL);
234 prepare_revision_walk(&rev);
235 memset(&authors, 0, sizeof(authors));
236 while ((commit = get_revision(&rev)) != NULL) {
237 add_commit(&authors, commit, period);
238 free(commit->buffer);
239 free_commit_list(commit->parents);
240 }
241 return authors;
242}
243
244void print_combined_authorrow(struct string_list *authors, int from, int to,
245 const char *name, const char *leftclass, const char *centerclass,
246 const char *rightclass, struct cgit_period *period)
247{
248 struct string_list_item *author;
249 struct authorstat *authorstat;
250 struct string_list *items;
251 struct string_list_item *date;
252 time_t now;
253 long i, j, total, subtotal;
254 struct tm *tm;
255 char *tmp;
256
257 time(&now);
258 tm = gmtime(&now);
259 period->trunc(tm);
260 for (i = 1; i < period->count; i++)
261 period->dec(tm);
262
263 total = 0;
264 htmlf("<tr><td class='%s'>%s</td>", leftclass,
265 fmt(name, to - from + 1));
266 for (j = 0; j < period->count; j++) {
267 tmp = period->pretty(tm);
268 period->inc(tm);
269 subtotal = 0;
270 for (i = from; i <= to; i++) {
271 author = &authors->items[i];
272 authorstat = author->util;
273 items = &authorstat->list;
274 date = string_list_lookup(tmp, items);
275 if (date)
276 subtotal += (size_t)date->util;
277 }
278 htmlf("<td class='%s'>%d</td>", centerclass, subtotal);
279 total += subtotal;
280 }
281 htmlf("<td class='%s'>%d</td></tr>", rightclass, total);
282}
283
284void print_authors(struct string_list *authors, int top,
285 struct cgit_period *period)
286{
287 struct string_list_item *author;
288 struct authorstat *authorstat;
289 struct string_list *items;
290 struct string_list_item *date;
291 time_t now;
292 long i, j, total;
293 struct tm *tm;
294 char *tmp;
295
296 time(&now);
297 tm = gmtime(&now);
298 period->trunc(tm);
299 for (i = 1; i < period->count; i++)
300 period->dec(tm);
301
302 html("<table class='stats'><tr><th>Author</th>");
303 for (j = 0; j < period->count; j++) {
304 tmp = period->pretty(tm);
305 htmlf("<th>%s</th>", tmp);
306 period->inc(tm);
307 }
308 html("<th>Total</th></tr>\n");
309
310 if (top <= 0 || top > authors->nr)
311 top = authors->nr;
312
313 for (i = 0; i < top; i++) {
314 author = &authors->items[i];
315 html("<tr><td class='left'>");
316 html_txt(author->string);
317 html("</td>");
318 authorstat = author->util;
319 items = &authorstat->list;
320 total = 0;
321 for (j = 0; j < period->count; j++)
322 period->dec(tm);
323 for (j = 0; j < period->count; j++) {
324 tmp = period->pretty(tm);
325 period->inc(tm);
326 date = string_list_lookup(tmp, items);
327 if (!date)
328 html("<td>0</td>");
329 else {
330 htmlf("<td>%d</td>", date->util);
331 total += (size_t)date->util;
332 }
333 }
334 htmlf("<td class='sum'>%d</td></tr>", total);
335 }
336
337 if (top < authors->nr)
338 print_combined_authorrow(authors, top, authors->nr - 1,
339 "Others (%d)", "left", "", "sum", period);
340
341 print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
342 "total", "sum", "sum", period);
343 html("</table>");
344}
345
346/* Create a sorted string_list with one entry per author. The util-field
347 * for each author is another string_list which is used to calculate the
348 * number of commits per time-interval.
349 */
350void cgit_show_stats(struct cgit_context *ctx)
351{
352 struct string_list authors;
353 struct cgit_period *period;
354 int top, i;
355 const char *code = "w";
356
357 if (ctx->qry.period)
358 code = ctx->qry.period;
359
360 i = cgit_find_stats_period(code, &period);
361 if (!i) {
362 cgit_print_error(fmt("Unknown statistics type: %c", code));
363 return;
364 }
365 if (i > ctx->repo->max_stats) {
366 cgit_print_error(fmt("Statistics type disabled: %s",
367 period->name));
368 return;
369 }
370 authors = collect_stats(ctx, period);
371 qsort(authors.items, authors.nr, sizeof(struct string_list_item),
372 cmp_total_commits);
373
374 top = ctx->qry.ofs;
375 if (!top)
376 top = 10;
377 htmlf("<h2>Commits per author per %s", period->name);
378 if (ctx->qry.path) {
379 html(" (path '");
380 html_txt(ctx->qry.path);
381 html("')");
382 }
383 html("</h2>");
384
385 html("<form method='get' action='' style='float: right; text-align: right;'>");
386 cgit_add_hidden_formfields(1, 0, "stats");
387 if (ctx->repo->max_stats > 1) {
388 html("Period: ");
389 html("<select name='period' onchange='this.form.submit();'>");
390 for (i = 0; i < ctx->repo->max_stats; i++)
391 htmlf("<option value='%c'%s>%s</option>",
392 periods[i].code,
393 period == &periods[i] ? " selected" : "",
394 periods[i].name);
395 html("</select><br/><br/>");
396 }
397 html("Authors: ");
398 html("");
399 html("<select name='ofs' onchange='this.form.submit();'>");
400 htmlf("<option value='10'%s>10</option>", top == 10 ? " selected" : "");
401 htmlf("<option value='25'%s>25</option>", top == 25 ? " selected" : "");
402 htmlf("<option value='50'%s>50</option>", top == 50 ? " selected" : "");
403 htmlf("<option value='100'%s>100</option>", top == 100 ? " selected" : "");
404 htmlf("<option value='-1'%s>All</option>", top == -1 ? " selected" : "");
405 html("</select>");
406 html("<noscript>&nbsp;&nbsp;<input type='submit' value='Reload'/></noscript>");
407 html("</form>");
408 print_authors(&authors, top, period);
409}
410
diff --git a/ui-stats.h b/ui-stats.h
new file mode 100644
index 0000000..4f13dba
--- a/dev/null
+++ b/ui-stats.h
@@ -0,0 +1,27 @@
1#ifndef UI_STATS_H
2#define UI_STATS_H
3
4#include "cgit.h"
5
6struct cgit_period {
7 const char code;
8 const char *name;
9 int max_periods;
10 int count;
11
12 /* Convert a tm value to the first day in the period */
13 void (*trunc)(struct tm *tm);
14
15 /* Update tm value to start of next/previous period */
16 void (*dec)(struct tm *tm);
17 void (*inc)(struct tm *tm);
18
19 /* Pretty-print a tm value */
20 char *(*pretty)(struct tm *tm);
21};
22
23extern int cgit_find_stats_period(const char *expr, struct cgit_period **period);
24
25extern void cgit_show_stats(struct cgit_context *ctx);
26
27#endif /* UI_STATS_H */
diff --git a/ui-tree.c b/ui-tree.c
index 9876c99..4b8e7a0 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -106,12 +106,15 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
106 } 106 }
107 htmlf("</td><td class='ls-size'>%li</td>", size); 107 htmlf("</td><td class='ls-size'>%li</td>", size);
108 108
109 html("<td>"); 109 html("<td>");
110 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, 110 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev,
111 fullpath, 0, NULL, NULL, ctx.qry.showmsg); 111 fullpath, 0, NULL, NULL, ctx.qry.showmsg);
112 if (ctx.repo->max_stats)
113 cgit_stats_link("stats", NULL, "button", ctx.qry.head,
114 fullpath);
112 html("</td></tr>\n"); 115 html("</td></tr>\n");
113 free(name); 116 free(name);
114 return 0; 117 return 0;
115} 118}
116 119
117static void ls_head() 120static void ls_head()