Fossil SCM

fossil-scm / src / repolist.c
Blame History Raw 433 lines
1
/*
2
** Copyright (c) 2018 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This module contains code to implement the repository list page when
19
** "fossil server" or "fossil ui" is serving a directory full of repositories.
20
*/
21
#include "config.h"
22
#include "repolist.h"
23
24
#if INTERFACE
25
/*
26
** Return value from the remote_repo_info() command. zRepoName is the
27
** input. All other fields are outputs.
28
*/
29
struct RepoInfo {
30
char *zRepoName; /* Name of the repository file */
31
int isValid; /* True if zRepoName is a valid Fossil repository */
32
int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
33
** for the repository list. 2 means do use this
34
** repository but do not display it in the list. */
35
char *zProjName; /* Project Name. Memory from fossil_malloc() */
36
char *zProjDesc; /* Project Description. Memory from fossil_malloc() */
37
char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
38
double rMTime; /* Last update. Julian day number */
39
};
40
#endif
41
42
/*
43
** Discover information about the repository given by
44
** pRepo->zRepoName. The discovered information is stored in other
45
** fields of the RepoInfo object.
46
*/
47
static void remote_repo_info(RepoInfo *pRepo){
48
sqlite3 *db;
49
sqlite3_stmt *pStmt;
50
int rc;
51
52
pRepo->isRepolistSkin = 0;
53
pRepo->isValid = 0;
54
pRepo->zProjName = 0;
55
pRepo->zProjDesc = 0;
56
pRepo->zLoginGroup = 0;
57
pRepo->rMTime = 0.0;
58
59
g.dbIgnoreErrors++;
60
rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
61
if( rc ) goto finish_repo_list;
62
rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
63
" WHERE name='repolist-skin'",
64
-1, &pStmt, 0);
65
if( rc ) goto finish_repo_list;
66
if( sqlite3_step(pStmt)==SQLITE_ROW ){
67
pRepo->isRepolistSkin = sqlite3_column_int(pStmt,0);
68
}
69
sqlite3_finalize(pStmt);
70
if( rc ) goto finish_repo_list;
71
rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
72
" WHERE name='project-name'",
73
-1, &pStmt, 0);
74
if( rc ) goto finish_repo_list;
75
if( sqlite3_step(pStmt)==SQLITE_ROW ){
76
pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
77
}
78
sqlite3_finalize(pStmt);
79
if( rc ) goto finish_repo_list;
80
rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
81
" WHERE name='project-description'",
82
-1, &pStmt, 0);
83
if( rc ) goto finish_repo_list;
84
if( sqlite3_step(pStmt)==SQLITE_ROW ){
85
pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
86
}
87
sqlite3_finalize(pStmt);
88
rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
89
" WHERE name='login-group-name'",
90
-1, &pStmt, 0);
91
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
92
pRepo->zLoginGroup = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
93
}
94
sqlite3_finalize(pStmt);
95
rc = sqlite3_prepare_v2(db, "SELECT max(mtime) FROM event", -1, &pStmt, 0);
96
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
97
pRepo->rMTime = sqlite3_column_double(pStmt,0);
98
}
99
pRepo->isValid = 1;
100
sqlite3_finalize(pStmt);
101
finish_repo_list:
102
g.dbIgnoreErrors--;
103
sqlite3_close(db);
104
}
105
106
/*
107
** Generate a web-page that lists all repositories located under the
108
** g.zRepositoryName directory and return non-zero.
109
**
110
** For the special case when g.zRepositoryName is a non-chroot-jail "/",
111
** compose the list using the "repo:" entries in the global_config
112
** table of the configuration database. These entries comprise all
113
** of the repositories known to the "all" command. The special case
114
** processing is disallowed for chroot jails because g.zRepositoryName
115
** is always "/" inside a chroot jail and so it cannot be used as a flag
116
** to signal the special processing in that case. The special case
117
** processing is intended for the "fossil all ui" command which never
118
** runs in a chroot jail anyhow.
119
**
120
** Or, if no repositories can be located beneath g.zRepositoryName,
121
** close g.db and return 0.
122
*/
123
int repo_list_page(void){
124
Blob base; /* document root for all repositories */
125
int n = 0; /* Number of repositories found */
126
int allRepo; /* True if running "fossil ui all".
127
** False if a directory scan of base for repos */
128
Blob html; /* Html for the body of the repository list */
129
char *zSkinRepo = 0; /* Name of the repository database used for skins */
130
char *zSkinUrl = 0; /* URL for the skin database */
131
const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */
132
int bShowDesc = 0; /* True to show the description column */
133
int bShowLg = 0; /* True to show the login-group column */
134
const char *zHome; /* Home page */
135
136
assert( g.db==0 );
137
zShow = P("FOSSIL_REPOLIST_SHOW");
138
if( zShow ){
139
bShowDesc = strstr(zShow,"description")!=0;
140
bShowLg = strstr(zShow,"login-group")!=0;
141
}
142
blob_init(&html, 0, 0);
143
zHome = PD("home","home");
144
if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
145
/* For the special case of the "repository directory" being "/",
146
** show all of the repositories named in the ~/.fossil database.
147
**
148
** On unix systems, then entries are of the form "repo:/home/..."
149
** and on Windows systems they are like on unix, starting with a "/"
150
** or they can begin with a drive letter: "repo:C:/Users/...". In either
151
** case, we want returned path to omit any initial "/".
152
*/
153
db_open_config(1, 0);
154
db_multi_exec(
155
"CREATE TEMP VIEW sfile AS"
156
" SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
157
" WHERE name GLOB 'repo:*'"
158
);
159
allRepo = 1;
160
}else{
161
/* The default case: All repositories under the g.zRepositoryName
162
** directory.
163
*/
164
Glob *pExclude;
165
blob_init(&base, g.zRepositoryName, -1);
166
db_close(0);
167
assert( g.db==0 );
168
sqlite3_open(":memory:", &g.db);
169
db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
170
db_multi_exec("CREATE TABLE vfile(pathname);");
171
pExclude = glob_create("*/proc,proc");
172
vfile_scan(&base, blob_size(&base), 0, pExclude, 0, ExtFILE);
173
glob_free(pExclude);
174
db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
175
#if USE_SEE
176
" AND pathname NOT GLOB '*[^/].efossil'"
177
#endif
178
);
179
allRepo = 0;
180
}
181
n = db_int(0, "SELECT count(*) FROM sfile");
182
if( n==0 ){
183
sqlite3_close(g.db);
184
g.db = 0;
185
g.repositoryOpen = 0;
186
g.localOpen = 0;
187
return 0;
188
}else{
189
Stmt q;
190
double rNow;
191
char zType[16]; /* Column type letters for class "sortable" */
192
int nType;
193
zType[0] = 't'; /* Repo name */
194
zType[1] = 'x'; /* Space between repo-name and project-name */
195
zType[2] = 't'; /* Project name */
196
nType = 3;
197
if( bShowDesc ){
198
zType[nType++] = 'x'; /* Space between name and description */
199
zType[nType++] = 't'; /* Project description */
200
}
201
zType[nType++] = 'x'; /* space before age */
202
zType[nType++] = 'k'; /* Project age */
203
if( bShowLg ){
204
zType[nType++] = 'x'; /* space before login-group */
205
zType[nType++] = 't'; /* Login Group */
206
}
207
zType[nType] = 0;
208
blob_appendf(&html,
209
"<table border='0' class='sortable' data-init-sort='1'"
210
" data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n"
211
"<tr><th>Filename</th><th>&emsp;</th>\n"
212
"<th%s><nobr>Project Name</nobr></th>\n",
213
zType, (bShowDesc ? " width='25%'" : ""));
214
if( bShowDesc ){
215
blob_appendf(&html,
216
"<th>&emsp;</th>\n"
217
"<th width='25%%'><nobr>Project Description</nobr></th>\n"
218
);
219
}
220
blob_appendf(&html,
221
"<th>&emsp;</th>"
222
"<th><nobr>Last Modified</nobr></th>\n"
223
);
224
if( bShowLg ){
225
blob_appendf(&html,
226
"<th>&emsp;</th>"
227
"<th><nobr>Login Group</nobr></th></tr>\n"
228
);
229
}
230
blob_appendf(&html,"</thead><tbody>\n");
231
db_prepare(&q, "SELECT pathname"
232
" FROM sfile ORDER BY pathname COLLATE nocase;");
233
rNow = db_double(0, "SELECT julianday('now')");
234
while( db_step(&q)==SQLITE_ROW ){
235
const char *zName = db_column_text(&q, 0);
236
int nName = (int)strlen(zName);
237
int nSuffix = 7; /* ".fossil" */
238
char *zUrl;
239
char *zAge;
240
char *zFull;
241
RepoInfo x;
242
sqlite3_int64 iAge;
243
#if USE_SEE
244
int bEncrypted = sqlite3_strglob("*.efossil", zName)==0;
245
if( bEncrypted ) nSuffix = 8; /* ".efossil" */
246
#endif
247
if( nName<nSuffix ) continue;
248
zUrl = sqlite3_mprintf("%.*s", nName-nSuffix, zName);
249
if( zName[0]=='/'
250
#ifdef _WIN32
251
|| sqlite3_strglob("[a-zA-Z]:/*", zName)==0
252
#endif
253
){
254
zFull = fossil_strdup(zName);
255
}else if ( allRepo ){
256
zFull = mprintf("/%s", zName);
257
}else{
258
zFull = mprintf("%s/%s", g.zRepositoryName, zName);
259
}
260
x.zRepoName = zFull;
261
remote_repo_info(&x);
262
if( x.isRepolistSkin ){
263
if( zSkinRepo==0 ){
264
zSkinRepo = fossil_strdup(x.zRepoName);
265
zSkinUrl = fossil_strdup(zUrl);
266
}
267
}
268
fossil_free(zFull);
269
if( !x.isValid
270
#if USE_SEE
271
&& !bEncrypted
272
#endif
273
){
274
continue;
275
}
276
if( x.isRepolistSkin==2 && !allRepo ){
277
/* Repositories with repolist-skin==2 are omitted from directory
278
** scan lists, but included in "fossil all ui" lists */
279
continue;
280
}
281
if( rNow <= x.rMTime ){
282
x.rMTime = rNow;
283
}else if( x.rMTime<0.0 ){
284
x.rMTime = rNow;
285
}
286
iAge = (sqlite3_int64)((rNow - x.rMTime)*86400);
287
zAge = human_readable_age(rNow - x.rMTime);
288
if( x.rMTime==0.0 ){
289
/* This repository has no entry in the "event" table.
290
** Its age will still be maximum, so data-sortkey will work. */
291
zAge = mprintf("unknown");
292
}
293
blob_appendf(&html, "<tr><td valign='top'><nobr>");
294
if( !file_ends_with_repository_extension(zName,0) ){
295
/* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
296
** do not work for repositories whose names do not end in ".fossil".
297
** So do not hyperlink those cases. */
298
blob_appendf(&html,"%h",zName);
299
} else if( sqlite3_strglob("*/.*", zName)==0 ){
300
/* Do not show hyperlinks for hidden repos */
301
blob_appendf(&html, "%h (hidden)", zName);
302
} else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
303
blob_appendf(&html,
304
"<a href='%R/%T/%T' target='_blank'>/%h</a>\n",
305
zUrl, zHome, zName);
306
}else if( file_ends_with_repository_extension(zName,1) ){
307
/* As described in
308
** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
309
** foo.fossil and foo/bar.fossil both exist and we create a
310
** link to foo/bar/... then the URI dispatcher will instead
311
** see that as a link to foo.fossil. In such cases, do not
312
** emit a link to foo/bar.fossil. */
313
char * zDirPart = file_dirname(zName);
314
if( db_exists("SELECT 1 FROM sfile "
315
"WHERE pathname=(%Q || '.fossil') COLLATE nocase"
316
#if USE_SEE
317
" OR pathname=(%Q || '.efossil') COLLATE nocase"
318
#endif
319
, zDirPart
320
#if USE_SEE
321
, zDirPart
322
#endif
323
) ){
324
blob_appendf(&html,
325
"<s>%h</s> (directory/repo name collision)\n",
326
zName);
327
}else{
328
blob_appendf(&html,
329
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
330
zUrl, zName);
331
}
332
fossil_free(zDirPart);
333
}else{
334
blob_appendf(&html,
335
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
336
zUrl, zName);
337
}
338
blob_appendf(&html,"</nobr></td>\n");
339
if( x.zProjName ){
340
blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
341
x.zProjName);
342
fossil_free(x.zProjName);
343
}else{
344
blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
345
}
346
if( !bShowDesc ){
347
/* Do nothing */
348
}else if( x.zProjDesc ){
349
blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
350
x.zProjDesc);
351
fossil_free(x.zProjDesc);
352
}else{
353
blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
354
}
355
blob_appendf(&html,
356
"<td>&emsp;</td><td data-sortkey='%08x' align='center' valign='top'>"
357
"<nobr>%h</nobr></td>\n",
358
(int)iAge, zAge);
359
fossil_free(zAge);
360
if( !bShowLg ){
361
blob_appendf(&html, "</tr>\n");
362
}else if( x.zLoginGroup ){
363
blob_appendf(&html, "<td>&emsp;</td><td valign='top'>"
364
"<nobr>%h</nobr></td></tr>\n",
365
x.zLoginGroup);
366
fossil_free(x.zLoginGroup);
367
}else{
368
blob_appendf(&html, "<td>&emsp;</td><td></td></tr>\n");
369
}
370
sqlite3_free(zUrl);
371
}
372
db_finalize(&q);
373
blob_appendf(&html,"</tbody></table>\n");
374
}
375
if( zSkinRepo ){
376
char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
377
g.zBaseURL = 0;
378
set_base_url(zNewBase);
379
db_open_repository(zSkinRepo);
380
fossil_free(zSkinRepo);
381
fossil_free(zSkinUrl);
382
}
383
if( g.repositoryOpen ){
384
/* This case runs if remote_repo_info() found a repository
385
** that has the "repolist_skin" property set to non-zero and left
386
** that repository open in g.db. Use the skin of that repository
387
** for display. */
388
login_check_credentials();
389
style_set_current_feature("repolist");
390
style_header("Repository List");
391
@ %s(blob_str(&html))
392
style_table_sorter();
393
style_finish_page();
394
}else{
395
const char *zTitle = PD("FOSSIL_REPOLIST_TITLE","Repository List");
396
/* If no repositories were found that had the "repolist_skin"
397
** property set, then use a default skin */
398
@ <html>
399
@ <head>
400
@ <base href="%s(g.zBaseURL)/">
401
@ <meta name="viewport" content="width=device-width, initial-scale=1.0">
402
@ <title>%h(zTitle)</title>
403
@ </head>
404
@ <body>
405
@ <h1 align="center">%h(zTitle)</h1>
406
@ %s(blob_str(&html))
407
@ <script>%s(builtin_text("sorttable.js"))</script>
408
@ </body>
409
@ </html>
410
}
411
blob_reset(&html);
412
cgi_reply();
413
return n;
414
}
415
416
/*
417
** COMMAND: test-list-page
418
**
419
** Usage: %fossil test-list-page DIRECTORY
420
**
421
** Show all repositories underneath DIRECTORY. Or if DIRECTORY is "/"
422
** show all repositories in the ~/.fossil file.
423
*/
424
void test_list_page(void){
425
if( g.argc<3 ){
426
g.zRepositoryName = "/";
427
}else{
428
g.zRepositoryName = g.argv[2];
429
}
430
g.httpOut = stdout;
431
repo_list_page();
432
}
433

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button