1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
|
# cgit — Repository Discovery
## Overview
cgit discovers repositories through two mechanisms: explicit `repo.url=`
entries in the configuration file, and automatic filesystem scanning via
`scan-path`. The scan-tree subsystem recursively searches directories for
git repositories and auto-configures them.
Source files: `scan-tree.c`, `scan-tree.h`, `shared.c` (repository list management).
## Manual Repository Configuration
Repositories can be explicitly defined in the cgitrc file:
```ini
repo.url=myproject
repo.path=/srv/git/myproject.git
repo.desc=My project description
repo.owner=Alice
```
Each `repo.url=` triggers `cgit_add_repo()` in `shared.c`, which creates a
new `cgit_repo` entry in the global repository list.
### `cgit_add_repo()`
```c
struct cgit_repo *cgit_add_repo(const char *url)
{
struct cgit_repo *ret;
/* grow the repo array if needed */
if (cgit_repolist.count >= cgit_repolist.length) {
/* realloc with doubled capacity */
}
ret = &cgit_repolist.repos[cgit_repolist.count++];
/* initialize with defaults from ctx.cfg */
ret->url = xstrdup(url);
ret->name = ret->url;
ret->path = NULL;
ret->desc = cgit_default_repo_desc;
ret->owner = NULL;
ret->section = ctx.cfg.section;
ret->snapshots = ctx.cfg.snapshots;
/* ... inherit all global defaults ... */
return ret;
}
```
## Repository Lookup
```c
struct cgit_repo *cgit_get_repoinfo(const char *url)
{
int i;
for (i = 0; i < cgit_repolist.count; i++) {
if (!strcmp(cgit_repolist.repos[i].url, url))
return &cgit_repolist.repos[i];
}
return NULL;
}
```
This is a linear scan — adequate for typical installations with dozens to
hundreds of repositories.
## Filesystem Scanning: `scan-path`
The `scan-path` configuration directive triggers automatic repository
discovery. When encountered in the config file, `scan_tree()` or
`scan_projects()` is called immediately.
### `scan_tree()`
```c
void scan_tree(const char *path, repo_config_fn fn)
```
Recursively scans `path` for git repositories:
```c
static void scan_path(const char *base, const char *path, repo_config_fn fn)
{
DIR *dir;
struct dirent *ent;
dir = opendir(path);
if (!dir) return;
while ((ent = readdir(dir)) != NULL) {
/* skip "." and ".." */
/* skip hidden directories unless scan-hidden-path=1 */
if (is_git_dir(fullpath)) {
/* found a bare repository */
add_repo(base, fullpath, fn);
} else if (is_git_dir(fullpath + "/.git")) {
/* found a non-bare repository */
add_repo(base, fullpath + "/.git", fn);
} else {
/* recurse into subdirectory */
scan_path(base, fullpath, fn);
}
}
closedir(dir);
}
```
### Git Directory Detection: `is_git_dir()`
```c
static int is_git_dir(const char *path)
{
struct stat st;
struct strbuf pathbuf = STRBUF_INIT;
/* check for path/HEAD */
strbuf_addf(&pathbuf, "%s/HEAD", path);
if (stat(pathbuf.buf, &st)) {
strbuf_release(&pathbuf);
return 0;
}
/* check for path/objects */
strbuf_reset(&pathbuf);
strbuf_addf(&pathbuf, "%s/objects", path);
if (stat(pathbuf.buf, &st) || !S_ISDIR(st.st_mode)) {
strbuf_release(&pathbuf);
return 0;
}
/* check for path/refs */
strbuf_reset(&pathbuf);
strbuf_addf(&pathbuf, "%s/refs", path);
if (stat(pathbuf.buf, &st) || !S_ISDIR(st.st_mode)) {
strbuf_release(&pathbuf);
return 0;
}
strbuf_release(&pathbuf);
return 1;
}
```
A directory is considered a git repository if it contains `HEAD`, `objects/`,
and `refs/` subdirectories.
### Repository Registration: `add_repo()`
When a git directory is found, `add_repo()` creates a repository entry:
```c
static void add_repo(const char *base, const char *path, repo_config_fn fn)
{
/* derive URL from path relative to base */
/* strip .git suffix if remove-suffix is set */
struct cgit_repo *repo = cgit_add_repo(url);
repo->path = xstrdup(path);
/* read gitweb config from the repo */
if (ctx.cfg.enable_git_config) {
char *gitconfig = fmt("%s/config", path);
parse_configfile(gitconfig, gitconfig_config);
}
/* read owner from filesystem */
if (!repo->owner) {
/* stat the repo dir and lookup uid owner */
struct stat st;
if (!stat(path, &st)) {
struct passwd *pw = getpwuid(st.st_uid);
if (pw)
repo->owner = xstrdup(pw->pw_name);
}
}
/* read description from description file */
if (!repo->desc) {
char *descfile = fmt("%s/description", path);
/* read first line */
}
}
```
### Git Config Integration: `gitconfig_config()`
When `enable-git-config=1`, each discovered repository's `.git/config` is
parsed for metadata:
```c
static int gitconfig_config(const char *key, const char *value)
{
if (!strcmp(key, "gitweb.owner"))
repo_config(repo, "owner", value);
else if (!strcmp(key, "gitweb.description"))
repo_config(repo, "desc", value);
else if (!strcmp(key, "gitweb.category"))
repo_config(repo, "section", value);
else if (!strcmp(key, "gitweb.homepage"))
repo_config(repo, "homepage", value);
else if (skip_prefix(key, "cgit.", &name))
repo_config(repo, name, value);
return 0;
}
```
This is compatible with gitweb's configuration keys and also supports
cgit-specific `cgit.*` keys.
## Project List Scanning: `scan_projects()`
```c
void scan_projects(const char *path, const char *projectsfile,
repo_config_fn fn)
```
Instead of recursively scanning a directory, reads a text file listing
project paths (one per line). Each path is appended to the base path and
checked with `is_git_dir()`.
This is useful for large installations where full recursive scanning is too
slow.
```ini
project-list=/etc/cgit/projects.list
scan-path=/srv/git
```
The `projects.list` file contains relative paths:
```
myproject.git
team/frontend.git
team/backend.git
```
## Section Derivation
When `section-from-path` is set, repository sections are automatically
derived from the directory structure:
| Value | Behavior |
|-------|----------|
| `0` | No auto-sectioning |
| `1` | First path component becomes section |
| `2` | First two components become section |
| `-1` | Last component becomes section |
Example with `section-from-path=1` and `scan-path=/srv/git`:
```
/srv/git/team/project.git → section="team"
/srv/git/personal/test.git → section="personal"
```
## Age File
The modification time of a repository is determined by:
1. The `agefile` (default: `info/web/last-modified`) — if this file exists
in the repository, its contents (a date string) or modification time is
used
2. Otherwise, the mtime of the loose `refs/` directory
3. As a fallback, the repository directory's own mtime
```c
static time_t read_agefile(const char *path)
{
FILE *f;
static char buf[64];
f = fopen(path, "r");
if (!f)
return -1;
if (fgets(buf, sizeof(buf), f)) {
fclose(f);
return parse_date(buf, NULL);
}
fclose(f);
/* fallback to file mtime */
struct stat st;
if (!stat(path, &st))
return st.st_mtime;
return 0;
}
```
## Repository List Management
The global repository list is a dynamically-sized array:
```c
struct cgit_repolist {
int count;
int length; /* allocated capacity */
struct cgit_repo *repos;
};
struct cgit_repolist cgit_repolist;
```
### Sorting
The repository list can be sorted by different criteria:
```c
static int cmp_name(const void *a, const void *b); /* by name */
static int cmp_section(const void *a, const void *b); /* by section */
static int cmp_idle(const void *a, const void *b); /* by age */
```
Sorting is controlled by the `repository-sort` directive and the `s` query
parameter.
## Repository Visibility
Two directives control repository visibility:
| Directive | Effect |
|-----------|--------|
| `repo.hide=1` | Repository is hidden from the index but accessible by URL |
| `repo.ignore=1` | Repository is completely ignored |
Additionally, `strict-export` restricts export to repositories containing a
specific file (e.g., `git-daemon-export-ok`):
```ini
strict-export=git-daemon-export-ok
```
## Scan Path Caching
Scanning large directory trees can be slow. The `cache-scanrc-ttl` directive
controls how long scan results are cached:
```ini
cache-scanrc-ttl=15 # cache scan results for 15 minutes
```
When caching is enabled, the scan is performed only when the cached result
expires.
## Configuration Reference
| Directive | Default | Description |
|-----------|---------|-------------|
| `scan-path` | (none) | Directory to scan for repos |
| `project-list` | (none) | File listing project paths |
| `enable-git-config` | 0 | Read repo metadata from git config |
| `scan-hidden-path` | 0 | Include hidden directories in scan |
| `remove-suffix` | 0 | Strip `.git` suffix from URLs |
| `section-from-path` | 0 | Auto-derive section from path |
| `strict-export` | (none) | Required file for repo visibility |
| `agefile` | `info/web/last-modified` | File checked for repo age |
| `cache-scanrc-ttl` | 15 | TTL for cached scan results (minutes) |
|