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