summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--cache.h2
-rw-r--r--cgit.c265
-rw-r--r--cgit.css2
-rw-r--r--cgit.h9
-rw-r--r--cgitrc.5.txt48
-rw-r--r--scan-tree.c32
-rw-r--r--scan-tree.h2
-rw-r--r--shared.c3
-rw-r--r--ui-repolist.c37
-rw-r--r--ui-stats.c8
-rw-r--r--ui-stats.h1
11 files changed, 329 insertions, 80 deletions
diff --git a/cache.h b/cache.h
index 66cc41f..ac9276b 100644
--- a/cache.h
+++ b/cache.h
@@ -1,35 +1,37 @@
1/* 1/*
2 * Since git has it's own cache.h which we include, 2 * Since git has it's own cache.h which we include,
3 * lets test on CGIT_CACHE_H to avoid confusion 3 * lets test on CGIT_CACHE_H to avoid confusion
4 */ 4 */
5 5
6#ifndef CGIT_CACHE_H 6#ifndef CGIT_CACHE_H
7#define CGIT_CACHE_H 7#define CGIT_CACHE_H
8 8
9typedef void (*cache_fill_fn)(void *cbdata); 9typedef void (*cache_fill_fn)(void *cbdata);
10 10
11 11
12/* Print cached content to stdout, generate the content if necessary. 12/* Print cached content to stdout, generate the content if necessary.
13 * 13 *
14 * Parameters 14 * Parameters
15 * size max number of cache files 15 * size max number of cache files
16 * path directory used to store cache files 16 * path directory used to store cache files
17 * key the key used to lookup cache files 17 * key the key used to lookup cache files
18 * ttl max cache time in seconds for this key 18 * ttl max cache time in seconds for this key
19 * fn content generator function for this key 19 * fn content generator function for this key
20 * cbdata user-supplied data to the content generator function 20 * cbdata user-supplied data to the content generator function
21 * 21 *
22 * Return value 22 * Return value
23 * 0 indicates success, everyting else is an error 23 * 0 indicates success, everyting else is an error
24 */ 24 */
25extern int cache_process(int size, const char *path, const char *key, int ttl, 25extern int cache_process(int size, const char *path, const char *key, int ttl,
26 cache_fill_fn fn, void *cbdata); 26 cache_fill_fn fn, void *cbdata);
27 27
28 28
29/* List info about all cache entries on stdout */ 29/* List info about all cache entries on stdout */
30extern int cache_ls(const char *path); 30extern 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.c b/cgit.c
index ec40e1f..bd37788 100644
--- a/cgit.c
+++ b/cgit.c
@@ -1,561 +1,718 @@
1/* cgit.c: cgi for the git scm 1/* cgit.c: cgi for the git scm
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "cache.h" 10#include "cache.h"
11#include "cmd.h" 11#include "cmd.h"
12#include "configfile.h" 12#include "configfile.h"
13#include "html.h" 13#include "html.h"
14#include "ui-shared.h" 14#include "ui-shared.h"
15#include "ui-stats.h" 15#include "ui-stats.h"
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) 20void add_mimetype(const char *name, const char *value)
21{ 21{
22 struct string_list_item *item; 22 struct string_list_item *item;
23 23
24 item = string_list_insert(xstrdup(name), &ctx.cfg.mimetypes); 24 item = string_list_insert(xstrdup(name), &ctx.cfg.mimetypes);
25 item->util = xstrdup(value); 25 item->util = xstrdup(value);
26} 26}
27 27
28struct cgit_filter *new_filter(const char *cmd, int extra_args) 28struct cgit_filter *new_filter(const char *cmd, int extra_args)
29{ 29{
30 struct cgit_filter *f; 30 struct cgit_filter *f;
31 31
32 if (!cmd || !cmd[0]) 32 if (!cmd || !cmd[0])
33 return NULL; 33 return NULL;
34 34
35 f = xmalloc(sizeof(struct cgit_filter)); 35 f = xmalloc(sizeof(struct cgit_filter));
36 f->cmd = xstrdup(cmd); 36 f->cmd = xstrdup(cmd);
37 f->argv = xmalloc((2 + extra_args) * sizeof(char *)); 37 f->argv = xmalloc((2 + extra_args) * sizeof(char *));
38 f->argv[0] = f->cmd; 38 f->argv[0] = f->cmd;
39 f->argv[1] = NULL; 39 f->argv[1] = NULL;
40 return f; 40 return f;
41} 41}
42 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
43void config_cb(const char *name, const char *value) 84void config_cb(const char *name, const char *value)
44{ 85{
45 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"))
46 ctx.cfg.root_title = xstrdup(value); 95 ctx.cfg.root_title = xstrdup(value);
47 else if (!strcmp(name, "root-desc")) 96 else if (!strcmp(name, "root-desc"))
48 ctx.cfg.root_desc = xstrdup(value); 97 ctx.cfg.root_desc = xstrdup(value);
49 else if (!strcmp(name, "root-readme")) 98 else if (!strcmp(name, "root-readme"))
50 ctx.cfg.root_readme = xstrdup(value); 99 ctx.cfg.root_readme = xstrdup(value);
51 else if (!strcmp(name, "css")) 100 else if (!strcmp(name, "css"))
52 ctx.cfg.css = xstrdup(value); 101 ctx.cfg.css = xstrdup(value);
53 else if (!strcmp(name, "favicon")) 102 else if (!strcmp(name, "favicon"))
54 ctx.cfg.favicon = xstrdup(value); 103 ctx.cfg.favicon = xstrdup(value);
55 else if (!strcmp(name, "footer")) 104 else if (!strcmp(name, "footer"))
56 ctx.cfg.footer = xstrdup(value); 105 ctx.cfg.footer = xstrdup(value);
57 else if (!strcmp(name, "head-include")) 106 else if (!strcmp(name, "head-include"))
58 ctx.cfg.head_include = xstrdup(value); 107 ctx.cfg.head_include = xstrdup(value);
59 else if (!strcmp(name, "header")) 108 else if (!strcmp(name, "header"))
60 ctx.cfg.header = xstrdup(value); 109 ctx.cfg.header = xstrdup(value);
61 else if (!strcmp(name, "logo")) 110 else if (!strcmp(name, "logo"))
62 ctx.cfg.logo = xstrdup(value); 111 ctx.cfg.logo = xstrdup(value);
63 else if (!strcmp(name, "index-header")) 112 else if (!strcmp(name, "index-header"))
64 ctx.cfg.index_header = xstrdup(value); 113 ctx.cfg.index_header = xstrdup(value);
65 else if (!strcmp(name, "index-info")) 114 else if (!strcmp(name, "index-info"))
66 ctx.cfg.index_info = xstrdup(value); 115 ctx.cfg.index_info = xstrdup(value);
67 else if (!strcmp(name, "logo-link")) 116 else if (!strcmp(name, "logo-link"))
68 ctx.cfg.logo_link = xstrdup(value); 117 ctx.cfg.logo_link = xstrdup(value);
69 else if (!strcmp(name, "module-link")) 118 else if (!strcmp(name, "module-link"))
70 ctx.cfg.module_link = xstrdup(value); 119 ctx.cfg.module_link = xstrdup(value);
71 else if (!strcmp(name, "virtual-root")) { 120 else if (!strcmp(name, "virtual-root")) {
72 ctx.cfg.virtual_root = trim_end(value, '/'); 121 ctx.cfg.virtual_root = trim_end(value, '/');
73 if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 122 if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
74 ctx.cfg.virtual_root = ""; 123 ctx.cfg.virtual_root = "";
75 } else if (!strcmp(name, "nocache")) 124 } else if (!strcmp(name, "nocache"))
76 ctx.cfg.nocache = atoi(value); 125 ctx.cfg.nocache = atoi(value);
77 else if (!strcmp(name, "noplainemail")) 126 else if (!strcmp(name, "noplainemail"))
78 ctx.cfg.noplainemail = atoi(value); 127 ctx.cfg.noplainemail = atoi(value);
79 else if (!strcmp(name, "noheader")) 128 else if (!strcmp(name, "noheader"))
80 ctx.cfg.noheader = atoi(value); 129 ctx.cfg.noheader = atoi(value);
81 else if (!strcmp(name, "snapshots")) 130 else if (!strcmp(name, "snapshots"))
82 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);
83 else if (!strcmp(name, "enable-index-links")) 134 else if (!strcmp(name, "enable-index-links"))
84 ctx.cfg.enable_index_links = atoi(value); 135 ctx.cfg.enable_index_links = atoi(value);
85 else if (!strcmp(name, "enable-log-filecount")) 136 else if (!strcmp(name, "enable-log-filecount"))
86 ctx.cfg.enable_log_filecount = atoi(value); 137 ctx.cfg.enable_log_filecount = atoi(value);
87 else if (!strcmp(name, "enable-log-linecount")) 138 else if (!strcmp(name, "enable-log-linecount"))
88 ctx.cfg.enable_log_linecount = atoi(value); 139 ctx.cfg.enable_log_linecount = atoi(value);
89 else if (!strcmp(name, "enable-tree-linenumbers")) 140 else if (!strcmp(name, "enable-tree-linenumbers"))
90 ctx.cfg.enable_tree_linenumbers = atoi(value); 141 ctx.cfg.enable_tree_linenumbers = atoi(value);
91 else if (!strcmp(name, "max-stats")) 142 else if (!strcmp(name, "max-stats"))
92 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); 143 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
93 else if (!strcmp(name, "cache-size")) 144 else if (!strcmp(name, "cache-size"))
94 ctx.cfg.cache_size = atoi(value); 145 ctx.cfg.cache_size = atoi(value);
95 else if (!strcmp(name, "cache-root")) 146 else if (!strcmp(name, "cache-root"))
96 ctx.cfg.cache_root = xstrdup(value); 147 ctx.cfg.cache_root = xstrdup(value);
97 else if (!strcmp(name, "cache-root-ttl")) 148 else if (!strcmp(name, "cache-root-ttl"))
98 ctx.cfg.cache_root_ttl = atoi(value); 149 ctx.cfg.cache_root_ttl = atoi(value);
99 else if (!strcmp(name, "cache-repo-ttl")) 150 else if (!strcmp(name, "cache-repo-ttl"))
100 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);
101 else if (!strcmp(name, "cache-static-ttl")) 154 else if (!strcmp(name, "cache-static-ttl"))
102 ctx.cfg.cache_static_ttl = atoi(value); 155 ctx.cfg.cache_static_ttl = atoi(value);
103 else if (!strcmp(name, "cache-dynamic-ttl")) 156 else if (!strcmp(name, "cache-dynamic-ttl"))
104 ctx.cfg.cache_dynamic_ttl = atoi(value); 157 ctx.cfg.cache_dynamic_ttl = atoi(value);
105 else if (!strcmp(name, "about-filter")) 158 else if (!strcmp(name, "about-filter"))
106 ctx.cfg.about_filter = new_filter(value, 0); 159 ctx.cfg.about_filter = new_filter(value, 0);
107 else if (!strcmp(name, "commit-filter")) 160 else if (!strcmp(name, "commit-filter"))
108 ctx.cfg.commit_filter = new_filter(value, 0); 161 ctx.cfg.commit_filter = new_filter(value, 0);
109 else if (!strcmp(name, "embedded")) 162 else if (!strcmp(name, "embedded"))
110 ctx.cfg.embedded = atoi(value); 163 ctx.cfg.embedded = atoi(value);
111 else if (!strcmp(name, "max-message-length")) 164 else if (!strcmp(name, "max-message-length"))
112 ctx.cfg.max_msg_len = atoi(value); 165 ctx.cfg.max_msg_len = atoi(value);
113 else if (!strcmp(name, "max-repodesc-length")) 166 else if (!strcmp(name, "max-repodesc-length"))
114 ctx.cfg.max_repodesc_len = atoi(value); 167 ctx.cfg.max_repodesc_len = atoi(value);
115 else if (!strcmp(name, "max-repo-count")) 168 else if (!strcmp(name, "max-repo-count"))
116 ctx.cfg.max_repo_count = atoi(value); 169 ctx.cfg.max_repo_count = atoi(value);
117 else if (!strcmp(name, "max-commit-count")) 170 else if (!strcmp(name, "max-commit-count"))
118 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);
119 else if (!strcmp(name, "source-filter")) 177 else if (!strcmp(name, "source-filter"))
120 ctx.cfg.source_filter = new_filter(value, 1); 178 ctx.cfg.source_filter = new_filter(value, 1);
121 else if (!strcmp(name, "summary-log")) 179 else if (!strcmp(name, "summary-log"))
122 ctx.cfg.summary_log = atoi(value); 180 ctx.cfg.summary_log = atoi(value);
123 else if (!strcmp(name, "summary-branches")) 181 else if (!strcmp(name, "summary-branches"))
124 ctx.cfg.summary_branches = atoi(value); 182 ctx.cfg.summary_branches = atoi(value);
125 else if (!strcmp(name, "summary-tags")) 183 else if (!strcmp(name, "summary-tags"))
126 ctx.cfg.summary_tags = atoi(value); 184 ctx.cfg.summary_tags = atoi(value);
127 else if (!strcmp(name, "agefile")) 185 else if (!strcmp(name, "agefile"))
128 ctx.cfg.agefile = xstrdup(value); 186 ctx.cfg.agefile = xstrdup(value);
129 else if (!strcmp(name, "renamelimit")) 187 else if (!strcmp(name, "renamelimit"))
130 ctx.cfg.renamelimit = atoi(value); 188 ctx.cfg.renamelimit = atoi(value);
131 else if (!strcmp(name, "robots")) 189 else if (!strcmp(name, "robots"))
132 ctx.cfg.robots = xstrdup(value); 190 ctx.cfg.robots = xstrdup(value);
133 else if (!strcmp(name, "clone-prefix")) 191 else if (!strcmp(name, "clone-prefix"))
134 ctx.cfg.clone_prefix = xstrdup(value); 192 ctx.cfg.clone_prefix = xstrdup(value);
135 else if (!strcmp(name, "local-time")) 193 else if (!strcmp(name, "local-time"))
136 ctx.cfg.local_time = atoi(value); 194 ctx.cfg.local_time = atoi(value);
137 else if (!prefixcmp(name, "mimetype.")) 195 else if (!prefixcmp(name, "mimetype."))
138 add_mimetype(name + 9, value); 196 add_mimetype(name + 9, value);
139 else if (!strcmp(name, "repo.group")) 197 else if (!strcmp(name, "include"))
140 ctx.cfg.repo_group = xstrdup(value);
141 else if (!strcmp(name, "repo.url"))
142 ctx.repo = cgit_add_repo(value);
143 else if (!strcmp(name, "repo.name"))
144 ctx.repo->name = xstrdup(value);
145 else if (ctx.repo && !strcmp(name, "repo.path"))
146 ctx.repo->path = trim_end(value, '/');
147 else if (ctx.repo && !strcmp(name, "repo.clone-url"))
148 ctx.repo->clone_url = xstrdup(value);
149 else if (ctx.repo && !strcmp(name, "repo.desc"))
150 ctx.repo->desc = xstrdup(value);
151 else if (ctx.repo && !strcmp(name, "repo.owner"))
152 ctx.repo->owner = xstrdup(value);
153 else if (ctx.repo && !strcmp(name, "repo.defbranch"))
154 ctx.repo->defbranch = xstrdup(value);
155 else if (ctx.repo && !strcmp(name, "repo.snapshots"))
156 ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
157 else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
158 ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
159 else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
160 ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
161 else if (ctx.repo && !strcmp(name, "repo.max-stats"))
162 ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
163 else if (ctx.repo && !strcmp(name, "repo.module-link"))
164 ctx.repo->module_link= xstrdup(value);
165 else if (ctx.repo && !strcmp(name, "repo.about-filter"))
166 ctx.repo->about_filter = new_filter(value, 0);
167 else if (ctx.repo && !strcmp(name, "repo.commit-filter"))
168 ctx.repo->commit_filter = new_filter(value, 0);
169 else if (ctx.repo && !strcmp(name, "repo.source-filter"))
170 ctx.repo->source_filter = new_filter(value, 1);
171 else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
172 if (*value == '/')
173 ctx.repo->readme = xstrdup(value);
174 else
175 ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
176 } else if (!strcmp(name, "include"))
177 parse_configfile(value, config_cb); 198 parse_configfile(value, config_cb);
178} 199}
179 200
180static void querystring_cb(const char *name, const char *value) 201static void querystring_cb(const char *name, const char *value)
181{ 202{
182 if (!value) 203 if (!value)
183 value = ""; 204 value = "";
184 205
185 if (!strcmp(name,"r")) { 206 if (!strcmp(name,"r")) {
186 ctx.qry.repo = xstrdup(value); 207 ctx.qry.repo = xstrdup(value);
187 ctx.repo = cgit_get_repoinfo(value); 208 ctx.repo = cgit_get_repoinfo(value);
188 } else if (!strcmp(name, "p")) { 209 } else if (!strcmp(name, "p")) {
189 ctx.qry.page = xstrdup(value); 210 ctx.qry.page = xstrdup(value);
190 } else if (!strcmp(name, "url")) { 211 } else if (!strcmp(name, "url")) {
191 ctx.qry.url = xstrdup(value); 212 ctx.qry.url = xstrdup(value);
192 cgit_parse_url(value); 213 cgit_parse_url(value);
193 } else if (!strcmp(name, "qt")) { 214 } else if (!strcmp(name, "qt")) {
194 ctx.qry.grep = xstrdup(value); 215 ctx.qry.grep = xstrdup(value);
195 } else if (!strcmp(name, "q")) { 216 } else if (!strcmp(name, "q")) {
196 ctx.qry.search = xstrdup(value); 217 ctx.qry.search = xstrdup(value);
197 } else if (!strcmp(name, "h")) { 218 } else if (!strcmp(name, "h")) {
198 ctx.qry.head = xstrdup(value); 219 ctx.qry.head = xstrdup(value);
199 ctx.qry.has_symref = 1; 220 ctx.qry.has_symref = 1;
200 } else if (!strcmp(name, "id")) { 221 } else if (!strcmp(name, "id")) {
201 ctx.qry.sha1 = xstrdup(value); 222 ctx.qry.sha1 = xstrdup(value);
202 ctx.qry.has_sha1 = 1; 223 ctx.qry.has_sha1 = 1;
203 } else if (!strcmp(name, "id2")) { 224 } else if (!strcmp(name, "id2")) {
204 ctx.qry.sha2 = xstrdup(value); 225 ctx.qry.sha2 = xstrdup(value);
205 ctx.qry.has_sha1 = 1; 226 ctx.qry.has_sha1 = 1;
206 } else if (!strcmp(name, "ofs")) { 227 } else if (!strcmp(name, "ofs")) {
207 ctx.qry.ofs = atoi(value); 228 ctx.qry.ofs = atoi(value);
208 } else if (!strcmp(name, "path")) { 229 } else if (!strcmp(name, "path")) {
209 ctx.qry.path = trim_end(value, '/'); 230 ctx.qry.path = trim_end(value, '/');
210 } else if (!strcmp(name, "name")) { 231 } else if (!strcmp(name, "name")) {
211 ctx.qry.name = xstrdup(value); 232 ctx.qry.name = xstrdup(value);
212 } else if (!strcmp(name, "mimetype")) { 233 } else if (!strcmp(name, "mimetype")) {
213 ctx.qry.mimetype = xstrdup(value); 234 ctx.qry.mimetype = xstrdup(value);
214 } else if (!strcmp(name, "s")){ 235 } else if (!strcmp(name, "s")){
215 ctx.qry.sort = xstrdup(value); 236 ctx.qry.sort = xstrdup(value);
216 } else if (!strcmp(name, "showmsg")) { 237 } else if (!strcmp(name, "showmsg")) {
217 ctx.qry.showmsg = atoi(value); 238 ctx.qry.showmsg = atoi(value);
218 } else if (!strcmp(name, "period")) { 239 } else if (!strcmp(name, "period")) {
219 ctx.qry.period = xstrdup(value); 240 ctx.qry.period = xstrdup(value);
220 } 241 }
221} 242}
222 243
223char *xstrdupn(const char *str) 244char *xstrdupn(const char *str)
224{ 245{
225 return (str ? xstrdup(str) : NULL); 246 return (str ? xstrdup(str) : NULL);
226} 247}
227 248
228static void prepare_context(struct cgit_context *ctx) 249static void prepare_context(struct cgit_context *ctx)
229{ 250{
230 memset(ctx, 0, sizeof(ctx)); 251 memset(ctx, 0, sizeof(ctx));
231 ctx->cfg.agefile = "info/web/last-modified"; 252 ctx->cfg.agefile = "info/web/last-modified";
232 ctx->cfg.nocache = 0; 253 ctx->cfg.nocache = 0;
233 ctx->cfg.cache_size = 0; 254 ctx->cfg.cache_size = 0;
234 ctx->cfg.cache_dynamic_ttl = 5; 255 ctx->cfg.cache_dynamic_ttl = 5;
235 ctx->cfg.cache_max_create_time = 5; 256 ctx->cfg.cache_max_create_time = 5;
236 ctx->cfg.cache_repo_ttl = 5; 257 ctx->cfg.cache_repo_ttl = 5;
237 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 258 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
238 ctx->cfg.cache_root_ttl = 5; 259 ctx->cfg.cache_root_ttl = 5;
260 ctx->cfg.cache_scanrc_ttl = 15;
239 ctx->cfg.cache_static_ttl = -1; 261 ctx->cfg.cache_static_ttl = -1;
240 ctx->cfg.css = "/cgit.css"; 262 ctx->cfg.css = "/cgit.css";
241 ctx->cfg.logo = "/cgit.png"; 263 ctx->cfg.logo = "/cgit.png";
242 ctx->cfg.local_time = 0; 264 ctx->cfg.local_time = 0;
243 ctx->cfg.enable_tree_linenumbers = 1; 265 ctx->cfg.enable_tree_linenumbers = 1;
244 ctx->cfg.max_repo_count = 50; 266 ctx->cfg.max_repo_count = 50;
245 ctx->cfg.max_commit_count = 50; 267 ctx->cfg.max_commit_count = 50;
246 ctx->cfg.max_lock_attempts = 5; 268 ctx->cfg.max_lock_attempts = 5;
247 ctx->cfg.max_msg_len = 80; 269 ctx->cfg.max_msg_len = 80;
248 ctx->cfg.max_repodesc_len = 80; 270 ctx->cfg.max_repodesc_len = 80;
249 ctx->cfg.max_stats = 0; 271 ctx->cfg.max_stats = 0;
250 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 272 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
251 ctx->cfg.renamelimit = -1; 273 ctx->cfg.renamelimit = -1;
252 ctx->cfg.robots = "index, nofollow"; 274 ctx->cfg.robots = "index, nofollow";
253 ctx->cfg.root_title = "Git repository browser"; 275 ctx->cfg.root_title = "Git repository browser";
254 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 276 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
255 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 277 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
278 ctx->cfg.section = "";
256 ctx->cfg.summary_branches = 10; 279 ctx->cfg.summary_branches = 10;
257 ctx->cfg.summary_log = 10; 280 ctx->cfg.summary_log = 10;
258 ctx->cfg.summary_tags = 10; 281 ctx->cfg.summary_tags = 10;
259 ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG")); 282 ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG"));
260 ctx->env.http_host = xstrdupn(getenv("HTTP_HOST")); 283 ctx->env.http_host = xstrdupn(getenv("HTTP_HOST"));
261 ctx->env.https = xstrdupn(getenv("HTTPS")); 284 ctx->env.https = xstrdupn(getenv("HTTPS"));
262 ctx->env.no_http = xstrdupn(getenv("NO_HTTP")); 285 ctx->env.no_http = xstrdupn(getenv("NO_HTTP"));
263 ctx->env.path_info = xstrdupn(getenv("PATH_INFO")); 286 ctx->env.path_info = xstrdupn(getenv("PATH_INFO"));
264 ctx->env.query_string = xstrdupn(getenv("QUERY_STRING")); 287 ctx->env.query_string = xstrdupn(getenv("QUERY_STRING"));
265 ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD")); 288 ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD"));
266 ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME")); 289 ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME"));
267 ctx->env.server_name = xstrdupn(getenv("SERVER_NAME")); 290 ctx->env.server_name = xstrdupn(getenv("SERVER_NAME"));
268 ctx->env.server_port = xstrdupn(getenv("SERVER_PORT")); 291 ctx->env.server_port = xstrdupn(getenv("SERVER_PORT"));
269 ctx->page.mimetype = "text/html"; 292 ctx->page.mimetype = "text/html";
270 ctx->page.charset = PAGE_ENCODING; 293 ctx->page.charset = PAGE_ENCODING;
271 ctx->page.filename = NULL; 294 ctx->page.filename = NULL;
272 ctx->page.size = 0; 295 ctx->page.size = 0;
273 ctx->page.modified = time(NULL); 296 ctx->page.modified = time(NULL);
274 ctx->page.expires = ctx->page.modified; 297 ctx->page.expires = ctx->page.modified;
275 ctx->page.etag = NULL; 298 ctx->page.etag = NULL;
276 memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list)); 299 memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list));
277 if (ctx->env.script_name) 300 if (ctx->env.script_name)
278 ctx->cfg.script_name = ctx->env.script_name; 301 ctx->cfg.script_name = ctx->env.script_name;
279 if (ctx->env.query_string) 302 if (ctx->env.query_string)
280 ctx->qry.raw = ctx->env.query_string; 303 ctx->qry.raw = ctx->env.query_string;
281 if (!ctx->env.cgit_config) 304 if (!ctx->env.cgit_config)
282 ctx->env.cgit_config = CGIT_CONFIG; 305 ctx->env.cgit_config = CGIT_CONFIG;
283} 306}
284 307
285struct refmatch { 308struct refmatch {
286 char *req_ref; 309 char *req_ref;
287 char *first_ref; 310 char *first_ref;
288 int match; 311 int match;
289}; 312};
290 313
291int find_current_ref(const char *refname, const unsigned char *sha1, 314int find_current_ref(const char *refname, const unsigned char *sha1,
292 int flags, void *cb_data) 315 int flags, void *cb_data)
293{ 316{
294 struct refmatch *info; 317 struct refmatch *info;
295 318
296 info = (struct refmatch *)cb_data; 319 info = (struct refmatch *)cb_data;
297 if (!strcmp(refname, info->req_ref)) 320 if (!strcmp(refname, info->req_ref))
298 info->match = 1; 321 info->match = 1;
299 if (!info->first_ref) 322 if (!info->first_ref)
300 info->first_ref = xstrdup(refname); 323 info->first_ref = xstrdup(refname);
301 return info->match; 324 return info->match;
302} 325}
303 326
304char *find_default_branch(struct cgit_repo *repo) 327char *find_default_branch(struct cgit_repo *repo)
305{ 328{
306 struct refmatch info; 329 struct refmatch info;
307 char *ref; 330 char *ref;
308 331
309 info.req_ref = repo->defbranch; 332 info.req_ref = repo->defbranch;
310 info.first_ref = NULL; 333 info.first_ref = NULL;
311 info.match = 0; 334 info.match = 0;
312 for_each_branch_ref(find_current_ref, &info); 335 for_each_branch_ref(find_current_ref, &info);
313 if (info.match) 336 if (info.match)
314 ref = info.req_ref; 337 ref = info.req_ref;
315 else 338 else
316 ref = info.first_ref; 339 ref = info.first_ref;
317 if (ref) 340 if (ref)
318 ref = xstrdup(ref); 341 ref = xstrdup(ref);
319 return ref; 342 return ref;
320} 343}
321 344
322static int prepare_repo_cmd(struct cgit_context *ctx) 345static int prepare_repo_cmd(struct cgit_context *ctx)
323{ 346{
324 char *tmp; 347 char *tmp;
325 unsigned char sha1[20]; 348 unsigned char sha1[20];
326 int nongit = 0; 349 int nongit = 0;
327 350
328 setenv("GIT_DIR", ctx->repo->path, 1); 351 setenv("GIT_DIR", ctx->repo->path, 1);
329 setup_git_directory_gently(&nongit); 352 setup_git_directory_gently(&nongit);
330 if (nongit) { 353 if (nongit) {
331 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title, 354 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title,
332 "config error"); 355 "config error");
333 tmp = fmt("Not a git repository: '%s'", ctx->repo->path); 356 tmp = fmt("Not a git repository: '%s'", ctx->repo->path);
334 ctx->repo = NULL; 357 ctx->repo = NULL;
335 cgit_print_http_headers(ctx); 358 cgit_print_http_headers(ctx);
336 cgit_print_docstart(ctx); 359 cgit_print_docstart(ctx);
337 cgit_print_pageheader(ctx); 360 cgit_print_pageheader(ctx);
338 cgit_print_error(tmp); 361 cgit_print_error(tmp);
339 cgit_print_docend(); 362 cgit_print_docend();
340 return 1; 363 return 1;
341 } 364 }
342 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc); 365 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc);
343 366
344 if (!ctx->qry.head) { 367 if (!ctx->qry.head) {
345 ctx->qry.nohead = 1; 368 ctx->qry.nohead = 1;
346 ctx->qry.head = find_default_branch(ctx->repo); 369 ctx->qry.head = find_default_branch(ctx->repo);
347 ctx->repo->defbranch = ctx->qry.head; 370 ctx->repo->defbranch = ctx->qry.head;
348 } 371 }
349 372
350 if (!ctx->qry.head) { 373 if (!ctx->qry.head) {
351 cgit_print_http_headers(ctx); 374 cgit_print_http_headers(ctx);
352 cgit_print_docstart(ctx); 375 cgit_print_docstart(ctx);
353 cgit_print_pageheader(ctx); 376 cgit_print_pageheader(ctx);
354 cgit_print_error("Repository seems to be empty"); 377 cgit_print_error("Repository seems to be empty");
355 cgit_print_docend(); 378 cgit_print_docend();
356 return 1; 379 return 1;
357 } 380 }
358 381
359 if (get_sha1(ctx->qry.head, sha1)) { 382 if (get_sha1(ctx->qry.head, sha1)) {
360 tmp = xstrdup(ctx->qry.head); 383 tmp = xstrdup(ctx->qry.head);
361 ctx->qry.head = ctx->repo->defbranch; 384 ctx->qry.head = ctx->repo->defbranch;
362 ctx->page.status = 404; 385 ctx->page.status = 404;
363 ctx->page.statusmsg = "not found"; 386 ctx->page.statusmsg = "not found";
364 cgit_print_http_headers(ctx); 387 cgit_print_http_headers(ctx);
365 cgit_print_docstart(ctx); 388 cgit_print_docstart(ctx);
366 cgit_print_pageheader(ctx); 389 cgit_print_pageheader(ctx);
367 cgit_print_error(fmt("Invalid branch: %s", tmp)); 390 cgit_print_error(fmt("Invalid branch: %s", tmp));
368 cgit_print_docend(); 391 cgit_print_docend();
369 return 1; 392 return 1;
370 } 393 }
371 return 0; 394 return 0;
372} 395}
373 396
374static void process_request(void *cbdata) 397static void process_request(void *cbdata)
375{ 398{
376 struct cgit_context *ctx = cbdata; 399 struct cgit_context *ctx = cbdata;
377 struct cgit_cmd *cmd; 400 struct cgit_cmd *cmd;
378 401
379 cmd = cgit_get_cmd(ctx); 402 cmd = cgit_get_cmd(ctx);
380 if (!cmd) { 403 if (!cmd) {
381 ctx->page.title = "cgit error"; 404 ctx->page.title = "cgit error";
382 cgit_print_http_headers(ctx); 405 cgit_print_http_headers(ctx);
383 cgit_print_docstart(ctx); 406 cgit_print_docstart(ctx);
384 cgit_print_pageheader(ctx); 407 cgit_print_pageheader(ctx);
385 cgit_print_error("Invalid request"); 408 cgit_print_error("Invalid request");
386 cgit_print_docend(); 409 cgit_print_docend();
387 return; 410 return;
388 } 411 }
389 412
390 if (cmd->want_repo && !ctx->repo) { 413 if (cmd->want_repo && !ctx->repo) {
391 cgit_print_http_headers(ctx); 414 cgit_print_http_headers(ctx);
392 cgit_print_docstart(ctx); 415 cgit_print_docstart(ctx);
393 cgit_print_pageheader(ctx); 416 cgit_print_pageheader(ctx);
394 cgit_print_error(fmt("No repository selected")); 417 cgit_print_error(fmt("No repository selected"));
395 cgit_print_docend(); 418 cgit_print_docend();
396 return; 419 return;
397 } 420 }
398 421
399 if (ctx->repo && prepare_repo_cmd(ctx)) 422 if (ctx->repo && prepare_repo_cmd(ctx))
400 return; 423 return;
401 424
402 if (cmd->want_layout) { 425 if (cmd->want_layout) {
403 cgit_print_http_headers(ctx); 426 cgit_print_http_headers(ctx);
404 cgit_print_docstart(ctx); 427 cgit_print_docstart(ctx);
405 cgit_print_pageheader(ctx); 428 cgit_print_pageheader(ctx);
406 } 429 }
407 430
408 cmd->fn(ctx); 431 cmd->fn(ctx);
409 432
410 if (cmd->want_layout) 433 if (cmd->want_layout)
411 cgit_print_docend(); 434 cgit_print_docend();
412} 435}
413 436
414int cmp_repos(const void *a, const void *b) 437int cmp_repos(const void *a, const void *b)
415{ 438{
416 const struct cgit_repo *ra = a, *rb = b; 439 const struct cgit_repo *ra = a, *rb = b;
417 return strcmp(ra->url, rb->url); 440 return strcmp(ra->url, rb->url);
418} 441}
419 442
420void print_repo(struct cgit_repo *repo) 443char *build_snapshot_setting(int bitmap)
444{
445 const struct cgit_snapshot_format *f;
446 char *result = xstrdup("");
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)
421{ 473{
422 printf("repo.url=%s\n", repo->url); 474 fprintf(f, "repo.url=%s\n", repo->url);
423 printf("repo.name=%s\n", repo->name); 475 fprintf(f, "repo.name=%s\n", repo->name);
424 printf("repo.path=%s\n", repo->path); 476 fprintf(f, "repo.path=%s\n", repo->path);
425 if (repo->owner) 477 if (repo->owner)
426 printf("repo.owner=%s\n", repo->owner); 478 fprintf(f, "repo.owner=%s\n", repo->owner);
427 if (repo->desc) 479 if (repo->desc) {
428 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 }
429 if (repo->readme) 484 if (repo->readme)
430 printf("repo.readme=%s\n", repo->readme); 485 fprintf(f, "repo.readme=%s\n", repo->readme);
431 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");
432} 513}
433 514
434void print_repolist(struct cgit_repolist *list) 515void print_repolist(FILE *f, struct cgit_repolist *list, int start)
435{ 516{
436 int i; 517 int i;
437 518
438 for(i = 0; i < list->count; i++) 519 for(i = start; i < list->count; i++)
439 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;
440} 551}
441 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}
442 588
443static void cgit_parse_args(int argc, const char **argv) 589static void cgit_parse_args(int argc, const char **argv)
444{ 590{
445 int i; 591 int i;
446 int scan = 0; 592 int scan = 0;
447 593
448 for (i = 1; i < argc; i++) { 594 for (i = 1; i < argc; i++) {
449 if (!strncmp(argv[i], "--cache=", 8)) { 595 if (!strncmp(argv[i], "--cache=", 8)) {
450 ctx.cfg.cache_root = xstrdup(argv[i]+8); 596 ctx.cfg.cache_root = xstrdup(argv[i]+8);
451 } 597 }
452 if (!strcmp(argv[i], "--nocache")) { 598 if (!strcmp(argv[i], "--nocache")) {
453 ctx.cfg.nocache = 1; 599 ctx.cfg.nocache = 1;
454 } 600 }
455 if (!strcmp(argv[i], "--nohttp")) { 601 if (!strcmp(argv[i], "--nohttp")) {
456 ctx.env.no_http = "1"; 602 ctx.env.no_http = "1";
457 } 603 }
458 if (!strncmp(argv[i], "--query=", 8)) { 604 if (!strncmp(argv[i], "--query=", 8)) {
459 ctx.qry.raw = xstrdup(argv[i]+8); 605 ctx.qry.raw = xstrdup(argv[i]+8);
460 } 606 }
461 if (!strncmp(argv[i], "--repo=", 7)) { 607 if (!strncmp(argv[i], "--repo=", 7)) {
462 ctx.qry.repo = xstrdup(argv[i]+7); 608 ctx.qry.repo = xstrdup(argv[i]+7);
463 } 609 }
464 if (!strncmp(argv[i], "--page=", 7)) { 610 if (!strncmp(argv[i], "--page=", 7)) {
465 ctx.qry.page = xstrdup(argv[i]+7); 611 ctx.qry.page = xstrdup(argv[i]+7);
466 } 612 }
467 if (!strncmp(argv[i], "--head=", 7)) { 613 if (!strncmp(argv[i], "--head=", 7)) {
468 ctx.qry.head = xstrdup(argv[i]+7); 614 ctx.qry.head = xstrdup(argv[i]+7);
469 ctx.qry.has_symref = 1; 615 ctx.qry.has_symref = 1;
470 } 616 }
471 if (!strncmp(argv[i], "--sha1=", 7)) { 617 if (!strncmp(argv[i], "--sha1=", 7)) {
472 ctx.qry.sha1 = xstrdup(argv[i]+7); 618 ctx.qry.sha1 = xstrdup(argv[i]+7);
473 ctx.qry.has_sha1 = 1; 619 ctx.qry.has_sha1 = 1;
474 } 620 }
475 if (!strncmp(argv[i], "--ofs=", 6)) { 621 if (!strncmp(argv[i], "--ofs=", 6)) {
476 ctx.qry.ofs = atoi(argv[i]+6); 622 ctx.qry.ofs = atoi(argv[i]+6);
477 } 623 }
478 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;
479 scan++; 636 scan++;
480 scan_tree(argv[i] + 12); 637 scan_tree(argv[i] + 12, repo_config);
481 } 638 }
482 } 639 }
483 if (scan) { 640 if (scan) {
484 qsort(cgit_repolist.repos, cgit_repolist.count, 641 qsort(cgit_repolist.repos, cgit_repolist.count,
485 sizeof(struct cgit_repo), cmp_repos); 642 sizeof(struct cgit_repo), cmp_repos);
486 print_repolist(&cgit_repolist); 643 print_repolist(stdout, &cgit_repolist, 0);
487 exit(0); 644 exit(0);
488 } 645 }
489} 646}
490 647
491static int calc_ttl() 648static int calc_ttl()
492{ 649{
493 if (!ctx.repo) 650 if (!ctx.repo)
494 return ctx.cfg.cache_root_ttl; 651 return ctx.cfg.cache_root_ttl;
495 652
496 if (!ctx.qry.page) 653 if (!ctx.qry.page)
497 return ctx.cfg.cache_repo_ttl; 654 return ctx.cfg.cache_repo_ttl;
498 655
499 if (ctx.qry.has_symref) 656 if (ctx.qry.has_symref)
500 return ctx.cfg.cache_dynamic_ttl; 657 return ctx.cfg.cache_dynamic_ttl;
501 658
502 if (ctx.qry.has_sha1) 659 if (ctx.qry.has_sha1)
503 return ctx.cfg.cache_static_ttl; 660 return ctx.cfg.cache_static_ttl;
504 661
505 return ctx.cfg.cache_repo_ttl; 662 return ctx.cfg.cache_repo_ttl;
506} 663}
507 664
508int main(int argc, const char **argv) 665int main(int argc, const char **argv)
509{ 666{
510 const char *path; 667 const char *path;
511 char *qry; 668 char *qry;
512 int err, ttl; 669 int err, ttl;
513 670
514 prepare_context(&ctx); 671 prepare_context(&ctx);
515 cgit_repolist.length = 0; 672 cgit_repolist.length = 0;
516 cgit_repolist.count = 0; 673 cgit_repolist.count = 0;
517 cgit_repolist.repos = NULL; 674 cgit_repolist.repos = NULL;
518 675
519 cgit_parse_args(argc, argv); 676 cgit_parse_args(argc, argv);
520 parse_configfile(ctx.env.cgit_config, config_cb); 677 parse_configfile(ctx.env.cgit_config, config_cb);
521 ctx.repo = NULL; 678 ctx.repo = NULL;
522 http_parse_querystring(ctx.qry.raw, querystring_cb); 679 http_parse_querystring(ctx.qry.raw, querystring_cb);
523 680
524 /* If virtual-root isn't specified in cgitrc, lets pretend 681 /* If virtual-root isn't specified in cgitrc, lets pretend
525 * that virtual-root equals SCRIPT_NAME. 682 * that virtual-root equals SCRIPT_NAME.
526 */ 683 */
527 if (!ctx.cfg.virtual_root) 684 if (!ctx.cfg.virtual_root)
528 ctx.cfg.virtual_root = ctx.cfg.script_name; 685 ctx.cfg.virtual_root = ctx.cfg.script_name;
529 686
530 /* If no url parameter is specified on the querystring, lets 687 /* If no url parameter is specified on the querystring, lets
531 * 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
532 * urls without the need for rewriterules in the webserver (as 689 * urls without the need for rewriterules in the webserver (as
533 * long as PATH_INFO is included in the cache lookup key). 690 * long as PATH_INFO is included in the cache lookup key).
534 */ 691 */
535 path = ctx.env.path_info; 692 path = ctx.env.path_info;
536 if (!ctx.qry.url && path) { 693 if (!ctx.qry.url && path) {
537 if (path[0] == '/') 694 if (path[0] == '/')
538 path++; 695 path++;
539 ctx.qry.url = xstrdup(path); 696 ctx.qry.url = xstrdup(path);
540 if (ctx.qry.raw) { 697 if (ctx.qry.raw) {
541 qry = ctx.qry.raw; 698 qry = ctx.qry.raw;
542 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 699 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
543 free(qry); 700 free(qry);
544 } else 701 } else
545 ctx.qry.raw = xstrdup(ctx.qry.url); 702 ctx.qry.raw = xstrdup(ctx.qry.url);
546 cgit_parse_url(ctx.qry.url); 703 cgit_parse_url(ctx.qry.url);
547 } 704 }
548 705
549 ttl = calc_ttl(); 706 ttl = calc_ttl();
550 ctx.page.expires += ttl*60; 707 ctx.page.expires += ttl*60;
551 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")) 708 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
552 ctx.cfg.nocache = 1; 709 ctx.cfg.nocache = 1;
553 if (ctx.cfg.nocache) 710 if (ctx.cfg.nocache)
554 ctx.cfg.cache_size = 0; 711 ctx.cfg.cache_size = 0;
555 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 712 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
556 ctx.qry.raw, ttl, process_request, &ctx); 713 ctx.qry.raw, ttl, process_request, &ctx);
557 if (err) 714 if (err)
558 cgit_print_error(fmt("Error processing page: %s (%d)", 715 cgit_print_error(fmt("Error processing page: %s (%d)",
559 strerror(err), err)); 716 strerror(err), err));
560 return err; 717 return err;
561} 718}
diff --git a/cgit.css b/cgit.css
index ebf3322..c47ebc9 100644
--- a/cgit.css
+++ b/cgit.css
@@ -336,193 +336,193 @@ table.diffstat th {
336 336
337table.diffstat td { 337table.diffstat td {
338 padding: 0.2em 0.2em 0.1em 0.1em; 338 padding: 0.2em 0.2em 0.1em 0.1em;
339 font-size: 100%; 339 font-size: 100%;
340 border: none; 340 border: none;
341} 341}
342 342
343table.diffstat td.mode { 343table.diffstat td.mode {
344 white-space: nowrap; 344 white-space: nowrap;
345} 345}
346 346
347table.diffstat td span.modechange { 347table.diffstat td span.modechange {
348 padding-left: 1em; 348 padding-left: 1em;
349 color: red; 349 color: red;
350} 350}
351 351
352table.diffstat td.add a { 352table.diffstat td.add a {
353 color: green; 353 color: green;
354} 354}
355 355
356table.diffstat td.del a { 356table.diffstat td.del a {
357 color: red; 357 color: red;
358} 358}
359 359
360table.diffstat td.upd a { 360table.diffstat td.upd a {
361 color: blue; 361 color: blue;
362} 362}
363 363
364table.diffstat td.graph { 364table.diffstat td.graph {
365 width: 500px; 365 width: 500px;
366 vertical-align: middle; 366 vertical-align: middle;
367} 367}
368 368
369table.diffstat td.graph table { 369table.diffstat td.graph table {
370 border: none; 370 border: none;
371} 371}
372 372
373table.diffstat td.graph td { 373table.diffstat td.graph td {
374 padding: 0px; 374 padding: 0px;
375 border: 0px; 375 border: 0px;
376 height: 7pt; 376 height: 7pt;
377} 377}
378 378
379table.diffstat td.graph td.add { 379table.diffstat td.graph td.add {
380 background-color: #5c5; 380 background-color: #5c5;
381} 381}
382 382
383table.diffstat td.graph td.rem { 383table.diffstat td.graph td.rem {
384 background-color: #c55; 384 background-color: #c55;
385} 385}
386 386
387div.diffstat-summary { 387div.diffstat-summary {
388 color: #888; 388 color: #888;
389 padding-top: 0.5em; 389 padding-top: 0.5em;
390} 390}
391 391
392table.diff { 392table.diff {
393 width: 100%; 393 width: 100%;
394} 394}
395 395
396table.diff td { 396table.diff td {
397 font-family: monospace; 397 font-family: monospace;
398 white-space: pre; 398 white-space: pre;
399} 399}
400 400
401table.diff td div.head { 401table.diff td div.head {
402 font-weight: bold; 402 font-weight: bold;
403 margin-top: 1em; 403 margin-top: 1em;
404 color: black; 404 color: black;
405} 405}
406 406
407table.diff td div.hunk { 407table.diff td div.hunk {
408 color: #009; 408 color: #009;
409} 409}
410 410
411table.diff td div.add { 411table.diff td div.add {
412 color: green; 412 color: green;
413} 413}
414 414
415table.diff td div.del { 415table.diff td div.del {
416 color: red; 416 color: red;
417} 417}
418 418
419.sha1 { 419.sha1 {
420 font-family: monospace; 420 font-family: monospace;
421 font-size: 90%; 421 font-size: 90%;
422} 422}
423 423
424.left { 424.left {
425 text-align: left; 425 text-align: left;
426} 426}
427 427
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
437a.button { 437a.button {
438 font-size: 80%; 438 font-size: 80%;
439 padding: 0em 0.5em; 439 padding: 0em 0.5em;
440} 440}
441 441
442a.primary { 442a.primary {
443 font-size: 100%; 443 font-size: 100%;
444} 444}
445 445
446a.secondary { 446a.secondary {
447 font-size: 90%; 447 font-size: 90%;
448} 448}
449 449
450td.toplevel-repo { 450td.toplevel-repo {
451 451
452} 452}
453 453
454table.list td.sublevel-repo { 454table.list td.sublevel-repo {
455 padding-left: 1.5em; 455 padding-left: 1.5em;
456} 456}
457 457
458div.pager { 458div.pager {
459 text-align: center; 459 text-align: center;
460 margin: 1em 0em 0em 0em; 460 margin: 1em 0em 0em 0em;
461} 461}
462 462
463div.pager a { 463div.pager a {
464 color: #777; 464 color: #777;
465 margin: 0em 0.5em; 465 margin: 0em 0.5em;
466} 466}
467 467
468span.age-mins { 468span.age-mins {
469 font-weight: bold; 469 font-weight: bold;
470 color: #080; 470 color: #080;
471} 471}
472 472
473span.age-hours { 473span.age-hours {
474 color: #080; 474 color: #080;
475} 475}
476 476
477span.age-days { 477span.age-days {
478 color: #040; 478 color: #040;
479} 479}
480 480
481span.age-weeks { 481span.age-weeks {
482 color: #444; 482 color: #444;
483} 483}
484 484
485span.age-months { 485span.age-months {
486 color: #888; 486 color: #888;
487} 487}
488 488
489span.age-years { 489span.age-years {
490 color: #bbb; 490 color: #bbb;
491} 491}
492div.footer { 492div.footer {
493 margin-top: 0.5em; 493 margin-top: 0.5em;
494 text-align: center; 494 text-align: center;
495 font-size: 80%; 495 font-size: 80%;
496 color: #ccc; 496 color: #ccc;
497} 497}
498a.branch-deco { 498a.branch-deco {
499 margin: 0px 0.5em; 499 margin: 0px 0.5em;
500 padding: 0px 0.25em; 500 padding: 0px 0.25em;
501 background-color: #88ff88; 501 background-color: #88ff88;
502 border: solid 1px #007700; 502 border: solid 1px #007700;
503} 503}
504a.tag-deco { 504a.tag-deco {
505 margin: 0px 0.5em; 505 margin: 0px 0.5em;
506 padding: 0px 0.25em; 506 padding: 0px 0.25em;
507 background-color: #ffff88; 507 background-color: #ffff88;
508 border: solid 1px #777700; 508 border: solid 1px #777700;
509} 509}
510a.remote-deco { 510a.remote-deco {
511 margin: 0px 0.5em; 511 margin: 0px 0.5em;
512 padding: 0px 0.25em; 512 padding: 0px 0.25em;
513 background-color: #ccccff; 513 background-color: #ccccff;
514 border: solid 1px #000077; 514 border: solid 1px #000077;
515} 515}
516a.deco { 516a.deco {
517 margin: 0px 0.5em; 517 margin: 0px 0.5em;
518 padding: 0px 0.25em; 518 padding: 0px 0.25em;
519 background-color: #ff8888; 519 background-color: #ff8888;
520 border: solid 1px #770000; 520 border: solid 1px #770000;
521} 521}
522 522
523div.commit-subject a { 523div.commit-subject a {
524 margin-left: 1em; 524 margin-left: 1em;
525 font-size: 75%; 525 font-size: 75%;
526} 526}
527 527
528table.stats { 528table.stats {
diff --git a/cgit.h b/cgit.h
index a20679a..6c6c460 100644
--- a/cgit.h
+++ b/cgit.h
@@ -1,268 +1,273 @@
1#ifndef CGIT_H 1#ifndef CGIT_H
2#define CGIT_H 2#define CGIT_H
3 3
4 4
5#include <git-compat-util.h> 5#include <git-compat-util.h>
6#include <cache.h> 6#include <cache.h>
7#include <grep.h> 7#include <grep.h>
8#include <object.h> 8#include <object.h>
9#include <tree.h> 9#include <tree.h>
10#include <commit.h> 10#include <commit.h>
11#include <tag.h> 11#include <tag.h>
12#include <diff.h> 12#include <diff.h>
13#include <diffcore.h> 13#include <diffcore.h>
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 <string-list.h>
19#include <xdiff-interface.h> 19#include <xdiff-interface.h>
20#include <xdiff/xdiff.h> 20#include <xdiff/xdiff.h>
21#include <utf8.h> 21#include <utf8.h>
22 22
23 23
24/* 24/*
25 * Dateformats used on misc. pages 25 * Dateformats used on misc. pages
26 */ 26 */
27#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)" 27#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)"
28#define FMT_SHORTDATE "%Y-%m-%d" 28#define FMT_SHORTDATE "%Y-%m-%d"
29#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ" 29#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ"
30 30
31 31
32/* 32/*
33 * Limits used for relative dates 33 * Limits used for relative dates
34 */ 34 */
35#define TM_MIN 60 35#define TM_MIN 60
36#define TM_HOUR (TM_MIN * 60) 36#define TM_HOUR (TM_MIN * 60)
37#define TM_DAY (TM_HOUR * 24) 37#define TM_DAY (TM_HOUR * 24)
38#define TM_WEEK (TM_DAY * 7) 38#define TM_WEEK (TM_DAY * 7)
39#define TM_YEAR (TM_DAY * 365) 39#define TM_YEAR (TM_DAY * 365)
40#define TM_MONTH (TM_YEAR / 12.0) 40#define TM_MONTH (TM_YEAR / 12.0)
41 41
42 42
43/* 43/*
44 * Default encoding 44 * Default encoding
45 */ 45 */
46#define PAGE_ENCODING "UTF-8" 46#define PAGE_ENCODING "UTF-8"
47 47
48typedef void (*configfn)(const char *name, const char *value); 48typedef void (*configfn)(const char *name, const char *value);
49typedef void (*filepair_fn)(struct diff_filepair *pair); 49typedef void (*filepair_fn)(struct diff_filepair *pair);
50typedef void (*linediff_fn)(char *line, int len); 50typedef void (*linediff_fn)(char *line, int len);
51 51
52struct cgit_filter { 52struct cgit_filter {
53 char *cmd; 53 char *cmd;
54 char **argv; 54 char **argv;
55 int old_stdout; 55 int old_stdout;
56 int pipe_fh[2]; 56 int pipe_fh[2];
57 int pid; 57 int pid;
58 int exitstatus; 58 int exitstatus;
59}; 59};
60 60
61struct cgit_repo { 61struct cgit_repo {
62 char *url; 62 char *url;
63 char *name; 63 char *name;
64 char *path; 64 char *path;
65 char *desc; 65 char *desc;
66 char *owner; 66 char *owner;
67 char *defbranch; 67 char *defbranch;
68 char *group;
69 char *module_link; 68 char *module_link;
70 char *readme; 69 char *readme;
70 char *section;
71 char *clone_url; 71 char *clone_url;
72 int snapshots; 72 int snapshots;
73 int enable_log_filecount; 73 int enable_log_filecount;
74 int enable_log_linecount; 74 int enable_log_linecount;
75 int max_stats; 75 int max_stats;
76 time_t mtime; 76 time_t mtime;
77 struct cgit_filter *about_filter; 77 struct cgit_filter *about_filter;
78 struct cgit_filter *commit_filter; 78 struct cgit_filter *commit_filter;
79 struct cgit_filter *source_filter; 79 struct cgit_filter *source_filter;
80}; 80};
81 81
82typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name,
83 const char *value);
84
82struct cgit_repolist { 85struct cgit_repolist {
83 int length; 86 int length;
84 int count; 87 int count;
85 struct cgit_repo *repos; 88 struct cgit_repo *repos;
86}; 89};
87 90
88struct commitinfo { 91struct commitinfo {
89 struct commit *commit; 92 struct commit *commit;
90 char *author; 93 char *author;
91 char *author_email; 94 char *author_email;
92 unsigned long author_date; 95 unsigned long author_date;
93 char *committer; 96 char *committer;
94 char *committer_email; 97 char *committer_email;
95 unsigned long committer_date; 98 unsigned long committer_date;
96 char *subject; 99 char *subject;
97 char *msg; 100 char *msg;
98 char *msg_encoding; 101 char *msg_encoding;
99}; 102};
100 103
101struct taginfo { 104struct taginfo {
102 char *tagger; 105 char *tagger;
103 char *tagger_email; 106 char *tagger_email;
104 unsigned long tagger_date; 107 unsigned long tagger_date;
105 char *msg; 108 char *msg;
106}; 109};
107 110
108struct refinfo { 111struct refinfo {
109 const char *refname; 112 const char *refname;
110 struct object *object; 113 struct object *object;
111 union { 114 union {
112 struct taginfo *tag; 115 struct taginfo *tag;
113 struct commitinfo *commit; 116 struct commitinfo *commit;
114 }; 117 };
115}; 118};
116 119
117struct reflist { 120struct reflist {
118 struct refinfo **refs; 121 struct refinfo **refs;
119 int alloc; 122 int alloc;
120 int count; 123 int count;
121}; 124};
122 125
123struct cgit_query { 126struct cgit_query {
124 int has_symref; 127 int has_symref;
125 int has_sha1; 128 int has_sha1;
126 char *raw; 129 char *raw;
127 char *repo; 130 char *repo;
128 char *page; 131 char *page;
129 char *search; 132 char *search;
130 char *grep; 133 char *grep;
131 char *head; 134 char *head;
132 char *sha1; 135 char *sha1;
133 char *sha2; 136 char *sha2;
134 char *path; 137 char *path;
135 char *name; 138 char *name;
136 char *mimetype; 139 char *mimetype;
137 char *url; 140 char *url;
138 char *period; 141 char *period;
139 int ofs; 142 int ofs;
140 int nohead; 143 int nohead;
141 char *sort; 144 char *sort;
142 int showmsg; 145 int showmsg;
143}; 146};
144 147
145struct cgit_config { 148struct cgit_config {
146 char *agefile; 149 char *agefile;
147 char *cache_root; 150 char *cache_root;
148 char *clone_prefix; 151 char *clone_prefix;
149 char *css; 152 char *css;
150 char *favicon; 153 char *favicon;
151 char *footer; 154 char *footer;
152 char *head_include; 155 char *head_include;
153 char *header; 156 char *header;
154 char *index_header; 157 char *index_header;
155 char *index_info; 158 char *index_info;
156 char *logo; 159 char *logo;
157 char *logo_link; 160 char *logo_link;
158 char *module_link; 161 char *module_link;
159 char *repo_group;
160 char *robots; 162 char *robots;
161 char *root_title; 163 char *root_title;
162 char *root_desc; 164 char *root_desc;
163 char *root_readme; 165 char *root_readme;
164 char *script_name; 166 char *script_name;
167 char *section;
165 char *virtual_root; 168 char *virtual_root;
166 int cache_size; 169 int cache_size;
167 int cache_dynamic_ttl; 170 int cache_dynamic_ttl;
168 int cache_max_create_time; 171 int cache_max_create_time;
169 int cache_repo_ttl; 172 int cache_repo_ttl;
170 int cache_root_ttl; 173 int cache_root_ttl;
174 int cache_scanrc_ttl;
171 int cache_static_ttl; 175 int cache_static_ttl;
172 int embedded; 176 int embedded;
177 int enable_filter_overrides;
173 int enable_index_links; 178 int enable_index_links;
174 int enable_log_filecount; 179 int enable_log_filecount;
175 int enable_log_linecount; 180 int enable_log_linecount;
176 int enable_tree_linenumbers; 181 int enable_tree_linenumbers;
177 int local_time; 182 int local_time;
178 int max_repo_count; 183 int max_repo_count;
179 int max_commit_count; 184 int max_commit_count;
180 int max_lock_attempts; 185 int max_lock_attempts;
181 int max_msg_len; 186 int max_msg_len;
182 int max_repodesc_len; 187 int max_repodesc_len;
183 int max_stats; 188 int max_stats;
184 int nocache; 189 int nocache;
185 int noplainemail; 190 int noplainemail;
186 int noheader; 191 int noheader;
187 int renamelimit; 192 int renamelimit;
188 int snapshots; 193 int snapshots;
189 int summary_branches; 194 int summary_branches;
190 int summary_log; 195 int summary_log;
191 int summary_tags; 196 int summary_tags;
192 struct string_list mimetypes; 197 struct string_list mimetypes;
193 struct cgit_filter *about_filter; 198 struct cgit_filter *about_filter;
194 struct cgit_filter *commit_filter; 199 struct cgit_filter *commit_filter;
195 struct cgit_filter *source_filter; 200 struct cgit_filter *source_filter;
196}; 201};
197 202
198struct cgit_page { 203struct cgit_page {
199 time_t modified; 204 time_t modified;
200 time_t expires; 205 time_t expires;
201 size_t size; 206 size_t size;
202 char *mimetype; 207 char *mimetype;
203 char *charset; 208 char *charset;
204 char *filename; 209 char *filename;
205 char *etag; 210 char *etag;
206 char *title; 211 char *title;
207 int status; 212 int status;
208 char *statusmsg; 213 char *statusmsg;
209}; 214};
210 215
211struct cgit_environment { 216struct cgit_environment {
212 char *cgit_config; 217 char *cgit_config;
213 char *http_host; 218 char *http_host;
214 char *https; 219 char *https;
215 char *no_http; 220 char *no_http;
216 char *path_info; 221 char *path_info;
217 char *query_string; 222 char *query_string;
218 char *request_method; 223 char *request_method;
219 char *script_name; 224 char *script_name;
220 char *server_name; 225 char *server_name;
221 char *server_port; 226 char *server_port;
222}; 227};
223 228
224struct cgit_context { 229struct cgit_context {
225 struct cgit_environment env; 230 struct cgit_environment env;
226 struct cgit_query qry; 231 struct cgit_query qry;
227 struct cgit_config cfg; 232 struct cgit_config cfg;
228 struct cgit_repo *repo; 233 struct cgit_repo *repo;
229 struct cgit_page page; 234 struct cgit_page page;
230}; 235};
231 236
232struct cgit_snapshot_format { 237struct cgit_snapshot_format {
233 const char *suffix; 238 const char *suffix;
234 const char *mimetype; 239 const char *mimetype;
235 write_archive_fn_t write_func; 240 write_archive_fn_t write_func;
236 int bit; 241 int bit;
237}; 242};
238 243
239extern const char *cgit_version; 244extern const char *cgit_version;
240 245
241extern struct cgit_repolist cgit_repolist; 246extern struct cgit_repolist cgit_repolist;
242extern struct cgit_context ctx; 247extern struct cgit_context ctx;
243extern const struct cgit_snapshot_format cgit_snapshot_formats[]; 248extern const struct cgit_snapshot_format cgit_snapshot_formats[];
244 249
245extern struct cgit_repo *cgit_add_repo(const char *url); 250extern struct cgit_repo *cgit_add_repo(const char *url);
246extern struct cgit_repo *cgit_get_repoinfo(const char *url); 251extern struct cgit_repo *cgit_get_repoinfo(const char *url);
247extern void cgit_repo_config_cb(const char *name, const char *value); 252extern void cgit_repo_config_cb(const char *name, const char *value);
248 253
249extern int chk_zero(int result, char *msg); 254extern int chk_zero(int result, char *msg);
250extern int chk_positive(int result, char *msg); 255extern int chk_positive(int result, char *msg);
251extern int chk_non_negative(int result, char *msg); 256extern int chk_non_negative(int result, char *msg);
252 257
253extern char *trim_end(const char *str, char c); 258extern char *trim_end(const char *str, char c);
254extern char *strlpart(char *txt, int maxlen); 259extern char *strlpart(char *txt, int maxlen);
255extern char *strrpart(char *txt, int maxlen); 260extern char *strrpart(char *txt, int maxlen);
256 261
257extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); 262extern void cgit_add_ref(struct reflist *list, struct refinfo *ref);
258extern int cgit_refs_cb(const char *refname, const unsigned char *sha1, 263extern int cgit_refs_cb(const char *refname, const unsigned char *sha1,
259 int flags, void *cb_data); 264 int flags, void *cb_data);
260 265
261extern void *cgit_free_commitinfo(struct commitinfo *info); 266extern void *cgit_free_commitinfo(struct commitinfo *info);
262 267
263extern int cgit_diff_files(const unsigned char *old_sha1, 268extern int cgit_diff_files(const unsigned char *old_sha1,
264 const unsigned char *new_sha1, 269 const unsigned char *new_sha1,
265 unsigned long *old_size, unsigned long *new_size, 270 unsigned long *old_size, unsigned long *new_size,
266 int *binary, linediff_fn fn); 271 int *binary, linediff_fn fn);
267 272
268extern void cgit_diff_tree(const unsigned char *old_sha1, 273extern void cgit_diff_tree(const unsigned char *old_sha1,
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 3b16db9..4dc383d 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -1,411 +1,449 @@
1CGITRC(5) 1CGITRC(5)
2======== 2========
3 3
4 4
5NAME 5NAME
6---- 6----
7cgitrc - runtime configuration for cgit 7cgitrc - runtime configuration for cgit
8 8
9 9
10SYNOPSIS 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 17LOCATION
18-------- 18--------
19The default location of cgitrc, defined at compile time, is /etc/cgitrc. At 19The default location of cgitrc, defined at compile time, is /etc/cgitrc. At
20runtime, cgit will consult the environment variable CGIT_CONFIG and, if 20runtime, cgit will consult the environment variable CGIT_CONFIG and, if
21defined, use its value instead. 21defined, use its value instead.
22 22
23 23
24GLOBAL SETTINGS 24GLOBAL SETTINGS
25--------------- 25---------------
26about-filter:: 26about-filter::
27 Specifies a command which will be invoked to format the content of 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 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 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: 30 command will be included verbatim on the about page. Default value:
31 none. 31 none.
32 32
33agefile:: 33agefile::
34 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
35 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.
36 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"
37 function in libgit. Recommended timestamp-format is "yyyy-mm-dd 37 function in libgit. Recommended timestamp-format is "yyyy-mm-dd
38 hh:mm:ss". Default value: "info/web/last-modified". 38 hh:mm:ss". Default value: "info/web/last-modified".
39 39
40cache-root:: 40cache-root::
41 Path used to store the cgit cache entries. Default value: 41 Path used to store the cgit cache entries. Default value:
42 "/var/cache/cgit". 42 "/var/cache/cgit".
43 43
44cache-dynamic-ttl:: 44cache-dynamic-ttl::
45 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
46 version of repository pages accessed without a fixed SHA1. Default 46 version of repository pages accessed without a fixed SHA1. Default
47 value: "5". 47 value: "5".
48 48
49cache-repo-ttl:: 49cache-repo-ttl::
50 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
51 version of the repository summary page. Default value: "5". 51 version of the repository summary page. Default value: "5".
52 52
53cache-root-ttl:: 53cache-root-ttl::
54 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
55 version of the repository index page. Default value: "5". 55 version of the repository index page. Default value: "5".
56 56
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
57cache-size:: 61cache-size::
58 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"
59 (i.e. caching is disabled). 63 (i.e. caching is disabled).
60 64
61cache-static-ttl:: 65cache-static-ttl::
62 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
63 version of repository pages accessed with a fixed SHA1. Default value: 67 version of repository pages accessed with a fixed SHA1. Default value:
64 "5". 68 "5".
65 69
66clone-prefix:: 70clone-prefix::
67 Space-separated list of common prefixes which, when combined with a 71 Space-separated list of common prefixes which, when combined with a
68 repository url, generates valid clone urls for the repository. This 72 repository url, generates valid clone urls for the repository. This
69 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:
70 none. 74 none.
71 75
72commit-filter:: 76commit-filter::
73 Specifies a command which will be invoked to format commit messages. 77 Specifies a command which will be invoked to format commit messages.
74 The command will get the message on its STDIN, and the STDOUT from the 78 The command will get the message on its STDIN, and the STDOUT from the
75 command will be included verbatim as the commit message, i.e. this can 79 command will be included verbatim as the commit message, i.e. this can
76 be used to implement bugtracker integration. Default value: none. 80 be used to implement bugtracker integration. Default value: none.
77 81
78css:: 82css::
79 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.
80 Default value: "/cgit.css". 84 Default value: "/cgit.css".
81 85
82embedded:: 86embedded::
83 Flag which, when set to "1", will make cgit generate a html fragment 87 Flag which, when set to "1", will make cgit generate a html fragment
84 suitable for embedding in other html pages. Default value: none. See 88 suitable for embedding in other html pages. Default value: none. See
85 also: "noheader". 89 also: "noheader".
86 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
87enable-index-links:: 95enable-index-links::
88 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
89 each repo in the repository index (specifically, to the "summary", 97 each repo in the repository index (specifically, to the "summary",
90 "commit" and "tree" pages). Default value: "0". 98 "commit" and "tree" pages). Default value: "0".
91 99
92enable-log-filecount:: 100enable-log-filecount::
93 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
94 modified files for each commit on the repository log page. Default 102 modified files for each commit on the repository log page. Default
95 value: "0". 103 value: "0".
96 104
97enable-log-linecount:: 105enable-log-linecount::
98 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
99 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
100 value: "0". 108 value: "0".
101 109
102enable-tree-linenumbers:: 110enable-tree-linenumbers::
103 Flag which, when set to "1", will make cgit generate linenumber links 111 Flag which, when set to "1", will make cgit generate linenumber links
104 for plaintext blobs printed in the tree view. Default value: "1". 112 for plaintext blobs printed in the tree view. Default value: "1".
105 113
106favicon:: 114favicon::
107 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
108 suggested to use the value "/favicon.ico" since certain browsers will 116 suggested to use the value "/favicon.ico" since certain browsers will
109 ignore other values. Default value: none. 117 ignore other values. Default value: none.
110 118
111footer:: 119footer::
112 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
113 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
114 "generated by..." message. Default value: none. 122 "generated by..." message. Default value: none.
115 123
116head-include:: 124head-include::
117 The content of the file specified with this option will be included 125 The content of the file specified with this option will be included
118 verbatim in the html HEAD section on all pages. Default value: none. 126 verbatim in the html HEAD section on all pages. Default value: none.
119 127
120header:: 128header::
121 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
122 verbatim at the top of all pages. Default value: none. 130 verbatim at the top of all pages. Default value: none.
123 131
124include:: 132include::
125 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-
126 file is parsed. Default value: none. 134 file is parsed. Default value: none.
127 135
128index-header:: 136index-header::
129 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
130 verbatim above the repository index. This setting is deprecated, and 138 verbatim above the repository index. This setting is deprecated, and
131 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
132 value: none. 140 value: none.
133 141
134index-info:: 142index-info::
135 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
136 verbatim below the heading on the repository index page. This setting 144 verbatim below the heading on the repository index page. This setting
137 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
138 instead). Default value: none. 146 instead). Default value: none.
139 147
140local-time:: 148local-time::
141 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
142 servers timezone. Default value: "0". 150 servers timezone. Default value: "0".
143 151
144logo:: 152logo::
145 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
146 on all cgit pages. Default value: "/cgit.png". 154 on all cgit pages. Default value: "/cgit.png".
147 155
148logo-link:: 156logo-link::
149 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
150 calculated url of the repository index page will be used. Default 158 calculated url of the repository index page will be used. Default
151 value: none. 159 value: none.
152 160
153max-commit-count:: 161max-commit-count::
154 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
155 value: "50". 163 value: "50".
156 164
157max-message-length:: 165max-message-length::
158 Specifies the maximum number of commit message characters to display in 166 Specifies the maximum number of commit message characters to display in
159 "log" view. Default value: "80". 167 "log" view. Default value: "80".
160 168
161max-repo-count:: 169max-repo-count::
162 Specifies the number of entries to list per page on therepository 170 Specifies the number of entries to list per page on therepository
163 index page. Default value: "50". 171 index page. Default value: "50".
164 172
165max-repodesc-length:: 173max-repodesc-length::
166 Specifies the maximum number of repo description characters to display 174 Specifies the maximum number of repo description characters to display
167 on the repository index page. Default value: "80". 175 on the repository index page. Default value: "80".
168 176
169max-stats:: 177max-stats::
170 Set the default maximum statistics period. Valid values are "week", 178 Set the default maximum statistics period. Valid values are "week",
171 "month", "quarter" and "year". If unspecified, statistics are 179 "month", "quarter" and "year". If unspecified, statistics are
172 disabled. Default value: none. See also: "repo.max-stats". 180 disabled. Default value: none. See also: "repo.max-stats".
173 181
174mimetype.<ext>:: 182mimetype.<ext>::
175 Set the mimetype for the specified filename extension. This is used 183 Set the mimetype for the specified filename extension. This is used
176 by the `plain` command when returning blob content. 184 by the `plain` command when returning blob content.
177 185
178module-link:: 186module-link::
179 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
180 submodule is printed in a directory listing. The arguments for the 188 submodule is printed in a directory listing. The arguments for the
181 formatstring are the path and SHA1 of the submodule commit. Default 189 formatstring are the path and SHA1 of the submodule commit. Default
182 value: "./?repo=%s&page=commit&id=%s" 190 value: "./?repo=%s&page=commit&id=%s"
183 191
184nocache:: 192nocache::
185 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
186 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
187 value: "0". 195 value: "0".
188 196
189noplainemail:: 197noplainemail::
190 If set to "1" showing full author email adresses will be disabled. 198 If set to "1" showing full author email adresses will be disabled.
191 Default value: "0". 199 Default value: "0".
192 200
193noheader:: 201noheader::
194 Flag which, when set to "1", will make cgit omit the standard header 202 Flag which, when set to "1", will make cgit omit the standard header
195 on all pages. Default value: none. See also: "embedded". 203 on all pages. Default value: none. See also: "embedded".
196 204
197renamelimit:: 205renamelimit::
198 Maximum number of files to consider when detecting renames. The value 206 Maximum number of files to consider when detecting renames. The value
199 "-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
200 `man git-diff`). Default value: "-1". 208 `man git-diff`). Default value: "-1".
201 209
202repo.group:: 210repo.group::
203 A value for the current repository group, which all repositories 211 Legacy alias for "section". This option is deprecated and will not be
204 specified after this setting will inherit. Default value: none. 212 supported in cgit-1.0.
205 213
206robots:: 214robots::
207 Text used as content for the "robots" meta-tag. Default value: 215 Text used as content for the "robots" meta-tag. Default value:
208 "index, nofollow". 216 "index, nofollow".
209 217
210root-desc:: 218root-desc::
211 Text printed below the heading on the repository index page. Default 219 Text printed below the heading on the repository index page. Default
212 value: "a fast webinterface for the git dscm". 220 value: "a fast webinterface for the git dscm".
213 221
214root-readme:: 222root-readme::
215 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
216 verbatim below the "about" link on the repository index page. Default 224 verbatim below the "about" link on the repository index page. Default
217 value: none. 225 value: none.
218 226
219root-title:: 227root-title::
220 Text printed as heading on the repository index page. Default value: 228 Text printed as heading on the repository index page. Default value:
221 "Git Repository Browser". 229 "Git Repository Browser".
222 230
231scan-path::
232 A path which will be scanned for repositories. If caching is enabled,
233 the result will be cached as a cgitrc include-file in the cache
234 directory. Default value: none. See also: cache-scanrc-ttl.
235
236section::
237 The name of the current repository section - all repositories defined
238 after this option will inherit the current section name. Default value:
239 none.
240
223snapshots:: 241snapshots::
224 Text which specifies the default set of snapshot formats generated by 242 Text which specifies the default set of snapshot formats generated by
225 cgit. The value is a space-separated list of zero or more of the 243 cgit. The value is a space-separated list of zero or more of the
226 values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none. 244 values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none.
227 245
228source-filter:: 246source-filter::
229 Specifies a command which will be invoked to format plaintext blobs 247 Specifies a command which will be invoked to format plaintext blobs
230 in the tree view. The command will get the blob content on its STDIN 248 in the tree view. The command will get the blob content on its STDIN
231 and the name of the blob as its only command line argument. The STDOUT 249 and the name of the blob as its only command line argument. The STDOUT
232 from the command will be included verbatim as the blob contents, i.e. 250 from the command will be included verbatim as the blob contents, i.e.
233 this can be used to implement e.g. syntax highlighting. Default value: 251 this can be used to implement e.g. syntax highlighting. Default value:
234 none. 252 none.
235 253
236summary-branches:: 254summary-branches::
237 Specifies the number of branches to display in the repository "summary" 255 Specifies the number of branches to display in the repository "summary"
238 view. Default value: "10". 256 view. Default value: "10".
239 257
240summary-log:: 258summary-log::
241 Specifies the number of log entries to display in the repository 259 Specifies the number of log entries to display in the repository
242 "summary" view. Default value: "10". 260 "summary" view. Default value: "10".
243 261
244summary-tags:: 262summary-tags::
245 Specifies the number of tags to display in the repository "summary" 263 Specifies the number of tags to display in the repository "summary"
246 view. Default value: "10". 264 view. Default value: "10".
247 265
248virtual-root:: 266virtual-root::
249 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
250 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
251 '/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
252 value: none. 270 value: none.
253 NOTE: cgit has recently learned how to use PATH_INFO to achieve the 271 NOTE: cgit has recently learned how to use PATH_INFO to achieve the
254 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.
255 273
256REPOSITORY SETTINGS 274REPOSITORY SETTINGS
257------------------- 275-------------------
258repo.about-filter:: 276repo.about-filter::
259 Override the default about-filter. Default value: <about-filter>. 277 Override the default about-filter. Default value: none. See also:
278 "enable-filter-overrides".
260 279
261repo.clone-url:: 280repo.clone-url::
262 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.
263 Default value: none. 282 Default value: none.
264 283
265repo.commit-filter:: 284repo.commit-filter::
266 Override the default commit-filter. Default value: <commit-filter>. 285 Override the default commit-filter. Default value: none. See also:
286 "enable-filter-overrides".
267 287
268repo.defbranch:: 288repo.defbranch::
269 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
270 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
271 as default instead. Default value: "master". 291 as default instead. Default value: "master".
272 292
273repo.desc:: 293repo.desc::
274 The value to show as repository description. Default value: none. 294 The value to show as repository description. Default value: none.
275 295
276repo.enable-log-filecount:: 296repo.enable-log-filecount::
277 A flag which can be used to disable the global setting 297 A flag which can be used to disable the global setting
278 `enable-log-filecount'. Default value: none. 298 `enable-log-filecount'. Default value: none.
279 299
280repo.enable-log-linecount:: 300repo.enable-log-linecount::
281 A flag which can be used to disable the global setting 301 A flag which can be used to disable the global setting
282 `enable-log-linecount'. Default value: none. 302 `enable-log-linecount'. Default value: none.
283 303
284repo.max-stats:: 304repo.max-stats::
285 Override the default maximum statistics period. Valid values are equal 305 Override the default maximum statistics period. Valid values are equal
286 to the values specified for the global "max-stats" setting. Default 306 to the values specified for the global "max-stats" setting. Default
287 value: none. 307 value: none.
288 308
289repo.name:: 309repo.name::
290 The value to show as repository name. Default value: <repo.url>. 310 The value to show as repository name. Default value: <repo.url>.
291 311
292repo.owner:: 312repo.owner::
293 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:
294 none. 314 none.
295 315
296repo.path:: 316repo.path::
297 An absolute path to the repository directory. For non-bare repositories 317 An absolute path to the repository directory. For non-bare repositories
298 this is the .git-directory. Default value: none. 318 this is the .git-directory. Default value: none.
299 319
300repo.readme:: 320repo.readme::
301 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
302 verbatim as the "About" page for this repo. Default value: none. 322 verbatim as the "About" page for this repo. Default value: none.
303 323
304repo.snapshots:: 324repo.snapshots::
305 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
306 "snapshots" global setting. Default value: <snapshots>. 326 "snapshots" global setting. Default value: <snapshots>.
307 327
328repo.section::
329 Override the current section name for this repository. Default value:
330 none.
331
308repo.source-filter:: 332repo.source-filter::
309 Override the default source-filter. Default value: <source-filter>. 333 Override the default source-filter. Default value: none. See also:
334 "enable-filter-overrides".
310 335
311repo.url:: 336repo.url::
312 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
313 setting specified for each repo. Default value: none. 338 setting specified for each repo. Default value: none.
314 339
315 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
316EXAMPLE CGITRC FILE 354EXAMPLE CGITRC FILE
317------------------- 355-------------------
318 356
319.... 357....
320# Enable caching of up to 1000 output entriess 358# Enable caching of up to 1000 output entriess
321cache-size=1000 359cache-size=1000
322 360
323 361
324# Specify some default clone prefixes 362# Specify some default clone prefixes
325clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git 363clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git
326 364
327# Specify the css url 365# Specify the css url
328css=/css/cgit.css 366css=/css/cgit.css
329 367
330 368
331# Show extra links for each repository on the index page 369# Show extra links for each repository on the index page
332enable-index-links=1 370enable-index-links=1
333 371
334 372
335# Show number of affected files per commit on the log pages 373# Show number of affected files per commit on the log pages
336enable-log-filecount=1 374enable-log-filecount=1
337 375
338 376
339# Show number of added/removed lines per commit on the log pages 377# Show number of added/removed lines per commit on the log pages
340enable-log-linecount=1 378enable-log-linecount=1
341 379
342 380
343# Add a cgit favicon 381# Add a cgit favicon
344favicon=/favicon.ico 382favicon=/favicon.ico
345 383
346 384
347# Use a custom logo 385# Use a custom logo
348logo=/img/mylogo.png 386logo=/img/mylogo.png
349 387
350 388
351# Enable statistics per week, month and quarter 389# Enable statistics per week, month and quarter
352max-stats=quarter 390max-stats=quarter
353 391
354 392
355# Set the title and heading of the repository index page 393# Set the title and heading of the repository index page
356root-title=foobar.com git repositories 394root-title=foobar.com git repositories
357 395
358 396
359# Set a subheading for the repository index page 397# Set a subheading for the repository index page
360root-desc=tracking the foobar development 398root-desc=tracking the foobar development
361 399
362 400
363# Include some more info about foobar.com on the index page 401# Include some more info about foobar.com on the index page
364root-readme=/var/www/htdocs/about.html 402root-readme=/var/www/htdocs/about.html
365 403
366 404
367# Allow download of tar.gz, tar.bz2 and zip-files 405# Allow download of tar.gz, tar.bz2 and zip-files
368snapshots=tar.gz tar.bz2 zip 406snapshots=tar.gz tar.bz2 zip
369 407
370 408
371## 409##
372## List of common mimetypes 410## List of common mimetypes
373## 411##
374 412
375mimetype.git=image/git 413mimetype.git=image/git
376mimetype.html=text/html 414mimetype.html=text/html
377mimetype.jpg=image/jpeg 415mimetype.jpg=image/jpeg
378mimetype.jpeg=image/jpeg 416mimetype.jpeg=image/jpeg
379mimetype.pdf=application/pdf 417mimetype.pdf=application/pdf
380mimetype.png=image/png 418mimetype.png=image/png
381mimetype.svg=image/svg+xml 419mimetype.svg=image/svg+xml
382 420
383 421
384## 422##
385## List of repositories. 423## List of repositories.
386## 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
387## displayed under a group heading 425## displayed under a group heading
388## 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')
389## and included like this: 427## and included like this:
390## include=/etc/cgitrepos 428## include=/etc/cgitrepos
391## 429##
392 430
393 431
394repo.url=foo 432repo.url=foo
395repo.path=/pub/git/foo.git 433repo.path=/pub/git/foo.git
396repo.desc=the master foo repository 434repo.desc=the master foo repository
397repo.owner=fooman@foobar.com 435repo.owner=fooman@foobar.com
398repo.readme=info/web/about.html 436repo.readme=info/web/about.html
399 437
400 438
401repo.url=bar 439repo.url=bar
402repo.path=/pub/git/bar.git 440repo.path=/pub/git/bar.git
403repo.desc=the bars for your foo 441repo.desc=the bars for your foo
404repo.owner=barman@foobar.com 442repo.owner=barman@foobar.com
405repo.readme=info/web/about.html 443repo.readme=info/web/about.html
406 444
407 445
408# The next repositories will be displayed under the 'extras' heading 446# The next repositories will be displayed under the 'extras' heading
409repo.group=extras 447repo.group=extras
410 448
411 449
diff --git a/scan-tree.c b/scan-tree.c
index 4da21a4..dbca797 100644
--- a/scan-tree.c
+++ b/scan-tree.c
@@ -1,128 +1,146 @@
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
6/* return 1 if path contains a objects/ directory and a HEAD file */ 7/* return 1 if path contains a objects/ directory and a HEAD file */
7static int is_git_dir(const char *path) 8static int is_git_dir(const char *path)
8{ 9{
9 struct stat st; 10 struct stat st;
10 static char buf[MAX_PATH]; 11 static char buf[MAX_PATH];
11 12
12 if (snprintf(buf, MAX_PATH, "%s/objects", path) >= MAX_PATH) { 13 if (snprintf(buf, MAX_PATH, "%s/objects", path) >= MAX_PATH) {
13 fprintf(stderr, "Insanely long path: %s\n", path); 14 fprintf(stderr, "Insanely long path: %s\n", path);
14 return 0; 15 return 0;
15 } 16 }
16 if (stat(buf, &st)) { 17 if (stat(buf, &st)) {
17 if (errno != ENOENT) 18 if (errno != ENOENT)
18 fprintf(stderr, "Error checking path %s: %s (%d)\n", 19 fprintf(stderr, "Error checking path %s: %s (%d)\n",
19 path, strerror(errno), errno); 20 path, strerror(errno), errno);
20 return 0; 21 return 0;
21 } 22 }
22 if (!S_ISDIR(st.st_mode)) 23 if (!S_ISDIR(st.st_mode))
23 return 0; 24 return 0;
24 25
25 sprintf(buf, "%s/HEAD", path); 26 sprintf(buf, "%s/HEAD", path);
26 if (stat(buf, &st)) { 27 if (stat(buf, &st)) {
27 if (errno != ENOENT) 28 if (errno != ENOENT)
28 fprintf(stderr, "Error checking path %s: %s (%d)\n", 29 fprintf(stderr, "Error checking path %s: %s (%d)\n",
29 path, strerror(errno), errno); 30 path, strerror(errno), errno);
30 return 0; 31 return 0;
31 } 32 }
32 if (!S_ISREG(st.st_mode)) 33 if (!S_ISREG(st.st_mode))
33 return 0; 34 return 0;
34 35
35 return 1; 36 return 1;
36} 37}
37 38
38static void add_repo(const char *base, const char *path) 39struct cgit_repo *repo;
40repo_config_fn config_fn;
41
42static void repo_config(const char *name, const char *value)
43{
44 config_fn(repo, name, value);
45}
46
47static void add_repo(const char *base, const char *path, repo_config_fn fn)
39{ 48{
40 struct cgit_repo *repo;
41 struct stat st; 49 struct stat st;
42 struct passwd *pwd; 50 struct passwd *pwd;
43 char *p; 51 char *p;
44 size_t size; 52 size_t size;
45 53
46 if (stat(path, &st)) { 54 if (stat(path, &st)) {
47 fprintf(stderr, "Error accessing %s: %s (%d)\n", 55 fprintf(stderr, "Error accessing %s: %s (%d)\n",
48 path, strerror(errno), errno); 56 path, strerror(errno), errno);
49 return; 57 return;
50 } 58 }
51 if ((pwd = getpwuid(st.st_uid)) == NULL) { 59 if ((pwd = getpwuid(st.st_uid)) == NULL) {
52 fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n", 60 fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n",
53 path, strerror(errno), errno); 61 path, strerror(errno), errno);
54 return; 62 return;
55 } 63 }
56 if (base == path) 64 if (base == path)
57 p = fmt("%s", path); 65 p = fmt("%s", path);
58 else 66 else
59 p = fmt("%s", path + strlen(base) + 1); 67 p = fmt("%s", path + strlen(base) + 1);
60 68
61 if (!strcmp(p + strlen(p) - 5, "/.git")) 69 if (!strcmp(p + strlen(p) - 5, "/.git"))
62 p[strlen(p) - 5] = '\0'; 70 p[strlen(p) - 5] = '\0';
63 71
64 repo = cgit_add_repo(xstrdup(p)); 72 repo = cgit_add_repo(xstrdup(p));
65 repo->name = repo->url; 73 repo->name = repo->url;
66 repo->path = xstrdup(path); 74 repo->path = xstrdup(path);
67 p = (pwd && pwd->pw_gecos) ? strchr(pwd->pw_gecos, ',') : NULL; 75 p = (pwd && pwd->pw_gecos) ? strchr(pwd->pw_gecos, ',') : NULL;
68 if (p) 76 if (p)
69 *p = '\0'; 77 *p = '\0';
70 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) : "");
71 79
72 p = fmt("%s/description", path); 80 p = fmt("%s/description", path);
73 if (!stat(p, &st)) 81 if (!stat(p, &st))
74 readfile(p, &repo->desc, &size); 82 readfile(p, &repo->desc, &size);
75 83
76 p = fmt("%s/README.html", path); 84 p = fmt("%s/README.html", path);
77 if (!stat(p, &st)) 85 if (!stat(p, &st))
78 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 }
79} 93}
80 94
81static void scan_path(const char *base, const char *path) 95static void scan_path(const char *base, const char *path, repo_config_fn fn)
82{ 96{
83 DIR *dir; 97 DIR *dir;
84 struct dirent *ent; 98 struct dirent *ent;
85 char *buf; 99 char *buf;
86 struct stat st; 100 struct stat st;
87 101
88 if (is_git_dir(path)) { 102 if (is_git_dir(path)) {
89 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);
90 return; 108 return;
91 } 109 }
92 dir = opendir(path); 110 dir = opendir(path);
93 if (!dir) { 111 if (!dir) {
94 fprintf(stderr, "Error opening directory %s: %s (%d)\n", 112 fprintf(stderr, "Error opening directory %s: %s (%d)\n",
95 path, strerror(errno), errno); 113 path, strerror(errno), errno);
96 return; 114 return;
97 } 115 }
98 while((ent = readdir(dir)) != NULL) { 116 while((ent = readdir(dir)) != NULL) {
99 if (ent->d_name[0] == '.') { 117 if (ent->d_name[0] == '.') {
100 if (ent->d_name[1] == '\0') 118 if (ent->d_name[1] == '\0')
101 continue; 119 continue;
102 if (ent->d_name[1] == '.' && ent->d_name[2] == '\0') 120 if (ent->d_name[1] == '.' && ent->d_name[2] == '\0')
103 continue; 121 continue;
104 } 122 }
105 buf = malloc(strlen(path) + strlen(ent->d_name) + 2); 123 buf = malloc(strlen(path) + strlen(ent->d_name) + 2);
106 if (!buf) { 124 if (!buf) {
107 fprintf(stderr, "Alloc error on %s: %s (%d)\n", 125 fprintf(stderr, "Alloc error on %s: %s (%d)\n",
108 path, strerror(errno), errno); 126 path, strerror(errno), errno);
109 exit(1); 127 exit(1);
110 } 128 }
111 sprintf(buf, "%s/%s", path, ent->d_name); 129 sprintf(buf, "%s/%s", path, ent->d_name);
112 if (stat(buf, &st)) { 130 if (stat(buf, &st)) {
113 fprintf(stderr, "Error checking path %s: %s (%d)\n", 131 fprintf(stderr, "Error checking path %s: %s (%d)\n",
114 buf, strerror(errno), errno); 132 buf, strerror(errno), errno);
115 free(buf); 133 free(buf);
116 continue; 134 continue;
117 } 135 }
118 if (S_ISDIR(st.st_mode)) 136 if (S_ISDIR(st.st_mode))
119 scan_path(base, buf); 137 scan_path(base, buf, fn);
120 free(buf); 138 free(buf);
121 } 139 }
122 closedir(dir); 140 closedir(dir);
123} 141}
124 142
125void scan_tree(const char *path) 143void scan_tree(const char *path, repo_config_fn fn)
126{ 144{
127 scan_path(path, path); 145 scan_path(path, path, fn);
128} 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 4cb9573..d7b2d5a 100644
--- a/shared.c
+++ b/shared.c
@@ -1,152 +1,153 @@
1/* shared.c: global vars + some callback functions 1/* shared.c: global vars + some callback functions
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10 10
11struct cgit_repolist cgit_repolist; 11struct cgit_repolist cgit_repolist;
12struct cgit_context ctx; 12struct cgit_context ctx;
13int cgit_cmd; 13int cgit_cmd;
14 14
15int chk_zero(int result, char *msg) 15int chk_zero(int result, char *msg)
16{ 16{
17 if (result != 0) 17 if (result != 0)
18 die("%s: %s", msg, strerror(errno)); 18 die("%s: %s", msg, strerror(errno));
19 return result; 19 return result;
20} 20}
21 21
22int chk_positive(int result, char *msg) 22int chk_positive(int result, char *msg)
23{ 23{
24 if (result <= 0) 24 if (result <= 0)
25 die("%s: %s", msg, strerror(errno)); 25 die("%s: %s", msg, strerror(errno));
26 return result; 26 return result;
27} 27}
28 28
29int chk_non_negative(int result, char *msg) 29int chk_non_negative(int result, char *msg)
30{ 30{
31 if (result < 0) 31 if (result < 0)
32 die("%s: %s",msg, strerror(errno)); 32 die("%s: %s",msg, strerror(errno));
33 return result; 33 return result;
34} 34}
35 35
36struct cgit_repo *cgit_add_repo(const char *url) 36struct cgit_repo *cgit_add_repo(const char *url)
37{ 37{
38 struct cgit_repo *ret; 38 struct cgit_repo *ret;
39 39
40 if (++cgit_repolist.count > cgit_repolist.length) { 40 if (++cgit_repolist.count > cgit_repolist.length) {
41 if (cgit_repolist.length == 0) 41 if (cgit_repolist.length == 0)
42 cgit_repolist.length = 8; 42 cgit_repolist.length = 8;
43 else 43 else
44 cgit_repolist.length *= 2; 44 cgit_repolist.length *= 2;
45 cgit_repolist.repos = xrealloc(cgit_repolist.repos, 45 cgit_repolist.repos = xrealloc(cgit_repolist.repos,
46 cgit_repolist.length * 46 cgit_repolist.length *
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;
65 ret->about_filter = ctx.cfg.about_filter; 66 ret->about_filter = ctx.cfg.about_filter;
66 ret->commit_filter = ctx.cfg.commit_filter; 67 ret->commit_filter = ctx.cfg.commit_filter;
67 ret->source_filter = ctx.cfg.source_filter; 68 ret->source_filter = ctx.cfg.source_filter;
68 return ret; 69 return ret;
69} 70}
70 71
71struct cgit_repo *cgit_get_repoinfo(const char *url) 72struct cgit_repo *cgit_get_repoinfo(const char *url)
72{ 73{
73 int i; 74 int i;
74 struct cgit_repo *repo; 75 struct cgit_repo *repo;
75 76
76 for (i=0; i<cgit_repolist.count; i++) { 77 for (i=0; i<cgit_repolist.count; i++) {
77 repo = &cgit_repolist.repos[i]; 78 repo = &cgit_repolist.repos[i];
78 if (!strcmp(repo->url, url)) 79 if (!strcmp(repo->url, url))
79 return repo; 80 return repo;
80 } 81 }
81 return NULL; 82 return NULL;
82} 83}
83 84
84void *cgit_free_commitinfo(struct commitinfo *info) 85void *cgit_free_commitinfo(struct commitinfo *info)
85{ 86{
86 free(info->author); 87 free(info->author);
87 free(info->author_email); 88 free(info->author_email);
88 free(info->committer); 89 free(info->committer);
89 free(info->committer_email); 90 free(info->committer_email);
90 free(info->subject); 91 free(info->subject);
91 free(info->msg); 92 free(info->msg);
92 free(info->msg_encoding); 93 free(info->msg_encoding);
93 free(info); 94 free(info);
94 return NULL; 95 return NULL;
95} 96}
96 97
97char *trim_end(const char *str, char c) 98char *trim_end(const char *str, char c)
98{ 99{
99 int len; 100 int len;
100 char *s, *t; 101 char *s, *t;
101 102
102 if (str == NULL) 103 if (str == NULL)
103 return NULL; 104 return NULL;
104 t = (char *)str; 105 t = (char *)str;
105 len = strlen(t); 106 len = strlen(t);
106 while(len > 0 && t[len - 1] == c) 107 while(len > 0 && t[len - 1] == c)
107 len--; 108 len--;
108 109
109 if (len == 0) 110 if (len == 0)
110 return NULL; 111 return NULL;
111 112
112 c = t[len]; 113 c = t[len];
113 t[len] = '\0'; 114 t[len] = '\0';
114 s = xstrdup(t); 115 s = xstrdup(t);
115 t[len] = c; 116 t[len] = c;
116 return s; 117 return s;
117} 118}
118 119
119char *strlpart(char *txt, int maxlen) 120char *strlpart(char *txt, int maxlen)
120{ 121{
121 char *result; 122 char *result;
122 123
123 if (!txt) 124 if (!txt)
124 return txt; 125 return txt;
125 126
126 if (strlen(txt) <= maxlen) 127 if (strlen(txt) <= maxlen)
127 return txt; 128 return txt;
128 result = xmalloc(maxlen + 1); 129 result = xmalloc(maxlen + 1);
129 memcpy(result, txt, maxlen - 3); 130 memcpy(result, txt, maxlen - 3);
130 result[maxlen-1] = result[maxlen-2] = result[maxlen-3] = '.'; 131 result[maxlen-1] = result[maxlen-2] = result[maxlen-3] = '.';
131 result[maxlen] = '\0'; 132 result[maxlen] = '\0';
132 return result; 133 return result;
133} 134}
134 135
135char *strrpart(char *txt, int maxlen) 136char *strrpart(char *txt, int maxlen)
136{ 137{
137 char *result; 138 char *result;
138 139
139 if (!txt) 140 if (!txt)
140 return txt; 141 return txt;
141 142
142 if (strlen(txt) <= maxlen) 143 if (strlen(txt) <= maxlen)
143 return txt; 144 return txt;
144 result = xmalloc(maxlen + 1); 145 result = xmalloc(maxlen + 1);
145 memcpy(result + 3, txt + strlen(txt) - maxlen + 4, maxlen - 3); 146 memcpy(result + 3, txt + strlen(txt) - maxlen + 4, maxlen - 3);
146 result[0] = result[1] = result[2] = '.'; 147 result[0] = result[1] = result[2] = '.';
147 return result; 148 return result;
148} 149}
149 150
150void cgit_add_ref(struct reflist *list, struct refinfo *ref) 151void cgit_add_ref(struct reflist *list, struct refinfo *ref)
151{ 152{
152 size_t size; 153 size_t size;
diff --git a/ui-repolist.c b/ui-repolist.c
index 7c7aa9b..3ef2e99 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -43,243 +43,262 @@ static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
43 if (repo->mtime != -1) { 43 if (repo->mtime != -1) {
44 *mtime = repo->mtime; 44 *mtime = repo->mtime;
45 return 1; 45 return 1;
46 } 46 }
47 path = fmt("%s/%s", repo->path, ctx.cfg.agefile); 47 path = fmt("%s/%s", repo->path, ctx.cfg.agefile);
48 if (stat(path, &s) == 0) { 48 if (stat(path, &s) == 0) {
49 *mtime = read_agefile(path); 49 *mtime = read_agefile(path);
50 r->mtime = *mtime; 50 r->mtime = *mtime;
51 return 1; 51 return 1;
52 } 52 }
53 53
54 path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch); 54 path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch);
55 if (stat(path, &s) == 0) 55 if (stat(path, &s) == 0)
56 *mtime = s.st_mtime; 56 *mtime = s.st_mtime;
57 else 57 else
58 *mtime = 0; 58 *mtime = 0;
59 59
60 r->mtime = *mtime; 60 r->mtime = *mtime;
61 return (r->mtime != 0); 61 return (r->mtime != 0);
62} 62}
63 63
64static void print_modtime(struct cgit_repo *repo) 64static void print_modtime(struct cgit_repo *repo)
65{ 65{
66 time_t t; 66 time_t t;
67 if (get_repo_modtime(repo, &t)) 67 if (get_repo_modtime(repo, &t))
68 cgit_print_age(t, -1, NULL); 68 cgit_print_age(t, -1, NULL);
69} 69}
70 70
71int is_match(struct cgit_repo *repo) 71int is_match(struct cgit_repo *repo)
72{ 72{
73 if (!ctx.qry.search) 73 if (!ctx.qry.search)
74 return 1; 74 return 1;
75 if (repo->url && strcasestr(repo->url, ctx.qry.search)) 75 if (repo->url && strcasestr(repo->url, ctx.qry.search))
76 return 1; 76 return 1;
77 if (repo->name && strcasestr(repo->name, ctx.qry.search)) 77 if (repo->name && strcasestr(repo->name, ctx.qry.search))
78 return 1; 78 return 1;
79 if (repo->desc && strcasestr(repo->desc, ctx.qry.search)) 79 if (repo->desc && strcasestr(repo->desc, ctx.qry.search))
80 return 1; 80 return 1;
81 if (repo->owner && strcasestr(repo->owner, ctx.qry.search)) 81 if (repo->owner && strcasestr(repo->owner, ctx.qry.search))
82 return 1; 82 return 1;
83 return 0; 83 return 0;
84} 84}
85 85
86int is_in_url(struct cgit_repo *repo) 86int is_in_url(struct cgit_repo *repo)
87{ 87{
88 if (!ctx.qry.url) 88 if (!ctx.qry.url)
89 return 1; 89 return 1;
90 if (repo->url && !prefixcmp(repo->url, ctx.qry.url)) 90 if (repo->url && !prefixcmp(repo->url, ctx.qry.url))
91 return 1; 91 return 1;
92 return 0; 92 return 0;
93} 93}
94 94
95void print_sort_header(const char *title, const char *sort) 95void print_sort_header(const char *title, const char *sort)
96{ 96{
97 htmlf("<th class='left'><a href='./?s=%s", sort); 97 htmlf("<th class='left'><a href='./?s=%s", sort);
98 if (ctx.qry.search) { 98 if (ctx.qry.search) {
99 html("&q="); 99 html("&q=");
100 html_url_arg(ctx.qry.search); 100 html_url_arg(ctx.qry.search);
101 } 101 }
102 htmlf("'>%s</a></th>", title); 102 htmlf("'>%s</a></th>", title);
103} 103}
104 104
105void print_header(int columns) 105void print_header(int columns)
106{ 106{
107 html("<tr class='nohover'>"); 107 html("<tr class='nohover'>");
108 print_sort_header("Name", "name"); 108 print_sort_header("Name", "name");
109 print_sort_header("Description", "desc"); 109 print_sort_header("Description", "desc");
110 print_sort_header("Owner", "owner"); 110 print_sort_header("Owner", "owner");
111 print_sort_header("Idle", "idle"); 111 print_sort_header("Idle", "idle");
112 if (ctx.cfg.enable_index_links) 112 if (ctx.cfg.enable_index_links)
113 html("<th class='left'>Links</th>"); 113 html("<th class='left'>Links</th>");
114 html("</tr>\n"); 114 html("</tr>\n");
115} 115}
116 116
117 117
118void print_pager(int items, int pagelen, char *search) 118void print_pager(int items, int pagelen, char *search)
119{ 119{
120 int i; 120 int i;
121 html("<div class='pager'>"); 121 html("<div class='pager'>");
122 for(i = 0; i * pagelen < items; i++) 122 for(i = 0; i * pagelen < items; i++)
123 cgit_index_link(fmt("[%d]", i+1), fmt("Page %d", i+1), NULL, 123 cgit_index_link(fmt("[%d]", i+1), fmt("Page %d", i+1), NULL,
124 search, i * pagelen); 124 search, i * pagelen);
125 html("</div>"); 125 html("</div>");
126} 126}
127 127
128static int cmp(const char *s1, const char *s2) 128static int cmp(const char *s1, const char *s2)
129{ 129{
130 if (s1 && s2) 130 if (s1 && s2)
131 return strcmp(s1, s2); 131 return strcmp(s1, s2);
132 if (s1 && !s2) 132 if (s1 && !s2)
133 return -1; 133 return -1;
134 if (s2 && !s1) 134 if (s2 && !s1)
135 return 1; 135 return 1;
136 return 0; 136 return 0;
137} 137}
138 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
139static int sort_name(const void *a, const void *b) 151static int sort_name(const void *a, const void *b)
140{ 152{
141 const struct cgit_repo *r1 = a; 153 const struct cgit_repo *r1 = a;
142 const struct cgit_repo *r2 = b; 154 const struct cgit_repo *r2 = b;
143 155
144 return cmp(r1->name, r2->name); 156 return cmp(r1->name, r2->name);
145} 157}
146 158
147static int sort_desc(const void *a, const void *b) 159static int sort_desc(const void *a, const void *b)
148{ 160{
149 const struct cgit_repo *r1 = a; 161 const struct cgit_repo *r1 = a;
150 const struct cgit_repo *r2 = b; 162 const struct cgit_repo *r2 = b;
151 163
152 return cmp(r1->desc, r2->desc); 164 return cmp(r1->desc, r2->desc);
153} 165}
154 166
155static int sort_owner(const void *a, const void *b) 167static int sort_owner(const void *a, const void *b)
156{ 168{
157 const struct cgit_repo *r1 = a; 169 const struct cgit_repo *r1 = a;
158 const struct cgit_repo *r2 = b; 170 const struct cgit_repo *r2 = b;
159 171
160 return cmp(r1->owner, r2->owner); 172 return cmp(r1->owner, r2->owner);
161} 173}
162 174
163static int sort_idle(const void *a, const void *b) 175static int sort_idle(const void *a, const void *b)
164{ 176{
165 const struct cgit_repo *r1 = a; 177 const struct cgit_repo *r1 = a;
166 const struct cgit_repo *r2 = b; 178 const struct cgit_repo *r2 = b;
167 time_t t1, t2; 179 time_t t1, t2;
168 180
169 t1 = t2 = 0; 181 t1 = t2 = 0;
170 get_repo_modtime(r1, &t1); 182 get_repo_modtime(r1, &t1);
171 get_repo_modtime(r2, &t2); 183 get_repo_modtime(r2, &t2);
172 return t2 - t1; 184 return t2 - t1;
173} 185}
174 186
175struct sortcolumn { 187struct sortcolumn {
176 const char *name; 188 const char *name;
177 int (*fn)(const void *a, const void *b); 189 int (*fn)(const void *a, const void *b);
178}; 190};
179 191
180struct sortcolumn sortcolumn[] = { 192struct sortcolumn sortcolumn[] = {
193 {"section", sort_section},
181 {"name", sort_name}, 194 {"name", sort_name},
182 {"desc", sort_desc}, 195 {"desc", sort_desc},
183 {"owner", sort_owner}, 196 {"owner", sort_owner},
184 {"idle", sort_idle}, 197 {"idle", sort_idle},
185 {NULL, NULL} 198 {NULL, NULL}
186}; 199};
187 200
188int sort_repolist(char *field) 201int sort_repolist(char *field)
189{ 202{
190 struct sortcolumn *column; 203 struct sortcolumn *column;
191 204
192 for (column = &sortcolumn[0]; column->name; column++) { 205 for (column = &sortcolumn[0]; column->name; column++) {
193 if (strcmp(field, column->name)) 206 if (strcmp(field, column->name))
194 continue; 207 continue;
195 qsort(cgit_repolist.repos, cgit_repolist.count, 208 qsort(cgit_repolist.repos, cgit_repolist.count,
196 sizeof(struct cgit_repo), column->fn); 209 sizeof(struct cgit_repo), column->fn);
197 return 1; 210 return 1;
198 } 211 }
199 return 0; 212 return 0;
200} 213}
201 214
202 215
203void cgit_print_repolist() 216void cgit_print_repolist()
204{ 217{
205 int i, columns = 4, hits = 0, header = 0; 218 int i, columns = 4, hits = 0, header = 0;
206 char *last_group = NULL; 219 char *last_section = NULL;
220 char *section;
207 int sorted = 0; 221 int sorted = 0;
208 222
209 if (ctx.cfg.enable_index_links) 223 if (ctx.cfg.enable_index_links)
210 columns++; 224 columns++;
211 225
212 ctx.page.title = ctx.cfg.root_title; 226 ctx.page.title = ctx.cfg.root_title;
213 cgit_print_http_headers(&ctx); 227 cgit_print_http_headers(&ctx);
214 cgit_print_docstart(&ctx); 228 cgit_print_docstart(&ctx);
215 cgit_print_pageheader(&ctx); 229 cgit_print_pageheader(&ctx);
216 230
217 if (ctx.cfg.index_header) 231 if (ctx.cfg.index_header)
218 html_include(ctx.cfg.index_header); 232 html_include(ctx.cfg.index_header);
219 233
220 if(ctx.qry.sort) 234 if(ctx.qry.sort)
221 sorted = sort_repolist(ctx.qry.sort); 235 sorted = sort_repolist(ctx.qry.sort);
236 else
237 sort_repolist("section");
222 238
223 html("<table summary='repository list' class='list nowrap'>"); 239 html("<table summary='repository list' class='list nowrap'>");
224 for (i=0; i<cgit_repolist.count; i++) { 240 for (i=0; i<cgit_repolist.count; i++) {
225 ctx.repo = &cgit_repolist.repos[i]; 241 ctx.repo = &cgit_repolist.repos[i];
226 if (!(is_match(ctx.repo) && is_in_url(ctx.repo))) 242 if (!(is_match(ctx.repo) && is_in_url(ctx.repo)))
227 continue; 243 continue;
228 hits++; 244 hits++;
229 if (hits <= ctx.qry.ofs) 245 if (hits <= ctx.qry.ofs)
230 continue; 246 continue;
231 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count) 247 if (hits > ctx.qry.ofs + ctx.cfg.max_repo_count)
232 continue; 248 continue;
233 if (!header++) 249 if (!header++)
234 print_header(columns); 250 print_header(columns);
251 section = ctx.repo->section;
252 if (section && !strcmp(section, ""))
253 section = NULL;
235 if (!sorted && 254 if (!sorted &&
236 ((last_group == NULL && ctx.repo->group != NULL) || 255 ((last_section == NULL && section != NULL) ||
237 (last_group != NULL && ctx.repo->group == NULL) || 256 (last_section != NULL && section == NULL) ||
238 (last_group != NULL && ctx.repo->group != NULL && 257 (last_section != NULL && section != NULL &&
239 strcmp(ctx.repo->group, last_group)))) { 258 strcmp(section, last_section)))) {
240 htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>", 259 htmlf("<tr class='nohover'><td colspan='%d' class='reposection'>",
241 columns); 260 columns);
242 html_txt(ctx.repo->group); 261 html_txt(section);
243 html("</td></tr>"); 262 html("</td></tr>");
244 last_group = ctx.repo->group; 263 last_section = section;
245 } 264 }
246 htmlf("<tr><td class='%s'>", 265 htmlf("<tr><td class='%s'>",
247 !sorted && ctx.repo->group ? "sublevel-repo" : "toplevel-repo"); 266 !sorted && section ? "sublevel-repo" : "toplevel-repo");
248 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL); 267 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
249 html("</td><td>"); 268 html("</td><td>");
250 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL); 269 html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL);
251 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc); 270 html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc);
252 html_link_close(); 271 html_link_close();
253 html("</td><td>"); 272 html("</td><td>");
254 html_txt(ctx.repo->owner); 273 html_txt(ctx.repo->owner);
255 html("</td><td>"); 274 html("</td><td>");
256 print_modtime(ctx.repo); 275 print_modtime(ctx.repo);
257 html("</td>"); 276 html("</td>");
258 if (ctx.cfg.enable_index_links) { 277 if (ctx.cfg.enable_index_links) {
259 html("<td>"); 278 html("<td>");
260 cgit_summary_link("summary", NULL, "button", NULL); 279 cgit_summary_link("summary", NULL, "button", NULL);
261 cgit_log_link("log", NULL, "button", NULL, NULL, NULL, 280 cgit_log_link("log", NULL, "button", NULL, NULL, NULL,
262 0, NULL, NULL, ctx.qry.showmsg); 281 0, NULL, NULL, ctx.qry.showmsg);
263 cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL); 282 cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
264 html("</td>"); 283 html("</td>");
265 } 284 }
266 html("</tr>\n"); 285 html("</tr>\n");
267 } 286 }
268 html("</table>"); 287 html("</table>");
269 if (!hits) 288 if (!hits)
270 cgit_print_error("No repositories found"); 289 cgit_print_error("No repositories found");
271 else if (hits > ctx.cfg.max_repo_count) 290 else if (hits > ctx.cfg.max_repo_count)
272 print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search); 291 print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search);
273 cgit_print_docend(); 292 cgit_print_docend();
274} 293}
275 294
276void cgit_print_site_readme() 295void cgit_print_site_readme()
277{ 296{
278 if (!ctx.cfg.root_readme) 297 if (!ctx.cfg.root_readme)
279 return; 298 return;
280 if (ctx.cfg.about_filter) 299 if (ctx.cfg.about_filter)
281 cgit_open_filter(ctx.cfg.about_filter); 300 cgit_open_filter(ctx.cfg.about_filter);
282 html_include(ctx.cfg.root_readme); 301 html_include(ctx.cfg.root_readme);
283 if (ctx.cfg.about_filter) 302 if (ctx.cfg.about_filter)
284 cgit_close_filter(ctx.cfg.about_filter); 303 cgit_close_filter(ctx.cfg.about_filter);
285} 304}
diff --git a/ui-stats.c b/ui-stats.c
index 9fc06d3..bdaf9cc 100644
--- a/ui-stats.c
+++ b/ui-stats.c
@@ -61,192 +61,200 @@ static void dec_month(struct tm *tm)
61static void inc_month(struct tm *tm) 61static void inc_month(struct tm *tm)
62{ 62{
63 tm->tm_mon++; 63 tm->tm_mon++;
64 if (tm->tm_mon > 11) { 64 if (tm->tm_mon > 11) {
65 tm->tm_year++; 65 tm->tm_year++;
66 tm->tm_mon = 0; 66 tm->tm_mon = 0;
67 } 67 }
68} 68}
69 69
70static char *pretty_month(struct tm *tm) 70static char *pretty_month(struct tm *tm)
71{ 71{
72 static const char *months[] = { 72 static const char *months[] = {
73 "Jan", "Feb", "Mar", "Apr", "May", "Jun", 73 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
74 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 74 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
75 }; 75 };
76 return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900); 76 return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
77} 77}
78 78
79static void trunc_quarter(struct tm *tm) 79static void trunc_quarter(struct tm *tm)
80{ 80{
81 trunc_month(tm); 81 trunc_month(tm);
82 while(tm->tm_mon % 3 != 0) 82 while(tm->tm_mon % 3 != 0)
83 dec_month(tm); 83 dec_month(tm);
84} 84}
85 85
86static void dec_quarter(struct tm *tm) 86static void dec_quarter(struct tm *tm)
87{ 87{
88 dec_month(tm); 88 dec_month(tm);
89 dec_month(tm); 89 dec_month(tm);
90 dec_month(tm); 90 dec_month(tm);
91} 91}
92 92
93static void inc_quarter(struct tm *tm) 93static void inc_quarter(struct tm *tm)
94{ 94{
95 inc_month(tm); 95 inc_month(tm);
96 inc_month(tm); 96 inc_month(tm);
97 inc_month(tm); 97 inc_month(tm);
98} 98}
99 99
100static char *pretty_quarter(struct tm *tm) 100static char *pretty_quarter(struct tm *tm)
101{ 101{
102 return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900); 102 return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
103} 103}
104 104
105static void trunc_year(struct tm *tm) 105static void trunc_year(struct tm *tm)
106{ 106{
107 trunc_month(tm); 107 trunc_month(tm);
108 tm->tm_mon = 0; 108 tm->tm_mon = 0;
109} 109}
110 110
111static void dec_year(struct tm *tm) 111static void dec_year(struct tm *tm)
112{ 112{
113 tm->tm_year--; 113 tm->tm_year--;
114} 114}
115 115
116static void inc_year(struct tm *tm) 116static void inc_year(struct tm *tm)
117{ 117{
118 tm->tm_year++; 118 tm->tm_year++;
119} 119}
120 120
121static char *pretty_year(struct tm *tm) 121static char *pretty_year(struct tm *tm)
122{ 122{
123 return fmt("%d", tm->tm_year + 1900); 123 return fmt("%d", tm->tm_year + 1900);
124} 124}
125 125
126struct cgit_period periods[] = { 126struct cgit_period periods[] = {
127 {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week}, 127 {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
128 {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month}, 128 {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
129 {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter}, 129 {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
130 {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year}, 130 {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
131}; 131};
132 132
133/* Given a period code or name, return a period index (1, 2, 3 or 4) 133/* Given a period code or name, return a period index (1, 2, 3 or 4)
134 * and update the period pointer to the correcsponding struct. 134 * and update the period pointer to the correcsponding struct.
135 * If no matching code is found, return 0. 135 * If no matching code is found, return 0.
136 */ 136 */
137int cgit_find_stats_period(const char *expr, struct cgit_period **period) 137int cgit_find_stats_period(const char *expr, struct cgit_period **period)
138{ 138{
139 int i; 139 int i;
140 char code = '\0'; 140 char code = '\0';
141 141
142 if (!expr) 142 if (!expr)
143 return 0; 143 return 0;
144 144
145 if (strlen(expr) == 1) 145 if (strlen(expr) == 1)
146 code = expr[0]; 146 code = expr[0];
147 147
148 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++) 148 for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
149 if (periods[i].code == code || !strcmp(periods[i].name, expr)) { 149 if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
150 if (period) 150 if (period)
151 *period = &periods[i]; 151 *period = &periods[i];
152 return i+1; 152 return i+1;
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;
161 struct string_list_item *author, *item; 169 struct string_list_item *author, *item;
162 struct authorstat *authorstat; 170 struct authorstat *authorstat;
163 struct string_list *items; 171 struct string_list *items;
164 char *tmp; 172 char *tmp;
165 struct tm *date; 173 struct tm *date;
166 time_t t; 174 time_t t;
167 175
168 info = cgit_parse_commit(commit); 176 info = cgit_parse_commit(commit);
169 tmp = xstrdup(info->author); 177 tmp = xstrdup(info->author);
170 author = string_list_insert(tmp, authors); 178 author = string_list_insert(tmp, authors);
171 if (!author->util) 179 if (!author->util)
172 author->util = xcalloc(1, sizeof(struct authorstat)); 180 author->util = xcalloc(1, sizeof(struct authorstat));
173 else 181 else
174 free(tmp); 182 free(tmp);
175 authorstat = author->util; 183 authorstat = author->util;
176 items = &authorstat->list; 184 items = &authorstat->list;
177 t = info->committer_date; 185 t = info->committer_date;
178 date = gmtime(&t); 186 date = gmtime(&t);
179 period->trunc(date); 187 period->trunc(date);
180 tmp = xstrdup(period->pretty(date)); 188 tmp = xstrdup(period->pretty(date));
181 item = string_list_insert(tmp, items); 189 item = string_list_insert(tmp, items);
182 if (item->util) 190 if (item->util)
183 free(tmp); 191 free(tmp);
184 item->util++; 192 item->util++;
185 authorstat->total++; 193 authorstat->total++;
186 cgit_free_commitinfo(info); 194 cgit_free_commitinfo(info);
187} 195}
188 196
189static int cmp_total_commits(const void *a1, const void *a2) 197static int cmp_total_commits(const void *a1, const void *a2)
190{ 198{
191 const struct string_list_item *i1 = a1; 199 const struct string_list_item *i1 = a1;
192 const struct string_list_item *i2 = a2; 200 const struct string_list_item *i2 = a2;
193 const struct authorstat *auth1 = i1->util; 201 const struct authorstat *auth1 = i1->util;
194 const struct authorstat *auth2 = i2->util; 202 const struct authorstat *auth2 = i2->util;
195 203
196 return auth2->total - auth1->total; 204 return auth2->total - auth1->total;
197} 205}
198 206
199/* Walk the commit DAG and collect number of commits per author per 207/* Walk the commit DAG and collect number of commits per author per
200 * timeperiod into a nested string_list collection. 208 * timeperiod into a nested string_list collection.
201 */ 209 */
202struct string_list collect_stats(struct cgit_context *ctx, 210struct string_list collect_stats(struct cgit_context *ctx,
203 struct cgit_period *period) 211 struct cgit_period *period)
204{ 212{
205 struct string_list authors; 213 struct string_list authors;
206 struct rev_info rev; 214 struct rev_info rev;
207 struct commit *commit; 215 struct commit *commit;
208 const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL}; 216 const char *argv[] = {NULL, ctx->qry.head, NULL, NULL, NULL, NULL};
209 int argc = 3; 217 int argc = 3;
210 time_t now; 218 time_t now;
211 long i; 219 long i;
212 struct tm *tm; 220 struct tm *tm;
213 char tmp[11]; 221 char tmp[11];
214 222
215 time(&now); 223 time(&now);
216 tm = gmtime(&now); 224 tm = gmtime(&now);
217 period->trunc(tm); 225 period->trunc(tm);
218 for (i = 1; i < period->count; i++) 226 for (i = 1; i < period->count; i++)
219 period->dec(tm); 227 period->dec(tm);
220 strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm); 228 strftime(tmp, sizeof(tmp), "%Y-%m-%d", tm);
221 argv[2] = xstrdup(fmt("--since=%s", tmp)); 229 argv[2] = xstrdup(fmt("--since=%s", tmp));
222 if (ctx->qry.path) { 230 if (ctx->qry.path) {
223 argv[3] = "--"; 231 argv[3] = "--";
224 argv[4] = ctx->qry.path; 232 argv[4] = ctx->qry.path;
225 argc += 2; 233 argc += 2;
226 } 234 }
227 init_revisions(&rev, NULL); 235 init_revisions(&rev, NULL);
228 rev.abbrev = DEFAULT_ABBREV; 236 rev.abbrev = DEFAULT_ABBREV;
229 rev.commit_format = CMIT_FMT_DEFAULT; 237 rev.commit_format = CMIT_FMT_DEFAULT;
230 rev.no_merges = 1; 238 rev.no_merges = 1;
231 rev.verbose_header = 1; 239 rev.verbose_header = 1;
232 rev.show_root_diff = 0; 240 rev.show_root_diff = 0;
233 setup_revisions(argc, argv, &rev, NULL); 241 setup_revisions(argc, argv, &rev, NULL);
234 prepare_revision_walk(&rev); 242 prepare_revision_walk(&rev);
235 memset(&authors, 0, sizeof(authors)); 243 memset(&authors, 0, sizeof(authors));
236 while ((commit = get_revision(&rev)) != NULL) { 244 while ((commit = get_revision(&rev)) != NULL) {
237 add_commit(&authors, commit, period); 245 add_commit(&authors, commit, period);
238 free(commit->buffer); 246 free(commit->buffer);
239 free_commit_list(commit->parents); 247 free_commit_list(commit->parents);
240 } 248 }
241 return authors; 249 return authors;
242} 250}
243 251
244void print_combined_authorrow(struct string_list *authors, int from, int to, 252void print_combined_authorrow(struct string_list *authors, int from, int to,
245 const char *name, const char *leftclass, const char *centerclass, 253 const char *name, const char *leftclass, const char *centerclass,
246 const char *rightclass, struct cgit_period *period) 254 const char *rightclass, struct cgit_period *period)
247{ 255{
248 struct string_list_item *author; 256 struct string_list_item *author;
249 struct authorstat *authorstat; 257 struct authorstat *authorstat;
250 struct string_list *items; 258 struct string_list *items;
251 struct string_list_item *date; 259 struct string_list_item *date;
252 time_t now; 260 time_t now;
diff --git a/ui-stats.h b/ui-stats.h
index 4f13dba..f0761ba 100644
--- a/ui-stats.h
+++ b/ui-stats.h
@@ -1,27 +1,28 @@
1#ifndef UI_STATS_H 1#ifndef UI_STATS_H
2#define UI_STATS_H 2#define UI_STATS_H
3 3
4#include "cgit.h" 4#include "cgit.h"
5 5
6struct cgit_period { 6struct cgit_period {
7 const char code; 7 const char code;
8 const char *name; 8 const char *name;
9 int max_periods; 9 int max_periods;
10 int count; 10 int count;
11 11
12 /* Convert a tm value to the first day in the period */ 12 /* Convert a tm value to the first day in the period */
13 void (*trunc)(struct tm *tm); 13 void (*trunc)(struct tm *tm);
14 14
15 /* Update tm value to start of next/previous period */ 15 /* Update tm value to start of next/previous period */
16 void (*dec)(struct tm *tm); 16 void (*dec)(struct tm *tm);
17 void (*inc)(struct tm *tm); 17 void (*inc)(struct tm *tm);
18 18
19 /* Pretty-print a tm value */ 19 /* Pretty-print a tm value */
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 */