|
1
|
/* |
|
2
|
** Copyright (c) 2008 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 file contains code to implement the file browser web interface. |
|
19
|
*/ |
|
20
|
#include "config.h" |
|
21
|
#include "browse.h" |
|
22
|
#include <assert.h> |
|
23
|
|
|
24
|
/* |
|
25
|
** This is the implementation of the "pathelement(X,N)" SQL function. |
|
26
|
** |
|
27
|
** If X is a unix-like pathname (with "/" separators) and N is an |
|
28
|
** integer, then skip the initial N characters of X and return the |
|
29
|
** name of the path component that begins on the N+1th character |
|
30
|
** (numbered from 0). If the path component is a directory (if |
|
31
|
** it is followed by other path components) then prepend "/". |
|
32
|
** |
|
33
|
** Examples: |
|
34
|
** |
|
35
|
** pathelement('abc/pqr/xyz', 4) -> '/pqr' |
|
36
|
** pathelement('abc/pqr', 4) -> 'pqr' |
|
37
|
** pathelement('abc/pqr/xyz', 0) -> '/abc' |
|
38
|
*/ |
|
39
|
void pathelementFunc( |
|
40
|
sqlite3_context *context, |
|
41
|
int argc, |
|
42
|
sqlite3_value **argv |
|
43
|
){ |
|
44
|
const unsigned char *z; |
|
45
|
int len, n, i; |
|
46
|
char *zOut; |
|
47
|
|
|
48
|
assert( argc==2 ); |
|
49
|
z = sqlite3_value_text(argv[0]); |
|
50
|
if( z==0 ) return; |
|
51
|
len = sqlite3_value_bytes(argv[0]); |
|
52
|
n = sqlite3_value_int(argv[1]); |
|
53
|
if( len<=n ) return; |
|
54
|
if( n>0 && z[n-1]!='/' ) return; |
|
55
|
for(i=n; i<len && z[i]!='/'; i++){} |
|
56
|
if( i==len ){ |
|
57
|
sqlite3_result_text(context, (char*)&z[n], len-n, SQLITE_TRANSIENT); |
|
58
|
}else{ |
|
59
|
zOut = sqlite3_mprintf("/%.*s", i-n, &z[n]); |
|
60
|
sqlite3_result_text(context, zOut, i-n+1, sqlite3_free); |
|
61
|
} |
|
62
|
} |
|
63
|
|
|
64
|
/* |
|
65
|
** Flag arguments for hyperlinked_path() |
|
66
|
*/ |
|
67
|
#if INTERFACE |
|
68
|
# define LINKPATH_FINFO 0x0001 /* Link final term to /finfo */ |
|
69
|
# define LINKPATH_FILE 0x0002 /* Link final term to /file */ |
|
70
|
#endif |
|
71
|
|
|
72
|
/* |
|
73
|
** Given a pathname which is a relative path from the root of |
|
74
|
** the repository to a file or directory, compute a string which |
|
75
|
** is an HTML rendering of that path with hyperlinks on each |
|
76
|
** directory component of the path where the hyperlink redirects |
|
77
|
** to the "dir" page for the directory. |
|
78
|
** |
|
79
|
** There is no hyperlink on the file element of the path. |
|
80
|
** |
|
81
|
** The computed string is appended to the pOut blob. pOut should |
|
82
|
** have already been initialized. |
|
83
|
*/ |
|
84
|
void hyperlinked_path( |
|
85
|
const char *zPath, /* Path to render */ |
|
86
|
Blob *pOut, /* Write into this blob */ |
|
87
|
const char *zCI, /* check-in name, or NULL */ |
|
88
|
const char *zURI, /* "dir" or "tree" */ |
|
89
|
const char *zREx, /* Extra query parameters */ |
|
90
|
unsigned int mFlags /* Extra flags */ |
|
91
|
){ |
|
92
|
int i, j; |
|
93
|
char *zSep = ""; |
|
94
|
|
|
95
|
for(i=0; zPath[i]; i=j){ |
|
96
|
for(j=i; zPath[j] && zPath[j]!='/'; j++){} |
|
97
|
if( zPath[j]==0 ){ |
|
98
|
if( mFlags & LINKPATH_FILE ){ |
|
99
|
zURI = "file"; |
|
100
|
}else if( mFlags & LINKPATH_FINFO ){ |
|
101
|
zURI = "finfo"; |
|
102
|
}else{ |
|
103
|
blob_appendf(pOut, "/%h", zPath+i); |
|
104
|
break; |
|
105
|
} |
|
106
|
} |
|
107
|
if( zCI ){ |
|
108
|
char *zLink = href("%R/%s?name=%#T%s&ci=%T", zURI, j, zPath, zREx,zCI); |
|
109
|
blob_appendf(pOut, "%s%z%#h</a>", |
|
110
|
zSep, zLink, j-i, &zPath[i]); |
|
111
|
}else{ |
|
112
|
char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx); |
|
113
|
blob_appendf(pOut, "%s%z%#h</a>", |
|
114
|
zSep, zLink, j-i, &zPath[i]); |
|
115
|
} |
|
116
|
zSep = "/"; |
|
117
|
while( zPath[j]=='/' ){ j++; } |
|
118
|
} |
|
119
|
} |
|
120
|
|
|
121
|
/* |
|
122
|
** WEBPAGE: docdir |
|
123
|
** |
|
124
|
** Show the files and subdirectories within a single directory of the |
|
125
|
** source tree. This works similarly to /dir but with the following |
|
126
|
** differences: |
|
127
|
** |
|
128
|
** * Links to files go to /doc (showing the file content directly, |
|
129
|
** depending on mimetype) rather than to /file (which always shows |
|
130
|
** the file embedded in a standard Fossil page frame). |
|
131
|
** |
|
132
|
** * The submenu and the page title is not shown. The page is plain. |
|
133
|
** |
|
134
|
** The /docdir page is a shorthand for /dir with the "dx" query parameter. |
|
135
|
** |
|
136
|
** Query parameters: |
|
137
|
** |
|
138
|
** ci=LABEL Show only files in this check-in. If omitted, the |
|
139
|
** "trunk" directory is used. |
|
140
|
** name=PATH Directory to display. Optional. Top-level if missing |
|
141
|
** re=REGEXP Show only files matching REGEXP |
|
142
|
** noreadme Do not attempt to display the README file. |
|
143
|
** dx File links to go to /doc instead of /file or /finfo. |
|
144
|
*/ |
|
145
|
void page_docdir(void){ page_dir(); } |
|
146
|
|
|
147
|
/* |
|
148
|
** WEBPAGE: dir |
|
149
|
** |
|
150
|
** Show the files and subdirectories within a single directory of the |
|
151
|
** source tree. Only files for a single check-in are shown if the ci= |
|
152
|
** query parameter is present. If ci= is missing, the union of files |
|
153
|
** across all check-ins is shown. |
|
154
|
** |
|
155
|
** Query parameters: |
|
156
|
** |
|
157
|
** ci=LABEL Show only files in this check-in. Optional. |
|
158
|
** name=PATH Directory to display. Optional. Top-level if missing |
|
159
|
** re=REGEXP Show only files matching REGEXP |
|
160
|
** type=TYPE TYPE=flat: use this display |
|
161
|
** TYPE=tree: use the /tree display instead |
|
162
|
** noreadme Do not attempt to display the README file. |
|
163
|
** dx Behave like /docdir |
|
164
|
*/ |
|
165
|
void page_dir(void){ |
|
166
|
char *zD = fossil_strdup(P("name")); |
|
167
|
int nD = zD ? strlen(zD)+1 : 0; |
|
168
|
int mxLen; |
|
169
|
char *zPrefix; |
|
170
|
Stmt q; |
|
171
|
const char *zCI = P("ci"); |
|
172
|
int rid = 0; |
|
173
|
char *zUuid = 0; |
|
174
|
Manifest *pM = 0; |
|
175
|
const char *zSubdirLink; |
|
176
|
HQuery sURI; |
|
177
|
int isSymbolicCI = 0; /* ci= is symbolic name, not a hash prefix */ |
|
178
|
int isBranchCI = 0; /* True if ci= refers to a branch name */ |
|
179
|
char *zHeader = 0; |
|
180
|
const char *zRegexp; /* The re= query parameter */ |
|
181
|
char *zMatch; /* Extra title text describing the match */ |
|
182
|
int bDocDir = PB("dx") || strncmp(g.zPath, "docdir", 6)==0; |
|
183
|
|
|
184
|
if( zCI && strlen(zCI)==0 ){ zCI = 0; } |
|
185
|
if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; } |
|
186
|
login_check_credentials(); |
|
187
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
188
|
while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
|
189
|
|
|
190
|
/* If the name= parameter is an empty string, make it a NULL pointer */ |
|
191
|
if( zD && strlen(zD)==0 ){ zD = 0; } |
|
192
|
|
|
193
|
/* If a specific check-in is requested, fetch and parse it. If the |
|
194
|
** specific check-in does not exist, clear zCI. zCI==0 will cause all |
|
195
|
** files from all check-ins to be displayed. |
|
196
|
*/ |
|
197
|
if( bDocDir && zCI==0 ) zCI = db_main_branch(); |
|
198
|
if( zCI ){ |
|
199
|
pM = manifest_get_by_name(zCI, &rid); |
|
200
|
if( pM ){ |
|
201
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
202
|
isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0); |
|
203
|
isBranchCI = branch_includes_uuid(zCI, zUuid); |
|
204
|
if( bDocDir ) zCI = mprintf("%S", zUuid); |
|
205
|
Th_StoreUnsafe("current_checkin", zCI); |
|
206
|
}else{ |
|
207
|
zCI = 0; |
|
208
|
} |
|
209
|
} |
|
210
|
|
|
211
|
assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) ); |
|
212
|
if( zD==0 ){ |
|
213
|
if( zCI ){ |
|
214
|
zHeader = mprintf("Top-level Files of %s", zCI); |
|
215
|
}else{ |
|
216
|
zHeader = mprintf("All Top-level Files"); |
|
217
|
} |
|
218
|
}else{ |
|
219
|
if( zCI ){ |
|
220
|
zHeader = mprintf("Files in %s/ of %s", zD, zCI); |
|
221
|
}else{ |
|
222
|
zHeader = mprintf("All Files in %s/", zD); |
|
223
|
} |
|
224
|
} |
|
225
|
zRegexp = P("re"); |
|
226
|
if( zRegexp ){ |
|
227
|
zHeader = mprintf("%z matching \"%s\"", zHeader, zRegexp); |
|
228
|
zMatch = mprintf(" matching \"%h\"", zRegexp); |
|
229
|
}else{ |
|
230
|
zMatch = ""; |
|
231
|
} |
|
232
|
style_header("%s", zHeader); |
|
233
|
fossil_free(zHeader); |
|
234
|
if( rid && zD==0 && zMatch[0]==0 && g.perm.Zip ){ |
|
235
|
style_submenu_element("Download","%R/rchvdwnld/%!S",zUuid); |
|
236
|
} |
|
237
|
style_adunit_config(ADUNIT_RIGHT_OK); |
|
238
|
sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0, |
|
239
|
pathelementFunc, 0, 0); |
|
240
|
url_initialize(&sURI, "dir"); |
|
241
|
cgi_check_for_malice(); |
|
242
|
cgi_query_parameters_to_url(&sURI); |
|
243
|
|
|
244
|
/* Compute the title of the page */ |
|
245
|
if( bDocDir ){ |
|
246
|
zPrefix = zD ? mprintf("%s/",zD) : ""; |
|
247
|
}else if( zD ){ |
|
248
|
Blob dirname; |
|
249
|
blob_init(&dirname, 0, 0); |
|
250
|
hyperlinked_path(zD, &dirname, zCI, "dir", "", 0); |
|
251
|
@ <h2>Files in directory %s(blob_str(&dirname)) \ |
|
252
|
blob_reset(&dirname); |
|
253
|
zPrefix = mprintf("%s/", zD); |
|
254
|
style_submenu_element("Top-Level", "%s", |
|
255
|
url_render(&sURI, "name", 0, 0, 0)); |
|
256
|
}else{ |
|
257
|
@ <h2>Files in the top-level directory \ |
|
258
|
zPrefix = ""; |
|
259
|
} |
|
260
|
if( zCI ){ |
|
261
|
if( bDocDir ){ |
|
262
|
/* No header for /docdir. Just give the list of files. */ |
|
263
|
}else if( fossil_strcmp(zCI,"tip")==0 ){ |
|
264
|
@ from the %z(href("%R/info?name=%T",zCI))latest check-in</a>\ |
|
265
|
@ %s(zMatch)</h2> |
|
266
|
}else if( isBranchCI ){ |
|
267
|
@ from the %z(href("%R/info?name=%T",zCI))latest check-in</a> \ |
|
268
|
@ of branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a>\ |
|
269
|
@ %s(zMatch)</h2> |
|
270
|
}else { |
|
271
|
@ of check-in %z(href("%R/info?name=%T",zCI))%h(zCI)</a>\ |
|
272
|
@ %s(zMatch)</h2> |
|
273
|
} |
|
274
|
if( bDocDir ){ |
|
275
|
zSubdirLink = mprintf("%R/docdir?ci=%T&name=%T", zCI, zPrefix); |
|
276
|
}else{ |
|
277
|
zSubdirLink = mprintf("%R/dir?ci=%T&name=%T", zCI, zPrefix); |
|
278
|
} |
|
279
|
if( nD==0 && !bDocDir ){ |
|
280
|
style_submenu_element("File Ages", "%R/fileage?name=%T", zCI); |
|
281
|
} |
|
282
|
}else{ |
|
283
|
@ in any check-in</h2> |
|
284
|
zSubdirLink = mprintf("%R/dir?name=%T", zPrefix); |
|
285
|
} |
|
286
|
if( zD && !bDocDir ){ |
|
287
|
style_submenu_element("History","%R/timeline?chng=%T/*", zD); |
|
288
|
} |
|
289
|
if( !bDocDir ){ |
|
290
|
style_submenu_element("Tree-View", "%s", |
|
291
|
url_render(&sURI, "type", "tree", 0, 0)); |
|
292
|
} |
|
293
|
|
|
294
|
if( !bDocDir ){ |
|
295
|
/* Generate the Branch list submenu */ |
|
296
|
generate_branch_submenu_multichoice("ci", zCI); |
|
297
|
} |
|
298
|
|
|
299
|
/* Compute the temporary table "localfiles" containing the names |
|
300
|
** of all files and subdirectories in the zD[] directory. |
|
301
|
** |
|
302
|
** Subdirectory names begin with "/". This causes them to sort |
|
303
|
** first and it also gives us an easy way to distinguish files |
|
304
|
** from directories in the loop that follows. |
|
305
|
*/ |
|
306
|
db_multi_exec( |
|
307
|
"CREATE TEMP TABLE localfiles(x UNIQUE NOT NULL, u);" |
|
308
|
); |
|
309
|
if( zCI ){ |
|
310
|
/* Files in the specific checked given by zCI */ |
|
311
|
if( zD ){ |
|
312
|
db_multi_exec( |
|
313
|
"INSERT OR IGNORE INTO localfiles" |
|
314
|
" SELECT pathelement(filename,%d), uuid" |
|
315
|
" FROM files_of_checkin(%Q)" |
|
316
|
" WHERE filename GLOB '%q/*'", |
|
317
|
nD, zCI, zD |
|
318
|
); |
|
319
|
}else{ |
|
320
|
db_multi_exec( |
|
321
|
"INSERT OR IGNORE INTO localfiles" |
|
322
|
" SELECT pathelement(filename,%d), uuid" |
|
323
|
" FROM files_of_checkin(%Q)", |
|
324
|
nD, zCI |
|
325
|
); |
|
326
|
} |
|
327
|
}else{ |
|
328
|
/* All files across all check-ins */ |
|
329
|
if( zD ){ |
|
330
|
db_multi_exec( |
|
331
|
"INSERT OR IGNORE INTO localfiles" |
|
332
|
" SELECT pathelement(name,%d), NULL FROM filename" |
|
333
|
" WHERE name GLOB '%q/*'", |
|
334
|
nD, zD |
|
335
|
); |
|
336
|
}else{ |
|
337
|
db_multi_exec( |
|
338
|
"INSERT OR IGNORE INTO localfiles" |
|
339
|
" SELECT pathelement(name,0), NULL FROM filename" |
|
340
|
); |
|
341
|
} |
|
342
|
} |
|
343
|
|
|
344
|
/* If the re=REGEXP query parameter is present, filter out names that |
|
345
|
** do not match the pattern */ |
|
346
|
if( zRegexp ){ |
|
347
|
db_multi_exec( |
|
348
|
"DELETE FROM localfiles WHERE x NOT REGEXP %Q", zRegexp |
|
349
|
); |
|
350
|
} |
|
351
|
|
|
352
|
/* Generate a multi-column table listing the contents of zD[] |
|
353
|
** directory. |
|
354
|
*/ |
|
355
|
mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/"); |
|
356
|
if( mxLen<12 ) mxLen = 12; |
|
357
|
mxLen += (mxLen+9)/10; |
|
358
|
db_prepare(&q, |
|
359
|
"SELECT x, u FROM localfiles ORDER BY x COLLATE uintnocase /*scan*/"); |
|
360
|
@ <div class="columns files" style="columns: %d(mxLen)ex auto"> |
|
361
|
@ <ul class="browser"> |
|
362
|
while( db_step(&q)==SQLITE_ROW ){ |
|
363
|
const char *zFN; |
|
364
|
zFN = db_column_text(&q, 0); |
|
365
|
if( zFN[0]=='/' ){ |
|
366
|
zFN++; |
|
367
|
@ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li> |
|
368
|
}else{ |
|
369
|
const char *zLink; |
|
370
|
if( bDocDir ){ |
|
371
|
zLink = href("%R/doc/%T/%T%T", zCI, zPrefix, zFN); |
|
372
|
}else if( zCI ){ |
|
373
|
zLink = href("%R/file?name=%T%T&ci=%T",zPrefix,zFN,zCI); |
|
374
|
}else{ |
|
375
|
zLink = href("%R/finfo?name=%T%T",zPrefix,zFN); |
|
376
|
} |
|
377
|
@ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li> |
|
378
|
} |
|
379
|
} |
|
380
|
db_finalize(&q); |
|
381
|
manifest_destroy(pM); |
|
382
|
@ </ul></div> |
|
383
|
|
|
384
|
/* If the "noreadme" query parameter is present, do not try to |
|
385
|
** show the content of the README file. |
|
386
|
*/ |
|
387
|
if( P("noreadme")!=0 ){ |
|
388
|
style_finish_page(); |
|
389
|
return; |
|
390
|
} |
|
391
|
|
|
392
|
/* If the directory contains a readme file, then display its content below |
|
393
|
** the list of files |
|
394
|
*/ |
|
395
|
db_prepare(&q, |
|
396
|
"SELECT x, u FROM localfiles" |
|
397
|
" WHERE x COLLATE nocase IN" |
|
398
|
" ('readme','readme.txt','readme.md','readme.wiki','readme.markdown'," |
|
399
|
" 'readme.html') ORDER BY x COLLATE uintnocase LIMIT 1;" |
|
400
|
); |
|
401
|
if( db_step(&q)==SQLITE_ROW ){ |
|
402
|
const char *zName = db_column_text(&q,0); |
|
403
|
const char *zUuid = db_column_text(&q,1); |
|
404
|
if( zUuid ){ |
|
405
|
rid = fast_uuid_to_rid(zUuid); |
|
406
|
}else{ |
|
407
|
if( zD ){ |
|
408
|
rid = db_int(0, |
|
409
|
"SELECT fid FROM filename, mlink, event" |
|
410
|
" WHERE name='%q/%q'" |
|
411
|
" AND mlink.fnid=filename.fnid" |
|
412
|
" AND event.objid=mlink.mid" |
|
413
|
" ORDER BY event.mtime DESC LIMIT 1", |
|
414
|
zD, zName |
|
415
|
); |
|
416
|
}else{ |
|
417
|
rid = db_int(0, |
|
418
|
"SELECT fid FROM filename, mlink, event" |
|
419
|
" WHERE name='%q'" |
|
420
|
" AND mlink.fnid=filename.fnid" |
|
421
|
" AND event.objid=mlink.mid" |
|
422
|
" ORDER BY event.mtime DESC LIMIT 1", |
|
423
|
zName |
|
424
|
); |
|
425
|
} |
|
426
|
} |
|
427
|
if( rid ){ |
|
428
|
@ <hr> |
|
429
|
if( sqlite3_strlike("readme.html", zName, 0)==0 ){ |
|
430
|
if( zUuid==0 ){ |
|
431
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
432
|
} |
|
433
|
@ <iframe src="%R/raw/%s(zUuid)" |
|
434
|
@ width="100%%" frameborder="0" marginwidth="0" marginheight="0" |
|
435
|
@ sandbox="allow-same-origin" |
|
436
|
@ onload="this.height=this.contentDocument.documentElement.scrollHeight;"> |
|
437
|
@ </iframe> |
|
438
|
}else{ |
|
439
|
Blob content; |
|
440
|
const char *zMime = mimetype_from_name(zName); |
|
441
|
content_get(rid, &content); |
|
442
|
safe_html_context(DOCSRC_FILE); |
|
443
|
wiki_render_by_mimetype(&content, zMime); |
|
444
|
document_emit_js(); |
|
445
|
} |
|
446
|
} |
|
447
|
} |
|
448
|
db_finalize(&q); |
|
449
|
style_finish_page(); |
|
450
|
} |
|
451
|
|
|
452
|
/* |
|
453
|
** Objects used by the "tree" webpage. |
|
454
|
*/ |
|
455
|
typedef struct FileTreeNode FileTreeNode; |
|
456
|
typedef struct FileTree FileTree; |
|
457
|
|
|
458
|
/* |
|
459
|
** A single line of the file hierarchy |
|
460
|
*/ |
|
461
|
struct FileTreeNode { |
|
462
|
FileTreeNode *pNext; /* Next entry in an ordered list of them all */ |
|
463
|
FileTreeNode *pParent; /* Directory containing this entry */ |
|
464
|
FileTreeNode *pSibling; /* Next element in the same subdirectory */ |
|
465
|
FileTreeNode *pChild; /* List of child nodes */ |
|
466
|
FileTreeNode *pLastChild; /* Last child on the pChild list */ |
|
467
|
char *zName; /* Name of this entry. The "tail" */ |
|
468
|
char *zFullName; /* Full pathname of this entry */ |
|
469
|
char *zUuid; /* Artifact hash of this file. May be NULL. */ |
|
470
|
double mtime; /* Modification time for this entry */ |
|
471
|
double sortBy; /* Either mtime or size, depending on desired |
|
472
|
sort order */ |
|
473
|
int iSize; /* Size for this entry */ |
|
474
|
unsigned nFullName; /* Length of zFullName */ |
|
475
|
unsigned iLevel; /* Levels of parent directories */ |
|
476
|
}; |
|
477
|
|
|
478
|
/* |
|
479
|
** A complete file hierarchy |
|
480
|
*/ |
|
481
|
struct FileTree { |
|
482
|
FileTreeNode *pFirst; /* First line of the list */ |
|
483
|
FileTreeNode *pLast; /* Last line of the list */ |
|
484
|
FileTreeNode *pLastTop; /* Last top-level node */ |
|
485
|
}; |
|
486
|
|
|
487
|
/* |
|
488
|
** Add one or more new FileTreeNodes to the FileTree object so that the |
|
489
|
** leaf object zPathname is at the end of the node list. |
|
490
|
** |
|
491
|
** The caller invokes this routine once for each leaf node (each file |
|
492
|
** as opposed to each directory). This routine fills in any missing |
|
493
|
** intermediate nodes automatically. |
|
494
|
** |
|
495
|
** When constructing a list of FileTreeNodes, all entries that have |
|
496
|
** a common directory prefix must be added consecutively in order for |
|
497
|
** the tree to be constructed properly. |
|
498
|
*/ |
|
499
|
static void tree_add_node( |
|
500
|
FileTree *pTree, /* Tree into which nodes are added */ |
|
501
|
const char *zPath, /* The full pathname of file to add */ |
|
502
|
const char *zUuid, /* Hash of the file. Might be NULL. */ |
|
503
|
double mtime, /* Modification time for this entry */ |
|
504
|
int size, /* Size for this entry */ |
|
505
|
int sortOrder /* 0: filename, 1: mtime, 2: size */ |
|
506
|
){ |
|
507
|
int i; |
|
508
|
FileTreeNode *pParent; /* Parent (directory) of the next node to insert */ |
|
509
|
|
|
510
|
/* Make pParent point to the most recent ancestor of zPath, or |
|
511
|
** NULL if there are no prior entires that are a container for zPath. |
|
512
|
*/ |
|
513
|
pParent = pTree->pLast; |
|
514
|
while( pParent!=0 && |
|
515
|
( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0 |
|
516
|
|| zPath[pParent->nFullName]!='/' ) |
|
517
|
){ |
|
518
|
pParent = pParent->pParent; |
|
519
|
} |
|
520
|
i = pParent ? pParent->nFullName+1 : 0; |
|
521
|
while( zPath[i] ){ |
|
522
|
FileTreeNode *pNew; |
|
523
|
int iStart = i; |
|
524
|
int nByte; |
|
525
|
while( zPath[i] && zPath[i]!='/' ){ i++; } |
|
526
|
nByte = sizeof(*pNew) + i + 1; |
|
527
|
if( zUuid!=0 && zPath[i]==0 ) nByte += HNAME_MAX+1; |
|
528
|
pNew = fossil_malloc( nByte ); |
|
529
|
memset(pNew, 0, sizeof(*pNew)); |
|
530
|
pNew->zFullName = (char*)&pNew[1]; |
|
531
|
memcpy(pNew->zFullName, zPath, i); |
|
532
|
pNew->zFullName[i] = 0; |
|
533
|
pNew->nFullName = i; |
|
534
|
if( zUuid!=0 && zPath[i]==0 ){ |
|
535
|
pNew->zUuid = pNew->zFullName + i + 1; |
|
536
|
memcpy(pNew->zUuid, zUuid, strlen(zUuid)+1); |
|
537
|
} |
|
538
|
pNew->zName = pNew->zFullName + iStart; |
|
539
|
if( pTree->pLast ){ |
|
540
|
pTree->pLast->pNext = pNew; |
|
541
|
}else{ |
|
542
|
pTree->pFirst = pNew; |
|
543
|
} |
|
544
|
pTree->pLast = pNew; |
|
545
|
pNew->pParent = pParent; |
|
546
|
if( pParent ){ |
|
547
|
if( pParent->pChild ){ |
|
548
|
pParent->pLastChild->pSibling = pNew; |
|
549
|
}else{ |
|
550
|
pParent->pChild = pNew; |
|
551
|
} |
|
552
|
pNew->iLevel = pParent->iLevel + 1; |
|
553
|
pParent->pLastChild = pNew; |
|
554
|
}else{ |
|
555
|
if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew; |
|
556
|
pTree->pLastTop = pNew; |
|
557
|
} |
|
558
|
pNew->mtime = mtime; |
|
559
|
pNew->iSize = size; |
|
560
|
if( sortOrder ){ |
|
561
|
pNew->sortBy = sortOrder==1 ? mtime : (double)size; |
|
562
|
}else{ |
|
563
|
pNew->sortBy = 0.0; |
|
564
|
} |
|
565
|
while( zPath[i]=='/' ){ i++; } |
|
566
|
pParent = pNew; |
|
567
|
} |
|
568
|
while( pParent && pParent->pParent ){ |
|
569
|
if( pParent->pParent->mtime < pParent->mtime ){ |
|
570
|
pParent->pParent->mtime = pParent->mtime; |
|
571
|
} |
|
572
|
pParent = pParent->pParent; |
|
573
|
} |
|
574
|
} |
|
575
|
|
|
576
|
/* Comparison function for two FileTreeNode objects. Sort first by |
|
577
|
** sortBy (larger numbers first) and then by zName (smaller names first). |
|
578
|
** |
|
579
|
** The sortBy field will be the same as mtime in order to sort by time, |
|
580
|
** or the same as iSize to sort by file size. |
|
581
|
** |
|
582
|
** Return negative if pLeft<pRight. |
|
583
|
** Return positive if pLeft>pRight. |
|
584
|
** Return zero if pLeft==pRight. |
|
585
|
*/ |
|
586
|
static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){ |
|
587
|
if( pLeft->sortBy>pRight->sortBy ) return -1; |
|
588
|
if( pLeft->sortBy<pRight->sortBy ) return +1; |
|
589
|
return fossil_stricmp(pLeft->zName, pRight->zName); |
|
590
|
} |
|
591
|
|
|
592
|
/* Merge together two sorted lists of FileTreeNode objects */ |
|
593
|
static FileTreeNode *mergeNodes(FileTreeNode *pLeft, FileTreeNode *pRight){ |
|
594
|
FileTreeNode *pEnd; |
|
595
|
FileTreeNode base; |
|
596
|
pEnd = &base; |
|
597
|
while( pLeft && pRight ){ |
|
598
|
if( compareNodes(pLeft,pRight)<=0 ){ |
|
599
|
pEnd = pEnd->pSibling = pLeft; |
|
600
|
pLeft = pLeft->pSibling; |
|
601
|
}else{ |
|
602
|
pEnd = pEnd->pSibling = pRight; |
|
603
|
pRight = pRight->pSibling; |
|
604
|
} |
|
605
|
} |
|
606
|
if( pLeft ){ |
|
607
|
pEnd->pSibling = pLeft; |
|
608
|
}else{ |
|
609
|
pEnd->pSibling = pRight; |
|
610
|
} |
|
611
|
return base.pSibling; |
|
612
|
} |
|
613
|
|
|
614
|
/* Sort a list of FileTreeNode objects in sortmtime order. */ |
|
615
|
static FileTreeNode *sortNodes(FileTreeNode *p){ |
|
616
|
FileTreeNode *a[30]; |
|
617
|
FileTreeNode *pX; |
|
618
|
int i; |
|
619
|
|
|
620
|
memset(a, 0, sizeof(a)); |
|
621
|
while( p ){ |
|
622
|
pX = p; |
|
623
|
p = pX->pSibling; |
|
624
|
pX->pSibling = 0; |
|
625
|
for(i=0; i<count(a)-1 && a[i]!=0; i++){ |
|
626
|
pX = mergeNodes(a[i], pX); |
|
627
|
a[i] = 0; |
|
628
|
} |
|
629
|
a[i] = mergeNodes(a[i], pX); |
|
630
|
} |
|
631
|
pX = 0; |
|
632
|
for(i=0; i<count(a); i++){ |
|
633
|
pX = mergeNodes(a[i], pX); |
|
634
|
} |
|
635
|
return pX; |
|
636
|
} |
|
637
|
|
|
638
|
/* Sort an entire FileTreeNode tree by mtime |
|
639
|
** |
|
640
|
** This routine invalidates the following fields: |
|
641
|
** |
|
642
|
** FileTreeNode.pLastChild |
|
643
|
** FileTreeNode.pNext |
|
644
|
** |
|
645
|
** Use relinkTree to reconnect the pNext pointers. |
|
646
|
*/ |
|
647
|
static FileTreeNode *sortTree(FileTreeNode *p){ |
|
648
|
FileTreeNode *pX; |
|
649
|
for(pX=p; pX; pX=pX->pSibling){ |
|
650
|
if( pX->pChild ) pX->pChild = sortTree(pX->pChild); |
|
651
|
} |
|
652
|
return sortNodes(p); |
|
653
|
} |
|
654
|
|
|
655
|
/* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext |
|
656
|
** fields in sequential order. |
|
657
|
*/ |
|
658
|
static void relinkTree(FileTree *pTree, FileTreeNode *pRoot){ |
|
659
|
while( pRoot ){ |
|
660
|
if( pTree->pLast ){ |
|
661
|
pTree->pLast->pNext = pRoot; |
|
662
|
}else{ |
|
663
|
pTree->pFirst = pRoot; |
|
664
|
} |
|
665
|
pTree->pLast = pRoot; |
|
666
|
if( pRoot->pChild ) relinkTree(pTree, pRoot->pChild); |
|
667
|
pRoot = pRoot->pSibling; |
|
668
|
} |
|
669
|
if( pTree->pLast ) pTree->pLast->pNext = 0; |
|
670
|
} |
|
671
|
|
|
672
|
|
|
673
|
/* |
|
674
|
** WEBPAGE: tree |
|
675
|
** |
|
676
|
** Show the files using a tree-view. If the ci= query parameter is present |
|
677
|
** then show only the files for the check-in identified. If ci= is omitted, |
|
678
|
** then show the union of files over all check-ins. |
|
679
|
** |
|
680
|
** The type=tree query parameter is required or else the /dir format is |
|
681
|
** used. |
|
682
|
** |
|
683
|
** Query parameters: |
|
684
|
** |
|
685
|
** type=tree Required to prevent use of /dir format |
|
686
|
** name=PATH Directory to display. Optional |
|
687
|
** ci=LABEL Show only files in this check-in. Optional. |
|
688
|
** re=REGEXP Show only files matching REGEXP. Optional. |
|
689
|
** expand Begin with the tree fully expanded. |
|
690
|
** nofiles Show directories (folders) only. Omit files. |
|
691
|
** sort 0: by filename, 1: by mtime, 2: by size |
|
692
|
*/ |
|
693
|
void page_tree(void){ |
|
694
|
char *zD = fossil_strdup(P("name")); |
|
695
|
int nD = zD ? strlen(zD)+1 : 0; |
|
696
|
const char *zCI = P("ci"); |
|
697
|
int rid = 0; |
|
698
|
char *zUuid = 0; |
|
699
|
Blob dirname; |
|
700
|
Manifest *pM = 0; |
|
701
|
double rNow = 0; |
|
702
|
char *zNow = 0; |
|
703
|
int useMtime = atoi(PD("mtime","0")); |
|
704
|
int sortOrder = atoi(PD("sort",useMtime?"1":"0")); |
|
705
|
const char *zRE; /* the value for the re=REGEXP query parameter */ |
|
706
|
const char *zObjType; /* "files" by default or "folders" for "nofiles" */ |
|
707
|
char *zREx = ""; /* Extra parameters for path hyperlinks */ |
|
708
|
ReCompiled *pRE = 0; /* Compiled regular expression */ |
|
709
|
FileTreeNode *p; /* One line of the tree */ |
|
710
|
FileTree sTree; /* The complete tree of files */ |
|
711
|
HQuery sURI; /* Hyperlink */ |
|
712
|
int startExpanded; /* True to start out with the tree expanded */ |
|
713
|
int showDirOnly; /* Show directories only. Omit files */ |
|
714
|
int nDir = 0; /* Number of directories. Used for ID attributes */ |
|
715
|
char *zProjectName = db_get("project-name", 0); |
|
716
|
int isSymbolicCI = 0; /* ci= is a symbolic name, not a hash prefix */ |
|
717
|
int isBranchCI = 0; /* ci= refers to a branch name */ |
|
718
|
char *zHeader = 0; |
|
719
|
|
|
720
|
if( zCI && strlen(zCI)==0 ){ zCI = 0; } |
|
721
|
if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; } |
|
722
|
memset(&sTree, 0, sizeof(sTree)); |
|
723
|
login_check_credentials(); |
|
724
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
725
|
while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; } |
|
726
|
sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0, |
|
727
|
pathelementFunc, 0, 0); |
|
728
|
url_initialize(&sURI, "tree"); |
|
729
|
cgi_query_parameters_to_url(&sURI); |
|
730
|
if( PB("nofiles") ){ |
|
731
|
showDirOnly = 1; |
|
732
|
}else{ |
|
733
|
showDirOnly = 0; |
|
734
|
} |
|
735
|
style_adunit_config(ADUNIT_RIGHT_OK); |
|
736
|
if( PB("expand") ){ |
|
737
|
startExpanded = 1; |
|
738
|
}else{ |
|
739
|
startExpanded = 0; |
|
740
|
} |
|
741
|
|
|
742
|
/* If a regular expression is specified, compile it */ |
|
743
|
zRE = P("re"); |
|
744
|
if( zRE ){ |
|
745
|
fossil_re_compile(&pRE, zRE, 0); |
|
746
|
zREx = mprintf("&re=%T", zRE); |
|
747
|
} |
|
748
|
cgi_check_for_malice(); |
|
749
|
|
|
750
|
/* If the name= parameter is an empty string, make it a NULL pointer */ |
|
751
|
if( zD && strlen(zD)==0 ){ zD = 0; } |
|
752
|
|
|
753
|
/* If a specific check-in is requested, fetch and parse it. If the |
|
754
|
** specific check-in does not exist, clear zCI. zCI==0 will cause all |
|
755
|
** files from all check-ins to be displayed. |
|
756
|
*/ |
|
757
|
if( zCI ){ |
|
758
|
pM = manifest_get_by_name(zCI, &rid); |
|
759
|
if( pM ){ |
|
760
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
761
|
rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
|
762
|
zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
|
763
|
" FROM event WHERE objid=%d", rid); |
|
764
|
isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0); |
|
765
|
isBranchCI = branch_includes_uuid(zCI, zUuid); |
|
766
|
Th_StoreUnsafe("current_checkin", zCI); |
|
767
|
}else{ |
|
768
|
zCI = 0; |
|
769
|
} |
|
770
|
} |
|
771
|
if( zCI==0 ){ |
|
772
|
rNow = db_double(0.0, "SELECT max(mtime) FROM event"); |
|
773
|
zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event"); |
|
774
|
} |
|
775
|
|
|
776
|
/* Generate the Branch list submenu */ |
|
777
|
generate_branch_submenu_multichoice("ci", zCI); |
|
778
|
|
|
779
|
assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) ); |
|
780
|
if( zD==0 ){ |
|
781
|
if( zCI ){ |
|
782
|
zHeader = mprintf("Top-level Files of %s", zCI); |
|
783
|
}else{ |
|
784
|
zHeader = mprintf("All Top-level Files"); |
|
785
|
} |
|
786
|
}else{ |
|
787
|
if( zCI ){ |
|
788
|
zHeader = mprintf("Files in %s/ of %s", zD, zCI); |
|
789
|
}else{ |
|
790
|
zHeader = mprintf("All Files in %s/", zD); |
|
791
|
} |
|
792
|
} |
|
793
|
style_header("%s", zHeader); |
|
794
|
fossil_free(zHeader); |
|
795
|
|
|
796
|
/* Compute the title of the page */ |
|
797
|
blob_zero(&dirname); |
|
798
|
if( zD ){ |
|
799
|
blob_append(&dirname, "within directory ", -1); |
|
800
|
hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0); |
|
801
|
if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE); |
|
802
|
style_submenu_element("Top-Level", "%s", |
|
803
|
url_render(&sURI, "name", 0, 0, 0)); |
|
804
|
}else if( zRE ){ |
|
805
|
blob_appendf(&dirname, "matching \"%s\"", zRE); |
|
806
|
} |
|
807
|
{ |
|
808
|
static const char *const sort_orders[] = { |
|
809
|
"0", "Sort By Filename", |
|
810
|
"1", "Sort By Age", |
|
811
|
"2", "Sort By Size" |
|
812
|
}; |
|
813
|
style_submenu_multichoice("sort", 3, sort_orders, 0); |
|
814
|
} |
|
815
|
if( zCI ){ |
|
816
|
if( nD==0 && !showDirOnly ){ |
|
817
|
style_submenu_element("File Ages", "%R/fileage?name=%T", zCI); |
|
818
|
} |
|
819
|
} |
|
820
|
style_submenu_element("Flat-View", "%s", |
|
821
|
url_render(&sURI, "type", "flat", 0, 0)); |
|
822
|
if( rid && zD==0 && zRE==0 && !showDirOnly && g.perm.Zip ){ |
|
823
|
style_submenu_element("Download","%R/rchvdwnld/%!S", zUuid); |
|
824
|
} |
|
825
|
|
|
826
|
/* Compute the file hierarchy. |
|
827
|
*/ |
|
828
|
if( zCI ){ |
|
829
|
Stmt q; |
|
830
|
compute_fileage(rid, 0); |
|
831
|
db_prepare(&q, |
|
832
|
"SELECT filename.name, blob.uuid, blob.size, fileage.mtime\n" |
|
833
|
" FROM fileage, filename, blob\n" |
|
834
|
" WHERE filename.fnid=fileage.fnid\n" |
|
835
|
" AND blob.rid=fileage.fid\n" |
|
836
|
" ORDER BY filename.name COLLATE uintnocase;" |
|
837
|
); |
|
838
|
while( db_step(&q)==SQLITE_ROW ){ |
|
839
|
const char *zFile = db_column_text(&q,0); |
|
840
|
const char *zUuid = db_column_text(&q,1); |
|
841
|
int size = db_column_int(&q,2); |
|
842
|
double mtime = db_column_double(&q,3); |
|
843
|
if( nD>0 && (fossil_strncmp(zFile, zD, nD-1)!=0 || zFile[nD-1]!='/') ){ |
|
844
|
continue; |
|
845
|
} |
|
846
|
if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue; |
|
847
|
tree_add_node(&sTree, zFile, zUuid, mtime, size, sortOrder); |
|
848
|
} |
|
849
|
db_finalize(&q); |
|
850
|
}else{ |
|
851
|
Stmt q; |
|
852
|
db_prepare(&q, |
|
853
|
"WITH mx(fnid,fid,mtime) AS (\n" |
|
854
|
" SELECT fnid, fid, max(event.mtime)\n" |
|
855
|
" FROM mlink, event\n" |
|
856
|
" WHERE event.objid=mlink.mid\n" |
|
857
|
" GROUP BY 1\n" |
|
858
|
")\n" |
|
859
|
"SELECT\n" |
|
860
|
" filename.name,\n" |
|
861
|
" blob.uuid,\n" |
|
862
|
" blob.size,\n" |
|
863
|
" mx.mtime\n" |
|
864
|
"FROM mx\n" |
|
865
|
" LEFT JOIN filename ON filename.fnid=mx.fnid\n" |
|
866
|
" LEFT JOIN blob ON blob.rid=mx.fid\n" |
|
867
|
" ORDER BY 1 COLLATE uintnocase;"); |
|
868
|
while( db_step(&q)==SQLITE_ROW ){ |
|
869
|
const char *zName = db_column_text(&q, 0); |
|
870
|
const char *zUuid = db_column_text(&q,1); |
|
871
|
int size = db_column_int(&q,2); |
|
872
|
double mtime = db_column_double(&q,3); |
|
873
|
if( nD>0 && (fossil_strncmp(zName, zD, nD-1)!=0 || zName[nD-1]!='/') ){ |
|
874
|
continue; |
|
875
|
} |
|
876
|
if( pRE && re_match(pRE, (const u8*)zName, -1)==0 ) continue; |
|
877
|
tree_add_node(&sTree, zName, zUuid, mtime, size, sortOrder); |
|
878
|
} |
|
879
|
db_finalize(&q); |
|
880
|
} |
|
881
|
style_submenu_checkbox("nofiles", "Folders Only", 0, 0); |
|
882
|
|
|
883
|
if( showDirOnly ){ |
|
884
|
zObjType = "Folders"; |
|
885
|
}else{ |
|
886
|
zObjType = "Files"; |
|
887
|
} |
|
888
|
|
|
889
|
if( zCI && strcmp(zCI,"tip")==0 ){ |
|
890
|
@ <h2>%s(zObjType) in the %z(href("%R/info?name=tip"))latest check-in</a> |
|
891
|
}else if( isBranchCI ){ |
|
892
|
@ <h2>%s(zObjType) in the %z(href("%R/info?name=%T",zCI))latest check-in\ |
|
893
|
@ </a> for branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a> |
|
894
|
if( blob_size(&dirname) ){ |
|
895
|
@ and %s(blob_str(&dirname)) |
|
896
|
} |
|
897
|
}else if( zCI ){ |
|
898
|
@ <h2>%s(zObjType) for check-in \ |
|
899
|
@ %z(href("%R/info?name=%T",zCI))%h(zCI)</a> |
|
900
|
if( blob_size(&dirname) ){ |
|
901
|
@ and %s(blob_str(&dirname)) |
|
902
|
} |
|
903
|
}else{ |
|
904
|
int n = db_int(0, "SELECT count(*) FROM plink"); |
|
905
|
@ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname)) |
|
906
|
} |
|
907
|
if( sortOrder==1 ){ |
|
908
|
@ sorted by modification time</h2> |
|
909
|
}else if( sortOrder==2 ){ |
|
910
|
@ sorted by size</h2> |
|
911
|
}else{ |
|
912
|
@ sorted by filename</h2> |
|
913
|
} |
|
914
|
|
|
915
|
if( zNow ){ |
|
916
|
@ <p>File ages are expressed relative to the check-in time of |
|
917
|
@ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p> |
|
918
|
} |
|
919
|
|
|
920
|
/* Generate tree of lists. |
|
921
|
** |
|
922
|
** Each file and directory is a list element: <li>. Files have class=file |
|
923
|
** and if the filename as the suffix "xyz" the file also has class=file-xyz. |
|
924
|
** Directories have class=dir. The directory specfied by the name= query |
|
925
|
** parameter (or the top-level directory if there is no name= query parameter) |
|
926
|
** adds class=subdir. |
|
927
|
** |
|
928
|
** The <li> element for directories also contains a sublist <ul> |
|
929
|
** for the contents of that directory. |
|
930
|
*/ |
|
931
|
@ <div class="filetree"><ul> |
|
932
|
if( nD ){ |
|
933
|
@ <li class="dir last"> |
|
934
|
}else{ |
|
935
|
@ <li class="dir subdir last"> |
|
936
|
} |
|
937
|
@ <div class="filetreeline"> |
|
938
|
@ %z(href("%s",url_render(&sURI,"name",0,0,0)))%h(zProjectName)</a> |
|
939
|
if( zNow ){ |
|
940
|
@ <div class="filetreeage">Last Change</div> |
|
941
|
@ <div class="filetreesize">Size</div> |
|
942
|
} |
|
943
|
@ </div> |
|
944
|
@ <ul> |
|
945
|
if( sortOrder ){ |
|
946
|
p = sortTree(sTree.pFirst); |
|
947
|
memset(&sTree, 0, sizeof(sTree)); |
|
948
|
relinkTree(&sTree, p); |
|
949
|
} |
|
950
|
for(p=sTree.pFirst, nDir=0; p; p=p->pNext){ |
|
951
|
const char *zLastClass = p->pSibling==0 ? " last" : ""; |
|
952
|
if( p->pChild ){ |
|
953
|
const char *zSubdirClass = (int)(p->nFullName)==nD-1 ? " subdir" : ""; |
|
954
|
@ <li class="dir%s(zSubdirClass)%s(zLastClass)"><div class="filetreeline"> |
|
955
|
@ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a> |
|
956
|
if( p->mtime>0.0 ){ |
|
957
|
char *zAge = human_readable_age(rNow - p->mtime); |
|
958
|
@ <div class="filetreeage">%s(zAge)</div> |
|
959
|
@ <div class="filetreesize"></div> |
|
960
|
} |
|
961
|
@ </div> |
|
962
|
if( startExpanded || (int)(p->nFullName)<=nD ){ |
|
963
|
@ <ul id="dir%d(nDir)"> |
|
964
|
}else{ |
|
965
|
@ <ul id="dir%d(nDir)" class="collapsed"> |
|
966
|
} |
|
967
|
nDir++; |
|
968
|
}else if( !showDirOnly ){ |
|
969
|
const char *zFileClass = fileext_class(p->zName); |
|
970
|
char *zLink; |
|
971
|
if( zCI ){ |
|
972
|
zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI); |
|
973
|
}else{ |
|
974
|
zLink = href("%R/finfo?name=%T",p->zFullName); |
|
975
|
} |
|
976
|
@ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline"> |
|
977
|
@ %z(zLink)%h(p->zName)</a> |
|
978
|
if( p->mtime>0 ){ |
|
979
|
char *zAge = human_readable_age(rNow - p->mtime); |
|
980
|
@ <div class="filetreeage">%s(zAge)</div> |
|
981
|
@ <div class="filetreesize">%s(p->iSize ? mprintf("%,d",p->iSize) : "-")</div> |
|
982
|
} |
|
983
|
@ </div> |
|
984
|
} |
|
985
|
if( p->pSibling==0 ){ |
|
986
|
int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0); |
|
987
|
while( nClose-- > 0 ){ |
|
988
|
@ </ul> |
|
989
|
} |
|
990
|
} |
|
991
|
} |
|
992
|
@ </ul> |
|
993
|
@ </ul></div> |
|
994
|
builtin_request_js("tree.js"); |
|
995
|
style_finish_page(); |
|
996
|
|
|
997
|
/* We could free memory used by sTree here if we needed to. But |
|
998
|
** the process is about to exit, so doing so would not really accomplish |
|
999
|
** anything useful. */ |
|
1000
|
} |
|
1001
|
|
|
1002
|
/* |
|
1003
|
** Return a CSS class name based on the given filename's extension. |
|
1004
|
** Result must be freed by the caller. |
|
1005
|
**/ |
|
1006
|
const char *fileext_class(const char *zFilename){ |
|
1007
|
char *zClass; |
|
1008
|
const char *zExt = strrchr(zFilename, '.'); |
|
1009
|
int isExt = zExt && zExt!=zFilename && zExt[1]; |
|
1010
|
int i; |
|
1011
|
for( i=1; isExt && zExt[i]; i++ ) isExt &= fossil_isalnum(zExt[i]); |
|
1012
|
if( isExt ){ |
|
1013
|
zClass = mprintf("file file-%s", zExt+1); |
|
1014
|
for( i=5; zClass[i]; i++ ) zClass[i] = fossil_tolower(zClass[i]); |
|
1015
|
}else{ |
|
1016
|
zClass = mprintf("file"); |
|
1017
|
} |
|
1018
|
return zClass; |
|
1019
|
} |
|
1020
|
|
|
1021
|
/* |
|
1022
|
** SQL used to compute the age of all files in check-in :ckin whose |
|
1023
|
** names match :glob |
|
1024
|
*/ |
|
1025
|
static const char zComputeFileAgeSetup[] = |
|
1026
|
@ CREATE TABLE IF NOT EXISTS temp.fileage( |
|
1027
|
@ fnid INTEGER PRIMARY KEY, |
|
1028
|
@ fid INTEGER, |
|
1029
|
@ mid INTEGER, |
|
1030
|
@ mtime DATETIME, |
|
1031
|
@ pathname TEXT, |
|
1032
|
@ uuid TEXT |
|
1033
|
@ ); |
|
1034
|
@ CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin; |
|
1035
|
; |
|
1036
|
|
|
1037
|
static const char zComputeFileAgeRun[] = |
|
1038
|
@ WITH RECURSIVE |
|
1039
|
@ ckin(x) AS (VALUES(:ckin) |
|
1040
|
@ UNION |
|
1041
|
@ SELECT plink.pid |
|
1042
|
@ FROM ckin, plink |
|
1043
|
@ WHERE plink.cid=ckin.x) |
|
1044
|
@ INSERT OR IGNORE INTO fileage(fnid, fid, mid, mtime, pathname, uuid) |
|
1045
|
@ SELECT filename.fnid, mlink.fid, mlink.mid, event.mtime, filename.name, |
|
1046
|
@ foci.uuid |
|
1047
|
@ FROM foci, filename, blob, mlink, event |
|
1048
|
@ WHERE foci.checkinID=:ckin |
|
1049
|
@ AND foci.filename GLOB :glob |
|
1050
|
@ AND filename.name=foci.filename |
|
1051
|
@ AND blob.uuid=foci.uuid |
|
1052
|
@ AND mlink.fid=blob.rid |
|
1053
|
@ AND mlink.fid!=mlink.pid |
|
1054
|
@ AND mlink.mid IN (SELECT x FROM ckin) |
|
1055
|
@ AND event.objid=mlink.mid |
|
1056
|
@ ORDER BY event.mtime ASC; |
|
1057
|
; |
|
1058
|
|
|
1059
|
/* |
|
1060
|
** Look at all file containing in the version "vid". Construct a |
|
1061
|
** temporary table named "fileage" that contains the file-id for each |
|
1062
|
** files, the pathname, the check-in where the file was added, and the |
|
1063
|
** mtime on that check-in. If zGlob and *zGlob then only files matching |
|
1064
|
** the given glob are computed. |
|
1065
|
*/ |
|
1066
|
int compute_fileage(int vid, const char* zGlob){ |
|
1067
|
Stmt q; |
|
1068
|
db_exec_sql(zComputeFileAgeSetup); |
|
1069
|
db_prepare(&q, zComputeFileAgeRun /*works-like:"constant"*/); |
|
1070
|
db_bind_int(&q, ":ckin", vid); |
|
1071
|
db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*"); |
|
1072
|
db_exec(&q); |
|
1073
|
db_finalize(&q); |
|
1074
|
return 0; |
|
1075
|
} |
|
1076
|
|
|
1077
|
/* |
|
1078
|
** Render the number of days in rAge as a more human-readable time span. |
|
1079
|
** Different units (seconds, minutes, hours, days, months, years) are |
|
1080
|
** selected depending on the magnitude of rAge. |
|
1081
|
** |
|
1082
|
** The string returned is obtained from fossil_malloc() and should be |
|
1083
|
** freed by the caller. |
|
1084
|
*/ |
|
1085
|
char *human_readable_age(double rAge){ |
|
1086
|
if( rAge*86400.0<120 ){ |
|
1087
|
if( rAge*86400.0<1.0 ){ |
|
1088
|
return mprintf("current"); |
|
1089
|
}else{ |
|
1090
|
return mprintf("%d seconds", (int)(rAge*86400.0)); |
|
1091
|
} |
|
1092
|
}else if( rAge*1440.0<90 ){ |
|
1093
|
return mprintf("%.1f minutes", rAge*1440.0); |
|
1094
|
}else if( rAge*24.0<36 ){ |
|
1095
|
return mprintf("%.1f hours", rAge*24.0); |
|
1096
|
}else if( rAge<365.0 ){ |
|
1097
|
return mprintf("%.1f days", rAge); |
|
1098
|
}else{ |
|
1099
|
return mprintf("%.2f years", rAge/365.2425); |
|
1100
|
} |
|
1101
|
} |
|
1102
|
|
|
1103
|
/* |
|
1104
|
** COMMAND: test-fileage |
|
1105
|
** |
|
1106
|
** Usage: %fossil test-fileage CHECKIN |
|
1107
|
*/ |
|
1108
|
void test_fileage_cmd(void){ |
|
1109
|
int mid; |
|
1110
|
Stmt q; |
|
1111
|
const char *zGlob = find_option("glob",0,1); |
|
1112
|
db_find_and_open_repository(0,0); |
|
1113
|
verify_all_options(); |
|
1114
|
if( g.argc!=3 ) usage("CHECKIN"); |
|
1115
|
mid = name_to_typed_rid(g.argv[2],"ci"); |
|
1116
|
compute_fileage(mid, zGlob); |
|
1117
|
db_prepare(&q, |
|
1118
|
"SELECT fid, mid, julianday('now') - mtime, pathname" |
|
1119
|
" FROM fileage" |
|
1120
|
); |
|
1121
|
while( db_step(&q)==SQLITE_ROW ){ |
|
1122
|
char *zAge = human_readable_age(db_column_double(&q,2)); |
|
1123
|
fossil_print("%8d %8d %16s %s\n", |
|
1124
|
db_column_int(&q,0), |
|
1125
|
db_column_int(&q,1), |
|
1126
|
zAge, |
|
1127
|
db_column_text(&q,3)); |
|
1128
|
fossil_free(zAge); |
|
1129
|
} |
|
1130
|
db_finalize(&q); |
|
1131
|
} |
|
1132
|
|
|
1133
|
/* |
|
1134
|
** WEBPAGE: fileage |
|
1135
|
** |
|
1136
|
** Show all files in a single check-in (identified by the name= query |
|
1137
|
** parameter) in order of increasing age. |
|
1138
|
** |
|
1139
|
** Parameters: |
|
1140
|
** name=VERSION Selects the check-in version (default=tip). |
|
1141
|
** glob=STRING Only shows files matching this glob pattern |
|
1142
|
** (e.g. *.c or *.txt). |
|
1143
|
** showid Show RID values for debugging |
|
1144
|
*/ |
|
1145
|
void fileage_page(void){ |
|
1146
|
int rid; |
|
1147
|
const char *zName; |
|
1148
|
const char *zGlob; |
|
1149
|
const char *zUuid; |
|
1150
|
const char *zNow; /* Time of check-in */ |
|
1151
|
int isBranchCI; /* name= is a branch name */ |
|
1152
|
int showId = PB("showid"); |
|
1153
|
Stmt q1, q2; |
|
1154
|
double baseTime; |
|
1155
|
login_check_credentials(); |
|
1156
|
if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
|
1157
|
zName = P("name"); |
|
1158
|
if( zName==0 ) zName = "tip"; |
|
1159
|
rid = symbolic_name_to_rid(zName, "ci"); |
|
1160
|
if( rid==0 ){ |
|
1161
|
fossil_fatal("not a valid check-in: %s", zName); |
|
1162
|
} |
|
1163
|
zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
1164
|
isBranchCI = branch_includes_uuid(zName,zUuid); |
|
1165
|
baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid); |
|
1166
|
zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event" |
|
1167
|
" WHERE objid=%d", rid); |
|
1168
|
style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName); |
|
1169
|
|
|
1170
|
/* Generate the Branch list submenu */ |
|
1171
|
generate_branch_submenu_multichoice("name", zName); |
|
1172
|
|
|
1173
|
style_header("File Ages"); |
|
1174
|
zGlob = P("glob"); |
|
1175
|
cgi_check_for_malice(); |
|
1176
|
compute_fileage(rid,zGlob); |
|
1177
|
db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);"); |
|
1178
|
|
|
1179
|
if( fossil_strcmp(zName,"tip")==0 ){ |
|
1180
|
@ <h1>Files in the %z(href("%R/info?name=tip"))latest check-in</a> |
|
1181
|
}else if( isBranchCI ){ |
|
1182
|
@ <h1>Files in the %z(href("%R/info?name=%T",zName))latest check-in</a> |
|
1183
|
@ of branch %z(href("%R/timeline?r=%T",zName))%h(zName)</a> |
|
1184
|
}else{ |
|
1185
|
@ <h1>Files in check-in %z(href("%R/info?name=%T",zName))%h(zName)</a> |
|
1186
|
} |
|
1187
|
if( zGlob && zGlob[0] ){ |
|
1188
|
@ that match "%h(zGlob)" |
|
1189
|
} |
|
1190
|
@ ordered by age</h1> |
|
1191
|
@ |
|
1192
|
@ <p>File ages are expressed relative to the check-in time of |
|
1193
|
@ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p> |
|
1194
|
@ |
|
1195
|
@ <div class='fileage'><table> |
|
1196
|
@ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr> |
|
1197
|
db_prepare(&q1, |
|
1198
|
"SELECT event.mtime, event.objid, blob.uuid,\n" |
|
1199
|
" coalesce(event.ecomment,event.comment),\n" |
|
1200
|
" coalesce(event.euser,event.user),\n" |
|
1201
|
" coalesce((SELECT value FROM tagxref\n" |
|
1202
|
" WHERE tagtype>0 AND tagid=%d\n" |
|
1203
|
" AND rid=event.objid),'trunk')\n" |
|
1204
|
" FROM event, blob\n" |
|
1205
|
" WHERE event.objid IN (SELECT mid FROM fileage)\n" |
|
1206
|
" AND blob.rid=event.objid\n" |
|
1207
|
" ORDER BY event.mtime DESC;", |
|
1208
|
TAG_BRANCH |
|
1209
|
); |
|
1210
|
db_prepare(&q2, |
|
1211
|
"SELECT filename.name, fileage.fid\n" |
|
1212
|
" FROM fileage, filename\n" |
|
1213
|
" WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid" |
|
1214
|
); |
|
1215
|
while( db_step(&q1)==SQLITE_ROW ){ |
|
1216
|
double age = baseTime - db_column_double(&q1, 0); |
|
1217
|
int mid = db_column_int(&q1, 1); |
|
1218
|
const char *zUuid = db_column_text(&q1, 2); |
|
1219
|
const char *zComment = db_column_text(&q1, 3); |
|
1220
|
const char *zUser = db_column_text(&q1, 4); |
|
1221
|
const char *zBranch = db_column_text(&q1, 5); |
|
1222
|
char *zAge = human_readable_age(age); |
|
1223
|
@ <tr><td>%s(zAge)</td> |
|
1224
|
@ <td> |
|
1225
|
db_bind_int(&q2, ":mid", mid); |
|
1226
|
while( db_step(&q2)==SQLITE_ROW ){ |
|
1227
|
const char *zFile = db_column_text(&q2,0); |
|
1228
|
@ %z(href("%R/file?name=%T&ci=%!S",zFile,zUuid))%h(zFile)</a> \ |
|
1229
|
if( showId ){ |
|
1230
|
int fid = db_column_int(&q2,1); |
|
1231
|
@ (%d(fid))<br> |
|
1232
|
}else{ |
|
1233
|
@ </a><br> |
|
1234
|
} |
|
1235
|
} |
|
1236
|
db_reset(&q2); |
|
1237
|
@ </td> |
|
1238
|
@ <td> |
|
1239
|
@ %W(zComment) |
|
1240
|
@ (check-in: %z(href("%R/info/%!S",zUuid))%S(zUuid)</a>, |
|
1241
|
if( showId ){ |
|
1242
|
@ id: %d(mid) |
|
1243
|
} |
|
1244
|
@ user: %z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>, |
|
1245
|
@ branch: \ |
|
1246
|
@ %z(href("%R/timeline?r=%t&c=%!S&nd",zBranch,zUuid))%h(zBranch)</a>) |
|
1247
|
@ </td></tr> |
|
1248
|
@ |
|
1249
|
fossil_free(zAge); |
|
1250
|
} |
|
1251
|
@ </table></div> |
|
1252
|
db_finalize(&q1); |
|
1253
|
db_finalize(&q2); |
|
1254
|
style_finish_page(); |
|
1255
|
} |
|
1256
|
|