summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--.gitignore5
-rw-r--r--Makefile21
-rw-r--r--cache.h2
-rw-r--r--cgit-doc.css3
-rw-r--r--cgit.c345
-rw-r--r--cgit.css10
-rw-r--r--cgit.h52
-rw-r--r--cgitrc.5.txt244
-rw-r--r--cmd.c2
-rwxr-xr-xfilters/commit-links.sh12
-rwxr-xr-xfilters/syntax-highlighting.sh39
m---------git0
-rw-r--r--scan-tree.c45
-rw-r--r--scan-tree.h2
-rw-r--r--shared.c62
-rw-r--r--ui-atom.c8
-rw-r--r--ui-blob.c8
-rw-r--r--ui-commit.c22
-rw-r--r--ui-log.c6
-rw-r--r--ui-patch.c6
-rw-r--r--ui-plain.c18
-rw-r--r--ui-refs.c19
-rw-r--r--ui-repolist.c65
-rw-r--r--ui-shared.c81
-rw-r--r--ui-shared.h1
-rw-r--r--ui-snapshot.c35
-rw-r--r--ui-stats.c8
-rw-r--r--ui-stats.h1
-rw-r--r--ui-summary.c28
-rw-r--r--ui-summary.h2
-rw-r--r--ui-tag.c2
-rw-r--r--ui-tree.c55
32 files changed, 942 insertions, 267 deletions
diff --git a/.gitignore b/.gitignore
index 1e016e5..487728b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,11 @@
1# Files I don't care to see in git-status/commit 1# Files I don't care to see in git-status/commit
2cgit 2cgit
3cgit.conf 3cgit.conf
4VERSION 4VERSION
5cgitrc.5
6cgitrc.5.fo
7cgitrc.5.html
8cgitrc.5.pdf
9cgitrc.5.xml
5*.o 10*.o
6*.d 11*.d
diff --git a/Makefile b/Makefile
index 44138ea..da2518b 100644
--- a/Makefile
+++ b/Makefile
@@ -4,9 +4,9 @@ CGIT_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.1.1 8GIT_VER = 1.6.4.3
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.
@@ -99,9 +99,10 @@ ifdef NEEDS_LIBICONV
99 EXTLIBS += -liconv 99 EXTLIBS += -liconv
100endif 100endif
101 101
102 102
103.PHONY: all libgit test install uninstall clean force-version get-git 103.PHONY: all libgit test install uninstall clean force-version get-git \
104 doc man-doc html-doc clean-doc
104 105
105all: cgit 106all: cgit
106 107
107VERSION: force-version 108VERSION: force-version
@@ -148,9 +149,23 @@ uninstall:
148 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 149 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
149 rm -f $(CGIT_DATA_PATH)/cgit.css 150 rm -f $(CGIT_DATA_PATH)/cgit.css
150 rm -f $(CGIT_DATA_PATH)/cgit.png 151 rm -f $(CGIT_DATA_PATH)/cgit.png
151 152
152clean: 153doc: man-doc html-doc pdf-doc
154
155man-doc: cgitrc.5.txt
156 a2x -f manpage cgitrc.5.txt
157
158html-doc: cgitrc.5.txt
159 a2x -f xhtml --stylesheet=cgit-doc.css cgitrc.5.txt
160
161pdf-doc: cgitrc.5.txt
162 a2x -f pdf cgitrc.5.txt
163
164clean: clean-doc
153 rm -f cgit VERSION *.o *.d 165 rm -f cgit VERSION *.o *.d
154 166
167clean-doc:
168 rm -f cgitrc.5 cgitrc.5.html cgitrc.5.pdf cgitrc.5.xml cgitrc.5.fo
169
155get-git: 170get-git:
156 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git 171 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cache.h b/cache.h
index 66cc41f..ac9276b 100644
--- a/cache.h
+++ b/cache.h
@@ -31,5 +31,7 @@ extern int cache_ls(const char *path);
31 31
32/* Print a message to stdout */ 32/* Print a message to stdout */
33extern void cache_log(const char *format, ...); 33extern void cache_log(const char *format, ...);
34 34
35extern unsigned long hash_str(const char *str);
36
35#endif /* CGIT_CACHE_H */ 37#endif /* CGIT_CACHE_H */
diff --git a/cgit-doc.css b/cgit-doc.css
new file mode 100644
index 0000000..5a399b6
--- a/dev/null
+++ b/cgit-doc.css
@@ -0,0 +1,3 @@
1div.variablelist dt {
2 margin-top: 1em;
3}
diff --git a/cgit.c b/cgit.c
index 5301840..bd37788 100644
--- a/cgit.c
+++ b/cgit.c
@@ -16,11 +16,83 @@
16#include "scan-tree.h" 16#include "scan-tree.h"
17 17
18const char *cgit_version = CGIT_VERSION; 18const char *cgit_version = CGIT_VERSION;
19 19
20void add_mimetype(const char *name, const char *value)
21{
22 struct string_list_item *item;
23
24 item = string_list_insert(xstrdup(name), &ctx.cfg.mimetypes);
25 item->util = xstrdup(value);
26}
27
28struct cgit_filter *new_filter(const char *cmd, int extra_args)
29{
30 struct cgit_filter *f;
31
32 if (!cmd || !cmd[0])
33 return NULL;
34
35 f = xmalloc(sizeof(struct cgit_filter));
36 f->cmd = xstrdup(cmd);
37 f->argv = xmalloc((2 + extra_args) * sizeof(char *));
38 f->argv[0] = f->cmd;
39 f->argv[1] = NULL;
40 return f;
41}
42
43static void process_cached_repolist(const char *path);
44
45void repo_config(struct cgit_repo *repo, const char *name, const char *value)
46{
47 if (!strcmp(name, "name"))
48 repo->name = xstrdup(value);
49 else if (!strcmp(name, "clone-url"))
50 repo->clone_url = xstrdup(value);
51 else if (!strcmp(name, "desc"))
52 repo->desc = xstrdup(value);
53 else if (!strcmp(name, "owner"))
54 repo->owner = xstrdup(value);
55 else if (!strcmp(name, "defbranch"))
56 repo->defbranch = xstrdup(value);
57 else if (!strcmp(name, "snapshots"))
58 repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value);
59 else if (!strcmp(name, "enable-log-filecount"))
60 repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
61 else if (!strcmp(name, "enable-log-linecount"))
62 repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
63 else if (!strcmp(name, "max-stats"))
64 repo->max_stats = cgit_find_stats_period(value, NULL);
65 else if (!strcmp(name, "module-link"))
66 repo->module_link= xstrdup(value);
67 else if (!strcmp(name, "section"))
68 repo->section = xstrdup(value);
69 else if (!strcmp(name, "readme") && value != NULL) {
70 if (*value == '/')
71 ctx.repo->readme = xstrdup(value);
72 else
73 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
74 } else if (ctx.cfg.enable_filter_overrides) {
75 if (!strcmp(name, "about-filter"))
76 repo->about_filter = new_filter(value, 0);
77 else if (!strcmp(name, "commit-filter"))
78 repo->commit_filter = new_filter(value, 0);
79 else if (!strcmp(name, "source-filter"))
80 repo->source_filter = new_filter(value, 1);
81 }
82}
83
20void config_cb(const char *name, const char *value) 84void config_cb(const char *name, const char *value)
21{ 85{
22 if (!strcmp(name, "root-title")) 86 if (!strcmp(name, "section") || !strcmp(name, "repo.group"))
87 ctx.cfg.section = xstrdup(value);
88 else if (!strcmp(name, "repo.url"))
89 ctx.repo = cgit_add_repo(value);
90 else if (ctx.repo && !strcmp(name, "repo.path"))
91 ctx.repo->path = trim_end(value, '/');
92 else if (ctx.repo && !prefixcmp(name, "repo."))
93 repo_config(ctx.repo, name + 5, value);
94 else if (!strcmp(name, "root-title"))
23 ctx.cfg.root_title = xstrdup(value); 95 ctx.cfg.root_title = xstrdup(value);
24 else if (!strcmp(name, "root-desc")) 96 else if (!strcmp(name, "root-desc"))
25 ctx.cfg.root_desc = xstrdup(value); 97 ctx.cfg.root_desc = xstrdup(value);
26 else if (!strcmp(name, "root-readme")) 98 else if (!strcmp(name, "root-readme"))
@@ -30,8 +102,10 @@ void config_cb(const char *name, const char *value)
30 else if (!strcmp(name, "favicon")) 102 else if (!strcmp(name, "favicon"))
31 ctx.cfg.favicon = xstrdup(value); 103 ctx.cfg.favicon = xstrdup(value);
32 else if (!strcmp(name, "footer")) 104 else if (!strcmp(name, "footer"))
33 ctx.cfg.footer = xstrdup(value); 105 ctx.cfg.footer = xstrdup(value);
106 else if (!strcmp(name, "head-include"))
107 ctx.cfg.head_include = xstrdup(value);
34 else if (!strcmp(name, "header")) 108 else if (!strcmp(name, "header"))
35 ctx.cfg.header = xstrdup(value); 109 ctx.cfg.header = xstrdup(value);
36 else if (!strcmp(name, "logo")) 110 else if (!strcmp(name, "logo"))
37 ctx.cfg.logo = xstrdup(value); 111 ctx.cfg.logo = xstrdup(value);
@@ -48,16 +122,24 @@ void config_cb(const char *name, const char *value)
48 if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 122 if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
49 ctx.cfg.virtual_root = ""; 123 ctx.cfg.virtual_root = "";
50 } else if (!strcmp(name, "nocache")) 124 } else if (!strcmp(name, "nocache"))
51 ctx.cfg.nocache = atoi(value); 125 ctx.cfg.nocache = atoi(value);
126 else if (!strcmp(name, "noplainemail"))
127 ctx.cfg.noplainemail = atoi(value);
128 else if (!strcmp(name, "noheader"))
129 ctx.cfg.noheader = atoi(value);
52 else if (!strcmp(name, "snapshots")) 130 else if (!strcmp(name, "snapshots"))
53 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 131 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
132 else if (!strcmp(name, "enable-filter-overrides"))
133 ctx.cfg.enable_filter_overrides = atoi(value);
54 else if (!strcmp(name, "enable-index-links")) 134 else if (!strcmp(name, "enable-index-links"))
55 ctx.cfg.enable_index_links = atoi(value); 135 ctx.cfg.enable_index_links = atoi(value);
56 else if (!strcmp(name, "enable-log-filecount")) 136 else if (!strcmp(name, "enable-log-filecount"))
57 ctx.cfg.enable_log_filecount = atoi(value); 137 ctx.cfg.enable_log_filecount = atoi(value);
58 else if (!strcmp(name, "enable-log-linecount")) 138 else if (!strcmp(name, "enable-log-linecount"))
59 ctx.cfg.enable_log_linecount = atoi(value); 139 ctx.cfg.enable_log_linecount = atoi(value);
140 else if (!strcmp(name, "enable-tree-linenumbers"))
141 ctx.cfg.enable_tree_linenumbers = atoi(value);
60 else if (!strcmp(name, "max-stats")) 142 else if (!strcmp(name, "max-stats"))
61 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); 143 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
62 else if (!strcmp(name, "cache-size")) 144 else if (!strcmp(name, "cache-size"))
63 ctx.cfg.cache_size = atoi(value); 145 ctx.cfg.cache_size = atoi(value);
@@ -66,20 +148,35 @@ void config_cb(const char *name, const char *value)
66 else if (!strcmp(name, "cache-root-ttl")) 148 else if (!strcmp(name, "cache-root-ttl"))
67 ctx.cfg.cache_root_ttl = atoi(value); 149 ctx.cfg.cache_root_ttl = atoi(value);
68 else if (!strcmp(name, "cache-repo-ttl")) 150 else if (!strcmp(name, "cache-repo-ttl"))
69 ctx.cfg.cache_repo_ttl = atoi(value); 151 ctx.cfg.cache_repo_ttl = atoi(value);
152 else if (!strcmp(name, "cache-scanrc-ttl"))
153 ctx.cfg.cache_scanrc_ttl = atoi(value);
70 else if (!strcmp(name, "cache-static-ttl")) 154 else if (!strcmp(name, "cache-static-ttl"))
71 ctx.cfg.cache_static_ttl = atoi(value); 155 ctx.cfg.cache_static_ttl = atoi(value);
72 else if (!strcmp(name, "cache-dynamic-ttl")) 156 else if (!strcmp(name, "cache-dynamic-ttl"))
73 ctx.cfg.cache_dynamic_ttl = atoi(value); 157 ctx.cfg.cache_dynamic_ttl = atoi(value);
158 else if (!strcmp(name, "about-filter"))
159 ctx.cfg.about_filter = new_filter(value, 0);
160 else if (!strcmp(name, "commit-filter"))
161 ctx.cfg.commit_filter = new_filter(value, 0);
162 else if (!strcmp(name, "embedded"))
163 ctx.cfg.embedded = atoi(value);
74 else if (!strcmp(name, "max-message-length")) 164 else if (!strcmp(name, "max-message-length"))
75 ctx.cfg.max_msg_len = atoi(value); 165 ctx.cfg.max_msg_len = atoi(value);
76 else if (!strcmp(name, "max-repodesc-length")) 166 else if (!strcmp(name, "max-repodesc-length"))
77 ctx.cfg.max_repodesc_len = atoi(value); 167 ctx.cfg.max_repodesc_len = atoi(value);
78 else if (!strcmp(name, "max-repo-count")) 168 else if (!strcmp(name, "max-repo-count"))
79 ctx.cfg.max_repo_count = atoi(value); 169 ctx.cfg.max_repo_count = atoi(value);
80 else if (!strcmp(name, "max-commit-count")) 170 else if (!strcmp(name, "max-commit-count"))
81 ctx.cfg.max_commit_count = atoi(value); 171 ctx.cfg.max_commit_count = atoi(value);
172 else if (!strcmp(name, "scan-path"))
173 if (!ctx.cfg.nocache && ctx.cfg.cache_size)
174 process_cached_repolist(value);
175 else
176 scan_tree(value, repo_config);
177 else if (!strcmp(name, "source-filter"))
178 ctx.cfg.source_filter = new_filter(value, 1);
82 else if (!strcmp(name, "summary-log")) 179 else if (!strcmp(name, "summary-log"))
83 ctx.cfg.summary_log = atoi(value); 180 ctx.cfg.summary_log = atoi(value);
84 else if (!strcmp(name, "summary-branches")) 181 else if (!strcmp(name, "summary-branches"))
85 ctx.cfg.summary_branches = atoi(value); 182 ctx.cfg.summary_branches = atoi(value);
@@ -94,40 +191,11 @@ void config_cb(const char *name, const char *value)
94 else if (!strcmp(name, "clone-prefix")) 191 else if (!strcmp(name, "clone-prefix"))
95 ctx.cfg.clone_prefix = xstrdup(value); 192 ctx.cfg.clone_prefix = xstrdup(value);
96 else if (!strcmp(name, "local-time")) 193 else if (!strcmp(name, "local-time"))
97 ctx.cfg.local_time = atoi(value); 194 ctx.cfg.local_time = atoi(value);
98 else if (!strcmp(name, "repo.group")) 195 else if (!prefixcmp(name, "mimetype."))
99 ctx.cfg.repo_group = xstrdup(value); 196 add_mimetype(name + 9, value);
100 else if (!strcmp(name, "repo.url")) 197 else if (!strcmp(name, "include"))
101 ctx.repo = cgit_add_repo(value);
102 else if (!strcmp(name, "repo.name"))
103 ctx.repo->name = xstrdup(value);
104 else if (ctx.repo && !strcmp(name, "repo.path"))
105 ctx.repo->path = trim_end(value, '/');
106 else if (ctx.repo && !strcmp(name, "repo.clone-url"))
107 ctx.repo->clone_url = xstrdup(value);
108 else if (ctx.repo && !strcmp(name, "repo.desc"))
109 ctx.repo->desc = xstrdup(value);
110 else if (ctx.repo && !strcmp(name, "repo.owner"))
111 ctx.repo->owner = xstrdup(value);
112 else if (ctx.repo && !strcmp(name, "repo.defbranch"))
113 ctx.repo->defbranch = xstrdup(value);
114 else if (ctx.repo && !strcmp(name, "repo.snapshots"))
115 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
116 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
117 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
118 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
119 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
120 else if (ctx.repo && !strcmp(name, "repo.max-stats"))
121 ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
122 else if (ctx.repo && !strcmp(name, "repo.module-link"))
123 ctx.repo->module_link= xstrdup(value);
124 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
125 if (*value == '/')
126 ctx.repo->readme = xstrdup(value);
127 else
128 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
129 } else if (!strcmp(name, "include"))
130 parse_configfile(value, config_cb); 198 parse_configfile(value, config_cb);
131} 199}
132 200
133static void querystring_cb(const char *name, const char *value) 201static void querystring_cb(const char *name, const char *value)
@@ -172,8 +240,13 @@ static void querystring_cb(const char *name, const char *value)
172 ctx.qry.period = xstrdup(value); 240 ctx.qry.period = xstrdup(value);
173 } 241 }
174} 242}
175 243
244char *xstrdupn(const char *str)
245{
246 return (str ? xstrdup(str) : NULL);
247}
248
176static void prepare_context(struct cgit_context *ctx) 249static void prepare_context(struct cgit_context *ctx)
177{ 250{
178 memset(ctx, 0, sizeof(ctx)); 251 memset(ctx, 0, sizeof(ctx));
179 ctx->cfg.agefile = "info/web/last-modified"; 252 ctx->cfg.agefile = "info/web/last-modified";
@@ -183,12 +256,14 @@ static void prepare_context(struct cgit_context *ctx)
183 ctx->cfg.cache_max_create_time = 5; 256 ctx->cfg.cache_max_create_time = 5;
184 ctx->cfg.cache_repo_ttl = 5; 257 ctx->cfg.cache_repo_ttl = 5;
185 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 258 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
186 ctx->cfg.cache_root_ttl = 5; 259 ctx->cfg.cache_root_ttl = 5;
260 ctx->cfg.cache_scanrc_ttl = 15;
187 ctx->cfg.cache_static_ttl = -1; 261 ctx->cfg.cache_static_ttl = -1;
188 ctx->cfg.css = "/cgit.css"; 262 ctx->cfg.css = "/cgit.css";
189 ctx->cfg.logo = "/git-logo.png"; 263 ctx->cfg.logo = "/cgit.png";
190 ctx->cfg.local_time = 0; 264 ctx->cfg.local_time = 0;
265 ctx->cfg.enable_tree_linenumbers = 1;
191 ctx->cfg.max_repo_count = 50; 266 ctx->cfg.max_repo_count = 50;
192 ctx->cfg.max_commit_count = 50; 267 ctx->cfg.max_commit_count = 50;
193 ctx->cfg.max_lock_attempts = 5; 268 ctx->cfg.max_lock_attempts = 5;
194 ctx->cfg.max_msg_len = 80; 269 ctx->cfg.max_msg_len = 80;
@@ -199,17 +274,36 @@ static void prepare_context(struct cgit_context *ctx)
199 ctx->cfg.robots = "index, nofollow"; 274 ctx->cfg.robots = "index, nofollow";
200 ctx->cfg.root_title = "Git repository browser"; 275 ctx->cfg.root_title = "Git repository browser";
201 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 276 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
202 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 277 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
278 ctx->cfg.section = "";
203 ctx->cfg.summary_branches = 10; 279 ctx->cfg.summary_branches = 10;
204 ctx->cfg.summary_log = 10; 280 ctx->cfg.summary_log = 10;
205 ctx->cfg.summary_tags = 10; 281 ctx->cfg.summary_tags = 10;
282 ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG"));
283 ctx->env.http_host = xstrdupn(getenv("HTTP_HOST"));
284 ctx->env.https = xstrdupn(getenv("HTTPS"));
285 ctx->env.no_http = xstrdupn(getenv("NO_HTTP"));
286 ctx->env.path_info = xstrdupn(getenv("PATH_INFO"));
287 ctx->env.query_string = xstrdupn(getenv("QUERY_STRING"));
288 ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD"));
289 ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME"));
290 ctx->env.server_name = xstrdupn(getenv("SERVER_NAME"));
291 ctx->env.server_port = xstrdupn(getenv("SERVER_PORT"));
206 ctx->page.mimetype = "text/html"; 292 ctx->page.mimetype = "text/html";
207 ctx->page.charset = PAGE_ENCODING; 293 ctx->page.charset = PAGE_ENCODING;
208 ctx->page.filename = NULL; 294 ctx->page.filename = NULL;
209 ctx->page.size = 0; 295 ctx->page.size = 0;
210 ctx->page.modified = time(NULL); 296 ctx->page.modified = time(NULL);
211 ctx->page.expires = ctx->page.modified; 297 ctx->page.expires = ctx->page.modified;
298 ctx->page.etag = NULL;
299 memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list));
300 if (ctx->env.script_name)
301 ctx->cfg.script_name = ctx->env.script_name;
302 if (ctx->env.query_string)
303 ctx->qry.raw = ctx->env.query_string;
304 if (!ctx->env.cgit_config)
305 ctx->env.cgit_config = CGIT_CONFIG;
212} 306}
213 307
214struct refmatch { 308struct refmatch {
215 char *req_ref; 309 char *req_ref;
@@ -287,8 +381,10 @@ static int prepare_repo_cmd(struct cgit_context *ctx)
287 381
288 if (get_sha1(ctx->qry.head, sha1)) { 382 if (get_sha1(ctx->qry.head, sha1)) {
289 tmp = xstrdup(ctx->qry.head); 383 tmp = xstrdup(ctx->qry.head);
290 ctx->qry.head = ctx->repo->defbranch; 384 ctx->qry.head = ctx->repo->defbranch;
385 ctx->page.status = 404;
386 ctx->page.statusmsg = "not found";
291 cgit_print_http_headers(ctx); 387 cgit_print_http_headers(ctx);
292 cgit_print_docstart(ctx); 388 cgit_print_docstart(ctx);
293 cgit_print_pageheader(ctx); 389 cgit_print_pageheader(ctx);
294 cgit_print_error(fmt("Invalid branch: %s", tmp)); 390 cgit_print_error(fmt("Invalid branch: %s", tmp));
@@ -343,30 +439,153 @@ int cmp_repos(const void *a, const void *b)
343 const struct cgit_repo *ra = a, *rb = b; 439 const struct cgit_repo *ra = a, *rb = b;
344 return strcmp(ra->url, rb->url); 440 return strcmp(ra->url, rb->url);
345} 441}
346 442
347void print_repo(struct cgit_repo *repo) 443char *build_snapshot_setting(int bitmap)
348{ 444{
349 printf("repo.url=%s\n", repo->url); 445 const struct cgit_snapshot_format *f;
350 printf("repo.name=%s\n", repo->name); 446 char *result = xstrdup("");
351 printf("repo.path=%s\n", repo->path); 447 char *tmp;
448 int len;
449
450 for (f = cgit_snapshot_formats; f->suffix; f++) {
451 if (f->bit & bitmap) {
452 tmp = result;
453 result = xstrdup(fmt("%s%s ", tmp, f->suffix));
454 free(tmp);
455 }
456 }
457 len = strlen(result);
458 if (len)
459 result[len - 1] = '\0';
460 return result;
461}
462
463char *get_first_line(char *txt)
464{
465 char *t = xstrdup(txt);
466 char *p = strchr(t, '\n');
467 if (p)
468 *p = '\0';
469 return t;
470}
471
472void print_repo(FILE *f, struct cgit_repo *repo)
473{
474 fprintf(f, "repo.url=%s\n", repo->url);
475 fprintf(f, "repo.name=%s\n", repo->name);
476 fprintf(f, "repo.path=%s\n", repo->path);
352 if (repo->owner) 477 if (repo->owner)
353 printf("repo.owner=%s\n", repo->owner); 478 fprintf(f, "repo.owner=%s\n", repo->owner);
354 if (repo->desc) 479 if (repo->desc) {
355 printf("repo.desc=%s\n", repo->desc); 480 char *tmp = get_first_line(repo->desc);
481 fprintf(f, "repo.desc=%s\n", tmp);
482 free(tmp);
483 }
356 if (repo->readme) 484 if (repo->readme)
357 printf("repo.readme=%s\n", repo->readme); 485 fprintf(f, "repo.readme=%s\n", repo->readme);
358 printf("\n"); 486 if (repo->defbranch)
487 fprintf(f, "repo.defbranch=%s\n", repo->defbranch);
488 if (repo->module_link)
489 fprintf(f, "repo.module-link=%s\n", repo->module_link);
490 if (repo->section)
491 fprintf(f, "repo.section=%s\n", repo->section);
492 if (repo->clone_url)
493 fprintf(f, "repo.clone-url=%s\n", repo->clone_url);
494 fprintf(f, "repo.enable-log-filecount=%d\n",
495 repo->enable_log_filecount);
496 fprintf(f, "repo.enable-log-linecount=%d\n",
497 repo->enable_log_linecount);
498 if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter)
499 fprintf(f, "repo.about-filter=%s\n", repo->about_filter->cmd);
500 if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter)
501 fprintf(f, "repo.commit-filter=%s\n", repo->commit_filter->cmd);
502 if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter)
503 fprintf(f, "repo.source-filter=%s\n", repo->source_filter->cmd);
504 if (repo->snapshots != ctx.cfg.snapshots) {
505 char *tmp = build_snapshot_setting(repo->snapshots);
506 fprintf(f, "repo.snapshots=%s\n", tmp);
507 free(tmp);
508 }
509 if (repo->max_stats != ctx.cfg.max_stats)
510 fprintf(f, "repo.max-stats=%s\n",
511 cgit_find_stats_periodname(repo->max_stats));
512 fprintf(f, "\n");
359} 513}
360 514
361void print_repolist(struct cgit_repolist *list) 515void print_repolist(FILE *f, struct cgit_repolist *list, int start)
362{ 516{
363 int i; 517 int i;
364 518
365 for(i = 0; i < list->count; i++) 519 for(i = start; i < list->count; i++)
366 print_repo(&list->repos[i]); 520 print_repo(f, &list->repos[i]);
521}
522
523/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc'
524 * and return 0 on success.
525 */
526static int generate_cached_repolist(const char *path, const char *cached_rc)
527{
528 char *locked_rc;
529 int idx;
530 FILE *f;
531
532 locked_rc = xstrdup(fmt("%s.lock", cached_rc));
533 f = fopen(locked_rc, "wx");
534 if (!f) {
535 /* Inform about the error unless the lockfile already existed,
536 * since that only means we've got concurrent requests.
537 */
538 if (errno != EEXIST)
539 fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n",
540 locked_rc, strerror(errno), errno);
541 return errno;
542 }
543 idx = cgit_repolist.count;
544 scan_tree(path, repo_config);
545 print_repolist(f, &cgit_repolist, idx);
546 if (rename(locked_rc, cached_rc))
547 fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n",
548 locked_rc, cached_rc, strerror(errno), errno);
549 fclose(f);
550 return 0;
367} 551}
368 552
553static void process_cached_repolist(const char *path)
554{
555 struct stat st;
556 char *cached_rc;
557 time_t age;
558
559 cached_rc = xstrdup(fmt("%s/rc-%8x", ctx.cfg.cache_root,
560 hash_str(path)));
561
562 if (stat(cached_rc, &st)) {
563 /* Nothing is cached, we need to scan without forking. And
564 * if we fail to generate a cached repolist, we need to
565 * invoke scan_tree manually.
566 */
567 if (generate_cached_repolist(path, cached_rc))
568 scan_tree(path, repo_config);
569 return;
570 }
571
572 parse_configfile(cached_rc, config_cb);
573
574 /* If the cached configfile hasn't expired, lets exit now */
575 age = time(NULL) - st.st_mtime;
576 if (age <= (ctx.cfg.cache_scanrc_ttl * 60))
577 return;
578
579 /* The cached repolist has been parsed, but it was old. So lets
580 * rescan the specified path and generate a new cached repolist
581 * in a child-process to avoid latency for the current request.
582 */
583 if (fork())
584 return;
585
586 exit(generate_cached_repolist(path, cached_rc));
587}
369 588
370static void cgit_parse_args(int argc, const char **argv) 589static void cgit_parse_args(int argc, const char **argv)
371{ 590{
372 int i; 591 int i;
@@ -378,8 +597,11 @@ static void cgit_parse_args(int argc, const char **argv)
378 } 597 }
379 if (!strcmp(argv[i], "--nocache")) { 598 if (!strcmp(argv[i], "--nocache")) {
380 ctx.cfg.nocache = 1; 599 ctx.cfg.nocache = 1;
381 } 600 }
601 if (!strcmp(argv[i], "--nohttp")) {
602 ctx.env.no_http = "1";
603 }
382 if (!strncmp(argv[i], "--query=", 8)) { 604 if (!strncmp(argv[i], "--query=", 8)) {
383 ctx.qry.raw = xstrdup(argv[i]+8); 605 ctx.qry.raw = xstrdup(argv[i]+8);
384 } 606 }
385 if (!strncmp(argv[i], "--repo=", 7)) { 607 if (!strncmp(argv[i], "--repo=", 7)) {
@@ -398,17 +620,28 @@ static void cgit_parse_args(int argc, const char **argv)
398 } 620 }
399 if (!strncmp(argv[i], "--ofs=", 6)) { 621 if (!strncmp(argv[i], "--ofs=", 6)) {
400 ctx.qry.ofs = atoi(argv[i]+6); 622 ctx.qry.ofs = atoi(argv[i]+6);
401 } 623 }
402 if (!strncmp(argv[i], "--scan-tree=", 12)) { 624 if (!strncmp(argv[i], "--scan-tree=", 12) ||
625 !strncmp(argv[i], "--scan-path=", 12)) {
626 /* HACK: the global snapshot bitmask defines the
627 * set of allowed snapshot formats, but the config
628 * file hasn't been parsed yet so the mask is
629 * currently 0. By setting all bits high before
630 * scanning we make sure that any in-repo cgitrc
631 * snapshot setting is respected by scan_tree().
632 * BTW: we assume that there'll never be more than
633 * 255 different snapshot formats supported by cgit...
634 */
635 ctx.cfg.snapshots = 0xFF;
403 scan++; 636 scan++;
404 scan_tree(argv[i] + 12); 637 scan_tree(argv[i] + 12, repo_config);
405 } 638 }
406 } 639 }
407 if (scan) { 640 if (scan) {
408 qsort(cgit_repolist.repos, cgit_repolist.count, 641 qsort(cgit_repolist.repos, cgit_repolist.count,
409 sizeof(struct cgit_repo), cmp_repos); 642 sizeof(struct cgit_repo), cmp_repos);
410 print_repolist(&cgit_repolist); 643 print_repolist(stdout, &cgit_repolist, 0);
411 exit(0); 644 exit(0);
412 } 645 }
413} 646}
414 647
@@ -430,9 +663,8 @@ static int calc_ttl()
430} 663}
431 664
432int main(int argc, const char **argv) 665int main(int argc, const char **argv)
433{ 666{
434 const char *cgit_config_env = getenv("CGIT_CONFIG");
435 const char *path; 667 const char *path;
436 char *qry; 668 char *qry;
437 int err, ttl; 669 int err, ttl;
438 670
@@ -440,15 +672,10 @@ int main(int argc, const char **argv)
440 cgit_repolist.length = 0; 672 cgit_repolist.length = 0;
441 cgit_repolist.count = 0; 673 cgit_repolist.count = 0;
442 cgit_repolist.repos = NULL; 674 cgit_repolist.repos = NULL;
443 675
444 if (getenv("SCRIPT_NAME"))
445 ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME"));
446 if (getenv("QUERY_STRING"))
447 ctx.qry.raw = xstrdup(getenv("QUERY_STRING"));
448 cgit_parse_args(argc, argv); 676 cgit_parse_args(argc, argv);
449 parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG, 677 parse_configfile(ctx.env.cgit_config, config_cb);
450 config_cb);
451 ctx.repo = NULL; 678 ctx.repo = NULL;
452 http_parse_querystring(ctx.qry.raw, querystring_cb); 679 http_parse_querystring(ctx.qry.raw, querystring_cb);
453 680
454 /* If virtual-root isn't specified in cgitrc, lets pretend 681 /* If virtual-root isn't specified in cgitrc, lets pretend
@@ -461,9 +688,9 @@ int main(int argc, const char **argv)
461 * use PATH_INFO as url. This allows cgit to work with virtual 688 * use PATH_INFO as url. This allows cgit to work with virtual
462 * urls without the need for rewriterules in the webserver (as 689 * urls without the need for rewriterules in the webserver (as
463 * long as PATH_INFO is included in the cache lookup key). 690 * long as PATH_INFO is included in the cache lookup key).
464 */ 691 */
465 path = getenv("PATH_INFO"); 692 path = ctx.env.path_info;
466 if (!ctx.qry.url && path) { 693 if (!ctx.qry.url && path) {
467 if (path[0] == '/') 694 if (path[0] == '/')
468 path++; 695 path++;
469 ctx.qry.url = xstrdup(path); 696 ctx.qry.url = xstrdup(path);
@@ -471,14 +698,16 @@ int main(int argc, const char **argv)
471 qry = ctx.qry.raw; 698 qry = ctx.qry.raw;
472 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 699 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
473 free(qry); 700 free(qry);
474 } else 701 } else
475 ctx.qry.raw = ctx.qry.url; 702 ctx.qry.raw = xstrdup(ctx.qry.url);
476 cgit_parse_url(ctx.qry.url); 703 cgit_parse_url(ctx.qry.url);
477 } 704 }
478 705
479 ttl = calc_ttl(); 706 ttl = calc_ttl();
480 ctx.page.expires += ttl*60; 707 ctx.page.expires += ttl*60;
708 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
709 ctx.cfg.nocache = 1;
481 if (ctx.cfg.nocache) 710 if (ctx.cfg.nocache)
482 ctx.cfg.cache_size = 0; 711 ctx.cfg.cache_size = 0;
483 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 712 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
484 ctx.qry.raw, ttl, process_request, &ctx); 713 ctx.qry.raw, ttl, process_request, &ctx);
diff --git a/cgit.css b/cgit.css
index adfc8ae..c47ebc9 100644
--- a/cgit.css
+++ b/cgit.css
@@ -154,9 +154,9 @@ table.list td.logsubject {
154 154
155table.list td.logmsg { 155table.list td.logmsg {
156 font-family: monospace; 156 font-family: monospace;
157 white-space: pre; 157 white-space: pre;
158 padding: 1em 0em 2em 0em; 158 padding: 1em 0.5em 2em 0.5em;
159} 159}
160 160
161table.list td a { 161table.list td a {
162 color: black; 162 color: black;
@@ -236,18 +236,18 @@ table.blob {
236 border-top: solid 1px black; 236 border-top: solid 1px black;
237} 237}
238 238
239table.blob td.lines { 239table.blob td.lines {
240 margin: 0; padding: 0; 240 margin: 0; padding: 0 0 0 0.5em;
241 vertical-align: top; 241 vertical-align: top;
242 color: black; 242 color: black;
243} 243}
244 244
245table.blob td.linenumbers { 245table.blob td.linenumbers {
246 margin: 0; padding: 0; 246 margin: 0; padding: 0 0.5em 0 0.5em;
247 vertical-align: top; 247 vertical-align: top;
248 text-align: right;
248 border-right: 1px solid gray; 249 border-right: 1px solid gray;
249 background-color: #eee;
250} 250}
251 251
252table.blob pre { 252table.blob pre {
253 padding: 0; margin: 0; 253 padding: 0; margin: 0;
@@ -428,9 +428,9 @@ table.diff td div.del {
428.right { 428.right {
429 text-align: right; 429 text-align: right;
430} 430}
431 431
432table.list td.repogroup { 432table.list td.reposection {
433 font-style: italic; 433 font-style: italic;
434 color: #888; 434 color: #888;
435} 435}
436 436
diff --git a/cgit.h b/cgit.h
index 5f7af51..6c6c460 100644
--- a/cgit.h
+++ b/cgit.h
@@ -14,8 +14,9 @@
14#include <refs.h> 14#include <refs.h>
15#include <revision.h> 15#include <revision.h>
16#include <log-tree.h> 16#include <log-tree.h>
17#include <archive.h> 17#include <archive.h>
18#include <string-list.h>
18#include <xdiff-interface.h> 19#include <xdiff-interface.h>
19#include <xdiff/xdiff.h> 20#include <xdiff/xdiff.h>
20#include <utf8.h> 21#include <utf8.h>
21 22
@@ -47,26 +48,41 @@
47typedef void (*configfn)(const char *name, const char *value); 48typedef void (*configfn)(const char *name, const char *value);
48typedef void (*filepair_fn)(struct diff_filepair *pair); 49typedef void (*filepair_fn)(struct diff_filepair *pair);
49typedef void (*linediff_fn)(char *line, int len); 50typedef void (*linediff_fn)(char *line, int len);
50 51
52struct cgit_filter {
53 char *cmd;
54 char **argv;
55 int old_stdout;
56 int pipe_fh[2];
57 int pid;
58 int exitstatus;
59};
60
51struct cgit_repo { 61struct cgit_repo {
52 char *url; 62 char *url;
53 char *name; 63 char *name;
54 char *path; 64 char *path;
55 char *desc; 65 char *desc;
56 char *owner; 66 char *owner;
57 char *defbranch; 67 char *defbranch;
58 char *group;
59 char *module_link; 68 char *module_link;
60 char *readme; 69 char *readme;
70 char *section;
61 char *clone_url; 71 char *clone_url;
62 int snapshots; 72 int snapshots;
63 int enable_log_filecount; 73 int enable_log_filecount;
64 int enable_log_linecount; 74 int enable_log_linecount;
65 int max_stats; 75 int max_stats;
66 time_t mtime; 76 time_t mtime;
77 struct cgit_filter *about_filter;
78 struct cgit_filter *commit_filter;
79 struct cgit_filter *source_filter;
67}; 80};
68 81
82typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name,
83 const char *value);
84
69struct cgit_repolist { 85struct cgit_repolist {
70 int length; 86 int length;
71 int count; 87 int count;
72 struct cgit_repo *repos; 88 struct cgit_repo *repos;
@@ -135,43 +151,54 @@ struct cgit_config {
135 char *clone_prefix; 151 char *clone_prefix;
136 char *css; 152 char *css;
137 char *favicon; 153 char *favicon;
138 char *footer; 154 char *footer;
155 char *head_include;
139 char *header; 156 char *header;
140 char *index_header; 157 char *index_header;
141 char *index_info; 158 char *index_info;
142 char *logo; 159 char *logo;
143 char *logo_link; 160 char *logo_link;
144 char *module_link; 161 char *module_link;
145 char *repo_group;
146 char *robots; 162 char *robots;
147 char *root_title; 163 char *root_title;
148 char *root_desc; 164 char *root_desc;
149 char *root_readme; 165 char *root_readme;
150 char *script_name; 166 char *script_name;
167 char *section;
151 char *virtual_root; 168 char *virtual_root;
152 int cache_size; 169 int cache_size;
153 int cache_dynamic_ttl; 170 int cache_dynamic_ttl;
154 int cache_max_create_time; 171 int cache_max_create_time;
155 int cache_repo_ttl; 172 int cache_repo_ttl;
156 int cache_root_ttl; 173 int cache_root_ttl;
174 int cache_scanrc_ttl;
157 int cache_static_ttl; 175 int cache_static_ttl;
176 int embedded;
177 int enable_filter_overrides;
158 int enable_index_links; 178 int enable_index_links;
159 int enable_log_filecount; 179 int enable_log_filecount;
160 int enable_log_linecount; 180 int enable_log_linecount;
181 int enable_tree_linenumbers;
161 int local_time; 182 int local_time;
162 int max_repo_count; 183 int max_repo_count;
163 int max_commit_count; 184 int max_commit_count;
164 int max_lock_attempts; 185 int max_lock_attempts;
165 int max_msg_len; 186 int max_msg_len;
166 int max_repodesc_len; 187 int max_repodesc_len;
167 int max_stats; 188 int max_stats;
168 int nocache; 189 int nocache;
190 int noplainemail;
191 int noheader;
169 int renamelimit; 192 int renamelimit;
170 int snapshots; 193 int snapshots;
171 int summary_branches; 194 int summary_branches;
172 int summary_log; 195 int summary_log;
173 int summary_tags; 196 int summary_tags;
197 struct string_list mimetypes;
198 struct cgit_filter *about_filter;
199 struct cgit_filter *commit_filter;
200 struct cgit_filter *source_filter;
174}; 201};
175 202
176struct cgit_page { 203struct cgit_page {
177 time_t modified; 204 time_t modified;
@@ -179,12 +206,29 @@ struct cgit_page {
179 size_t size; 206 size_t size;
180 char *mimetype; 207 char *mimetype;
181 char *charset; 208 char *charset;
182 char *filename; 209 char *filename;
210 char *etag;
183 char *title; 211 char *title;
212 int status;
213 char *statusmsg;
214};
215
216struct cgit_environment {
217 char *cgit_config;
218 char *http_host;
219 char *https;
220 char *no_http;
221 char *path_info;
222 char *query_string;
223 char *request_method;
224 char *script_name;
225 char *server_name;
226 char *server_port;
184}; 227};
185 228
186struct cgit_context { 229struct cgit_context {
230 struct cgit_environment env;
187 struct cgit_query qry; 231 struct cgit_query qry;
188 struct cgit_config cfg; 232 struct cgit_config cfg;
189 struct cgit_repo *repo; 233 struct cgit_repo *repo;
190 struct cgit_page page; 234 struct cgit_page page;
@@ -241,6 +285,10 @@ extern void cgit_parse_url(const char *url);
241extern const char *cgit_repobasename(const char *reponame); 285extern const char *cgit_repobasename(const char *reponame);
242 286
243extern int cgit_parse_snapshots_mask(const char *str); 287extern int cgit_parse_snapshots_mask(const char *str);
244 288
289extern int cgit_open_filter(struct cgit_filter *filter);
290extern int cgit_close_filter(struct cgit_filter *filter);
291
292extern int readfile(const char *path, char **buf, size_t *size);
245 293
246#endif /* CGIT_H */ 294#endif /* CGIT_H */
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index fd299ae..4dc383d 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -1,204 +1,270 @@
1CGITRC 1CGITRC(5)
2====== 2========
3 3
4 4
5NAME 5NAME
6---- 6----
7 cgitrc - runtime configuration for cgit 7cgitrc - runtime configuration for cgit
8 8
9 9
10DESCRIPTION 10SYNOPSIS
11----------- 11--------
12Cgitrc contains all runtime settings for cgit, including the list of git 12Cgitrc contains all runtime settings for cgit, including the list of git
13repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank 13repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank
14lines, and lines starting with '#', are ignored. 14lines, and lines starting with '#', are ignored.
15 15
16 16
17LOCATION
18--------
19The default location of cgitrc, defined at compile time, is /etc/cgitrc. At
20runtime, cgit will consult the environment variable CGIT_CONFIG and, if
21defined, use its value instead.
22
23
17GLOBAL SETTINGS 24GLOBAL SETTINGS
18--------------- 25---------------
19agefile 26about-filter::
27 Specifies a command which will be invoked to format the content of
28 about pages (both top-level and for each repository). The command will
29 get the content of the about-file on its STDIN, and the STDOUT from the
30 command will be included verbatim on the about page. Default value:
31 none.
32
33agefile::
20 Specifies a path, relative to each repository path, which can be used 34 Specifies a path, relative to each repository path, which can be used
21 to specify the date and time of the youngest commit in the repository. 35 to specify the date and time of the youngest commit in the repository.
22 The first line in the file is used as input to the "parse_date" 36 The first line in the file is used as input to the "parse_date"
23 function in libgit. Recommended timestamp-format is "yyyy-mm-dd 37 function in libgit. Recommended timestamp-format is "yyyy-mm-dd
24 hh:mm:ss". Default value: "info/web/last-modified". 38 hh:mm:ss". Default value: "info/web/last-modified".
25 39
26cache-root 40cache-root::
27 Path used to store the cgit cache entries. Default value: 41 Path used to store the cgit cache entries. Default value:
28 "/var/cache/cgit". 42 "/var/cache/cgit".
29 43
30cache-dynamic-ttl 44cache-dynamic-ttl::
31 Number which specifies the time-to-live, in minutes, for the cached 45 Number which specifies the time-to-live, in minutes, for the cached
32 version of repository pages accessed without a fixed SHA1. Default 46 version of repository pages accessed without a fixed SHA1. Default
33 value: "5". 47 value: "5".
34 48
35cache-repo-ttl 49cache-repo-ttl::
36 Number which specifies the time-to-live, in minutes, for the cached 50 Number which specifies the time-to-live, in minutes, for the cached
37 version of the repository summary page. Default value: "5". 51 version of the repository summary page. Default value: "5".
38 52
39cache-root-ttl 53cache-root-ttl::
40 Number which specifies the time-to-live, in minutes, for the cached 54 Number which specifies the time-to-live, in minutes, for the cached
41 version of the repository index page. Default value: "5". 55 version of the repository index page. Default value: "5".
42 56
43cache-size 57cache-scanrc-ttl::
58 Number which specifies the time-to-live, in minutes, for the result
59 of scanning a path for git repositories. Default value: "15".
60
61cache-size::
44 The maximum number of entries in the cgit cache. Default value: "0" 62 The maximum number of entries in the cgit cache. Default value: "0"
45 (i.e. caching is disabled). 63 (i.e. caching is disabled).
46 64
47cache-static-ttl 65cache-static-ttl::
48 Number which specifies the time-to-live, in minutes, for the cached 66 Number which specifies the time-to-live, in minutes, for the cached
49 version of repository pages accessed with a fixed SHA1. Default value: 67 version of repository pages accessed with a fixed SHA1. Default value:
50 "5". 68 "5".
51 69
52clone-prefix 70clone-prefix::
53 Space-separated list of common prefixes which, when combined with a 71 Space-separated list of common prefixes which, when combined with a
54 repository url, generates valid clone urls for the repository. This 72 repository url, generates valid clone urls for the repository. This
55 setting is only used if `repo.clone-url` is unspecified. Default value: 73 setting is only used if `repo.clone-url` is unspecified. Default value:
56 none. 74 none.
57 75
58css 76commit-filter::
77 Specifies a command which will be invoked to format commit messages.
78 The command will get the message on its STDIN, and the STDOUT from the
79 command will be included verbatim as the commit message, i.e. this can
80 be used to implement bugtracker integration. Default value: none.
81
82css::
59 Url which specifies the css document to include in all cgit pages. 83 Url which specifies the css document to include in all cgit pages.
60 Default value: "/cgit.css". 84 Default value: "/cgit.css".
61 85
62enable-index-links 86embedded::
87 Flag which, when set to "1", will make cgit generate a html fragment
88 suitable for embedding in other html pages. Default value: none. See
89 also: "noheader".
90
91enable-filter-overrides::
92 Flag which, when set to "1", allows all filter settings to be
93 overridden in repository-specific cgitrc files. Default value: none.
94
95enable-index-links::
63 Flag which, when set to "1", will make cgit generate extra links for 96 Flag which, when set to "1", will make cgit generate extra links for
64 each repo in the repository index (specifically, to the "summary", 97 each repo in the repository index (specifically, to the "summary",
65 "commit" and "tree" pages). Default value: "0". 98 "commit" and "tree" pages). Default value: "0".
66 99
67enable-log-filecount 100enable-log-filecount::
68 Flag which, when set to "1", will make cgit print the number of 101 Flag which, when set to "1", will make cgit print the number of
69 modified files for each commit on the repository log page. Default 102 modified files for each commit on the repository log page. Default
70 value: "0". 103 value: "0".
71 104
72enable-log-linecount 105enable-log-linecount::
73 Flag which, when set to "1", will make cgit print the number of added 106 Flag which, when set to "1", will make cgit print the number of added
74 and removed lines for each commit on the repository log page. Default 107 and removed lines for each commit on the repository log page. Default
75 value: "0". 108 value: "0".
76 109
77favicon 110enable-tree-linenumbers::
111 Flag which, when set to "1", will make cgit generate linenumber links
112 for plaintext blobs printed in the tree view. Default value: "1".
113
114favicon::
78 Url used as link to a shortcut icon for cgit. If specified, it is 115 Url used as link to a shortcut icon for cgit. If specified, it is
79 suggested to use the value "/favicon.ico" since certain browsers will 116 suggested to use the value "/favicon.ico" since certain browsers will
80 ignore other values. Default value: none. 117 ignore other values. Default value: none.
81 118
82footer 119footer::
83 The content of the file specified with this option will be included 120 The content of the file specified with this option will be included
84 verbatim at the bottom of all pages (i.e. it replaces the standard 121 verbatim at the bottom of all pages (i.e. it replaces the standard
85 "generated by..." message. Default value: none. 122 "generated by..." message. Default value: none.
86 123
87header 124head-include::
125 The content of the file specified with this option will be included
126 verbatim in the html HEAD section on all pages. Default value: none.
127
128header::
88 The content of the file specified with this option will be included 129 The content of the file specified with this option will be included
89 verbatim at the top of all pages. Default value: none. 130 verbatim at the top of all pages. Default value: none.
90 131
91include 132include::
92 Name of a configfile to include before the rest of the current config- 133 Name of a configfile to include before the rest of the current config-
93 file is parsed. Default value: none. 134 file is parsed. Default value: none.
94 135
95index-header 136index-header::
96 The content of the file specified with this option will be included 137 The content of the file specified with this option will be included
97 verbatim above the repository index. This setting is deprecated, and 138 verbatim above the repository index. This setting is deprecated, and
98 will not be supported by cgit-1.0 (use root-readme instead). Default 139 will not be supported by cgit-1.0 (use root-readme instead). Default
99 value: none. 140 value: none.
100 141
101index-info 142index-info::
102 The content of the file specified with this option will be included 143 The content of the file specified with this option will be included
103 verbatim below the heading on the repository index page. This setting 144 verbatim below the heading on the repository index page. This setting
104 is deprecated, and will not be supported by cgit-1.0 (use root-desc 145 is deprecated, and will not be supported by cgit-1.0 (use root-desc
105 instead). Default value: none. 146 instead). Default value: none.
106 147
107local-time 148local-time::
108 Flag which, if set to "1", makes cgit print commit and tag times in the 149 Flag which, if set to "1", makes cgit print commit and tag times in the
109 servers timezone. Default value: "0". 150 servers timezone. Default value: "0".
110 151
111logo 152logo::
112 Url which specifies the source of an image which will be used as a logo 153 Url which specifies the source of an image which will be used as a logo
113 on all cgit pages. 154 on all cgit pages. Default value: "/cgit.png".
114 155
115logo-link 156logo-link::
116 Url loaded when clicking on the cgit logo image. If unspecified the 157 Url loaded when clicking on the cgit logo image. If unspecified the
117 calculated url of the repository index page will be used. Default 158 calculated url of the repository index page will be used. Default
118 value: none. 159 value: none.
119 160
120max-commit-count 161max-commit-count::
121 Specifies the number of entries to list per page in "log" view. Default 162 Specifies the number of entries to list per page in "log" view. Default
122 value: "50". 163 value: "50".
123 164
124max-message-length 165max-message-length::
125 Specifies the maximum number of commit message characters to display in 166 Specifies the maximum number of commit message characters to display in
126 "log" view. Default value: "80". 167 "log" view. Default value: "80".
127 168
128max-repo-count 169max-repo-count::
129 Specifies the number of entries to list per page on therepository 170 Specifies the number of entries to list per page on therepository
130 index page. Default value: "50". 171 index page. Default value: "50".
131 172
132max-repodesc-length 173max-repodesc-length::
133 Specifies the maximum number of repo description characters to display 174 Specifies the maximum number of repo description characters to display
134 on the repository index page. Default value: "80". 175 on the repository index page. Default value: "80".
135 176
136max-stats 177max-stats::
137 Set the default maximum statistics period. Valid values are "week", 178 Set the default maximum statistics period. Valid values are "week",
138 "month", "quarter" and "year". If unspecified, statistics are 179 "month", "quarter" and "year". If unspecified, statistics are
139 disabled. Default value: none. See also: "repo.max-stats". 180 disabled. Default value: none. See also: "repo.max-stats".
140 181
141module-link 182mimetype.<ext>::
183 Set the mimetype for the specified filename extension. This is used
184 by the `plain` command when returning blob content.
185
186module-link::
142 Text which will be used as the formatstring for a hyperlink when a 187 Text which will be used as the formatstring for a hyperlink when a
143 submodule is printed in a directory listing. The arguments for the 188 submodule is printed in a directory listing. The arguments for the
144 formatstring are the path and SHA1 of the submodule commit. Default 189 formatstring are the path and SHA1 of the submodule commit. Default
145 value: "./?repo=%s&page=commit&id=%s" 190 value: "./?repo=%s&page=commit&id=%s"
146 191
147nocache 192nocache::
148 If set to the value "1" caching will be disabled. This settings is 193 If set to the value "1" caching will be disabled. This settings is
149 deprecated, and will not be honored starting with cgit-1.0. Default 194 deprecated, and will not be honored starting with cgit-1.0. Default
150 value: "0". 195 value: "0".
151 196
152renamelimit 197noplainemail::
198 If set to "1" showing full author email adresses will be disabled.
199 Default value: "0".
200
201noheader::
202 Flag which, when set to "1", will make cgit omit the standard header
203 on all pages. Default value: none. See also: "embedded".
204
205renamelimit::
153 Maximum number of files to consider when detecting renames. The value 206 Maximum number of files to consider when detecting renames. The value
154 "-1" uses the compiletime value in git (for further info, look at 207 "-1" uses the compiletime value in git (for further info, look at
155 `man git-diff`). Default value: "-1". 208 `man git-diff`). Default value: "-1".
156 209
157repo.group 210repo.group::
158 A value for the current repository group, which all repositories 211 Legacy alias for "section". This option is deprecated and will not be
159 specified after this setting will inherit. Default value: none. 212 supported in cgit-1.0.
160 213
161robots 214robots::
162 Text used as content for the "robots" meta-tag. Default value: 215 Text used as content for the "robots" meta-tag. Default value:
163 "index, nofollow". 216 "index, nofollow".
164 217
165root-desc 218root-desc::
166 Text printed below the heading on the repository index page. Default 219 Text printed below the heading on the repository index page. Default
167 value: "a fast webinterface for the git dscm". 220 value: "a fast webinterface for the git dscm".
168 221
169root-readme: 222root-readme::
170 The content of the file specified with this option will be included 223 The content of the file specified with this option will be included
171 verbatim below the "about" link on the repository index page. Default 224 verbatim below the "about" link on the repository index page. Default
172 value: none. 225 value: none.
173 226
174root-title 227root-title::
175 Text printed as heading on the repository index page. Default value: 228 Text printed as heading on the repository index page. Default value:
176 "Git Repository Browser". 229 "Git Repository Browser".
177 230
178snapshots 231scan-path::
179 Text which specifies the default (and allowed) set of snapshot formats 232 A path which will be scanned for repositories. If caching is enabled,
180 supported by cgit. The value is a space-separated list of zero or more 233 the result will be cached as a cgitrc include-file in the cache
181 of the following values: 234 directory. Default value: none. See also: cache-scanrc-ttl.
182 "tar" uncompressed tar-file 235
183 "tar.gz"gzip-compressed tar-file 236section::
184 "tar.bz2"bzip-compressed tar-file 237 The name of the current repository section - all repositories defined
185 "zip" zip-file 238 after this option will inherit the current section name. Default value:
186 Default value: none. 239 none.
187 240
188summary-branches 241snapshots::
242 Text which specifies the default set of snapshot formats generated by
243 cgit. The value is a space-separated list of zero or more of the
244 values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none.
245
246source-filter::
247 Specifies a command which will be invoked to format plaintext blobs
248 in the tree view. The command will get the blob content on its STDIN
249 and the name of the blob as its only command line argument. The STDOUT
250 from the command will be included verbatim as the blob contents, i.e.
251 this can be used to implement e.g. syntax highlighting. Default value:
252 none.
253
254summary-branches::
189 Specifies the number of branches to display in the repository "summary" 255 Specifies the number of branches to display in the repository "summary"
190 view. Default value: "10". 256 view. Default value: "10".
191 257
192summary-log 258summary-log::
193 Specifies the number of log entries to display in the repository 259 Specifies the number of log entries to display in the repository
194 "summary" view. Default value: "10". 260 "summary" view. Default value: "10".
195 261
196summary-tags 262summary-tags::
197 Specifies the number of tags to display in the repository "summary" 263 Specifies the number of tags to display in the repository "summary"
198 view. Default value: "10". 264 view. Default value: "10".
199 265
200virtual-root 266virtual-root::
201 Url which, if specified, will be used as root for all cgit links. It 267 Url which, if specified, will be used as root for all cgit links. It
202 will also cause cgit to generate 'virtual urls', i.e. urls like 268 will also cause cgit to generate 'virtual urls', i.e. urls like
203 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default 269 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default
204 value: none. 270 value: none.
@@ -206,60 +272,90 @@ virtual-root
206 same kind of virtual urls, so this option will probably be deprecated. 272 same kind of virtual urls, so this option will probably be deprecated.
207 273
208REPOSITORY SETTINGS 274REPOSITORY SETTINGS
209------------------- 275-------------------
210repo.clone-url 276repo.about-filter::
277 Override the default about-filter. Default value: none. See also:
278 "enable-filter-overrides".
279
280repo.clone-url::
211 A list of space-separated urls which can be used to clone this repo. 281 A list of space-separated urls which can be used to clone this repo.
212 Default value: none. 282 Default value: none.
213 283
214repo.defbranch 284repo.commit-filter::
285 Override the default commit-filter. Default value: none. See also:
286 "enable-filter-overrides".
287
288repo.defbranch::
215 The name of the default branch for this repository. If no such branch 289 The name of the default branch for this repository. If no such branch
216 exists in the repository, the first branch name (when sorted) is used 290 exists in the repository, the first branch name (when sorted) is used
217 as default instead. Default value: "master". 291 as default instead. Default value: "master".
218 292
219repo.desc 293repo.desc::
220 The value to show as repository description. Default value: none. 294 The value to show as repository description. Default value: none.
221 295
222repo.enable-log-filecount 296repo.enable-log-filecount::
223 A flag which can be used to disable the global setting 297 A flag which can be used to disable the global setting
224 `enable-log-filecount'. Default value: none. 298 `enable-log-filecount'. Default value: none.
225 299
226repo.enable-log-linecount 300repo.enable-log-linecount::
227 A flag which can be used to disable the global setting 301 A flag which can be used to disable the global setting
228 `enable-log-linecount'. Default value: none. 302 `enable-log-linecount'. Default value: none.
229 303
230repo.max-stats 304repo.max-stats::
231 Override the default maximum statistics period. Valid values are equal 305 Override the default maximum statistics period. Valid values are equal
232 to the values specified for the global "max-stats" setting. Default 306 to the values specified for the global "max-stats" setting. Default
233 value: none. 307 value: none.
234 308
235repo.name 309repo.name::
236 The value to show as repository name. Default value: <repo.url>. 310 The value to show as repository name. Default value: <repo.url>.
237 311
238repo.owner 312repo.owner::
239 A value used to identify the owner of the repository. Default value: 313 A value used to identify the owner of the repository. Default value:
240 none. 314 none.
241 315
242repo.path 316repo.path::
243 An absolute path to the repository directory. For non-bare repositories 317 An absolute path to the repository directory. For non-bare repositories
244 this is the .git-directory. Default value: none. 318 this is the .git-directory. Default value: none.
245 319
246repo.readme 320repo.readme::
247 A path (relative to <repo.path>) which specifies a file to include 321 A path (relative to <repo.path>) which specifies a file to include
248 verbatim as the "About" page for this repo. Default value: none. 322 verbatim as the "About" page for this repo. Default value: none.
249 323
250repo.snapshots 324repo.snapshots::
251 A mask of allowed snapshot-formats for this repo, restricted by the 325 A mask of allowed snapshot-formats for this repo, restricted by the
252 "snapshots" global setting. Default value: <snapshots>. 326 "snapshots" global setting. Default value: <snapshots>.
253 327
254repo.url 328repo.section::
329 Override the current section name for this repository. Default value:
330 none.
331
332repo.source-filter::
333 Override the default source-filter. Default value: none. See also:
334 "enable-filter-overrides".
335
336repo.url::
255 The relative url used to access the repository. This must be the first 337 The relative url used to access the repository. This must be the first
256 setting specified for each repo. Default value: none. 338 setting specified for each repo. Default value: none.
257 339
258 340
341REPOSITORY-SPECIFIC CGITRC FILE
342-------------------------------
343When the option "scan-path" is used to auto-discover git repositories, cgit
344will try to parse the file "cgitrc" within any found repository. Such a
345repo-specific config file may contain any of the repo-specific options
346described above, except "repo.url" and "repo.path". Additionally, the "filter"
347options are only acknowledged in repo-specific config files when
348"enable-filter-overrides" is set to "1".
349
350Note: the "repo." prefix is dropped from the option names in repo-specific
351config files, e.g. "repo.desc" becomes "desc".
352
353
259EXAMPLE CGITRC FILE 354EXAMPLE CGITRC FILE
260------------------- 355-------------------
261 356
357....
262# Enable caching of up to 1000 output entriess 358# Enable caching of up to 1000 output entriess
263cache-size=1000 359cache-size=1000
264 360
265 361
@@ -310,8 +406,21 @@ root-readme=/var/www/htdocs/about.html
310snapshots=tar.gz tar.bz2 zip 406snapshots=tar.gz tar.bz2 zip
311 407
312 408
313## 409##
410## List of common mimetypes
411##
412
413mimetype.git=image/git
414mimetype.html=text/html
415mimetype.jpg=image/jpeg
416mimetype.jpeg=image/jpeg
417mimetype.pdf=application/pdf
418mimetype.png=image/png
419mimetype.svg=image/svg+xml
420
421
422##
314## List of repositories. 423## List of repositories.
315## PS: Any repositories listed when repo.group is unset will not be 424## PS: Any repositories listed when repo.group is unset will not be
316## displayed under a group heading 425## displayed under a group heading
317## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos') 426## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos')
@@ -367,8 +476,9 @@ repo.snapshots=0
367repo.enable-log-linecount=0 476repo.enable-log-linecount=0
368 477
369# Restrict the max statistics period for this repo 478# Restrict the max statistics period for this repo
370repo.max-stats=month 479repo.max-stats=month
480....
371 481
372 482
373BUGS 483BUGS
374---- 484----
diff --git a/cmd.c b/cmd.c
index cf97da7..766f903 100644
--- a/cmd.c
+++ b/cmd.c
@@ -38,9 +38,9 @@ static void atom_fn(struct cgit_context *ctx)
38 38
39static void about_fn(struct cgit_context *ctx) 39static void about_fn(struct cgit_context *ctx)
40{ 40{
41 if (ctx->repo) 41 if (ctx->repo)
42 cgit_print_repo_readme(); 42 cgit_print_repo_readme(ctx->qry.path);
43 else 43 else
44 cgit_print_site_readme(); 44 cgit_print_site_readme();
45} 45}
46 46
diff --git a/filters/commit-links.sh b/filters/commit-links.sh
new file mode 100755
index 0000000..165a533
--- a/dev/null
+++ b/filters/commit-links.sh
@@ -0,0 +1,12 @@
1#!/bin/sh
2# This script can be used to generate links in commit messages - the first
3# sed expression generates links to commits referenced by their SHA1, while
4# the second expression generates links to a fictional bugtracker.
5#
6# To use this script, refer to this file with either the commit-filter or the
7# repo.commit-filter options in cgitrc.
8
9sed -re '
10s|\b([0-9a-fA-F]{8,40})\b|<a href="./?id=\1">\1</a>|g
11s| #([0-9]+)\b|<a href="http://bugs.example.com/?bug=\1">#\1</a>|g
12'
diff --git a/filters/syntax-highlighting.sh b/filters/syntax-highlighting.sh
new file mode 100755
index 0000000..999ad0c
--- a/dev/null
+++ b/filters/syntax-highlighting.sh
@@ -0,0 +1,39 @@
1#!/bin/sh
2# This script can be used to implement syntax highlighting in the cgit
3# tree-view by refering to this file with the source-filter or repo.source-
4# filter options in cgitrc.
5#
6# Note: the highlight command (http://www.andre-simon.de/) uses css for syntax
7# highlighting, so you'll probably want something like the following included
8# in your css file (generated by highlight 2.4.8 and adapted for cgit):
9#
10# table.blob .num { color:#2928ff; }
11# table.blob .esc { color:#ff00ff; }
12# table.blob .str { color:#ff0000; }
13# table.blob .dstr { color:#818100; }
14# table.blob .slc { color:#838183; font-style:italic; }
15# table.blob .com { color:#838183; font-style:italic; }
16# table.blob .dir { color:#008200; }
17# table.blob .sym { color:#000000; }
18# table.blob .kwa { color:#000000; font-weight:bold; }
19# table.blob .kwb { color:#830000; }
20# table.blob .kwc { color:#000000; font-weight:bold; }
21# table.blob .kwd { color:#010181; }
22
23case "$1" in
24 *.c)
25 highlight -f -I -X -S c
26 ;;
27 *.h)
28 highlight -f -I -X -S c
29 ;;
30 *.sh)
31 highlight -f -I -X -S sh
32 ;;
33 *.css)
34 highlight -f -I -X -S css
35 ;;
36 *)
37 highlight -f -I -X -S txt
38 ;;
39esac
diff --git a/git b/git
Subproject 5c415311f743ccb11a50f350ff1c385778f049d Subproject 7fb6bcff2dece2ff9fbc5ebfe526d9b2a7e764c
diff --git a/scan-tree.c b/scan-tree.c
index 47f3988..dbca797 100644
--- a/scan-tree.c
+++ b/scan-tree.c
@@ -1,5 +1,6 @@
1#include "cgit.h" 1#include "cgit.h"
2#include "configfile.h"
2#include "html.h" 3#include "html.h"
3 4
4#define MAX_PATH 4096 5#define MAX_PATH 4096
5 6
@@ -34,27 +35,22 @@ static int is_git_dir(const char *path)
34 35
35 return 1; 36 return 1;
36} 37}
37 38
38char *readfile(const char *path) 39struct cgit_repo *repo;
39{ 40repo_config_fn config_fn;
40 FILE *f;
41 static char buf[MAX_PATH];
42 41
43 if (!(f = fopen(path, "r"))) 42static void repo_config(const char *name, const char *value)
44 return NULL; 43{
45 buf[0] = 0; 44 config_fn(repo, name, value);
46 fgets(buf, MAX_PATH, f);
47 fclose(f);
48 return buf;
49} 45}
50 46
51static void add_repo(const char *base, const char *path) 47static void add_repo(const char *base, const char *path, repo_config_fn fn)
52{ 48{
53 struct cgit_repo *repo;
54 struct stat st; 49 struct stat st;
55 struct passwd *pwd; 50 struct passwd *pwd;
56 char *p; 51 char *p;
52 size_t size;
57 53
58 if (stat(path, &st)) { 54 if (stat(path, &st)) {
59 fprintf(stderr, "Error accessing %s: %s (%d)\n", 55 fprintf(stderr, "Error accessing %s: %s (%d)\n",
60 path, strerror(errno), errno); 56 path, strerror(errno), errno);
@@ -75,28 +71,41 @@ static void add_repo(const char *base, const char *path)
75 71
76 repo = cgit_add_repo(xstrdup(p)); 72 repo = cgit_add_repo(xstrdup(p));
77 repo->name = repo->url; 73 repo->name = repo->url;
78 repo->path = xstrdup(path); 74 repo->path = xstrdup(path);
75 p = (pwd && pwd->pw_gecos) ? strchr(pwd->pw_gecos, ',') : NULL;
76 if (p)
77 *p = '\0';
79 repo->owner = (pwd ? xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name) : ""); 78 repo->owner = (pwd ? xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name) : "");
80 79
81 p = fmt("%s/description", path); 80 p = fmt("%s/description", path);
82 if (!stat(p, &st)) 81 if (!stat(p, &st))
83 repo->desc = xstrdup(readfile(p)); 82 readfile(p, &repo->desc, &size);
84 83
85 p = fmt("%s/README.html", path); 84 p = fmt("%s/README.html", path);
86 if (!stat(p, &st)) 85 if (!stat(p, &st))
87 repo->readme = "README.html"; 86 repo->readme = "README.html";
87
88 p = fmt("%s/cgitrc", path);
89 if (!stat(p, &st)) {
90 config_fn = fn;
91 parse_configfile(xstrdup(p), &repo_config);
92 }
88} 93}
89 94
90static void scan_path(const char *base, const char *path) 95static void scan_path(const char *base, const char *path, repo_config_fn fn)
91{ 96{
92 DIR *dir; 97 DIR *dir;
93 struct dirent *ent; 98 struct dirent *ent;
94 char *buf; 99 char *buf;
95 struct stat st; 100 struct stat st;
96 101
97 if (is_git_dir(path)) { 102 if (is_git_dir(path)) {
98 add_repo(base, path); 103 add_repo(base, path, fn);
104 return;
105 }
106 if (is_git_dir(fmt("%s/.git", path))) {
107 add_repo(base, fmt("%s/.git", path), fn);
99 return; 108 return;
100 } 109 }
101 dir = opendir(path); 110 dir = opendir(path);
102 if (!dir) { 111 if (!dir) {
@@ -124,14 +133,14 @@ static void scan_path(const char *base, const char *path)
124 free(buf); 133 free(buf);
125 continue; 134 continue;
126 } 135 }
127 if (S_ISDIR(st.st_mode)) 136 if (S_ISDIR(st.st_mode))
128 scan_path(base, buf); 137 scan_path(base, buf, fn);
129 free(buf); 138 free(buf);
130 } 139 }
131 closedir(dir); 140 closedir(dir);
132} 141}
133 142
134void scan_tree(const char *path) 143void scan_tree(const char *path, repo_config_fn fn)
135{ 144{
136 scan_path(path, path); 145 scan_path(path, path, fn);
137} 146}
diff --git a/scan-tree.h b/scan-tree.h
index b103b16..11539f4 100644
--- a/scan-tree.h
+++ b/scan-tree.h
@@ -1,3 +1,3 @@
1 1
2 2
3extern void scan_tree(const char *path); 3extern void scan_tree(const char *path, repo_config_fn fn);
diff --git a/shared.c b/shared.c
index cce0af4..d7b2d5a 100644
--- a/shared.c
+++ b/shared.c
@@ -47,22 +47,26 @@ struct cgit_repo *cgit_add_repo(const char *url)
47 sizeof(struct cgit_repo)); 47 sizeof(struct cgit_repo));
48 } 48 }
49 49
50 ret = &cgit_repolist.repos[cgit_repolist.count-1]; 50 ret = &cgit_repolist.repos[cgit_repolist.count-1];
51 memset(ret, 0, sizeof(struct cgit_repo));
51 ret->url = trim_end(url, '/'); 52 ret->url = trim_end(url, '/');
52 ret->name = ret->url; 53 ret->name = ret->url;
53 ret->path = NULL; 54 ret->path = NULL;
54 ret->desc = "[no description]"; 55 ret->desc = "[no description]";
55 ret->owner = NULL; 56 ret->owner = NULL;
56 ret->group = ctx.cfg.repo_group; 57 ret->section = ctx.cfg.section;
57 ret->defbranch = "master"; 58 ret->defbranch = "master";
58 ret->snapshots = ctx.cfg.snapshots; 59 ret->snapshots = ctx.cfg.snapshots;
59 ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 60 ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
60 ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 61 ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
61 ret->max_stats = ctx.cfg.max_stats; 62 ret->max_stats = ctx.cfg.max_stats;
62 ret->module_link = ctx.cfg.module_link; 63 ret->module_link = ctx.cfg.module_link;
63 ret->readme = NULL; 64 ret->readme = NULL;
64 ret->mtime = -1; 65 ret->mtime = -1;
66 ret->about_filter = ctx.cfg.about_filter;
67 ret->commit_filter = ctx.cfg.commit_filter;
68 ret->source_filter = ctx.cfg.source_filter;
65 return ret; 69 return ret;
66} 70}
67 71
68struct cgit_repo *cgit_get_repoinfo(const char *url) 72struct cgit_repo *cgit_get_repoinfo(const char *url)
@@ -354,4 +358,60 @@ int cgit_parse_snapshots_mask(const char *str)
354 str += tl; 358 str += tl;
355 } 359 }
356 return rv; 360 return rv;
357} 361}
362
363int cgit_open_filter(struct cgit_filter *filter)
364{
365
366 filter->old_stdout = chk_positive(dup(STDOUT_FILENO),
367 "Unable to duplicate STDOUT");
368 chk_zero(pipe(filter->pipe_fh), "Unable to create pipe to subprocess");
369 filter->pid = chk_non_negative(fork(), "Unable to create subprocess");
370 if (filter->pid == 0) {
371 close(filter->pipe_fh[1]);
372 chk_non_negative(dup2(filter->pipe_fh[0], STDIN_FILENO),
373 "Unable to use pipe as STDIN");
374 execvp(filter->cmd, filter->argv);
375 die("Unable to exec subprocess %s: %s (%d)", filter->cmd,
376 strerror(errno), errno);
377 }
378 close(filter->pipe_fh[0]);
379 chk_non_negative(dup2(filter->pipe_fh[1], STDOUT_FILENO),
380 "Unable to use pipe as STDOUT");
381 close(filter->pipe_fh[1]);
382 return 0;
383}
384
385int cgit_close_filter(struct cgit_filter *filter)
386{
387 chk_non_negative(dup2(filter->old_stdout, STDOUT_FILENO),
388 "Unable to restore STDOUT");
389 close(filter->old_stdout);
390 if (filter->pid < 0)
391 return 0;
392 waitpid(filter->pid, &filter->exitstatus, 0);
393 if (WIFEXITED(filter->exitstatus) && !WEXITSTATUS(filter->exitstatus))
394 return 0;
395 die("Subprocess %s exited abnormally", filter->cmd);
396}
397
398/* Read the content of the specified file into a newly allocated buffer,
399 * zeroterminate the buffer and return 0 on success, errno otherwise.
400 */
401int readfile(const char *path, char **buf, size_t *size)
402{
403 int fd;
404 struct stat st;
405
406 fd = open(path, O_RDONLY);
407 if (fd == -1)
408 return errno;
409 if (fstat(fd, &st))
410 return errno;
411 if (!S_ISREG(st.st_mode))
412 return EISDIR;
413 *buf = xmalloc(st.st_size + 1);
414 *size = read_in_full(fd, *buf, st.st_size);
415 (*buf)[*size] = '\0';
416 return (*size == st.st_size ? 0 : errno);
417}
diff --git a/ui-atom.c b/ui-atom.c
index a6ea3ee..808b2d0 100644
--- a/ui-atom.c
+++ b/ui-atom.c
@@ -31,9 +31,9 @@ void add_entry(struct commit *commit, char *host)
31 html("<name>"); 31 html("<name>");
32 html_txt(info->author); 32 html_txt(info->author);
33 html("</name>\n"); 33 html("</name>\n");
34 } 34 }
35 if (info->author_email) { 35 if (info->author_email && !ctx.cfg.noplainemail) {
36 mail = xstrdup(info->author_email); 36 mail = xstrdup(info->author_email);
37 t = strchr(mail, '<'); 37 t = strchr(mail, '<');
38 if (t) 38 if (t)
39 t++; 39 t++;
@@ -51,9 +51,10 @@ void add_entry(struct commit *commit, char *host)
51 html("<published>"); 51 html("<published>");
52 cgit_print_date(info->author_date, FMT_ATOMDATE, ctx.cfg.local_time); 52 cgit_print_date(info->author_date, FMT_ATOMDATE, ctx.cfg.local_time);
53 html("</published>\n"); 53 html("</published>\n");
54 if (host) { 54 if (host) {
55 html("<link rel='alternate' type='text/html' href='http://"); 55 html("<link rel='alternate' type='text/html' href='");
56 html(cgit_httpscheme());
56 html_attr(host); 57 html_attr(host);
57 html_attr(cgit_pageurl(ctx.repo->url, "commit", NULL)); 58 html_attr(cgit_pageurl(ctx.repo->url, "commit", NULL));
58 if (ctx.cfg.virtual_root) 59 if (ctx.cfg.virtual_root)
59 delim = '?'; 60 delim = '?';
@@ -112,9 +113,10 @@ void cgit_print_atom(char *tip, char *path, int max_count)
112 html("<subtitle>"); 113 html("<subtitle>");
113 html_txt(ctx.repo->desc); 114 html_txt(ctx.repo->desc);
114 html("</subtitle>\n"); 115 html("</subtitle>\n");
115 if (host) { 116 if (host) {
116 html("<link rel='alternate' type='text/html' href='http://"); 117 html("<link rel='alternate' type='text/html' href='");
118 html(cgit_httpscheme());
117 html_attr(host); 119 html_attr(host);
118 html_attr(cgit_repourl(ctx.repo->url)); 120 html_attr(cgit_repourl(ctx.repo->url));
119 html("'/>\n"); 121 html("'/>\n");
120 } 122 }
diff --git a/ui-blob.c b/ui-blob.c
index 3cda03d..2ccd31d 100644
--- a/ui-blob.c
+++ b/ui-blob.c
@@ -26,9 +26,9 @@ void cgit_print_blob(const char *hex, char *path, const char *head)
26{ 26{
27 27
28 unsigned char sha1[20]; 28 unsigned char sha1[20];
29 enum object_type type; 29 enum object_type type;
30 unsigned char *buf; 30 char *buf;
31 unsigned long size; 31 unsigned long size;
32 struct commit *commit; 32 struct commit *commit;
33 const char *paths[] = {path, NULL}; 33 const char *paths[] = {path, NULL};
34 34
@@ -66,8 +66,14 @@ void cgit_print_blob(const char *hex, char *path, const char *head)
66 } 66 }
67 67
68 buf[size] = '\0'; 68 buf[size] = '\0';
69 ctx.page.mimetype = ctx.qry.mimetype; 69 ctx.page.mimetype = ctx.qry.mimetype;
70 if (!ctx.page.mimetype) {
71 if (buffer_is_binary(buf, size))
72 ctx.page.mimetype = "application/octet-stream";
73 else
74 ctx.page.mimetype = "text/plain";
75 }
70 ctx.page.filename = path; 76 ctx.page.filename = path;
71 cgit_print_http_headers(&ctx); 77 cgit_print_http_headers(&ctx);
72 write(htmlfd, buf, size); 78 write(htmlfd, buf, size);
73} 79}
diff --git a/ui-commit.c b/ui-commit.c
index 41ce70e..f5b0ae5 100644
--- a/ui-commit.c
+++ b/ui-commit.c
@@ -34,22 +34,26 @@ void cgit_print_commit(char *hex)
34 return; 34 return;
35 } 35 }
36 info = cgit_parse_commit(commit); 36 info = cgit_parse_commit(commit);
37 37
38 load_ref_decorations(); 38 load_ref_decorations(DECORATE_FULL_REFS);
39 39
40 html("<table summary='commit info' class='commit-info'>\n"); 40 html("<table summary='commit info' class='commit-info'>\n");
41 html("<tr><th>author</th><td>"); 41 html("<tr><th>author</th><td>");
42 html_txt(info->author); 42 html_txt(info->author);
43 html(" "); 43 if (!ctx.cfg.noplainemail) {
44 html_txt(info->author_email); 44 html(" ");
45 html_txt(info->author_email);
46 }
45 html("</td><td class='right'>"); 47 html("</td><td class='right'>");
46 cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time); 48 cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time);
47 html("</td></tr>\n"); 49 html("</td></tr>\n");
48 html("<tr><th>committer</th><td>"); 50 html("<tr><th>committer</th><td>");
49 html_txt(info->committer); 51 html_txt(info->committer);
50 html(" "); 52 if (!ctx.cfg.noplainemail) {
51 html_txt(info->committer_email); 53 html(" ");
54 html_txt(info->committer_email);
55 }
52 html("</td><td class='right'>"); 56 html("</td><td class='right'>");
53 cgit_print_date(info->committer_date, FMT_LONGDATE, ctx.cfg.local_time); 57 cgit_print_date(info->committer_date, FMT_LONGDATE, ctx.cfg.local_time);
54 html("</td></tr>\n"); 58 html("</td></tr>\n");
55 html("<tr><th>commit</th><td colspan='2' class='sha1'>"); 59 html("<tr><th>commit</th><td colspan='2' class='sha1'>");
@@ -88,13 +92,21 @@ void cgit_print_commit(char *hex)
88 html("</td></tr>"); 92 html("</td></tr>");
89 } 93 }
90 html("</table>\n"); 94 html("</table>\n");
91 html("<div class='commit-subject'>"); 95 html("<div class='commit-subject'>");
96 if (ctx.repo->commit_filter)
97 cgit_open_filter(ctx.repo->commit_filter);
92 html_txt(info->subject); 98 html_txt(info->subject);
99 if (ctx.repo->commit_filter)
100 cgit_close_filter(ctx.repo->commit_filter);
93 show_commit_decorations(commit); 101 show_commit_decorations(commit);
94 html("</div>"); 102 html("</div>");
95 html("<div class='commit-msg'>"); 103 html("<div class='commit-msg'>");
104 if (ctx.repo->commit_filter)
105 cgit_open_filter(ctx.repo->commit_filter);
96 html_txt(info->msg); 106 html_txt(info->msg);
107 if (ctx.repo->commit_filter)
108 cgit_close_filter(ctx.repo->commit_filter);
97 html("</div>"); 109 html("</div>");
98 if (parents < 3) { 110 if (parents < 3) {
99 if (parents) 111 if (parents)
100 tmp = sha1_to_hex(commit->parents->item->object.sha1); 112 tmp = sha1_to_hex(commit->parents->item->object.sha1);
diff --git a/ui-log.c b/ui-log.c
index ba2ab03..f3132c9 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -52,8 +52,12 @@ void show_commit_decorations(struct commit *commit)
52 else if (!prefixcmp(deco->name, "tag: refs/tags/")) { 52 else if (!prefixcmp(deco->name, "tag: refs/tags/")) {
53 strncpy(buf, deco->name + 15, sizeof(buf) - 1); 53 strncpy(buf, deco->name + 15, sizeof(buf) - 1);
54 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf); 54 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
55 } 55 }
56 else if (!prefixcmp(deco->name, "refs/tags/")) {
57 strncpy(buf, deco->name + 10, sizeof(buf) - 1);
58 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
59 }
56 else if (!prefixcmp(deco->name, "refs/remotes/")) { 60 else if (!prefixcmp(deco->name, "refs/remotes/")) {
57 strncpy(buf, deco->name + 13, sizeof(buf) - 1); 61 strncpy(buf, deco->name + 13, sizeof(buf) - 1);
58 cgit_log_link(buf, NULL, "remote-deco", NULL, 62 cgit_log_link(buf, NULL, "remote-deco", NULL,
59 sha1_to_hex(commit->object.sha1), NULL, 63 sha1_to_hex(commit->object.sha1), NULL,
@@ -156,9 +160,9 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
156 rev.commit_format = CMIT_FMT_DEFAULT; 160 rev.commit_format = CMIT_FMT_DEFAULT;
157 rev.verbose_header = 1; 161 rev.verbose_header = 1;
158 rev.show_root_diff = 0; 162 rev.show_root_diff = 0;
159 setup_revisions(argc, argv, &rev, NULL); 163 setup_revisions(argc, argv, &rev, NULL);
160 load_ref_decorations(); 164 load_ref_decorations(DECORATE_FULL_REFS);
161 rev.show_decorations = 1; 165 rev.show_decorations = 1;
162 rev.grep_filter.regflags |= REG_ICASE; 166 rev.grep_filter.regflags |= REG_ICASE;
163 compile_grep_patterns(&rev.grep_filter); 167 compile_grep_patterns(&rev.grep_filter);
164 prepare_revision_walk(&rev); 168 prepare_revision_walk(&rev);
diff --git a/ui-patch.c b/ui-patch.c
index 5d665d3..2a8f7a5 100644
--- a/ui-patch.c
+++ b/ui-patch.c
@@ -107,9 +107,13 @@ void cgit_print_patch(char *hex)
107 ctx.page.mimetype = "text/plain"; 107 ctx.page.mimetype = "text/plain";
108 ctx.page.filename = patchname; 108 ctx.page.filename = patchname;
109 cgit_print_http_headers(&ctx); 109 cgit_print_http_headers(&ctx);
110 htmlf("From %s Mon Sep 17 00:00:00 2001\n", sha1_to_hex(sha1)); 110 htmlf("From %s Mon Sep 17 00:00:00 2001\n", sha1_to_hex(sha1));
111 htmlf("From: %s %s\n", info->author, info->author_email); 111 htmlf("From: %s", info->author);
112 if (!ctx.cfg.noplainemail) {
113 htmlf(" %s", info->author_email);
114 }
115 html("\n");
112 html("Date: "); 116 html("Date: ");
113 cgit_print_date(info->author_date, "%a, %d %b %Y %H:%M:%S %z%n", ctx.cfg.local_time); 117 cgit_print_date(info->author_date, "%a, %d %b %Y %H:%M:%S %z%n", ctx.cfg.local_time);
114 htmlf("Subject: %s\n\n", info->subject); 118 htmlf("Subject: %s\n\n", info->subject);
115 if (info->msg && *info->msg) { 119 if (info->msg && *info->msg) {
diff --git a/ui-plain.c b/ui-plain.c
index e08b15b..a4ce077 100644
--- a/ui-plain.c
+++ b/ui-plain.c
@@ -16,10 +16,11 @@ int match;
16 16
17static void print_object(const unsigned char *sha1, const char *path) 17static void print_object(const unsigned char *sha1, const char *path)
18{ 18{
19 enum object_type type; 19 enum object_type type;
20 char *buf; 20 char *buf, *ext;
21 unsigned long size; 21 unsigned long size;
22 struct string_list_item *mime;
22 23
23 type = sha1_object_info(sha1, &size); 24 type = sha1_object_info(sha1, &size);
24 if (type == OBJ_BAD) { 25 if (type == OBJ_BAD) {
25 html_status(404, "Not found", 0); 26 html_status(404, "Not found", 0);
@@ -30,11 +31,24 @@ static void print_object(const unsigned char *sha1, const char *path)
30 if (!buf) { 31 if (!buf) {
31 html_status(404, "Not found", 0); 32 html_status(404, "Not found", 0);
32 return; 33 return;
33 } 34 }
34 ctx.page.mimetype = "text/plain"; 35 ctx.page.mimetype = NULL;
36 ext = strrchr(path, '.');
37 if (ext && *(++ext)) {
38 mime = string_list_lookup(ext, &ctx.cfg.mimetypes);
39 if (mime)
40 ctx.page.mimetype = (char *)mime->util;
41 }
42 if (!ctx.page.mimetype) {
43 if (buffer_is_binary(buf, size))
44 ctx.page.mimetype = "application/octet-stream";
45 else
46 ctx.page.mimetype = "text/plain";
47 }
35 ctx.page.filename = fmt("%s", path); 48 ctx.page.filename = fmt("%s", path);
36 ctx.page.size = size; 49 ctx.page.size = size;
50 ctx.page.etag = sha1_to_hex(sha1);
37 cgit_print_http_headers(&ctx); 51 cgit_print_http_headers(&ctx);
38 html_raw(buf, size); 52 html_raw(buf, size);
39 match = 1; 53 match = 1;
40} 54}
diff --git a/ui-refs.c b/ui-refs.c
index 25da00a..d3b4f6e 100644
--- a/ui-refs.c
+++ b/ui-refs.c
@@ -45,10 +45,21 @@ static int cmp_branch_age(const void *a, const void *b)
45static int cmp_tag_age(const void *a, const void *b) 45static int cmp_tag_age(const void *a, const void *b)
46{ 46{
47 struct refinfo *r1 = *(struct refinfo **)a; 47 struct refinfo *r1 = *(struct refinfo **)a;
48 struct refinfo *r2 = *(struct refinfo **)b; 48 struct refinfo *r2 = *(struct refinfo **)b;
49 int r1date, r2date;
49 50
50 return cmp_age(r1->tag->tagger_date, r2->tag->tagger_date); 51 if (r1->object->type != OBJ_COMMIT)
52 r1date = r1->tag->tagger_date;
53 else
54 r1date = r1->commit->committer_date;
55
56 if (r2->object->type != OBJ_COMMIT)
57 r2date = r2->tag->tagger_date;
58 else
59 r2date = r2->commit->committer_date;
60
61 return cmp_age(r1date, r2date);
51} 62}
52 63
53static int print_branch(struct refinfo *ref) 64static int print_branch(struct refinfo *ref)
54{ 65{
@@ -144,8 +155,14 @@ static int print_tag(struct refinfo *ref)
144 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT)) 155 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT))
145 print_tag_downloads(ctx.repo, name); 156 print_tag_downloads(ctx.repo, name);
146 else 157 else
147 cgit_object_link(ref->object); 158 cgit_object_link(ref->object);
159 html("</td><td>");
160 if (ref->object->type == OBJ_COMMIT)
161 html(ref->commit->author);
162 html("</td><td colspan='2'>");
163 if (ref->object->type == OBJ_COMMIT)
164 cgit_print_age(ref->commit->commit->date, -1, NULL);
148 html("</td></tr>\n"); 165 html("</td></tr>\n");
149 } 166 }
150 return 0; 167 return 0;
151} 168}
diff --git a/ui-repolist.c b/ui-repolist.c
index 3aedde5..3ef2e99 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -17,21 +17,22 @@
17#include "ui-shared.h" 17#include "ui-shared.h"
18 18
19time_t read_agefile(char *path) 19time_t read_agefile(char *path)
20{ 20{
21 FILE *f; 21 time_t result;
22 static char buf[64], buf2[64]; 22 size_t size;
23 char *buf;
24 static char buf2[64];
23 25
24 if (!(f = fopen(path, "r"))) 26 if (readfile(path, &buf, &size))
25 return -1; 27 return -1;
26 buf[0] = 0; 28
27 if (fgets(buf, sizeof(buf), f) == NULL)
28 return -1;
29 fclose(f);
30 if (parse_date(buf, buf2, sizeof(buf2))) 29 if (parse_date(buf, buf2, sizeof(buf2)))
31 return strtoul(buf2, NULL, 10); 30 result = strtoul(buf2, NULL, 10);
32 else 31 else
33 return 0; 32 result = 0;
33 free(buf);
34 return result;
34} 35}
35 36
36static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime) 37static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
37{ 38{
@@ -134,8 +135,20 @@ static int cmp(const char *s1, const char *s2)
134 return 1; 135 return 1;
135 return 0; 136 return 0;
136} 137}
137 138
139static int sort_section(const void *a, const void *b)
140{
141 const struct cgit_repo *r1 = a;
142 const struct cgit_repo *r2 = b;
143 int result;
144
145 result = cmp(r1->section, r2->section);
146 if (!result)
147 result = cmp(r1->name, r2->name);
148 return result;
149}
150
138static int sort_name(const void *a, const void *b) 151static int sort_name(const void *a, const void *b)
139{ 152{
140 const struct cgit_repo *r1 = a; 153 const struct cgit_repo *r1 = a;
141 const struct cgit_repo *r2 = b; 154 const struct cgit_repo *r2 = b;
@@ -176,8 +189,9 @@ struct sortcolumn {
176 int (*fn)(const void *a, const void *b); 189 int (*fn)(const void *a, const void *b);
177}; 190};
178 191
179struct sortcolumn sortcolumn[] = { 192struct sortcolumn sortcolumn[] = {
193 {"section", sort_section},
180 {"name", sort_name}, 194 {"name", sort_name},
181 {"desc", sort_desc}, 195 {"desc", sort_desc},
182 {"owner", sort_owner}, 196 {"owner", sort_owner},
183 {"idle", sort_idle}, 197 {"idle", sort_idle},
@@ -201,9 +215,10 @@ int sort_repolist(char *field)
201 215
202void cgit_print_repolist() 216void cgit_print_repolist()
203{ 217{
204 int i, columns = 4, hits = 0, header = 0; 218 int i, columns = 4, hits = 0, header = 0;
205 char *last_group = NULL; 219 char *last_section = NULL;
220 char *section;
206 int sorted = 0; 221 int sorted = 0;
207 222
208 if (ctx.cfg.enable_index_links) 223 if (ctx.cfg.enable_index_links)
209 columns++; 224 columns++;
@@ -217,8 +232,10 @@ void cgit_print_repolist()
217 html_include(ctx.cfg.index_header); 232 html_include(ctx.cfg.index_header);
218 233
219 if(ctx.qry.sort) 234 if(ctx.qry.sort)
220 sorted = sort_repolist(ctx.qry.sort); 235 sorted = sort_repolist(ctx.qry.sort);
236 else
237 sort_repolist("section");
221 238
222 html("<table summary='repository list' class='list nowrap'>"); 239 html("<table summary='repository list' class='list nowrap'>");
223 for (i=0; i<cgit_repolist.count; i++) { 240 for (i=0; i<cgit_repolist.count; i++) {
224 ctx.repo = &cgit_repolist.repos[i]; 241 ctx.repo = &cgit_repolist.repos[i];
@@ -230,21 +247,24 @@ void cgit_print_repolist()
230 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count) 247 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count)
231 continue; 248 continue;
232 if (!header++) 249 if (!header++)
233 print_header(columns); 250 print_header(columns);
251 section = ctx.repo->section;
252 if (section && !strcmp(section, ""))
253 section = NULL;
234 if (!sorted && 254 if (!sorted &&
235 ((last_group == NULL && ctx.repo->group != NULL) || 255 ((last_section == NULL && section != NULL) ||
236 (last_group != NULL && ctx.repo->group == NULL) || 256 (last_section != NULL && section == NULL) ||
237 (last_group != NULL && ctx.repo->group != NULL && 257 (last_section != NULL && section != NULL &&
238 strcmp(ctx.repo->group, last_group)))) { 258 strcmp(section, last_section)))) {
239 htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>", 259 htmlf("<tr class='nohover'><td colspan='%d' class='reposection'>",
240 columns); 260 columns);
241 html_txt(ctx.repo->group); 261 html_txt(section);
242 html("</td></tr>"); 262 html("</td></tr>");
243 last_group = ctx.repo->group; 263 last_section = section;
244 } 264 }
245 htmlf("<tr><td class='%s'>", 265 htmlf("<tr><td class='%s'>",
246 !sorted && ctx.repo->group ? "sublevel-repo" : "toplevel-repo"); 266 !sorted && section ? "sublevel-repo" : "toplevel-repo");
247 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL); 267 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
248 html("</td><td>"); 268 html("</td><td>");
249 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL); 269 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL);
250 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc); 270 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc);
@@ -273,7 +293,12 @@ void cgit_print_repolist()
273} 293}
274 294
275void cgit_print_site_readme() 295void cgit_print_site_readme()
276{ 296{
277 if (ctx.cfg.root_readme) 297 if (!ctx.cfg.root_readme)
278 html_include(ctx.cfg.root_readme); 298 return;
299 if (ctx.cfg.about_filter)
300 cgit_open_filter(ctx.cfg.about_filter);
301 html_include(ctx.cfg.root_readme);
302 if (ctx.cfg.about_filter)
303 cgit_close_filter(ctx.cfg.about_filter);
279} 304}
diff --git a/ui-shared.c b/ui-shared.c
index 40060ba..07d5dd4 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -33,26 +33,25 @@ void cgit_print_error(char *msg)
33 html_txt(msg); 33 html_txt(msg);
34 html("</div>\n"); 34 html("</div>\n");
35} 35}
36 36
37char *cgit_hosturl() 37char *cgit_httpscheme()
38{ 38{
39 char *host, *port; 39 if (ctx.env.https && !strcmp(ctx.env.https, "on"))
40 return "https://";
41 else
42 return "http://";
43}
40 44
41 host = getenv("HTTP_HOST"); 45char *cgit_hosturl()
42 if (host) { 46{
43 host = xstrdup(host); 47 if (ctx.env.http_host)
44 } else { 48 return ctx.env.http_host;
45 host = getenv("SERVER_NAME"); 49 if (!ctx.env.server_name)
46 if (!host) 50 return NULL;
47 return NULL; 51 if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
48 port = getenv("SERVER_PORT"); 52 return ctx.env.server_name;
49 if (port && atoi(port) != 80) 53 return xstrdup(fmt("%s:%s", ctx.env.server_name, ctx.env.server_port));
50 host = xstrdup(fmt("%s:%d", host, atoi(port)));
51 else
52 host = xstrdup(host);
53 }
54 return host;
55} 54}
56 55
57char *cgit_rooturl() 56char *cgit_rooturl()
58{ 57{
@@ -455,8 +454,13 @@ void cgit_print_age(time_t t, time_t max_relative, char *format)
455} 454}
456 455
457void cgit_print_http_headers(struct cgit_context *ctx) 456void cgit_print_http_headers(struct cgit_context *ctx)
458{ 457{
458 if (ctx->env.no_http && !strcmp(ctx->env.no_http, "1"))
459 return;
460
461 if (ctx->page.status)
462 htmlf("Status: %d %s\n", ctx->page.status, ctx->page.statusmsg);
459 if (ctx->page.mimetype && ctx->page.charset) 463 if (ctx->page.mimetype && ctx->page.charset)
460 htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype, 464 htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype,
461 ctx->page.charset); 465 ctx->page.charset);
462 else if (ctx->page.mimetype) 466 else if (ctx->page.mimetype)
@@ -467,13 +471,23 @@ void cgit_print_http_headers(struct cgit_context *ctx)
467 htmlf("Content-Disposition: inline; filename=\"%s\"\n", 471 htmlf("Content-Disposition: inline; filename=\"%s\"\n",
468 ctx->page.filename); 472 ctx->page.filename);
469 htmlf("Last-Modified: %s\n", http_date(ctx->page.modified)); 473 htmlf("Last-Modified: %s\n", http_date(ctx->page.modified));
470 htmlf("Expires: %s\n", http_date(ctx->page.expires)); 474 htmlf("Expires: %s\n", http_date(ctx->page.expires));
475 if (ctx->page.etag)
476 htmlf("ETag: \"%s\"\n", ctx->page.etag);
471 html("\n"); 477 html("\n");
478 if (ctx->env.request_method && !strcmp(ctx->env.request_method, "HEAD"))
479 exit(0);
472} 480}
473 481
474void cgit_print_docstart(struct cgit_context *ctx) 482void cgit_print_docstart(struct cgit_context *ctx)
475{ 483{
484 if (ctx->cfg.embedded) {
485 if (ctx->cfg.header)
486 html_include(ctx->cfg.header);
487 return;
488 }
489
476 char *host = cgit_hosturl(); 490 char *host = cgit_hosturl();
477 html(cgit_doctype); 491 html(cgit_doctype);
478 html("<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n"); 492 html("<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n");
479 html("<head>\n"); 493 html("<head>\n");
@@ -491,31 +505,41 @@ void cgit_print_docstart(struct cgit_context *ctx)
491 html_attr(ctx->cfg.favicon); 505 html_attr(ctx->cfg.favicon);
492 html("'/>\n"); 506 html("'/>\n");
493 } 507 }
494 if (host && ctx->repo) { 508 if (host && ctx->repo) {
495 html("<link rel='alternate' title='Atom feed' href='http://"); 509 html("<link rel='alternate' title='Atom feed' href='");
510 html(cgit_httpscheme());
496 html_attr(cgit_hosturl()); 511 html_attr(cgit_hosturl());
497 html_attr(cgit_fileurl(ctx->repo->url, "atom", ctx->qry.path, 512 html_attr(cgit_fileurl(ctx->repo->url, "atom", ctx->qry.path,
498 fmt("h=%s", ctx->qry.head))); 513 fmt("h=%s", ctx->qry.head)));
499 html("' type='application/atom+xml'/>"); 514 html("' type='application/atom+xml'/>\n");
500 } 515 }
516 if (ctx->cfg.head_include)
517 html_include(ctx->cfg.head_include);
501 html("</head>\n"); 518 html("</head>\n");
502 html("<body>\n"); 519 html("<body>\n");
503 if (ctx->cfg.header) 520 if (ctx->cfg.header)
504 html_include(ctx->cfg.header); 521 html_include(ctx->cfg.header);
505} 522}
506 523
507void cgit_print_docend() 524void cgit_print_docend()
508{ 525{
509 html("</div>"); 526 html("</div> <!-- class=content -->\n");
527 if (ctx.cfg.embedded) {
528 html("</div> <!-- id=cgit -->\n");
529 if (ctx.cfg.footer)
530 html_include(ctx.cfg.footer);
531 return;
532 }
510 if (ctx.cfg.footer) 533 if (ctx.cfg.footer)
511 html_include(ctx.cfg.footer); 534 html_include(ctx.cfg.footer);
512 else { 535 else {
513 htmlf("<div class='footer'>generated by cgit %s at ", 536 htmlf("<div class='footer'>generated by cgit %s at ",
514 cgit_version); 537 cgit_version);
515 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time); 538 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time);
516 html("</div>\n"); 539 html("</div>\n");
517 } 540 }
541 html("</div> <!-- id=cgit -->\n");
518 html("</body>\n</html>\n"); 542 html("</body>\n</html>\n");
519} 543}
520 544
521int print_branch_option(const char *refname, const unsigned char *sha1, 545int print_branch_option(const char *refname, const unsigned char *sha1,
@@ -601,15 +625,10 @@ char *hc(struct cgit_cmd *cmd, const char *page)
601{ 625{
602 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active"); 626 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
603} 627}
604 628
605void cgit_print_pageheader(struct cgit_context *ctx) 629static void print_header(struct cgit_context *ctx)
606{ 630{
607 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
608
609 if (!cmd && ctx->repo)
610 fallback_cmd = "summary";
611
612 html("<table id='header'>\n"); 631 html("<table id='header'>\n");
613 html("<tr>\n"); 632 html("<tr>\n");
614 633
615 if (ctx->cfg.logo && ctx->cfg.logo[0] != 0) { 634 if (ctx->cfg.logo && ctx->cfg.logo[0] != 0) {
@@ -651,8 +670,20 @@ void cgit_print_pageheader(struct cgit_context *ctx)
651 else if (ctx->cfg.index_info) 670 else if (ctx->cfg.index_info)
652 html_include(ctx->cfg.index_info); 671 html_include(ctx->cfg.index_info);
653 } 672 }
654 html("</td></tr></table>\n"); 673 html("</td></tr></table>\n");
674}
675
676void cgit_print_pageheader(struct cgit_context *ctx)
677{
678 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
679
680 if (!cmd && ctx->repo)
681 fallback_cmd = "summary";
682
683 html("<div id='cgit'>");
684 if (!ctx->cfg.noheader)
685 print_header(ctx);
655 686
656 html("<table class='tabs'><tr><td>\n"); 687 html("<table class='tabs'><tr><td>\n");
657 if (ctx->repo) { 688 if (ctx->repo) {
658 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 689 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
diff --git a/ui-shared.h b/ui-shared.h
index 5a3821f..bff4826 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -1,7 +1,8 @@
1#ifndef UI_SHARED_H 1#ifndef UI_SHARED_H
2#define UI_SHARED_H 2#define UI_SHARED_H
3 3
4extern char *cgit_httpscheme();
4extern char *cgit_hosturl(); 5extern char *cgit_hosturl();
5extern char *cgit_repourl(const char *reponame); 6extern char *cgit_repourl(const char *reponame);
6extern char *cgit_fileurl(const char *reponame, const char *pagename, 7extern char *cgit_fileurl(const char *reponame, const char *pagename,
7 const char *filename, const char *query); 8 const char *filename, const char *query);
diff --git a/ui-snapshot.c b/ui-snapshot.c
index 5372f5d..4136b3e 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -11,39 +11,18 @@
11#include "ui-shared.h" 11#include "ui-shared.h"
12 12
13static int write_compressed_tar_archive(struct archiver_args *args,const char *filter) 13static int write_compressed_tar_archive(struct archiver_args *args,const char *filter)
14{ 14{
15 int rw[2];
16 pid_t gzpid;
17 int stdout2;
18 int status;
19 int rv; 15 int rv;
16 struct cgit_filter f;
20 17
21 stdout2 = chk_non_negative(dup(STDIN_FILENO), "Preserving STDOUT before compressing"); 18 f.cmd = xstrdup(filter);
22 chk_zero(pipe(rw), "Opening pipe from compressor subprocess"); 19 f.argv = malloc(2 * sizeof(char *));
23 gzpid = chk_non_negative(fork(), "Forking compressor subprocess"); 20 f.argv[0] = f.cmd;
24 if(gzpid==0) { 21 f.argv[1] = NULL;
25 /* child */ 22 cgit_open_filter(&f);
26 chk_zero(close(rw[1]), "Closing write end of pipe in child");
27 chk_zero(close(STDIN_FILENO), "Closing STDIN");
28 chk_non_negative(dup2(rw[0],STDIN_FILENO), "Redirecting compressor input to stdin");
29 execlp(filter,filter,NULL);
30 _exit(-1);
31 }
32 /* parent */
33 chk_zero(close(rw[0]), "Closing read end of pipe");
34 chk_non_negative(dup2(rw[1],STDOUT_FILENO), "Redirecting output to compressor");
35
36 rv = write_tar_archive(args); 23 rv = write_tar_archive(args);
37 24 cgit_close_filter(&f);
38 chk_zero(close(STDOUT_FILENO), "Closing STDOUT redirected to compressor");
39 chk_non_negative(dup2(stdout2,STDOUT_FILENO), "Restoring uncompressed STDOUT");
40 chk_zero(close(stdout2), "Closing uncompressed STDOUT");
41 chk_zero(close(rw[1]), "Closing write end of pipe in parent");
42 chk_positive(waitpid(gzpid,&status,0), "Waiting on compressor process");
43 if(! ( WIFEXITED(status) && WEXITSTATUS(status)==0 ) )
44 cgit_print_error("Failed to compress archive");
45
46 return rv; 25 return rv;
47} 26}
48 27
49static int write_tar_gzip_archive(struct archiver_args *args) 28static int write_tar_gzip_archive(struct archiver_args *args)
diff --git a/ui-stats.c b/ui-stats.c
index 9fc06d3..bdaf9cc 100644
--- a/ui-stats.c
+++ b/ui-stats.c
@@ -153,8 +153,16 @@ int cgit_find_stats_period(const char *expr, struct cgit_period **period)
153 } 153 }
154 return 0; 154 return 0;
155} 155}
156 156
157const char *cgit_find_stats_periodname(int idx)
158{
159 if (idx > 0 && idx < 4)
160 return periods[idx - 1].name;
161 else
162 return "";
163}
164
157static void add_commit(struct string_list *authors, struct commit *commit, 165static void add_commit(struct string_list *authors, struct commit *commit,
158 struct cgit_period *period) 166 struct cgit_period *period)
159{ 167{
160 struct commitinfo *info; 168 struct commitinfo *info;
diff --git a/ui-stats.h b/ui-stats.h
index 4f13dba..f0761ba 100644
--- a/ui-stats.h
+++ b/ui-stats.h
@@ -20,8 +20,9 @@ struct cgit_period {
20 char *(*pretty)(struct tm *tm); 20 char *(*pretty)(struct tm *tm);
21}; 21};
22 22
23extern int cgit_find_stats_period(const char *expr, struct cgit_period **period); 23extern int cgit_find_stats_period(const char *expr, struct cgit_period **period);
24extern const char *cgit_find_stats_periodname(int idx);
24 25
25extern void cgit_show_stats(struct cgit_context *ctx); 26extern void cgit_show_stats(struct cgit_context *ctx);
26 27
27#endif /* UI_STATS_H */ 28#endif /* UI_STATS_H */
diff --git a/ui-summary.c b/ui-summary.c
index ede4a62..a2c018e 100644
--- a/ui-summary.c
+++ b/ui-summary.c
@@ -65,12 +65,28 @@ void cgit_print_summary()
65 print_urls(ctx.cfg.clone_prefix, ctx.repo->url); 65 print_urls(ctx.cfg.clone_prefix, ctx.repo->url);
66 html("</table>"); 66 html("</table>");
67} 67}
68 68
69void cgit_print_repo_readme() 69void cgit_print_repo_readme(char *path)
70{ 70{
71 if (ctx.repo->readme) { 71 char *slash, *tmp;
72 html("<div id='summary'>"); 72
73 html_include(ctx.repo->readme); 73 if (!ctx.repo->readme)
74 html("</div>"); 74 return;
75 } 75
76 if (path) {
77 slash = strrchr(ctx.repo->readme, '/');
78 if (!slash)
79 return;
80 tmp = xmalloc(slash - ctx.repo->readme + 1 + strlen(path) + 1);
81 strncpy(tmp, ctx.repo->readme, slash - ctx.repo->readme + 1);
82 strcpy(tmp + (slash - ctx.repo->readme + 1), path);
83 } else
84 tmp = ctx.repo->readme;
85 html("<div id='summary'>");
86 if (ctx.repo->about_filter)
87 cgit_open_filter(ctx.repo->about_filter);
88 html_include(tmp);
89 if (ctx.repo->about_filter)
90 cgit_close_filter(ctx.repo->about_filter);
91 html("</div>");
76} 92}
diff --git a/ui-summary.h b/ui-summary.h
index 3e13039..c01f560 100644
--- a/ui-summary.h
+++ b/ui-summary.h
@@ -1,7 +1,7 @@
1#ifndef UI_SUMMARY_H 1#ifndef UI_SUMMARY_H
2#define UI_SUMMARY_H 2#define UI_SUMMARY_H
3 3
4extern void cgit_print_summary(); 4extern void cgit_print_summary();
5extern void cgit_print_repo_readme(); 5extern void cgit_print_repo_readme(char *path);
6 6
7#endif /* UI_SUMMARY_H */ 7#endif /* UI_SUMMARY_H */
diff --git a/ui-tag.c b/ui-tag.c
index 8c263ab..c2d72af 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -66,9 +66,9 @@ void cgit_print_tag(char *revname)
66 } 66 }
67 if (info->tagger) { 67 if (info->tagger) {
68 html("<tr><td>Tagged by</td><td>"); 68 html("<tr><td>Tagged by</td><td>");
69 html_txt(info->tagger); 69 html_txt(info->tagger);
70 if (info->tagger_email) { 70 if (info->tagger_email && !ctx.cfg.noplainemail) {
71 html(" "); 71 html(" ");
72 html_txt(info->tagger_email); 72 html_txt(info->tagger_email);
73 } 73 }
74 html("</td></tr>\n"); 74 html("</td></tr>\n");
diff --git a/ui-tree.c b/ui-tree.c
index 553dbaa..f53ab64 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -14,28 +14,45 @@
14char *curr_rev; 14char *curr_rev;
15char *match_path; 15char *match_path;
16int header = 0; 16int header = 0;
17 17
18static void print_text_buffer(char *buf, unsigned long size) 18static void print_text_buffer(const char *name, char *buf, unsigned long size)
19{ 19{
20 unsigned long lineno, idx; 20 unsigned long lineno, idx;
21 const char *numberfmt = 21 const char *numberfmt =
22 "<a class='no' id='n%1$d' name='n%1$d' href='#n%1$d'>%1$d</a>\n"; 22 "<a class='no' id='n%1$d' name='n%1$d' href='#n%1$d'>%1$d</a>\n";
23 23
24 html("<table summary='blob content' class='blob'>\n"); 24 html("<table summary='blob content' class='blob'>\n");
25 html("<tr><td class='linenumbers'><pre>"); 25
26 idx = 0; 26 if (ctx.cfg.enable_tree_linenumbers) {
27 lineno = 0; 27 html("<tr><td class='linenumbers'><pre>");
28 28 idx = 0;
29 if (size) { 29 lineno = 0;
30 htmlf(numberfmt, ++lineno); 30
31 while(idx < size - 1) { // skip absolute last newline 31 if (size) {
32 if (buf[idx] == '\n') 32 htmlf(numberfmt, ++lineno);
33 htmlf(numberfmt, ++lineno); 33 while(idx < size - 1) { // skip absolute last newline
34 idx++; 34 if (buf[idx] == '\n')
35 htmlf(numberfmt, ++lineno);
36 idx++;
37 }
35 } 38 }
39 html("</pre></td>\n");
40 }
41 else {
42 html("<tr>\n");
43 }
44
45 if (ctx.repo->source_filter) {
46 html("<td class='lines'><pre><code>");
47 ctx.repo->source_filter->argv[1] = xstrdup(name);
48 cgit_open_filter(ctx.repo->source_filter);
49 write(STDOUT_FILENO, buf, size);
50 cgit_close_filter(ctx.repo->source_filter);
51 html("</code></pre></td></tr></table>\n");
52 return;
36 } 53 }
37 html("</pre></td>\n"); 54
38 html("<td class='lines'><pre><code>"); 55 html("<td class='lines'><pre><code>");
39 html_txt(buf); 56 html_txt(buf);
40 html("</code></pre></td></tr></table>\n"); 57 html("</code></pre></td></tr></table>\n");
41} 58}
@@ -64,9 +81,9 @@ static void print_binary_buffer(char *buf, unsigned long size)
64 } 81 }
65 html("</table>\n"); 82 html("</table>\n");
66} 83}
67 84
68static void print_object(const unsigned char *sha1, char *path) 85static void print_object(const unsigned char *sha1, char *path, const char *basename)
69{ 86{
70 enum object_type type; 87 enum object_type type;
71 char *buf; 88 char *buf;
72 unsigned long size; 89 unsigned long size;
@@ -92,9 +109,9 @@ static void print_object(const unsigned char *sha1, char *path)
92 109
93 if (buffer_is_binary(buf, size)) 110 if (buffer_is_binary(buf, size))
94 print_binary_buffer(buf, size); 111 print_binary_buffer(buf, size);
95 else 112 else
96 print_text_buffer(buf, size); 113 print_text_buffer(basename, buf, size);
97} 114}
98 115
99 116
100static int ls_item(const unsigned char *sha1, const char *base, int baselen, 117static int ls_item(const unsigned char *sha1, const char *base, int baselen,
@@ -102,8 +119,9 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
102 void *cbdata) 119 void *cbdata)
103{ 120{
104 char *name; 121 char *name;
105 char *fullpath; 122 char *fullpath;
123 char *class;
106 enum object_type type; 124 enum object_type type;
107 unsigned long size = 0; 125 unsigned long size = 0;
108 126
109 name = xstrdup(pathname); 127 name = xstrdup(pathname);
@@ -134,9 +152,14 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
134 } else if (S_ISDIR(mode)) { 152 } else if (S_ISDIR(mode)) {
135 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, 153 cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head,
136 curr_rev, fullpath); 154 curr_rev, fullpath);
137 } else { 155 } else {
138 cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head, 156 class = strrchr(name, '.');
157 if (class != NULL) {
158 class = fmt("ls-blob %s", class + 1);
159 } else
160 class = "ls-blob";
161 cgit_tree_link(name, NULL, class, ctx.qry.head,
139 curr_rev, fullpath); 162 curr_rev, fullpath);
140 } 163 }
141 htmlf("</td><td class='ls-size'>%li</td>", size); 164 htmlf("</td><td class='ls-size'>%li</td>", size);
142 165
@@ -212,9 +235,9 @@ static int walk_tree(const unsigned char *sha1, const char *base, int baselen,
212 state = 1; 235 state = 1;
213 ls_head(); 236 ls_head();
214 return READ_TREE_RECURSIVE; 237 return READ_TREE_RECURSIVE;
215 } else { 238 } else {
216 print_object(sha1, buffer); 239 print_object(sha1, buffer, pathname);
217 return 0; 240 return 0;
218 } 241 }
219 } 242 }
220 ls_item(sha1, base, baselen, pathname, mode, stage, NULL); 243 ls_item(sha1, base, baselen, pathname, mode, stage, NULL);