summaryrefslogtreecommitdiffabout
authorJason A. Donenfeld <Jason@zx2c4.com>2010-07-29 15:52:29 (UTC)
committer Lars Hjemli <hjemli@gmail.com>2010-08-04 01:09:32 (UTC)
commit3516502aa0df95ecc241caa30161741f59e4e600 (patch) (unidiff)
tree345978808d18796444729f74eb139c8289bcb1c2
parentf9143171267e39f66a02ad2ecb0da2e8cc7f4142 (diff)
downloadcgit-3516502aa0df95ecc241caa30161741f59e4e600.zip
cgit-3516502aa0df95ecc241caa30161741f59e4e600.tar.gz
cgit-3516502aa0df95ecc241caa30161741f59e4e600.tar.bz2
Add support for 'project-list' option
This option specifies the location of a projectlist file as used by gitweb - when 'scan-tree' is later specified, only the projects listed in the projectlist file will be added. Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com> Signed-off-by: Lars Hjemli <hjemli@gmail.com>
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--cgit.c27
-rw-r--r--cgit.h1
-rw-r--r--cgitrc.5.txt10
-rw-r--r--scan-tree.c37
-rw-r--r--scan-tree.h3
5 files changed, 70 insertions, 8 deletions
diff --git a/cgit.c b/cgit.c
index c263872..2364d1c 100644
--- a/cgit.c
+++ b/cgit.c
@@ -1,751 +1,768 @@
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); 43static void process_cached_repolist(const char *path);
44 44
45void repo_config(struct cgit_repo *repo, const char *name, const char *value) 45void repo_config(struct cgit_repo *repo, const char *name, const char *value)
46{ 46{
47 if (!strcmp(name, "name")) 47 if (!strcmp(name, "name"))
48 repo->name = xstrdup(value); 48 repo->name = xstrdup(value);
49 else if (!strcmp(name, "clone-url")) 49 else if (!strcmp(name, "clone-url"))
50 repo->clone_url = xstrdup(value); 50 repo->clone_url = xstrdup(value);
51 else if (!strcmp(name, "desc")) 51 else if (!strcmp(name, "desc"))
52 repo->desc = xstrdup(value); 52 repo->desc = xstrdup(value);
53 else if (!strcmp(name, "owner")) 53 else if (!strcmp(name, "owner"))
54 repo->owner = xstrdup(value); 54 repo->owner = xstrdup(value);
55 else if (!strcmp(name, "defbranch")) 55 else if (!strcmp(name, "defbranch"))
56 repo->defbranch = xstrdup(value); 56 repo->defbranch = xstrdup(value);
57 else if (!strcmp(name, "snapshots")) 57 else if (!strcmp(name, "snapshots"))
58 repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); 58 repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value);
59 else if (!strcmp(name, "enable-log-filecount")) 59 else if (!strcmp(name, "enable-log-filecount"))
60 repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); 60 repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
61 else if (!strcmp(name, "enable-log-linecount")) 61 else if (!strcmp(name, "enable-log-linecount"))
62 repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); 62 repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
63 else if (!strcmp(name, "enable-remote-branches")) 63 else if (!strcmp(name, "enable-remote-branches"))
64 repo->enable_remote_branches = atoi(value); 64 repo->enable_remote_branches = atoi(value);
65 else if (!strcmp(name, "enable-subject-links")) 65 else if (!strcmp(name, "enable-subject-links"))
66 repo->enable_subject_links = atoi(value); 66 repo->enable_subject_links = atoi(value);
67 else if (!strcmp(name, "max-stats")) 67 else if (!strcmp(name, "max-stats"))
68 repo->max_stats = cgit_find_stats_period(value, NULL); 68 repo->max_stats = cgit_find_stats_period(value, NULL);
69 else if (!strcmp(name, "module-link")) 69 else if (!strcmp(name, "module-link"))
70 repo->module_link= xstrdup(value); 70 repo->module_link= xstrdup(value);
71 else if (!strcmp(name, "section")) 71 else if (!strcmp(name, "section"))
72 repo->section = xstrdup(value); 72 repo->section = xstrdup(value);
73 else if (!strcmp(name, "readme") && value != NULL) { 73 else if (!strcmp(name, "readme") && value != NULL) {
74 if (*value == '/') 74 if (*value == '/')
75 repo->readme = xstrdup(value); 75 repo->readme = xstrdup(value);
76 else 76 else
77 repo->readme = xstrdup(fmt("%s/%s", repo->path, value)); 77 repo->readme = xstrdup(fmt("%s/%s", repo->path, value));
78 } else if (ctx.cfg.enable_filter_overrides) { 78 } else if (ctx.cfg.enable_filter_overrides) {
79 if (!strcmp(name, "about-filter")) 79 if (!strcmp(name, "about-filter"))
80 repo->about_filter = new_filter(value, 0); 80 repo->about_filter = new_filter(value, 0);
81 else if (!strcmp(name, "commit-filter")) 81 else if (!strcmp(name, "commit-filter"))
82 repo->commit_filter = new_filter(value, 0); 82 repo->commit_filter = new_filter(value, 0);
83 else if (!strcmp(name, "source-filter")) 83 else if (!strcmp(name, "source-filter"))
84 repo->source_filter = new_filter(value, 1); 84 repo->source_filter = new_filter(value, 1);
85 } 85 }
86} 86}
87 87
88void config_cb(const char *name, const char *value) 88void config_cb(const char *name, const char *value)
89{ 89{
90 if (!strcmp(name, "section") || !strcmp(name, "repo.group")) 90 if (!strcmp(name, "section") || !strcmp(name, "repo.group"))
91 ctx.cfg.section = xstrdup(value); 91 ctx.cfg.section = xstrdup(value);
92 else if (!strcmp(name, "repo.url")) 92 else if (!strcmp(name, "repo.url"))
93 ctx.repo = cgit_add_repo(value); 93 ctx.repo = cgit_add_repo(value);
94 else if (ctx.repo && !strcmp(name, "repo.path")) 94 else if (ctx.repo && !strcmp(name, "repo.path"))
95 ctx.repo->path = trim_end(value, '/'); 95 ctx.repo->path = trim_end(value, '/');
96 else if (ctx.repo && !prefixcmp(name, "repo.")) 96 else if (ctx.repo && !prefixcmp(name, "repo."))
97 repo_config(ctx.repo, name + 5, value); 97 repo_config(ctx.repo, name + 5, value);
98 else if (!strcmp(name, "root-title")) 98 else if (!strcmp(name, "root-title"))
99 ctx.cfg.root_title = xstrdup(value); 99 ctx.cfg.root_title = xstrdup(value);
100 else if (!strcmp(name, "root-desc")) 100 else if (!strcmp(name, "root-desc"))
101 ctx.cfg.root_desc = xstrdup(value); 101 ctx.cfg.root_desc = xstrdup(value);
102 else if (!strcmp(name, "root-readme")) 102 else if (!strcmp(name, "root-readme"))
103 ctx.cfg.root_readme = xstrdup(value); 103 ctx.cfg.root_readme = xstrdup(value);
104 else if (!strcmp(name, "css")) 104 else if (!strcmp(name, "css"))
105 ctx.cfg.css = xstrdup(value); 105 ctx.cfg.css = xstrdup(value);
106 else if (!strcmp(name, "favicon")) 106 else if (!strcmp(name, "favicon"))
107 ctx.cfg.favicon = xstrdup(value); 107 ctx.cfg.favicon = xstrdup(value);
108 else if (!strcmp(name, "footer")) 108 else if (!strcmp(name, "footer"))
109 ctx.cfg.footer = xstrdup(value); 109 ctx.cfg.footer = xstrdup(value);
110 else if (!strcmp(name, "head-include")) 110 else if (!strcmp(name, "head-include"))
111 ctx.cfg.head_include = xstrdup(value); 111 ctx.cfg.head_include = xstrdup(value);
112 else if (!strcmp(name, "header")) 112 else if (!strcmp(name, "header"))
113 ctx.cfg.header = xstrdup(value); 113 ctx.cfg.header = xstrdup(value);
114 else if (!strcmp(name, "logo")) 114 else if (!strcmp(name, "logo"))
115 ctx.cfg.logo = xstrdup(value); 115 ctx.cfg.logo = xstrdup(value);
116 else if (!strcmp(name, "index-header")) 116 else if (!strcmp(name, "index-header"))
117 ctx.cfg.index_header = xstrdup(value); 117 ctx.cfg.index_header = xstrdup(value);
118 else if (!strcmp(name, "index-info")) 118 else if (!strcmp(name, "index-info"))
119 ctx.cfg.index_info = xstrdup(value); 119 ctx.cfg.index_info = xstrdup(value);
120 else if (!strcmp(name, "logo-link")) 120 else if (!strcmp(name, "logo-link"))
121 ctx.cfg.logo_link = xstrdup(value); 121 ctx.cfg.logo_link = xstrdup(value);
122 else if (!strcmp(name, "module-link")) 122 else if (!strcmp(name, "module-link"))
123 ctx.cfg.module_link = xstrdup(value); 123 ctx.cfg.module_link = xstrdup(value);
124 else if (!strcmp(name, "virtual-root")) { 124 else if (!strcmp(name, "virtual-root")) {
125 ctx.cfg.virtual_root = trim_end(value, '/'); 125 ctx.cfg.virtual_root = trim_end(value, '/');
126 if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 126 if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
127 ctx.cfg.virtual_root = ""; 127 ctx.cfg.virtual_root = "";
128 } else if (!strcmp(name, "nocache")) 128 } else if (!strcmp(name, "nocache"))
129 ctx.cfg.nocache = atoi(value); 129 ctx.cfg.nocache = atoi(value);
130 else if (!strcmp(name, "noplainemail")) 130 else if (!strcmp(name, "noplainemail"))
131 ctx.cfg.noplainemail = atoi(value); 131 ctx.cfg.noplainemail = atoi(value);
132 else if (!strcmp(name, "noheader")) 132 else if (!strcmp(name, "noheader"))
133 ctx.cfg.noheader = atoi(value); 133 ctx.cfg.noheader = atoi(value);
134 else if (!strcmp(name, "snapshots")) 134 else if (!strcmp(name, "snapshots"))
135 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 135 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
136 else if (!strcmp(name, "enable-filter-overrides")) 136 else if (!strcmp(name, "enable-filter-overrides"))
137 ctx.cfg.enable_filter_overrides = atoi(value); 137 ctx.cfg.enable_filter_overrides = atoi(value);
138 else if (!strcmp(name, "enable-index-links")) 138 else if (!strcmp(name, "enable-index-links"))
139 ctx.cfg.enable_index_links = atoi(value); 139 ctx.cfg.enable_index_links = atoi(value);
140 else if (!strcmp(name, "enable-log-filecount")) 140 else if (!strcmp(name, "enable-log-filecount"))
141 ctx.cfg.enable_log_filecount = atoi(value); 141 ctx.cfg.enable_log_filecount = atoi(value);
142 else if (!strcmp(name, "enable-log-linecount")) 142 else if (!strcmp(name, "enable-log-linecount"))
143 ctx.cfg.enable_log_linecount = atoi(value); 143 ctx.cfg.enable_log_linecount = atoi(value);
144 else if (!strcmp(name, "enable-remote-branches")) 144 else if (!strcmp(name, "enable-remote-branches"))
145 ctx.cfg.enable_remote_branches = atoi(value); 145 ctx.cfg.enable_remote_branches = atoi(value);
146 else if (!strcmp(name, "enable-subject-links")) 146 else if (!strcmp(name, "enable-subject-links"))
147 ctx.cfg.enable_subject_links = atoi(value); 147 ctx.cfg.enable_subject_links = atoi(value);
148 else if (!strcmp(name, "enable-tree-linenumbers")) 148 else if (!strcmp(name, "enable-tree-linenumbers"))
149 ctx.cfg.enable_tree_linenumbers = atoi(value); 149 ctx.cfg.enable_tree_linenumbers = atoi(value);
150 else if (!strcmp(name, "max-stats")) 150 else if (!strcmp(name, "max-stats"))
151 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); 151 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
152 else if (!strcmp(name, "cache-size")) 152 else if (!strcmp(name, "cache-size"))
153 ctx.cfg.cache_size = atoi(value); 153 ctx.cfg.cache_size = atoi(value);
154 else if (!strcmp(name, "cache-root")) 154 else if (!strcmp(name, "cache-root"))
155 ctx.cfg.cache_root = xstrdup(expand_macros(value)); 155 ctx.cfg.cache_root = xstrdup(expand_macros(value));
156 else if (!strcmp(name, "cache-root-ttl")) 156 else if (!strcmp(name, "cache-root-ttl"))
157 ctx.cfg.cache_root_ttl = atoi(value); 157 ctx.cfg.cache_root_ttl = atoi(value);
158 else if (!strcmp(name, "cache-repo-ttl")) 158 else if (!strcmp(name, "cache-repo-ttl"))
159 ctx.cfg.cache_repo_ttl = atoi(value); 159 ctx.cfg.cache_repo_ttl = atoi(value);
160 else if (!strcmp(name, "cache-scanrc-ttl")) 160 else if (!strcmp(name, "cache-scanrc-ttl"))
161 ctx.cfg.cache_scanrc_ttl = atoi(value); 161 ctx.cfg.cache_scanrc_ttl = atoi(value);
162 else if (!strcmp(name, "cache-static-ttl")) 162 else if (!strcmp(name, "cache-static-ttl"))
163 ctx.cfg.cache_static_ttl = atoi(value); 163 ctx.cfg.cache_static_ttl = atoi(value);
164 else if (!strcmp(name, "cache-dynamic-ttl")) 164 else if (!strcmp(name, "cache-dynamic-ttl"))
165 ctx.cfg.cache_dynamic_ttl = atoi(value); 165 ctx.cfg.cache_dynamic_ttl = atoi(value);
166 else if (!strcmp(name, "about-filter")) 166 else if (!strcmp(name, "about-filter"))
167 ctx.cfg.about_filter = new_filter(value, 0); 167 ctx.cfg.about_filter = new_filter(value, 0);
168 else if (!strcmp(name, "commit-filter")) 168 else if (!strcmp(name, "commit-filter"))
169 ctx.cfg.commit_filter = new_filter(value, 0); 169 ctx.cfg.commit_filter = new_filter(value, 0);
170 else if (!strcmp(name, "embedded")) 170 else if (!strcmp(name, "embedded"))
171 ctx.cfg.embedded = atoi(value); 171 ctx.cfg.embedded = atoi(value);
172 else if (!strcmp(name, "max-atom-items")) 172 else if (!strcmp(name, "max-atom-items"))
173 ctx.cfg.max_atom_items = atoi(value); 173 ctx.cfg.max_atom_items = atoi(value);
174 else if (!strcmp(name, "max-message-length")) 174 else if (!strcmp(name, "max-message-length"))
175 ctx.cfg.max_msg_len = atoi(value); 175 ctx.cfg.max_msg_len = atoi(value);
176 else if (!strcmp(name, "max-repodesc-length")) 176 else if (!strcmp(name, "max-repodesc-length"))
177 ctx.cfg.max_repodesc_len = atoi(value); 177 ctx.cfg.max_repodesc_len = atoi(value);
178 else if (!strcmp(name, "max-blob-size")) 178 else if (!strcmp(name, "max-blob-size"))
179 ctx.cfg.max_blob_size = atoi(value); 179 ctx.cfg.max_blob_size = atoi(value);
180 else if (!strcmp(name, "max-repo-count")) 180 else if (!strcmp(name, "max-repo-count"))
181 ctx.cfg.max_repo_count = atoi(value); 181 ctx.cfg.max_repo_count = atoi(value);
182 else if (!strcmp(name, "max-commit-count")) 182 else if (!strcmp(name, "max-commit-count"))
183 ctx.cfg.max_commit_count = atoi(value); 183 ctx.cfg.max_commit_count = atoi(value);
184 else if (!strcmp(name, "project-list"))
185 ctx.cfg.project_list = xstrdup(expand_macros(value));
184 else if (!strcmp(name, "scan-path")) 186 else if (!strcmp(name, "scan-path"))
185 if (!ctx.cfg.nocache && ctx.cfg.cache_size) 187 if (!ctx.cfg.nocache && ctx.cfg.cache_size)
186 process_cached_repolist(expand_macros(value)); 188 process_cached_repolist(expand_macros(value));
189 else if (ctx.cfg.project_list)
190 scan_projects(expand_macros(value),
191 ctx.cfg.project_list, repo_config);
187 else 192 else
188 scan_tree(expand_macros(value), repo_config); 193 scan_tree(expand_macros(value), repo_config);
189 else if (!strcmp(name, "source-filter")) 194 else if (!strcmp(name, "source-filter"))
190 ctx.cfg.source_filter = new_filter(value, 1); 195 ctx.cfg.source_filter = new_filter(value, 1);
191 else if (!strcmp(name, "summary-log")) 196 else if (!strcmp(name, "summary-log"))
192 ctx.cfg.summary_log = atoi(value); 197 ctx.cfg.summary_log = atoi(value);
193 else if (!strcmp(name, "summary-branches")) 198 else if (!strcmp(name, "summary-branches"))
194 ctx.cfg.summary_branches = atoi(value); 199 ctx.cfg.summary_branches = atoi(value);
195 else if (!strcmp(name, "summary-tags")) 200 else if (!strcmp(name, "summary-tags"))
196 ctx.cfg.summary_tags = atoi(value); 201 ctx.cfg.summary_tags = atoi(value);
197 else if (!strcmp(name, "side-by-side-diffs")) 202 else if (!strcmp(name, "side-by-side-diffs"))
198 ctx.cfg.ssdiff = atoi(value); 203 ctx.cfg.ssdiff = atoi(value);
199 else if (!strcmp(name, "agefile")) 204 else if (!strcmp(name, "agefile"))
200 ctx.cfg.agefile = xstrdup(value); 205 ctx.cfg.agefile = xstrdup(value);
201 else if (!strcmp(name, "renamelimit")) 206 else if (!strcmp(name, "renamelimit"))
202 ctx.cfg.renamelimit = atoi(value); 207 ctx.cfg.renamelimit = atoi(value);
203 else if (!strcmp(name, "robots")) 208 else if (!strcmp(name, "robots"))
204 ctx.cfg.robots = xstrdup(value); 209 ctx.cfg.robots = xstrdup(value);
205 else if (!strcmp(name, "clone-prefix")) 210 else if (!strcmp(name, "clone-prefix"))
206 ctx.cfg.clone_prefix = xstrdup(value); 211 ctx.cfg.clone_prefix = xstrdup(value);
207 else if (!strcmp(name, "local-time")) 212 else if (!strcmp(name, "local-time"))
208 ctx.cfg.local_time = atoi(value); 213 ctx.cfg.local_time = atoi(value);
209 else if (!prefixcmp(name, "mimetype.")) 214 else if (!prefixcmp(name, "mimetype."))
210 add_mimetype(name + 9, value); 215 add_mimetype(name + 9, value);
211 else if (!strcmp(name, "include")) 216 else if (!strcmp(name, "include"))
212 parse_configfile(expand_macros(value), config_cb); 217 parse_configfile(expand_macros(value), config_cb);
213} 218}
214 219
215static void querystring_cb(const char *name, const char *value) 220static void querystring_cb(const char *name, const char *value)
216{ 221{
217 if (!value) 222 if (!value)
218 value = ""; 223 value = "";
219 224
220 if (!strcmp(name,"r")) { 225 if (!strcmp(name,"r")) {
221 ctx.qry.repo = xstrdup(value); 226 ctx.qry.repo = xstrdup(value);
222 ctx.repo = cgit_get_repoinfo(value); 227 ctx.repo = cgit_get_repoinfo(value);
223 } else if (!strcmp(name, "p")) { 228 } else if (!strcmp(name, "p")) {
224 ctx.qry.page = xstrdup(value); 229 ctx.qry.page = xstrdup(value);
225 } else if (!strcmp(name, "url")) { 230 } else if (!strcmp(name, "url")) {
226 if (*value == '/') 231 if (*value == '/')
227 value++; 232 value++;
228 ctx.qry.url = xstrdup(value); 233 ctx.qry.url = xstrdup(value);
229 cgit_parse_url(value); 234 cgit_parse_url(value);
230 } else if (!strcmp(name, "qt")) { 235 } else if (!strcmp(name, "qt")) {
231 ctx.qry.grep = xstrdup(value); 236 ctx.qry.grep = xstrdup(value);
232 } else if (!strcmp(name, "q")) { 237 } else if (!strcmp(name, "q")) {
233 ctx.qry.search = xstrdup(value); 238 ctx.qry.search = xstrdup(value);
234 } else if (!strcmp(name, "h")) { 239 } else if (!strcmp(name, "h")) {
235 ctx.qry.head = xstrdup(value); 240 ctx.qry.head = xstrdup(value);
236 ctx.qry.has_symref = 1; 241 ctx.qry.has_symref = 1;
237 } else if (!strcmp(name, "id")) { 242 } else if (!strcmp(name, "id")) {
238 ctx.qry.sha1 = xstrdup(value); 243 ctx.qry.sha1 = xstrdup(value);
239 ctx.qry.has_sha1 = 1; 244 ctx.qry.has_sha1 = 1;
240 } else if (!strcmp(name, "id2")) { 245 } else if (!strcmp(name, "id2")) {
241 ctx.qry.sha2 = xstrdup(value); 246 ctx.qry.sha2 = xstrdup(value);
242 ctx.qry.has_sha1 = 1; 247 ctx.qry.has_sha1 = 1;
243 } else if (!strcmp(name, "ofs")) { 248 } else if (!strcmp(name, "ofs")) {
244 ctx.qry.ofs = atoi(value); 249 ctx.qry.ofs = atoi(value);
245 } else if (!strcmp(name, "path")) { 250 } else if (!strcmp(name, "path")) {
246 ctx.qry.path = trim_end(value, '/'); 251 ctx.qry.path = trim_end(value, '/');
247 } else if (!strcmp(name, "name")) { 252 } else if (!strcmp(name, "name")) {
248 ctx.qry.name = xstrdup(value); 253 ctx.qry.name = xstrdup(value);
249 } else if (!strcmp(name, "mimetype")) { 254 } else if (!strcmp(name, "mimetype")) {
250 ctx.qry.mimetype = xstrdup(value); 255 ctx.qry.mimetype = xstrdup(value);
251 } else if (!strcmp(name, "s")){ 256 } else if (!strcmp(name, "s")){
252 ctx.qry.sort = xstrdup(value); 257 ctx.qry.sort = xstrdup(value);
253 } else if (!strcmp(name, "showmsg")) { 258 } else if (!strcmp(name, "showmsg")) {
254 ctx.qry.showmsg = atoi(value); 259 ctx.qry.showmsg = atoi(value);
255 } else if (!strcmp(name, "period")) { 260 } else if (!strcmp(name, "period")) {
256 ctx.qry.period = xstrdup(value); 261 ctx.qry.period = xstrdup(value);
257 } else if (!strcmp(name, "ss")) { 262 } else if (!strcmp(name, "ss")) {
258 ctx.qry.ssdiff = atoi(value); 263 ctx.qry.ssdiff = atoi(value);
259 } else if (!strcmp(name, "all")) { 264 } else if (!strcmp(name, "all")) {
260 ctx.qry.show_all = atoi(value); 265 ctx.qry.show_all = atoi(value);
261 } else if (!strcmp(name, "context")) { 266 } else if (!strcmp(name, "context")) {
262 ctx.qry.context = atoi(value); 267 ctx.qry.context = atoi(value);
263 } else if (!strcmp(name, "ignorews")) { 268 } else if (!strcmp(name, "ignorews")) {
264 ctx.qry.ignorews = atoi(value); 269 ctx.qry.ignorews = atoi(value);
265 } 270 }
266} 271}
267 272
268char *xstrdupn(const char *str) 273char *xstrdupn(const char *str)
269{ 274{
270 return (str ? xstrdup(str) : NULL); 275 return (str ? xstrdup(str) : NULL);
271} 276}
272 277
273static void prepare_context(struct cgit_context *ctx) 278static void prepare_context(struct cgit_context *ctx)
274{ 279{
275 memset(ctx, 0, sizeof(*ctx)); 280 memset(ctx, 0, sizeof(*ctx));
276 ctx->cfg.agefile = "info/web/last-modified"; 281 ctx->cfg.agefile = "info/web/last-modified";
277 ctx->cfg.nocache = 0; 282 ctx->cfg.nocache = 0;
278 ctx->cfg.cache_size = 0; 283 ctx->cfg.cache_size = 0;
279 ctx->cfg.cache_dynamic_ttl = 5; 284 ctx->cfg.cache_dynamic_ttl = 5;
280 ctx->cfg.cache_max_create_time = 5; 285 ctx->cfg.cache_max_create_time = 5;
281 ctx->cfg.cache_repo_ttl = 5; 286 ctx->cfg.cache_repo_ttl = 5;
282 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 287 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
283 ctx->cfg.cache_root_ttl = 5; 288 ctx->cfg.cache_root_ttl = 5;
284 ctx->cfg.cache_scanrc_ttl = 15; 289 ctx->cfg.cache_scanrc_ttl = 15;
285 ctx->cfg.cache_static_ttl = -1; 290 ctx->cfg.cache_static_ttl = -1;
286 ctx->cfg.css = "/cgit.css"; 291 ctx->cfg.css = "/cgit.css";
287 ctx->cfg.logo = "/cgit.png"; 292 ctx->cfg.logo = "/cgit.png";
288 ctx->cfg.local_time = 0; 293 ctx->cfg.local_time = 0;
289 ctx->cfg.enable_tree_linenumbers = 1; 294 ctx->cfg.enable_tree_linenumbers = 1;
290 ctx->cfg.max_repo_count = 50; 295 ctx->cfg.max_repo_count = 50;
291 ctx->cfg.max_commit_count = 50; 296 ctx->cfg.max_commit_count = 50;
292 ctx->cfg.max_lock_attempts = 5; 297 ctx->cfg.max_lock_attempts = 5;
293 ctx->cfg.max_msg_len = 80; 298 ctx->cfg.max_msg_len = 80;
294 ctx->cfg.max_repodesc_len = 80; 299 ctx->cfg.max_repodesc_len = 80;
295 ctx->cfg.max_blob_size = 0; 300 ctx->cfg.max_blob_size = 0;
296 ctx->cfg.max_stats = 0; 301 ctx->cfg.max_stats = 0;
297 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 302 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
303 ctx->cfg.project_list = NULL;
298 ctx->cfg.renamelimit = -1; 304 ctx->cfg.renamelimit = -1;
299 ctx->cfg.robots = "index, nofollow"; 305 ctx->cfg.robots = "index, nofollow";
300 ctx->cfg.root_title = "Git repository browser"; 306 ctx->cfg.root_title = "Git repository browser";
301 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 307 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
302 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 308 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
303 ctx->cfg.section = ""; 309 ctx->cfg.section = "";
304 ctx->cfg.summary_branches = 10; 310 ctx->cfg.summary_branches = 10;
305 ctx->cfg.summary_log = 10; 311 ctx->cfg.summary_log = 10;
306 ctx->cfg.summary_tags = 10; 312 ctx->cfg.summary_tags = 10;
307 ctx->cfg.max_atom_items = 10; 313 ctx->cfg.max_atom_items = 10;
308 ctx->cfg.ssdiff = 0; 314 ctx->cfg.ssdiff = 0;
309 ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG")); 315 ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG"));
310 ctx->env.http_host = xstrdupn(getenv("HTTP_HOST")); 316 ctx->env.http_host = xstrdupn(getenv("HTTP_HOST"));
311 ctx->env.https = xstrdupn(getenv("HTTPS")); 317 ctx->env.https = xstrdupn(getenv("HTTPS"));
312 ctx->env.no_http = xstrdupn(getenv("NO_HTTP")); 318 ctx->env.no_http = xstrdupn(getenv("NO_HTTP"));
313 ctx->env.path_info = xstrdupn(getenv("PATH_INFO")); 319 ctx->env.path_info = xstrdupn(getenv("PATH_INFO"));
314 ctx->env.query_string = xstrdupn(getenv("QUERY_STRING")); 320 ctx->env.query_string = xstrdupn(getenv("QUERY_STRING"));
315 ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD")); 321 ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD"));
316 ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME")); 322 ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME"));
317 ctx->env.server_name = xstrdupn(getenv("SERVER_NAME")); 323 ctx->env.server_name = xstrdupn(getenv("SERVER_NAME"));
318 ctx->env.server_port = xstrdupn(getenv("SERVER_PORT")); 324 ctx->env.server_port = xstrdupn(getenv("SERVER_PORT"));
319 ctx->page.mimetype = "text/html"; 325 ctx->page.mimetype = "text/html";
320 ctx->page.charset = PAGE_ENCODING; 326 ctx->page.charset = PAGE_ENCODING;
321 ctx->page.filename = NULL; 327 ctx->page.filename = NULL;
322 ctx->page.size = 0; 328 ctx->page.size = 0;
323 ctx->page.modified = time(NULL); 329 ctx->page.modified = time(NULL);
324 ctx->page.expires = ctx->page.modified; 330 ctx->page.expires = ctx->page.modified;
325 ctx->page.etag = NULL; 331 ctx->page.etag = NULL;
326 memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list)); 332 memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list));
327 if (ctx->env.script_name) 333 if (ctx->env.script_name)
328 ctx->cfg.script_name = ctx->env.script_name; 334 ctx->cfg.script_name = ctx->env.script_name;
329 if (ctx->env.query_string) 335 if (ctx->env.query_string)
330 ctx->qry.raw = ctx->env.query_string; 336 ctx->qry.raw = ctx->env.query_string;
331 if (!ctx->env.cgit_config) 337 if (!ctx->env.cgit_config)
332 ctx->env.cgit_config = CGIT_CONFIG; 338 ctx->env.cgit_config = CGIT_CONFIG;
333} 339}
334 340
335struct refmatch { 341struct refmatch {
336 char *req_ref; 342 char *req_ref;
337 char *first_ref; 343 char *first_ref;
338 int match; 344 int match;
339}; 345};
340 346
341int find_current_ref(const char *refname, const unsigned char *sha1, 347int find_current_ref(const char *refname, const unsigned char *sha1,
342 int flags, void *cb_data) 348 int flags, void *cb_data)
343{ 349{
344 struct refmatch *info; 350 struct refmatch *info;
345 351
346 info = (struct refmatch *)cb_data; 352 info = (struct refmatch *)cb_data;
347 if (!strcmp(refname, info->req_ref)) 353 if (!strcmp(refname, info->req_ref))
348 info->match = 1; 354 info->match = 1;
349 if (!info->first_ref) 355 if (!info->first_ref)
350 info->first_ref = xstrdup(refname); 356 info->first_ref = xstrdup(refname);
351 return info->match; 357 return info->match;
352} 358}
353 359
354char *find_default_branch(struct cgit_repo *repo) 360char *find_default_branch(struct cgit_repo *repo)
355{ 361{
356 struct refmatch info; 362 struct refmatch info;
357 char *ref; 363 char *ref;
358 364
359 info.req_ref = repo->defbranch; 365 info.req_ref = repo->defbranch;
360 info.first_ref = NULL; 366 info.first_ref = NULL;
361 info.match = 0; 367 info.match = 0;
362 for_each_branch_ref(find_current_ref, &info); 368 for_each_branch_ref(find_current_ref, &info);
363 if (info.match) 369 if (info.match)
364 ref = info.req_ref; 370 ref = info.req_ref;
365 else 371 else
366 ref = info.first_ref; 372 ref = info.first_ref;
367 if (ref) 373 if (ref)
368 ref = xstrdup(ref); 374 ref = xstrdup(ref);
369 return ref; 375 return ref;
370} 376}
371 377
372static int prepare_repo_cmd(struct cgit_context *ctx) 378static int prepare_repo_cmd(struct cgit_context *ctx)
373{ 379{
374 char *tmp; 380 char *tmp;
375 unsigned char sha1[20]; 381 unsigned char sha1[20];
376 int nongit = 0; 382 int nongit = 0;
377 383
378 setenv("GIT_DIR", ctx->repo->path, 1); 384 setenv("GIT_DIR", ctx->repo->path, 1);
379 setup_git_directory_gently(&nongit); 385 setup_git_directory_gently(&nongit);
380 if (nongit) { 386 if (nongit) {
381 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title, 387 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title,
382 "config error"); 388 "config error");
383 tmp = fmt("Not a git repository: '%s'", ctx->repo->path); 389 tmp = fmt("Not a git repository: '%s'", ctx->repo->path);
384 ctx->repo = NULL; 390 ctx->repo = NULL;
385 cgit_print_http_headers(ctx); 391 cgit_print_http_headers(ctx);
386 cgit_print_docstart(ctx); 392 cgit_print_docstart(ctx);
387 cgit_print_pageheader(ctx); 393 cgit_print_pageheader(ctx);
388 cgit_print_error(tmp); 394 cgit_print_error(tmp);
389 cgit_print_docend(); 395 cgit_print_docend();
390 return 1; 396 return 1;
391 } 397 }
392 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc); 398 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc);
393 399
394 if (!ctx->qry.head) { 400 if (!ctx->qry.head) {
395 ctx->qry.nohead = 1; 401 ctx->qry.nohead = 1;
396 ctx->qry.head = find_default_branch(ctx->repo); 402 ctx->qry.head = find_default_branch(ctx->repo);
397 ctx->repo->defbranch = ctx->qry.head; 403 ctx->repo->defbranch = ctx->qry.head;
398 } 404 }
399 405
400 if (!ctx->qry.head) { 406 if (!ctx->qry.head) {
401 cgit_print_http_headers(ctx); 407 cgit_print_http_headers(ctx);
402 cgit_print_docstart(ctx); 408 cgit_print_docstart(ctx);
403 cgit_print_pageheader(ctx); 409 cgit_print_pageheader(ctx);
404 cgit_print_error("Repository seems to be empty"); 410 cgit_print_error("Repository seems to be empty");
405 cgit_print_docend(); 411 cgit_print_docend();
406 return 1; 412 return 1;
407 } 413 }
408 414
409 if (get_sha1(ctx->qry.head, sha1)) { 415 if (get_sha1(ctx->qry.head, sha1)) {
410 tmp = xstrdup(ctx->qry.head); 416 tmp = xstrdup(ctx->qry.head);
411 ctx->qry.head = ctx->repo->defbranch; 417 ctx->qry.head = ctx->repo->defbranch;
412 ctx->page.status = 404; 418 ctx->page.status = 404;
413 ctx->page.statusmsg = "not found"; 419 ctx->page.statusmsg = "not found";
414 cgit_print_http_headers(ctx); 420 cgit_print_http_headers(ctx);
415 cgit_print_docstart(ctx); 421 cgit_print_docstart(ctx);
416 cgit_print_pageheader(ctx); 422 cgit_print_pageheader(ctx);
417 cgit_print_error(fmt("Invalid branch: %s", tmp)); 423 cgit_print_error(fmt("Invalid branch: %s", tmp));
418 cgit_print_docend(); 424 cgit_print_docend();
419 return 1; 425 return 1;
420 } 426 }
421 return 0; 427 return 0;
422} 428}
423 429
424static void process_request(void *cbdata) 430static void process_request(void *cbdata)
425{ 431{
426 struct cgit_context *ctx = cbdata; 432 struct cgit_context *ctx = cbdata;
427 struct cgit_cmd *cmd; 433 struct cgit_cmd *cmd;
428 434
429 cmd = cgit_get_cmd(ctx); 435 cmd = cgit_get_cmd(ctx);
430 if (!cmd) { 436 if (!cmd) {
431 ctx->page.title = "cgit error"; 437 ctx->page.title = "cgit error";
432 cgit_print_http_headers(ctx); 438 cgit_print_http_headers(ctx);
433 cgit_print_docstart(ctx); 439 cgit_print_docstart(ctx);
434 cgit_print_pageheader(ctx); 440 cgit_print_pageheader(ctx);
435 cgit_print_error("Invalid request"); 441 cgit_print_error("Invalid request");
436 cgit_print_docend(); 442 cgit_print_docend();
437 return; 443 return;
438 } 444 }
439 445
440 /* If cmd->want_vpath is set, assume ctx->qry.path contains a "virtual" 446 /* If cmd->want_vpath is set, assume ctx->qry.path contains a "virtual"
441 * in-project path limit to be made available at ctx->qry.vpath. 447 * in-project path limit to be made available at ctx->qry.vpath.
442 * Otherwise, no path limit is in effect (ctx->qry.vpath = NULL). 448 * Otherwise, no path limit is in effect (ctx->qry.vpath = NULL).
443 */ 449 */
444 ctx->qry.vpath = cmd->want_vpath ? ctx->qry.path : NULL; 450 ctx->qry.vpath = cmd->want_vpath ? ctx->qry.path : NULL;
445 451
446 if (cmd->want_repo && !ctx->repo) { 452 if (cmd->want_repo && !ctx->repo) {
447 cgit_print_http_headers(ctx); 453 cgit_print_http_headers(ctx);
448 cgit_print_docstart(ctx); 454 cgit_print_docstart(ctx);
449 cgit_print_pageheader(ctx); 455 cgit_print_pageheader(ctx);
450 cgit_print_error(fmt("No repository selected")); 456 cgit_print_error(fmt("No repository selected"));
451 cgit_print_docend(); 457 cgit_print_docend();
452 return; 458 return;
453 } 459 }
454 460
455 if (ctx->repo && prepare_repo_cmd(ctx)) 461 if (ctx->repo && prepare_repo_cmd(ctx))
456 return; 462 return;
457 463
458 if (cmd->want_layout) { 464 if (cmd->want_layout) {
459 cgit_print_http_headers(ctx); 465 cgit_print_http_headers(ctx);
460 cgit_print_docstart(ctx); 466 cgit_print_docstart(ctx);
461 cgit_print_pageheader(ctx); 467 cgit_print_pageheader(ctx);
462 } 468 }
463 469
464 cmd->fn(ctx); 470 cmd->fn(ctx);
465 471
466 if (cmd->want_layout) 472 if (cmd->want_layout)
467 cgit_print_docend(); 473 cgit_print_docend();
468} 474}
469 475
470int cmp_repos(const void *a, const void *b) 476int cmp_repos(const void *a, const void *b)
471{ 477{
472 const struct cgit_repo *ra = a, *rb = b; 478 const struct cgit_repo *ra = a, *rb = b;
473 return strcmp(ra->url, rb->url); 479 return strcmp(ra->url, rb->url);
474} 480}
475 481
476char *build_snapshot_setting(int bitmap) 482char *build_snapshot_setting(int bitmap)
477{ 483{
478 const struct cgit_snapshot_format *f; 484 const struct cgit_snapshot_format *f;
479 char *result = xstrdup(""); 485 char *result = xstrdup("");
480 char *tmp; 486 char *tmp;
481 int len; 487 int len;
482 488
483 for (f = cgit_snapshot_formats; f->suffix; f++) { 489 for (f = cgit_snapshot_formats; f->suffix; f++) {
484 if (f->bit & bitmap) { 490 if (f->bit & bitmap) {
485 tmp = result; 491 tmp = result;
486 result = xstrdup(fmt("%s%s ", tmp, f->suffix)); 492 result = xstrdup(fmt("%s%s ", tmp, f->suffix));
487 free(tmp); 493 free(tmp);
488 } 494 }
489 } 495 }
490 len = strlen(result); 496 len = strlen(result);
491 if (len) 497 if (len)
492 result[len - 1] = '\0'; 498 result[len - 1] = '\0';
493 return result; 499 return result;
494} 500}
495 501
496char *get_first_line(char *txt) 502char *get_first_line(char *txt)
497{ 503{
498 char *t = xstrdup(txt); 504 char *t = xstrdup(txt);
499 char *p = strchr(t, '\n'); 505 char *p = strchr(t, '\n');
500 if (p) 506 if (p)
501 *p = '\0'; 507 *p = '\0';
502 return t; 508 return t;
503} 509}
504 510
505void print_repo(FILE *f, struct cgit_repo *repo) 511void print_repo(FILE *f, struct cgit_repo *repo)
506{ 512{
507 fprintf(f, "repo.url=%s\n", repo->url); 513 fprintf(f, "repo.url=%s\n", repo->url);
508 fprintf(f, "repo.name=%s\n", repo->name); 514 fprintf(f, "repo.name=%s\n", repo->name);
509 fprintf(f, "repo.path=%s\n", repo->path); 515 fprintf(f, "repo.path=%s\n", repo->path);
510 if (repo->owner) 516 if (repo->owner)
511 fprintf(f, "repo.owner=%s\n", repo->owner); 517 fprintf(f, "repo.owner=%s\n", repo->owner);
512 if (repo->desc) { 518 if (repo->desc) {
513 char *tmp = get_first_line(repo->desc); 519 char *tmp = get_first_line(repo->desc);
514 fprintf(f, "repo.desc=%s\n", tmp); 520 fprintf(f, "repo.desc=%s\n", tmp);
515 free(tmp); 521 free(tmp);
516 } 522 }
517 if (repo->readme) 523 if (repo->readme)
518 fprintf(f, "repo.readme=%s\n", repo->readme); 524 fprintf(f, "repo.readme=%s\n", repo->readme);
519 if (repo->defbranch) 525 if (repo->defbranch)
520 fprintf(f, "repo.defbranch=%s\n", repo->defbranch); 526 fprintf(f, "repo.defbranch=%s\n", repo->defbranch);
521 if (repo->module_link) 527 if (repo->module_link)
522 fprintf(f, "repo.module-link=%s\n", repo->module_link); 528 fprintf(f, "repo.module-link=%s\n", repo->module_link);
523 if (repo->section) 529 if (repo->section)
524 fprintf(f, "repo.section=%s\n", repo->section); 530 fprintf(f, "repo.section=%s\n", repo->section);
525 if (repo->clone_url) 531 if (repo->clone_url)
526 fprintf(f, "repo.clone-url=%s\n", repo->clone_url); 532 fprintf(f, "repo.clone-url=%s\n", repo->clone_url);
527 fprintf(f, "repo.enable-log-filecount=%d\n", 533 fprintf(f, "repo.enable-log-filecount=%d\n",
528 repo->enable_log_filecount); 534 repo->enable_log_filecount);
529 fprintf(f, "repo.enable-log-linecount=%d\n", 535 fprintf(f, "repo.enable-log-linecount=%d\n",
530 repo->enable_log_linecount); 536 repo->enable_log_linecount);
531 if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter) 537 if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter)
532 fprintf(f, "repo.about-filter=%s\n", repo->about_filter->cmd); 538 fprintf(f, "repo.about-filter=%s\n", repo->about_filter->cmd);
533 if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter) 539 if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter)
534 fprintf(f, "repo.commit-filter=%s\n", repo->commit_filter->cmd); 540 fprintf(f, "repo.commit-filter=%s\n", repo->commit_filter->cmd);
535 if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter) 541 if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter)
536 fprintf(f, "repo.source-filter=%s\n", repo->source_filter->cmd); 542 fprintf(f, "repo.source-filter=%s\n", repo->source_filter->cmd);
537 if (repo->snapshots != ctx.cfg.snapshots) { 543 if (repo->snapshots != ctx.cfg.snapshots) {
538 char *tmp = build_snapshot_setting(repo->snapshots); 544 char *tmp = build_snapshot_setting(repo->snapshots);
539 fprintf(f, "repo.snapshots=%s\n", tmp); 545 fprintf(f, "repo.snapshots=%s\n", tmp);
540 free(tmp); 546 free(tmp);
541 } 547 }
542 if (repo->max_stats != ctx.cfg.max_stats) 548 if (repo->max_stats != ctx.cfg.max_stats)
543 fprintf(f, "repo.max-stats=%s\n", 549 fprintf(f, "repo.max-stats=%s\n",
544 cgit_find_stats_periodname(repo->max_stats)); 550 cgit_find_stats_periodname(repo->max_stats));
545 fprintf(f, "\n"); 551 fprintf(f, "\n");
546} 552}
547 553
548void print_repolist(FILE *f, struct cgit_repolist *list, int start) 554void print_repolist(FILE *f, struct cgit_repolist *list, int start)
549{ 555{
550 int i; 556 int i;
551 557
552 for(i = start; i < list->count; i++) 558 for(i = start; i < list->count; i++)
553 print_repo(f, &list->repos[i]); 559 print_repo(f, &list->repos[i]);
554} 560}
555 561
556/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc' 562/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc'
557 * and return 0 on success. 563 * and return 0 on success.
558 */ 564 */
559static int generate_cached_repolist(const char *path, const char *cached_rc) 565static int generate_cached_repolist(const char *path, const char *cached_rc)
560{ 566{
561 char *locked_rc; 567 char *locked_rc;
562 int idx; 568 int idx;
563 FILE *f; 569 FILE *f;
564 570
565 locked_rc = xstrdup(fmt("%s.lock", cached_rc)); 571 locked_rc = xstrdup(fmt("%s.lock", cached_rc));
566 f = fopen(locked_rc, "wx"); 572 f = fopen(locked_rc, "wx");
567 if (!f) { 573 if (!f) {
568 /* Inform about the error unless the lockfile already existed, 574 /* Inform about the error unless the lockfile already existed,
569 * since that only means we've got concurrent requests. 575 * since that only means we've got concurrent requests.
570 */ 576 */
571 if (errno != EEXIST) 577 if (errno != EEXIST)
572 fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n", 578 fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n",
573 locked_rc, strerror(errno), errno); 579 locked_rc, strerror(errno), errno);
574 return errno; 580 return errno;
575 } 581 }
576 idx = cgit_repolist.count; 582 idx = cgit_repolist.count;
577 scan_tree(path, repo_config); 583 if (ctx.cfg.project_list)
584 scan_projects(path, ctx.cfg.project_list, repo_config);
585 else
586 scan_tree(path, repo_config);
578 print_repolist(f, &cgit_repolist, idx); 587 print_repolist(f, &cgit_repolist, idx);
579 if (rename(locked_rc, cached_rc)) 588 if (rename(locked_rc, cached_rc))
580 fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n", 589 fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n",
581 locked_rc, cached_rc, strerror(errno), errno); 590 locked_rc, cached_rc, strerror(errno), errno);
582 fclose(f); 591 fclose(f);
583 return 0; 592 return 0;
584} 593}
585 594
586static void process_cached_repolist(const char *path) 595static void process_cached_repolist(const char *path)
587{ 596{
588 struct stat st; 597 struct stat st;
589 char *cached_rc; 598 char *cached_rc;
590 time_t age; 599 time_t age;
600 unsigned long hash;
591 601
592 cached_rc = xstrdup(fmt("%s/rc-%8x", ctx.cfg.cache_root, 602 hash = hash_str(path);
593 hash_str(path))); 603 if (ctx.cfg.project_list)
604 hash += hash_str(ctx.cfg.project_list);
605 cached_rc = xstrdup(fmt("%s/rc-%8x", ctx.cfg.cache_root, hash));
594 606
595 if (stat(cached_rc, &st)) { 607 if (stat(cached_rc, &st)) {
596 /* Nothing is cached, we need to scan without forking. And 608 /* Nothing is cached, we need to scan without forking. And
597 * if we fail to generate a cached repolist, we need to 609 * if we fail to generate a cached repolist, we need to
598 * invoke scan_tree manually. 610 * invoke scan_tree manually.
599 */ 611 */
600 if (generate_cached_repolist(path, cached_rc)) 612 if (generate_cached_repolist(path, cached_rc)) {
601 scan_tree(path, repo_config); 613 if (ctx.cfg.project_list)
614 scan_projects(path, ctx.cfg.project_list,
615 repo_config);
616 else
617 scan_tree(path, repo_config);
618 }
602 return; 619 return;
603 } 620 }
604 621
605 parse_configfile(cached_rc, config_cb); 622 parse_configfile(cached_rc, config_cb);
606 623
607 /* If the cached configfile hasn't expired, lets exit now */ 624 /* If the cached configfile hasn't expired, lets exit now */
608 age = time(NULL) - st.st_mtime; 625 age = time(NULL) - st.st_mtime;
609 if (age <= (ctx.cfg.cache_scanrc_ttl * 60)) 626 if (age <= (ctx.cfg.cache_scanrc_ttl * 60))
610 return; 627 return;
611 628
612 /* The cached repolist has been parsed, but it was old. So lets 629 /* The cached repolist has been parsed, but it was old. So lets
613 * rescan the specified path and generate a new cached repolist 630 * rescan the specified path and generate a new cached repolist
614 * in a child-process to avoid latency for the current request. 631 * in a child-process to avoid latency for the current request.
615 */ 632 */
616 if (fork()) 633 if (fork())
617 return; 634 return;
618 635
619 exit(generate_cached_repolist(path, cached_rc)); 636 exit(generate_cached_repolist(path, cached_rc));
620} 637}
621 638
622static void cgit_parse_args(int argc, const char **argv) 639static void cgit_parse_args(int argc, const char **argv)
623{ 640{
624 int i; 641 int i;
625 int scan = 0; 642 int scan = 0;
626 643
627 for (i = 1; i < argc; i++) { 644 for (i = 1; i < argc; i++) {
628 if (!strncmp(argv[i], "--cache=", 8)) { 645 if (!strncmp(argv[i], "--cache=", 8)) {
629 ctx.cfg.cache_root = xstrdup(argv[i]+8); 646 ctx.cfg.cache_root = xstrdup(argv[i]+8);
630 } 647 }
631 if (!strcmp(argv[i], "--nocache")) { 648 if (!strcmp(argv[i], "--nocache")) {
632 ctx.cfg.nocache = 1; 649 ctx.cfg.nocache = 1;
633 } 650 }
634 if (!strcmp(argv[i], "--nohttp")) { 651 if (!strcmp(argv[i], "--nohttp")) {
635 ctx.env.no_http = "1"; 652 ctx.env.no_http = "1";
636 } 653 }
637 if (!strncmp(argv[i], "--query=", 8)) { 654 if (!strncmp(argv[i], "--query=", 8)) {
638 ctx.qry.raw = xstrdup(argv[i]+8); 655 ctx.qry.raw = xstrdup(argv[i]+8);
639 } 656 }
640 if (!strncmp(argv[i], "--repo=", 7)) { 657 if (!strncmp(argv[i], "--repo=", 7)) {
641 ctx.qry.repo = xstrdup(argv[i]+7); 658 ctx.qry.repo = xstrdup(argv[i]+7);
642 } 659 }
643 if (!strncmp(argv[i], "--page=", 7)) { 660 if (!strncmp(argv[i], "--page=", 7)) {
644 ctx.qry.page = xstrdup(argv[i]+7); 661 ctx.qry.page = xstrdup(argv[i]+7);
645 } 662 }
646 if (!strncmp(argv[i], "--head=", 7)) { 663 if (!strncmp(argv[i], "--head=", 7)) {
647 ctx.qry.head = xstrdup(argv[i]+7); 664 ctx.qry.head = xstrdup(argv[i]+7);
648 ctx.qry.has_symref = 1; 665 ctx.qry.has_symref = 1;
649 } 666 }
650 if (!strncmp(argv[i], "--sha1=", 7)) { 667 if (!strncmp(argv[i], "--sha1=", 7)) {
651 ctx.qry.sha1 = xstrdup(argv[i]+7); 668 ctx.qry.sha1 = xstrdup(argv[i]+7);
652 ctx.qry.has_sha1 = 1; 669 ctx.qry.has_sha1 = 1;
653 } 670 }
654 if (!strncmp(argv[i], "--ofs=", 6)) { 671 if (!strncmp(argv[i], "--ofs=", 6)) {
655 ctx.qry.ofs = atoi(argv[i]+6); 672 ctx.qry.ofs = atoi(argv[i]+6);
656 } 673 }
657 if (!strncmp(argv[i], "--scan-tree=", 12) || 674 if (!strncmp(argv[i], "--scan-tree=", 12) ||
658 !strncmp(argv[i], "--scan-path=", 12)) { 675 !strncmp(argv[i], "--scan-path=", 12)) {
659 /* HACK: the global snapshot bitmask defines the 676 /* HACK: the global snapshot bitmask defines the
660 * set of allowed snapshot formats, but the config 677 * set of allowed snapshot formats, but the config
661 * file hasn't been parsed yet so the mask is 678 * file hasn't been parsed yet so the mask is
662 * currently 0. By setting all bits high before 679 * currently 0. By setting all bits high before
663 * scanning we make sure that any in-repo cgitrc 680 * scanning we make sure that any in-repo cgitrc
664 * snapshot setting is respected by scan_tree(). 681 * snapshot setting is respected by scan_tree().
665 * BTW: we assume that there'll never be more than 682 * BTW: we assume that there'll never be more than
666 * 255 different snapshot formats supported by cgit... 683 * 255 different snapshot formats supported by cgit...
667 */ 684 */
668 ctx.cfg.snapshots = 0xFF; 685 ctx.cfg.snapshots = 0xFF;
669 scan++; 686 scan++;
670 scan_tree(argv[i] + 12, repo_config); 687 scan_tree(argv[i] + 12, repo_config);
671 } 688 }
672 } 689 }
673 if (scan) { 690 if (scan) {
674 qsort(cgit_repolist.repos, cgit_repolist.count, 691 qsort(cgit_repolist.repos, cgit_repolist.count,
675 sizeof(struct cgit_repo), cmp_repos); 692 sizeof(struct cgit_repo), cmp_repos);
676 print_repolist(stdout, &cgit_repolist, 0); 693 print_repolist(stdout, &cgit_repolist, 0);
677 exit(0); 694 exit(0);
678 } 695 }
679} 696}
680 697
681static int calc_ttl() 698static int calc_ttl()
682{ 699{
683 if (!ctx.repo) 700 if (!ctx.repo)
684 return ctx.cfg.cache_root_ttl; 701 return ctx.cfg.cache_root_ttl;
685 702
686 if (!ctx.qry.page) 703 if (!ctx.qry.page)
687 return ctx.cfg.cache_repo_ttl; 704 return ctx.cfg.cache_repo_ttl;
688 705
689 if (ctx.qry.has_symref) 706 if (ctx.qry.has_symref)
690 return ctx.cfg.cache_dynamic_ttl; 707 return ctx.cfg.cache_dynamic_ttl;
691 708
692 if (ctx.qry.has_sha1) 709 if (ctx.qry.has_sha1)
693 return ctx.cfg.cache_static_ttl; 710 return ctx.cfg.cache_static_ttl;
694 711
695 return ctx.cfg.cache_repo_ttl; 712 return ctx.cfg.cache_repo_ttl;
696} 713}
697 714
698int main(int argc, const char **argv) 715int main(int argc, const char **argv)
699{ 716{
700 const char *path; 717 const char *path;
701 char *qry; 718 char *qry;
702 int err, ttl; 719 int err, ttl;
703 720
704 prepare_context(&ctx); 721 prepare_context(&ctx);
705 cgit_repolist.length = 0; 722 cgit_repolist.length = 0;
706 cgit_repolist.count = 0; 723 cgit_repolist.count = 0;
707 cgit_repolist.repos = NULL; 724 cgit_repolist.repos = NULL;
708 725
709 cgit_parse_args(argc, argv); 726 cgit_parse_args(argc, argv);
710 parse_configfile(expand_macros(ctx.env.cgit_config), config_cb); 727 parse_configfile(expand_macros(ctx.env.cgit_config), config_cb);
711 ctx.repo = NULL; 728 ctx.repo = NULL;
712 http_parse_querystring(ctx.qry.raw, querystring_cb); 729 http_parse_querystring(ctx.qry.raw, querystring_cb);
713 730
714 /* If virtual-root isn't specified in cgitrc, lets pretend 731 /* If virtual-root isn't specified in cgitrc, lets pretend
715 * that virtual-root equals SCRIPT_NAME. 732 * that virtual-root equals SCRIPT_NAME.
716 */ 733 */
717 if (!ctx.cfg.virtual_root) 734 if (!ctx.cfg.virtual_root)
718 ctx.cfg.virtual_root = ctx.cfg.script_name; 735 ctx.cfg.virtual_root = ctx.cfg.script_name;
719 736
720 /* If no url parameter is specified on the querystring, lets 737 /* If no url parameter is specified on the querystring, lets
721 * use PATH_INFO as url. This allows cgit to work with virtual 738 * use PATH_INFO as url. This allows cgit to work with virtual
722 * urls without the need for rewriterules in the webserver (as 739 * urls without the need for rewriterules in the webserver (as
723 * long as PATH_INFO is included in the cache lookup key). 740 * long as PATH_INFO is included in the cache lookup key).
724 */ 741 */
725 path = ctx.env.path_info; 742 path = ctx.env.path_info;
726 if (!ctx.qry.url && path) { 743 if (!ctx.qry.url && path) {
727 if (path[0] == '/') 744 if (path[0] == '/')
728 path++; 745 path++;
729 ctx.qry.url = xstrdup(path); 746 ctx.qry.url = xstrdup(path);
730 if (ctx.qry.raw) { 747 if (ctx.qry.raw) {
731 qry = ctx.qry.raw; 748 qry = ctx.qry.raw;
732 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 749 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
733 free(qry); 750 free(qry);
734 } else 751 } else
735 ctx.qry.raw = xstrdup(ctx.qry.url); 752 ctx.qry.raw = xstrdup(ctx.qry.url);
736 cgit_parse_url(ctx.qry.url); 753 cgit_parse_url(ctx.qry.url);
737 } 754 }
738 755
739 ttl = calc_ttl(); 756 ttl = calc_ttl();
740 ctx.page.expires += ttl*60; 757 ctx.page.expires += ttl*60;
741 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")) 758 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
742 ctx.cfg.nocache = 1; 759 ctx.cfg.nocache = 1;
743 if (ctx.cfg.nocache) 760 if (ctx.cfg.nocache)
744 ctx.cfg.cache_size = 0; 761 ctx.cfg.cache_size = 0;
745 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 762 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
746 ctx.qry.raw, ttl, process_request, &ctx); 763 ctx.qry.raw, ttl, process_request, &ctx);
747 if (err) 764 if (err)
748 cgit_print_error(fmt("Error processing page: %s (%d)", 765 cgit_print_error(fmt("Error processing page: %s (%d)",
749 strerror(err), err)); 766 strerror(err), err));
750 return err; 767 return err;
751} 768}
diff --git a/cgit.h b/cgit.h
index e9e2718..4591f8c 100644
--- a/cgit.h
+++ b/cgit.h
@@ -1,309 +1,310 @@
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 *module_link; 68 char *module_link;
69 char *readme; 69 char *readme;
70 char *section; 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 enable_remote_branches; 75 int enable_remote_branches;
76 int enable_subject_links; 76 int enable_subject_links;
77 int max_stats; 77 int max_stats;
78 time_t mtime; 78 time_t mtime;
79 struct cgit_filter *about_filter; 79 struct cgit_filter *about_filter;
80 struct cgit_filter *commit_filter; 80 struct cgit_filter *commit_filter;
81 struct cgit_filter *source_filter; 81 struct cgit_filter *source_filter;
82}; 82};
83 83
84typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name, 84typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name,
85 const char *value); 85 const char *value);
86 86
87struct cgit_repolist { 87struct cgit_repolist {
88 int length; 88 int length;
89 int count; 89 int count;
90 struct cgit_repo *repos; 90 struct cgit_repo *repos;
91}; 91};
92 92
93struct commitinfo { 93struct commitinfo {
94 struct commit *commit; 94 struct commit *commit;
95 char *author; 95 char *author;
96 char *author_email; 96 char *author_email;
97 unsigned long author_date; 97 unsigned long author_date;
98 char *committer; 98 char *committer;
99 char *committer_email; 99 char *committer_email;
100 unsigned long committer_date; 100 unsigned long committer_date;
101 char *subject; 101 char *subject;
102 char *msg; 102 char *msg;
103 char *msg_encoding; 103 char *msg_encoding;
104}; 104};
105 105
106struct taginfo { 106struct taginfo {
107 char *tagger; 107 char *tagger;
108 char *tagger_email; 108 char *tagger_email;
109 unsigned long tagger_date; 109 unsigned long tagger_date;
110 char *msg; 110 char *msg;
111}; 111};
112 112
113struct refinfo { 113struct refinfo {
114 const char *refname; 114 const char *refname;
115 struct object *object; 115 struct object *object;
116 union { 116 union {
117 struct taginfo *tag; 117 struct taginfo *tag;
118 struct commitinfo *commit; 118 struct commitinfo *commit;
119 }; 119 };
120}; 120};
121 121
122struct reflist { 122struct reflist {
123 struct refinfo **refs; 123 struct refinfo **refs;
124 int alloc; 124 int alloc;
125 int count; 125 int count;
126}; 126};
127 127
128struct cgit_query { 128struct cgit_query {
129 int has_symref; 129 int has_symref;
130 int has_sha1; 130 int has_sha1;
131 char *raw; 131 char *raw;
132 char *repo; 132 char *repo;
133 char *page; 133 char *page;
134 char *search; 134 char *search;
135 char *grep; 135 char *grep;
136 char *head; 136 char *head;
137 char *sha1; 137 char *sha1;
138 char *sha2; 138 char *sha2;
139 char *path; 139 char *path;
140 char *name; 140 char *name;
141 char *mimetype; 141 char *mimetype;
142 char *url; 142 char *url;
143 char *period; 143 char *period;
144 int ofs; 144 int ofs;
145 int nohead; 145 int nohead;
146 char *sort; 146 char *sort;
147 int showmsg; 147 int showmsg;
148 int ssdiff; 148 int ssdiff;
149 int show_all; 149 int show_all;
150 int context; 150 int context;
151 int ignorews; 151 int ignorews;
152 char *vpath; 152 char *vpath;
153}; 153};
154 154
155struct cgit_config { 155struct cgit_config {
156 char *agefile; 156 char *agefile;
157 char *cache_root; 157 char *cache_root;
158 char *clone_prefix; 158 char *clone_prefix;
159 char *css; 159 char *css;
160 char *favicon; 160 char *favicon;
161 char *footer; 161 char *footer;
162 char *head_include; 162 char *head_include;
163 char *header; 163 char *header;
164 char *index_header; 164 char *index_header;
165 char *index_info; 165 char *index_info;
166 char *logo; 166 char *logo;
167 char *logo_link; 167 char *logo_link;
168 char *module_link; 168 char *module_link;
169 char *project_list;
169 char *robots; 170 char *robots;
170 char *root_title; 171 char *root_title;
171 char *root_desc; 172 char *root_desc;
172 char *root_readme; 173 char *root_readme;
173 char *script_name; 174 char *script_name;
174 char *section; 175 char *section;
175 char *virtual_root; 176 char *virtual_root;
176 int cache_size; 177 int cache_size;
177 int cache_dynamic_ttl; 178 int cache_dynamic_ttl;
178 int cache_max_create_time; 179 int cache_max_create_time;
179 int cache_repo_ttl; 180 int cache_repo_ttl;
180 int cache_root_ttl; 181 int cache_root_ttl;
181 int cache_scanrc_ttl; 182 int cache_scanrc_ttl;
182 int cache_static_ttl; 183 int cache_static_ttl;
183 int embedded; 184 int embedded;
184 int enable_filter_overrides; 185 int enable_filter_overrides;
185 int enable_index_links; 186 int enable_index_links;
186 int enable_log_filecount; 187 int enable_log_filecount;
187 int enable_log_linecount; 188 int enable_log_linecount;
188 int enable_remote_branches; 189 int enable_remote_branches;
189 int enable_subject_links; 190 int enable_subject_links;
190 int enable_tree_linenumbers; 191 int enable_tree_linenumbers;
191 int local_time; 192 int local_time;
192 int max_atom_items; 193 int max_atom_items;
193 int max_repo_count; 194 int max_repo_count;
194 int max_commit_count; 195 int max_commit_count;
195 int max_lock_attempts; 196 int max_lock_attempts;
196 int max_msg_len; 197 int max_msg_len;
197 int max_repodesc_len; 198 int max_repodesc_len;
198 int max_blob_size; 199 int max_blob_size;
199 int max_stats; 200 int max_stats;
200 int nocache; 201 int nocache;
201 int noplainemail; 202 int noplainemail;
202 int noheader; 203 int noheader;
203 int renamelimit; 204 int renamelimit;
204 int snapshots; 205 int snapshots;
205 int summary_branches; 206 int summary_branches;
206 int summary_log; 207 int summary_log;
207 int summary_tags; 208 int summary_tags;
208 int ssdiff; 209 int ssdiff;
209 struct string_list mimetypes; 210 struct string_list mimetypes;
210 struct cgit_filter *about_filter; 211 struct cgit_filter *about_filter;
211 struct cgit_filter *commit_filter; 212 struct cgit_filter *commit_filter;
212 struct cgit_filter *source_filter; 213 struct cgit_filter *source_filter;
213}; 214};
214 215
215struct cgit_page { 216struct cgit_page {
216 time_t modified; 217 time_t modified;
217 time_t expires; 218 time_t expires;
218 size_t size; 219 size_t size;
219 char *mimetype; 220 char *mimetype;
220 char *charset; 221 char *charset;
221 char *filename; 222 char *filename;
222 char *etag; 223 char *etag;
223 char *title; 224 char *title;
224 int status; 225 int status;
225 char *statusmsg; 226 char *statusmsg;
226}; 227};
227 228
228struct cgit_environment { 229struct cgit_environment {
229 char *cgit_config; 230 char *cgit_config;
230 char *http_host; 231 char *http_host;
231 char *https; 232 char *https;
232 char *no_http; 233 char *no_http;
233 char *path_info; 234 char *path_info;
234 char *query_string; 235 char *query_string;
235 char *request_method; 236 char *request_method;
236 char *script_name; 237 char *script_name;
237 char *server_name; 238 char *server_name;
238 char *server_port; 239 char *server_port;
239}; 240};
240 241
241struct cgit_context { 242struct cgit_context {
242 struct cgit_environment env; 243 struct cgit_environment env;
243 struct cgit_query qry; 244 struct cgit_query qry;
244 struct cgit_config cfg; 245 struct cgit_config cfg;
245 struct cgit_repo *repo; 246 struct cgit_repo *repo;
246 struct cgit_page page; 247 struct cgit_page page;
247}; 248};
248 249
249struct cgit_snapshot_format { 250struct cgit_snapshot_format {
250 const char *suffix; 251 const char *suffix;
251 const char *mimetype; 252 const char *mimetype;
252 write_archive_fn_t write_func; 253 write_archive_fn_t write_func;
253 int bit; 254 int bit;
254}; 255};
255 256
256extern const char *cgit_version; 257extern const char *cgit_version;
257 258
258extern struct cgit_repolist cgit_repolist; 259extern struct cgit_repolist cgit_repolist;
259extern struct cgit_context ctx; 260extern struct cgit_context ctx;
260extern const struct cgit_snapshot_format cgit_snapshot_formats[]; 261extern const struct cgit_snapshot_format cgit_snapshot_formats[];
261 262
262extern struct cgit_repo *cgit_add_repo(const char *url); 263extern struct cgit_repo *cgit_add_repo(const char *url);
263extern struct cgit_repo *cgit_get_repoinfo(const char *url); 264extern struct cgit_repo *cgit_get_repoinfo(const char *url);
264extern void cgit_repo_config_cb(const char *name, const char *value); 265extern void cgit_repo_config_cb(const char *name, const char *value);
265 266
266extern int chk_zero(int result, char *msg); 267extern int chk_zero(int result, char *msg);
267extern int chk_positive(int result, char *msg); 268extern int chk_positive(int result, char *msg);
268extern int chk_non_negative(int result, char *msg); 269extern int chk_non_negative(int result, char *msg);
269 270
270extern char *trim_end(const char *str, char c); 271extern char *trim_end(const char *str, char c);
271extern char *strlpart(char *txt, int maxlen); 272extern char *strlpart(char *txt, int maxlen);
272extern char *strrpart(char *txt, int maxlen); 273extern char *strrpart(char *txt, int maxlen);
273 274
274extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); 275extern void cgit_add_ref(struct reflist *list, struct refinfo *ref);
275extern int cgit_refs_cb(const char *refname, const unsigned char *sha1, 276extern int cgit_refs_cb(const char *refname, const unsigned char *sha1,
276 int flags, void *cb_data); 277 int flags, void *cb_data);
277 278
278extern void *cgit_free_commitinfo(struct commitinfo *info); 279extern void *cgit_free_commitinfo(struct commitinfo *info);
279 280
280extern int cgit_diff_files(const unsigned char *old_sha1, 281extern int cgit_diff_files(const unsigned char *old_sha1,
281 const unsigned char *new_sha1, 282 const unsigned char *new_sha1,
282 unsigned long *old_size, unsigned long *new_size, 283 unsigned long *old_size, unsigned long *new_size,
283 int *binary, int context, int ignorews, 284 int *binary, int context, int ignorews,
284 linediff_fn fn); 285 linediff_fn fn);
285 286
286extern void cgit_diff_tree(const unsigned char *old_sha1, 287extern void cgit_diff_tree(const unsigned char *old_sha1,
287 const unsigned char *new_sha1, 288 const unsigned char *new_sha1,
288 filepair_fn fn, const char *prefix, int ignorews); 289 filepair_fn fn, const char *prefix, int ignorews);
289 290
290extern void cgit_diff_commit(struct commit *commit, filepair_fn fn); 291extern void cgit_diff_commit(struct commit *commit, filepair_fn fn);
291 292
292extern char *fmt(const char *format,...); 293extern char *fmt(const char *format,...);
293 294
294extern struct commitinfo *cgit_parse_commit(struct commit *commit); 295extern struct commitinfo *cgit_parse_commit(struct commit *commit);
295extern struct taginfo *cgit_parse_tag(struct tag *tag); 296extern struct taginfo *cgit_parse_tag(struct tag *tag);
296extern void cgit_parse_url(const char *url); 297extern void cgit_parse_url(const char *url);
297 298
298extern const char *cgit_repobasename(const char *reponame); 299extern const char *cgit_repobasename(const char *reponame);
299 300
300extern int cgit_parse_snapshots_mask(const char *str); 301extern int cgit_parse_snapshots_mask(const char *str);
301 302
302extern int cgit_open_filter(struct cgit_filter *filter); 303extern int cgit_open_filter(struct cgit_filter *filter);
303extern int cgit_close_filter(struct cgit_filter *filter); 304extern int cgit_close_filter(struct cgit_filter *filter);
304 305
305extern int readfile(const char *path, char **buf, size_t *size); 306extern int readfile(const char *path, char **buf, size_t *size);
306 307
307extern char *expand_macros(const char *txt); 308extern char *expand_macros(const char *txt);
308 309
309#endif /* CGIT_H */ 310#endif /* CGIT_H */
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index a853522..ec004d4 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -1,532 +1,540 @@
1:man source: cgit 1:man source: cgit
2:man manual: cgit 2:man manual: cgit
3 3
4CGITRC(5) 4CGITRC(5)
5======== 5========
6 6
7 7
8NAME 8NAME
9---- 9----
10cgitrc - runtime configuration for cgit 10cgitrc - runtime configuration for cgit
11 11
12 12
13SYNOPSIS 13SYNOPSIS
14-------- 14--------
15Cgitrc contains all runtime settings for cgit, including the list of git 15Cgitrc contains all runtime settings for cgit, including the list of git
16repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank 16repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank
17lines, and lines starting with '#', are ignored. 17lines, and lines starting with '#', are ignored.
18 18
19 19
20LOCATION 20LOCATION
21-------- 21--------
22The default location of cgitrc, defined at compile time, is /etc/cgitrc. At 22The default location of cgitrc, defined at compile time, is /etc/cgitrc. At
23runtime, cgit will consult the environment variable CGIT_CONFIG and, if 23runtime, cgit will consult the environment variable CGIT_CONFIG and, if
24defined, use its value instead. 24defined, use its value instead.
25 25
26 26
27GLOBAL SETTINGS 27GLOBAL SETTINGS
28--------------- 28---------------
29about-filter:: 29about-filter::
30 Specifies a command which will be invoked to format the content of 30 Specifies a command which will be invoked to format the content of
31 about pages (both top-level and for each repository). The command will 31 about pages (both top-level and for each repository). The command will
32 get the content of the about-file on its STDIN, and the STDOUT from the 32 get the content of the about-file on its STDIN, and the STDOUT from the
33 command will be included verbatim on the about page. Default value: 33 command will be included verbatim on the about page. Default value:
34 none. 34 none.
35 35
36agefile:: 36agefile::
37 Specifies a path, relative to each repository path, which can be used 37 Specifies a path, relative to each repository path, which can be used
38 to specify the date and time of the youngest commit in the repository. 38 to specify the date and time of the youngest commit in the repository.
39 The first line in the file is used as input to the "parse_date" 39 The first line in the file is used as input to the "parse_date"
40 function in libgit. Recommended timestamp-format is "yyyy-mm-dd 40 function in libgit. Recommended timestamp-format is "yyyy-mm-dd
41 hh:mm:ss". Default value: "info/web/last-modified". 41 hh:mm:ss". Default value: "info/web/last-modified".
42 42
43cache-root:: 43cache-root::
44 Path used to store the cgit cache entries. Default value: 44 Path used to store the cgit cache entries. Default value:
45 "/var/cache/cgit". 45 "/var/cache/cgit".
46 46
47cache-dynamic-ttl:: 47cache-dynamic-ttl::
48 Number which specifies the time-to-live, in minutes, for the cached 48 Number which specifies the time-to-live, in minutes, for the cached
49 version of repository pages accessed without a fixed SHA1. Default 49 version of repository pages accessed without a fixed SHA1. Default
50 value: "5". 50 value: "5".
51 51
52cache-repo-ttl:: 52cache-repo-ttl::
53 Number which specifies the time-to-live, in minutes, for the cached 53 Number which specifies the time-to-live, in minutes, for the cached
54 version of the repository summary page. Default value: "5". 54 version of the repository summary page. Default value: "5".
55 55
56cache-root-ttl:: 56cache-root-ttl::
57 Number which specifies the time-to-live, in minutes, for the cached 57 Number which specifies the time-to-live, in minutes, for the cached
58 version of the repository index page. Default value: "5". 58 version of the repository index page. Default value: "5".
59 59
60cache-scanrc-ttl:: 60cache-scanrc-ttl::
61 Number which specifies the time-to-live, in minutes, for the result 61 Number which specifies the time-to-live, in minutes, for the result
62 of scanning a path for git repositories. Default value: "15". 62 of scanning a path for git repositories. Default value: "15".
63 63
64cache-size:: 64cache-size::
65 The maximum number of entries in the cgit cache. Default value: "0" 65 The maximum number of entries in the cgit cache. Default value: "0"
66 (i.e. caching is disabled). 66 (i.e. caching is disabled).
67 67
68cache-static-ttl:: 68cache-static-ttl::
69 Number which specifies the time-to-live, in minutes, for the cached 69 Number which specifies the time-to-live, in minutes, for the cached
70 version of repository pages accessed with a fixed SHA1. Default value: 70 version of repository pages accessed with a fixed SHA1. Default value:
71 "5". 71 "5".
72 72
73clone-prefix:: 73clone-prefix::
74 Space-separated list of common prefixes which, when combined with a 74 Space-separated list of common prefixes which, when combined with a
75 repository url, generates valid clone urls for the repository. This 75 repository url, generates valid clone urls for the repository. This
76 setting is only used if `repo.clone-url` is unspecified. Default value: 76 setting is only used if `repo.clone-url` is unspecified. Default value:
77 none. 77 none.
78 78
79commit-filter:: 79commit-filter::
80 Specifies a command which will be invoked to format commit messages. 80 Specifies a command which will be invoked to format commit messages.
81 The command will get the message on its STDIN, and the STDOUT from the 81 The command will get the message on its STDIN, and the STDOUT from the
82 command will be included verbatim as the commit message, i.e. this can 82 command will be included verbatim as the commit message, i.e. this can
83 be used to implement bugtracker integration. Default value: none. 83 be used to implement bugtracker integration. Default value: none.
84 84
85css:: 85css::
86 Url which specifies the css document to include in all cgit pages. 86 Url which specifies the css document to include in all cgit pages.
87 Default value: "/cgit.css". 87 Default value: "/cgit.css".
88 88
89embedded:: 89embedded::
90 Flag which, when set to "1", will make cgit generate a html fragment 90 Flag which, when set to "1", will make cgit generate a html fragment
91 suitable for embedding in other html pages. Default value: none. See 91 suitable for embedding in other html pages. Default value: none. See
92 also: "noheader". 92 also: "noheader".
93 93
94enable-filter-overrides:: 94enable-filter-overrides::
95 Flag which, when set to "1", allows all filter settings to be 95 Flag which, when set to "1", allows all filter settings to be
96 overridden in repository-specific cgitrc files. Default value: none. 96 overridden in repository-specific cgitrc files. Default value: none.
97 97
98enable-index-links:: 98enable-index-links::
99 Flag which, when set to "1", will make cgit generate extra links for 99 Flag which, when set to "1", will make cgit generate extra links for
100 each repo in the repository index (specifically, to the "summary", 100 each repo in the repository index (specifically, to the "summary",
101 "commit" and "tree" pages). Default value: "0". 101 "commit" and "tree" pages). Default value: "0".
102 102
103enable-log-filecount:: 103enable-log-filecount::
104 Flag which, when set to "1", will make cgit print the number of 104 Flag which, when set to "1", will make cgit print the number of
105 modified files for each commit on the repository log page. Default 105 modified files for each commit on the repository log page. Default
106 value: "0". 106 value: "0".
107 107
108enable-log-linecount:: 108enable-log-linecount::
109 Flag which, when set to "1", will make cgit print the number of added 109 Flag which, when set to "1", will make cgit print the number of added
110 and removed lines for each commit on the repository log page. Default 110 and removed lines for each commit on the repository log page. Default
111 value: "0". 111 value: "0".
112 112
113enable-remote-branches:: 113enable-remote-branches::
114 Flag which, when set to "1", will make cgit display remote branches 114 Flag which, when set to "1", will make cgit display remote branches
115 in the summary and refs views. Default value: "0". See also: 115 in the summary and refs views. Default value: "0". See also:
116 "repo.enable-remote-branches". 116 "repo.enable-remote-branches".
117 117
118enable-subject-links:: 118enable-subject-links::
119 Flag which, when set to "1", will make cgit use the subject of the 119 Flag which, when set to "1", will make cgit use the subject of the
120 parent commit as link text when generating links to parent commits 120 parent commit as link text when generating links to parent commits
121 in commit view. Default value: "0". See also: 121 in commit view. Default value: "0". See also:
122 "repo.enable-subject-links". 122 "repo.enable-subject-links".
123 123
124enable-tree-linenumbers:: 124enable-tree-linenumbers::
125 Flag which, when set to "1", will make cgit generate linenumber links 125 Flag which, when set to "1", will make cgit generate linenumber links
126 for plaintext blobs printed in the tree view. Default value: "1". 126 for plaintext blobs printed in the tree view. Default value: "1".
127 127
128favicon:: 128favicon::
129 Url used as link to a shortcut icon for cgit. If specified, it is 129 Url used as link to a shortcut icon for cgit. If specified, it is
130 suggested to use the value "/favicon.ico" since certain browsers will 130 suggested to use the value "/favicon.ico" since certain browsers will
131 ignore other values. Default value: none. 131 ignore other values. Default value: none.
132 132
133footer:: 133footer::
134 The content of the file specified with this option will be included 134 The content of the file specified with this option will be included
135 verbatim at the bottom of all pages (i.e. it replaces the standard 135 verbatim at the bottom of all pages (i.e. it replaces the standard
136 "generated by..." message. Default value: none. 136 "generated by..." message. Default value: none.
137 137
138head-include:: 138head-include::
139 The content of the file specified with this option will be included 139 The content of the file specified with this option will be included
140 verbatim in the html HEAD section on all pages. Default value: none. 140 verbatim in the html HEAD section on all pages. Default value: none.
141 141
142header:: 142header::
143 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
144 verbatim at the top of all pages. Default value: none. 144 verbatim at the top of all pages. Default value: none.
145 145
146include:: 146include::
147 Name of a configfile to include before the rest of the current config- 147 Name of a configfile to include before the rest of the current config-
148 file is parsed. Default value: none. 148 file is parsed. Default value: none.
149 149
150index-header:: 150index-header::
151 The content of the file specified with this option will be included 151 The content of the file specified with this option will be included
152 verbatim above the repository index. This setting is deprecated, and 152 verbatim above the repository index. This setting is deprecated, and
153 will not be supported by cgit-1.0 (use root-readme instead). Default 153 will not be supported by cgit-1.0 (use root-readme instead). Default
154 value: none. 154 value: none.
155 155
156index-info:: 156index-info::
157 The content of the file specified with this option will be included 157 The content of the file specified with this option will be included
158 verbatim below the heading on the repository index page. This setting 158 verbatim below the heading on the repository index page. This setting
159 is deprecated, and will not be supported by cgit-1.0 (use root-desc 159 is deprecated, and will not be supported by cgit-1.0 (use root-desc
160 instead). Default value: none. 160 instead). Default value: none.
161 161
162local-time:: 162local-time::
163 Flag which, if set to "1", makes cgit print commit and tag times in the 163 Flag which, if set to "1", makes cgit print commit and tag times in the
164 servers timezone. Default value: "0". 164 servers timezone. Default value: "0".
165 165
166logo:: 166logo::
167 Url which specifies the source of an image which will be used as a logo 167 Url which specifies the source of an image which will be used as a logo
168 on all cgit pages. Default value: "/cgit.png". 168 on all cgit pages. Default value: "/cgit.png".
169 169
170logo-link:: 170logo-link::
171 Url loaded when clicking on the cgit logo image. If unspecified the 171 Url loaded when clicking on the cgit logo image. If unspecified the
172 calculated url of the repository index page will be used. Default 172 calculated url of the repository index page will be used. Default
173 value: none. 173 value: none.
174 174
175max-atom-items:: 175max-atom-items::
176 Specifies the number of items to display in atom feeds view. Default 176 Specifies the number of items to display in atom feeds view. Default
177 value: "10". 177 value: "10".
178 178
179max-commit-count:: 179max-commit-count::
180 Specifies the number of entries to list per page in "log" view. Default 180 Specifies the number of entries to list per page in "log" view. Default
181 value: "50". 181 value: "50".
182 182
183max-message-length:: 183max-message-length::
184 Specifies the maximum number of commit message characters to display in 184 Specifies the maximum number of commit message characters to display in
185 "log" view. Default value: "80". 185 "log" view. Default value: "80".
186 186
187max-repo-count:: 187max-repo-count::
188 Specifies the number of entries to list per page on therepository 188 Specifies the number of entries to list per page on therepository
189 index page. Default value: "50". 189 index page. Default value: "50".
190 190
191max-repodesc-length:: 191max-repodesc-length::
192 Specifies the maximum number of repo description characters to display 192 Specifies the maximum number of repo description characters to display
193 on the repository index page. Default value: "80". 193 on the repository index page. Default value: "80".
194 194
195max-blob-size:: 195max-blob-size::
196 Specifies the maximum size of a blob to display HTML for in KBytes. 196 Specifies the maximum size of a blob to display HTML for in KBytes.
197 Default value: "0" (limit disabled). 197 Default value: "0" (limit disabled).
198 198
199max-stats:: 199max-stats::
200 Set the default maximum statistics period. Valid values are "week", 200 Set the default maximum statistics period. Valid values are "week",
201 "month", "quarter" and "year". If unspecified, statistics are 201 "month", "quarter" and "year". If unspecified, statistics are
202 disabled. Default value: none. See also: "repo.max-stats". 202 disabled. Default value: none. See also: "repo.max-stats".
203 203
204mimetype.<ext>:: 204mimetype.<ext>::
205 Set the mimetype for the specified filename extension. This is used 205 Set the mimetype for the specified filename extension. This is used
206 by the `plain` command when returning blob content. 206 by the `plain` command when returning blob content.
207 207
208module-link:: 208module-link::
209 Text which will be used as the formatstring for a hyperlink when a 209 Text which will be used as the formatstring for a hyperlink when a
210 submodule is printed in a directory listing. The arguments for the 210 submodule is printed in a directory listing. The arguments for the
211 formatstring are the path and SHA1 of the submodule commit. Default 211 formatstring are the path and SHA1 of the submodule commit. Default
212 value: "./?repo=%s&page=commit&id=%s" 212 value: "./?repo=%s&page=commit&id=%s"
213 213
214nocache:: 214nocache::
215 If set to the value "1" caching will be disabled. This settings is 215 If set to the value "1" caching will be disabled. This settings is
216 deprecated, and will not be honored starting with cgit-1.0. Default 216 deprecated, and will not be honored starting with cgit-1.0. Default
217 value: "0". 217 value: "0".
218 218
219noplainemail:: 219noplainemail::
220 If set to "1" showing full author email adresses will be disabled. 220 If set to "1" showing full author email adresses will be disabled.
221 Default value: "0". 221 Default value: "0".
222 222
223noheader:: 223noheader::
224 Flag which, when set to "1", will make cgit omit the standard header 224 Flag which, when set to "1", will make cgit omit the standard header
225 on all pages. Default value: none. See also: "embedded". 225 on all pages. Default value: none. See also: "embedded".
226 226
227project-list::
228 A list of subdirectories inside of scan-path, relative to it, that
229 should loaded as git repositories. This must be defined prior to
230 scan-path. Default value: none. See also: scan-path.
231
227renamelimit:: 232renamelimit::
228 Maximum number of files to consider when detecting renames. The value 233 Maximum number of files to consider when detecting renames. The value
229 "-1" uses the compiletime value in git (for further info, look at 234 "-1" uses the compiletime value in git (for further info, look at
230 `man git-diff`). Default value: "-1". 235 `man git-diff`). Default value: "-1".
231 236
232repo.group:: 237repo.group::
233 Legacy alias for "section". This option is deprecated and will not be 238 Legacy alias for "section". This option is deprecated and will not be
234 supported in cgit-1.0. 239 supported in cgit-1.0.
235 240
236robots:: 241robots::
237 Text used as content for the "robots" meta-tag. Default value: 242 Text used as content for the "robots" meta-tag. Default value:
238 "index, nofollow". 243 "index, nofollow".
239 244
240root-desc:: 245root-desc::
241 Text printed below the heading on the repository index page. Default 246 Text printed below the heading on the repository index page. Default
242 value: "a fast webinterface for the git dscm". 247 value: "a fast webinterface for the git dscm".
243 248
244root-readme:: 249root-readme::
245 The content of the file specified with this option will be included 250 The content of the file specified with this option will be included
246 verbatim below the "about" link on the repository index page. Default 251 verbatim below the "about" link on the repository index page. Default
247 value: none. 252 value: none.
248 253
249root-title:: 254root-title::
250 Text printed as heading on the repository index page. Default value: 255 Text printed as heading on the repository index page. Default value:
251 "Git Repository Browser". 256 "Git Repository Browser".
252 257
253scan-path:: 258scan-path::
254 A path which will be scanned for repositories. If caching is enabled, 259 A path which will be scanned for repositories. If caching is enabled,
255 the result will be cached as a cgitrc include-file in the cache 260 the result will be cached as a cgitrc include-file in the cache
256 directory. Default value: none. See also: cache-scanrc-ttl. 261 directory. If project-list has been defined prior to scan-path,
262 scan-path loads only the directories listed in the file pointed to by
263 project-list. Default value: none. See also: cache-scanrc-ttl,
264 project-list.
257 265
258section:: 266section::
259 The name of the current repository section - all repositories defined 267 The name of the current repository section - all repositories defined
260 after this option will inherit the current section name. Default value: 268 after this option will inherit the current section name. Default value:
261 none. 269 none.
262 270
263side-by-side-diffs:: 271side-by-side-diffs::
264 If set to "1" shows side-by-side diffs instead of unidiffs per 272 If set to "1" shows side-by-side diffs instead of unidiffs per
265 default. Default value: "0". 273 default. Default value: "0".
266 274
267snapshots:: 275snapshots::
268 Text which specifies the default set of snapshot formats generated by 276 Text which specifies the default set of snapshot formats generated by
269 cgit. The value is a space-separated list of zero or more of the 277 cgit. The value is a space-separated list of zero or more of the
270 values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none. 278 values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none.
271 279
272source-filter:: 280source-filter::
273 Specifies a command which will be invoked to format plaintext blobs 281 Specifies a command which will be invoked to format plaintext blobs
274 in the tree view. The command will get the blob content on its STDIN 282 in the tree view. The command will get the blob content on its STDIN
275 and the name of the blob as its only command line argument. The STDOUT 283 and the name of the blob as its only command line argument. The STDOUT
276 from the command will be included verbatim as the blob contents, i.e. 284 from the command will be included verbatim as the blob contents, i.e.
277 this can be used to implement e.g. syntax highlighting. Default value: 285 this can be used to implement e.g. syntax highlighting. Default value:
278 none. 286 none.
279 287
280summary-branches:: 288summary-branches::
281 Specifies the number of branches to display in the repository "summary" 289 Specifies the number of branches to display in the repository "summary"
282 view. Default value: "10". 290 view. Default value: "10".
283 291
284summary-log:: 292summary-log::
285 Specifies the number of log entries to display in the repository 293 Specifies the number of log entries to display in the repository
286 "summary" view. Default value: "10". 294 "summary" view. Default value: "10".
287 295
288summary-tags:: 296summary-tags::
289 Specifies the number of tags to display in the repository "summary" 297 Specifies the number of tags to display in the repository "summary"
290 view. Default value: "10". 298 view. Default value: "10".
291 299
292virtual-root:: 300virtual-root::
293 Url which, if specified, will be used as root for all cgit links. It 301 Url which, if specified, will be used as root for all cgit links. It
294 will also cause cgit to generate 'virtual urls', i.e. urls like 302 will also cause cgit to generate 'virtual urls', i.e. urls like
295 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default 303 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default
296 value: none. 304 value: none.
297 NOTE: cgit has recently learned how to use PATH_INFO to achieve the 305 NOTE: cgit has recently learned how to use PATH_INFO to achieve the
298 same kind of virtual urls, so this option will probably be deprecated. 306 same kind of virtual urls, so this option will probably be deprecated.
299 307
300REPOSITORY SETTINGS 308REPOSITORY SETTINGS
301------------------- 309-------------------
302repo.about-filter:: 310repo.about-filter::
303 Override the default about-filter. Default value: none. See also: 311 Override the default about-filter. Default value: none. See also:
304 "enable-filter-overrides". 312 "enable-filter-overrides".
305 313
306repo.clone-url:: 314repo.clone-url::
307 A list of space-separated urls which can be used to clone this repo. 315 A list of space-separated urls which can be used to clone this repo.
308 Default value: none. 316 Default value: none.
309 317
310repo.commit-filter:: 318repo.commit-filter::
311 Override the default commit-filter. Default value: none. See also: 319 Override the default commit-filter. Default value: none. See also:
312 "enable-filter-overrides". 320 "enable-filter-overrides".
313 321
314repo.defbranch:: 322repo.defbranch::
315 The name of the default branch for this repository. If no such branch 323 The name of the default branch for this repository. If no such branch
316 exists in the repository, the first branch name (when sorted) is used 324 exists in the repository, the first branch name (when sorted) is used
317 as default instead. Default value: "master". 325 as default instead. Default value: "master".
318 326
319repo.desc:: 327repo.desc::
320 The value to show as repository description. Default value: none. 328 The value to show as repository description. Default value: none.
321 329
322repo.enable-log-filecount:: 330repo.enable-log-filecount::
323 A flag which can be used to disable the global setting 331 A flag which can be used to disable the global setting
324 `enable-log-filecount'. Default value: none. 332 `enable-log-filecount'. Default value: none.
325 333
326repo.enable-log-linecount:: 334repo.enable-log-linecount::
327 A flag which can be used to disable the global setting 335 A flag which can be used to disable the global setting
328 `enable-log-linecount'. Default value: none. 336 `enable-log-linecount'. Default value: none.
329 337
330repo.enable-remote-branches:: 338repo.enable-remote-branches::
331 Flag which, when set to "1", will make cgit display remote branches 339 Flag which, when set to "1", will make cgit display remote branches
332 in the summary and refs views. Default value: <enable-remote-branches>. 340 in the summary and refs views. Default value: <enable-remote-branches>.
333 341
334repo.enable-subject-links:: 342repo.enable-subject-links::
335 A flag which can be used to override the global setting 343 A flag which can be used to override the global setting
336 `enable-subject-links'. Default value: none. 344 `enable-subject-links'. Default value: none.
337 345
338repo.max-stats:: 346repo.max-stats::
339 Override the default maximum statistics period. Valid values are equal 347 Override the default maximum statistics period. Valid values are equal
340 to the values specified for the global "max-stats" setting. Default 348 to the values specified for the global "max-stats" setting. Default
341 value: none. 349 value: none.
342 350
343repo.name:: 351repo.name::
344 The value to show as repository name. Default value: <repo.url>. 352 The value to show as repository name. Default value: <repo.url>.
345 353
346repo.owner:: 354repo.owner::
347 A value used to identify the owner of the repository. Default value: 355 A value used to identify the owner of the repository. Default value:
348 none. 356 none.
349 357
350repo.path:: 358repo.path::
351 An absolute path to the repository directory. For non-bare repositories 359 An absolute path to the repository directory. For non-bare repositories
352 this is the .git-directory. Default value: none. 360 this is the .git-directory. Default value: none.
353 361
354repo.readme:: 362repo.readme::
355 A path (relative to <repo.path>) which specifies a file to include 363 A path (relative to <repo.path>) which specifies a file to include
356 verbatim as the "About" page for this repo. Default value: none. 364 verbatim as the "About" page for this repo. Default value: none.
357 365
358repo.snapshots:: 366repo.snapshots::
359 A mask of allowed snapshot-formats for this repo, restricted by the 367 A mask of allowed snapshot-formats for this repo, restricted by the
360 "snapshots" global setting. Default value: <snapshots>. 368 "snapshots" global setting. Default value: <snapshots>.
361 369
362repo.section:: 370repo.section::
363 Override the current section name for this repository. Default value: 371 Override the current section name for this repository. Default value:
364 none. 372 none.
365 373
366repo.source-filter:: 374repo.source-filter::
367 Override the default source-filter. Default value: none. See also: 375 Override the default source-filter. Default value: none. See also:
368 "enable-filter-overrides". 376 "enable-filter-overrides".
369 377
370repo.url:: 378repo.url::
371 The relative url used to access the repository. This must be the first 379 The relative url used to access the repository. This must be the first
372 setting specified for each repo. Default value: none. 380 setting specified for each repo. Default value: none.
373 381
374 382
375REPOSITORY-SPECIFIC CGITRC FILE 383REPOSITORY-SPECIFIC CGITRC FILE
376------------------------------- 384-------------------------------
377When the option "scan-path" is used to auto-discover git repositories, cgit 385When the option "scan-path" is used to auto-discover git repositories, cgit
378will try to parse the file "cgitrc" within any found repository. Such a 386will try to parse the file "cgitrc" within any found repository. Such a
379repo-specific config file may contain any of the repo-specific options 387repo-specific config file may contain any of the repo-specific options
380described above, except "repo.url" and "repo.path". Additionally, the "filter" 388described above, except "repo.url" and "repo.path". Additionally, the "filter"
381options are only acknowledged in repo-specific config files when 389options are only acknowledged in repo-specific config files when
382"enable-filter-overrides" is set to "1". 390"enable-filter-overrides" is set to "1".
383 391
384Note: the "repo." prefix is dropped from the option names in repo-specific 392Note: the "repo." prefix is dropped from the option names in repo-specific
385config files, e.g. "repo.desc" becomes "desc". 393config files, e.g. "repo.desc" becomes "desc".
386 394
387 395
388EXAMPLE CGITRC FILE 396EXAMPLE CGITRC FILE
389------------------- 397-------------------
390 398
391.... 399....
392# Enable caching of up to 1000 output entriess 400# Enable caching of up to 1000 output entriess
393cache-size=1000 401cache-size=1000
394 402
395 403
396# Specify some default clone prefixes 404# Specify some default clone prefixes
397clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git 405clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git
398 406
399# Specify the css url 407# Specify the css url
400css=/css/cgit.css 408css=/css/cgit.css
401 409
402 410
403# Show extra links for each repository on the index page 411# Show extra links for each repository on the index page
404enable-index-links=1 412enable-index-links=1
405 413
406 414
407# Show number of affected files per commit on the log pages 415# Show number of affected files per commit on the log pages
408enable-log-filecount=1 416enable-log-filecount=1
409 417
410 418
411# Show number of added/removed lines per commit on the log pages 419# Show number of added/removed lines per commit on the log pages
412enable-log-linecount=1 420enable-log-linecount=1
413 421
414 422
415# Add a cgit favicon 423# Add a cgit favicon
416favicon=/favicon.ico 424favicon=/favicon.ico
417 425
418 426
419# Use a custom logo 427# Use a custom logo
420logo=/img/mylogo.png 428logo=/img/mylogo.png
421 429
422 430
423# Enable statistics per week, month and quarter 431# Enable statistics per week, month and quarter
424max-stats=quarter 432max-stats=quarter
425 433
426 434
427# Set the title and heading of the repository index page 435# Set the title and heading of the repository index page
428root-title=foobar.com git repositories 436root-title=foobar.com git repositories
429 437
430 438
431# Set a subheading for the repository index page 439# Set a subheading for the repository index page
432root-desc=tracking the foobar development 440root-desc=tracking the foobar development
433 441
434 442
435# Include some more info about foobar.com on the index page 443# Include some more info about foobar.com on the index page
436root-readme=/var/www/htdocs/about.html 444root-readme=/var/www/htdocs/about.html
437 445
438 446
439# Allow download of tar.gz, tar.bz2 and zip-files 447# Allow download of tar.gz, tar.bz2 and zip-files
440snapshots=tar.gz tar.bz2 zip 448snapshots=tar.gz tar.bz2 zip
441 449
442 450
443## 451##
444## List of common mimetypes 452## List of common mimetypes
445## 453##
446 454
447mimetype.gif=image/gif 455mimetype.gif=image/gif
448mimetype.html=text/html 456mimetype.html=text/html
449mimetype.jpg=image/jpeg 457mimetype.jpg=image/jpeg
450mimetype.jpeg=image/jpeg 458mimetype.jpeg=image/jpeg
451mimetype.pdf=application/pdf 459mimetype.pdf=application/pdf
452mimetype.png=image/png 460mimetype.png=image/png
453mimetype.svg=image/svg+xml 461mimetype.svg=image/svg+xml
454 462
455 463
456## 464##
457## List of repositories. 465## List of repositories.
458## PS: Any repositories listed when section is unset will not be 466## PS: Any repositories listed when section is unset will not be
459## displayed under a section heading 467## displayed under a section heading
460## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos') 468## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos')
461## and included like this: 469## and included like this:
462## include=/etc/cgitrepos 470## include=/etc/cgitrepos
463## 471##
464 472
465 473
466repo.url=foo 474repo.url=foo
467repo.path=/pub/git/foo.git 475repo.path=/pub/git/foo.git
468repo.desc=the master foo repository 476repo.desc=the master foo repository
469repo.owner=fooman@foobar.com 477repo.owner=fooman@foobar.com
470repo.readme=info/web/about.html 478repo.readme=info/web/about.html
471 479
472 480
473repo.url=bar 481repo.url=bar
474repo.path=/pub/git/bar.git 482repo.path=/pub/git/bar.git
475repo.desc=the bars for your foo 483repo.desc=the bars for your foo
476repo.owner=barman@foobar.com 484repo.owner=barman@foobar.com
477repo.readme=info/web/about.html 485repo.readme=info/web/about.html
478 486
479 487
480# The next repositories will be displayed under the 'extras' heading 488# The next repositories will be displayed under the 'extras' heading
481section=extras 489section=extras
482 490
483 491
484repo.url=baz 492repo.url=baz
485repo.path=/pub/git/baz.git 493repo.path=/pub/git/baz.git
486repo.desc=a set of extensions for bar users 494repo.desc=a set of extensions for bar users
487 495
488repo.url=wiz 496repo.url=wiz
489repo.path=/pub/git/wiz.git 497repo.path=/pub/git/wiz.git
490repo.desc=the wizard of foo 498repo.desc=the wizard of foo
491 499
492 500
493# Add some mirrored repositories 501# Add some mirrored repositories
494section=mirrors 502section=mirrors
495 503
496 504
497repo.url=git 505repo.url=git
498repo.path=/pub/git/git.git 506repo.path=/pub/git/git.git
499repo.desc=the dscm 507repo.desc=the dscm
500 508
501 509
502repo.url=linux 510repo.url=linux
503repo.path=/pub/git/linux.git 511repo.path=/pub/git/linux.git
504repo.desc=the kernel 512repo.desc=the kernel
505 513
506# Disable adhoc downloads of this repo 514# Disable adhoc downloads of this repo
507repo.snapshots=0 515repo.snapshots=0
508 516
509# Disable line-counts for this repo 517# Disable line-counts for this repo
510repo.enable-log-linecount=0 518repo.enable-log-linecount=0
511 519
512# Restrict the max statistics period for this repo 520# Restrict the max statistics period for this repo
513repo.max-stats=month 521repo.max-stats=month
514.... 522....
515 523
516 524
517BUGS 525BUGS
518---- 526----
519Comments currently cannot appear on the same line as a setting; the comment 527Comments currently cannot appear on the same line as a setting; the comment
520will be included as part of the value. E.g. this line: 528will be included as part of the value. E.g. this line:
521 529
522 robots=index # allow indexing 530 robots=index # allow indexing
523 531
524will generate the following html element: 532will generate the following html element:
525 533
526 <meta name='robots' content='index # allow indexing'/> 534 <meta name='robots' content='index # allow indexing'/>
527 535
528 536
529 537
530AUTHOR 538AUTHOR
531------ 539------
532Lars Hjemli <hjemli@gmail.com> 540Lars Hjemli <hjemli@gmail.com>
diff --git a/scan-tree.c b/scan-tree.c
index 1e18f3c..9bf9b38 100644
--- a/scan-tree.c
+++ b/scan-tree.c
@@ -1,148 +1,185 @@
1/* scan-tree.c
2 *
3 * Copyright (C) 2008-2009 Lars Hjemli
4 * Copyright (C) 2010 Jason A. Donenfeld <Jason@zx2c4.com>
5 *
6 * Licensed under GNU General Public License v2
7 * (see COPYING for full license text)
8 */
9
1#include "cgit.h" 10#include "cgit.h"
2#include "configfile.h" 11#include "configfile.h"
3#include "html.h" 12#include "html.h"
4 13
5#define MAX_PATH 4096 14#define MAX_PATH 4096
6 15
7/* return 1 if path contains a objects/ directory and a HEAD file */ 16/* return 1 if path contains a objects/ directory and a HEAD file */
8static int is_git_dir(const char *path) 17static int is_git_dir(const char *path)
9{ 18{
10 struct stat st; 19 struct stat st;
11 static char buf[MAX_PATH]; 20 static char buf[MAX_PATH];
12 21
13 if (snprintf(buf, MAX_PATH, "%s/objects", path) >= MAX_PATH) { 22 if (snprintf(buf, MAX_PATH, "%s/objects", path) >= MAX_PATH) {
14 fprintf(stderr, "Insanely long path: %s\n", path); 23 fprintf(stderr, "Insanely long path: %s\n", path);
15 return 0; 24 return 0;
16 } 25 }
17 if (stat(buf, &st)) { 26 if (stat(buf, &st)) {
18 if (errno != ENOENT) 27 if (errno != ENOENT)
19 fprintf(stderr, "Error checking path %s: %s (%d)\n", 28 fprintf(stderr, "Error checking path %s: %s (%d)\n",
20 path, strerror(errno), errno); 29 path, strerror(errno), errno);
21 return 0; 30 return 0;
22 } 31 }
23 if (!S_ISDIR(st.st_mode)) 32 if (!S_ISDIR(st.st_mode))
24 return 0; 33 return 0;
25 34
26 sprintf(buf, "%s/HEAD", path); 35 sprintf(buf, "%s/HEAD", path);
27 if (stat(buf, &st)) { 36 if (stat(buf, &st)) {
28 if (errno != ENOENT) 37 if (errno != ENOENT)
29 fprintf(stderr, "Error checking path %s: %s (%d)\n", 38 fprintf(stderr, "Error checking path %s: %s (%d)\n",
30 path, strerror(errno), errno); 39 path, strerror(errno), errno);
31 return 0; 40 return 0;
32 } 41 }
33 if (!S_ISREG(st.st_mode)) 42 if (!S_ISREG(st.st_mode))
34 return 0; 43 return 0;
35 44
36 return 1; 45 return 1;
37} 46}
38 47
39struct cgit_repo *repo; 48struct cgit_repo *repo;
40repo_config_fn config_fn; 49repo_config_fn config_fn;
41 50
42static void repo_config(const char *name, const char *value) 51static void repo_config(const char *name, const char *value)
43{ 52{
44 config_fn(repo, name, value); 53 config_fn(repo, name, value);
45} 54}
46 55
47static void add_repo(const char *base, const char *path, repo_config_fn fn) 56static void add_repo(const char *base, const char *path, repo_config_fn fn)
48{ 57{
49 struct stat st; 58 struct stat st;
50 struct passwd *pwd; 59 struct passwd *pwd;
51 char *p; 60 char *p;
52 size_t size; 61 size_t size;
53 62
54 if (stat(path, &st)) { 63 if (stat(path, &st)) {
55 fprintf(stderr, "Error accessing %s: %s (%d)\n", 64 fprintf(stderr, "Error accessing %s: %s (%d)\n",
56 path, strerror(errno), errno); 65 path, strerror(errno), errno);
57 return; 66 return;
58 } 67 }
59 if (!stat(fmt("%s/noweb", path), &st)) 68 if (!stat(fmt("%s/noweb", path), &st))
60 return; 69 return;
61 if ((pwd = getpwuid(st.st_uid)) == NULL) { 70 if ((pwd = getpwuid(st.st_uid)) == NULL) {
62 fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n", 71 fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n",
63 path, strerror(errno), errno); 72 path, strerror(errno), errno);
64 return; 73 return;
65 } 74 }
66 if (base == path) 75 if (base == path)
67 p = fmt("%s", path); 76 p = fmt("%s", path);
68 else 77 else
69 p = fmt("%s", path + strlen(base) + 1); 78 p = fmt("%s", path + strlen(base) + 1);
70 79
71 if (!strcmp(p + strlen(p) - 5, "/.git")) 80 if (!strcmp(p + strlen(p) - 5, "/.git"))
72 p[strlen(p) - 5] = '\0'; 81 p[strlen(p) - 5] = '\0';
73 82
74 repo = cgit_add_repo(xstrdup(p)); 83 repo = cgit_add_repo(xstrdup(p));
75 repo->name = repo->url; 84 repo->name = repo->url;
76 repo->path = xstrdup(path); 85 repo->path = xstrdup(path);
77 p = (pwd && pwd->pw_gecos) ? strchr(pwd->pw_gecos, ',') : NULL; 86 p = (pwd && pwd->pw_gecos) ? strchr(pwd->pw_gecos, ',') : NULL;
78 if (p) 87 if (p)
79 *p = '\0'; 88 *p = '\0';
80 repo->owner = (pwd ? xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name) : ""); 89 repo->owner = (pwd ? xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name) : "");
81 90
82 p = fmt("%s/description", path); 91 p = fmt("%s/description", path);
83 if (!stat(p, &st)) 92 if (!stat(p, &st))
84 readfile(p, &repo->desc, &size); 93 readfile(p, &repo->desc, &size);
85 94
86 p = fmt("%s/README.html", path); 95 p = fmt("%s/README.html", path);
87 if (!stat(p, &st)) 96 if (!stat(p, &st))
88 repo->readme = "README.html"; 97 repo->readme = "README.html";
89 98
90 p = fmt("%s/cgitrc", path); 99 p = fmt("%s/cgitrc", path);
91 if (!stat(p, &st)) { 100 if (!stat(p, &st)) {
92 config_fn = fn; 101 config_fn = fn;
93 parse_configfile(xstrdup(p), &repo_config); 102 parse_configfile(xstrdup(p), &repo_config);
94 } 103 }
95} 104}
96 105
97static void scan_path(const char *base, const char *path, repo_config_fn fn) 106static void scan_path(const char *base, const char *path, repo_config_fn fn)
98{ 107{
99 DIR *dir; 108 DIR *dir;
100 struct dirent *ent; 109 struct dirent *ent;
101 char *buf; 110 char *buf;
102 struct stat st; 111 struct stat st;
103 112
104 if (is_git_dir(path)) { 113 if (is_git_dir(path)) {
105 add_repo(base, path, fn); 114 add_repo(base, path, fn);
106 return; 115 return;
107 } 116 }
108 if (is_git_dir(fmt("%s/.git", path))) { 117 if (is_git_dir(fmt("%s/.git", path))) {
109 add_repo(base, fmt("%s/.git", path), fn); 118 add_repo(base, fmt("%s/.git", path), fn);
110 return; 119 return;
111 } 120 }
112 dir = opendir(path); 121 dir = opendir(path);
113 if (!dir) { 122 if (!dir) {
114 fprintf(stderr, "Error opening directory %s: %s (%d)\n", 123 fprintf(stderr, "Error opening directory %s: %s (%d)\n",
115 path, strerror(errno), errno); 124 path, strerror(errno), errno);
116 return; 125 return;
117 } 126 }
118 while((ent = readdir(dir)) != NULL) { 127 while((ent = readdir(dir)) != NULL) {
119 if (ent->d_name[0] == '.') { 128 if (ent->d_name[0] == '.') {
120 if (ent->d_name[1] == '\0') 129 if (ent->d_name[1] == '\0')
121 continue; 130 continue;
122 if (ent->d_name[1] == '.' && ent->d_name[2] == '\0') 131 if (ent->d_name[1] == '.' && ent->d_name[2] == '\0')
123 continue; 132 continue;
124 } 133 }
125 buf = malloc(strlen(path) + strlen(ent->d_name) + 2); 134 buf = malloc(strlen(path) + strlen(ent->d_name) + 2);
126 if (!buf) { 135 if (!buf) {
127 fprintf(stderr, "Alloc error on %s: %s (%d)\n", 136 fprintf(stderr, "Alloc error on %s: %s (%d)\n",
128 path, strerror(errno), errno); 137 path, strerror(errno), errno);
129 exit(1); 138 exit(1);
130 } 139 }
131 sprintf(buf, "%s/%s", path, ent->d_name); 140 sprintf(buf, "%s/%s", path, ent->d_name);
132 if (stat(buf, &st)) { 141 if (stat(buf, &st)) {
133 fprintf(stderr, "Error checking path %s: %s (%d)\n", 142 fprintf(stderr, "Error checking path %s: %s (%d)\n",
134 buf, strerror(errno), errno); 143 buf, strerror(errno), errno);
135 free(buf); 144 free(buf);
136 continue; 145 continue;
137 } 146 }
138 if (S_ISDIR(st.st_mode)) 147 if (S_ISDIR(st.st_mode))
139 scan_path(base, buf, fn); 148 scan_path(base, buf, fn);
140 free(buf); 149 free(buf);
141 } 150 }
142 closedir(dir); 151 closedir(dir);
143} 152}
144 153
154#define lastc(s) s[strlen(s) - 1]
155
156void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn)
157{
158 char line[MAX_PATH * 2], *z;
159 FILE *projects;
160 int err;
161
162 projects = fopen(projectsfile, "r");
163 if (!projects) {
164 fprintf(stderr, "Error opening projectsfile %s: %s (%d)\n",
165 projectsfile, strerror(errno), errno);
166 }
167 while (fgets(line, sizeof(line), projects) != NULL) {
168 for (z = &lastc(line);
169 strlen(line) && strchr("\n\r", *z);
170 z = &lastc(line))
171 *z = '\0';
172 if (strlen(line))
173 scan_path(path, fmt("%s/%s", path, line), fn);
174 }
175 if ((err = ferror(projects))) {
176 fprintf(stderr, "Error reading from projectsfile %s: %s (%d)\n",
177 projectsfile, strerror(err), err);
178 }
179 fclose(projects);
180}
181
145void scan_tree(const char *path, repo_config_fn fn) 182void scan_tree(const char *path, repo_config_fn fn)
146{ 183{
147 scan_path(path, path, fn); 184 scan_path(path, path, fn);
148} 185}
diff --git a/scan-tree.h b/scan-tree.h
index 11539f4..1afbd4b 100644
--- a/scan-tree.h
+++ b/scan-tree.h
@@ -1,3 +1,2 @@
1 1extern void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn);
2
3extern void scan_tree(const char *path, repo_config_fn fn); 2extern void scan_tree(const char *path, repo_config_fn fn);