Fossil SCM

Merge the age-in-file-tree changes into trunk.

drh 2014-12-17 00:16 trunk merge
Commit 0183c9a29250bd2d15a248c571998b1571069802
2 files changed +239 -78 +10 -1
+239 -78
--- src/browse.c
+++ src/browse.c
@@ -304,89 +304,206 @@
304304
305305
/*
306306
** A single line of the file hierarchy
307307
*/
308308
struct FileTreeNode {
309
- FileTreeNode *pNext; /* Next line in sequence */
310
- FileTreeNode *pPrev; /* Previous line */
311
- FileTreeNode *pParent; /* Directory containing this line */
309
+ FileTreeNode *pNext; /* Next entry in an ordered list of them all */
310
+ FileTreeNode *pParent; /* Directory containing this entry */
311
+ FileTreeNode *pSibling; /* Next element in the same subdirectory */
312
+ FileTreeNode *pChild; /* List of child nodes */
313
+ FileTreeNode *pLastChild; /* Last child on the pChild list */
312314
char *zName; /* Name of this entry. The "tail" */
313315
char *zFullName; /* Full pathname of this entry */
314316
char *zUuid; /* SHA1 hash of this file. May be NULL. */
317
+ double mtime; /* Modification time for this entry */
315318
unsigned nFullName; /* Length of zFullName */
316319
unsigned iLevel; /* Levels of parent directories */
317
- u8 isDir; /* True if there are children */
318
- u8 isLast; /* True if this is the last child of its parent */
319320
};
320321
321322
/*
322323
** A complete file hierarchy
323324
*/
324325
struct FileTree {
325326
FileTreeNode *pFirst; /* First line of the list */
326327
FileTreeNode *pLast; /* Last line of the list */
328
+ FileTreeNode *pLastTop; /* Last top-level node */
327329
};
328330
329331
/*
330332
** Add one or more new FileTreeNodes to the FileTree object so that the
331
-** leaf object zPathname is at the end of the node list
333
+** leaf object zPathname is at the end of the node list.
334
+**
335
+** The caller invokes this routine once for each leaf node (each file
336
+** as opposed to each directory). This routine fills in any missing
337
+** intermediate nodes automatically.
338
+**
339
+** When constructing a list of FileTreeNodes, all entries that have
340
+** a common directory prefix must be added consecutively in order for
341
+** the tree to be constructed properly.
332342
*/
333343
static void tree_add_node(
334344
FileTree *pTree, /* Tree into which nodes are added */
335345
const char *zPath, /* The full pathname of file to add */
336
- const char *zUuid /* UUID of the file. Might be NULL. */
346
+ const char *zUuid, /* UUID of the file. Might be NULL. */
347
+ double mtime /* Modification time for this entry */
337348
){
338349
int i;
339
- FileTreeNode *pParent;
340
- FileTreeNode *pChild;
350
+ FileTreeNode *pParent; /* Parent (directory) of the next node to insert */
341351
342
- pChild = pTree->pLast;
343
- pParent = pChild ? pChild->pParent : 0;
352
+ /* Make pParent point to the most recent ancestor of zPath, or
353
+ ** NULL if there are no prior entires that are a container for zPath.
354
+ */
355
+ pParent = pTree->pLast;
344356
while( pParent!=0 &&
345357
( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0
346358
|| zPath[pParent->nFullName]!='/' )
347359
){
348
- pChild = pParent;
349
- pParent = pChild->pParent;
360
+ pParent = pParent->pParent;
350361
}
351362
i = pParent ? pParent->nFullName+1 : 0;
352
- if( pChild ) pChild->isLast = 0;
353363
while( zPath[i] ){
354364
FileTreeNode *pNew;
355365
int iStart = i;
356366
int nByte;
357367
while( zPath[i] && zPath[i]!='/' ){ i++; }
358368
nByte = sizeof(*pNew) + i + 1;
359369
if( zUuid!=0 && zPath[i]==0 ) nByte += UUID_SIZE+1;
360370
pNew = fossil_malloc( nByte );
371
+ memset(pNew, 0, sizeof(*pNew));
361372
pNew->zFullName = (char*)&pNew[1];
362373
memcpy(pNew->zFullName, zPath, i);
363374
pNew->zFullName[i] = 0;
364375
pNew->nFullName = i;
365376
if( zUuid!=0 && zPath[i]==0 ){
366377
pNew->zUuid = pNew->zFullName + i + 1;
367378
memcpy(pNew->zUuid, zUuid, UUID_SIZE+1);
368
- }else{
369
- pNew->zUuid = 0;
370379
}
371380
pNew->zName = pNew->zFullName + iStart;
372381
if( pTree->pLast ){
373382
pTree->pLast->pNext = pNew;
374383
}else{
375384
pTree->pFirst = pNew;
376385
}
377
- pNew->pPrev = pTree->pLast;
378
- pNew->pNext = 0;
386
+ pTree->pLast = pNew;
379387
pNew->pParent = pParent;
380
- pTree->pLast = pNew;
381
- pNew->iLevel = pParent ? pParent->iLevel+1 : 0;
382
- pNew->isDir = zPath[i]=='/';
383
- pNew->isLast = 1;
388
+ if( pParent ){
389
+ if( pParent->pChild ){
390
+ pParent->pLastChild->pSibling = pNew;
391
+ }else{
392
+ pParent->pChild = pNew;
393
+ }
394
+ pNew->iLevel = pParent->iLevel + 1;
395
+ pParent->pLastChild = pNew;
396
+ }else{
397
+ if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew;
398
+ pTree->pLastTop = pNew;
399
+ }
400
+ pNew->mtime = mtime;
384401
while( zPath[i]=='/' ){ i++; }
385402
pParent = pNew;
386403
}
404
+ while( pParent && pParent->pParent ){
405
+ if( pParent->pParent->mtime < pParent->mtime ){
406
+ pParent->pParent->mtime = pParent->mtime;
407
+ }
408
+ pParent = pParent->pParent;
409
+ }
410
+}
411
+
412
+/* Comparison function for two FileTreeNode objects. Sort first by
413
+** mtime (larger numbers first) and then by zName (smaller names first).
414
+**
415
+** Return negative if pLeft<pRight.
416
+** Return positive if pLeft>pRight.
417
+** Return zero if pLeft==pRight.
418
+*/
419
+static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
420
+ if( pLeft->mtime>pRight->mtime ) return -1;
421
+ if( pLeft->mtime<pRight->mtime ) return +1;
422
+ return fossil_stricmp(pLeft->zName, pRight->zName);
423
+}
424
+
425
+/* Merge together two sorted lists of FileTreeNode objects */
426
+static FileTreeNode *mergeNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
427
+ FileTreeNode *pEnd;
428
+ FileTreeNode base;
429
+ pEnd = &base;
430
+ while( pLeft && pRight ){
431
+ if( compareNodes(pLeft,pRight)<=0 ){
432
+ pEnd = pEnd->pSibling = pLeft;
433
+ pLeft = pLeft->pSibling;
434
+ }else{
435
+ pEnd = pEnd->pSibling = pRight;
436
+ pRight = pRight->pSibling;
437
+ }
438
+ }
439
+ if( pLeft ){
440
+ pEnd->pSibling = pLeft;
441
+ }else{
442
+ pEnd->pSibling = pRight;
443
+ }
444
+ return base.pSibling;
445
+}
446
+
447
+/* Sort a list of FileTreeNode objects in mtime order. */
448
+static FileTreeNode *sortNodesByMtime(FileTreeNode *p){
449
+ FileTreeNode *a[30];
450
+ FileTreeNode *pX;
451
+ int i;
452
+
453
+ memset(a, 0, sizeof(a));
454
+ while( p ){
455
+ pX = p;
456
+ p = pX->pSibling;
457
+ pX->pSibling = 0;
458
+ for(i=0; i<count(a)-1 && a[i]!=0; i++){
459
+ pX = mergeNodes(a[i], pX);
460
+ a[i] = 0;
461
+ }
462
+ a[i] = mergeNodes(a[i], pX);
463
+ }
464
+ pX = 0;
465
+ for(i=0; i<count(a); i++){
466
+ pX = mergeNodes(a[i], pX);
467
+ }
468
+ return pX;
469
+}
470
+
471
+/* Sort an entire FileTreeNode tree by mtime
472
+**
473
+** This routine invalidates the following fields:
474
+**
475
+** FileTreeNode.pLastChild
476
+** FileTreeNode.pNext
477
+**
478
+** Use relinkTree to reconnect the pNext pointers.
479
+*/
480
+static FileTreeNode *sortTreeByMtime(FileTreeNode *p){
481
+ FileTreeNode *pX;
482
+ for(pX=p; pX; pX=pX->pSibling){
483
+ if( pX->pChild ) pX->pChild = sortTreeByMtime(pX->pChild);
484
+ }
485
+ return sortNodesByMtime(p);
486
+}
487
+
488
+/* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext
489
+** fields in sequential order.
490
+*/
491
+static void relinkTree(FileTree *pTree, FileTreeNode *pRoot){
492
+ while( pRoot ){
493
+ if( pTree->pLast ){
494
+ pTree->pLast->pNext = pRoot;
495
+ }else{
496
+ pTree->pFirst = pRoot;
497
+ }
498
+ pTree->pLast = pRoot;
499
+ if( pRoot->pChild ) relinkTree(pTree, pRoot->pChild);
500
+ pRoot = pRoot->pSibling;
501
+ }
502
+ if( pTree->pLast ) pTree->pLast->pNext = 0;
387503
}
504
+
388505
389506
/*
390507
** WEBPAGE: tree
391508
**
392509
** Query parameters:
@@ -394,19 +511,23 @@
394511
** name=PATH Directory to display. Optional
395512
** ci=LABEL Show only files in this check-in. Optional.
396513
** re=REGEXP Show only files matching REGEXP. Optional.
397514
** expand Begin with the tree fully expanded.
398515
** nofiles Show directories (folders) only. Omit files.
516
+** mtime Order directory elements by decreasing mtime
399517
*/
400518
void page_tree(void){
401519
char *zD = fossil_strdup(P("name"));
402520
int nD = zD ? strlen(zD)+1 : 0;
403521
const char *zCI = P("ci");
404522
int rid = 0;
405523
char *zUuid = 0;
406524
Blob dirname;
407525
Manifest *pM = 0;
526
+ double rNow = 0;
527
+ char *zNow = 0;
528
+ int useMtime = atoi(PD("mtime","0"));
408529
int nFile = 0; /* Number of files (or folders with "nofiles") */
409530
int linkTrunk = 1; /* include link to "trunk" */
410531
int linkTip = 1; /* include link to "tip" */
411532
const char *zRE; /* the value for the re=REGEXP query parameter */
412533
const char *zObjType; /* "files" by default or "folders" for "nofiles" */
@@ -464,13 +585,18 @@
464585
int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
465586
linkTrunk = trunkRid && rid != trunkRid;
466587
linkTip = rid != symbolic_name_to_rid("tip", "ci");
467588
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
468589
url_add_parameter(&sURI, "ci", zCI);
590
+ rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
591
+ zNow = db_text("", "SELECT datetime(mtime,'localtime')"
592
+ " FROM event WHERE objid=%d", rid);
469593
}else{
470594
zCI = 0;
471595
}
596
+ }else{
597
+ useMtime = 0;
472598
}
473599
474600
/* Compute the title of the page */
475601
blob_zero(&dirname);
476602
if( zD ){
@@ -483,62 +609,57 @@
483609
}else{
484610
if( zRE ){
485611
blob_appendf(&dirname, "matching \"%s\"", zRE);
486612
}
487613
}
614
+ if( !showDirOnly ){
615
+ style_submenu_element("Flat-View", "Flat-View", "%s",
616
+ url_render(&sURI, "type", "flat", 0, 0));
617
+ }
488618
if( zCI ){
489619
style_submenu_element("All", "All", "%s",
490620
url_render(&sURI, "ci", 0, 0, 0));
491621
if( nD==0 && !showDirOnly ){
492622
style_submenu_element("File Ages", "File Ages", "%R/fileage?name=%s",
493623
zUuid);
494624
}
625
+ if( useMtime ){
626
+ style_submenu_element("Sort By Filename","Sort By Filename", "%s",
627
+ url_render(&sURI, 0, 0, 0, 0));
628
+ url_add_parameter(&sURI, "mtime", "1");
629
+ }else{
630
+ style_submenu_element("Sort By Time","Sort By Time", "%s",
631
+ url_render(&sURI, "mtime", "1", 0, 0));
632
+ }
495633
}
496634
if( linkTrunk ){
497635
style_submenu_element("Trunk", "Trunk", "%s",
498636
url_render(&sURI, "ci", "trunk", 0, 0));
499637
}
500638
if( linkTip ){
501639
style_submenu_element("Tip", "Tip", "%s",
502640
url_render(&sURI, "ci", "tip", 0, 0));
503641
}
504
- if( !showDirOnly ){
505
- style_submenu_element("Flat-View", "Flat-View", "%s",
506
- url_render(&sURI, "type", "flat", 0, 0));
507
- }
508642
509643
/* Compute the file hierarchy.
510644
*/
511645
if( zCI ){
512
- Stmt ins, q;
513
- ManifestFile *pFile;
514
-
515
- db_multi_exec(
516
- "CREATE TEMP TABLE filelist("
517
- " x TEXT PRIMARY KEY COLLATE nocase,"
518
- " uuid TEXT"
519
- ") WITHOUT ROWID;"
646
+ Stmt q;
647
+ compute_fileage(rid, 0);
648
+ db_prepare(&q,
649
+ "SELECT filename.name, blob.uuid, fileage.mtime\n"
650
+ " FROM fileage, filename, blob\n"
651
+ " WHERE filename.fnid=fileage.fnid\n"
652
+ " AND blob.rid=fileage.fid\n"
653
+ " ORDER BY filename.name COLLATE nocase;"
520654
);
521
- db_prepare(&ins, "INSERT OR IGNORE INTO filelist VALUES(:f,:u)");
522
- manifest_file_rewind(pM);
523
- while( (pFile = manifest_file_next(pM,0))!=0 ){
524
- if( nD>0
525
- && (fossil_strncmp(pFile->zName, zD, nD-1)!=0
526
- || pFile->zName[nD-1]!='/')
527
- ){
528
- continue;
529
- }
530
- if( pRE && re_match(pRE, (const u8*)pFile->zName, -1)==0 ) continue;
531
- db_bind_text(&ins, ":f", pFile->zName);
532
- db_bind_text(&ins, ":u", pFile->zUuid);
533
- db_step(&ins);
534
- db_reset(&ins);
535
- }
536
- db_finalize(&ins);
537
- db_prepare(&q, "SELECT x, uuid FROM filelist ORDER BY x");
538655
while( db_step(&q)==SQLITE_ROW ){
539
- tree_add_node(&sTree, db_column_text(&q,0), db_column_text(&q,1));
656
+ const char *zFile = db_column_text(&q,0);
657
+ const char *zUuid = db_column_text(&q,1);
658
+ double mtime = db_column_double(&q,2);
659
+ if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue;
660
+ tree_add_node(&sTree, zFile, zUuid, mtime);
540661
nFile++;
541662
}
542663
db_finalize(&q);
543664
}else{
544665
Stmt q;
@@ -547,38 +668,43 @@
547668
const char *z = db_column_text(&q, 0);
548669
if( nD>0 && (fossil_strncmp(z, zD, nD-1)!=0 || z[nD-1]!='/') ){
549670
continue;
550671
}
551672
if( pRE && re_match(pRE, (const u8*)z, -1)==0 ) continue;
552
- tree_add_node(&sTree, z, 0);
673
+ tree_add_node(&sTree, z, 0, 0.0);
553674
nFile++;
554675
}
555676
db_finalize(&q);
556677
}
557678
558679
if( showDirOnly ){
559680
for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
560
- if( p->isDir && p->nFullName>nD ) nFile++;
681
+ if( p->pChild!=0 && p->nFullName>nD ) nFile++;
561682
}
562
- zObjType = "folders";
683
+ zObjType = "Folders";
563684
style_submenu_element("Files","Files","%s",
564685
url_render(&sURI,"nofiles",0,0,0));
565686
}else{
566
- zObjType = "files";
687
+ zObjType = "Files";
567688
style_submenu_element("Folders","Folders","%s",
568689
url_render(&sURI,"nofiles","1",0,0));
569690
}
570691
571692
if( zCI ){
572
- @ <h2>%d(nFile) %s(zObjType) of check-in
693
+ @ <h2>%s(zObjType) from
573694
if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){
574695
@ "%h(zCI)"
575696
}
576
- @ [%z(href("vinfo?name=%s",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname))</h2>
697
+ @ [%z(href("vinfo?name=%s",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname))
698
+ if( useMtime ){
699
+ @ sorted by modification time</h2>
700
+ }else{
701
+ @ sorted by filename</h2>
702
+ }
577703
}else{
578704
int n = db_int(0, "SELECT count(*) FROM plink");
579
- @ <h2>%d(nFile) %s(zObjType) from all %d(n) check-ins
705
+ @ <h2>%s(zObjType) from all %d(n) check-ins
580706
@ %s(blob_str(&dirname))</h2>
581707
}
582708
583709
584710
/* Generate tree of lists.
@@ -596,18 +722,33 @@
596722
if( nD ){
597723
@ <li class="dir last">
598724
}else{
599725
@ <li class="dir subdir last">
600726
}
727
+ @ <div class="filetreeline">
601728
@ %z(href("%s",url_render(&sURI,"name",0,0,0)))%h(zProjectName)</a>
729
+ if( zNow ){
730
+ @ <div class="filetreeage">%s(zNow)</div>
731
+ }
732
+ @ </div>
602733
@ <ul>
734
+ if( zCI && useMtime ){
735
+ p = sortTreeByMtime(sTree.pFirst);
736
+ memset(&sTree, 0, sizeof(sTree));
737
+ relinkTree(&sTree, p);
738
+ }
603739
for(p=sTree.pFirst, nDir=0; p; p=p->pNext){
604
- const char *zLastClass = p->isLast ? " last" : "";
605
- if( p->isDir ){
740
+ const char *zLastClass = p->pSibling==0 ? " last" : "";
741
+ if( p->pChild ){
606742
const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : "";
607
- @ <li class="dir%s(zSubdirClass)%s(zLastClass)">
743
+ @ <li class="dir%s(zSubdirClass)%s(zLastClass)"><div class="filetreeline">
608744
@ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a>
745
+ if( p->mtime>0.0 ){
746
+ char *zAge = human_readable_age(rNow - p->mtime);
747
+ @ <div class="filetreeage">%s(zAge)</div>
748
+ }
749
+ @ </div>
609750
if( startExpanded || p->nFullName<=nD ){
610751
@ <ul id="dir%d(nDir)">
611752
}else{
612753
@ <ul id="dir%d(nDir)" class="collapsed">
613754
}
@@ -618,13 +759,19 @@
618759
if( zCI ){
619760
zLink = href("%R/artifact/%s",p->zUuid);
620761
}else{
621762
zLink = href("%R/finfo?name=%T",p->zFullName);
622763
}
623
- @ <li class="%z(zFileClass)%s(zLastClass)">%z(zLink)%h(p->zName)</a>
764
+ @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline">
765
+ @ %z(zLink)%h(p->zName)</a>
766
+ if( p->mtime>0 ){
767
+ char *zAge = human_readable_age(rNow - p->mtime);
768
+ @ <div class="filetreeage">%s(zAge)</div>
769
+ }
770
+ @ </div>
624771
}
625
- if( p->isLast ){
772
+ if( p->pSibling==0 ){
626773
int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0);
627774
while( nClose-- > 0 ){
628775
@ </ul>
629776
}
630777
}
@@ -687,16 +834,16 @@
687834
@ checkState();
688835
@ outer_ul.onclick = function(e){
689836
@ e = e || window.event;
690837
@ var a = e.target || e.srcElement;
691838
@ if( a.nodeName!='A' ) return true;
692
- @ if( a.parentNode==subdir ){
839
+ @ if( a.parentNode.parentNode==subdir ){
693840
@ toggleAll(outer_ul);
694841
@ return false;
695842
@ }
696843
@ if( !belowSubdir(a) ) return true;
697
- @ var ul = a.nextSibling;
844
+ @ var ul = a.parentNode.nextSibling;
698845
@ while( ul && ul.nodeName!='UL' ) ul = ul.nextSibling;
699846
@ if( !ul ) return true; /* This is a file link, not a directory */
700847
@ toggleDir(ul);
701848
@ return false;
702849
@ }
@@ -785,19 +932,23 @@
785932
** The string returned is obtained from fossil_malloc() and should be
786933
** freed by the caller.
787934
*/
788935
char *human_readable_age(double rAge){
789936
if( rAge*86400.0<120 ){
790
- return mprintf("%d seconds", (int)(rAge*86400.0));
937
+ if( rAge*86400.0<1.0 ){
938
+ return mprintf("current");
939
+ }else{
940
+ return mprintf("-%d seconds", (int)(rAge*86400.0));
941
+ }
791942
}else if( rAge*1440.0<90 ){
792
- return mprintf("%.1f minutes", rAge*1440.0);
943
+ return mprintf("-%.1f minutes", rAge*1440.0);
793944
}else if( rAge*24.0<36 ){
794
- return mprintf("%.1f hours", rAge*24.0);
945
+ return mprintf("-%.1f hours", rAge*24.0);
795946
}else if( rAge<365.0 ){
796
- return mprintf("%.1f days", rAge);
947
+ return mprintf("-%.1f days", rAge);
797948
}else{
798
- return mprintf("%.2f years", rAge/365.0);
949
+ return mprintf("-%.2f years", rAge/365.0);
799950
}
800951
}
801952
802953
/*
803954
** COMMAND: test-fileage
@@ -839,10 +990,12 @@
839990
*/
840991
void fileage_page(void){
841992
int rid;
842993
const char *zName;
843994
const char *zGlob;
995
+ const char *zUuid;
996
+ const char *zNow; /* Time of checkin */
844997
Stmt q1, q2;
845998
double baseTime;
846999
login_check_credentials();
8471000
if( !g.perm.Read ){ login_needed(); return; }
8481001
zName = P("name");
@@ -849,26 +1002,34 @@
8491002
if( zName==0 ) zName = "tip";
8501003
rid = symbolic_name_to_rid(zName, "ci");
8511004
if( rid==0 ){
8521005
fossil_fatal("not a valid check-in: %s", zName);
8531006
}
854
- style_submenu_element("Tree-View", "Tree-View", "%R/tree?ci=%T", zName);
1007
+ zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
1008
+ baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid);
1009
+ zNow = db_text("", "SELECT datetime(mtime,'localtime') FROM event"
1010
+ " WHERE objid=%d", rid);
1011
+ style_submenu_element("Tree-View", "Tree-View", "%R/tree?ci=%T&mtime=1",
1012
+ zName);
8551013
style_header("File Ages");
8561014
zGlob = P("glob");
8571015
compute_fileage(rid,zGlob);
8581016
db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
8591017
860
- baseTime = db_double(0.0, "SELECT julianday('now');");
861
- @ <h2>Most recent change to files in checkin
862
- @ %z(href("%R/info?name=%T",zName))%h(zName)</a>
1018
+ @ <h2>Files in
1019
+ @ %z(href("%R/info?name=%T",zUuid))[%S(zUuid)]</a>
8631020
if( zGlob && zGlob[0] ){
864
- @ that match "%h(zGlob)"
1021
+ @ that match "%h(zGlob)" and
8651022
}
866
- @</h2>
1023
+ @ ordered by check-in time</h2>
1024
+ @
1025
+ @ <p>Times are relative to the checkin time for
1026
+ @ %z(href("%R/ci/%s",zUuid))[%S(zUuid)]</a> which is
1027
+ @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p>
8671028
@
8681029
@ <div class='fileage'><table>
869
- @ <tr><th>Age</th><th>Files</th><th>Checkin</th></tr>
1030
+ @ <tr><th>Time</th><th>Files</th><th>Checkin</th></tr>
8701031
db_prepare(&q1,
8711032
"SELECT event.mtime, event.objid, blob.uuid,\n"
8721033
" coalesce(event.ecomment,event.comment),\n"
8731034
" coalesce(event.euser,event.user),\n"
8741035
" coalesce((SELECT value FROM tagxref\n"
8751036
--- src/browse.c
+++ src/browse.c
@@ -304,89 +304,206 @@
304
305 /*
306 ** A single line of the file hierarchy
307 */
308 struct FileTreeNode {
309 FileTreeNode *pNext; /* Next line in sequence */
310 FileTreeNode *pPrev; /* Previous line */
311 FileTreeNode *pParent; /* Directory containing this line */
 
 
312 char *zName; /* Name of this entry. The "tail" */
313 char *zFullName; /* Full pathname of this entry */
314 char *zUuid; /* SHA1 hash of this file. May be NULL. */
 
315 unsigned nFullName; /* Length of zFullName */
316 unsigned iLevel; /* Levels of parent directories */
317 u8 isDir; /* True if there are children */
318 u8 isLast; /* True if this is the last child of its parent */
319 };
320
321 /*
322 ** A complete file hierarchy
323 */
324 struct FileTree {
325 FileTreeNode *pFirst; /* First line of the list */
326 FileTreeNode *pLast; /* Last line of the list */
 
327 };
328
329 /*
330 ** Add one or more new FileTreeNodes to the FileTree object so that the
331 ** leaf object zPathname is at the end of the node list
 
 
 
 
 
 
 
 
332 */
333 static void tree_add_node(
334 FileTree *pTree, /* Tree into which nodes are added */
335 const char *zPath, /* The full pathname of file to add */
336 const char *zUuid /* UUID of the file. Might be NULL. */
 
337 ){
338 int i;
339 FileTreeNode *pParent;
340 FileTreeNode *pChild;
341
342 pChild = pTree->pLast;
343 pParent = pChild ? pChild->pParent : 0;
 
 
344 while( pParent!=0 &&
345 ( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0
346 || zPath[pParent->nFullName]!='/' )
347 ){
348 pChild = pParent;
349 pParent = pChild->pParent;
350 }
351 i = pParent ? pParent->nFullName+1 : 0;
352 if( pChild ) pChild->isLast = 0;
353 while( zPath[i] ){
354 FileTreeNode *pNew;
355 int iStart = i;
356 int nByte;
357 while( zPath[i] && zPath[i]!='/' ){ i++; }
358 nByte = sizeof(*pNew) + i + 1;
359 if( zUuid!=0 && zPath[i]==0 ) nByte += UUID_SIZE+1;
360 pNew = fossil_malloc( nByte );
 
361 pNew->zFullName = (char*)&pNew[1];
362 memcpy(pNew->zFullName, zPath, i);
363 pNew->zFullName[i] = 0;
364 pNew->nFullName = i;
365 if( zUuid!=0 && zPath[i]==0 ){
366 pNew->zUuid = pNew->zFullName + i + 1;
367 memcpy(pNew->zUuid, zUuid, UUID_SIZE+1);
368 }else{
369 pNew->zUuid = 0;
370 }
371 pNew->zName = pNew->zFullName + iStart;
372 if( pTree->pLast ){
373 pTree->pLast->pNext = pNew;
374 }else{
375 pTree->pFirst = pNew;
376 }
377 pNew->pPrev = pTree->pLast;
378 pNew->pNext = 0;
379 pNew->pParent = pParent;
380 pTree->pLast = pNew;
381 pNew->iLevel = pParent ? pParent->iLevel+1 : 0;
382 pNew->isDir = zPath[i]=='/';
383 pNew->isLast = 1;
 
 
 
 
 
 
 
 
 
384 while( zPath[i]=='/' ){ i++; }
385 pParent = pNew;
386 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387 }
 
388
389 /*
390 ** WEBPAGE: tree
391 **
392 ** Query parameters:
@@ -394,19 +511,23 @@
394 ** name=PATH Directory to display. Optional
395 ** ci=LABEL Show only files in this check-in. Optional.
396 ** re=REGEXP Show only files matching REGEXP. Optional.
397 ** expand Begin with the tree fully expanded.
398 ** nofiles Show directories (folders) only. Omit files.
 
399 */
400 void page_tree(void){
401 char *zD = fossil_strdup(P("name"));
402 int nD = zD ? strlen(zD)+1 : 0;
403 const char *zCI = P("ci");
404 int rid = 0;
405 char *zUuid = 0;
406 Blob dirname;
407 Manifest *pM = 0;
 
 
 
408 int nFile = 0; /* Number of files (or folders with "nofiles") */
409 int linkTrunk = 1; /* include link to "trunk" */
410 int linkTip = 1; /* include link to "tip" */
411 const char *zRE; /* the value for the re=REGEXP query parameter */
412 const char *zObjType; /* "files" by default or "folders" for "nofiles" */
@@ -464,13 +585,18 @@
464 int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
465 linkTrunk = trunkRid && rid != trunkRid;
466 linkTip = rid != symbolic_name_to_rid("tip", "ci");
467 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
468 url_add_parameter(&sURI, "ci", zCI);
 
 
 
469 }else{
470 zCI = 0;
471 }
 
 
472 }
473
474 /* Compute the title of the page */
475 blob_zero(&dirname);
476 if( zD ){
@@ -483,62 +609,57 @@
483 }else{
484 if( zRE ){
485 blob_appendf(&dirname, "matching \"%s\"", zRE);
486 }
487 }
 
 
 
 
488 if( zCI ){
489 style_submenu_element("All", "All", "%s",
490 url_render(&sURI, "ci", 0, 0, 0));
491 if( nD==0 && !showDirOnly ){
492 style_submenu_element("File Ages", "File Ages", "%R/fileage?name=%s",
493 zUuid);
494 }
 
 
 
 
 
 
 
 
495 }
496 if( linkTrunk ){
497 style_submenu_element("Trunk", "Trunk", "%s",
498 url_render(&sURI, "ci", "trunk", 0, 0));
499 }
500 if( linkTip ){
501 style_submenu_element("Tip", "Tip", "%s",
502 url_render(&sURI, "ci", "tip", 0, 0));
503 }
504 if( !showDirOnly ){
505 style_submenu_element("Flat-View", "Flat-View", "%s",
506 url_render(&sURI, "type", "flat", 0, 0));
507 }
508
509 /* Compute the file hierarchy.
510 */
511 if( zCI ){
512 Stmt ins, q;
513 ManifestFile *pFile;
514
515 db_multi_exec(
516 "CREATE TEMP TABLE filelist("
517 " x TEXT PRIMARY KEY COLLATE nocase,"
518 " uuid TEXT"
519 ") WITHOUT ROWID;"
520 );
521 db_prepare(&ins, "INSERT OR IGNORE INTO filelist VALUES(:f,:u)");
522 manifest_file_rewind(pM);
523 while( (pFile = manifest_file_next(pM,0))!=0 ){
524 if( nD>0
525 && (fossil_strncmp(pFile->zName, zD, nD-1)!=0
526 || pFile->zName[nD-1]!='/')
527 ){
528 continue;
529 }
530 if( pRE && re_match(pRE, (const u8*)pFile->zName, -1)==0 ) continue;
531 db_bind_text(&ins, ":f", pFile->zName);
532 db_bind_text(&ins, ":u", pFile->zUuid);
533 db_step(&ins);
534 db_reset(&ins);
535 }
536 db_finalize(&ins);
537 db_prepare(&q, "SELECT x, uuid FROM filelist ORDER BY x");
538 while( db_step(&q)==SQLITE_ROW ){
539 tree_add_node(&sTree, db_column_text(&q,0), db_column_text(&q,1));
 
 
 
 
540 nFile++;
541 }
542 db_finalize(&q);
543 }else{
544 Stmt q;
@@ -547,38 +668,43 @@
547 const char *z = db_column_text(&q, 0);
548 if( nD>0 && (fossil_strncmp(z, zD, nD-1)!=0 || z[nD-1]!='/') ){
549 continue;
550 }
551 if( pRE && re_match(pRE, (const u8*)z, -1)==0 ) continue;
552 tree_add_node(&sTree, z, 0);
553 nFile++;
554 }
555 db_finalize(&q);
556 }
557
558 if( showDirOnly ){
559 for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
560 if( p->isDir && p->nFullName>nD ) nFile++;
561 }
562 zObjType = "folders";
563 style_submenu_element("Files","Files","%s",
564 url_render(&sURI,"nofiles",0,0,0));
565 }else{
566 zObjType = "files";
567 style_submenu_element("Folders","Folders","%s",
568 url_render(&sURI,"nofiles","1",0,0));
569 }
570
571 if( zCI ){
572 @ <h2>%d(nFile) %s(zObjType) of check-in
573 if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){
574 @ "%h(zCI)"
575 }
576 @ [%z(href("vinfo?name=%s",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname))</h2>
 
 
 
 
 
577 }else{
578 int n = db_int(0, "SELECT count(*) FROM plink");
579 @ <h2>%d(nFile) %s(zObjType) from all %d(n) check-ins
580 @ %s(blob_str(&dirname))</h2>
581 }
582
583
584 /* Generate tree of lists.
@@ -596,18 +722,33 @@
596 if( nD ){
597 @ <li class="dir last">
598 }else{
599 @ <li class="dir subdir last">
600 }
 
601 @ %z(href("%s",url_render(&sURI,"name",0,0,0)))%h(zProjectName)</a>
 
 
 
 
602 @ <ul>
 
 
 
 
 
603 for(p=sTree.pFirst, nDir=0; p; p=p->pNext){
604 const char *zLastClass = p->isLast ? " last" : "";
605 if( p->isDir ){
606 const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : "";
607 @ <li class="dir%s(zSubdirClass)%s(zLastClass)">
608 @ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a>
 
 
 
 
 
609 if( startExpanded || p->nFullName<=nD ){
610 @ <ul id="dir%d(nDir)">
611 }else{
612 @ <ul id="dir%d(nDir)" class="collapsed">
613 }
@@ -618,13 +759,19 @@
618 if( zCI ){
619 zLink = href("%R/artifact/%s",p->zUuid);
620 }else{
621 zLink = href("%R/finfo?name=%T",p->zFullName);
622 }
623 @ <li class="%z(zFileClass)%s(zLastClass)">%z(zLink)%h(p->zName)</a>
 
 
 
 
 
 
624 }
625 if( p->isLast ){
626 int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0);
627 while( nClose-- > 0 ){
628 @ </ul>
629 }
630 }
@@ -687,16 +834,16 @@
687 @ checkState();
688 @ outer_ul.onclick = function(e){
689 @ e = e || window.event;
690 @ var a = e.target || e.srcElement;
691 @ if( a.nodeName!='A' ) return true;
692 @ if( a.parentNode==subdir ){
693 @ toggleAll(outer_ul);
694 @ return false;
695 @ }
696 @ if( !belowSubdir(a) ) return true;
697 @ var ul = a.nextSibling;
698 @ while( ul && ul.nodeName!='UL' ) ul = ul.nextSibling;
699 @ if( !ul ) return true; /* This is a file link, not a directory */
700 @ toggleDir(ul);
701 @ return false;
702 @ }
@@ -785,19 +932,23 @@
785 ** The string returned is obtained from fossil_malloc() and should be
786 ** freed by the caller.
787 */
788 char *human_readable_age(double rAge){
789 if( rAge*86400.0<120 ){
790 return mprintf("%d seconds", (int)(rAge*86400.0));
 
 
 
 
791 }else if( rAge*1440.0<90 ){
792 return mprintf("%.1f minutes", rAge*1440.0);
793 }else if( rAge*24.0<36 ){
794 return mprintf("%.1f hours", rAge*24.0);
795 }else if( rAge<365.0 ){
796 return mprintf("%.1f days", rAge);
797 }else{
798 return mprintf("%.2f years", rAge/365.0);
799 }
800 }
801
802 /*
803 ** COMMAND: test-fileage
@@ -839,10 +990,12 @@
839 */
840 void fileage_page(void){
841 int rid;
842 const char *zName;
843 const char *zGlob;
 
 
844 Stmt q1, q2;
845 double baseTime;
846 login_check_credentials();
847 if( !g.perm.Read ){ login_needed(); return; }
848 zName = P("name");
@@ -849,26 +1002,34 @@
849 if( zName==0 ) zName = "tip";
850 rid = symbolic_name_to_rid(zName, "ci");
851 if( rid==0 ){
852 fossil_fatal("not a valid check-in: %s", zName);
853 }
854 style_submenu_element("Tree-View", "Tree-View", "%R/tree?ci=%T", zName);
 
 
 
 
 
855 style_header("File Ages");
856 zGlob = P("glob");
857 compute_fileage(rid,zGlob);
858 db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
859
860 baseTime = db_double(0.0, "SELECT julianday('now');");
861 @ <h2>Most recent change to files in checkin
862 @ %z(href("%R/info?name=%T",zName))%h(zName)</a>
863 if( zGlob && zGlob[0] ){
864 @ that match "%h(zGlob)"
865 }
866 @</h2>
 
 
 
 
867 @
868 @ <div class='fileage'><table>
869 @ <tr><th>Age</th><th>Files</th><th>Checkin</th></tr>
870 db_prepare(&q1,
871 "SELECT event.mtime, event.objid, blob.uuid,\n"
872 " coalesce(event.ecomment,event.comment),\n"
873 " coalesce(event.euser,event.user),\n"
874 " coalesce((SELECT value FROM tagxref\n"
875
--- src/browse.c
+++ src/browse.c
@@ -304,89 +304,206 @@
304
305 /*
306 ** A single line of the file hierarchy
307 */
308 struct FileTreeNode {
309 FileTreeNode *pNext; /* Next entry in an ordered list of them all */
310 FileTreeNode *pParent; /* Directory containing this entry */
311 FileTreeNode *pSibling; /* Next element in the same subdirectory */
312 FileTreeNode *pChild; /* List of child nodes */
313 FileTreeNode *pLastChild; /* Last child on the pChild list */
314 char *zName; /* Name of this entry. The "tail" */
315 char *zFullName; /* Full pathname of this entry */
316 char *zUuid; /* SHA1 hash of this file. May be NULL. */
317 double mtime; /* Modification time for this entry */
318 unsigned nFullName; /* Length of zFullName */
319 unsigned iLevel; /* Levels of parent directories */
 
 
320 };
321
322 /*
323 ** A complete file hierarchy
324 */
325 struct FileTree {
326 FileTreeNode *pFirst; /* First line of the list */
327 FileTreeNode *pLast; /* Last line of the list */
328 FileTreeNode *pLastTop; /* Last top-level node */
329 };
330
331 /*
332 ** Add one or more new FileTreeNodes to the FileTree object so that the
333 ** leaf object zPathname is at the end of the node list.
334 **
335 ** The caller invokes this routine once for each leaf node (each file
336 ** as opposed to each directory). This routine fills in any missing
337 ** intermediate nodes automatically.
338 **
339 ** When constructing a list of FileTreeNodes, all entries that have
340 ** a common directory prefix must be added consecutively in order for
341 ** the tree to be constructed properly.
342 */
343 static void tree_add_node(
344 FileTree *pTree, /* Tree into which nodes are added */
345 const char *zPath, /* The full pathname of file to add */
346 const char *zUuid, /* UUID of the file. Might be NULL. */
347 double mtime /* Modification time for this entry */
348 ){
349 int i;
350 FileTreeNode *pParent; /* Parent (directory) of the next node to insert */
 
351
352 /* Make pParent point to the most recent ancestor of zPath, or
353 ** NULL if there are no prior entires that are a container for zPath.
354 */
355 pParent = pTree->pLast;
356 while( pParent!=0 &&
357 ( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0
358 || zPath[pParent->nFullName]!='/' )
359 ){
360 pParent = pParent->pParent;
 
361 }
362 i = pParent ? pParent->nFullName+1 : 0;
 
363 while( zPath[i] ){
364 FileTreeNode *pNew;
365 int iStart = i;
366 int nByte;
367 while( zPath[i] && zPath[i]!='/' ){ i++; }
368 nByte = sizeof(*pNew) + i + 1;
369 if( zUuid!=0 && zPath[i]==0 ) nByte += UUID_SIZE+1;
370 pNew = fossil_malloc( nByte );
371 memset(pNew, 0, sizeof(*pNew));
372 pNew->zFullName = (char*)&pNew[1];
373 memcpy(pNew->zFullName, zPath, i);
374 pNew->zFullName[i] = 0;
375 pNew->nFullName = i;
376 if( zUuid!=0 && zPath[i]==0 ){
377 pNew->zUuid = pNew->zFullName + i + 1;
378 memcpy(pNew->zUuid, zUuid, UUID_SIZE+1);
 
 
379 }
380 pNew->zName = pNew->zFullName + iStart;
381 if( pTree->pLast ){
382 pTree->pLast->pNext = pNew;
383 }else{
384 pTree->pFirst = pNew;
385 }
386 pTree->pLast = pNew;
 
387 pNew->pParent = pParent;
388 if( pParent ){
389 if( pParent->pChild ){
390 pParent->pLastChild->pSibling = pNew;
391 }else{
392 pParent->pChild = pNew;
393 }
394 pNew->iLevel = pParent->iLevel + 1;
395 pParent->pLastChild = pNew;
396 }else{
397 if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew;
398 pTree->pLastTop = pNew;
399 }
400 pNew->mtime = mtime;
401 while( zPath[i]=='/' ){ i++; }
402 pParent = pNew;
403 }
404 while( pParent && pParent->pParent ){
405 if( pParent->pParent->mtime < pParent->mtime ){
406 pParent->pParent->mtime = pParent->mtime;
407 }
408 pParent = pParent->pParent;
409 }
410 }
411
412 /* Comparison function for two FileTreeNode objects. Sort first by
413 ** mtime (larger numbers first) and then by zName (smaller names first).
414 **
415 ** Return negative if pLeft<pRight.
416 ** Return positive if pLeft>pRight.
417 ** Return zero if pLeft==pRight.
418 */
419 static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
420 if( pLeft->mtime>pRight->mtime ) return -1;
421 if( pLeft->mtime<pRight->mtime ) return +1;
422 return fossil_stricmp(pLeft->zName, pRight->zName);
423 }
424
425 /* Merge together two sorted lists of FileTreeNode objects */
426 static FileTreeNode *mergeNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
427 FileTreeNode *pEnd;
428 FileTreeNode base;
429 pEnd = &base;
430 while( pLeft && pRight ){
431 if( compareNodes(pLeft,pRight)<=0 ){
432 pEnd = pEnd->pSibling = pLeft;
433 pLeft = pLeft->pSibling;
434 }else{
435 pEnd = pEnd->pSibling = pRight;
436 pRight = pRight->pSibling;
437 }
438 }
439 if( pLeft ){
440 pEnd->pSibling = pLeft;
441 }else{
442 pEnd->pSibling = pRight;
443 }
444 return base.pSibling;
445 }
446
447 /* Sort a list of FileTreeNode objects in mtime order. */
448 static FileTreeNode *sortNodesByMtime(FileTreeNode *p){
449 FileTreeNode *a[30];
450 FileTreeNode *pX;
451 int i;
452
453 memset(a, 0, sizeof(a));
454 while( p ){
455 pX = p;
456 p = pX->pSibling;
457 pX->pSibling = 0;
458 for(i=0; i<count(a)-1 && a[i]!=0; i++){
459 pX = mergeNodes(a[i], pX);
460 a[i] = 0;
461 }
462 a[i] = mergeNodes(a[i], pX);
463 }
464 pX = 0;
465 for(i=0; i<count(a); i++){
466 pX = mergeNodes(a[i], pX);
467 }
468 return pX;
469 }
470
471 /* Sort an entire FileTreeNode tree by mtime
472 **
473 ** This routine invalidates the following fields:
474 **
475 ** FileTreeNode.pLastChild
476 ** FileTreeNode.pNext
477 **
478 ** Use relinkTree to reconnect the pNext pointers.
479 */
480 static FileTreeNode *sortTreeByMtime(FileTreeNode *p){
481 FileTreeNode *pX;
482 for(pX=p; pX; pX=pX->pSibling){
483 if( pX->pChild ) pX->pChild = sortTreeByMtime(pX->pChild);
484 }
485 return sortNodesByMtime(p);
486 }
487
488 /* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext
489 ** fields in sequential order.
490 */
491 static void relinkTree(FileTree *pTree, FileTreeNode *pRoot){
492 while( pRoot ){
493 if( pTree->pLast ){
494 pTree->pLast->pNext = pRoot;
495 }else{
496 pTree->pFirst = pRoot;
497 }
498 pTree->pLast = pRoot;
499 if( pRoot->pChild ) relinkTree(pTree, pRoot->pChild);
500 pRoot = pRoot->pSibling;
501 }
502 if( pTree->pLast ) pTree->pLast->pNext = 0;
503 }
504
505
506 /*
507 ** WEBPAGE: tree
508 **
509 ** Query parameters:
@@ -394,19 +511,23 @@
511 ** name=PATH Directory to display. Optional
512 ** ci=LABEL Show only files in this check-in. Optional.
513 ** re=REGEXP Show only files matching REGEXP. Optional.
514 ** expand Begin with the tree fully expanded.
515 ** nofiles Show directories (folders) only. Omit files.
516 ** mtime Order directory elements by decreasing mtime
517 */
518 void page_tree(void){
519 char *zD = fossil_strdup(P("name"));
520 int nD = zD ? strlen(zD)+1 : 0;
521 const char *zCI = P("ci");
522 int rid = 0;
523 char *zUuid = 0;
524 Blob dirname;
525 Manifest *pM = 0;
526 double rNow = 0;
527 char *zNow = 0;
528 int useMtime = atoi(PD("mtime","0"));
529 int nFile = 0; /* Number of files (or folders with "nofiles") */
530 int linkTrunk = 1; /* include link to "trunk" */
531 int linkTip = 1; /* include link to "tip" */
532 const char *zRE; /* the value for the re=REGEXP query parameter */
533 const char *zObjType; /* "files" by default or "folders" for "nofiles" */
@@ -464,13 +585,18 @@
585 int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
586 linkTrunk = trunkRid && rid != trunkRid;
587 linkTip = rid != symbolic_name_to_rid("tip", "ci");
588 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
589 url_add_parameter(&sURI, "ci", zCI);
590 rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
591 zNow = db_text("", "SELECT datetime(mtime,'localtime')"
592 " FROM event WHERE objid=%d", rid);
593 }else{
594 zCI = 0;
595 }
596 }else{
597 useMtime = 0;
598 }
599
600 /* Compute the title of the page */
601 blob_zero(&dirname);
602 if( zD ){
@@ -483,62 +609,57 @@
609 }else{
610 if( zRE ){
611 blob_appendf(&dirname, "matching \"%s\"", zRE);
612 }
613 }
614 if( !showDirOnly ){
615 style_submenu_element("Flat-View", "Flat-View", "%s",
616 url_render(&sURI, "type", "flat", 0, 0));
617 }
618 if( zCI ){
619 style_submenu_element("All", "All", "%s",
620 url_render(&sURI, "ci", 0, 0, 0));
621 if( nD==0 && !showDirOnly ){
622 style_submenu_element("File Ages", "File Ages", "%R/fileage?name=%s",
623 zUuid);
624 }
625 if( useMtime ){
626 style_submenu_element("Sort By Filename","Sort By Filename", "%s",
627 url_render(&sURI, 0, 0, 0, 0));
628 url_add_parameter(&sURI, "mtime", "1");
629 }else{
630 style_submenu_element("Sort By Time","Sort By Time", "%s",
631 url_render(&sURI, "mtime", "1", 0, 0));
632 }
633 }
634 if( linkTrunk ){
635 style_submenu_element("Trunk", "Trunk", "%s",
636 url_render(&sURI, "ci", "trunk", 0, 0));
637 }
638 if( linkTip ){
639 style_submenu_element("Tip", "Tip", "%s",
640 url_render(&sURI, "ci", "tip", 0, 0));
641 }
 
 
 
 
642
643 /* Compute the file hierarchy.
644 */
645 if( zCI ){
646 Stmt q;
647 compute_fileage(rid, 0);
648 db_prepare(&q,
649 "SELECT filename.name, blob.uuid, fileage.mtime\n"
650 " FROM fileage, filename, blob\n"
651 " WHERE filename.fnid=fileage.fnid\n"
652 " AND blob.rid=fileage.fid\n"
653 " ORDER BY filename.name COLLATE nocase;"
654 );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655 while( db_step(&q)==SQLITE_ROW ){
656 const char *zFile = db_column_text(&q,0);
657 const char *zUuid = db_column_text(&q,1);
658 double mtime = db_column_double(&q,2);
659 if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue;
660 tree_add_node(&sTree, zFile, zUuid, mtime);
661 nFile++;
662 }
663 db_finalize(&q);
664 }else{
665 Stmt q;
@@ -547,38 +668,43 @@
668 const char *z = db_column_text(&q, 0);
669 if( nD>0 && (fossil_strncmp(z, zD, nD-1)!=0 || z[nD-1]!='/') ){
670 continue;
671 }
672 if( pRE && re_match(pRE, (const u8*)z, -1)==0 ) continue;
673 tree_add_node(&sTree, z, 0, 0.0);
674 nFile++;
675 }
676 db_finalize(&q);
677 }
678
679 if( showDirOnly ){
680 for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
681 if( p->pChild!=0 && p->nFullName>nD ) nFile++;
682 }
683 zObjType = "Folders";
684 style_submenu_element("Files","Files","%s",
685 url_render(&sURI,"nofiles",0,0,0));
686 }else{
687 zObjType = "Files";
688 style_submenu_element("Folders","Folders","%s",
689 url_render(&sURI,"nofiles","1",0,0));
690 }
691
692 if( zCI ){
693 @ <h2>%s(zObjType) from
694 if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){
695 @ "%h(zCI)"
696 }
697 @ [%z(href("vinfo?name=%s",zUuid))%S(zUuid)</a>] %s(blob_str(&dirname))
698 if( useMtime ){
699 @ sorted by modification time</h2>
700 }else{
701 @ sorted by filename</h2>
702 }
703 }else{
704 int n = db_int(0, "SELECT count(*) FROM plink");
705 @ <h2>%s(zObjType) from all %d(n) check-ins
706 @ %s(blob_str(&dirname))</h2>
707 }
708
709
710 /* Generate tree of lists.
@@ -596,18 +722,33 @@
722 if( nD ){
723 @ <li class="dir last">
724 }else{
725 @ <li class="dir subdir last">
726 }
727 @ <div class="filetreeline">
728 @ %z(href("%s",url_render(&sURI,"name",0,0,0)))%h(zProjectName)</a>
729 if( zNow ){
730 @ <div class="filetreeage">%s(zNow)</div>
731 }
732 @ </div>
733 @ <ul>
734 if( zCI && useMtime ){
735 p = sortTreeByMtime(sTree.pFirst);
736 memset(&sTree, 0, sizeof(sTree));
737 relinkTree(&sTree, p);
738 }
739 for(p=sTree.pFirst, nDir=0; p; p=p->pNext){
740 const char *zLastClass = p->pSibling==0 ? " last" : "";
741 if( p->pChild ){
742 const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : "";
743 @ <li class="dir%s(zSubdirClass)%s(zLastClass)"><div class="filetreeline">
744 @ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a>
745 if( p->mtime>0.0 ){
746 char *zAge = human_readable_age(rNow - p->mtime);
747 @ <div class="filetreeage">%s(zAge)</div>
748 }
749 @ </div>
750 if( startExpanded || p->nFullName<=nD ){
751 @ <ul id="dir%d(nDir)">
752 }else{
753 @ <ul id="dir%d(nDir)" class="collapsed">
754 }
@@ -618,13 +759,19 @@
759 if( zCI ){
760 zLink = href("%R/artifact/%s",p->zUuid);
761 }else{
762 zLink = href("%R/finfo?name=%T",p->zFullName);
763 }
764 @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline">
765 @ %z(zLink)%h(p->zName)</a>
766 if( p->mtime>0 ){
767 char *zAge = human_readable_age(rNow - p->mtime);
768 @ <div class="filetreeage">%s(zAge)</div>
769 }
770 @ </div>
771 }
772 if( p->pSibling==0 ){
773 int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0);
774 while( nClose-- > 0 ){
775 @ </ul>
776 }
777 }
@@ -687,16 +834,16 @@
834 @ checkState();
835 @ outer_ul.onclick = function(e){
836 @ e = e || window.event;
837 @ var a = e.target || e.srcElement;
838 @ if( a.nodeName!='A' ) return true;
839 @ if( a.parentNode.parentNode==subdir ){
840 @ toggleAll(outer_ul);
841 @ return false;
842 @ }
843 @ if( !belowSubdir(a) ) return true;
844 @ var ul = a.parentNode.nextSibling;
845 @ while( ul && ul.nodeName!='UL' ) ul = ul.nextSibling;
846 @ if( !ul ) return true; /* This is a file link, not a directory */
847 @ toggleDir(ul);
848 @ return false;
849 @ }
@@ -785,19 +932,23 @@
932 ** The string returned is obtained from fossil_malloc() and should be
933 ** freed by the caller.
934 */
935 char *human_readable_age(double rAge){
936 if( rAge*86400.0<120 ){
937 if( rAge*86400.0<1.0 ){
938 return mprintf("current");
939 }else{
940 return mprintf("-%d seconds", (int)(rAge*86400.0));
941 }
942 }else if( rAge*1440.0<90 ){
943 return mprintf("-%.1f minutes", rAge*1440.0);
944 }else if( rAge*24.0<36 ){
945 return mprintf("-%.1f hours", rAge*24.0);
946 }else if( rAge<365.0 ){
947 return mprintf("-%.1f days", rAge);
948 }else{
949 return mprintf("-%.2f years", rAge/365.0);
950 }
951 }
952
953 /*
954 ** COMMAND: test-fileage
@@ -839,10 +990,12 @@
990 */
991 void fileage_page(void){
992 int rid;
993 const char *zName;
994 const char *zGlob;
995 const char *zUuid;
996 const char *zNow; /* Time of checkin */
997 Stmt q1, q2;
998 double baseTime;
999 login_check_credentials();
1000 if( !g.perm.Read ){ login_needed(); return; }
1001 zName = P("name");
@@ -849,26 +1002,34 @@
1002 if( zName==0 ) zName = "tip";
1003 rid = symbolic_name_to_rid(zName, "ci");
1004 if( rid==0 ){
1005 fossil_fatal("not a valid check-in: %s", zName);
1006 }
1007 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
1008 baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid);
1009 zNow = db_text("", "SELECT datetime(mtime,'localtime') FROM event"
1010 " WHERE objid=%d", rid);
1011 style_submenu_element("Tree-View", "Tree-View", "%R/tree?ci=%T&mtime=1",
1012 zName);
1013 style_header("File Ages");
1014 zGlob = P("glob");
1015 compute_fileage(rid,zGlob);
1016 db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
1017
1018 @ <h2>Files in
1019 @ %z(href("%R/info?name=%T",zUuid))[%S(zUuid)]</a>
 
1020 if( zGlob && zGlob[0] ){
1021 @ that match "%h(zGlob)" and
1022 }
1023 @ ordered by check-in time</h2>
1024 @
1025 @ <p>Times are relative to the checkin time for
1026 @ %z(href("%R/ci/%s",zUuid))[%S(zUuid)]</a> which is
1027 @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p>
1028 @
1029 @ <div class='fileage'><table>
1030 @ <tr><th>Time</th><th>Files</th><th>Checkin</th></tr>
1031 db_prepare(&q1,
1032 "SELECT event.mtime, event.objid, blob.uuid,\n"
1033 " coalesce(event.ecomment,event.comment),\n"
1034 " coalesce(event.euser,event.user),\n"
1035 " coalesce((SELECT value FROM tagxref\n"
1036
+10 -1
--- src/style.c
+++ src/style.c
@@ -839,14 +839,23 @@
839839
@ padding-left: 21px;
840840
@ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP\/\/\/yEhIf\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14GqFXImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw==);
841841
@ background-position: center left;
842842
@ background-repeat: no-repeat;
843843
},
844
- { ".filetree .dir > a",
844
+ { ".filetree .dir > div.filetreeline > a",
845845
"tree-view directory links",
846846
@ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiIv\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo+jUs6b5Z/K4siDu5RPUFADs=);
847847
},
848
+ { "div.filetreeage",
849
+ "Last change floating display on the right",
850
+ @ clear: right;
851
+ @ float: right;
852
+ },
853
+ { "div.filetreeline:hover",
854
+ "Highlight the line of a file tree",
855
+ @ background-color: #eee;
856
+ },
848857
{ "table.login_out",
849858
"table format for login/out label/input table",
850859
@ text-align: left;
851860
@ margin-right: 10px;
852861
@ margin-left: 10px;
853862
--- src/style.c
+++ src/style.c
@@ -839,14 +839,23 @@
839 @ padding-left: 21px;
840 @ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP\/\/\/yEhIf\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14GqFXImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw==);
841 @ background-position: center left;
842 @ background-repeat: no-repeat;
843 },
844 { ".filetree .dir > a",
845 "tree-view directory links",
846 @ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiIv\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo+jUs6b5Z/K4siDu5RPUFADs=);
847 },
 
 
 
 
 
 
 
 
 
848 { "table.login_out",
849 "table format for login/out label/input table",
850 @ text-align: left;
851 @ margin-right: 10px;
852 @ margin-left: 10px;
853
--- src/style.c
+++ src/style.c
@@ -839,14 +839,23 @@
839 @ padding-left: 21px;
840 @ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP\/\/\/yEhIf\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14GqFXImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw==);
841 @ background-position: center left;
842 @ background-repeat: no-repeat;
843 },
844 { ".filetree .dir > div.filetreeline > a",
845 "tree-view directory links",
846 @ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiIv\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo+jUs6b5Z/K4siDu5RPUFADs=);
847 },
848 { "div.filetreeage",
849 "Last change floating display on the right",
850 @ clear: right;
851 @ float: right;
852 },
853 { "div.filetreeline:hover",
854 "Highlight the line of a file tree",
855 @ background-color: #eee;
856 },
857 { "table.login_out",
858 "table format for login/out label/input table",
859 @ text-align: left;
860 @ margin-right: 10px;
861 @ margin-left: 10px;
862

Keyboard Shortcuts

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