summaryrefslogtreecommitdiffabout
authorLars Hjemli <hjemli@gmail.com>2009-01-27 19:16:37 (UTC)
committer Lars Hjemli <hjemli@gmail.com>2009-01-27 19:16:37 (UTC)
commit7710178e45dee61e85ea77c4221309ce8c086f95 (patch) (unidiff)
tree281c5251777308f18c05d323183b28470445f4bc
parente78186dcb63ec67a38dddfcd8f91d2108583320b (diff)
parentb54ef9749c083afd86573112fad3b3ed8ee2d0e4 (diff)
downloadcgit-7710178e45dee61e85ea77c4221309ce8c086f95.zip
cgit-7710178e45dee61e85ea77c4221309ce8c086f95.tar.gz
cgit-7710178e45dee61e85ea77c4221309ce8c086f95.tar.bz2
Merge branch 'lh/stats'
Conflicts: cgit.c cgit.css cgit.h ui-tree.c Signed-off-by: Lars Hjemli <hjemli@gmail.com>
Diffstat (more/less context) (show 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
@@ -85,16 +85,17 @@ OBJECTS += ui-commit.o
85OBJECTS += ui-diff.o 85OBJECTS += ui-diff.o
86OBJECTS += ui-log.o 86OBJECTS += 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
99endif 100endif
100 101
diff --git a/cgit.c b/cgit.c
index f35f605..608cab6 100644
--- a/cgit.c
+++ b/cgit.c
@@ -7,16 +7,17 @@
7 */ 7 */
8 8
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{
21 if (!strcmp(name, "root-title")) 22 if (!strcmp(name, "root-title"))
22 ctx.cfg.root_title = xstrdup(value); 23 ctx.cfg.root_title = xstrdup(value);
@@ -49,16 +50,18 @@ void config_cb(const char *name, const char *value)
49 else if (!strcmp(name, "snapshots")) 50 else if (!strcmp(name, "snapshots"))
50 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 51 ctx.cfg.snapshots = cgit_parse_snapshots_mask(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);
63 else if (!strcmp(name, "cache-repo-ttl")) 66 else if (!strcmp(name, "cache-repo-ttl"))
64 ctx.cfg.cache_repo_ttl = atoi(value); 67 ctx.cfg.cache_repo_ttl = atoi(value);
@@ -107,16 +110,18 @@ void config_cb(const char *name, const char *value)
107 else if (ctx.repo && !strcmp(name, "repo.defbranch")) 110 else if (ctx.repo && !strcmp(name, "repo.defbranch"))
108 ctx.repo->defbranch = xstrdup(value); 111 ctx.repo->defbranch = xstrdup(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
121 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value)); 126 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
122 } else if (!strcmp(name, "include")) 127 } else if (!strcmp(name, "include"))
@@ -153,16 +158,18 @@ static void querystring_cb(const char *name, const char *value)
153 } else if (!strcmp(name, "name")) { 158 } else if (!strcmp(name, "name")) {
154 ctx.qry.name = xstrdup(value); 159 ctx.qry.name = xstrdup(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));
167 ctx->cfg.agefile = "info/web/last-modified"; 174 ctx->cfg.agefile = "info/web/last-modified";
168 ctx->cfg.nocache = 0; 175 ctx->cfg.nocache = 0;
@@ -176,16 +183,17 @@ static void prepare_context(struct cgit_context *ctx)
176 ctx->cfg.css = "/cgit.css"; 183 ctx->cfg.css = "/cgit.css";
177 ctx->cfg.logo = "/git-logo.png"; 184 ctx->cfg.logo = "/git-logo.png";
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;
190 ctx->cfg.summary_branches = 10; 198 ctx->cfg.summary_branches = 10;
191 ctx->cfg.summary_log = 10; 199 ctx->cfg.summary_log = 10;
diff --git a/cgit.css b/cgit.css
index f19446d..e8214de 100644
--- a/cgit.css
+++ b/cgit.css
@@ -490,8 +490,84 @@ a.remote-deco {
490 border: solid 1px #000077; 490 border: solid 1px #000077;
491} 491}
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
@@ -56,16 +56,17 @@ struct cgit_repo {
56 char *defbranch; 56 char *defbranch;
57 char *group; 57 char *group;
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;
70 struct cgit_repo *repos; 71 struct cgit_repo *repos;
71}; 72};
@@ -115,16 +116,17 @@ struct cgit_query {
115 char *grep; 116 char *grep;
116 char *head; 117 char *head;
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
129struct cgit_config { 131struct cgit_config {
130 char *agefile; 132 char *agefile;
@@ -155,16 +157,17 @@ struct cgit_config {
155 int enable_log_filecount; 157 int enable_log_filecount;
156 int enable_log_linecount; 158 int enable_log_linecount;
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;
169}; 172};
170 173
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index ab9ab66..09f56a6 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -124,16 +124,21 @@ max-message-length
124max-repo-count 124max-repo-count
125 Specifies the number of entries to list per page on therepository 125 Specifies the number of entries to list per page on therepository
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
138nocache 143nocache
139 If set to the value "1" caching will be disabled. This settings is 144 If set to the value "1" caching will be disabled. This settings is
@@ -213,16 +218,21 @@ repo.desc
213repo.enable-log-filecount 218repo.enable-log-filecount
214 A flag which can be used to disable the global setting 219 A flag which can be used to disable the global setting
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.
227 237
228repo.path 238repo.path
@@ -271,16 +281,20 @@ enable-log-linecount=1
271# Add a cgit favicon 281# Add a cgit favicon
272favicon=/favicon.ico 282favicon=/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
285 299
286 300
@@ -343,16 +357,19 @@ repo.path=/pub/git/linux.git
343repo.desc=the kernel 357repo.desc=the kernel
344 358
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
357 robots=index # allow indexing 374 robots=index # allow indexing
358 375
diff --git a/cmd.c b/cmd.c
index 8914fa5..cf97da7 100644
--- a/cmd.c
+++ b/cmd.c
@@ -16,16 +16,17 @@
16#include "ui-commit.h" 16#include "ui-commit.h"
17#include "ui-diff.h" 17#include "ui-diff.h"
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{
30 cgit_clone_head(ctx); 31 cgit_clone_head(ctx);
31} 32}
@@ -103,16 +104,21 @@ static void refs_fn(struct cgit_context *ctx)
103} 104}
104 105
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)
117{ 123{
118 cgit_print_tag(ctx->qry.sha1); 124 cgit_print_tag(ctx->qry.sha1);
@@ -139,16 +145,17 @@ struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
139 def_cmd(log, 1, 1), 145 def_cmd(log, 1, 1),
140 def_cmd(ls_cache, 0, 0), 146 def_cmd(ls_cache, 0, 0),
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
153 if (ctx->qry.page == NULL) { 160 if (ctx->qry.page == NULL) {
154 if (ctx->repo) 161 if (ctx->repo)
diff --git a/shared.c b/shared.c
index a764c4d..578a544 100644
--- a/shared.c
+++ b/shared.c
@@ -53,16 +53,17 @@ struct cgit_repo *cgit_add_repo(const char *url)
53 ret->path = NULL; 53 ret->path = NULL;
54 ret->desc = "[no description]"; 54 ret->desc = "[no description]";
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
67struct cgit_repo *cgit_get_repoinfo(const char *url) 68struct cgit_repo *cgit_get_repoinfo(const char *url)
68{ 69{
diff --git a/ui-shared.c b/ui-shared.c
index fba1ba6..4f28512 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -364,16 +364,22 @@ void cgit_diff_link(char *name, char *title, char *class, char *head,
364} 364}
365 365
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);
378 shortrev[10] = '\0'; 384 shortrev[10] = '\0';
379 if (obj->type == OBJ_COMMIT) { 385 if (obj->type == OBJ_COMMIT) {
@@ -552,17 +558,17 @@ int print_archive_ref(const char *refname, const unsigned char *sha1,
552 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid), 558 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid),
553 buf)); 559 buf));
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)
567 url = fmt("%s/%s", url, ctx.qry.path); 573 url = fmt("%s/%s", url, ctx.qry.path);
568 html_hidden("url", url); 574 html_hidden("url", url);
@@ -614,17 +620,17 @@ void cgit_print_pageheader(struct cgit_context *ctx)
614 620
615 html("<td class='main'>"); 621 html("<td class='main'>");
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
629 html_txt(ctx->cfg.root_title); 635 html_txt(ctx->cfg.root_title);
630 html("</td></tr>\n"); 636 html("</td></tr>\n");
@@ -651,27 +657,30 @@ void cgit_print_pageheader(struct cgit_context *ctx)
651 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 657 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
652 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg); 658 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg);
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='");
676 html_attr(ctx->qry.search); 685 html_attr(ctx->qry.search);
677 html("'/>\n"); 686 html("'/>\n");
diff --git a/ui-shared.h b/ui-shared.h
index 2ab53ae..5a3821f 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -25,22 +25,25 @@ extern void cgit_commit_link(char *name, char *title, char *class, char *head,
25extern void cgit_patch_link(char *name, char *title, char *class, char *head, 25extern void cgit_patch_link(char *name, char *title, char *class, char *head,
26 char *rev); 26 char *rev);
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
@@ -104,16 +104,19 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
104 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head, 104 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head,
105 curr_rev, fullpath); 105 curr_rev, fullpath);
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()
118{ 121{
119 html("<table summary='tree listing' class='list'>\n"); 122 html("<table summary='tree listing' class='list'>\n");