Index: src/browse.c ================================================================== --- src/browse.c +++ src/browse.c @@ -304,89 +304,206 @@ /* ** A single line of the file hierarchy */ struct FileTreeNode { - FileTreeNode *pNext; /* Next line in sequence */ - FileTreeNode *pPrev; /* Previous line */ - FileTreeNode *pParent; /* Directory containing this line */ + FileTreeNode *pNext; /* Next entry in an ordered list of them all */ + FileTreeNode *pParent; /* Directory containing this entry */ + FileTreeNode *pSibling; /* Next element in the same subdirectory */ + FileTreeNode *pChild; /* List of child nodes */ + FileTreeNode *pLastChild; /* Last child on the pChild list */ char *zName; /* Name of this entry. The "tail" */ char *zFullName; /* Full pathname of this entry */ char *zUuid; /* SHA1 hash of this file. May be NULL. */ + double mtime; /* Modification time for this entry */ unsigned nFullName; /* Length of zFullName */ unsigned iLevel; /* Levels of parent directories */ - u8 isDir; /* True if there are children */ - u8 isLast; /* True if this is the last child of its parent */ }; /* ** A complete file hierarchy */ struct FileTree { FileTreeNode *pFirst; /* First line of the list */ FileTreeNode *pLast; /* Last line of the list */ + FileTreeNode *pLastTop; /* Last top-level node */ }; /* ** Add one or more new FileTreeNodes to the FileTree object so that the -** leaf object zPathname is at the end of the node list +** leaf object zPathname is at the end of the node list. +** +** The caller invokes this routine once for each leaf node (each file +** as opposed to each directory). This routine fills in any missing +** intermediate nodes automatically. +** +** When constructing a list of FileTreeNodes, all entries that have +** a common directory prefix must be added consecutively in order for +** the tree to be constructed properly. */ static void tree_add_node( FileTree *pTree, /* Tree into which nodes are added */ const char *zPath, /* The full pathname of file to add */ - const char *zUuid /* UUID of the file. Might be NULL. */ + const char *zUuid, /* UUID of the file. Might be NULL. */ + double mtime /* Modification time for this entry */ ){ int i; - FileTreeNode *pParent; - FileTreeNode *pChild; + FileTreeNode *pParent; /* Parent (directory) of the next node to insert */ - pChild = pTree->pLast; - pParent = pChild ? pChild->pParent : 0; + /* Make pParent point to the most recent ancestor of zPath, or + ** NULL if there are no prior entires that are a container for zPath. + */ + pParent = pTree->pLast; while( pParent!=0 && ( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0 || zPath[pParent->nFullName]!='/' ) ){ - pChild = pParent; - pParent = pChild->pParent; + pParent = pParent->pParent; } i = pParent ? pParent->nFullName+1 : 0; - if( pChild ) pChild->isLast = 0; while( zPath[i] ){ FileTreeNode *pNew; int iStart = i; int nByte; while( zPath[i] && zPath[i]!='/' ){ i++; } nByte = sizeof(*pNew) + i + 1; if( zUuid!=0 && zPath[i]==0 ) nByte += UUID_SIZE+1; pNew = fossil_malloc( nByte ); + memset(pNew, 0, sizeof(*pNew)); pNew->zFullName = (char*)&pNew[1]; memcpy(pNew->zFullName, zPath, i); pNew->zFullName[i] = 0; pNew->nFullName = i; if( zUuid!=0 && zPath[i]==0 ){ pNew->zUuid = pNew->zFullName + i + 1; memcpy(pNew->zUuid, zUuid, UUID_SIZE+1); - }else{ - pNew->zUuid = 0; } pNew->zName = pNew->zFullName + iStart; if( pTree->pLast ){ pTree->pLast->pNext = pNew; }else{ pTree->pFirst = pNew; } - pNew->pPrev = pTree->pLast; - pNew->pNext = 0; + pTree->pLast = pNew; pNew->pParent = pParent; - pTree->pLast = pNew; - pNew->iLevel = pParent ? pParent->iLevel+1 : 0; - pNew->isDir = zPath[i]=='/'; - pNew->isLast = 1; + if( pParent ){ + if( pParent->pChild ){ + pParent->pLastChild->pSibling = pNew; + }else{ + pParent->pChild = pNew; + } + pNew->iLevel = pParent->iLevel + 1; + pParent->pLastChild = pNew; + }else{ + if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew; + pTree->pLastTop = pNew; + } + pNew->mtime = mtime; while( zPath[i]=='/' ){ i++; } pParent = pNew; } + while( pParent && pParent->pParent ){ + if( pParent->pParent->mtime < pParent->mtime ){ + pParent->pParent->mtime = pParent->mtime; + } + pParent = pParent->pParent; + } +} + +/* Comparison function for two FileTreeNode objects. Sort first by +** mtime (larger numbers first) and then by zName (smaller names first). +** +** Return negative if pLeftpRight. +** Return zero if pLeft==pRight. +*/ +static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){ + if( pLeft->mtime>pRight->mtime ) return -1; + if( pLeft->mtimemtime ) return +1; + return fossil_stricmp(pLeft->zName, pRight->zName); +} + +/* Merge together two sorted lists of FileTreeNode objects */ +static FileTreeNode *mergeNodes(FileTreeNode *pLeft, FileTreeNode *pRight){ + FileTreeNode *pEnd; + FileTreeNode base; + pEnd = &base; + while( pLeft && pRight ){ + if( compareNodes(pLeft,pRight)<=0 ){ + pEnd = pEnd->pSibling = pLeft; + pLeft = pLeft->pSibling; + }else{ + pEnd = pEnd->pSibling = pRight; + pRight = pRight->pSibling; + } + } + if( pLeft ){ + pEnd->pSibling = pLeft; + }else{ + pEnd->pSibling = pRight; + } + return base.pSibling; +} + +/* Sort a list of FileTreeNode objects in mtime order. */ +static FileTreeNode *sortNodesByMtime(FileTreeNode *p){ + FileTreeNode *a[30]; + FileTreeNode *pX; + int i; + + memset(a, 0, sizeof(a)); + while( p ){ + pX = p; + p = pX->pSibling; + pX->pSibling = 0; + for(i=0; ipSibling){ + if( pX->pChild ) pX->pChild = sortTreeByMtime(pX->pChild); + } + return sortNodesByMtime(p); +} + +/* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext +** fields in sequential order. +*/ +static void relinkTree(FileTree *pTree, FileTreeNode *pRoot){ + while( pRoot ){ + if( pTree->pLast ){ + pTree->pLast->pNext = pRoot; + }else{ + pTree->pFirst = pRoot; + } + pTree->pLast = pRoot; + if( pRoot->pChild ) relinkTree(pTree, pRoot->pChild); + pRoot = pRoot->pSibling; + } + if( pTree->pLast ) pTree->pLast->pNext = 0; } + /* ** WEBPAGE: tree ** ** Query parameters: @@ -394,19 +511,23 @@ ** name=PATH Directory to display. Optional ** ci=LABEL Show only files in this check-in. Optional. ** re=REGEXP Show only files matching REGEXP. Optional. ** expand Begin with the tree fully expanded. ** nofiles Show directories (folders) only. Omit files. +** mtime Order directory elements by decreasing mtime */ void page_tree(void){ char *zD = fossil_strdup(P("name")); int nD = zD ? strlen(zD)+1 : 0; const char *zCI = P("ci"); int rid = 0; char *zUuid = 0; Blob dirname; Manifest *pM = 0; + double rNow = 0; + char *zNow = 0; + int useMtime = atoi(PD("mtime","0")); int nFile = 0; /* Number of files (or folders with "nofiles") */ int linkTrunk = 1; /* include link to "trunk" */ int linkTip = 1; /* include link to "tip" */ const char *zRE; /* the value for the re=REGEXP query parameter */ const char *zObjType; /* "files" by default or "folders" for "nofiles" */ @@ -464,13 +585,18 @@ int trunkRid = symbolic_name_to_rid("tag:trunk", "ci"); linkTrunk = trunkRid && rid != trunkRid; linkTip = rid != symbolic_name_to_rid("tip", "ci"); zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); url_add_parameter(&sURI, "ci", zCI); + rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); + zNow = db_text("", "SELECT datetime(mtime,'localtime')" + " FROM event WHERE objid=%d", rid); }else{ zCI = 0; } + }else{ + useMtime = 0; } /* Compute the title of the page */ blob_zero(&dirname); if( zD ){ @@ -483,62 +609,57 @@ }else{ if( zRE ){ blob_appendf(&dirname, "matching \"%s\"", zRE); } } + if( !showDirOnly ){ + style_submenu_element("Flat-View", "Flat-View", "%s", + url_render(&sURI, "type", "flat", 0, 0)); + } if( zCI ){ style_submenu_element("All", "All", "%s", url_render(&sURI, "ci", 0, 0, 0)); if( nD==0 && !showDirOnly ){ style_submenu_element("File Ages", "File Ages", "%R/fileage?name=%s", zUuid); } + if( useMtime ){ + style_submenu_element("Sort By Filename","Sort By Filename", "%s", + url_render(&sURI, 0, 0, 0, 0)); + url_add_parameter(&sURI, "mtime", "1"); + }else{ + style_submenu_element("Sort By Time","Sort By Time", "%s", + url_render(&sURI, "mtime", "1", 0, 0)); + } } if( linkTrunk ){ style_submenu_element("Trunk", "Trunk", "%s", url_render(&sURI, "ci", "trunk", 0, 0)); } if( linkTip ){ style_submenu_element("Tip", "Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0)); } - if( !showDirOnly ){ - style_submenu_element("Flat-View", "Flat-View", "%s", - url_render(&sURI, "type", "flat", 0, 0)); - } /* Compute the file hierarchy. */ if( zCI ){ - Stmt ins, q; - ManifestFile *pFile; - - db_multi_exec( - "CREATE TEMP TABLE filelist(" - " x TEXT PRIMARY KEY COLLATE nocase," - " uuid TEXT" - ") WITHOUT ROWID;" + Stmt q; + compute_fileage(rid, 0); + db_prepare(&q, + "SELECT filename.name, blob.uuid, fileage.mtime\n" + " FROM fileage, filename, blob\n" + " WHERE filename.fnid=fileage.fnid\n" + " AND blob.rid=fileage.fid\n" + " ORDER BY filename.name COLLATE nocase;" ); - db_prepare(&ins, "INSERT OR IGNORE INTO filelist VALUES(:f,:u)"); - manifest_file_rewind(pM); - while( (pFile = manifest_file_next(pM,0))!=0 ){ - if( nD>0 - && (fossil_strncmp(pFile->zName, zD, nD-1)!=0 - || pFile->zName[nD-1]!='/') - ){ - continue; - } - if( pRE && re_match(pRE, (const u8*)pFile->zName, -1)==0 ) continue; - db_bind_text(&ins, ":f", pFile->zName); - db_bind_text(&ins, ":u", pFile->zUuid); - db_step(&ins); - db_reset(&ins); - } - db_finalize(&ins); - db_prepare(&q, "SELECT x, uuid FROM filelist ORDER BY x"); while( db_step(&q)==SQLITE_ROW ){ - tree_add_node(&sTree, db_column_text(&q,0), db_column_text(&q,1)); + const char *zFile = db_column_text(&q,0); + const char *zUuid = db_column_text(&q,1); + double mtime = db_column_double(&q,2); + if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue; + tree_add_node(&sTree, zFile, zUuid, mtime); nFile++; } db_finalize(&q); }else{ Stmt q; @@ -547,38 +668,43 @@ const char *z = db_column_text(&q, 0); if( nD>0 && (fossil_strncmp(z, zD, nD-1)!=0 || z[nD-1]!='/') ){ continue; } if( pRE && re_match(pRE, (const u8*)z, -1)==0 ) continue; - tree_add_node(&sTree, z, 0); + tree_add_node(&sTree, z, 0, 0.0); nFile++; } db_finalize(&q); } if( showDirOnly ){ for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ - if( p->isDir && p->nFullName>nD ) nFile++; + if( p->pChild!=0 && p->nFullName>nD ) nFile++; } - zObjType = "folders"; + zObjType = "Folders"; style_submenu_element("Files","Files","%s", url_render(&sURI,"nofiles",0,0,0)); }else{ - zObjType = "files"; + zObjType = "Files"; style_submenu_element("Folders","Folders","%s", url_render(&sURI,"nofiles","1",0,0)); } if( zCI ){ - @

%d(nFile) %s(zObjType) of check-in + @

%s(zObjType) from if( sqlite3_strnicmp(zCI, zUuid, (int)strlen(zCI))!=0 ){ @ "%h(zCI)" } - @ [%z(href("vinfo?name=%s",zUuid))%S(zUuid)] %s(blob_str(&dirname))

+ @ [%z(href("vinfo?name=%s",zUuid))%S(zUuid)] %s(blob_str(&dirname)) + if( useMtime ){ + @ sorted by modification time + }else{ + @ sorted by filename + } }else{ int n = db_int(0, "SELECT count(*) FROM plink"); - @

%d(nFile) %s(zObjType) from all %d(n) check-ins + @

%s(zObjType) from all %d(n) check-ins @ %s(blob_str(&dirname))

} /* Generate tree of lists. @@ -596,18 +722,33 @@ if( nD ){ @
  • }else{ @
  • } + @
    @ %z(href("%s",url_render(&sURI,"name",0,0,0)))%h(zProjectName) + if( zNow ){ + @
    %s(zNow)
    + } + @
    @
      + if( zCI && useMtime ){ + p = sortTreeByMtime(sTree.pFirst); + memset(&sTree, 0, sizeof(sTree)); + relinkTree(&sTree, p); + } for(p=sTree.pFirst, nDir=0; p; p=p->pNext){ - const char *zLastClass = p->isLast ? " last" : ""; - if( p->isDir ){ + const char *zLastClass = p->pSibling==0 ? " last" : ""; + if( p->pChild ){ const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : ""; - @
    • + @
    • @ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName) + if( p->mtime>0.0 ){ + char *zAge = human_readable_age(rNow - p->mtime); + @
      %s(zAge)
      + } + @
      if( startExpanded || p->nFullName<=nD ){ @
        }else{ @ } } @@ -687,16 +834,16 @@ @ checkState(); @ outer_ul.onclick = function(e){ @ e = e || window.event; @ var a = e.target || e.srcElement; @ if( a.nodeName!='A' ) return true; - @ if( a.parentNode==subdir ){ + @ if( a.parentNode.parentNode==subdir ){ @ toggleAll(outer_ul); @ return false; @ } @ if( !belowSubdir(a) ) return true; - @ var ul = a.nextSibling; + @ var ul = a.parentNode.nextSibling; @ while( ul && ul.nodeName!='UL' ) ul = ul.nextSibling; @ if( !ul ) return true; /* This is a file link, not a directory */ @ toggleDir(ul); @ return false; @ } @@ -785,19 +932,23 @@ ** The string returned is obtained from fossil_malloc() and should be ** freed by the caller. */ char *human_readable_age(double rAge){ if( rAge*86400.0<120 ){ - return mprintf("%d seconds", (int)(rAge*86400.0)); + if( rAge*86400.0<1.0 ){ + return mprintf("current"); + }else{ + return mprintf("-%d seconds", (int)(rAge*86400.0)); + } }else if( rAge*1440.0<90 ){ - return mprintf("%.1f minutes", rAge*1440.0); + return mprintf("-%.1f minutes", rAge*1440.0); }else if( rAge*24.0<36 ){ - return mprintf("%.1f hours", rAge*24.0); + return mprintf("-%.1f hours", rAge*24.0); }else if( rAge<365.0 ){ - return mprintf("%.1f days", rAge); + return mprintf("-%.1f days", rAge); }else{ - return mprintf("%.2f years", rAge/365.0); + return mprintf("-%.2f years", rAge/365.0); } } /* ** COMMAND: test-fileage @@ -839,10 +990,12 @@ */ void fileage_page(void){ int rid; const char *zName; const char *zGlob; + const char *zUuid; + const char *zNow; /* Time of checkin */ Stmt q1, q2; double baseTime; login_check_credentials(); if( !g.perm.Read ){ login_needed(); return; } zName = P("name"); @@ -849,26 +1002,34 @@ if( zName==0 ) zName = "tip"; rid = symbolic_name_to_rid(zName, "ci"); if( rid==0 ){ fossil_fatal("not a valid check-in: %s", zName); } - style_submenu_element("Tree-View", "Tree-View", "%R/tree?ci=%T", zName); + zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); + baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid); + zNow = db_text("", "SELECT datetime(mtime,'localtime') FROM event" + " WHERE objid=%d", rid); + style_submenu_element("Tree-View", "Tree-View", "%R/tree?ci=%T&mtime=1", + zName); style_header("File Ages"); zGlob = P("glob"); compute_fileage(rid,zGlob); db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);"); - baseTime = db_double(0.0, "SELECT julianday('now');"); - @

        Most recent change to files in checkin - @ %z(href("%R/info?name=%T",zName))%h(zName) + @

        Files in + @ %z(href("%R/info?name=%T",zUuid))[%S(zUuid)] if( zGlob && zGlob[0] ){ - @ that match "%h(zGlob)" + @ that match "%h(zGlob)" and } - @

        + @ ordered by check-in time + @ + @

        Times are relative to the checkin time for + @ %z(href("%R/ci/%s",zUuid))[%S(zUuid)] which is + @ %z(href("%R/timeline?c=%t",zNow))%s(zNow).

        @ @
        - @ + @ db_prepare(&q1, "SELECT event.mtime, event.objid, blob.uuid,\n" " coalesce(event.ecomment,event.comment),\n" " coalesce(event.euser,event.user),\n" " coalesce((SELECT value FROM tagxref\n" Index: src/style.c ================================================================== --- src/style.c +++ src/style.c @@ -839,14 +839,23 @@ @ padding-left: 21px; @ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP\/\/\/yEhIf\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14GqFXImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw==); @ background-position: center left; @ background-repeat: no-repeat; }, - { ".filetree .dir > a", + { ".filetree .dir > div.filetreeline > a", "tree-view directory links", @ background-image: url(data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiIv\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo+jUs6b5Z/K4siDu5RPUFADs=); }, + { "div.filetreeage", + "Last change floating display on the right", + @ clear: right; + @ float: right; + }, + { "div.filetreeline:hover", + "Highlight the line of a file tree", + @ background-color: #eee; + }, { "table.login_out", "table format for login/out label/input table", @ text-align: left; @ margin-right: 10px; @ margin-left: 10px;
        AgeFilesCheckin
        TimeFilesCheckin