summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (show whitespace changes)
-rw-r--r--Makefile20
-rw-r--r--cgit.c28
-rw-r--r--cgit.css100
-rw-r--r--cgit.h3
-rw-r--r--cgitrc.5.txt21
-rw-r--r--cmd.c7
-rw-r--r--shared.c1
-rwxr-xr-xtests/t0107-snapshot.sh22
-rw-r--r--ui-log.c38
-rw-r--r--ui-refs.c4
-rw-r--r--ui-shared.c22
-rw-r--r--ui-shared.h5
-rw-r--r--ui-snapshot.c10
-rw-r--r--ui-stats.c410
-rw-r--r--ui-stats.h27
-rw-r--r--ui-tag.c14
-rw-r--r--ui-tree.c3
17 files changed, 690 insertions, 45 deletions
diff --git a/Makefile b/Makefile
index 3c7ec07..a52285e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,16 @@
1CGIT_VERSION = v0.8.1 1CGIT_VERSION = v0.8.1
2CGIT_SCRIPT_NAME = cgit.cgi 2CGIT_SCRIPT_NAME = cgit.cgi
3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit 3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
4CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
4CGIT_CONFIG = /etc/cgitrc 5CGIT_CONFIG = /etc/cgitrc
5CACHE_ROOT = /var/cache/cgit 6CACHE_ROOT = /var/cache/cgit
6SHA1_HEADER = <openssl/sha.h> 7SHA1_HEADER = <openssl/sha.h>
7GIT_VER = 1.6.1 8GIT_VER = 1.6.1
8GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2 9GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
10INSTALL = install
9 11
10# Define NO_STRCASESTR if you don't have strcasestr. 12# Define NO_STRCASESTR if you don't have strcasestr.
11# 13#
12# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin). 14# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin).
13# 15#
14 16
@@ -85,12 +87,13 @@ OBJECTS += ui-log.o
85OBJECTS += ui-patch.o 87OBJECTS += ui-patch.o
86OBJECTS += ui-plain.o 88OBJECTS += ui-plain.o
87OBJECTS += ui-refs.o 89OBJECTS += ui-refs.o
88OBJECTS += ui-repolist.o 90OBJECTS += ui-repolist.o
89OBJECTS += ui-shared.o 91OBJECTS += ui-shared.o
90OBJECTS += ui-snapshot.o 92OBJECTS += ui-snapshot.o
93OBJECTS += ui-stats.o
91OBJECTS += ui-summary.o 94OBJECTS += ui-summary.o
92OBJECTS += ui-tag.o 95OBJECTS += ui-tag.o
93OBJECTS += ui-tree.o 96OBJECTS += ui-tree.o
94 97
95ifdef NEEDS_LIBICONV 98ifdef NEEDS_LIBICONV
96 EXTLIBS += -liconv 99 EXTLIBS += -liconv
@@ -125,28 +128,29 @@ cgit: $(OBJECTS) libgit
125 128
126cgit.o: VERSION 129cgit.o: VERSION
127 130
128-include $(OBJECTS:.o=.d) 131-include $(OBJECTS:.o=.d)
129 132
130libgit: 133libgit:
131 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) libgit.a 134 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 libgit.a
132 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) xdiff/lib.a 135 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 xdiff/lib.a
133 136
134test: all 137test: all
135 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all 138 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
136 139
137install: all 140install: all
138 mkdir -p $(DESTDIR)$(CGIT_SCRIPT_PATH) 141 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH)
139 install cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 142 $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
140 install -m 0644 cgit.css $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.css 143 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
141 install -m 0644 cgit.png $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.png 144 $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
145 $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
142 146
143uninstall: 147uninstall:
144 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 148 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
145 rm -f $(CGIT_SCRIPT_PATH)/cgit.css 149 rm -f $(CGIT_DATA_PATH)/cgit.css
146 rm -f $(CGIT_SCRIPT_PATH)/cgit.png 150 rm -f $(CGIT_DATA_PATH)/cgit.png
147 151
148clean: 152clean:
149 rm -f cgit VERSION *.o *.d 153 rm -f cgit VERSION *.o *.d
150 154
151get-git: 155get-git:
152 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git 156 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cgit.c b/cgit.c
index 166fbc6..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;
@@ -290,13 +298,12 @@ static void process_request(void *cbdata)
290 struct cgit_context *ctx = cbdata; 298 struct cgit_context *ctx = cbdata;
291 struct cgit_cmd *cmd; 299 struct cgit_cmd *cmd;
292 300
293 cmd = cgit_get_cmd(ctx); 301 cmd = cgit_get_cmd(ctx);
294 if (!cmd) { 302 if (!cmd) {
295 ctx->page.title = "cgit error"; 303 ctx->page.title = "cgit error";
296 ctx->repo = NULL;
297 cgit_print_http_headers(ctx); 304 cgit_print_http_headers(ctx);
298 cgit_print_docstart(ctx); 305 cgit_print_docstart(ctx);
299 cgit_print_pageheader(ctx); 306 cgit_print_pageheader(ctx);
300 cgit_print_error("Invalid request"); 307 cgit_print_error("Invalid request");
301 cgit_print_docend(); 308 cgit_print_docend();
302 return; 309 return;
@@ -436,35 +443,36 @@ int main(int argc, const char **argv)
436 cgit_parse_args(argc, argv); 443 cgit_parse_args(argc, argv);
437 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG, 444 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG,
438 config_cb); 445 config_cb);
439 ctx.repo = NULL; 446 ctx.repo = NULL;
440 http_parse_querystring(ctx.qry.raw, querystring_cb); 447 http_parse_querystring(ctx.qry.raw, querystring_cb);
441 448
442 /* If virtual-root isn't specified in cgitrc and no url 449 /* If virtual-root isn't specified in cgitrc, lets pretend
443 * parameter is specified on the querystring, lets pretend 450 * that virtual-root equals SCRIPT_NAME.
444 * that virtualroot equals SCRIPT_NAME and use PATH_INFO as
445 * url. This allows cgit to work with virtual urls without
446 * the need for rewriterules in the webserver (as long as
447 * PATH_INFO is included in the cache lookup key).
448 */ 451 */
449 if (!ctx.cfg.virtual_root && !ctx.qry.url) { 452 if (!ctx.cfg.virtual_root)
450 ctx.cfg.virtual_root = ctx.cfg.script_name; 453 ctx.cfg.virtual_root = ctx.cfg.script_name;
454
455 /* If no url parameter is specified on the querystring, lets
456 * use PATH_INFO as url. This allows cgit to work with virtual
457 * urls without the need for rewriterules in the webserver (as
458 * long as PATH_INFO is included in the cache lookup key).
459 */
451 path = getenv("PATH_INFO"); 460 path = getenv("PATH_INFO");
452 if (path) { 461 if (!ctx.qry.url && path) {
453 if (path[0] == '/') 462 if (path[0] == '/')
454 path++; 463 path++;
455 ctx.qry.url = xstrdup(path); 464 ctx.qry.url = xstrdup(path);
456 if (ctx.qry.raw) { 465 if (ctx.qry.raw) {
457 qry = ctx.qry.raw; 466 qry = ctx.qry.raw;
458 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 467 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
459 free(qry); 468 free(qry);
460 } else 469 } else
461 ctx.qry.raw = ctx.qry.url; 470 ctx.qry.raw = ctx.qry.url;
462 cgit_parse_url(ctx.qry.url); 471 cgit_parse_url(ctx.qry.url);
463 } 472 }
464 }
465 473
466 ttl = calc_ttl(); 474 ttl = calc_ttl();
467 ctx.page.expires += ttl*60; 475 ctx.page.expires += ttl*60;
468 if (ctx.cfg.nocache) 476 if (ctx.cfg.nocache)
469 ctx.cfg.cache_size = 0; 477 ctx.cfg.cache_size = 0;
470 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 478 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
diff --git a/cgit.css b/cgit.css
index 068c37b..f844efa 100644
--- a/cgit.css
+++ b/cgit.css
@@ -473,6 +473,106 @@ span.age-years {
473div.footer { 473div.footer {
474 margin-top: 0.5em; 474 margin-top: 0.5em;
475 text-align: center; 475 text-align: center;
476 font-size: 80%; 476 font-size: 80%;
477 color: #ccc; 477 color: #ccc;
478} 478}
479a.branch-deco {
480 margin: 0px 0.5em;
481 padding: 0px 0.25em;
482 background-color: #88ff88;
483 border: solid 1px #007700;
484}
485a.tag-deco {
486 margin: 0px 0.5em;
487 padding: 0px 0.25em;
488 background-color: #ffff88;
489 border: solid 1px #777700;
490}
491a.remote-deco {
492 margin: 0px 0.5em;
493 padding: 0px 0.25em;
494 background-color: #ccccff;
495 border: solid 1px #000077;
496}
497a.deco {
498 margin: 0px 0.5em;
499 padding: 0px 0.25em;
500 background-color: #ff8888;
501 border: solid 1px #770000;
502}
503table.stats {
504 border: solid 1px black;
505 border-collapse: collapse;
506}
507
508table.stats th {
509 text-align: left;
510 padding: 1px 0.5em;
511 background-color: #eee;
512 border: solid 1px black;
513}
514
515table.stats td {
516 text-align: right;
517 padding: 1px 0.5em;
518 border: solid 1px black;
519}
520
521table.stats td.total {
522 font-weight: bold;
523 text-align: left;
524}
525
526table.stats td.sum {
527 color: #c00;
528 font-weight: bold;
529 /*background-color: #eee; */
530}
531
532table.stats td.left {
533 text-align: left;
534}
535
536table.vgraph {
537 border-collapse: separate;
538 border: solid 1px black;
539 height: 200px;
540}
541
542table.vgraph th {
543 background-color: #eee;
544 font-weight: bold;
545 border: solid 1px white;
546 padding: 1px 0.5em;
547}
548
549table.vgraph td {
550 vertical-align: bottom;
551 padding: 0px 10px;
552}
553
554table.vgraph div.bar {
555 background-color: #eee;
556}
557
558table.hgraph {
559 border: solid 1px black;
560 width: 800px;
561}
562
563table.hgraph th {
564 background-color: #eee;
565 font-weight: bold;
566 border: solid 1px black;
567 padding: 1px 0.5em;
568}
569
570table.hgraph td {
571 vertical-align: center;
572 padding: 2px 2px;
573}
574
575table.hgraph div.bar {
576 background-color: #eee;
577 height: 1em;
578}
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 7887b02..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,26 +283,30 @@ 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
285 299
286 300
287# Include some more info about foobar.com on the index page 301# Include some more info about foobar.com on the index page
288root-readme=/var/www/htdocs/about.html 302root-readme=/var/www/htdocs/about.html
289 303
290 304
291# Allow download of tar.gz, tar.bz and zip-files 305# Allow download of tar.gz, tar.bz2 and zip-files
292snapshots=tar.gz tar.bz zip 306snapshots=tar.gz tar.bz2 zip
293 307
294 308
295## 309##
296## List of repositories. 310## List of repositories.
297## PS: Any repositories listed when repo.group is unset will not be 311## PS: Any repositories listed when repo.group is unset will not be
298## displayed under a group heading 312## displayed under a group heading
@@ -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/tests/t0107-snapshot.sh b/tests/t0107-snapshot.sh
index d97c465..8ab4912 100755
--- a/tests/t0107-snapshot.sh
+++ b/tests/t0107-snapshot.sh
@@ -1,39 +1,39 @@
1#!/bin/sh 1#!/bin/sh
2 2
3. ./setup.sh 3. ./setup.sh
4 4
5prepare_tests "Verify snapshot" 5prepare_tests "Verify snapshot"
6 6
7run_test 'get foo/snapshot/test.tar.gz' ' 7run_test 'get foo/snapshot/master.tar.gz' '
8 cgit_url "foo/snapshot/test.tar.gz" >trash/tmp 8 cgit_url "foo/snapshot/master.tar.gz" >trash/tmp
9' 9'
10 10
11run_test 'check html headers' ' 11run_test 'check html headers' '
12 head -n 1 trash/tmp | 12 head -n 1 trash/tmp |
13 grep -e "Content-Type: application/x-tar" && 13 grep -e "Content-Type: application/x-gzip" &&
14 14
15 head -n 2 trash/tmp | 15 head -n 2 trash/tmp |
16 grep -e "Content-Disposition: inline; filename=.test.tar.gz." 16 grep -e "Content-Disposition: inline; filename=.master.tar.gz."
17' 17'
18 18
19run_test 'strip off the header lines' ' 19run_test 'strip off the header lines' '
20 tail -n +6 trash/tmp > trash/test.tar.gz 20 tail -n +6 trash/tmp > trash/master.tar.gz
21' 21'
22 22
23run_test 'verify gzip format' 'gunzip --test trash/test.tar.gz' 23run_test 'verify gzip format' 'gunzip --test trash/master.tar.gz'
24run_test 'untar' ' 24run_test 'untar' '
25 rm -rf trash/foo && 25 rm -rf trash/master &&
26 tar -xf trash/test.tar.gz -C trash 26 tar -xf trash/master.tar.gz -C trash
27' 27'
28 28
29run_test 'count files' ' 29run_test 'count files' '
30 c=$(ls -1 trash/foo/ | wc -l) && 30 c=$(ls -1 trash/master/ | wc -l) &&
31 test $c = 5 31 test $c = 5
32' 32'
33 33
34run_test 'verify untarred file-5' ' 34run_test 'verify untarred file-5' '
35 grep -e "^5$" trash/foo/file-5 && 35 grep -e "^5$" trash/master/file-5 &&
36 test $(cat trash/foo/file-5 | wc -l) = 1 36 test $(cat trash/master/file-5 | wc -l) = 1
37' 37'
38 38
39tests_done 39tests_done
diff --git a/ui-log.c b/ui-log.c
index 2f90778..3202848 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -28,12 +28,44 @@ void inspect_files(struct diff_filepair *pair)
28{ 28{
29 files++; 29 files++;
30 if (ctx.repo->enable_log_linecount) 30 if (ctx.repo->enable_log_linecount)
31 cgit_diff_files(pair->one->sha1, pair->two->sha1, count_lines); 31 cgit_diff_files(pair->one->sha1, pair->two->sha1, count_lines);
32} 32}
33 33
34void show_commit_decorations(struct commit *commit)
35{
36 struct name_decoration *deco;
37 static char buf[1024];
38
39 buf[sizeof(buf) - 1] = 0;
40 deco = lookup_decoration(&name_decoration, &commit->object);
41 while (deco) {
42 if (!prefixcmp(deco->name, "refs/heads/")) {
43 strncpy(buf, deco->name + 11, sizeof(buf) - 1);
44 cgit_log_link(buf, NULL, "branch-deco", buf, NULL, NULL,
45 0, NULL, NULL, ctx.qry.showmsg);
46 }
47 else if (!prefixcmp(deco->name, "tag: refs/tags/")) {
48 strncpy(buf, deco->name + 15, sizeof(buf) - 1);
49 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
50 }
51 else if (!prefixcmp(deco->name, "refs/remotes/")) {
52 strncpy(buf, deco->name + 13, sizeof(buf) - 1);
53 cgit_log_link(buf, NULL, "remote-deco", NULL,
54 sha1_to_hex(commit->object.sha1), NULL,
55 0, NULL, NULL, ctx.qry.showmsg);
56 }
57 else {
58 strncpy(buf, deco->name, sizeof(buf) - 1);
59 cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
60 sha1_to_hex(commit->object.sha1));
61 }
62 deco = deco->next;
63 }
64}
65
34void print_commit(struct commit *commit) 66void print_commit(struct commit *commit)
35{ 67{
36 struct commitinfo *info; 68 struct commitinfo *info;
37 char *tmp; 69 char *tmp;
38 int cols = 2; 70 int cols = 2;
39 71
@@ -46,12 +78,13 @@ void print_commit(struct commit *commit)
46 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); 78 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
47 html_link_close(); 79 html_link_close();
48 htmlf("</td><td%s>", 80 htmlf("</td><td%s>",
49 ctx.qry.showmsg ? " class='logsubject'" : ""); 81 ctx.qry.showmsg ? " class='logsubject'" : "");
50 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, 82 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
51 sha1_to_hex(commit->object.sha1)); 83 sha1_to_hex(commit->object.sha1));
84 show_commit_decorations(commit);
52 html("</td><td>"); 85 html("</td><td>");
53 html_txt(info->author); 86 html_txt(info->author);
54 if (ctx.repo->enable_log_filecount) { 87 if (ctx.repo->enable_log_filecount) {
55 files = 0; 88 files = 0;
56 add_lines = 0; 89 add_lines = 0;
57 rem_lines = 0; 90 rem_lines = 0;
@@ -116,24 +149,27 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
116 init_revisions(&rev, NULL); 149 init_revisions(&rev, NULL);
117 rev.abbrev = DEFAULT_ABBREV; 150 rev.abbrev = DEFAULT_ABBREV;
118 rev.commit_format = CMIT_FMT_DEFAULT; 151 rev.commit_format = CMIT_FMT_DEFAULT;
119 rev.verbose_header = 1; 152 rev.verbose_header = 1;
120 rev.show_root_diff = 0; 153 rev.show_root_diff = 0;
121 setup_revisions(argc, argv, &rev, NULL); 154 setup_revisions(argc, argv, &rev, NULL);
155 load_ref_decorations();
156 rev.show_decorations = 1;
122 rev.grep_filter.regflags |= REG_ICASE; 157 rev.grep_filter.regflags |= REG_ICASE;
123 compile_grep_patterns(&rev.grep_filter); 158 compile_grep_patterns(&rev.grep_filter);
124 prepare_revision_walk(&rev); 159 prepare_revision_walk(&rev);
125 160
126 if (pager) 161 if (pager)
127 html("<table class='list nowrap'>"); 162 html("<table class='list nowrap'>");
128 163
129 html("<tr class='nohover'><th class='left'>Age</th>" 164 html("<tr class='nohover'><th class='left'>Age</th>"
130 "<th class='left'>Commit message"); 165 "<th class='left'>Commit message");
131 if (pager) { 166 if (pager) {
132 html(" ("); 167 html(" (");
133 cgit_log_link("toggle", NULL, NULL, ctx.qry.head, ctx.qry.sha1, 168 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
169 NULL, ctx.qry.head, ctx.qry.sha1,
134 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep, 170 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep,
135 ctx.qry.search, ctx.qry.showmsg ? 0 : 1); 171 ctx.qry.search, ctx.qry.showmsg ? 0 : 1);
136 html(")"); 172 html(")");
137 } 173 }
138 html("</th><th class='left'>Author</th>"); 174 html("</th><th class='left'>Author</th>");
139 if (ctx.repo->enable_log_filecount) { 175 if (ctx.repo->enable_log_filecount) {
diff --git a/ui-refs.c b/ui-refs.c
index d61ee7c..25da00a 100644
--- a/ui-refs.c
+++ b/ui-refs.c
@@ -136,15 +136,15 @@ static int print_tag(struct refinfo *ref)
136 cgit_print_age(info->tagger_date, -1, NULL); 136 cgit_print_age(info->tagger_date, -1, NULL);
137 html("</td></tr>\n"); 137 html("</td></tr>\n");
138 } else { 138 } else {
139 if (!header) 139 if (!header)
140 print_tag_header(); 140 print_tag_header();
141 html("<tr><td>"); 141 html("<tr><td>");
142 html_txt(name); 142 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
143 html("</td><td>"); 143 html("</td><td>");
144 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT)) 144 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT))
145 print_tag_downloads(ctx.repo, name); 145 print_tag_downloads(ctx.repo, name);
146 else 146 else
147 cgit_object_link(ref->object); 147 cgit_object_link(ref->object);
148 html("</td></tr>\n"); 148 html("</td></tr>\n");
149 } 149 }
150 return 0; 150 return 0;
diff --git a/ui-shared.c b/ui-shared.c
index 95dfeb4..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)
@@ -584,21 +590,26 @@ void add_hidden_formfields(int incl_head, int incl_search, char *page)
584 html_hidden("qt", ctx.qry.grep); 590 html_hidden("qt", ctx.qry.grep);
585 if (ctx.qry.search) 591 if (ctx.qry.search)
586 html_hidden("q", ctx.qry.search); 592 html_hidden("q", ctx.qry.search);
587 } 593 }
588} 594}
589 595
596const char *fallback_cmd = "repolist";
597
590char *hc(struct cgit_cmd *cmd, const char *page) 598char *hc(struct cgit_cmd *cmd, const char *page)
591{ 599{
592 return (strcmp(cmd->name, page) ? NULL : "active"); 600 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
593} 601}
594 602
595void cgit_print_pageheader(struct cgit_context *ctx) 603void cgit_print_pageheader(struct cgit_context *ctx)
596{ 604{
597 struct cgit_cmd *cmd = cgit_get_cmd(ctx); 605 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
598 606
607 if (!cmd && ctx->repo)
608 fallback_cmd = "summary";
609
599 html("<table id='header'>\n"); 610 html("<table id='header'>\n");
600 html("<tr>\n"); 611 html("<tr>\n");
601 html("<td class='logo' rowspan='2'><a href='"); 612 html("<td class='logo' rowspan='2'><a href='");
602 if (ctx->cfg.logo_link) 613 if (ctx->cfg.logo_link)
603 html_attr(ctx->cfg.logo_link); 614 html_attr(ctx->cfg.logo_link);
604 else 615 else
@@ -611,13 +622,13 @@ void cgit_print_pageheader(struct cgit_context *ctx)
611 if (ctx->repo) { 622 if (ctx->repo) {
612 cgit_index_link("index", NULL, NULL, NULL, 0); 623 cgit_index_link("index", NULL, NULL, NULL, 0);
613 html(" : "); 624 html(" : ");
614 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 625 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
615 html("</td><td class='form'>"); 626 html("</td><td class='form'>");
616 html("<form method='get' action=''>\n"); 627 html("<form method='get' action=''>\n");
617 add_hidden_formfields(0, 1, ctx->qry.page); 628 cgit_add_hidden_formfields(0, 1, ctx->qry.page);
618 html("<select name='h' onchange='this.form.submit();'>\n"); 629 html("<select name='h' onchange='this.form.submit();'>\n");
619 for_each_branch_ref(print_branch_option, ctx->qry.head); 630 for_each_branch_ref(print_branch_option, ctx->qry.head);
620 html("</select> "); 631 html("</select> ");
621 html("<input type='submit' name='' value='switch'/>"); 632 html("<input type='submit' name='' value='switch'/>");
622 html("</form>"); 633 html("</form>");
623 } else 634 } else
@@ -648,23 +659,26 @@ void cgit_print_pageheader(struct cgit_context *ctx)
648 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 659 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
649 ctx->qry.sha1, NULL); 660 ctx->qry.sha1, NULL);
650 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 661 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
651 ctx->qry.head, ctx->qry.sha1); 662 ctx->qry.head, ctx->qry.sha1);
652 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 663 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
653 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);
654 if (ctx->repo->readme) 668 if (ctx->repo->readme)
655 reporevlink("about", "about", NULL, 669 reporevlink("about", "about", NULL,
656 hc(cmd, "about"), ctx->qry.head, NULL, 670 hc(cmd, "about"), ctx->qry.head, NULL,
657 NULL); 671 NULL);
658 html("</td><td class='form'>"); 672 html("</td><td class='form'>");
659 html("<form class='right' method='get' action='"); 673 html("<form class='right' method='get' action='");
660 if (ctx->cfg.virtual_root) 674 if (ctx->cfg.virtual_root)
661 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 675 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
662 ctx->qry.path, NULL)); 676 ctx->qry.path, NULL));
663 html("'>\n"); 677 html("'>\n");
664 add_hidden_formfields(1, 0, "log"); 678 cgit_add_hidden_formfields(1, 0, "log");
665 html("<select name='qt'>\n"); 679 html("<select name='qt'>\n");
666 html_option("grep", "log msg", ctx->qry.grep); 680 html_option("grep", "log msg", ctx->qry.grep);
667 html_option("author", "author", ctx->qry.grep); 681 html_option("author", "author", ctx->qry.grep);
668 html_option("committer", "committer", ctx->qry.grep); 682 html_option("committer", "committer", ctx->qry.grep);
669 html("</select>\n"); 683 html("</select>\n");
670 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-snapshot.c b/ui-snapshot.c
index 6f09151..f25613e 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -55,14 +55,14 @@ static int write_tar_bzip2_archive(struct archiver_args *args)
55{ 55{
56 return write_compressed_tar_archive(args,"bzip2"); 56 return write_compressed_tar_archive(args,"bzip2");
57} 57}
58 58
59const struct cgit_snapshot_format cgit_snapshot_formats[] = { 59const struct cgit_snapshot_format cgit_snapshot_formats[] = {
60 { ".zip", "application/x-zip", write_zip_archive, 0x1 }, 60 { ".zip", "application/x-zip", write_zip_archive, 0x1 },
61 { ".tar.gz", "application/x-tar", write_tar_gzip_archive, 0x2 }, 61 { ".tar.gz", "application/x-gzip", write_tar_gzip_archive, 0x2 },
62 { ".tar.bz2", "application/x-tar", write_tar_bzip2_archive, 0x4 }, 62 { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive, 0x4 },
63 { ".tar", "application/x-tar", write_tar_archive, 0x8 }, 63 { ".tar", "application/x-tar", write_tar_archive, 0x8 },
64 {} 64 {}
65}; 65};
66 66
67static const struct cgit_snapshot_format *get_format(const char *filename) 67static const struct cgit_snapshot_format *get_format(const char *filename)
68{ 68{
@@ -172,17 +172,19 @@ void cgit_print_snapshot(const char *head, const char *hex,
172 cgit_print_docend(); 172 cgit_print_docend();
173 return; 173 return;
174 } 174 }
175 175
176 if (!hex && dwim) { 176 if (!hex && dwim) {
177 hex = get_ref_from_filename(ctx.repo->url, filename, f); 177 hex = get_ref_from_filename(ctx.repo->url, filename, f);
178 if (hex != NULL) { 178 if (hex == NULL) {
179 html_status(404, "Not found", 0);
180 return;
181 }
179 prefix = xstrdup(filename); 182 prefix = xstrdup(filename);
180 prefix[strlen(filename) - strlen(f->suffix)] = '\0'; 183 prefix[strlen(filename) - strlen(f->suffix)] = '\0';
181 } 184 }
182 }
183 185
184 if (!hex) 186 if (!hex)
185 hex = head; 187 hex = head;
186 188
187 if (!prefix) 189 if (!prefix)
188 prefix = xstrdup(cgit_repobasename(ctx.repo->url)); 190 prefix = xstrdup(cgit_repobasename(ctx.repo->url));
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-tag.c b/ui-tag.c
index 3aea87d..0e056e0 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -50,14 +50,15 @@ void cgit_print_tag(char *revname)
50 tag = lookup_tag(sha1); 50 tag = lookup_tag(sha1);
51 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) { 51 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) {
52 cgit_print_error(fmt("Bad tag object: %s", revname)); 52 cgit_print_error(fmt("Bad tag object: %s", revname));
53 return; 53 return;
54 } 54 }
55 html("<table class='commit-info'>\n"); 55 html("<table class='commit-info'>\n");
56 htmlf("<tr><td>Tag name</td><td>%s (%s)</td></tr>\n", 56 htmlf("<tr><td>Tag name</td><td>");
57 revname, sha1_to_hex(sha1)); 57 html_txt(revname);
58 htmlf(" (%s)</td></tr>\n", sha1_to_hex(sha1));
58 if (info->tagger_date > 0) { 59 if (info->tagger_date > 0) {
59 html("<tr><td>Tag date</td><td>"); 60 html("<tr><td>Tag date</td><td>");
60 cgit_print_date(info->tagger_date, FMT_LONGDATE, ctx.cfg.local_time); 61 cgit_print_date(info->tagger_date, FMT_LONGDATE, ctx.cfg.local_time);
61 html("</td></tr>\n"); 62 html("</td></tr>\n");
62 } 63 }
63 if (info->tagger) { 64 if (info->tagger) {
@@ -71,9 +72,18 @@ void cgit_print_tag(char *revname)
71 } 72 }
72 html("<tr><td>Tagged object</td><td>"); 73 html("<tr><td>Tagged object</td><td>");
73 cgit_object_link(tag->tagged); 74 cgit_object_link(tag->tagged);
74 html("</td></tr>\n"); 75 html("</td></tr>\n");
75 html("</table>\n"); 76 html("</table>\n");
76 print_tag_content(info->msg); 77 print_tag_content(info->msg);
78 } else {
79 html("<table class='commit-info'>\n");
80 htmlf("<tr><td>Tag name</td><td>");
81 html_txt(revname);
82 html("</td></tr>\n");
83 html("<tr><td>Tagged object</td><td>");
84 cgit_object_link(obj);
85 html("</td></tr>\n");
86 html("</table>\n");
77 } 87 }
78 return; 88 return;
79} 89}
diff --git a/ui-tree.c b/ui-tree.c
index 2a8625c..c26ba4c 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -107,12 +107,15 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
107 } 107 }
108 htmlf("</td><td class='ls-size'>%li</td>", size); 108 htmlf("</td><td class='ls-size'>%li</td>", size);
109 109
110 html("<td>"); 110 html("<td>");
111 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, 111 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev,
112 fullpath, 0, NULL, NULL, ctx.qry.showmsg); 112 fullpath, 0, NULL, NULL, ctx.qry.showmsg);
113 if (ctx.repo->max_stats)
114 cgit_stats_link("stats", NULL, "button", ctx.qry.head,
115 fullpath);
113 html("</td></tr>\n"); 116 html("</td></tr>\n");
114 free(name); 117 free(name);
115 return 0; 118 return 0;
116} 119}
117 120
118static void ls_head() 121static void ls_head()