aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--cgit.c53
-rw-r--r--cgit.css148
-rw-r--r--cgit.h6
-rw-r--r--cgitrc.5.txt17
-rw-r--r--cmd.c10
m---------git0
-rw-r--r--shared.c6
-rwxr-xr-xtests/t0104-tree.sh4
-rwxr-xr-xtests/t0105-commit.sh2
-rwxr-xr-xtests/t0107-snapshot.sh22
-rw-r--r--ui-commit.c4
-rw-r--r--ui-log.c71
-rw-r--r--ui-log.h1
-rw-r--r--ui-refs.c42
-rw-r--r--ui-repolist.c137
-rw-r--r--ui-shared.c44
-rw-r--r--ui-shared.h7
-rw-r--r--ui-snapshot.c100
-rw-r--r--ui-snapshot.h3
-rw-r--r--ui-stats.c410
-rw-r--r--ui-stats.h27
-rw-r--r--ui-tag.c11
-rw-r--r--ui-tree.c36
24 files changed, 1007 insertions, 157 deletions
diff --git a/Makefile b/Makefile
index 52b3586..19e3bb8 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
5CGIT_CONFIG = /etc/cgitrc 5CGIT_CONFIG = /etc/cgitrc
6CACHE_ROOT = /var/cache/cgit 6CACHE_ROOT = /var/cache/cgit
7SHA1_HEADER = <openssl/sha.h> 7SHA1_HEADER = <openssl/sha.h>
8GIT_VER = 1.6.0.3 8GIT_VER = 1.6.1
9GIT_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 10INSTALL = install
11 11
@@ -90,6 +90,7 @@ OBJECTS += 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
diff --git a/cgit.c b/cgit.c
index 6e5215e..608cab6 100644
--- a/cgit.c
+++ b/cgit.c
@@ -12,6 +12,7 @@
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;
@@ -54,6 +55,8 @@ void config_cb(const char *name, const char *value)
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"))
@@ -112,6 +115,8 @@ void config_cb(const char *name, const char *value)
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) {
@@ -154,6 +159,12 @@ static void querystring_cb(const char *name, const char *value)
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);
162 } else if (!strcmp(name, "s")){
163 ctx.qry.sort = xstrdup(value);
164 } else if (!strcmp(name, "showmsg")) {
165 ctx.qry.showmsg = atoi(value);
166 } else if (!strcmp(name, "period")) {
167 ctx.qry.period = xstrdup(value);
157 } 168 }
158} 169}
159 170
@@ -177,6 +188,7 @@ static void prepare_context(struct cgit_context *ctx)
177 ctx->cfg.max_lock_attempts = 5; 188 ctx->cfg.max_lock_attempts = 5;
178 ctx->cfg.max_msg_len = 80; 189 ctx->cfg.max_msg_len = 80;
179 ctx->cfg.max_repodesc_len = 80; 190 ctx->cfg.max_repodesc_len = 80;
191 ctx->cfg.max_stats = 0;
180 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 192 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
181 ctx->cfg.renamelimit = -1; 193 ctx->cfg.renamelimit = -1;
182 ctx->cfg.robots = "index, nofollow"; 194 ctx->cfg.robots = "index, nofollow";
@@ -434,28 +446,29 @@ int main(int argc, const char **argv)
434 ctx.repo = NULL; 446 ctx.repo = NULL;
435 http_parse_querystring(ctx.qry.raw, querystring_cb); 447 http_parse_querystring(ctx.qry.raw, querystring_cb);
436 448
437 /* If virtual-root isn't specified in cgitrc and no url 449 /* If virtual-root isn't specified in cgitrc, lets pretend
438 * parameter is specified on the querystring, lets pretend 450 * that virtual-root equals SCRIPT_NAME.
439 * that virtualroot equals SCRIPT_NAME and use PATH_INFO as
440 * url. This allows cgit to work with virtual urls without
441 * the need for rewriterules in the webserver (as long as
442 * PATH_INFO is included in the cache lookup key).
443 */ 451 */
444 if (!ctx.cfg.virtual_root && !ctx.qry.url) { 452 if (!ctx.cfg.virtual_root)
445 ctx.cfg.virtual_root = ctx.cfg.script_name; 453 ctx.cfg.virtual_root = ctx.cfg.script_name;
446 path = getenv("PATH_INFO"); 454
447 if (path) { 455 /* If no url parameter is specified on the querystring, lets
448 if (path[0] == '/') 456 * use PATH_INFO as url. This allows cgit to work with virtual
449 path++; 457 * urls without the need for rewriterules in the webserver (as
450 ctx.qry.url = xstrdup(path); 458 * long as PATH_INFO is included in the cache lookup key).
451 if (ctx.qry.raw) { 459 */
452 qry = ctx.qry.raw; 460 path = getenv("PATH_INFO");
453 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 461 if (!ctx.qry.url && path) {
454 free(qry); 462 if (path[0] == '/')
455 } else 463 path++;
456 ctx.qry.raw = ctx.qry.url; 464 ctx.qry.url = xstrdup(path);
457 cgit_parse_url(ctx.qry.url); 465 if (ctx.qry.raw) {
458 } 466 qry = ctx.qry.raw;
467 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
468 free(qry);
469 } else
470 ctx.qry.raw = ctx.qry.url;
471 cgit_parse_url(ctx.qry.url);
459 } 472 }
460 473
461 ttl = calc_ttl(); 474 ttl = calc_ttl();
diff --git a/cgit.css b/cgit.css
index a37d218..957d5aa 100644
--- a/cgit.css
+++ b/cgit.css
@@ -120,6 +120,10 @@ table.list tr {
120 background: white; 120 background: white;
121} 121}
122 122
123table.list tr.logheader {
124 background: #eee;
125}
126
123table.list tr:hover { 127table.list tr:hover {
124 background: #eee; 128 background: #eee;
125} 129}
@@ -143,6 +147,17 @@ table.list td {
143 padding: 0.1em 0.5em 0.1em 0.5em; 147 padding: 0.1em 0.5em 0.1em 0.5em;
144} 148}
145 149
150table.list td.logsubject {
151 font-family: monospace;
152 font-weight: bold;
153}
154
155table.list td.logmsg {
156 font-family: monospace;
157 white-space: pre;
158 padding: 1em 0em 2em 0em;
159}
160
146table.list td a { 161table.list td a {
147 color: black; 162 color: black;
148} 163}
@@ -221,26 +236,31 @@ table.blob {
221 border-top: solid 1px black; 236 border-top: solid 1px black;
222} 237}
223 238
224table.blob td.no { 239table.blob td.lines {
225 border-right: solid 1px black; 240 margin: 0; padding: 0;
241 vertical-align: top;
226 color: black; 242 color: black;
243}
244
245table.blob td.linenumbers {
246 margin: 0; padding: 0;
247 vertical-align: top;
248 border-right: 1px solid gray;
227 background-color: #eee; 249 background-color: #eee;
228 text-align: right;
229} 250}
230 251
231table.blob td.no a { 252table.blob pre {
232 color: black; 253 padding: 0; margin: 0;
233} 254}
234 255
235table.blob td.no a:hover { 256table.blob a.no {
236 color: black; 257 color: gray;
258 text-align: right;
237 text-decoration: none; 259 text-decoration: none;
238} 260}
239 261
240table.blob td.txt { 262table.blob a.no a:hover {
241 white-space: pre; 263 color: black;
242 font-family: monospace;
243 padding-left: 0.5em;
244} 264}
245 265
246table.nowrap td { 266table.nowrap td {
@@ -456,3 +476,109 @@ div.footer {
456 font-size: 80%; 476 font-size: 80%;
457 color: #ccc; 477 color: #ccc;
458} 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}
503
504div.commit-subject a {
505 margin-left: 1em;
506 font-size: 75%;
507}
508
509table.stats {
510 border: solid 1px black;
511 border-collapse: collapse;
512}
513
514table.stats th {
515 text-align: left;
516 padding: 1px 0.5em;
517 background-color: #eee;
518 border: solid 1px black;
519}
520
521table.stats td {
522 text-align: right;
523 padding: 1px 0.5em;
524 border: solid 1px black;
525}
526
527table.stats td.total {
528 font-weight: bold;
529 text-align: left;
530}
531
532table.stats td.sum {
533 color: #c00;
534 font-weight: bold;
535/* background-color: #eee; */
536}
537
538table.stats td.left {
539 text-align: left;
540}
541
542table.vgraph {
543 border-collapse: separate;
544 border: solid 1px black;
545 height: 200px;
546}
547
548table.vgraph th {
549 background-color: #eee;
550 font-weight: bold;
551 border: solid 1px white;
552 padding: 1px 0.5em;
553}
554
555table.vgraph td {
556 vertical-align: bottom;
557 padding: 0px 10px;
558}
559
560table.vgraph div.bar {
561 background-color: #eee;
562}
563
564table.hgraph {
565 border: solid 1px black;
566 width: 800px;
567}
568
569table.hgraph th {
570 background-color: #eee;
571 font-weight: bold;
572 border: solid 1px black;
573 padding: 1px 0.5em;
574}
575
576table.hgraph td {
577 vertical-align: center;
578 padding: 2px 2px;
579}
580
581table.hgraph div.bar {
582 background-color: #eee;
583 height: 1em;
584}
diff --git a/cgit.h b/cgit.h
index 92f0c5a..4fe94c6 100644
--- a/cgit.h
+++ b/cgit.h
@@ -61,6 +61,8 @@ struct cgit_repo {
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;
65 time_t mtime;
64}; 66};
65 67
66struct cgit_repolist { 68struct cgit_repolist {
@@ -119,8 +121,11 @@ struct cgit_query {
119 char *name; 121 char *name;
120 char *mimetype; 122 char *mimetype;
121 char *url; 123 char *url;
124 char *period;
122 int ofs; 125 int ofs;
123 int nohead; 126 int nohead;
127 char *sort;
128 int showmsg;
124}; 129};
125 130
126struct cgit_config { 131struct cgit_config {
@@ -157,6 +162,7 @@ struct cgit_config {
157 int max_lock_attempts; 162 int max_lock_attempts;
158 int max_msg_len; 163 int max_msg_len;
159 int max_repodesc_len; 164 int max_repodesc_len;
165 int max_stats;
160 int nocache; 166 int nocache;
161 int renamelimit; 167 int renamelimit;
162 int snapshots; 168 int snapshots;
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index ab9ab66..09f56a6 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -129,6 +129,11 @@ max-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
@@ -218,6 +223,11 @@ repo.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
@@ -276,6 +286,10 @@ favicon=/favicon.ico
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
@@ -348,6 +362,9 @@ repo.snapshots=0
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----
diff --git a/cmd.c b/cmd.c
index 5b3c14c..cf97da7 100644
--- a/cmd.c
+++ b/cmd.c
@@ -21,6 +21,7 @@
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"
@@ -104,11 +105,15 @@ static void refs_fn(struct cgit_context *ctx)
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, 108 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, ctx->qry.path,
108 cgit_repobasename(ctx->repo->url), ctx->qry.path,
109 ctx->repo->snapshots, ctx->qry.nohead); 109 ctx->repo->snapshots, ctx->qry.nohead);
110} 110}
111 111
112static void stats_fn(struct cgit_context *ctx)
113{
114 cgit_show_stats(ctx);
115}
116
112static void summary_fn(struct cgit_context *ctx) 117static void summary_fn(struct cgit_context *ctx)
113{ 118{
114 cgit_print_summary(); 119 cgit_print_summary();
@@ -145,6 +150,7 @@ struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
145 def_cmd(refs, 1, 1), 150 def_cmd(refs, 1, 1),
146 def_cmd(repolist, 0, 0), 151 def_cmd(repolist, 0, 0),
147 def_cmd(snapshot, 1, 0), 152 def_cmd(snapshot, 1, 0),
153 def_cmd(stats, 1, 1),
148 def_cmd(summary, 1, 1), 154 def_cmd(summary, 1, 1),
149 def_cmd(tag, 1, 1), 155 def_cmd(tag, 1, 1),
150 def_cmd(tree, 1, 1), 156 def_cmd(tree, 1, 1),
diff --git a/git b/git
Subproject 031e6c898f61db1ae0c0be641eac6532c1000d5 Subproject 8104ebfe8276657ee803cca7eb8665a78cf3ef8
diff --git a/shared.c b/shared.c
index f5875e4..578a544 100644
--- a/shared.c
+++ b/shared.c
@@ -58,8 +58,10 @@ struct cgit_repo *cgit_add_repo(const char *url)
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;
64 ret->mtime = -1;
63 return ret; 65 return ret;
64} 66}
65 67
@@ -266,10 +268,12 @@ int cgit_diff_files(const unsigned char *old_sha1,
266 if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1)) 268 if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1))
267 return 1; 269 return 1;
268 270
271 memset(&diff_params, 0, sizeof(diff_params));
272 memset(&emit_params, 0, sizeof(emit_params));
273 memset(&emit_cb, 0, sizeof(emit_cb));
269 diff_params.flags = XDF_NEED_MINIMAL; 274 diff_params.flags = XDF_NEED_MINIMAL;
270 emit_params.ctxlen = 3; 275 emit_params.ctxlen = 3;
271 emit_params.flags = XDL_EMIT_FUNCNAMES; 276 emit_params.flags = XDL_EMIT_FUNCNAMES;
272 emit_params.find_func = NULL;
273 emit_cb.outf = filediff_cb; 277 emit_cb.outf = filediff_cb;
274 emit_cb.priv = fn; 278 emit_cb.priv = fn;
275 xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb); 279 xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb);
diff --git a/tests/t0104-tree.sh b/tests/t0104-tree.sh
index 33f4eb0..2ce1251 100755
--- a/tests/t0104-tree.sh
+++ b/tests/t0104-tree.sh
@@ -11,11 +11,11 @@ run_test 'find file-50' 'grep -e "file-50" trash/tmp'
11run_test 'generate bar/tree/file-50' 'cgit_url "bar/tree/file-50" >trash/tmp' 11run_test 'generate bar/tree/file-50' 'cgit_url "bar/tree/file-50" >trash/tmp'
12 12
13run_test 'find line 1' ' 13run_test 'find line 1' '
14 grep -e "<a id=.n1. name=.n1. href=.#n1.>1</a>" trash/tmp 14 grep -e "<a class=.no. id=.n1. name=.n1. href=.#n1.>1</a>" trash/tmp
15' 15'
16 16
17run_test 'no line 2' ' 17run_test 'no line 2' '
18 ! grep -e "<a id=.n2. name=.n2. href=.#n2.>2</a>" trash/tmp 18 ! grep -e "<a class=.no. id=.n2. name=.n2. href=.#n2.>2</a>" trash/tmp
19' 19'
20 20
21run_test 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >trash/tmp' 21run_test 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >trash/tmp'
diff --git a/tests/t0105-commit.sh b/tests/t0105-commit.sh
index a864612..ae794c8 100755
--- a/tests/t0105-commit.sh
+++ b/tests/t0105-commit.sh
@@ -9,7 +9,7 @@ run_test 'find tree link' 'grep -e "<a href=./foo/tree/.>" trash/tmp'
9run_test 'find parent link' 'grep -E "<a href=./foo/commit/\?id=.+>" trash/tmp' 9run_test 'find parent link' 'grep -E "<a href=./foo/commit/\?id=.+>" trash/tmp'
10 10
11run_test 'find commit subject' ' 11run_test 'find commit subject' '
12 grep -e "<div class=.commit-subject.>commit 5</div>" trash/tmp 12 grep -e "<div class=.commit-subject.>commit 5<" trash/tmp
13' 13'
14 14
15run_test 'find commit msg' 'grep -e "<div class=.commit-msg.></div>" trash/tmp' 15run_test 'find commit msg' 'grep -e "<div class=.commit-msg.></div>" trash/tmp'
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
@@ -4,36 +4,36 @@
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-commit.c b/ui-commit.c
index a6a85a4..41ce70e 100644
--- a/ui-commit.c
+++ b/ui-commit.c
@@ -10,6 +10,7 @@
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12#include "ui-diff.h" 12#include "ui-diff.h"
13#include "ui-log.h"
13 14
14void cgit_print_commit(char *hex) 15void cgit_print_commit(char *hex)
15{ 16{
@@ -34,6 +35,8 @@ void cgit_print_commit(char *hex)
34 } 35 }
35 info = cgit_parse_commit(commit); 36 info = cgit_parse_commit(commit);
36 37
38 load_ref_decorations();
39
37 html("<table summary='commit info' class='commit-info'>\n"); 40 html("<table summary='commit info' class='commit-info'>\n");
38 html("<tr><th>author</th><td>"); 41 html("<tr><th>author</th><td>");
39 html_txt(info->author); 42 html_txt(info->author);
@@ -87,6 +90,7 @@ void cgit_print_commit(char *hex)
87 html("</table>\n"); 90 html("</table>\n");
88 html("<div class='commit-subject'>"); 91 html("<div class='commit-subject'>");
89 html_txt(info->subject); 92 html_txt(info->subject);
93 show_commit_decorations(commit);
90 html("</div>"); 94 html("</div>");
91 html("<div class='commit-msg'>"); 95 html("<div class='commit-msg'>");
92 html_txt(info->msg); 96 html_txt(info->msg);
diff --git a/ui-log.c b/ui-log.c
index d212984..3202848 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -31,21 +31,57 @@ void inspect_files(struct diff_filepair *pair)
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;
70 int cols = 2;
38 71
39 info = cgit_parse_commit(commit); 72 info = cgit_parse_commit(commit);
40 html("<tr><td>"); 73 htmlf("<tr%s><td>",
74 ctx.qry.showmsg ? " class='logheader'" : "");
41 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1)); 75 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1));
42 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp); 76 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp);
43 html_link_open(tmp, NULL, NULL); 77 html_link_open(tmp, NULL, NULL);
44 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); 78 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
45 html_link_close(); 79 html_link_close();
46 html("</td><td>"); 80 htmlf("</td><td%s>",
81 ctx.qry.showmsg ? " class='logsubject'" : "");
47 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, 82 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
48 sha1_to_hex(commit->object.sha1)); 83 sha1_to_hex(commit->object.sha1));
84 show_commit_decorations(commit);
49 html("</td><td>"); 85 html("</td><td>");
50 html_txt(info->author); 86 html_txt(info->author);
51 if (ctx.repo->enable_log_filecount) { 87 if (ctx.repo->enable_log_filecount) {
@@ -61,6 +97,17 @@ void print_commit(struct commit *commit)
61 } 97 }
62 } 98 }
63 html("</td></tr>\n"); 99 html("</td></tr>\n");
100 if (ctx.qry.showmsg) {
101 if (ctx.repo->enable_log_filecount) {
102 cols++;
103 if (ctx.repo->enable_log_linecount)
104 cols++;
105 }
106 htmlf("<tr class='nohover'><td/><td colspan='%d' class='logmsg'>",
107 cols);
108 html_txt(info->msg);
109 html("</td></tr>\n");
110 }
64 cgit_free_commitinfo(info); 111 cgit_free_commitinfo(info);
65} 112}
66 113
@@ -105,6 +152,8 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
105 rev.verbose_header = 1; 152 rev.verbose_header = 1;
106 rev.show_root_diff = 0; 153 rev.show_root_diff = 0;
107 setup_revisions(argc, argv, &rev, NULL); 154 setup_revisions(argc, argv, &rev, NULL);
155 load_ref_decorations();
156 rev.show_decorations = 1;
108 rev.grep_filter.regflags |= REG_ICASE; 157 rev.grep_filter.regflags |= REG_ICASE;
109 compile_grep_patterns(&rev.grep_filter); 158 compile_grep_patterns(&rev.grep_filter);
110 prepare_revision_walk(&rev); 159 prepare_revision_walk(&rev);
@@ -113,8 +162,16 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
113 html("<table class='list nowrap'>"); 162 html("<table class='list nowrap'>");
114 163
115 html("<tr class='nohover'><th class='left'>Age</th>" 164 html("<tr class='nohover'><th class='left'>Age</th>"
116 "<th class='left'>Commit message</th>" 165 "<th class='left'>Commit message");
117 "<th class='left'>Author</th>"); 166 if (pager) {
167 html(" (");
168 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
169 NULL, ctx.qry.head, ctx.qry.sha1,
170 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep,
171 ctx.qry.search, ctx.qry.showmsg ? 0 : 1);
172 html(")");
173 }
174 html("</th><th class='left'>Author</th>");
118 if (ctx.repo->enable_log_filecount) { 175 if (ctx.repo->enable_log_filecount) {
119 html("<th class='left'>Files</th>"); 176 html("<th class='left'>Files</th>");
120 columns++; 177 columns++;
@@ -149,20 +206,20 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
149 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, 206 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head,
150 ctx.qry.sha1, ctx.qry.path, 207 ctx.qry.sha1, ctx.qry.path,
151 ofs - cnt, ctx.qry.grep, 208 ofs - cnt, ctx.qry.grep,
152 ctx.qry.search); 209 ctx.qry.search, ctx.qry.showmsg);
153 html("&nbsp;"); 210 html("&nbsp;");
154 } 211 }
155 if ((commit = get_revision(&rev)) != NULL) { 212 if ((commit = get_revision(&rev)) != NULL) {
156 cgit_log_link("[next]", NULL, NULL, ctx.qry.head, 213 cgit_log_link("[next]", NULL, NULL, ctx.qry.head,
157 ctx.qry.sha1, ctx.qry.path, 214 ctx.qry.sha1, ctx.qry.path,
158 ofs + cnt, ctx.qry.grep, 215 ofs + cnt, ctx.qry.grep,
159 ctx.qry.search); 216 ctx.qry.search, ctx.qry.showmsg);
160 } 217 }
161 html("</div>"); 218 html("</div>");
162 } else if ((commit = get_revision(&rev)) != NULL) { 219 } else if ((commit = get_revision(&rev)) != NULL) {
163 html("<tr class='nohover'><td colspan='3'>"); 220 html("<tr class='nohover'><td colspan='3'>");
164 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, NULL, 0, 221 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, NULL, 0,
165 NULL, NULL); 222 NULL, NULL, ctx.qry.showmsg);
166 html("</td></tr>\n"); 223 html("</td></tr>\n");
167 } 224 }
168} 225}
diff --git a/ui-log.h b/ui-log.h
index 877e40e..6034055 100644
--- a/ui-log.h
+++ b/ui-log.h
@@ -3,5 +3,6 @@
3 3
4extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, 4extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep,
5 char *pattern, char *path, int pager); 5 char *pattern, char *path, int pager);
6extern void show_commit_decorations(struct commit *commit);
6 7
7#endif /* UI_LOG_H */ 8#endif /* UI_LOG_H */
diff --git a/ui-refs.c b/ui-refs.c
index 32e0429..25da00a 100644
--- a/ui-refs.c
+++ b/ui-refs.c
@@ -58,7 +58,8 @@ static int print_branch(struct refinfo *ref)
58 if (!info) 58 if (!info)
59 return 1; 59 return 1;
60 html("<tr><td>"); 60 html("<tr><td>");
61 cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL); 61 cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL,
62 ctx.qry.showmsg);
62 html("</td><td>"); 63 html("</td><td>");
63 64
64 if (ref->object->type == OBJ_COMMIT) { 65 if (ref->object->type == OBJ_COMMIT) {
@@ -78,12 +79,37 @@ static int print_branch(struct refinfo *ref)
78static void print_tag_header() 79static void print_tag_header()
79{ 80{
80 html("<tr class='nohover'><th class='left'>Tag</th>" 81 html("<tr class='nohover'><th class='left'>Tag</th>"
81 "<th class='left'>Reference</th>" 82 "<th class='left'>Download</th>"
82 "<th class='left'>Author</th>" 83 "<th class='left'>Author</th>"
83 "<th class='left' colspan='2'>Age</th></tr>\n"); 84 "<th class='left' colspan='2'>Age</th></tr>\n");
84 header = 1; 85 header = 1;
85} 86}
86 87
88static void print_tag_downloads(const struct cgit_repo *repo, const char *ref)
89{
90 const struct cgit_snapshot_format* f;
91 char *filename;
92 const char *basename;
93
94 if (!ref || strlen(ref) < 2)
95 return;
96
97 basename = cgit_repobasename(repo->url);
98 if (prefixcmp(ref, basename) != 0) {
99 if ((ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]))
100 ref++;
101 if (isdigit(ref[0]))
102 ref = xstrdup(fmt("%s-%s", basename, ref));
103 }
104
105 for (f = cgit_snapshot_formats; f->suffix; f++) {
106 if (!(repo->snapshots & f->bit))
107 continue;
108 filename = fmt("%s%s", ref, f->suffix);
109 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
110 html("&nbsp;&nbsp;");
111 }
112}
87static int print_tag(struct refinfo *ref) 113static int print_tag(struct refinfo *ref)
88{ 114{
89 struct tag *tag; 115 struct tag *tag;
@@ -98,7 +124,10 @@ static int print_tag(struct refinfo *ref)
98 html("<tr><td>"); 124 html("<tr><td>");
99 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name); 125 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
100 html("</td><td>"); 126 html("</td><td>");
101 cgit_object_link(tag->tagged); 127 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT))
128 print_tag_downloads(ctx.repo, name);
129 else
130 cgit_object_link(tag->tagged);
102 html("</td><td>"); 131 html("</td><td>");
103 if (info->tagger) 132 if (info->tagger)
104 html(info->tagger); 133 html(info->tagger);
@@ -110,9 +139,12 @@ static int print_tag(struct refinfo *ref)
110 if (!header) 139 if (!header)
111 print_tag_header(); 140 print_tag_header();
112 html("<tr><td>"); 141 html("<tr><td>");
113 html_txt(name); 142 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
114 html("</td><td>"); 143 html("</td><td>");
115 cgit_object_link(ref->object); 144 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT))
145 print_tag_downloads(ctx.repo, name);
146 else
147 cgit_object_link(ref->object);
116 html("</td></tr>\n"); 148 html("</td></tr>\n");
117 } 149 }
118 return 0; 150 return 0;
diff --git a/ui-repolist.c b/ui-repolist.c
index 2324273..2c13d50 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -32,21 +32,38 @@ time_t read_agefile(char *path)
32 return 0; 32 return 0;
33} 33}
34 34
35static void print_modtime(struct cgit_repo *repo) 35static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
36{ 36{
37 char *path; 37 char *path;
38 struct stat s; 38 struct stat s;
39 struct cgit_repo *r = (struct cgit_repo *)repo;
39 40
41 if (repo->mtime != -1) {
42 *mtime = repo->mtime;
43 return 1;
44 }
40 path = fmt("%s/%s", repo->path, ctx.cfg.agefile); 45 path = fmt("%s/%s", repo->path, ctx.cfg.agefile);
41 if (stat(path, &s) == 0) { 46 if (stat(path, &s) == 0) {
42 cgit_print_age(read_agefile(path), -1, NULL); 47 *mtime = read_agefile(path);
43 return; 48 r->mtime = *mtime;
49 return 1;
44 } 50 }
45 51
46 path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch); 52 path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch);
47 if (stat(path, &s) != 0) 53 if (stat(path, &s) == 0)
48 return; 54 *mtime = s.st_mtime;
49 cgit_print_age(s.st_mtime, -1, NULL); 55 else
56 *mtime = 0;
57
58 r->mtime = *mtime;
59 return (r->mtime != 0);
60}
61
62static void print_modtime(struct cgit_repo *repo)
63{
64 time_t t;
65 if (get_repo_modtime(repo, &t))
66 cgit_print_age(t, -1, NULL);
50} 67}
51 68
52int is_match(struct cgit_repo *repo) 69int is_match(struct cgit_repo *repo)
@@ -73,13 +90,23 @@ int is_in_url(struct cgit_repo *repo)
73 return 0; 90 return 0;
74} 91}
75 92
93void print_sort_header(const char *title, const char *sort)
94{
95 htmlf("<th class='left'><a href='./?s=%s", sort);
96 if (ctx.qry.search) {
97 html("&q=");
98 html_url_arg(ctx.qry.search);
99 }
100 htmlf("'>%s</a></th>", title);
101}
102
76void print_header(int columns) 103void print_header(int columns)
77{ 104{
78 html("<tr class='nohover'>" 105 html("<tr class='nohover'>");
79 "<th class='left'>Name</th>" 106 print_sort_header("Name", "name");
80 "<th class='left'>Description</th>" 107 print_sort_header("Description", "desc");
81 "<th class='left'>Owner</th>" 108 print_sort_header("Owner", "owner");
82 "<th class='left'>Idle</th>"); 109 print_sort_header("Idle", "idle");
83 if (ctx.cfg.enable_index_links) 110 if (ctx.cfg.enable_index_links)
84 html("<th class='left'>Links</th>"); 111 html("<th class='left'>Links</th>");
85 html("</tr>\n"); 112 html("</tr>\n");
@@ -96,10 +123,86 @@ void print_pager(int items, int pagelen, char *search)
96 html("</div>"); 123 html("</div>");
97} 124}
98 125
126static int cmp(const char *s1, const char *s2)
127{
128 if (s1 && s2)
129 return strcmp(s1, s2);
130 if (s1 && !s2)
131 return -1;
132 if (s2 && !s1)
133 return 1;
134 return 0;
135}
136
137static int sort_name(const void *a, const void *b)
138{
139 const struct cgit_repo *r1 = a;
140 const struct cgit_repo *r2 = b;
141
142 return cmp(r1->name, r2->name);
143}
144
145static int sort_desc(const void *a, const void *b)
146{
147 const struct cgit_repo *r1 = a;
148 const struct cgit_repo *r2 = b;
149
150 return cmp(r1->desc, r2->desc);
151}
152
153static int sort_owner(const void *a, const void *b)
154{
155 const struct cgit_repo *r1 = a;
156 const struct cgit_repo *r2 = b;
157
158 return cmp(r1->owner, r2->owner);
159}
160
161static int sort_idle(const void *a, const void *b)
162{
163 const struct cgit_repo *r1 = a;
164 const struct cgit_repo *r2 = b;
165 time_t t1, t2;
166
167 t1 = t2 = 0;
168 get_repo_modtime(r1, &t1);
169 get_repo_modtime(r2, &t2);
170 return t2 - t1;
171}
172
173struct sortcolumn {
174 const char *name;
175 int (*fn)(const void *a, const void *b);
176};
177
178struct sortcolumn sortcolumn[] = {
179 {"name", sort_name},
180 {"desc", sort_desc},
181 {"owner", sort_owner},
182 {"idle", sort_idle},
183 {NULL, NULL}
184};
185
186int sort_repolist(char *field)
187{
188 struct sortcolumn *column;
189
190 for (column = &sortcolumn[0]; column->name; column++) {
191 if (strcmp(field, column->name))
192 continue;
193 qsort(cgit_repolist.repos, cgit_repolist.count,
194 sizeof(struct cgit_repo), column->fn);
195 return 1;
196 }
197 return 0;
198}
199
200
99void cgit_print_repolist() 201void cgit_print_repolist()
100{ 202{
101 int i, columns = 4, hits = 0, header = 0; 203 int i, columns = 4, hits = 0, header = 0;
102 char *last_group = NULL; 204 char *last_group = NULL;
205 int sorted = 0;
103 206
104 if (ctx.cfg.enable_index_links) 207 if (ctx.cfg.enable_index_links)
105 columns++; 208 columns++;
@@ -112,6 +215,9 @@ void cgit_print_repolist()
112 if (ctx.cfg.index_header) 215 if (ctx.cfg.index_header)
113 html_include(ctx.cfg.index_header); 216 html_include(ctx.cfg.index_header);
114 217
218 if(ctx.qry.sort)
219 sorted = sort_repolist(ctx.qry.sort);
220
115 html("<table summary='repository list' class='list nowrap'>"); 221 html("<table summary='repository list' class='list nowrap'>");
116 for (i=0; i<cgit_repolist.count; i++) { 222 for (i=0; i<cgit_repolist.count; i++) {
117 ctx.repo = &cgit_repolist.repos[i]; 223 ctx.repo = &cgit_repolist.repos[i];
@@ -124,10 +230,11 @@ void cgit_print_repolist()
124 continue; 230 continue;
125 if (!header++) 231 if (!header++)
126 print_header(columns); 232 print_header(columns);
127 if ((last_group == NULL && ctx.repo->group != NULL) || 233 if (!sorted &&
234 ((last_group == NULL && ctx.repo->group != NULL) ||
128 (last_group != NULL && ctx.repo->group == NULL) || 235 (last_group != NULL && ctx.repo->group == NULL) ||
129 (last_group != NULL && ctx.repo->group != NULL && 236 (last_group != NULL && ctx.repo->group != NULL &&
130 strcmp(ctx.repo->group, last_group))) { 237 strcmp(ctx.repo->group, last_group)))) {
131 htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>", 238 htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>",
132 columns); 239 columns);
133 html_txt(ctx.repo->group); 240 html_txt(ctx.repo->group);
@@ -135,7 +242,7 @@ void cgit_print_repolist()
135 last_group = ctx.repo->group; 242 last_group = ctx.repo->group;
136 } 243 }
137 htmlf("<tr><td class='%s'>", 244 htmlf("<tr><td class='%s'>",
138 ctx.repo->group ? "sublevel-repo" : "toplevel-repo"); 245 !sorted && ctx.repo->group ? "sublevel-repo" : "toplevel-repo");
139 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL); 246 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
140 html("</td><td>"); 247 html("</td><td>");
141 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL); 248 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL);
@@ -150,7 +257,7 @@ void cgit_print_repolist()
150 html("<td>"); 257 html("<td>");
151 cgit_summary_link("summary", NULL, "button", NULL); 258 cgit_summary_link("summary", NULL, "button", NULL);
152 cgit_log_link("log", NULL, "button", NULL, NULL, NULL, 259 cgit_log_link("log", NULL, "button", NULL, NULL, NULL,
153 0, NULL, NULL); 260 0, NULL, NULL, ctx.qry.showmsg);
154 cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL); 261 cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
155 html("</td>"); 262 html("</td>");
156 } 263 }
diff --git a/ui-shared.c b/ui-shared.c
index 76cd00d..4f28512 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -281,7 +281,8 @@ void cgit_plain_link(char *name, char *title, char *class, char *head,
281} 281}
282 282
283void cgit_log_link(char *name, char *title, char *class, char *head, 283void cgit_log_link(char *name, char *title, char *class, char *head,
284 char *rev, char *path, int ofs, char *grep, char *pattern) 284 char *rev, char *path, int ofs, char *grep, char *pattern,
285 int showmsg)
285{ 286{
286 char *delim; 287 char *delim;
287 288
@@ -305,6 +306,11 @@ void cgit_log_link(char *name, char *title, char *class, char *head,
305 html(delim); 306 html(delim);
306 html("ofs="); 307 html("ofs=");
307 htmlf("%d", ofs); 308 htmlf("%d", ofs);
309 delim = "&";
310 }
311 if (showmsg) {
312 html(delim);
313 html("showmsg=1");
308 } 314 }
309 html("'>"); 315 html("'>");
310 html_txt(name); 316 html_txt(name);
@@ -363,13 +369,22 @@ void cgit_patch_link(char *name, char *title, char *class, char *head,
363 reporevlink("patch", name, title, class, head, rev, NULL); 369 reporevlink("patch", name, title, class, head, rev, NULL);
364} 370}
365 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
366void cgit_object_link(struct object *obj) 378void cgit_object_link(struct object *obj)
367{ 379{
368 char *page, *rev, *name; 380 char *page, *shortrev, *fullrev, *name;
369 381
382 fullrev = sha1_to_hex(obj->sha1);
383 shortrev = xstrdup(fullrev);
384 shortrev[10] = '\0';
370 if (obj->type == OBJ_COMMIT) { 385 if (obj->type == OBJ_COMMIT) {
371 cgit_commit_link(fmt("commit %s", sha1_to_hex(obj->sha1)), NULL, NULL, 386 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
372 ctx.qry.head, sha1_to_hex(obj->sha1)); 387 ctx.qry.head, fullrev);
373 return; 388 return;
374 } else if (obj->type == OBJ_TREE) 389 } else if (obj->type == OBJ_TREE)
375 page = "tree"; 390 page = "tree";
@@ -377,9 +392,8 @@ void cgit_object_link(struct object *obj)
377 page = "tag"; 392 page = "tag";
378 else 393 else
379 page = "blob"; 394 page = "blob";
380 rev = sha1_to_hex(obj->sha1); 395 name = fmt("%s %s...", typename(obj->type), shortrev);
381 name = fmt("%s %s", typename(obj->type), rev); 396 reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
382 reporevlink(page, name, NULL, NULL, ctx.qry.head, rev, NULL);
383} 397}
384 398
385void cgit_print_date(time_t secs, char *format, int local_time) 399void cgit_print_date(time_t secs, char *format, int local_time)
@@ -549,7 +563,7 @@ int print_archive_ref(const char *refname, const unsigned char *sha1,
549 return 0; 563 return 0;
550} 564}
551 565
552void add_hidden_formfields(int incl_head, int incl_search, char *page) 566void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page)
553{ 567{
554 char *url; 568 char *url;
555 569
@@ -568,6 +582,8 @@ void add_hidden_formfields(int incl_head, int incl_search, char *page)
568 html_hidden("id", ctx.qry.sha1); 582 html_hidden("id", ctx.qry.sha1);
569 if (ctx.qry.sha2) 583 if (ctx.qry.sha2)
570 html_hidden("id2", ctx.qry.sha2); 584 html_hidden("id2", ctx.qry.sha2);
585 if (ctx.qry.showmsg)
586 html_hidden("showmsg", "1");
571 587
572 if (incl_search) { 588 if (incl_search) {
573 if (ctx.qry.grep) 589 if (ctx.qry.grep)
@@ -609,7 +625,7 @@ void cgit_print_pageheader(struct cgit_context *ctx)
609 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 625 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
610 html("</td><td class='form'>"); 626 html("</td><td class='form'>");
611 html("<form method='get' action=''>\n"); 627 html("<form method='get' action=''>\n");
612 add_hidden_formfields(0, 1, ctx->qry.page); 628 cgit_add_hidden_formfields(0, 1, ctx->qry.page);
613 html("<select name='h' onchange='this.form.submit();'>\n"); 629 html("<select name='h' onchange='this.form.submit();'>\n");
614 for_each_branch_ref(print_branch_option, ctx->qry.head); 630 for_each_branch_ref(print_branch_option, ctx->qry.head);
615 html("</select> "); 631 html("</select> ");
@@ -639,13 +655,16 @@ void cgit_print_pageheader(struct cgit_context *ctx)
639 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 655 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
640 ctx->qry.sha1, NULL); 656 ctx->qry.sha1, NULL);
641 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 657 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
642 NULL, NULL, 0, NULL, NULL); 658 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg);
643 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 659 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
644 ctx->qry.sha1, NULL); 660 ctx->qry.sha1, NULL);
645 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 661 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
646 ctx->qry.head, ctx->qry.sha1); 662 ctx->qry.head, ctx->qry.sha1);
647 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 663 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
648 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);
649 if (ctx->repo->readme) 668 if (ctx->repo->readme)
650 reporevlink("about", "about", NULL, 669 reporevlink("about", "about", NULL,
651 hc(cmd, "about"), ctx->qry.head, NULL, 670 hc(cmd, "about"), ctx->qry.head, NULL,
@@ -656,7 +675,7 @@ void cgit_print_pageheader(struct cgit_context *ctx)
656 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 675 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
657 ctx->qry.path, NULL)); 676 ctx->qry.path, NULL));
658 html("'>\n"); 677 html("'>\n");
659 add_hidden_formfields(1, 0, "log"); 678 cgit_add_hidden_formfields(1, 0, "log");
660 html("<select name='qt'>\n"); 679 html("<select name='qt'>\n");
661 html_option("grep", "log msg", ctx->qry.grep); 680 html_option("grep", "log msg", ctx->qry.grep);
662 html_option("author", "author", ctx->qry.grep); 681 html_option("author", "author", ctx->qry.grep);
@@ -712,8 +731,7 @@ void cgit_print_snapshot_links(const char *repo, const char *head,
712 continue; 731 continue;
713 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex, 732 filename = fmt("%s-%s%s", cgit_repobasename(repo), hex,
714 f->suffix); 733 f->suffix);
715 cgit_snapshot_link(filename, NULL, NULL, (char *)head, 734 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
716 (char *)hex, filename);
717 html("<br/>"); 735 html("<br/>");
718 } 736 }
719} 737}
diff --git a/ui-shared.h b/ui-shared.h
index 3c8a6d0..5a3821f 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -19,7 +19,7 @@ extern void cgit_plain_link(char *name, char *title, char *class, char *head,
19 char *rev, char *path); 19 char *rev, char *path);
20extern void cgit_log_link(char *name, char *title, char *class, char *head, 20extern void cgit_log_link(char *name, char *title, char *class, char *head,
21 char *rev, char *path, int ofs, char *grep, 21 char *rev, char *path, int ofs, char *grep,
22 char *pattern); 22 char *pattern, int showmsg);
23extern void cgit_commit_link(char *name, char *title, char *class, char *head, 23extern void cgit_commit_link(char *name, char *title, char *class, char *head,
24 char *rev); 24 char *rev);
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,
@@ -30,6 +30,8 @@ extern 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);
@@ -42,5 +44,6 @@ extern 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 9c4d086..f25613e 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -58,8 +58,8 @@ static int write_tar_bzip2_archive(struct archiver_args *args)
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};
@@ -114,58 +114,53 @@ static int make_snapshot(const struct cgit_snapshot_format *format,
114 return 0; 114 return 0;
115} 115}
116 116
117char *dwim_filename = NULL; 117/* Try to guess the requested revision from the requested snapshot name.
118const char *dwim_refname = NULL; 118 * First the format extension is stripped, e.g. "cgit-0.7.2.tar.gz" become
119 119 * "cgit-0.7.2". If this is a valid commit object name we've got a winner.
120static int ref_cb(const char *refname, const unsigned char *sha1, int flags, 120 * Otherwise, if the snapshot name has a prefix matching the result from
121 void *cb_data) 121 * repo_basename(), we strip the basename and any following '-' and '_'
122{ 122 * characters ("cgit-0.7.2" -> "0.7.2") and check the resulting name once
123 const char *r = refname; 123 * more. If this still isn't a valid commit object name, we check if pre-
124 while (r && *r) { 124 * pending a 'v' to the remaining snapshot name ("0.7.2" -> "v0.7.2") gives
125 fprintf(stderr, " cmp %s with %s:", dwim_filename, r); 125 * us something valid.
126 if (!strcmp(dwim_filename, r)) {
127 fprintf(stderr, "MATCH!\n");
128 dwim_refname = refname;
129 return 1;
130 }
131 fprintf(stderr, "no match\n");
132 if (isdigit(*r))
133 break;
134 r++;
135 }
136 return 0;
137}
138
139/* Try to guess the requested revision by combining repo name and tag name
140 * and comparing this to the requested snapshot name. E.g. the requested
141 * snapshot is "cgit-0.7.2.tar.gz" while repo name is "cgit" and tag name
142 * is "v0.7.2". First, the reponame is stripped off, leaving "-0.7.2.tar.gz".
143 * Next, any '-' and '_' characters are stripped, leaving "0.7.2.tar.gz".
144 * Finally, the requested format suffix is removed and we end up with "0.7.2".
145 * Then we test each tag against this dwimmed filename, and for each tag
146 * we even try to remove any leading characters which are non-digits. I.e.
147 * we first compare with "v0.7.2", then with "0.7.2" and we've got a match.
148 */ 126 */
149static const char *get_ref_from_filename(const char *url, const char *filename, 127static const char *get_ref_from_filename(const char *url, const char *filename,
150 const struct cgit_snapshot_format *fmt) 128 const struct cgit_snapshot_format *format)
151{ 129{
152 const char *reponame = cgit_repobasename(url); 130 const char *reponame;
153 fprintf(stderr, "reponame=%s, filename=%s\n", reponame, filename); 131 unsigned char sha1[20];
154 if (prefixcmp(filename, reponame)) 132 char *snapshot;
155 return NULL; 133
156 filename += strlen(reponame); 134 snapshot = xstrdup(filename);
157 while (filename && (*filename == '-' || *filename == '_')) 135 snapshot[strlen(snapshot) - strlen(format->suffix)] = '\0';
158 filename++; 136 fprintf(stderr, "snapshot=%s\n", snapshot);
159 dwim_filename = xstrdup(filename); 137
160 dwim_filename[strlen(filename) - strlen(fmt->suffix)] = '\0'; 138 if (get_sha1(snapshot, sha1) == 0)
161 for_each_tag_ref(ref_cb, NULL); 139 return snapshot;
162 return dwim_refname; 140
141 reponame = cgit_repobasename(url);
142 fprintf(stderr, "reponame=%s\n", reponame);
143 if (prefixcmp(snapshot, reponame) == 0) {
144 snapshot += strlen(reponame);
145 while (snapshot && (*snapshot == '-' || *snapshot == '_'))
146 snapshot++;
147 }
148
149 if (get_sha1(snapshot, sha1) == 0)
150 return snapshot;
151
152 snapshot = fmt("v%s", snapshot);
153 if (get_sha1(snapshot, sha1) == 0)
154 return snapshot;
155
156 return NULL;
163} 157}
164 158
165void cgit_print_snapshot(const char *head, const char *hex, const char *prefix, 159void cgit_print_snapshot(const char *head, const char *hex,
166 const char *filename, int snapshots, int dwim) 160 const char *filename, int snapshots, int dwim)
167{ 161{
168 const struct cgit_snapshot_format* f; 162 const struct cgit_snapshot_format* f;
163 char *prefix = NULL;
169 164
170 f = get_format(filename); 165 f = get_format(filename);
171 if (!f) { 166 if (!f) {
@@ -178,11 +173,22 @@ void cgit_print_snapshot(const char *head, const char *hex, const char *prefix,
178 return; 173 return;
179 } 174 }
180 175
181 if (!hex && dwim) 176 if (!hex && dwim) {
182 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) {
179 html_status(404, "Not found", 0);
180 return;
181 }
182 prefix = xstrdup(filename);
183 prefix[strlen(filename) - strlen(f->suffix)] = '\0';
184 }
183 185
184 if (!hex) 186 if (!hex)
185 hex = head; 187 hex = head;
186 188
189 if (!prefix)
190 prefix = xstrdup(cgit_repobasename(ctx.repo->url));
191
187 make_snapshot(f, hex, prefix, filename); 192 make_snapshot(f, hex, prefix, filename);
193 free(prefix);
188} 194}
diff --git a/ui-snapshot.h b/ui-snapshot.h
index 3540303..b6ede52 100644
--- a/ui-snapshot.h
+++ b/ui-snapshot.h
@@ -2,7 +2,6 @@
2#define UI_SNAPSHOT_H 2#define UI_SNAPSHOT_H
3 3
4extern void cgit_print_snapshot(const char *head, const char *hex, 4extern void cgit_print_snapshot(const char *head, const char *hex,
5 const char *prefix, const char *filename, 5 const char *filename, int snapshot, int dwim);
6 int snapshot, int dwim);
7 6
8#endif /* UI_SNAPSHOT_H */ 7#endif /* UI_SNAPSHOT_H */
diff --git a/ui-stats.c b/ui-stats.c
new file mode 100644
index 0000000..9fc06d3
--- /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
--- /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 54b9f4c..0e056e0 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -75,6 +75,15 @@ void cgit_print_tag(char *revname)
75 html("</td></tr>\n"); 75 html("</td></tr>\n");
76 html("</table>\n"); 76 html("</table>\n");
77 print_tag_content(info->msg); 77 print_tag_content(info->msg);
78 } 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");
87 }
79 return; 88 return;
80} 89}
diff --git a/ui-tree.c b/ui-tree.c
index 4bf372a..c26ba4c 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -18,8 +18,8 @@ static void print_object(const unsigned char *sha1, char *path)
18{ 18{
19 enum object_type type; 19 enum object_type type;
20 char *buf; 20 char *buf;
21 unsigned long size, lineno, start, idx; 21 unsigned long size, lineno, idx;
22 const char *linefmt = "<tr><td class='no'><a id='n%1$d' name='n%1$d' href='#n%1$d'>%1$d</a></td><td class='txt'>"; 22 const char *numberfmt = "<a class='no' id='n%1$d' name='n%1$d' href='#n%1$d'>%1$d</a>\n";
23 23
24 type = sha1_object_info(sha1, &size); 24 type = sha1_object_info(sha1, &size);
25 if (type == OBJ_BAD) { 25 if (type == OBJ_BAD) {
@@ -38,27 +38,28 @@ static void print_object(const unsigned char *sha1, char *path)
38 html(" ("); 38 html(" (");
39 cgit_plain_link("plain", NULL, NULL, ctx.qry.head, 39 cgit_plain_link("plain", NULL, NULL, ctx.qry.head,
40 curr_rev, path); 40 curr_rev, path);
41 htmlf(")<br/>blob: %s", sha1_to_hex(sha1)); 41 htmlf(")<br/>blob: %s\n", sha1_to_hex(sha1));
42 42
43 html("<table summary='blob content' class='blob'>\n"); 43 html("<table summary='blob content' class='blob'>\n");
44 html("<tr>\n");
45
46 html("<td class='linenumbers'><pre>");
44 idx = 0; 47 idx = 0;
45 start = 0;
46 lineno = 0; 48 lineno = 0;
47 while(idx < size) { 49 htmlf(numberfmt, ++lineno);
50 while(idx < size - 1) { // skip absolute last newline
48 if (buf[idx] == '\n') { 51 if (buf[idx] == '\n') {
49 buf[idx] = '\0'; 52 htmlf(numberfmt, ++lineno);
50 htmlf(linefmt, ++lineno);
51 html_txt(buf + start);
52 html("</td></tr>\n");
53 start = idx + 1;
54 } 53 }
55 idx++; 54 idx++;
56 } 55 }
57 if (start < idx) { 56 html("</pre></td>\n");
58 htmlf(linefmt, ++lineno); 57
59 html_txt(buf + start); 58 html("<td class='lines'><pre><code>");
60 } 59 html_txt(buf);
61 html("</td></tr>\n"); 60 html("</code></pre></td>\n");
61
62 html("</tr>\n");
62 html("</table>\n"); 63 html("</table>\n");
63} 64}
64 65
@@ -108,7 +109,10 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
108 109
109 html("<td>"); 110 html("<td>");
110 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, 111 cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev,
111 fullpath, 0, NULL, NULL); 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);
112 html("</td></tr>\n"); 116 html("</td></tr>\n");
113 free(name); 117 free(name);
114 return 0; 118 return 0;