Fossil SCM

fossil-scm / src / browse.c
Blame History Raw 1256 lines
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:&nbsp;%z(href("%R/info/%!S",zUuid))%S(zUuid)</a>,
1241
if( showId ){
1242
@ id: %d(mid)
1243
}
1244
@ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
1245
@ branch:&nbsp;\
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

Keyboard Shortcuts

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