summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (show whitespace changes)
-rw-r--r--Makefile3
-rw-r--r--cgit.c31
-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.c38
-rw-r--r--ui-repolist.c137
-rw-r--r--ui-shared.c44
-rw-r--r--ui-shared.h7
-rw-r--r--ui-snapshot.c98
-rw-r--r--ui-snapshot.h3
-rw-r--r--ui-stats.c410
-rw-r--r--ui-stats.h27
-rw-r--r--ui-tag.c9
-rw-r--r--ui-tree.c36
24 files changed, 992 insertions, 142 deletions
diff --git a/Makefile b/Makefile
index 52b3586..19e3bb8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,20 @@
1CGIT_VERSION = v0.8.1.1 1CGIT_VERSION = v0.8.1.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_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
12# Define NO_STRCASESTR if you don't have strcasestr. 12# Define NO_STRCASESTR if you don't have strcasestr.
13# 13#
14# 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).
15# 15#
16 16
17#-include config.mak 17#-include config.mak
18 18
19# 19#
20# Platform specific tweaks 20# Platform specific tweaks
@@ -81,24 +81,25 @@ OBJECTS += shared.o
81OBJECTS += ui-atom.o 81OBJECTS += ui-atom.o
82OBJECTS += ui-blob.o 82OBJECTS += ui-blob.o
83OBJECTS += ui-clone.o 83OBJECTS += ui-clone.o
84OBJECTS += ui-commit.o 84OBJECTS += ui-commit.o
85OBJECTS += ui-diff.o 85OBJECTS += ui-diff.o
86OBJECTS += ui-log.o 86OBJECTS += ui-log.o
87OBJECTS += ui-patch.o 87OBJECTS += ui-patch.o
88OBJECTS += ui-plain.o 88OBJECTS += ui-plain.o
89OBJECTS += ui-refs.o 89OBJECTS += ui-refs.o
90OBJECTS += ui-repolist.o 90OBJECTS += ui-repolist.o
91OBJECTS += ui-shared.o 91OBJECTS += ui-shared.o
92OBJECTS += ui-snapshot.o 92OBJECTS += ui-snapshot.o
93OBJECTS += ui-stats.o
93OBJECTS += ui-summary.o 94OBJECTS += ui-summary.o
94OBJECTS += ui-tag.o 95OBJECTS += ui-tag.o
95OBJECTS += ui-tree.o 96OBJECTS += ui-tree.o
96 97
97ifdef NEEDS_LIBICONV 98ifdef NEEDS_LIBICONV
98 EXTLIBS += -liconv 99 EXTLIBS += -liconv
99endif 100endif
100 101
101 102
102.PHONY: all libgit test install uninstall clean force-version get-git 103.PHONY: all libgit test install uninstall clean force-version get-git
103 104
104all: cgit 105all: cgit
diff --git a/cgit.c b/cgit.c
index 6e5215e..608cab6 100644
--- a/cgit.c
+++ b/cgit.c
@@ -3,24 +3,25 @@
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "cache.h" 10#include "cache.h"
11#include "cmd.h" 11#include "cmd.h"
12#include "configfile.h" 12#include "configfile.h"
13#include "html.h" 13#include "html.h"
14#include "ui-shared.h" 14#include "ui-shared.h"
15#include "ui-stats.h"
15#include "scan-tree.h" 16#include "scan-tree.h"
16 17
17const char *cgit_version = CGIT_VERSION; 18const char *cgit_version = CGIT_VERSION;
18 19
19void config_cb(const char *name, const char *value) 20void config_cb(const char *name, const char *value)
20{ 21{
21 if (!strcmp(name, "root-title")) 22 if (!strcmp(name, "root-title"))
22 ctx.cfg.root_title = xstrdup(value); 23 ctx.cfg.root_title = xstrdup(value);
23 else if (!strcmp(name, "root-desc")) 24 else if (!strcmp(name, "root-desc"))
24 ctx.cfg.root_desc = xstrdup(value); 25 ctx.cfg.root_desc = xstrdup(value);
25 else if (!strcmp(name, "root-readme")) 26 else if (!strcmp(name, "root-readme"))
26 ctx.cfg.root_readme = xstrdup(value); 27 ctx.cfg.root_readme = xstrdup(value);
@@ -45,24 +46,26 @@ void config_cb(const char *name, const char *value)
45 if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 46 if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
46 ctx.cfg.virtual_root = ""; 47 ctx.cfg.virtual_root = "";
47 } else if (!strcmp(name, "nocache")) 48 } else if (!strcmp(name, "nocache"))
48 ctx.cfg.nocache = atoi(value); 49 ctx.cfg.nocache = atoi(value);
49 else if (!strcmp(name, "snapshots")) 50 else if (!strcmp(name, "snapshots"))
50 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 51 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
51 else if (!strcmp(name, "enable-index-links")) 52 else if (!strcmp(name, "enable-index-links"))
52 ctx.cfg.enable_index_links = atoi(value); 53 ctx.cfg.enable_index_links = atoi(value);
53 else if (!strcmp(name, "enable-log-filecount")) 54 else if (!strcmp(name, "enable-log-filecount"))
54 ctx.cfg.enable_log_filecount = atoi(value); 55 ctx.cfg.enable_log_filecount = atoi(value);
55 else if (!strcmp(name, "enable-log-linecount")) 56 else if (!strcmp(name, "enable-log-linecount"))
56 ctx.cfg.enable_log_linecount = atoi(value); 57 ctx.cfg.enable_log_linecount = atoi(value);
58 else if (!strcmp(name, "max-stats"))
59 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
57 else if (!strcmp(name, "cache-size")) 60 else if (!strcmp(name, "cache-size"))
58 ctx.cfg.cache_size = atoi(value); 61 ctx.cfg.cache_size = atoi(value);
59 else if (!strcmp(name, "cache-root")) 62 else if (!strcmp(name, "cache-root"))
60 ctx.cfg.cache_root = xstrdup(value); 63 ctx.cfg.cache_root = xstrdup(value);
61 else if (!strcmp(name, "cache-root-ttl")) 64 else if (!strcmp(name, "cache-root-ttl"))
62 ctx.cfg.cache_root_ttl = atoi(value); 65 ctx.cfg.cache_root_ttl = atoi(value);
63 else if (!strcmp(name, "cache-repo-ttl")) 66 else if (!strcmp(name, "cache-repo-ttl"))
64 ctx.cfg.cache_repo_ttl = atoi(value); 67 ctx.cfg.cache_repo_ttl = atoi(value);
65 else if (!strcmp(name, "cache-static-ttl")) 68 else if (!strcmp(name, "cache-static-ttl"))
66 ctx.cfg.cache_static_ttl = atoi(value); 69 ctx.cfg.cache_static_ttl = atoi(value);
67 else if (!strcmp(name, "cache-dynamic-ttl")) 70 else if (!strcmp(name, "cache-dynamic-ttl"))
68 ctx.cfg.cache_dynamic_ttl = atoi(value); 71 ctx.cfg.cache_dynamic_ttl = atoi(value);
@@ -103,24 +106,26 @@ void config_cb(const char *name, const char *value)
103 else if (ctx.repo && !strcmp(name, "repo.desc")) 106 else if (ctx.repo && !strcmp(name, "repo.desc"))
104 ctx.repo->desc = xstrdup(value); 107 ctx.repo->desc = xstrdup(value);
105 else if (ctx.repo && !strcmp(name, "repo.owner")) 108 else if (ctx.repo && !strcmp(name, "repo.owner"))
106 ctx.repo->owner = xstrdup(value); 109 ctx.repo->owner = xstrdup(value);
107 else if (ctx.repo && !strcmp(name, "repo.defbranch")) 110 else if (ctx.repo && !strcmp(name, "repo.defbranch"))
108 ctx.repo->defbranch = xstrdup(value); 111 ctx.repo->defbranch = xstrdup(value);
109 else if (ctx.repo && !strcmp(name, "repo.snapshots")) 112 else if (ctx.repo && !strcmp(name, "repo.snapshots"))
110 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */ 113 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
111 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount")) 114 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
112 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); 115 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
113 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) 116 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
114 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); 117 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
118 else if (ctx.repo && !strcmp(name, "repo.max-stats"))
119 ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
115 else if (ctx.repo && !strcmp(name, "repo.module-link")) 120 else if (ctx.repo && !strcmp(name, "repo.module-link"))
116 ctx.repo->module_link= xstrdup(value); 121 ctx.repo->module_link= xstrdup(value);
117 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { 122 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
118 if (*value == '/') 123 if (*value == '/')
119 ctx.repo->readme = xstrdup(value); 124 ctx.repo->readme = xstrdup(value);
120 else 125 else
121 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value)); 126 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
122 } else if (!strcmp(name, "include")) 127 } else if (!strcmp(name, "include"))
123 parse_configfile(value, config_cb); 128 parse_configfile(value, config_cb);
124} 129}
125 130
126static void querystring_cb(const char *name, const char *value) 131static void querystring_cb(const char *name, const char *value)
@@ -145,47 +150,54 @@ static void querystring_cb(const char *name, const char *value)
145 ctx.qry.has_sha1 = 1; 150 ctx.qry.has_sha1 = 1;
146 } else if (!strcmp(name, "id2")) { 151 } else if (!strcmp(name, "id2")) {
147 ctx.qry.sha2 = xstrdup(value); 152 ctx.qry.sha2 = xstrdup(value);
148 ctx.qry.has_sha1 = 1; 153 ctx.qry.has_sha1 = 1;
149 } else if (!strcmp(name, "ofs")) { 154 } else if (!strcmp(name, "ofs")) {
150 ctx.qry.ofs = atoi(value); 155 ctx.qry.ofs = atoi(value);
151 } else if (!strcmp(name, "path")) { 156 } else if (!strcmp(name, "path")) {
152 ctx.qry.path = trim_end(value, '/'); 157 ctx.qry.path = trim_end(value, '/');
153 } else if (!strcmp(name, "name")) { 158 } else if (!strcmp(name, "name")) {
154 ctx.qry.name = xstrdup(value); 159 ctx.qry.name = xstrdup(value);
155 } else if (!strcmp(name, "mimetype")) { 160 } else if (!strcmp(name, "mimetype")) {
156 ctx.qry.mimetype = xstrdup(value); 161 ctx.qry.mimetype = xstrdup(value);
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
160static void prepare_context(struct cgit_context *ctx) 171static void prepare_context(struct cgit_context *ctx)
161{ 172{
162 memset(ctx, 0, sizeof(ctx)); 173 memset(ctx, 0, sizeof(ctx));
163 ctx->cfg.agefile = "info/web/last-modified"; 174 ctx->cfg.agefile = "info/web/last-modified";
164 ctx->cfg.nocache = 0; 175 ctx->cfg.nocache = 0;
165 ctx->cfg.cache_size = 0; 176 ctx->cfg.cache_size = 0;
166 ctx->cfg.cache_dynamic_ttl = 5; 177 ctx->cfg.cache_dynamic_ttl = 5;
167 ctx->cfg.cache_max_create_time = 5; 178 ctx->cfg.cache_max_create_time = 5;
168 ctx->cfg.cache_repo_ttl = 5; 179 ctx->cfg.cache_repo_ttl = 5;
169 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 180 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
170 ctx->cfg.cache_root_ttl = 5; 181 ctx->cfg.cache_root_ttl = 5;
171 ctx->cfg.cache_static_ttl = -1; 182 ctx->cfg.cache_static_ttl = -1;
172 ctx->cfg.css = "/cgit.css"; 183 ctx->cfg.css = "/cgit.css";
173 ctx->cfg.logo = "/git-logo.png"; 184 ctx->cfg.logo = "/git-logo.png";
174 ctx->cfg.local_time = 0; 185 ctx->cfg.local_time = 0;
175 ctx->cfg.max_repo_count = 50; 186 ctx->cfg.max_repo_count = 50;
176 ctx->cfg.max_commit_count = 50; 187 ctx->cfg.max_commit_count = 50;
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";
183 ctx->cfg.root_title = "Git repository browser"; 195 ctx->cfg.root_title = "Git repository browser";
184 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 196 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
185 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 197 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
186 ctx->cfg.summary_branches = 10; 198 ctx->cfg.summary_branches = 10;
187 ctx->cfg.summary_log = 10; 199 ctx->cfg.summary_log = 10;
188 ctx->cfg.summary_tags = 10; 200 ctx->cfg.summary_tags = 10;
189 ctx->page.mimetype = "text/html"; 201 ctx->page.mimetype = "text/html";
190 ctx->page.charset = PAGE_ENCODING; 202 ctx->page.charset = PAGE_ENCODING;
191 ctx->page.filename = NULL; 203 ctx->page.filename = NULL;
@@ -425,47 +437,48 @@ int main(int argc, const char **argv)
425 cgit_repolist.repos = NULL; 437 cgit_repolist.repos = NULL;
426 438
427 if (getenv("SCRIPT_NAME")) 439 if (getenv("SCRIPT_NAME"))
428 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME")); 440 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME"));
429 if (getenv("QUERY_STRING")) 441 if (getenv("QUERY_STRING"))
430 ctx.qry.raw = xstrdup(getenv("QUERY_STRING")); 442 ctx.qry.raw = xstrdup(getenv("QUERY_STRING"));
431 cgit_parse_args(argc, argv); 443 cgit_parse_args(argc, argv);
432 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG, 444 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG,
433 config_cb); 445 config_cb);
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;
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 */
446 path = getenv("PATH_INFO"); 460 path = getenv("PATH_INFO");
447 if (path) { 461 if (!ctx.qry.url && path) {
448 if (path[0] == '/') 462 if (path[0] == '/')
449 path++; 463 path++;
450 ctx.qry.url = xstrdup(path); 464 ctx.qry.url = xstrdup(path);
451 if (ctx.qry.raw) { 465 if (ctx.qry.raw) {
452 qry = ctx.qry.raw; 466 qry = ctx.qry.raw;
453 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 467 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
454 free(qry); 468 free(qry);
455 } else 469 } else
456 ctx.qry.raw = ctx.qry.url; 470 ctx.qry.raw = ctx.qry.url;
457 cgit_parse_url(ctx.qry.url); 471 cgit_parse_url(ctx.qry.url);
458 } 472 }
459 }
460 473
461 ttl = calc_ttl(); 474 ttl = calc_ttl();
462 ctx.page.expires += ttl*60; 475 ctx.page.expires += ttl*60;
463 if (ctx.cfg.nocache) 476 if (ctx.cfg.nocache)
464 ctx.cfg.cache_size = 0; 477 ctx.cfg.cache_size = 0;
465 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 478 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
466 ctx.qry.raw, ttl, process_request, &ctx); 479 ctx.qry.raw, ttl, process_request, &ctx);
467 if (err) 480 if (err)
468 cgit_print_error(fmt("Error processing page: %s (%d)", 481 cgit_print_error(fmt("Error processing page: %s (%d)",
469 strerror(err), err)); 482 strerror(err), err));
470 return err; 483 return err;
471} 484}
diff --git a/cgit.css b/cgit.css
index a37d218..957d5aa 100644
--- a/cgit.css
+++ b/cgit.css
@@ -111,47 +111,62 @@ div.content {
111 111
112 112
113table.list { 113table.list {
114 width: 100%; 114 width: 100%;
115 border: none; 115 border: none;
116 border-collapse: collapse; 116 border-collapse: collapse;
117} 117}
118 118
119table.list tr { 119table.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}
126 130
127table.list tr.nohover:hover { 131table.list tr.nohover:hover {
128 background: white; 132 background: white;
129} 133}
130 134
131table.list th { 135table.list th {
132 font-weight: bold; 136 font-weight: bold;
133 /* color: #888; 137 /* color: #888;
134 border-top: dashed 1px #888; 138 border-top: dashed 1px #888;
135 border-bottom: dashed 1px #888; 139 border-bottom: dashed 1px #888;
136 */ 140 */
137 padding: 0.1em 0.5em 0.05em 0.5em; 141 padding: 0.1em 0.5em 0.05em 0.5em;
138 vertical-align: baseline; 142 vertical-align: baseline;
139} 143}
140 144
141table.list td { 145table.list td {
142 border: none; 146 border: none;
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}
149 164
150table.list td a:hover { 165table.list td a:hover {
151 color: #00f; 166 color: #00f;
152} 167}
153 168
154img { 169img {
155 border: none; 170 border: none;
156} 171}
157 172
@@ -212,44 +227,49 @@ td.ls-size {
212} 227}
213 228
214td.ls-mode { 229td.ls-mode {
215 font-family: monospace; 230 font-family: monospace;
216 width: 10em; 231 width: 10em;
217} 232}
218 233
219table.blob { 234table.blob {
220 margin-top: 0.5em; 235 margin-top: 0.5em;
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 {
247 white-space: nowrap; 267 white-space: nowrap;
248} 268}
249 269
250table.commit-info { 270table.commit-info {
251 border-collapse: collapse; 271 border-collapse: collapse;
252 margin-top: 1.5em; 272 margin-top: 1.5em;
253} 273}
254 274
255table.commit-info th { 275table.commit-info th {
@@ -447,12 +467,118 @@ span.age-months {
447 color: #888; 467 color: #888;
448} 468}
449 469
450span.age-years { 470span.age-years {
451 color: #bbb; 471 color: #bbb;
452} 472}
453div.footer { 473div.footer {
454 margin-top: 0.5em; 474 margin-top: 0.5em;
455 text-align: center; 475 text-align: center;
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
@@ -52,24 +52,26 @@ struct cgit_repo {
52 char *name; 52 char *name;
53 char *path; 53 char *path;
54 char *desc; 54 char *desc;
55 char *owner; 55 char *owner;
56 char *defbranch; 56 char *defbranch;
57 char *group; 57 char *group;
58 char *module_link; 58 char *module_link;
59 char *readme; 59 char *readme;
60 char *clone_url; 60 char *clone_url;
61 int snapshots; 61 int snapshots;
62 int enable_log_filecount; 62 int enable_log_filecount;
63 int enable_log_linecount; 63 int enable_log_linecount;
64 int max_stats;
65 time_t mtime;
64}; 66};
65 67
66struct cgit_repolist { 68struct cgit_repolist {
67 int length; 69 int length;
68 int count; 70 int count;
69 struct cgit_repo *repos; 71 struct cgit_repo *repos;
70}; 72};
71 73
72struct commitinfo { 74struct commitinfo {
73 struct commit *commit; 75 struct commit *commit;
74 char *author; 76 char *author;
75 char *author_email; 77 char *author_email;
@@ -110,26 +112,29 @@ struct cgit_query {
110 char *raw; 112 char *raw;
111 char *repo; 113 char *repo;
112 char *page; 114 char *page;
113 char *search; 115 char *search;
114 char *grep; 116 char *grep;
115 char *head; 117 char *head;
116 char *sha1; 118 char *sha1;
117 char *sha2; 119 char *sha2;
118 char *path; 120 char *path;
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 {
127 char *agefile; 132 char *agefile;
128 char *cache_root; 133 char *cache_root;
129 char *clone_prefix; 134 char *clone_prefix;
130 char *css; 135 char *css;
131 char *favicon; 136 char *favicon;
132 char *footer; 137 char *footer;
133 char *index_header; 138 char *index_header;
134 char *index_info; 139 char *index_info;
135 char *logo; 140 char *logo;
@@ -148,24 +153,25 @@ struct cgit_config {
148 int cache_repo_ttl; 153 int cache_repo_ttl;
149 int cache_root_ttl; 154 int cache_root_ttl;
150 int cache_static_ttl; 155 int cache_static_ttl;
151 int enable_index_links; 156 int enable_index_links;
152 int enable_log_filecount; 157 int enable_log_filecount;
153 int enable_log_linecount; 158 int enable_log_linecount;
154 int local_time; 159 int local_time;
155 int max_repo_count; 160 int max_repo_count;
156 int max_commit_count; 161 int max_commit_count;
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;
163 int summary_branches; 169 int summary_branches;
164 int summary_log; 170 int summary_log;
165 int summary_tags; 171 int summary_tags;
166}; 172};
167 173
168struct cgit_page { 174struct cgit_page {
169 time_t modified; 175 time_t modified;
170 time_t expires; 176 time_t expires;
171 size_t size; 177 size_t size;
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index ab9ab66..09f56a6 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -120,24 +120,29 @@ max-commit-count
120max-message-length 120max-message-length
121 Specifies the maximum number of commit message characters to display in 121 Specifies the maximum number of commit message characters to display in
122 "log" view. Default value: "80". 122 "log" view. Default value: "80".
123 123
124max-repo-count 124max-repo-count
125 Specifies the number of entries to list per page on therepository 125 Specifies the number of entries to list per page on therepository
126 index page. Default value: "50". 126 index page. Default value: "50".
127 127
128max-repodesc-length 128max-repodesc-length
129 Specifies the maximum number of repo description characters to display 129 Specifies the maximum number of repo description characters to display
130 on the repository index page. Default value: "80". 130 on the repository index page. Default value: "80".
131 131
132max-stats
133 Set the default maximum statistics period. Valid values are "week",
134 "month", "quarter" and "year". If unspecified, statistics are
135 disabled. Default value: none. See also: "repo.max-stats".
136
132module-link 137module-link
133 Text which will be used as the formatstring for a hyperlink when a 138 Text which will be used as the formatstring for a hyperlink when a
134 submodule is printed in a directory listing. The arguments for the 139 submodule is printed in a directory listing. The arguments for the
135 formatstring are the path and SHA1 of the submodule commit. Default 140 formatstring are the path and SHA1 of the submodule commit. Default
136 value: "./?repo=%s&page=commit&id=%s" 141 value: "./?repo=%s&page=commit&id=%s"
137 142
138nocache 143nocache
139 If set to the value "1" caching will be disabled. This settings is 144 If set to the value "1" caching will be disabled. This settings is
140 deprecated, and will not be honored starting with cgit-1.0. Default 145 deprecated, and will not be honored starting with cgit-1.0. Default
141 value: "0". 146 value: "0".
142 147
143renamelimit 148renamelimit
@@ -209,24 +214,29 @@ repo.defbranch
209 214
210repo.desc 215repo.desc
211 The value to show as repository description. Default value: none. 216 The value to show as repository description. Default value: none.
212 217
213repo.enable-log-filecount 218repo.enable-log-filecount
214 A flag which can be used to disable the global setting 219 A flag which can be used to disable the global setting
215 `enable-log-filecount'. Default value: none. 220 `enable-log-filecount'. Default value: none.
216 221
217repo.enable-log-linecount 222repo.enable-log-linecount
218 A flag which can be used to disable the global setting 223 A flag which can be used to disable the global setting
219 `enable-log-linecount'. Default value: none. 224 `enable-log-linecount'. Default value: none.
220 225
226repo.max-stats
227 Override the default maximum statistics period. Valid values are equal
228 to the values specified for the global "max-stats" setting. Default
229 value: none.
230
221repo.name 231repo.name
222 The value to show as repository name. Default value: <repo.url>. 232 The value to show as repository name. Default value: <repo.url>.
223 233
224repo.owner 234repo.owner
225 A value used to identify the owner of the repository. Default value: 235 A value used to identify the owner of the repository. Default value:
226 none. 236 none.
227 237
228repo.path 238repo.path
229 An absolute path to the repository directory. For non-bare repositories 239 An absolute path to the repository directory. For non-bare repositories
230 this is the .git-directory. Default value: none. 240 this is the .git-directory. Default value: none.
231 241
232repo.readme 242repo.readme
@@ -267,24 +277,28 @@ enable-log-filecount=1
267# Show number of added/removed lines per commit on the log pages 277# Show number of added/removed lines per commit on the log pages
268enable-log-linecount=1 278enable-log-linecount=1
269 279
270 280
271# Add a cgit favicon 281# Add a cgit favicon
272favicon=/favicon.ico 282favicon=/favicon.ico
273 283
274 284
275# Use a custom logo 285# Use a custom logo
276logo=/img/mylogo.png 286logo=/img/mylogo.png
277 287
278 288
289# Enable statistics per week, month and quarter
290max-stats=quarter
291
292
279# Set the title and heading of the repository index page 293# Set the title and heading of the repository index page
280root-title=foobar.com git repositories 294root-title=foobar.com git repositories
281 295
282 296
283# Set a subheading for the repository index page 297# Set a subheading for the repository index page
284root-desc=tracking the foobar development 298root-desc=tracking the foobar development
285 299
286 300
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
@@ -339,24 +353,27 @@ repo.desc=the dscm
339 353
340 354
341repo.url=linux 355repo.url=linux
342repo.path=/pub/git/linux.git 356repo.path=/pub/git/linux.git
343repo.desc=the kernel 357repo.desc=the kernel
344 358
345# Disable adhoc downloads of this repo 359# Disable adhoc downloads of this repo
346repo.snapshots=0 360repo.snapshots=0
347 361
348# Disable line-counts for this repo 362# Disable line-counts for this repo
349repo.enable-log-linecount=0 363repo.enable-log-linecount=0
350 364
365# Restrict the max statistics period for this repo
366repo.max-stats=month
367
351 368
352BUGS 369BUGS
353---- 370----
354Comments currently cannot appear on the same line as a setting; the comment 371Comments currently cannot appear on the same line as a setting; the comment
355will be included as part of the value. E.g. this line: 372will be included as part of the value. E.g. this line:
356 373
357 robots=index # allow indexing 374 robots=index # allow indexing
358 375
359will generate the following html element: 376will generate the following html element:
360 377
361 <meta name='robots' content='index # allow indexing'/> 378 <meta name='robots' content='index # allow indexing'/>
362 379
diff --git a/cmd.c b/cmd.c
index 5b3c14c..cf97da7 100644
--- a/cmd.c
+++ b/cmd.c
@@ -12,24 +12,25 @@
12#include "ui-shared.h" 12#include "ui-shared.h"
13#include "ui-atom.h" 13#include "ui-atom.h"
14#include "ui-blob.h" 14#include "ui-blob.h"
15#include "ui-clone.h" 15#include "ui-clone.h"
16#include "ui-commit.h" 16#include "ui-commit.h"
17#include "ui-diff.h" 17#include "ui-diff.h"
18#include "ui-log.h" 18#include "ui-log.h"
19#include "ui-patch.h" 19#include "ui-patch.h"
20#include "ui-plain.h" 20#include "ui-plain.h"
21#include "ui-refs.h" 21#include "ui-refs.h"
22#include "ui-repolist.h" 22#include "ui-repolist.h"
23#include "ui-snapshot.h" 23#include "ui-snapshot.h"
24#include "ui-stats.h"
24#include "ui-summary.h" 25#include "ui-summary.h"
25#include "ui-tag.h" 26#include "ui-tag.h"
26#include "ui-tree.h" 27#include "ui-tree.h"
27 28
28static void HEAD_fn(struct cgit_context *ctx) 29static void HEAD_fn(struct cgit_context *ctx)
29{ 30{
30 cgit_clone_head(ctx); 31 cgit_clone_head(ctx);
31} 32}
32 33
33static void atom_fn(struct cgit_context *ctx) 34static void atom_fn(struct cgit_context *ctx)
34{ 35{
35 cgit_print_atom(ctx->qry.head, ctx->qry.path, 10); 36 cgit_print_atom(ctx->qry.head, ctx->qry.path, 10);
@@ -95,29 +96,33 @@ static void patch_fn(struct cgit_context *ctx)
95static void plain_fn(struct cgit_context *ctx) 96static void plain_fn(struct cgit_context *ctx)
96{ 97{
97 cgit_print_plain(ctx); 98 cgit_print_plain(ctx);
98} 99}
99 100
100static void refs_fn(struct cgit_context *ctx) 101static void refs_fn(struct cgit_context *ctx)
101{ 102{
102 cgit_print_refs(); 103 cgit_print_refs();
103} 104}
104 105
105static void snapshot_fn(struct cgit_context *ctx) 106static void snapshot_fn(struct cgit_context *ctx)
106{ 107{
107 cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, 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();
115} 120}
116 121
117static void tag_fn(struct cgit_context *ctx) 122static void tag_fn(struct cgit_context *ctx)
118{ 123{
119 cgit_print_tag(ctx->qry.sha1); 124 cgit_print_tag(ctx->qry.sha1);
120} 125}
121 126
122static void tree_fn(struct cgit_context *ctx) 127static void tree_fn(struct cgit_context *ctx)
123{ 128{
@@ -136,24 +141,25 @@ struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx)
136 def_cmd(blob, 1, 0), 141 def_cmd(blob, 1, 0),
137 def_cmd(commit, 1, 1), 142 def_cmd(commit, 1, 1),
138 def_cmd(diff, 1, 1), 143 def_cmd(diff, 1, 1),
139 def_cmd(info, 1, 0), 144 def_cmd(info, 1, 0),
140 def_cmd(log, 1, 1), 145 def_cmd(log, 1, 1),
141 def_cmd(ls_cache, 0, 0), 146 def_cmd(ls_cache, 0, 0),
142 def_cmd(objects, 1, 0), 147 def_cmd(objects, 1, 0),
143 def_cmd(patch, 1, 0), 148 def_cmd(patch, 1, 0),
144 def_cmd(plain, 1, 0), 149 def_cmd(plain, 1, 0),
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),
151 }; 157 };
152 int i; 158 int i;
153 159
154 if (ctx->qry.page == NULL) { 160 if (ctx->qry.page == NULL) {
155 if (ctx->repo) 161 if (ctx->repo)
156 ctx->qry.page = "summary"; 162 ctx->qry.page = "summary";
157 else 163 else
158 ctx->qry.page = "repolist"; 164 ctx->qry.page = "repolist";
159 } 165 }
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
@@ -49,26 +49,28 @@ struct cgit_repo *cgit_add_repo(const char *url)
49 49
50 ret = &cgit_repolist.repos[cgit_repolist.count-1]; 50 ret = &cgit_repolist.repos[cgit_repolist.count-1];
51 ret->url = trim_end(url, '/'); 51 ret->url = trim_end(url, '/');
52 ret->name = ret->url; 52 ret->name = ret->url;
53 ret->path = NULL; 53 ret->path = NULL;
54 ret->desc = "[no description]"; 54 ret->desc = "[no description]";
55 ret->owner = NULL; 55 ret->owner = NULL;
56 ret->group = ctx.cfg.repo_group; 56 ret->group = ctx.cfg.repo_group;
57 ret->defbranch = "master"; 57 ret->defbranch = "master";
58 ret->snapshots = ctx.cfg.snapshots; 58 ret->snapshots = ctx.cfg.snapshots;
59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
61 ret->max_stats = ctx.cfg.max_stats;
61 ret->module_link = ctx.cfg.module_link; 62 ret->module_link = ctx.cfg.module_link;
62 ret->readme = NULL; 63 ret->readme = NULL;
64 ret->mtime = -1;
63 return ret; 65 return ret;
64} 66}
65 67
66struct cgit_repo *cgit_get_repoinfo(const char *url) 68struct cgit_repo *cgit_get_repoinfo(const char *url)
67{ 69{
68 int i; 70 int i;
69 struct cgit_repo *repo; 71 struct cgit_repo *repo;
70 72
71 for (i=0; i<cgit_repolist.count; i++) { 73 for (i=0; i<cgit_repolist.count; i++) {
72 repo = &cgit_repolist.repos[i]; 74 repo = &cgit_repolist.repos[i];
73 if (!strcmp(repo->url, url)) 75 if (!strcmp(repo->url, url))
74 return repo; 76 return repo;
@@ -257,28 +259,30 @@ int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf)
257int cgit_diff_files(const unsigned char *old_sha1, 259int cgit_diff_files(const unsigned char *old_sha1,
258 const unsigned char *new_sha1, 260 const unsigned char *new_sha1,
259 linediff_fn fn) 261 linediff_fn fn)
260{ 262{
261 mmfile_t file1, file2; 263 mmfile_t file1, file2;
262 xpparam_t diff_params; 264 xpparam_t diff_params;
263 xdemitconf_t emit_params; 265 xdemitconf_t emit_params;
264 xdemitcb_t emit_cb; 266 xdemitcb_t emit_cb;
265 267
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);
276 return 0; 280 return 0;
277} 281}
278 282
279void cgit_diff_tree(const unsigned char *old_sha1, 283void cgit_diff_tree(const unsigned char *old_sha1,
280 const unsigned char *new_sha1, 284 const unsigned char *new_sha1,
281 filepair_fn fn, const char *prefix) 285 filepair_fn fn, const char *prefix)
282{ 286{
283 struct diff_options opt; 287 struct diff_options opt;
284 int ret; 288 int ret;
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
@@ -2,29 +2,29 @@
2 2
3. ./setup.sh 3. ./setup.sh
4 4
5prepare_tests "Check content on tree page" 5prepare_tests "Check content on tree page"
6 6
7run_test 'generate bar/tree' 'cgit_url "bar/tree" >trash/tmp' 7run_test 'generate bar/tree' 'cgit_url "bar/tree" >trash/tmp'
8run_test 'find file-1' 'grep -e "file-1" trash/tmp' 8run_test 'find file-1' 'grep -e "file-1" trash/tmp'
9run_test 'find file-50' 'grep -e "file-50" trash/tmp' 9run_test 'find file-50' 'grep -e "file-50" trash/tmp'
10 10
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'
22 22
23run_test 'verify a+b link' ' 23run_test 'verify a+b link' '
24 grep -e "/foo+bar/tree/a+b" trash/tmp 24 grep -e "/foo+bar/tree/a+b" trash/tmp
25' 25'
26 26
27run_test 'generate foo+bar/tree?h=1+2' 'cgit_url "foo%2bbar/tree&h=1%2b2" >trash/tmp' 27run_test 'generate foo+bar/tree?h=1+2' 'cgit_url "foo%2bbar/tree&h=1%2b2" >trash/tmp'
28 28
29run_test 'verify a+b?h=1+2 link' ' 29run_test 'verify a+b?h=1+2 link' '
30 grep -e "/foo+bar/tree/a+b?h=1%2b2" trash/tmp 30 grep -e "/foo+bar/tree/a+b?h=1%2b2" 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
@@ -1,24 +1,24 @@
1#!/bin/sh 1#!/bin/sh
2 2
3. ./setup.sh 3. ./setup.sh
4 4
5prepare_tests "Check content on commit page" 5prepare_tests "Check content on commit page"
6 6
7run_test 'generate foo/commit' 'cgit_url "foo/commit" >trash/tmp' 7run_test 'generate foo/commit' 'cgit_url "foo/commit" >trash/tmp'
8run_test 'find tree link' 'grep -e "<a href=./foo/tree/.>" trash/tmp' 8run_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'
16run_test 'find diffstat' 'grep -e "<table summary=.diffstat. class=.diffstat.>" trash/tmp' 16run_test 'find diffstat' 'grep -e "<table summary=.diffstat. class=.diffstat.>" trash/tmp'
17 17
18run_test 'find diff summary' ' 18run_test 'find diff summary' '
19 grep -e "1 files changed, 1 insertions, 0 deletions" trash/tmp 19 grep -e "1 files changed, 1 insertions, 0 deletions" trash/tmp
20' 20'
21 21
22run_test 'get root commit' ' 22run_test 'get root commit' '
23 root=$(cd trash/repos/foo && git rev-list --reverse HEAD | head -1) && 23 root=$(cd trash/repos/foo && git rev-list --reverse HEAD | head -1) &&
24 cgit_url "foo/commit&id=$root" >trash/tmp && 24 cgit_url "foo/commit&id=$root" >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
@@ -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-commit.c b/ui-commit.c
index a6a85a4..41ce70e 100644
--- a/ui-commit.c
+++ b/ui-commit.c
@@ -1,48 +1,51 @@
1/* ui-commit.c: generate commit view 1/* ui-commit.c: generate commit view
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
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{
16 struct commit *commit, *parent; 17 struct commit *commit, *parent;
17 struct commitinfo *info; 18 struct commitinfo *info;
18 struct commit_list *p; 19 struct commit_list *p;
19 unsigned char sha1[20]; 20 unsigned char sha1[20];
20 char *tmp; 21 char *tmp;
21 int parents = 0; 22 int parents = 0;
22 23
23 if (!hex) 24 if (!hex)
24 hex = ctx.qry.head; 25 hex = ctx.qry.head;
25 26
26 if (get_sha1(hex, sha1)) { 27 if (get_sha1(hex, sha1)) {
27 cgit_print_error(fmt("Bad object id: %s", hex)); 28 cgit_print_error(fmt("Bad object id: %s", hex));
28 return; 29 return;
29 } 30 }
30 commit = lookup_commit_reference(sha1); 31 commit = lookup_commit_reference(sha1);
31 if (!commit) { 32 if (!commit) {
32 cgit_print_error(fmt("Bad commit reference: %s", hex)); 33 cgit_print_error(fmt("Bad commit reference: %s", hex));
33 return; 34 return;
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);
40 html(" "); 43 html(" ");
41 html_txt(info->author_email); 44 html_txt(info->author_email);
42 html("</td><td class='right'>"); 45 html("</td><td class='right'>");
43 cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time); 46 cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time);
44 html("</td></tr>\n"); 47 html("</td></tr>\n");
45 html("<tr><th>committer</th><td>"); 48 html("<tr><th>committer</th><td>");
46 html_txt(info->committer); 49 html_txt(info->committer);
47 html(" "); 50 html(" ");
48 html_txt(info->committer_email); 51 html_txt(info->committer_email);
@@ -78,24 +81,25 @@ void cgit_print_commit(char *hex)
78 html(")</td></tr>"); 81 html(")</td></tr>");
79 parents++; 82 parents++;
80 } 83 }
81 if (ctx.repo->snapshots) { 84 if (ctx.repo->snapshots) {
82 html("<tr><th>download</th><td colspan='2' class='sha1'>"); 85 html("<tr><th>download</th><td colspan='2' class='sha1'>");
83 cgit_print_snapshot_links(ctx.qry.repo, ctx.qry.head, 86 cgit_print_snapshot_links(ctx.qry.repo, ctx.qry.head,
84 hex, ctx.repo->snapshots); 87 hex, ctx.repo->snapshots);
85 html("</td></tr>"); 88 html("</td></tr>");
86 } 89 }
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);
93 html("</div>"); 97 html("</div>");
94 if (parents < 3) { 98 if (parents < 3) {
95 if (parents) 99 if (parents)
96 tmp = sha1_to_hex(commit->parents->item->object.sha1); 100 tmp = sha1_to_hex(commit->parents->item->object.sha1);
97 else 101 else
98 tmp = NULL; 102 tmp = NULL;
99 cgit_print_diff(ctx.qry.sha1, tmp, NULL); 103 cgit_print_diff(ctx.qry.sha1, tmp, NULL);
100 } 104 }
101 cgit_free_commitinfo(info); 105 cgit_free_commitinfo(info);
diff --git a/ui-log.c b/ui-log.c
index d212984..3202848 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -22,54 +22,101 @@ void count_lines(char *line, int size)
22 22
23 else if (line[0] == '-') 23 else if (line[0] == '-')
24 rem_lines++; 24 rem_lines++;
25} 25}
26 26
27void inspect_files(struct diff_filepair *pair) 27void 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;
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) {
52 files = 0; 88 files = 0;
53 add_lines = 0; 89 add_lines = 0;
54 rem_lines = 0; 90 rem_lines = 0;
55 cgit_diff_commit(commit, inspect_files); 91 cgit_diff_commit(commit, inspect_files);
56 html("</td><td>"); 92 html("</td><td>");
57 htmlf("%d", files); 93 htmlf("%d", files);
58 if (ctx.repo->enable_log_linecount) { 94 if (ctx.repo->enable_log_linecount) {
59 html("</td><td>"); 95 html("</td><td>");
60 htmlf("-%d/+%d", rem_lines, add_lines); 96 htmlf("-%d/+%d", rem_lines, add_lines);
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
67static const char *disambiguate_ref(const char *ref) 114static const char *disambiguate_ref(const char *ref)
68{ 115{
69 unsigned char sha1[20]; 116 unsigned char sha1[20];
70 const char *longref; 117 const char *longref;
71 118
72 longref = fmt("refs/heads/%s", ref); 119 longref = fmt("refs/heads/%s", ref);
73 if (get_sha1(longref, sha1) == 0) 120 if (get_sha1(longref, sha1) == 0)
74 return longref; 121 return longref;
75 122
@@ -96,34 +143,44 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
96 argv[argc++] = fmt("--%s=%s", grep, pattern); 143 argv[argc++] = fmt("--%s=%s", grep, pattern);
97 144
98 if (path) { 145 if (path) {
99 argv[argc++] = "--"; 146 argv[argc++] = "--";
100 argv[argc++] = path; 147 argv[argc++] = path;
101 } 148 }
102 init_revisions(&rev, NULL); 149 init_revisions(&rev, NULL);
103 rev.abbrev = DEFAULT_ABBREV; 150 rev.abbrev = DEFAULT_ABBREV;
104 rev.commit_format = CMIT_FMT_DEFAULT; 151 rev.commit_format = CMIT_FMT_DEFAULT;
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);
111 160
112 if (pager) 161 if (pager)
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++;
121 if (ctx.repo->enable_log_linecount) { 178 if (ctx.repo->enable_log_linecount) {
122 html("<th class='left'>Lines</th>"); 179 html("<th class='left'>Lines</th>");
123 columns++; 180 columns++;
124 } 181 }
125 } 182 }
126 html("</tr>\n"); 183 html("</tr>\n");
127 184
128 if (ofs<0) 185 if (ofs<0)
129 ofs = 0; 186 ofs = 0;
@@ -140,29 +197,29 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
140 free(commit->buffer); 197 free(commit->buffer);
141 commit->buffer = NULL; 198 commit->buffer = NULL;
142 free_commit_list(commit->parents); 199 free_commit_list(commit->parents);
143 commit->parents = NULL; 200 commit->parents = NULL;
144 } 201 }
145 if (pager) { 202 if (pager) {
146 htmlf("</table><div class='pager'>", 203 htmlf("</table><div class='pager'>",
147 columns); 204 columns);
148 if (ofs > 0) { 205 if (ofs > 0) {
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
@@ -1,7 +1,8 @@
1#ifndef UI_LOG_H 1#ifndef UI_LOG_H
2#define UI_LOG_H 2#define UI_LOG_H
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
@@ -49,78 +49,110 @@ static int cmp_tag_age(const void *a, const void *b)
49 49
50 return cmp_age(r1->tag->tagger_date, r2->tag->tagger_date); 50 return cmp_age(r1->tag->tagger_date, r2->tag->tagger_date);
51} 51}
52 52
53static int print_branch(struct refinfo *ref) 53static int print_branch(struct refinfo *ref)
54{ 54{
55 struct commitinfo *info = ref->commit; 55 struct commitinfo *info = ref->commit;
56 char *name = (char *)ref->refname; 56 char *name = (char *)ref->refname;
57 57
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) {
65 cgit_commit_link(info->subject, NULL, NULL, name, NULL); 66 cgit_commit_link(info->subject, NULL, NULL, name, NULL);
66 html("</td><td>"); 67 html("</td><td>");
67 html_txt(info->author); 68 html_txt(info->author);
68 html("</td><td colspan='2'>"); 69 html("</td><td colspan='2'>");
69 cgit_print_age(info->commit->date, -1, NULL); 70 cgit_print_age(info->commit->date, -1, NULL);
70 } else { 71 } else {
71 html("</td><td></td><td>"); 72 html("</td><td></td><td>");
72 cgit_object_link(ref->object); 73 cgit_object_link(ref->object);
73 } 74 }
74 html("</td></tr>\n"); 75 html("</td></tr>\n");
75 return 0; 76 return 0;
76} 77}
77 78
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;
90 struct taginfo *info; 116 struct taginfo *info;
91 char *name = (char *)ref->refname; 117 char *name = (char *)ref->refname;
92 118
93 if (ref->object->type == OBJ_TAG) { 119 if (ref->object->type == OBJ_TAG) {
94 tag = (struct tag *)ref->object; 120 tag = (struct tag *)ref->object;
95 info = ref->tag; 121 info = ref->tag;
96 if (!tag || !info) 122 if (!tag || !info)
97 return 1; 123 return 1;
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>");
127 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT))
128 print_tag_downloads(ctx.repo, name);
129 else
101 cgit_object_link(tag->tagged); 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);
105 html("</td><td colspan='2'>"); 134 html("</td><td colspan='2'>");
106 if (info->tagger_date > 0) 135 if (info->tagger_date > 0)
107 cgit_print_age(info->tagger_date, -1, NULL); 136 cgit_print_age(info->tagger_date, -1, NULL);
108 html("</td></tr>\n"); 137 html("</td></tr>\n");
109 } else { 138 } else {
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>");
144 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT))
145 print_tag_downloads(ctx.repo, name);
146 else
115 cgit_object_link(ref->object); 147 cgit_object_link(ref->object);
116 html("</td></tr>\n"); 148 html("</td></tr>\n");
117 } 149 }
118 return 0; 150 return 0;
119} 151}
120 152
121static void print_refs_link(char *path) 153static void print_refs_link(char *path)
122{ 154{
123 html("<tr class='nohover'><td colspan='4'>"); 155 html("<tr class='nohover'><td colspan='4'>");
124 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path); 156 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path);
125 html("</td></tr>"); 157 html("</td></tr>");
126} 158}
diff --git a/ui-repolist.c b/ui-repolist.c
index 2324273..2c13d50 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -23,39 +23,56 @@ time_t read_agefile(char *path)
23 23
24 if (!(f = fopen(path, "r"))) 24 if (!(f = fopen(path, "r")))
25 return -1; 25 return -1;
26 if (fgets(buf, sizeof(buf), f) == NULL) 26 if (fgets(buf, sizeof(buf), f) == NULL)
27 return -1; 27 return -1;
28 fclose(f); 28 fclose(f);
29 if (parse_date(buf, buf2, sizeof(buf2))) 29 if (parse_date(buf, buf2, sizeof(buf2)))
30 return strtoul(buf2, NULL, 10); 30 return strtoul(buf2, NULL, 10);
31 else 31 else
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)
53{ 70{
54 if (!ctx.qry.search) 71 if (!ctx.qry.search)
55 return 1; 72 return 1;
56 if (repo->url && strcasestr(repo->url, ctx.qry.search)) 73 if (repo->url && strcasestr(repo->url, ctx.qry.search))
57 return 1; 74 return 1;
58 if (repo->name && strcasestr(repo->name, ctx.qry.search)) 75 if (repo->name && strcasestr(repo->name, ctx.qry.search))
59 return 1; 76 return 1;
60 if (repo->desc && strcasestr(repo->desc, ctx.qry.search)) 77 if (repo->desc && strcasestr(repo->desc, ctx.qry.search))
61 return 1; 78 return 1;
@@ -64,102 +81,192 @@ int is_match(struct cgit_repo *repo)
64 return 0; 81 return 0;
65} 82}
66 83
67int is_in_url(struct cgit_repo *repo) 84int is_in_url(struct cgit_repo *repo)
68{ 85{
69 if (!ctx.qry.url) 86 if (!ctx.qry.url)
70 return 1; 87 return 1;
71 if (repo->url && !prefixcmp(repo->url, ctx.qry.url)) 88 if (repo->url && !prefixcmp(repo->url, ctx.qry.url))
72 return 1; 89 return 1;
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");
86} 113}
87 114
88 115
89void print_pager(int items, int pagelen, char *search) 116void print_pager(int items, int pagelen, char *search)
90{ 117{
91 int i; 118 int i;
92 html("<div class='pager'>"); 119 html("<div class='pager'>");
93 for(i = 0; i * pagelen < items; i++) 120 for(i = 0; i * pagelen < items; i++)
94 cgit_index_link(fmt("[%d]", i+1), fmt("Page %d", i+1), NULL, 121 cgit_index_link(fmt("[%d]", i+1), fmt("Page %d", i+1), NULL,
95 search, i * pagelen); 122 search, i * pagelen);
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++;
106 209
107 ctx.page.title = ctx.cfg.root_title; 210 ctx.page.title = ctx.cfg.root_title;
108 cgit_print_http_headers(&ctx); 211 cgit_print_http_headers(&ctx);
109 cgit_print_docstart(&ctx); 212 cgit_print_docstart(&ctx);
110 cgit_print_pageheader(&ctx); 213 cgit_print_pageheader(&ctx);
111 214
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];
118 if (!(is_match(ctx.repo) && is_in_url(ctx.repo))) 224 if (!(is_match(ctx.repo) && is_in_url(ctx.repo)))
119 continue; 225 continue;
120 hits++; 226 hits++;
121 if (hits <= ctx.qry.ofs) 227 if (hits <= ctx.qry.ofs)
122 continue; 228 continue;
123 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count) 229 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count)
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);
134 html("</td></tr>"); 241 html("</td></tr>");
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);
142 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc); 249 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc);
143 html_link_close(); 250 html_link_close();
144 html("</td><td>"); 251 html("</td><td>");
145 html_txt(ctx.repo->owner); 252 html_txt(ctx.repo->owner);
146 html("</td><td>"); 253 html("</td><td>");
147 print_modtime(ctx.repo); 254 print_modtime(ctx.repo);
148 html("</td>"); 255 html("</td>");
149 if (ctx.cfg.enable_index_links) { 256 if (ctx.cfg.enable_index_links) {
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 }
157 html("</tr>\n"); 264 html("</tr>\n");
158 } 265 }
159 html("</table>"); 266 html("</table>");
160 if (!hits) 267 if (!hits)
161 cgit_print_error("No repositories found"); 268 cgit_print_error("No repositories found");
162 else if (hits > ctx.cfg.max_repo_count) 269 else if (hits > ctx.cfg.max_repo_count)
163 print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search); 270 print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search);
164 cgit_print_docend(); 271 cgit_print_docend();
165} 272}
diff --git a/ui-shared.c b/ui-shared.c
index 76cd00d..4f28512 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -272,48 +272,54 @@ void cgit_tree_link(char *name, char *title, char *class, char *head,
272 char *rev, char *path) 272 char *rev, char *path)
273{ 273{
274 reporevlink("tree", name, title, class, head, rev, path); 274 reporevlink("tree", name, title, class, head, rev, path);
275} 275}
276 276
277void cgit_plain_link(char *name, char *title, char *class, char *head, 277void cgit_plain_link(char *name, char *title, char *class, char *head,
278 char *rev, char *path) 278 char *rev, char *path)
279{ 279{
280 reporevlink("plain", name, title, class, head, rev, path); 280 reporevlink("plain", name, title, class, head, rev, path);
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
288 delim = repolink(title, class, "log", head, path); 289 delim = repolink(title, class, "log", head, path);
289 if (rev && strcmp(rev, ctx.qry.head)) { 290 if (rev && strcmp(rev, ctx.qry.head)) {
290 html(delim); 291 html(delim);
291 html("id="); 292 html("id=");
292 html_url_arg(rev); 293 html_url_arg(rev);
293 delim = "&"; 294 delim = "&";
294 } 295 }
295 if (grep && pattern) { 296 if (grep && pattern) {
296 html(delim); 297 html(delim);
297 html("qt="); 298 html("qt=");
298 html_url_arg(grep); 299 html_url_arg(grep);
299 delim = "&"; 300 delim = "&";
300 html(delim); 301 html(delim);
301 html("q="); 302 html("q=");
302 html_url_arg(pattern); 303 html_url_arg(pattern);
303 } 304 }
304 if (ofs > 0) { 305 if (ofs > 0) {
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);
311 html("</a>"); 317 html("</a>");
312} 318}
313 319
314void cgit_commit_link(char *name, char *title, char *class, char *head, 320void cgit_commit_link(char *name, char *title, char *class, char *head,
315 char *rev) 321 char *rev)
316{ 322{
317 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { 323 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
318 name[ctx.cfg.max_msg_len] = '\0'; 324 name[ctx.cfg.max_msg_len] = '\0';
319 name[ctx.cfg.max_msg_len - 1] = '.'; 325 name[ctx.cfg.max_msg_len - 1] = '.';
@@ -354,41 +360,49 @@ void cgit_diff_link(char *name, char *title, char *class, char *head,
354 } 360 }
355 html("'>"); 361 html("'>");
356 html_txt(name); 362 html_txt(name);
357 html("</a>"); 363 html("</a>");
358} 364}
359 365
360void cgit_patch_link(char *name, char *title, char *class, char *head, 366void cgit_patch_link(char *name, char *title, char *class, char *head,
361 char *rev) 367 char *rev)
362{ 368{
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";
376 else if (obj->type == OBJ_TAG) 391 else if (obj->type == OBJ_TAG)
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)
386{ 400{
387 char buf[64]; 401 char buf[64];
388 struct tm *time; 402 struct tm *time;
389 403
390 if (!secs) 404 if (!secs)
391 return; 405 return;
392 if(local_time) 406 if(local_time)
393 time = localtime(&secs); 407 time = localtime(&secs);
394 else 408 else
@@ -540,43 +554,45 @@ int print_archive_ref(const char *refname, const unsigned char *sha1,
540 html("<h1>download</h1>\n"); 554 html("<h1>download</h1>\n");
541 *header = 1; 555 *header = 1;
542 } 556 }
543 url = cgit_pageurl(ctx.qry.repo, "blob", 557 url = cgit_pageurl(ctx.qry.repo, "blob",
544 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid), 558 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid),
545 buf)); 559 buf));
546 html_link_open(url, NULL, "menu"); 560 html_link_open(url, NULL, "menu");
547 html_txt(strlpart(buf, 20)); 561 html_txt(strlpart(buf, 20));
548 html_link_close(); 562 html_link_close();
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
556 if (!ctx.cfg.virtual_root) { 570 if (!ctx.cfg.virtual_root) {
557 url = fmt("%s/%s", ctx.qry.repo, page); 571 url = fmt("%s/%s", ctx.qry.repo, page);
558 if (ctx.qry.path) 572 if (ctx.qry.path)
559 url = fmt("%s/%s", url, ctx.qry.path); 573 url = fmt("%s/%s", url, ctx.qry.path);
560 html_hidden("url", url); 574 html_hidden("url", url);
561 } 575 }
562 576
563 if (incl_head && ctx.qry.head && ctx.repo->defbranch && 577 if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
564 strcmp(ctx.qry.head, ctx.repo->defbranch)) 578 strcmp(ctx.qry.head, ctx.repo->defbranch))
565 html_hidden("h", ctx.qry.head); 579 html_hidden("h", ctx.qry.head);
566 580
567 if (ctx.qry.sha1) 581 if (ctx.qry.sha1)
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)
574 html_hidden("qt", ctx.qry.grep); 590 html_hidden("qt", ctx.qry.grep);
575 if (ctx.qry.search) 591 if (ctx.qry.search)
576 html_hidden("q", ctx.qry.search); 592 html_hidden("q", ctx.qry.search);
577 } 593 }
578} 594}
579 595
580const char *fallback_cmd = "repolist"; 596const char *fallback_cmd = "repolist";
581 597
582char *hc(struct cgit_cmd *cmd, const char *page) 598char *hc(struct cgit_cmd *cmd, const char *page)
@@ -600,25 +616,25 @@ void cgit_print_pageheader(struct cgit_context *ctx)
600 html_attr(cgit_rooturl()); 616 html_attr(cgit_rooturl());
601 html("'><img src='"); 617 html("'><img src='");
602 html_attr(ctx->cfg.logo); 618 html_attr(ctx->cfg.logo);
603 html("' alt='cgit logo'/></a></td>\n"); 619 html("' alt='cgit logo'/></a></td>\n");
604 620
605 html("<td class='main'>"); 621 html("<td class='main'>");
606 if (ctx->repo) { 622 if (ctx->repo) {
607 cgit_index_link("index", NULL, NULL, NULL, 0); 623 cgit_index_link("index", NULL, NULL, NULL, 0);
608 html(" : "); 624 html(" : ");
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> ");
616 html("<input type='submit' name='' value='switch'/>"); 632 html("<input type='submit' name='' value='switch'/>");
617 html("</form>"); 633 html("</form>");
618 } else 634 } else
619 html_txt(ctx->cfg.root_title); 635 html_txt(ctx->cfg.root_title);
620 html("</td></tr>\n"); 636 html("</td></tr>\n");
621 637
622 html("<tr><td class='sub'>"); 638 html("<tr><td class='sub'>");
623 if (ctx->repo) { 639 if (ctx->repo) {
624 html_txt(ctx->repo->desc); 640 html_txt(ctx->repo->desc);
@@ -630,42 +646,45 @@ void cgit_print_pageheader(struct cgit_context *ctx)
630 else if (ctx->cfg.index_info) 646 else if (ctx->cfg.index_info)
631 html_include(ctx->cfg.index_info); 647 html_include(ctx->cfg.index_info);
632 } 648 }
633 html("</td></tr></table>\n"); 649 html("</td></tr></table>\n");
634 650
635 html("<table class='tabs'><tr><td>\n"); 651 html("<table class='tabs'><tr><td>\n");
636 if (ctx->repo) { 652 if (ctx->repo) {
637 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 653 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
638 ctx->qry.head); 654 ctx->qry.head);
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,
652 NULL); 671 NULL);
653 html("</td><td class='form'>"); 672 html("</td><td class='form'>");
654 html("<form class='right' method='get' action='"); 673 html("<form class='right' method='get' action='");
655 if (ctx->cfg.virtual_root) 674 if (ctx->cfg.virtual_root)
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);
663 html_option("committer", "committer", ctx->qry.grep); 682 html_option("committer", "committer", ctx->qry.grep);
664 html("</select>\n"); 683 html("</select>\n");
665 html("<input class='txt' type='text' size='10' name='q' value='"); 684 html("<input class='txt' type='text' size='10' name='q' value='");
666 html_attr(ctx->qry.search); 685 html_attr(ctx->qry.search);
667 html("'/>\n"); 686 html("'/>\n");
668 html("<input type='submit' value='search'/>\n"); 687 html("<input type='submit' value='search'/>\n");
669 html("</form>\n"); 688 html("</form>\n");
670 } else { 689 } else {
671 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0); 690 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0);
@@ -703,17 +722,16 @@ void cgit_print_filemode(unsigned short mode)
703 722
704void cgit_print_snapshot_links(const char *repo, const char *head, 723void cgit_print_snapshot_links(const char *repo, const char *head,
705 const char *hex, int snapshots) 724 const char *hex, int snapshots)
706{ 725{
707 const struct cgit_snapshot_format* f; 726 const struct cgit_snapshot_format* f;
708 char *filename; 727 char *filename;
709 728
710 for (f = cgit_snapshot_formats; f->suffix; f++) { 729 for (f = cgit_snapshot_formats; f->suffix; f++) {
711 if (!(snapshots & f->bit)) 730 if (!(snapshots & f->bit))
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
@@ -10,37 +10,40 @@ extern char *cgit_pageurl(const char *reponame, const char *pagename,
10 10
11extern void cgit_index_link(char *name, char *title, char *class, 11extern void cgit_index_link(char *name, char *title, char *class,
12 char *pattern, int ofs); 12 char *pattern, int ofs);
13extern void cgit_summary_link(char *name, char *title, char *class, char *head); 13extern void cgit_summary_link(char *name, char *title, char *class, char *head);
14extern void cgit_tag_link(char *name, char *title, char *class, char *head, 14extern void cgit_tag_link(char *name, char *title, char *class, char *head,
15 char *rev); 15 char *rev);
16extern void cgit_tree_link(char *name, char *title, char *class, char *head, 16extern void cgit_tree_link(char *name, char *title, char *class, char *head,
17 char *rev, char *path); 17 char *rev, char *path);
18extern void cgit_plain_link(char *name, char *title, char *class, char *head, 18extern 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,
26 char *rev); 26 char *rev);
27extern void cgit_refs_link(char *name, char *title, char *class, char *head, 27extern void cgit_refs_link(char *name, char *title, char *class, char *head,
28 char *rev, char *path); 28 char *rev, char *path);
29extern void cgit_snapshot_link(char *name, char *title, char *class, 29extern void cgit_snapshot_link(char *name, char *title, char *class,
30 char *head, char *rev, char *archivename); 30 char *head, char *rev, char *archivename);
31extern void cgit_diff_link(char *name, char *title, char *class, char *head, 31extern void cgit_diff_link(char *name, char *title, char *class, char *head,
32 char *new_rev, char *old_rev, char *path); 32 char *new_rev, char *old_rev, char *path);
33extern void cgit_stats_link(char *name, char *title, char *class, char *head,
34 char *path);
33extern void cgit_object_link(struct object *obj); 35extern void cgit_object_link(struct object *obj);
34 36
35extern void cgit_print_error(char *msg); 37extern void cgit_print_error(char *msg);
36extern void cgit_print_date(time_t secs, char *format, int local_time); 38extern void cgit_print_date(time_t secs, char *format, int local_time);
37extern void cgit_print_age(time_t t, time_t max_relative, char *format); 39extern void cgit_print_age(time_t t, time_t max_relative, char *format);
38extern void cgit_print_http_headers(struct cgit_context *ctx); 40extern void cgit_print_http_headers(struct cgit_context *ctx);
39extern void cgit_print_docstart(struct cgit_context *ctx); 41extern void cgit_print_docstart(struct cgit_context *ctx);
40extern void cgit_print_docend(); 42extern void cgit_print_docend();
41extern void cgit_print_pageheader(struct cgit_context *ctx); 43extern void cgit_print_pageheader(struct cgit_context *ctx);
42extern void cgit_print_filemode(unsigned short mode); 44extern void cgit_print_filemode(unsigned short mode);
43extern void cgit_print_snapshot_links(const char *repo, const char *head, 45extern void cgit_print_snapshot_links(const char *repo, const char *head,
44 const char *hex, int snapshots); 46 const char *hex, int snapshots);
45 47extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
48 char *page);
46#endif /* UI_SHARED_H */ 49#endif /* UI_SHARED_H */
diff --git a/ui-snapshot.c b/ui-snapshot.c
index 9c4d086..f25613e 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -49,26 +49,26 @@ static int write_compressed_tar_archive(struct archiver_args *args,const char *f
49static int write_tar_gzip_archive(struct archiver_args *args) 49static int write_tar_gzip_archive(struct archiver_args *args)
50{ 50{
51 return write_compressed_tar_archive(args,"gzip"); 51 return write_compressed_tar_archive(args,"gzip");
52} 52}
53 53
54static int write_tar_bzip2_archive(struct archiver_args *args) 54static 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{
69 const struct cgit_snapshot_format *fmt; 69 const struct cgit_snapshot_format *fmt;
70 int fl, sl; 70 int fl, sl;
71 71
72 fl = strlen(filename); 72 fl = strlen(filename);
73 for(fmt = cgit_snapshot_formats; fmt->suffix; fmt++) { 73 for(fmt = cgit_snapshot_formats; fmt->suffix; fmt++) {
74 sl = strlen(fmt->suffix); 74 sl = strlen(fmt->suffix);
@@ -105,84 +105,90 @@ static int make_snapshot(const struct cgit_snapshot_format *format,
105 args.base = ""; 105 args.base = "";
106 args.baselen = 0; 106 args.baselen = 0;
107 } 107 }
108 args.tree = commit->tree; 108 args.tree = commit->tree;
109 args.time = commit->date; 109 args.time = commit->date;
110 ctx.page.mimetype = xstrdup(format->mimetype); 110 ctx.page.mimetype = xstrdup(format->mimetype);
111 ctx.page.filename = xstrdup(filename); 111 ctx.page.filename = xstrdup(filename);
112 cgit_print_http_headers(&ctx); 112 cgit_print_http_headers(&ctx);
113 format->write_func(&args); 113 format->write_func(&args);
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;
133
134 snapshot = xstrdup(filename);
135 snapshot[strlen(snapshot) - strlen(format->suffix)] = '\0';
136 fprintf(stderr, "snapshot=%s\n", snapshot);
137
138 if (get_sha1(snapshot, sha1) == 0)
139 return snapshot;
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
155 return NULL; 156 return NULL;
156 filename += strlen(reponame);
157 while (filename && (*filename == '-' || *filename == '_'))
158 filename++;
159 dwim_filename = xstrdup(filename);
160 dwim_filename[strlen(filename) - strlen(fmt->suffix)] = '\0';
161 for_each_tag_ref(ref_cb, NULL);
162 return dwim_refname;
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) {
172 ctx.page.mimetype = "text/html"; 167 ctx.page.mimetype = "text/html";
173 cgit_print_http_headers(&ctx); 168 cgit_print_http_headers(&ctx);
174 cgit_print_docstart(&ctx); 169 cgit_print_docstart(&ctx);
175 cgit_print_pageheader(&ctx); 170 cgit_print_pageheader(&ctx);
176 cgit_print_error(fmt("Unsupported snapshot format: %s", filename)); 171 cgit_print_error(fmt("Unsupported snapshot format: %s", filename));
177 cgit_print_docend(); 172 cgit_print_docend();
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
@@ -1,8 +1,7 @@
1#ifndef UI_SNAPSHOT_H 1#ifndef UI_SNAPSHOT_H
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
--- 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 54b9f4c..0e056e0 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -66,15 +66,24 @@ void cgit_print_tag(char *revname)
66 html_txt(info->tagger); 66 html_txt(info->tagger);
67 if (info->tagger_email) { 67 if (info->tagger_email) {
68 html(" "); 68 html(" ");
69 html_txt(info->tagger_email); 69 html_txt(info->tagger_email);
70 } 70 }
71 html("</td></tr>\n"); 71 html("</td></tr>\n");
72 } 72 }
73 html("<tr><td>Tagged object</td><td>"); 73 html("<tr><td>Tagged object</td><td>");
74 cgit_object_link(tag->tagged); 74 cgit_object_link(tag->tagged);
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 } 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");
78 } 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
@@ -9,65 +9,66 @@
9#include "cgit.h" 9#include "cgit.h"
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12 12
13char *curr_rev; 13char *curr_rev;
14char *match_path; 14char *match_path;
15int header = 0; 15int header = 0;
16 16
17static void print_object(const unsigned char *sha1, char *path) 17static 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) {
26 cgit_print_error(fmt("Bad object name: %s", 26 cgit_print_error(fmt("Bad object name: %s",
27 sha1_to_hex(sha1))); 27 sha1_to_hex(sha1)));
28 return; 28 return;
29 } 29 }
30 30
31 buf = read_sha1_file(sha1, &type, &size); 31 buf = read_sha1_file(sha1, &type, &size);
32 if (!buf) { 32 if (!buf) {
33 cgit_print_error(fmt("Error reading object %s", 33 cgit_print_error(fmt("Error reading object %s",
34 sha1_to_hex(sha1))); 34 sha1_to_hex(sha1)));
35 return; 35 return;
36 } 36 }
37 37
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
65 66
66static int ls_item(const unsigned char *sha1, const char *base, int baselen, 67static int ls_item(const unsigned char *sha1, const char *base, int baselen,
67 const char *pathname, unsigned int mode, int stage, 68 const char *pathname, unsigned int mode, int stage,
68 void *cbdata) 69 void *cbdata)
69{ 70{
70 char *name; 71 char *name;
71 char *fullpath; 72 char *fullpath;
72 enum object_type type; 73 enum object_type type;
73 unsigned long size = 0; 74 unsigned long size = 0;
@@ -99,25 +100,28 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
99 html("</a>"); 100 html("</a>");
100 } else if (S_ISDIR(mode)) { 101 } else if (S_ISDIR(mode)) {
101 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, 102 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head,
102 curr_rev, fullpath); 103 curr_rev, fullpath);
103 } else { 104 } else {
104 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head, 105 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head,
105 curr_rev, fullpath); 106 curr_rev, fullpath);
106 } 107 }
107 htmlf("</td><td class='ls-size'>%li</td>", size); 108 htmlf("</td><td class='ls-size'>%li</td>", size);
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;
115} 119}
116 120
117static void ls_head() 121static void ls_head()
118{ 122{
119 html("<table summary='tree listing' class='list'>\n"); 123 html("<table summary='tree listing' class='list'>\n");
120 html("<tr class='nohover'>"); 124 html("<tr class='nohover'>");
121 html("<th class='left'>Mode</th>"); 125 html("<th class='left'>Mode</th>");
122 html("<th class='left'>Name</th>"); 126 html("<th class='left'>Name</th>");
123 html("<th class='right'>Size</th>"); 127 html("<th class='right'>Size</th>");