Fossil SCM
Add the mtime query parameter to the /tree page.
Commit
fd4e1b55d702de4d078473c58139baa0455b1534
Parent
987af58824c1504…
1 file changed
+139
-25
+139
-25
| --- src/browse.c | ||
| +++ src/browse.c | ||
| @@ -304,87 +304,101 @@ | ||
| 304 | 304 | |
| 305 | 305 | /* |
| 306 | 306 | ** A single line of the file hierarchy |
| 307 | 307 | */ |
| 308 | 308 | 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 */ | |
| 312 | 314 | char *zName; /* Name of this entry. The "tail" */ |
| 313 | 315 | char *zFullName; /* Full pathname of this entry */ |
| 314 | 316 | char *zUuid; /* SHA1 hash of this file. May be NULL. */ |
| 315 | 317 | double mtime; /* Modification time for this entry */ |
| 316 | 318 | unsigned nFullName; /* Length of zFullName */ |
| 317 | 319 | unsigned iLevel; /* Levels of parent directories */ |
| 318 | - u8 isDir; /* True if there are children */ | |
| 319 | - u8 isLast; /* True if this is the last child of its parent */ | |
| 320 | 320 | }; |
| 321 | 321 | |
| 322 | 322 | /* |
| 323 | 323 | ** A complete file hierarchy |
| 324 | 324 | */ |
| 325 | 325 | struct FileTree { |
| 326 | 326 | FileTreeNode *pFirst; /* First line of the list */ |
| 327 | 327 | FileTreeNode *pLast; /* Last line of the list */ |
| 328 | + FileTreeNode *pLastTop; /* Last top-level node */ | |
| 328 | 329 | }; |
| 329 | 330 | |
| 330 | 331 | /* |
| 331 | 332 | ** Add one or more new FileTreeNodes to the FileTree object so that the |
| 332 | -** 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. | |
| 333 | 342 | */ |
| 334 | 343 | static void tree_add_node( |
| 335 | 344 | FileTree *pTree, /* Tree into which nodes are added */ |
| 336 | 345 | const char *zPath, /* The full pathname of file to add */ |
| 337 | 346 | const char *zUuid, /* UUID of the file. Might be NULL. */ |
| 338 | 347 | double mtime /* Modification time for this entry */ |
| 339 | 348 | ){ |
| 340 | 349 | int i; |
| 341 | - FileTreeNode *pParent; | |
| 342 | - FileTreeNode *pChild; | |
| 350 | + FileTreeNode *pParent; /* Parent (directory) of the next node to insert */ | |
| 343 | 351 | |
| 344 | - pChild = pTree->pLast; | |
| 345 | - 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; | |
| 346 | 356 | while( pParent!=0 && |
| 347 | 357 | ( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0 |
| 348 | 358 | || zPath[pParent->nFullName]!='/' ) |
| 349 | 359 | ){ |
| 350 | - pChild = pParent; | |
| 351 | - pParent = pChild->pParent; | |
| 360 | + pParent = pParent->pParent; | |
| 352 | 361 | } |
| 353 | 362 | i = pParent ? pParent->nFullName+1 : 0; |
| 354 | - if( pChild ) pChild->isLast = 0; | |
| 355 | 363 | while( zPath[i] ){ |
| 356 | 364 | FileTreeNode *pNew; |
| 357 | 365 | int iStart = i; |
| 358 | 366 | int nByte; |
| 359 | 367 | while( zPath[i] && zPath[i]!='/' ){ i++; } |
| 360 | 368 | nByte = sizeof(*pNew) + i + 1; |
| 361 | 369 | if( zUuid!=0 && zPath[i]==0 ) nByte += UUID_SIZE+1; |
| 362 | 370 | pNew = fossil_malloc( nByte ); |
| 371 | + memset(pNew, 0, sizeof(*pNew)); | |
| 363 | 372 | pNew->zFullName = (char*)&pNew[1]; |
| 364 | 373 | memcpy(pNew->zFullName, zPath, i); |
| 365 | 374 | pNew->zFullName[i] = 0; |
| 366 | 375 | pNew->nFullName = i; |
| 367 | 376 | if( zUuid!=0 && zPath[i]==0 ){ |
| 368 | 377 | pNew->zUuid = pNew->zFullName + i + 1; |
| 369 | 378 | memcpy(pNew->zUuid, zUuid, UUID_SIZE+1); |
| 370 | - }else{ | |
| 371 | - pNew->zUuid = 0; | |
| 372 | 379 | } |
| 373 | 380 | pNew->zName = pNew->zFullName + iStart; |
| 374 | 381 | if( pTree->pLast ){ |
| 375 | 382 | pTree->pLast->pNext = pNew; |
| 376 | 383 | }else{ |
| 377 | 384 | pTree->pFirst = pNew; |
| 378 | 385 | } |
| 379 | - pNew->pPrev = pTree->pLast; | |
| 380 | - pNew->pNext = 0; | |
| 386 | + pTree->pLast = pNew; | |
| 381 | 387 | pNew->pParent = pParent; |
| 382 | - pTree->pLast = pNew; | |
| 383 | - pNew->iLevel = pParent ? pParent->iLevel+1 : 0; | |
| 384 | - pNew->isDir = zPath[i]=='/'; | |
| 385 | - 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 | + } | |
| 386 | 400 | pNew->mtime = mtime; |
| 387 | 401 | while( zPath[i]=='/' ){ i++; } |
| 388 | 402 | pParent = pNew; |
| 389 | 403 | } |
| 390 | 404 | while( pParent && pParent->pParent ){ |
| @@ -392,10 +406,104 @@ | ||
| 392 | 406 | pParent->pParent->mtime = pParent->mtime; |
| 393 | 407 | } |
| 394 | 408 | pParent = pParent->pParent; |
| 395 | 409 | } |
| 396 | 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 | + | |
| 397 | 505 | |
| 398 | 506 | /* |
| 399 | 507 | ** WEBPAGE: tree |
| 400 | 508 | ** |
| 401 | 509 | ** Query parameters: |
| @@ -403,10 +511,11 @@ | ||
| 403 | 511 | ** name=PATH Directory to display. Optional |
| 404 | 512 | ** ci=LABEL Show only files in this check-in. Optional. |
| 405 | 513 | ** re=REGEXP Show only files matching REGEXP. Optional. |
| 406 | 514 | ** expand Begin with the tree fully expanded. |
| 407 | 515 | ** nofiles Show directories (folders) only. Omit files. |
| 516 | +** mtime Order directory elements by decreasing mtime | |
| 408 | 517 | */ |
| 409 | 518 | void page_tree(void){ |
| 410 | 519 | char *zD = fossil_strdup(P("name")); |
| 411 | 520 | int nD = zD ? strlen(zD)+1 : 0; |
| 412 | 521 | const char *zCI = P("ci"); |
| @@ -556,11 +665,11 @@ | ||
| 556 | 665 | db_finalize(&q); |
| 557 | 666 | } |
| 558 | 667 | |
| 559 | 668 | if( showDirOnly ){ |
| 560 | 669 | for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ |
| 561 | - if( p->isDir && p->nFullName>nD ) nFile++; | |
| 670 | + if( p->pChild!=0 && p->nFullName>nD ) nFile++; | |
| 562 | 671 | } |
| 563 | 672 | zObjType = "folders"; |
| 564 | 673 | style_submenu_element("Files","Files","%s", |
| 565 | 674 | url_render(&sURI,"nofiles",0,0,0)); |
| 566 | 675 | }else{ |
| @@ -604,13 +713,18 @@ | ||
| 604 | 713 | if( zNow ){ |
| 605 | 714 | @ <div class="filetreeage">%s(zNow)</div> |
| 606 | 715 | } |
| 607 | 716 | @ </div> |
| 608 | 717 | @ <ul> |
| 718 | + if( zCI && P("mtime")!=0 ){ | |
| 719 | + p = sortTreeByMtime(sTree.pFirst); | |
| 720 | + memset(&sTree, 0, sizeof(sTree)); | |
| 721 | + relinkTree(&sTree, p); | |
| 722 | + } | |
| 609 | 723 | for(p=sTree.pFirst, nDir=0; p; p=p->pNext){ |
| 610 | - const char *zLastClass = p->isLast ? " last" : ""; | |
| 611 | - if( p->isDir ){ | |
| 724 | + const char *zLastClass = p->pSibling==0 ? " last" : ""; | |
| 725 | + if( p->pChild ){ | |
| 612 | 726 | const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : ""; |
| 613 | 727 | @ <li class="dir%s(zSubdirClass)%s(zLastClass)"><div class="filetreeline"> |
| 614 | 728 | @ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a> |
| 615 | 729 | if( p->mtime>0.0 ){ |
| 616 | 730 | char *zAge = human_readable_age(rNow - p->mtime); |
| @@ -637,11 +751,11 @@ | ||
| 637 | 751 | char *zAge = human_readable_age(rNow - p->mtime); |
| 638 | 752 | @ <div class="filetreeage">%s(zAge)</div> |
| 639 | 753 | } |
| 640 | 754 | @ </div> |
| 641 | 755 | } |
| 642 | - if( p->isLast ){ | |
| 756 | + if( p->pSibling==0 ){ | |
| 643 | 757 | int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0); |
| 644 | 758 | while( nClose-- > 0 ){ |
| 645 | 759 | @ </ul> |
| 646 | 760 | } |
| 647 | 761 | } |
| 648 | 762 |
| --- src/browse.c | |
| +++ src/browse.c | |
| @@ -304,87 +304,101 @@ | |
| 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 | double mtime; /* Modification time for this entry */ |
| 316 | unsigned nFullName; /* Length of zFullName */ |
| 317 | unsigned iLevel; /* Levels of parent directories */ |
| 318 | u8 isDir; /* True if there are children */ |
| 319 | u8 isLast; /* True if this is the last child of its parent */ |
| 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 | }; |
| 329 | |
| 330 | /* |
| 331 | ** Add one or more new FileTreeNodes to the FileTree object so that the |
| 332 | ** leaf object zPathname is at the end of the node list |
| 333 | */ |
| 334 | static void tree_add_node( |
| 335 | FileTree *pTree, /* Tree into which nodes are added */ |
| 336 | const char *zPath, /* The full pathname of file to add */ |
| 337 | const char *zUuid, /* UUID of the file. Might be NULL. */ |
| 338 | double mtime /* Modification time for this entry */ |
| 339 | ){ |
| 340 | int i; |
| 341 | FileTreeNode *pParent; |
| 342 | FileTreeNode *pChild; |
| 343 | |
| 344 | pChild = pTree->pLast; |
| 345 | pParent = pChild ? pChild->pParent : 0; |
| 346 | while( pParent!=0 && |
| 347 | ( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0 |
| 348 | || zPath[pParent->nFullName]!='/' ) |
| 349 | ){ |
| 350 | pChild = pParent; |
| 351 | pParent = pChild->pParent; |
| 352 | } |
| 353 | i = pParent ? pParent->nFullName+1 : 0; |
| 354 | if( pChild ) pChild->isLast = 0; |
| 355 | while( zPath[i] ){ |
| 356 | FileTreeNode *pNew; |
| 357 | int iStart = i; |
| 358 | int nByte; |
| 359 | while( zPath[i] && zPath[i]!='/' ){ i++; } |
| 360 | nByte = sizeof(*pNew) + i + 1; |
| 361 | if( zUuid!=0 && zPath[i]==0 ) nByte += UUID_SIZE+1; |
| 362 | pNew = fossil_malloc( nByte ); |
| 363 | pNew->zFullName = (char*)&pNew[1]; |
| 364 | memcpy(pNew->zFullName, zPath, i); |
| 365 | pNew->zFullName[i] = 0; |
| 366 | pNew->nFullName = i; |
| 367 | if( zUuid!=0 && zPath[i]==0 ){ |
| 368 | pNew->zUuid = pNew->zFullName + i + 1; |
| 369 | memcpy(pNew->zUuid, zUuid, UUID_SIZE+1); |
| 370 | }else{ |
| 371 | pNew->zUuid = 0; |
| 372 | } |
| 373 | pNew->zName = pNew->zFullName + iStart; |
| 374 | if( pTree->pLast ){ |
| 375 | pTree->pLast->pNext = pNew; |
| 376 | }else{ |
| 377 | pTree->pFirst = pNew; |
| 378 | } |
| 379 | pNew->pPrev = pTree->pLast; |
| 380 | pNew->pNext = 0; |
| 381 | pNew->pParent = pParent; |
| 382 | pTree->pLast = pNew; |
| 383 | pNew->iLevel = pParent ? pParent->iLevel+1 : 0; |
| 384 | pNew->isDir = zPath[i]=='/'; |
| 385 | pNew->isLast = 1; |
| 386 | pNew->mtime = mtime; |
| 387 | while( zPath[i]=='/' ){ i++; } |
| 388 | pParent = pNew; |
| 389 | } |
| 390 | while( pParent && pParent->pParent ){ |
| @@ -392,10 +406,104 @@ | |
| 392 | pParent->pParent->mtime = pParent->mtime; |
| 393 | } |
| 394 | pParent = pParent->pParent; |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | /* |
| 399 | ** WEBPAGE: tree |
| 400 | ** |
| 401 | ** Query parameters: |
| @@ -403,10 +511,11 @@ | |
| 403 | ** name=PATH Directory to display. Optional |
| 404 | ** ci=LABEL Show only files in this check-in. Optional. |
| 405 | ** re=REGEXP Show only files matching REGEXP. Optional. |
| 406 | ** expand Begin with the tree fully expanded. |
| 407 | ** nofiles Show directories (folders) only. Omit files. |
| 408 | */ |
| 409 | void page_tree(void){ |
| 410 | char *zD = fossil_strdup(P("name")); |
| 411 | int nD = zD ? strlen(zD)+1 : 0; |
| 412 | const char *zCI = P("ci"); |
| @@ -556,11 +665,11 @@ | |
| 556 | db_finalize(&q); |
| 557 | } |
| 558 | |
| 559 | if( showDirOnly ){ |
| 560 | for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ |
| 561 | if( p->isDir && p->nFullName>nD ) nFile++; |
| 562 | } |
| 563 | zObjType = "folders"; |
| 564 | style_submenu_element("Files","Files","%s", |
| 565 | url_render(&sURI,"nofiles",0,0,0)); |
| 566 | }else{ |
| @@ -604,13 +713,18 @@ | |
| 604 | if( zNow ){ |
| 605 | @ <div class="filetreeage">%s(zNow)</div> |
| 606 | } |
| 607 | @ </div> |
| 608 | @ <ul> |
| 609 | for(p=sTree.pFirst, nDir=0; p; p=p->pNext){ |
| 610 | const char *zLastClass = p->isLast ? " last" : ""; |
| 611 | if( p->isDir ){ |
| 612 | const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : ""; |
| 613 | @ <li class="dir%s(zSubdirClass)%s(zLastClass)"><div class="filetreeline"> |
| 614 | @ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a> |
| 615 | if( p->mtime>0.0 ){ |
| 616 | char *zAge = human_readable_age(rNow - p->mtime); |
| @@ -637,11 +751,11 @@ | |
| 637 | char *zAge = human_readable_age(rNow - p->mtime); |
| 638 | @ <div class="filetreeage">%s(zAge)</div> |
| 639 | } |
| 640 | @ </div> |
| 641 | } |
| 642 | if( p->isLast ){ |
| 643 | int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0); |
| 644 | while( nClose-- > 0 ){ |
| 645 | @ </ul> |
| 646 | } |
| 647 | } |
| 648 |
| --- src/browse.c | |
| +++ src/browse.c | |
| @@ -304,87 +304,101 @@ | |
| 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 ){ |
| @@ -392,10 +406,104 @@ | |
| 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: |
| @@ -403,10 +511,11 @@ | |
| 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"); |
| @@ -556,11 +665,11 @@ | |
| 665 | db_finalize(&q); |
| 666 | } |
| 667 | |
| 668 | if( showDirOnly ){ |
| 669 | for(nFile=0, p=sTree.pFirst; p; p=p->pNext){ |
| 670 | if( p->pChild!=0 && p->nFullName>nD ) nFile++; |
| 671 | } |
| 672 | zObjType = "folders"; |
| 673 | style_submenu_element("Files","Files","%s", |
| 674 | url_render(&sURI,"nofiles",0,0,0)); |
| 675 | }else{ |
| @@ -604,13 +713,18 @@ | |
| 713 | if( zNow ){ |
| 714 | @ <div class="filetreeage">%s(zNow)</div> |
| 715 | } |
| 716 | @ </div> |
| 717 | @ <ul> |
| 718 | if( zCI && P("mtime")!=0 ){ |
| 719 | p = sortTreeByMtime(sTree.pFirst); |
| 720 | memset(&sTree, 0, sizeof(sTree)); |
| 721 | relinkTree(&sTree, p); |
| 722 | } |
| 723 | for(p=sTree.pFirst, nDir=0; p; p=p->pNext){ |
| 724 | const char *zLastClass = p->pSibling==0 ? " last" : ""; |
| 725 | if( p->pChild ){ |
| 726 | const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : ""; |
| 727 | @ <li class="dir%s(zSubdirClass)%s(zLastClass)"><div class="filetreeline"> |
| 728 | @ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a> |
| 729 | if( p->mtime>0.0 ){ |
| 730 | char *zAge = human_readable_age(rNow - p->mtime); |
| @@ -637,11 +751,11 @@ | |
| 751 | char *zAge = human_readable_age(rNow - p->mtime); |
| 752 | @ <div class="filetreeage">%s(zAge)</div> |
| 753 | } |
| 754 | @ </div> |
| 755 | } |
| 756 | if( p->pSibling==0 ){ |
| 757 | int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0); |
| 758 | while( nClose-- > 0 ){ |
| 759 | @ </ul> |
| 760 | } |
| 761 | } |
| 762 |