Fossil SCM

fossil-scm / src / repolist.c
Blame History Raw 431 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
135
assert( g.db==0 );
136
zShow = P("FOSSIL_REPOLIST_SHOW");
137
if( zShow ){
138
bShowDesc = strstr(zShow,"description")!=0;
139
bShowLg = strstr(zShow,"login-group")!=0;
140
}
141
blob_init(&html, 0, 0);
142
if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
143
/* For the special case of the "repository directory" being "/",
144
** show all of the repositories named in the ~/.fossil database.
145
**
146
** On unix systems, then entries are of the form "repo:/home/..."
147
** and on Windows systems they are like on unix, starting with a "/"
148
** or they can begin with a drive letter: "repo:C:/Users/...". In either
149
** case, we want returned path to omit any initial "/".
150
*/
151
db_open_config(1, 0);
152
db_multi_exec(
153
"CREATE TEMP VIEW sfile AS"
154
" SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
155
" WHERE name GLOB 'repo:*'"
156
);
157
allRepo = 1;
158
}else{
159
/* The default case: All repositories under the g.zRepositoryName
160
** directory.
161
*/
162
Glob *pExclude;
163
blob_init(&base, g.zRepositoryName, -1);
164
db_close(0);
165
assert( g.db==0 );
166
sqlite3_open(":memory:", &g.db);
167
db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
168
db_multi_exec("CREATE TABLE vfile(pathname);");
169
pExclude = glob_create("*/proc,proc");
170
vfile_scan(&base, blob_size(&base), 0, pExclude, 0, ExtFILE);
171
glob_free(pExclude);
172
db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
173
#if USE_SEE
174
" AND pathname NOT GLOB '*[^/].efossil'"
175
#endif
176
);
177
allRepo = 0;
178
}
179
n = db_int(0, "SELECT count(*) FROM sfile");
180
if( n==0 ){
181
sqlite3_close(g.db);
182
g.db = 0;
183
g.repositoryOpen = 0;
184
g.localOpen = 0;
185
return 0;
186
}else{
187
Stmt q;
188
double rNow;
189
char zType[16]; /* Column type letters for class "sortable" */
190
int nType;
191
zType[0] = 't'; /* Repo name */
192
zType[1] = 'x'; /* Space between repo-name and project-name */
193
zType[2] = 't'; /* Project name */
194
nType = 3;
195
if( bShowDesc ){
196
zType[nType++] = 'x'; /* Space between name and description */
197
zType[nType++] = 't'; /* Project description */
198
}
199
zType[nType++] = 'x'; /* space before age */
200
zType[nType++] = 'k'; /* Project age */
201
if( bShowLg ){
202
zType[nType++] = 'x'; /* space before login-group */
203
zType[nType++] = 't'; /* Login Group */
204
}
205
zType[nType] = 0;
206
blob_appendf(&html,
207
"<table border='0' class='sortable' data-init-sort='1'"
208
" data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n"
209
"<tr><th>Filename</th><th>&emsp;</th>\n"
210
"<th%s><nobr>Project Name</nobr></th>\n",
211
zType, (bShowDesc ? " width='25%'" : ""));
212
if( bShowDesc ){
213
blob_appendf(&html,
214
"<th>&emsp;</th>\n"
215
"<th width='25%%'><nobr>Project Description</nobr></th>\n"
216
);
217
}
218
blob_appendf(&html,
219
"<th>&emsp;</th>"
220
"<th><nobr>Last Modified</nobr></th>\n"
221
);
222
if( bShowLg ){
223
blob_appendf(&html,
224
"<th>&emsp;</th>"
225
"<th><nobr>Login Group</nobr></th></tr>\n"
226
);
227
}
228
blob_appendf(&html,"</thead><tbody>\n");
229
db_prepare(&q, "SELECT pathname"
230
" FROM sfile ORDER BY pathname COLLATE nocase;");
231
rNow = db_double(0, "SELECT julianday('now')");
232
while( db_step(&q)==SQLITE_ROW ){
233
const char *zName = db_column_text(&q, 0);
234
int nName = (int)strlen(zName);
235
int nSuffix = 7; /* ".fossil" */
236
char *zUrl;
237
char *zAge;
238
char *zFull;
239
RepoInfo x;
240
sqlite3_int64 iAge;
241
#if USE_SEE
242
int bEncrypted = sqlite3_strglob("*.efossil", zName)==0;
243
if( bEncrypted ) nSuffix = 8; /* ".efossil" */
244
#endif
245
if( nName<nSuffix ) continue;
246
zUrl = sqlite3_mprintf("%.*s", nName-nSuffix, zName);
247
if( zName[0]=='/'
248
#ifdef _WIN32
249
|| sqlite3_strglob("[a-zA-Z]:/*", zName)==0
250
#endif
251
){
252
zFull = fossil_strdup(zName);
253
}else if ( allRepo ){
254
zFull = mprintf("/%s", zName);
255
}else{
256
zFull = mprintf("%s/%s", g.zRepositoryName, zName);
257
}
258
x.zRepoName = zFull;
259
remote_repo_info(&x);
260
if( x.isRepolistSkin ){
261
if( zSkinRepo==0 ){
262
zSkinRepo = fossil_strdup(x.zRepoName);
263
zSkinUrl = fossil_strdup(zUrl);
264
}
265
}
266
fossil_free(zFull);
267
if( !x.isValid
268
#if USE_SEE
269
&& !bEncrypted
270
#endif
271
){
272
continue;
273
}
274
if( x.isRepolistSkin==2 && !allRepo ){
275
/* Repositories with repolist-skin==2 are omitted from directory
276
** scan lists, but included in "fossil all ui" lists */
277
continue;
278
}
279
if( rNow <= x.rMTime ){
280
x.rMTime = rNow;
281
}else if( x.rMTime<0.0 ){
282
x.rMTime = rNow;
283
}
284
iAge = (sqlite3_int64)((rNow - x.rMTime)*86400);
285
zAge = human_readable_age(rNow - x.rMTime);
286
if( x.rMTime==0.0 ){
287
/* This repository has no entry in the "event" table.
288
** Its age will still be maximum, so data-sortkey will work. */
289
zAge = mprintf("unknown");
290
}
291
blob_appendf(&html, "<tr><td valign='top'><nobr>");
292
if( !file_ends_with_repository_extension(zName,0) ){
293
/* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
294
** do not work for repositories whose names do not end in ".fossil".
295
** So do not hyperlink those cases. */
296
blob_appendf(&html,"%h",zName);
297
} else if( sqlite3_strglob("*/.*", zName)==0 ){
298
/* Do not show hyperlinks for hidden repos */
299
blob_appendf(&html, "%h (hidden)", zName);
300
} else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
301
blob_appendf(&html,
302
"<a href='%R/%T/home' target='_blank'>/%h</a>\n",
303
zUrl, zName);
304
}else if( file_ends_with_repository_extension(zName,1) ){
305
/* As described in
306
** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
307
** foo.fossil and foo/bar.fossil both exist and we create a
308
** link to foo/bar/... then the URI dispatcher will instead
309
** see that as a link to foo.fossil. In such cases, do not
310
** emit a link to foo/bar.fossil. */
311
char * zDirPart = file_dirname(zName);
312
if( db_exists("SELECT 1 FROM sfile "
313
"WHERE pathname=(%Q || '.fossil') COLLATE nocase"
314
#if USE_SEE
315
" OR pathname=(%Q || '.efossil') COLLATE nocase"
316
#endif
317
, zDirPart
318
#if USE_SEE
319
, zDirPart
320
#endif
321
) ){
322
blob_appendf(&html,
323
"<s>%h</s> (directory/repo name collision)\n",
324
zName);
325
}else{
326
blob_appendf(&html,
327
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
328
zUrl, zName);
329
}
330
fossil_free(zDirPart);
331
}else{
332
blob_appendf(&html,
333
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
334
zUrl, zName);
335
}
336
blob_appendf(&html,"</nobr></td>\n");
337
if( x.zProjName ){
338
blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
339
x.zProjName);
340
fossil_free(x.zProjName);
341
}else{
342
blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
343
}
344
if( !bShowDesc ){
345
/* Do nothing */
346
}else if( x.zProjDesc ){
347
blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
348
x.zProjDesc);
349
fossil_free(x.zProjDesc);
350
}else{
351
blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
352
}
353
blob_appendf(&html,
354
"<td>&emsp;</td><td data-sortkey='%08x' align='center' valign='top'>"
355
"<nobr>%h</nobr></td>\n",
356
(int)iAge, zAge);
357
fossil_free(zAge);
358
if( !bShowLg ){
359
blob_appendf(&html, "</tr>\n");
360
}else if( x.zLoginGroup ){
361
blob_appendf(&html, "<td>&emsp;</td><td valign='top'>"
362
"<nobr>%h</nobr></td></tr>\n",
363
x.zLoginGroup);
364
fossil_free(x.zLoginGroup);
365
}else{
366
blob_appendf(&html, "<td>&emsp;</td><td></td></tr>\n");
367
}
368
sqlite3_free(zUrl);
369
}
370
db_finalize(&q);
371
blob_appendf(&html,"</tbody></table>\n");
372
}
373
if( zSkinRepo ){
374
char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
375
g.zBaseURL = 0;
376
set_base_url(zNewBase);
377
db_open_repository(zSkinRepo);
378
fossil_free(zSkinRepo);
379
fossil_free(zSkinUrl);
380
}
381
if( g.repositoryOpen ){
382
/* This case runs if remote_repo_info() found a repository
383
** that has the "repolist_skin" property set to non-zero and left
384
** that repository open in g.db. Use the skin of that repository
385
** for display. */
386
login_check_credentials();
387
style_set_current_feature("repolist");
388
style_header("Repository List");
389
@ %s(blob_str(&html))
390
style_table_sorter();
391
style_finish_page();
392
}else{
393
const char *zTitle = PD("FOSSIL_REPOLIST_TITLE","Repository List");
394
/* If no repositories were found that had the "repolist_skin"
395
** property set, then use a default skin */
396
@ <html>
397
@ <head>
398
@ <base href="%s(g.zBaseURL)/">
399
@ <meta name="viewport" content="width=device-width, initial-scale=1.0">
400
@ <title>%h(zTitle)</title>
401
@ </head>
402
@ <body>
403
@ <h1 align="center">%h(zTitle)</h1>
404
@ %s(blob_str(&html))
405
@ <script>%s(builtin_text("sorttable.js"))</script>
406
@ </body>
407
@ </html>
408
}
409
blob_reset(&html);
410
cgi_reply();
411
return n;
412
}
413
414
/*
415
** COMMAND: test-list-page
416
**
417
** Usage: %fossil test-list-page DIRECTORY
418
**
419
** Show all repositories underneath DIRECTORY. Or if DIRECTORY is "/"
420
** show all repositories in the ~/.fossil file.
421
*/
422
void test_list_page(void){
423
if( g.argc<3 ){
424
g.zRepositoryName = "/";
425
}else{
426
g.zRepositoryName = g.argv[2];
427
}
428
g.httpOut = stdout;
429
repo_list_page();
430
}
431

Keyboard Shortcuts

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