Fossil SCM
Initial implementation to show "local (uncommitted) changes" in Fossil's UI. Not yet tested to destruction, but fails no more errors in the test-suite than trunk. Currently there are no links to the new page: manually navigate to "/local" (a variant of the "/ci" page).
Commit
ccebe22576d1b607d428a376067ce6e7c5db4023dfda6a0751e904a201761854
Parent
e6ab947e1e8f51a…
1 file changed
+555
-96
+555
-96
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -1,5 +1,19 @@ | ||
| 1 | +/*TODO | |
| 2 | +** o Should /file behave differently for non-existent local files? | |
| 3 | +** o Look at adding an "extras" option (non-added, non-ignored files). | |
| 4 | +** o Look at sifting out "one line" differences from those with "diff blocks". | |
| 5 | +** Perhaps reset the query and re-run, displaying only non-diff entries the | |
| 6 | +** first time? Or perhaps buffer the output (probably bad idea). | |
| 7 | +** o If I keep the extra <hr/> I've added after a diff-block, is there a way | |
| 8 | +** to avoid the double <hr/> if the last entry has a diff-block? | |
| 9 | +** o Find a place to add links to /local. | |
| 10 | +** o Remove //TODO TESTING HACK TODO | |
| 11 | +** ?? In hexdump_page(), should content (and downloadName?) be reset/freed? | |
| 12 | +** ?? In the test fossil (\x\$Test\Fossil) there are (at time of writing) two | |
| 13 | +** commits under the same artifact... is this normal? | |
| 14 | +*/ | |
| 1 | 15 | /* |
| 2 | 16 | ** Copyright (c) 2007 D. Richard Hipp |
| 3 | 17 | ** |
| 4 | 18 | ** This program is free software; you can redistribute it and/or |
| 5 | 19 | ** modify it under the terms of the Simplified BSD License (also |
| @@ -322,17 +336,36 @@ | ||
| 322 | 336 | |TIMELINE_CHPICK, |
| 323 | 337 | 0, 0, 0, rid, rid2, 0); |
| 324 | 338 | db_finalize(&q); |
| 325 | 339 | } |
| 326 | 340 | |
| 341 | +/* | |
| 342 | +** Read the content of file zName (prepended with the checkout directory) | |
| 343 | +** and put it into the uninitialized blob. The blob is zeroed if the file | |
| 344 | +** does not exist (if the file cannot be read, blob_read_from_file() aborts | |
| 345 | +** the program). | |
| 346 | +*/ | |
| 347 | +static void content_from_file( | |
| 348 | + const char *zName, /* Filename (relative to checkout) of file to be read */ | |
| 349 | + Blob *pBlob /* Pointer to blob to receive contents */ | |
| 350 | +){ | |
| 351 | + const char *zFullPath = mprintf("%s%s", g.zLocalRoot, zName); | |
| 352 | + blob_zero(pBlob); | |
| 353 | + if( file_size(zFullPath, ExtFILE)>=0 ){ | |
| 354 | + blob_read_from_file(pBlob, zFullPath, ExtFILE); | |
| 355 | + } | |
| 356 | +} | |
| 327 | 357 | |
| 328 | 358 | /* |
| 329 | 359 | ** Append the difference between artifacts to the output |
| 360 | +** If zLocal is not NULL, instead compare against the local | |
| 361 | +** copy of the file it names in the repository. | |
| 330 | 362 | */ |
| 331 | 363 | static void append_diff( |
| 332 | 364 | const char *zFrom, /* Diff from this artifact */ |
| 333 | 365 | const char *zTo, /* ... to this artifact */ |
| 366 | + const char *zLocal, /* ... OR to this local file */ | |
| 334 | 367 | u64 diffFlags, /* Diff formatting flags */ |
| 335 | 368 | ReCompiled *pRe /* Only show change matching this regex */ |
| 336 | 369 | ){ |
| 337 | 370 | int fromid; |
| 338 | 371 | int toid; |
| @@ -344,10 +377,12 @@ | ||
| 344 | 377 | blob_zero(&from); |
| 345 | 378 | } |
| 346 | 379 | if( zTo ){ |
| 347 | 380 | toid = uuid_to_rid(zTo, 0); |
| 348 | 381 | content_get(toid, &to); |
| 382 | + }else if( zLocal ){ /* Read the file on disk */ | |
| 383 | + content_from_file(zLocal, &to); | |
| 349 | 384 | }else{ |
| 350 | 385 | blob_zero(&to); |
| 351 | 386 | } |
| 352 | 387 | blob_zero(&out); |
| 353 | 388 | if( diffFlags & DIFF_SIDEBYSIDE ){ |
| @@ -396,11 +431,11 @@ | ||
| 396 | 431 | } |
| 397 | 432 | }else{ |
| 398 | 433 | @ Changes to %h(zName). |
| 399 | 434 | } |
| 400 | 435 | if( diffFlags ){ |
| 401 | - append_diff(zOld, zNew, diffFlags, pRe); | |
| 436 | + append_diff(zOld, zNew, NULL, diffFlags, pRe); | |
| 402 | 437 | } |
| 403 | 438 | }else{ |
| 404 | 439 | if( zOld && zNew ){ |
| 405 | 440 | if( fossil_strcmp(zOld, zNew)!=0 ){ |
| 406 | 441 | @ Modified %z(href("%R/finfo?name=%T&m=%!S",zName,zNew))%h(zName)</a> |
| @@ -427,18 +462,172 @@ | ||
| 427 | 462 | }else{ |
| 428 | 463 | @ Added %z(href("%R/finfo?name=%T&m=%!S",zName,zNew))%h(zName)</a> |
| 429 | 464 | @ version %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>. |
| 430 | 465 | } |
| 431 | 466 | if( diffFlags ){ |
| 432 | - append_diff(zOld, zNew, diffFlags, pRe); | |
| 467 | + append_diff(zOld, zNew, NULL, diffFlags, pRe); | |
| 433 | 468 | }else if( zOld && zNew && fossil_strcmp(zOld,zNew)!=0 ){ |
| 434 | 469 | @ |
| 435 | 470 | @ %z(href("%R/fdiff?v1=%!S&v2=%!S",zOld,zNew))[diff]</a> |
| 436 | 471 | } |
| 437 | 472 | } |
| 438 | 473 | @ </p> |
| 439 | 474 | } |
| 475 | + | |
| 476 | +/* | |
| 477 | +** Append notice of executable or symlink being gained or lost. | |
| 478 | +*/ | |
| 479 | +static void append_status( | |
| 480 | + const char *zAction, /* Whether status was gained or lost */ | |
| 481 | + const char *zStatus, /* The status that was gained/lost */ | |
| 482 | + const char *zName, /* Name of file */ | |
| 483 | + const char *zOld /* Existing artifact */ | |
| 484 | +){ | |
| 485 | + if( !g.perm.Hyperlink ){ | |
| 486 | + @ %h(zName) %h(zAction) %h(zStatus) status. | |
| 487 | + }else{ | |
| 488 | + @ %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> | |
| 489 | + @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> | |
| 490 | + @ %h(zAction) %h(zStatus) status. | |
| 491 | + } | |
| 492 | +} | |
| 493 | + | |
| 494 | +/* | |
| 495 | +** Append web-page output that shows changes between a file at last check-in | |
| 496 | +** and its current state on disk (i.e. any uncommitted changes). The status | |
| 497 | +** ("changed", "missing" etc.) is a blend of that from append_file_change_line() | |
| 498 | +** and diff_against_disk() (in "diffcmd.c"). | |
| 499 | +** | |
| 500 | +** The file-differences (if being shown) use append_diff() as before, but | |
| 501 | +** there is an additional parameter (zLocal) which, if non-NULL, causes it | |
| 502 | +** to compare the checked-in version against the named file on disk. | |
| 503 | +*/ | |
| 504 | +static void append_local_file_change_line( | |
| 505 | + const char *zName, /* Name of the file that has changed */ | |
| 506 | + const char *zOld, /* blob.uuid before change. NULL for added files */ | |
| 507 | + int isDeleted, /* Has the file-on-disk been removed from Fossil? */ | |
| 508 | + int isChnged, /* Has the file changed in some way (see vfile.c) */ | |
| 509 | + int isNew, /* Has the file been ADDed (but not yet committed? */ | |
| 510 | + int isLink, /* Is the file a symbolic link? */ | |
| 511 | + u64 diffFlags, /* Flags for text_diff(). Zero to omit diffs */ | |
| 512 | + ReCompiled *pRe /* Only show diffs that match this regex, if not NULL */ | |
| 513 | +){ | |
| 514 | + char *zFullName = mprintf("%s%s", g.zLocalRoot, zName); | |
| 515 | + int isFilePresent = !file_access(zFullName, F_OK); | |
| 516 | + int showDiff = 0; | |
| 517 | +//TODO TESTING HACK TODO | |
| 518 | +if( strncmp(zName,"aa",2)==0 ){ | |
| 519 | + isChnged = atoi(zName+2); | |
| 520 | +} | |
| 521 | +//TODO TESTING HACK TODO | |
| 522 | + @ <p> | |
| 523 | + if( !g.perm.Hyperlink ){ | |
| 524 | + if( isDeleted ){ | |
| 525 | + if( isFilePresent ){ | |
| 526 | + @ Deleted %h(zName) (still present as a local file). | |
| 527 | + showDiff = 1; | |
| 528 | + }else{ | |
| 529 | + @ Deleted %h(zName). | |
| 530 | + } | |
| 531 | + }else if( isNew ){ | |
| 532 | + if( isFilePresent ){ | |
| 533 | + @ Added %h(zName) but not committed. | |
| 534 | + }else{ | |
| 535 | + @ Missing %h(zName) (was added to checkout). | |
| 536 | + } | |
| 537 | + }else switch( isChnged ){ | |
| 538 | + /*TODO | |
| 539 | + ** These "special cases" have not been properly tested (by creating | |
| 540 | + ** entries in a in a repository to trigger them), but they do display | |
| 541 | + ** as expected when "forced" to appear. | |
| 542 | + */ | |
| 543 | + case 3: | |
| 544 | + @ Added %h(zName) due to a merge. | |
| 545 | + break; | |
| 546 | + case 5: | |
| 547 | + @ Added %h(zName) due to an integration merge. | |
| 548 | + break; | |
| 549 | + case 6: append_status( "gained", "executable", zName, zOld); break; | |
| 550 | + case 7: append_status( "gained", "symlink", zName, zOld); break; | |
| 551 | + case 8: append_status( "lost", "executable", zName, zOld); break; | |
| 552 | + case 9: append_status( "lost", "symlink", zName, zOld); break; | |
| 553 | + | |
| 554 | + default: /* Normal edit */ | |
| 555 | + @ Local changes of %h(zName). | |
| 556 | + showDiff = 1; | |
| 557 | + } | |
| 558 | + if( showDiff && diffFlags ){ | |
| 559 | + append_diff(zOld, NULL, zName, diffFlags, pRe); | |
| 560 | + @ <hr/> | |
| 561 | + } | |
| 562 | + }else{ | |
| 563 | + if( isDeleted ){ | |
| 564 | + if( isFilePresent ){ /* DELETEd but still on disk */ | |
| 565 | + @ Deleted %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> | |
| 566 | + @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> (still present | |
| 567 | + @ as %z(href("%R/file/%T?ci=ckout&annot=removed from checkout",zName)) | |
| 568 | + @ [local file]</a>). | |
| 569 | + showDiff = 1; | |
| 570 | + }else{ /* DELETEd and deleted from disk */ | |
| 571 | + @ Deleted %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> | |
| 572 | + @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a>. | |
| 573 | + } | |
| 574 | + }else if( isNew ){ | |
| 575 | + if( isFilePresent ){ /* ADDed and present on disk */ | |
| 576 | + @ Added %z(href("%R/file/%T?ci=ckout",zName))%h(zName)</a> | |
| 577 | + @ but not committed. | |
| 578 | + }else{ /* ADDed but not present on disk */ | |
| 579 | + @ Missing %h(zName) (was added to checkout). | |
| 580 | + } | |
| 581 | + }else switch( isChnged ){ | |
| 582 | + /*TODO Not fully tested... see see no-hyperlink version above */ | |
| 583 | + case 3: /* Added by a merge */ | |
| 584 | + @ Added | |
| 585 | + @ %z(href("%R/file/%T?ci=ckout&annot=added by merge",zName))%h(zName) | |
| 586 | + @ </a> to %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> due to merge. | |
| 587 | + break; | |
| 588 | + case 5: /* Added by an integration merge */ | |
| 589 | + @ Added | |
| 590 | + @ %z(href("%R/file/%T?ci=ckout&annot=added by integration merge",zName)) | |
| 591 | + @ %h(zName)</a> to | |
| 592 | + @ %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> due to integrate merge. | |
| 593 | + break; | |
| 594 | + case 6: append_status( "gained", "executable", zName, zOld); break; | |
| 595 | + case 7: append_status( "gained", "symlink", zName, zOld); break; | |
| 596 | + case 8: append_status( "lost", "executable", zName, zOld); break; | |
| 597 | + case 9: append_status( "lost", "symlink", zName, zOld); break; | |
| 598 | + | |
| 599 | + default: /* Normal edit */ | |
| 600 | + @ Local changes of | |
| 601 | + @ %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> | |
| 602 | + @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> to | |
| 603 | + @ %z(href("%R/file/%T?ci=ckout&annot=edited locally",zName)) | |
| 604 | + @ [local file]</a> | |
| 605 | + showDiff = 1; | |
| 606 | + } | |
| 607 | + if( showDiff ){ | |
| 608 | + if( diffFlags ){ | |
| 609 | + append_diff(zOld, NULL, zName, diffFlags, pRe); | |
| 610 | + /*TODO | |
| 611 | + ** Not related to the local-mode, but if two or more files have been | |
| 612 | + ** changed in a commit/"local changes", it is sometimes easy to miss | |
| 613 | + ** the switch from one to the other. The following (IMHO) makes things | |
| 614 | + ** clearer, but can mean there's a double rule at the bottom of the | |
| 615 | + ** page. If kept, a similar <hr/> should probably be added to | |
| 616 | + ** append_file_change_line() (but would need to check how things look | |
| 617 | + ** when called from /vinfo). | |
| 618 | + */ | |
| 619 | + @ <hr/> | |
| 620 | + }else if( isChnged ){ | |
| 621 | + @ | |
| 622 | + @ %z(href("%R/localdiff?name=%T",zName))[diff]</a> | |
| 623 | + } | |
| 624 | + } | |
| 625 | + } | |
| 626 | + @ </p> | |
| 627 | + fossil_free(zFullName); | |
| 628 | +} | |
| 440 | 629 | |
| 441 | 630 | /* |
| 442 | 631 | ** Generate javascript to enhance HTML diffs. |
| 443 | 632 | */ |
| 444 | 633 | void append_diff_javascript(int sideBySide){ |
| @@ -596,19 +785,24 @@ | ||
| 596 | 785 | } |
| 597 | 786 | |
| 598 | 787 | /* |
| 599 | 788 | ** WEBPAGE: vinfo |
| 600 | 789 | ** WEBPAGE: ci |
| 790 | +** WEBPAGE: local | |
| 601 | 791 | ** URL: /ci/ARTIFACTID |
| 602 | 792 | ** OR: /ci?name=ARTIFACTID |
| 603 | 793 | ** |
| 604 | 794 | ** Display information about a particular check-in. The exact |
| 605 | 795 | ** same information is shown on the /info page if the name query |
| 606 | 796 | ** parameter to /info describes a check-in. |
| 607 | 797 | ** |
| 608 | 798 | ** The ARTIFACTID can be a unique prefix for the HASH of the check-in, |
| 609 | 799 | ** or a tag or branch name that identifies the check-in. |
| 800 | +** | |
| 801 | +** Use of /local (or the use of "ckout" for ARTIFACTID) will show the | |
| 802 | +** same header details as /ci/tip, but then displays any (uncommitted) | |
| 803 | +** edits made to files in the checkout directory. | |
| 610 | 804 | */ |
| 611 | 805 | void ci_page(void){ |
| 612 | 806 | Stmt q1, q2, q3; |
| 613 | 807 | int rid; |
| 614 | 808 | int isLeaf; |
| @@ -621,14 +815,37 @@ | ||
| 621 | 815 | ReCompiled *pRe = 0; /* regex */ |
| 622 | 816 | const char *zW; /* URL param for ignoring whitespace */ |
| 623 | 817 | const char *zPage = "vinfo"; /* Page that shows diffs */ |
| 624 | 818 | const char *zPageHide = "ci"; /* Page that hides diffs */ |
| 625 | 819 | const char *zBrName; /* Branch name */ |
| 820 | + int bLocalMode; /* TRUE for /local; FALSE otherwise */ | |
| 821 | + int vid; /* Virtual file system? */ | |
| 626 | 822 | |
| 627 | 823 | login_check_credentials(); |
| 628 | 824 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 629 | 825 | zName = P("name"); |
| 826 | + /* Local mode is selected by either "/local" or with a "name" of "ckout". | |
| 827 | + ** First, check we have access to the checkout (and report to the user if we | |
| 828 | + ** don't), then refresh the "vfile" table (recording which files in the | |
| 829 | + ** checkout have changed etc.). We then change the "name" parameter to "tip" | |
| 830 | + ** so that the "header" section displays info about the check-in that the | |
| 831 | + ** checkout came from. | |
| 832 | + */ | |
| 833 | + bLocalMode = (g.zPath[0]=='l') || (fossil_strcmp(zName,"ckout")==0); | |
| 834 | + if( bLocalMode ){ | |
| 835 | + vid = g.localOpen ? db_lget_int("checkout", 0) : 0; | |
| 836 | + if( vid==0 ){ | |
| 837 | + /*TODO Is this the right response? */ | |
| 838 | + style_header("No Local Checkout"); | |
| 839 | + @ No access to local checkout. | |
| 840 | + style_footer(); | |
| 841 | + return; | |
| 842 | + } | |
| 843 | + vfile_check_signature(vid, CKSIG_ENOTFILE); | |
| 844 | + zName = "tip"; | |
| 845 | + cgi_replace_parameter("name","tip"); /* Needed to get rid below */ | |
| 846 | + } | |
| 630 | 847 | rid = name_to_rid_www("name"); |
| 631 | 848 | if( rid==0 ){ |
| 632 | 849 | style_header("Check-in Information Error"); |
| 633 | 850 | @ No such object: %h(g.argv[2]) |
| 634 | 851 | style_footer(); |
| @@ -667,11 +884,15 @@ | ||
| 667 | 884 | int okWiki = 0; |
| 668 | 885 | Blob wiki_read_links = BLOB_INITIALIZER; |
| 669 | 886 | Blob wiki_add_links = BLOB_INITIALIZER; |
| 670 | 887 | |
| 671 | 888 | Th_Store("current_checkin", zName); |
| 672 | - style_header("Check-in [%S]", zUuid); | |
| 889 | + if( bLocalMode ){ | |
| 890 | + style_header("Local Changes from Check-in [%S]", zUuid); | |
| 891 | + }else{ | |
| 892 | + style_header("Check-in [%S]", zUuid); | |
| 893 | + } | |
| 673 | 894 | login_anonymous_available(); |
| 674 | 895 | zEUser = db_text(0, |
| 675 | 896 | "SELECT value FROM tagxref" |
| 676 | 897 | " WHERE tagid=%d AND rid=%d AND tagtype>0", |
| 677 | 898 | TAG_USER, rid); |
| @@ -873,14 +1094,25 @@ | ||
| 873 | 1094 | wiki_render_associated("checkin", zUuid, 0); |
| 874 | 1095 | } |
| 875 | 1096 | render_backlink_graph(zUuid, "<div class=\"section\">References</div>\n"); |
| 876 | 1097 | @ <div class="section">Context</div> |
| 877 | 1098 | render_checkin_context(rid, 0, 0); |
| 878 | - @ <div class="section">Changes</div> | |
| 1099 | + if( bLocalMode ){ | |
| 1100 | + @ <div class="section">Uncommitted Changes</div> | |
| 1101 | + }else{ | |
| 1102 | + @ <div class="section">Changes</div> | |
| 1103 | + } | |
| 879 | 1104 | @ <div class="sectionmenu"> |
| 880 | 1105 | diffFlags = construct_diff_flags(diffType); |
| 881 | 1106 | zW = (diffFlags&DIFF_IGNORE_ALLWS)?"&w":""; |
| 1107 | + /* In local mode, having displayed the header info for "tip", switch zName | |
| 1108 | + ** to be "ckout" so the style-altering links (unified or side-by-side etc.) | |
| 1109 | + ** will correctly re-select local-mode. | |
| 1110 | + */ | |
| 1111 | + if( bLocalMode ){ | |
| 1112 | + zName = "ckout"; | |
| 1113 | + } | |
| 882 | 1114 | if( diffType!=0 ){ |
| 883 | 1115 | @ %z(chref("button","%R/%s/%T?diff=0",zPageHide,zName))\ |
| 884 | 1116 | @ Hide Diffs</a> |
| 885 | 1117 | } |
| 886 | 1118 | if( diffType!=1 ){ |
| @@ -898,48 +1130,163 @@ | ||
| 898 | 1130 | }else{ |
| 899 | 1131 | @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType)) |
| 900 | 1132 | @ Ignore Whitespace</a> |
| 901 | 1133 | } |
| 902 | 1134 | } |
| 903 | - if( zParent ){ | |
| 904 | - @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) | |
| 905 | - @ Patch</a> | |
| 1135 | + if( bLocalMode ){ | |
| 1136 | + @ %z(chref("button","%R/localpatch")) Patch</a> | |
| 1137 | + }else{ | |
| 1138 | + if( zParent ){ | |
| 1139 | + @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) | |
| 1140 | + @ Patch</a> | |
| 1141 | + } | |
| 906 | 1142 | } |
| 907 | 1143 | if( g.perm.Admin ){ |
| 908 | 1144 | @ %z(chref("button","%R/mlink?ci=%!S",zUuid))MLink Table</a> |
| 909 | 1145 | } |
| 910 | 1146 | @</div> |
| 911 | 1147 | if( pRe ){ |
| 912 | 1148 | @ <p><b>Only differences that match regular expression "%h(zRe)" |
| 913 | 1149 | @ are shown.</b></p> |
| 914 | 1150 | } |
| 915 | - db_prepare(&q3, | |
| 916 | - "SELECT name," | |
| 917 | - " mperm," | |
| 918 | - " (SELECT uuid FROM blob WHERE rid=mlink.pid)," | |
| 919 | - " (SELECT uuid FROM blob WHERE rid=mlink.fid)," | |
| 920 | - " (SELECT name FROM filename WHERE filename.fnid=mlink.pfnid)" | |
| 921 | - " FROM mlink JOIN filename ON filename.fnid=mlink.fnid" | |
| 922 | - " WHERE mlink.mid=%d AND NOT mlink.isaux" | |
| 923 | - " AND (mlink.fid>0" | |
| 924 | - " OR mlink.fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=%d))" | |
| 925 | - " ORDER BY name /*sort*/", | |
| 926 | - rid, rid | |
| 927 | - ); | |
| 928 | - while( db_step(&q3)==SQLITE_ROW ){ | |
| 929 | - const char *zName = db_column_text(&q3,0); | |
| 930 | - int mperm = db_column_int(&q3, 1); | |
| 931 | - const char *zOld = db_column_text(&q3,2); | |
| 932 | - const char *zNew = db_column_text(&q3,3); | |
| 933 | - const char *zOldName = db_column_text(&q3, 4); | |
| 934 | - append_file_change_line(zName, zOld, zNew, zOldName, diffFlags,pRe,mperm); | |
| 935 | - } | |
| 936 | - db_finalize(&q3); | |
| 1151 | + if( bLocalMode ){ | |
| 1152 | + /* Following SQL taken from diff_against_disk() in diffcmd.c */ | |
| 1153 | + db_begin_transaction(); | |
| 1154 | + db_prepare(&q3, | |
| 1155 | + "SELECT pathname, deleted, chnged , rid==0, rid, islink" | |
| 1156 | + " FROM vfile" | |
| 1157 | + " WHERE vid=%d" | |
| 1158 | + " AND (deleted OR chnged OR rid==0)" | |
| 1159 | + " ORDER BY pathname /*scan*/", | |
| 1160 | + vid | |
| 1161 | + ); | |
| 1162 | + /* TODO Have the option of showing "extras" (non-ignored files in the | |
| 1163 | + ** checkout directory that have not been ADDed). If done, they should | |
| 1164 | + ** be ahead of any potential "diff-blocks" so they don't get lost | |
| 1165 | + ** (which is the inspiration for...) | |
| 1166 | + ** TODO Consider making this two-pass, where the first pass skips anything | |
| 1167 | + ** that would show a diff-block (and the second pass only shows such | |
| 1168 | + ** entries). This would group all "one-line" entries at the top so | |
| 1169 | + ** they are less likely to be missed. | |
| 1170 | + ** TODO Possibly (at some stage) have an option to commit? | |
| 1171 | + */ | |
| 1172 | + while( db_step(&q3)==SQLITE_ROW ){ | |
| 1173 | + const char *zPathname = db_column_text(&q3,0); | |
| 1174 | + int isDeleted = db_column_int(&q3, 1); | |
| 1175 | + int isChnged = db_column_int(&q3,2); | |
| 1176 | + int isNew = db_column_int(&q3,3); | |
| 1177 | + int srcid = db_column_int(&q3, 4); | |
| 1178 | + int isLink = db_column_int(&q3, 5); | |
| 1179 | + char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", srcid); | |
| 1180 | + append_local_file_change_line(zPathname, zUuid, | |
| 1181 | + isDeleted, isChnged, isNew, isLink, diffFlags,pRe); | |
| 1182 | + free(zUuid); | |
| 1183 | + } | |
| 1184 | + db_finalize(&q3); | |
| 1185 | + db_end_transaction(1); /* ROLLBACK */ | |
| 1186 | + }else{ /* Normal, non-local-mode: show diffs against parent */ | |
| 1187 | + db_prepare(&q3, | |
| 1188 | + "SELECT name," | |
| 1189 | + " mperm," | |
| 1190 | + " (SELECT uuid FROM blob WHERE rid=mlink.pid)," | |
| 1191 | + " (SELECT uuid FROM blob WHERE rid=mlink.fid)," | |
| 1192 | + " (SELECT name FROM filename WHERE filename.fnid=mlink.pfnid)" | |
| 1193 | + " FROM mlink JOIN filename ON filename.fnid=mlink.fnid" | |
| 1194 | + " WHERE mlink.mid=%d AND NOT mlink.isaux" | |
| 1195 | + " AND (mlink.fid>0" | |
| 1196 | + " OR mlink.fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=%d))" | |
| 1197 | + " ORDER BY name /*sort*/", | |
| 1198 | + rid, rid | |
| 1199 | + ); | |
| 1200 | + while( db_step(&q3)==SQLITE_ROW ){ | |
| 1201 | + const char *zName = db_column_text(&q3,0); | |
| 1202 | + int mperm = db_column_int(&q3, 1); | |
| 1203 | + const char *zOld = db_column_text(&q3,2); | |
| 1204 | + const char *zNew = db_column_text(&q3,3); | |
| 1205 | + const char *zOldName = db_column_text(&q3, 4); | |
| 1206 | + append_file_change_line(zName, zOld, zNew, zOldName, diffFlags,pRe,mperm); | |
| 1207 | + } | |
| 1208 | + db_finalize(&q3); | |
| 1209 | + } | |
| 937 | 1210 | append_diff_javascript(diffType==2); |
| 938 | 1211 | cookie_render(); |
| 939 | 1212 | style_footer(); |
| 940 | 1213 | } |
| 1214 | + | |
| 1215 | +/* | |
| 1216 | +** WEBPAGE: localpatch | |
| 1217 | +** URL: /localpatch | |
| 1218 | +** | |
| 1219 | +** Shows a patch from the current checkout, incorporating any | |
| 1220 | +** uncommitted local edits. | |
| 1221 | +*/ | |
| 1222 | +void localpatch_page(void){ | |
| 1223 | + Stmt q3; | |
| 1224 | + int vid; | |
| 1225 | + | |
| 1226 | + login_check_credentials(); | |
| 1227 | + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } | |
| 1228 | + | |
| 1229 | + vid = g.localOpen ? db_lget_int("checkout", 0) : 0; | |
| 1230 | + if( vid==0 ){ | |
| 1231 | + /*TODO Is this the right response? */ | |
| 1232 | + style_header("No Local Checkout"); | |
| 1233 | + @ No access to local checkout. | |
| 1234 | + style_footer(); | |
| 1235 | + return; | |
| 1236 | + } | |
| 1237 | + vfile_check_signature(vid, CKSIG_ENOTFILE); | |
| 1238 | + | |
| 1239 | + cgi_set_content_type("text/plain"); | |
| 1240 | + | |
| 1241 | + db_begin_transaction(); | |
| 1242 | + /*TODO | |
| 1243 | + ** This query is the same as in ci_page() for local-mode (as well as in | |
| 1244 | + ** diff_against_disk() in diffcmd.c, where it was originally taken from). | |
| 1245 | + ** Should they be "coalesced" in some way? | |
| 1246 | + */ | |
| 1247 | + db_prepare(&q3, | |
| 1248 | + "SELECT pathname, deleted, chnged , rid==0, rid, islink" | |
| 1249 | + " FROM vfile" | |
| 1250 | + " WHERE vid=%d" | |
| 1251 | + " AND (deleted OR chnged OR rid==0)" | |
| 1252 | + " ORDER BY pathname /*scan*/", | |
| 1253 | + vid | |
| 1254 | + ); | |
| 1255 | + while( db_step(&q3)==SQLITE_ROW ){ | |
| 1256 | + const char *zPathname = db_column_text(&q3,0); | |
| 1257 | + int isDeleted = db_column_int(&q3, 1); | |
| 1258 | + int isChnged = db_column_int(&q3,2); | |
| 1259 | + int isNew = db_column_int(&q3,3); | |
| 1260 | + int srcid = db_column_int(&q3, 4); | |
| 1261 | + int isLink = db_column_int(&q3, 5); | |
| 1262 | + char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", srcid); | |
| 1263 | + | |
| 1264 | + if( isChnged ){ | |
| 1265 | + Blob c1, c2; /* Content to diff */ | |
| 1266 | + Blob out; /* Diff output text */ | |
| 1267 | + int diffFlags = 4; | |
| 1268 | + | |
| 1269 | + content_get(srcid, &c1); | |
| 1270 | + content_from_file(zPathname, &c2); | |
| 1271 | + blob_zero(&out); | |
| 1272 | + text_diff(&c1, &c2, &out, 0, diffFlags); | |
| 1273 | + blob_reset(&c1); | |
| 1274 | + blob_reset(&c2); | |
| 1275 | + if( blob_size(&out) ){ | |
| 1276 | + diff_print_index(zPathname, diffFlags); | |
| 1277 | + diff_print_filenames(zPathname, zPathname, diffFlags); | |
| 1278 | + fossil_print("%s\n", blob_str(&out)); | |
| 1279 | + } | |
| 1280 | + /* Release memory resources */ | |
| 1281 | + blob_reset(&out); | |
| 1282 | + } | |
| 1283 | + free(zUuid); | |
| 1284 | + } | |
| 1285 | + db_finalize(&q3); | |
| 1286 | + db_end_transaction(1); /* ROLLBACK */ | |
| 1287 | +} | |
| 941 | 1288 | |
| 942 | 1289 | /* |
| 943 | 1290 | ** WEBPAGE: winfo |
| 944 | 1291 | ** URL: /winfo?name=UUID |
| 945 | 1292 | ** |
| @@ -1216,11 +1563,11 @@ | ||
| 1216 | 1563 | style_submenu_element("Side-by-Side Diff", |
| 1217 | 1564 | "%R/vdiff?%s&diff=2%s%T%s", |
| 1218 | 1565 | zQuery, |
| 1219 | 1566 | zGlob ? "&glob=" : "", zGlob ? zGlob : "", zW); |
| 1220 | 1567 | } |
| 1221 | - if( diffType!=1 ) { | |
| 1568 | + if( diffType!=1 ){ | |
| 1222 | 1569 | style_submenu_element("Unified Diff", |
| 1223 | 1570 | "%R/vdiff?%s&diff=1%s%T%s", |
| 1224 | 1571 | zQuery, |
| 1225 | 1572 | zGlob ? "&glob=" : "", zGlob ? zGlob : "", zW); |
| 1226 | 1573 | } |
| @@ -1400,11 +1747,11 @@ | ||
| 1400 | 1747 | } |
| 1401 | 1748 | cnt++; |
| 1402 | 1749 | continue; |
| 1403 | 1750 | } |
| 1404 | 1751 | if( !sameFilename ){ |
| 1405 | - if( prevName && showDetail ) { | |
| 1752 | + if( prevName && showDetail ){ | |
| 1406 | 1753 | @ </ul> |
| 1407 | 1754 | } |
| 1408 | 1755 | if( mPerm==PERM_LNK ){ |
| 1409 | 1756 | @ <li>Symbolic link |
| 1410 | 1757 | objType |= OBJTYPE_SYMLINK; |
| @@ -1520,11 +1867,11 @@ | ||
| 1520 | 1867 | objType |= OBJTYPE_TICKET; |
| 1521 | 1868 | }else if( zType[0]=='c' ){ |
| 1522 | 1869 | @ Manifest of check-in |
| 1523 | 1870 | objType |= OBJTYPE_CHECKIN; |
| 1524 | 1871 | }else if( zType[0]=='e' ){ |
| 1525 | - if( eventTagId != 0) { | |
| 1872 | + if( eventTagId != 0){ | |
| 1526 | 1873 | @ Instance of technote |
| 1527 | 1874 | objType |= OBJTYPE_EVENT; |
| 1528 | 1875 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1529 | 1876 | }else{ |
| 1530 | 1877 | @ Attachment to technote |
| @@ -1567,11 +1914,11 @@ | ||
| 1567 | 1914 | }else{ |
| 1568 | 1915 | @ Attachment "%h(zFilename)" to |
| 1569 | 1916 | } |
| 1570 | 1917 | objType |= OBJTYPE_ATTACHMENT; |
| 1571 | 1918 | if( fossil_is_uuid(zTarget) ){ |
| 1572 | - if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", | |
| 1919 | + if( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", | |
| 1573 | 1920 | zTarget) |
| 1574 | 1921 | ){ |
| 1575 | 1922 | if( g.perm.Hyperlink && g.anon.RdTkt ){ |
| 1576 | 1923 | @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] |
| 1577 | 1924 | }else{ |
| @@ -1625,11 +1972,13 @@ | ||
| 1625 | 1972 | } |
| 1626 | 1973 | |
| 1627 | 1974 | |
| 1628 | 1975 | /* |
| 1629 | 1976 | ** WEBPAGE: fdiff |
| 1977 | +** WEBPAGE: localdiff | |
| 1630 | 1978 | ** URL: fdiff?v1=UUID&v2=UUID |
| 1979 | +** URL: localdiff?name=filename | |
| 1631 | 1980 | ** |
| 1632 | 1981 | ** Two arguments, v1 and v2, identify the artifacts to be diffed. |
| 1633 | 1982 | ** Show diff side by side unless sbs is 0. Generate plain text if |
| 1634 | 1983 | ** "patch" is present, otherwise generate "pretty" HTML. |
| 1635 | 1984 | ** |
| @@ -1637,10 +1986,14 @@ | ||
| 1637 | 1986 | ** |
| 1638 | 1987 | ** If the "from" and "to" query parameters are both present, then they are |
| 1639 | 1988 | ** the names of two files within the check-in "ci" that are diffed. If the |
| 1640 | 1989 | ** "ci" parameter is omitted, then the most recent check-in ("tip") is |
| 1641 | 1990 | ** used. |
| 1991 | +** | |
| 1992 | +** The /localdiff version will diff the given filename from the most recent | |
| 1993 | +** check-in ("tip") against the current (edited) version in the checkout | |
| 1994 | +** directory. | |
| 1642 | 1995 | ** |
| 1643 | 1996 | ** Additional parameters: |
| 1644 | 1997 | ** |
| 1645 | 1998 | ** dc=N Show N lines of context around each diff |
| 1646 | 1999 | ** patch Use the patch diff format |
| @@ -1658,17 +2011,23 @@ | ||
| 1658 | 2011 | const char *zRe; |
| 1659 | 2012 | ReCompiled *pRe = 0; |
| 1660 | 2013 | u64 diffFlags; |
| 1661 | 2014 | u32 objdescFlags = 0; |
| 1662 | 2015 | int verbose = PB("verbose"); |
| 2016 | + int bLocalMode = g.zPath[0]=='l'; /* diff against checkout */ | |
| 2017 | + const char *zLocalName = NULL; /* Holds local filename */ | |
| 1663 | 2018 | |
| 1664 | 2019 | login_check_credentials(); |
| 1665 | 2020 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 1666 | 2021 | cookie_link_parameter("diff","diff","2"); |
| 1667 | 2022 | diffType = atoi(PD("diff","2")); |
| 1668 | 2023 | cookie_render(); |
| 1669 | - if( P("from") && P("to") ){ | |
| 2024 | + if( bLocalMode ){ | |
| 2025 | + zLocalName = P("name"); | |
| 2026 | + v1 = artifact_from_ci_and_filename("name"); | |
| 2027 | + v2 = (zLocalName!=NULL)?-1:0; /* -1 prevents "not found" check below */ | |
| 2028 | + }else if( P("from") && P("to") ){ | |
| 1670 | 2029 | v1 = artifact_from_ci_and_filename("from"); |
| 1671 | 2030 | v2 = artifact_from_ci_and_filename("to"); |
| 1672 | 2031 | }else{ |
| 1673 | 2032 | Stmt q; |
| 1674 | 2033 | v1 = name_to_rid_www("v1"); |
| @@ -1707,14 +2066,19 @@ | ||
| 1707 | 2066 | if( zRe ) re_compile(&pRe, zRe, 0); |
| 1708 | 2067 | if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
| 1709 | 2068 | if( isPatch ){ |
| 1710 | 2069 | Blob c1, c2, *pOut; |
| 1711 | 2070 | pOut = cgi_output_blob(); |
| 2071 | + | |
| 1712 | 2072 | cgi_set_content_type("text/plain"); |
| 1713 | 2073 | diffFlags = 4; |
| 1714 | 2074 | content_get(v1, &c1); |
| 1715 | - content_get(v2, &c2); | |
| 2075 | + if( bLocalMode ){ | |
| 2076 | + content_from_file(zLocalName, &c2); | |
| 2077 | + }else{ | |
| 2078 | + content_get(v2, &c2); | |
| 2079 | + } | |
| 1716 | 2080 | text_diff(&c1, &c2, pOut, pRe, diffFlags); |
| 1717 | 2081 | blob_reset(&c1); |
| 1718 | 2082 | blob_reset(&c2); |
| 1719 | 2083 | return; |
| 1720 | 2084 | } |
| @@ -1723,38 +2087,62 @@ | ||
| 1723 | 2087 | zV2 = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", v2); |
| 1724 | 2088 | diffFlags = construct_diff_flags(diffType) | DIFF_HTML; |
| 1725 | 2089 | |
| 1726 | 2090 | style_header("Diff"); |
| 1727 | 2091 | style_submenu_checkbox("w", "Ignore Whitespace", 0, 0); |
| 1728 | - if( diffType==2 ){ | |
| 1729 | - style_submenu_element("Unified Diff", "%R/fdiff?v1=%T&v2=%T&diff=1", | |
| 1730 | - P("v1"), P("v2")); | |
| 1731 | - }else{ | |
| 1732 | - style_submenu_element("Side-by-side Diff", "%R/fdiff?v1=%T&v2=%T&diff=2", | |
| 1733 | - P("v1"), P("v2")); | |
| 2092 | + if( bLocalMode ){ | |
| 2093 | + if( diffType==2 ){ | |
| 2094 | + style_submenu_element("Unified Diff", "%R/localdiff?name=%T&diff=1", | |
| 2095 | + zLocalName); | |
| 2096 | + }else{ | |
| 2097 | + style_submenu_element("Side-by-side Diff", "%R/localdiff?name=%T&diff=2", | |
| 2098 | + zLocalName); | |
| 2099 | + } | |
| 2100 | + }else{ /* Normal */ | |
| 2101 | + if( diffType==2 ){ | |
| 2102 | + style_submenu_element("Unified Diff", "%R/fdiff?v1=%T&v2=%T&diff=1", | |
| 2103 | + P("v1"), P("v2")); | |
| 2104 | + }else{ | |
| 2105 | + style_submenu_element("Side-by-side Diff", "%R/fdiff?v1=%T&v2=%T&diff=2", | |
| 2106 | + P("v1"), P("v2")); | |
| 2107 | + } | |
| 1734 | 2108 | } |
| 1735 | 2109 | style_submenu_checkbox("verbose", "Verbose", 0, 0); |
| 1736 | - style_submenu_element("Patch", "%R/fdiff?v1=%T&v2=%T&patch", | |
| 1737 | - P("v1"), P("v2")); | |
| 2110 | + if( bLocalMode ){ | |
| 2111 | + style_submenu_element("Patch", "%R/localdiff?name=%T&patch", zLocalName); | |
| 2112 | + }else{ | |
| 2113 | + style_submenu_element("Patch", "%R/fdiff?v1=%T&v2=%T&patch", | |
| 2114 | + P("v1"), P("v2")); | |
| 2115 | + } | |
| 1738 | 2116 | |
| 1739 | 2117 | if( P("smhdr")!=0 ){ |
| 1740 | 2118 | @ <h2>Differences From Artifact |
| 1741 | 2119 | @ %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a> To |
| 1742 | - @ %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>.</h2> | |
| 2120 | + if( bLocalMode ){ | |
| 2121 | + @ %z(href("%R/local"))[Local Changes]</a> of | |
| 2122 | + @ %z(href("%R/file/%T?ci=ckout",zLocalName))%h(zLocalName)</a>. | |
| 2123 | + }else{ | |
| 2124 | + @ %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>.</h2> | |
| 2125 | + } | |
| 1743 | 2126 | }else{ |
| 1744 | 2127 | @ <h2>Differences From |
| 1745 | 2128 | @ Artifact %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a>:</h2> |
| 1746 | 2129 | object_description(v1, objdescFlags,0, 0); |
| 1747 | - @ <h2>To Artifact %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>:</h2> | |
| 1748 | - object_description(v2, objdescFlags,0, 0); | |
| 2130 | + if( bLocalMode ){ | |
| 2131 | + @ <h2>To %z(href("%R/local"))[Local Changes]</a> | |
| 2132 | + @ of %z(href("%R/file/%T?ci=ckout",zLocalName))%h(zLocalName)</a>.</h2> | |
| 2133 | + }else{ | |
| 2134 | + @ <h2>To Artifact %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>:</h2> | |
| 2135 | + object_description(v2, objdescFlags,0, 0); | |
| 2136 | + } | |
| 1749 | 2137 | } |
| 1750 | 2138 | if( pRe ){ |
| 1751 | 2139 | @ <b>Only differences that match regular expression "%h(zRe)" |
| 1752 | 2140 | @ are shown.</b> |
| 1753 | 2141 | } |
| 1754 | 2142 | @ <hr /> |
| 1755 | - append_diff(zV1, zV2, diffFlags, pRe); | |
| 2143 | + append_diff(zV1, zV2, zLocalName, diffFlags, pRe); | |
| 1756 | 2144 | append_diff_javascript(diffType); |
| 1757 | 2145 | style_footer(); |
| 1758 | 2146 | } |
| 1759 | 2147 | |
| 1760 | 2148 | /* |
| @@ -1908,13 +2296,16 @@ | ||
| 1908 | 2296 | } |
| 1909 | 2297 | |
| 1910 | 2298 | /* |
| 1911 | 2299 | ** WEBPAGE: hexdump |
| 1912 | 2300 | ** URL: /hexdump?name=ARTIFACTID |
| 2301 | +** URL: /hexdump?local=FILENAME | |
| 1913 | 2302 | ** |
| 1914 | 2303 | ** Show the complete content of a file identified by ARTIFACTID |
| 1915 | 2304 | ** as preformatted text. |
| 2305 | +** | |
| 2306 | +** The second version does the same for FILENAME from the local checkout. | |
| 1916 | 2307 | ** |
| 1917 | 2308 | ** Other parameters: |
| 1918 | 2309 | ** |
| 1919 | 2310 | ** verbose Show more detail when describing the object |
| 1920 | 2311 | */ |
| @@ -1922,42 +2313,61 @@ | ||
| 1922 | 2313 | int rid; |
| 1923 | 2314 | Blob content; |
| 1924 | 2315 | Blob downloadName; |
| 1925 | 2316 | char *zUuid; |
| 1926 | 2317 | u32 objdescFlags = 0; |
| 2318 | + const char *zLocalName = P("local"); | |
| 2319 | + int bLocalMode = zLocalName!=NULL; | |
| 1927 | 2320 | |
| 1928 | 2321 | rid = name_to_rid_www("name"); |
| 1929 | 2322 | login_check_credentials(); |
| 1930 | 2323 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 1931 | - if( rid==0 ) fossil_redirect_home(); | |
| 1932 | - if( g.perm.Admin ){ | |
| 1933 | - const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 1934 | - if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ | |
| 1935 | - style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#delshun", | |
| 1936 | - g.zTop, zUuid); | |
| 1937 | - }else{ | |
| 1938 | - style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); | |
| 2324 | + if( !bLocalMode ){ | |
| 2325 | + if( rid==0 ) fossil_redirect_home(); | |
| 2326 | + if( g.perm.Admin ){ | |
| 2327 | + const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",rid); | |
| 2328 | + if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ | |
| 2329 | + style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#delshun", | |
| 2330 | + g.zTop, zUuid); | |
| 2331 | + }else{ | |
| 2332 | + style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); | |
| 2333 | + } | |
| 1939 | 2334 | } |
| 1940 | 2335 | } |
| 1941 | 2336 | style_header("Hex Artifact Content"); |
| 1942 | - zUuid = db_text("?","SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 1943 | - @ <h2>Artifact | |
| 1944 | - style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); | |
| 1945 | - if( g.perm.Setup ){ | |
| 1946 | - @ (%d(rid)):</h2> | |
| 2337 | + /* TODO | |
| 2338 | + ** Could the call to style_header() be moved so these two exclusion | |
| 2339 | + ** blocks could be merged? I don't think any of them make sense for | |
| 2340 | + ** a local file. | |
| 2341 | + */ | |
| 2342 | + if( !bLocalMode ){ | |
| 2343 | + zUuid = db_text("?","SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 2344 | + @ <h2>Artifact | |
| 2345 | + style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); | |
| 2346 | + if( g.perm.Setup ){ | |
| 2347 | + @ (%d(rid)):</h2> | |
| 2348 | + }else{ | |
| 2349 | + @ :</h2> | |
| 2350 | + } | |
| 2351 | + blob_zero(&downloadName); | |
| 2352 | + if( P("verbose")!=0 ) objdescFlags |= OBJDESC_DETAIL; | |
| 2353 | + object_description(rid, objdescFlags, 0, &downloadName); | |
| 2354 | + style_submenu_element("Download", "%R/raw/%s?at=%T", | |
| 2355 | + zUuid, file_tail(blob_str(&downloadName))); | |
| 1947 | 2356 | }else{ |
| 1948 | - @ :</h2> | |
| 2357 | + @ <h2>File %z(href("%R/finfo?name=%T&m=tip",zLocalName))%h(zLocalName)</a> | |
| 2358 | + @ from %z(href("%R/local"))[Local Changes]</a></h2> | |
| 1949 | 2359 | } |
| 1950 | - blob_zero(&downloadName); | |
| 1951 | - if( P("verbose")!=0 ) objdescFlags |= OBJDESC_DETAIL; | |
| 1952 | - object_description(rid, objdescFlags, 0, &downloadName); | |
| 1953 | - style_submenu_element("Download", "%R/raw/%s?at=%T", | |
| 1954 | - zUuid, file_tail(blob_str(&downloadName))); | |
| 1955 | 2360 | @ <hr /> |
| 1956 | - content_get(rid, &content); | |
| 2361 | + if( bLocalMode ){ | |
| 2362 | + content_from_file(zLocalName, &content); | |
| 2363 | + }else{ | |
| 2364 | + content_get(rid, &content); | |
| 2365 | + } | |
| 1957 | 2366 | @ <blockquote><pre> |
| 1958 | 2367 | hexdump(&content); |
| 2368 | + /* TODO: Should content (and downloadName?) be reset/freed? */ | |
| 1959 | 2369 | @ </pre></blockquote> |
| 1960 | 2370 | style_footer(); |
| 1961 | 2371 | } |
| 1962 | 2372 | |
| 1963 | 2373 | /* |
| @@ -2131,10 +2541,13 @@ | ||
| 2131 | 2541 | ** if name= cannot be understood as a hash, a default "tip" value is |
| 2132 | 2542 | ** used for ci=. |
| 2133 | 2543 | ** |
| 2134 | 2544 | ** For /file, name= can only be interpreted as a filename. As before, |
| 2135 | 2545 | ** a default value of "tip" is used for ci= if ci= is omitted. |
| 2546 | +** | |
| 2547 | +** If ci=ckout then display the content of the file NAME in the local | |
| 2548 | +** checkout directory. | |
| 2136 | 2549 | */ |
| 2137 | 2550 | void artifact_page(void){ |
| 2138 | 2551 | int rid = 0; |
| 2139 | 2552 | Blob content; |
| 2140 | 2553 | const char *zMime; |
| @@ -2153,10 +2566,11 @@ | ||
| 2153 | 2566 | HQuery url; |
| 2154 | 2567 | char *zCIUuid = 0; |
| 2155 | 2568 | int isSymbolicCI = 0; /* ci= exists and is a symbolic name, not a hash */ |
| 2156 | 2569 | int isBranchCI = 0; /* ci= refers to a branch name */ |
| 2157 | 2570 | char *zHeader = 0; |
| 2571 | + int bLocalMode = 0; /* TRUE if trying to show file in local checkout */ | |
| 2158 | 2572 | |
| 2159 | 2573 | login_check_credentials(); |
| 2160 | 2574 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2161 | 2575 | |
| 2162 | 2576 | /* Capture and normalize the name= and ci= query parameters */ |
| @@ -2200,11 +2614,14 @@ | ||
| 2200 | 2614 | ** name= as a hash for /artifact and /whatis. But for not for /file. |
| 2201 | 2615 | ** For /file, a name= without a ci= while prefer to use the default |
| 2202 | 2616 | ** "tip" value for ci=. */ |
| 2203 | 2617 | rid = name_to_rid(zName); |
| 2204 | 2618 | } |
| 2205 | - if( rid==0 ){ | |
| 2619 | + if( fossil_strcmp(zCI,"ckout")==0 ){ | |
| 2620 | + bLocalMode = 1; | |
| 2621 | + rid = -1; /* Dummy value to make it look found */ | |
| 2622 | + }else if( rid==0 ){ | |
| 2206 | 2623 | rid = artifact_from_ci_and_filename(0); |
| 2207 | 2624 | } |
| 2208 | 2625 | |
| 2209 | 2626 | if( rid==0 ){ /* Artifact not found */ |
| 2210 | 2627 | if( isFile ){ |
| @@ -2237,11 +2654,25 @@ | ||
| 2237 | 2654 | } |
| 2238 | 2655 | zUuid = db_text("?", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2239 | 2656 | |
| 2240 | 2657 | asText = P("txt")!=0; |
| 2241 | 2658 | if( isFile ){ |
| 2242 | - if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ | |
| 2659 | + if( bLocalMode ){ | |
| 2660 | + /*TODO | |
| 2661 | + ** Is this the best way of handling annotations to the description? | |
| 2662 | + ** If "annot=message" is part of the URL, the message is appended | |
| 2663 | + ** to the description of the file. Only used for "local" files to | |
| 2664 | + ** distinguish such files from part of the repository. | |
| 2665 | + */ | |
| 2666 | + const char *annot = P("annot"); | |
| 2667 | + @ <h2>File %z(href("%R/finfo?name=%T&m=tip",zName))%h(zName)</a> | |
| 2668 | + @ from %z(href("%R/local"))[Local Changes]</a> | |
| 2669 | + if( annot ){ | |
| 2670 | + @ (%h(annot)) | |
| 2671 | + } | |
| 2672 | + @ </h2> | |
| 2673 | + }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ | |
| 2243 | 2674 | zCI = "tip"; |
| 2244 | 2675 | isSymbolicCI = 1; /* Mark default-to-"tip" as symbolic */ |
| 2245 | 2676 | @ <h2>File %z(href("%R/finfo?name=%T&m=tip",zName))%h(zName)</a> |
| 2246 | 2677 | @ from the %z(href("%R/info/tip"))latest check-in</a></h2> |
| 2247 | 2678 | }else{ |
| @@ -2258,15 +2689,17 @@ | ||
| 2258 | 2689 | }else{ |
| 2259 | 2690 | @ as of check-in [%z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a>]</h2> |
| 2260 | 2691 | } |
| 2261 | 2692 | blob_reset(&path); |
| 2262 | 2693 | } |
| 2263 | - style_submenu_element("Artifact", "%R/artifact/%S", zUuid); | |
| 2264 | - style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", | |
| 2265 | - zName, zCI); | |
| 2266 | - style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", | |
| 2267 | - zName, zCI); | |
| 2694 | + if( !bLocalMode ){ | |
| 2695 | + style_submenu_element("Artifact", "%R/artifact/%S", zUuid); | |
| 2696 | + style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", | |
| 2697 | + zName, zCI); | |
| 2698 | + style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", | |
| 2699 | + zName, zCI); | |
| 2700 | + } | |
| 2268 | 2701 | blob_init(&downloadName, zName, -1); |
| 2269 | 2702 | objType = OBJTYPE_CONTENT; |
| 2270 | 2703 | }else{ |
| 2271 | 2704 | @ <h2>Artifact |
| 2272 | 2705 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| @@ -2278,29 +2711,33 @@ | ||
| 2278 | 2711 | blob_zero(&downloadName); |
| 2279 | 2712 | if( asText ) objdescFlags &= ~OBJDESC_BASE; |
| 2280 | 2713 | objType = object_description(rid, objdescFlags, |
| 2281 | 2714 | (isFile?zName:0), &downloadName); |
| 2282 | 2715 | } |
| 2283 | - if( !descOnly && P("download")!=0 ){ | |
| 2284 | - cgi_redirectf("%R/raw/%s?at=%T", | |
| 2285 | - db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), | |
| 2286 | - file_tail(blob_str(&downloadName))); | |
| 2287 | - /*NOTREACHED*/ | |
| 2288 | - } | |
| 2289 | - if( g.perm.Admin ){ | |
| 2290 | - const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 2291 | - if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ | |
| 2292 | - style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#accshun", | |
| 2293 | - g.zTop, zUuid); | |
| 2294 | - }else{ | |
| 2295 | - style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); | |
| 2716 | + if( !bLocalMode ){ | |
| 2717 | + if( !descOnly && P("download")!=0 ){ | |
| 2718 | + cgi_redirectf("%R/raw/%s?at=%T", | |
| 2719 | + db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), | |
| 2720 | + file_tail(blob_str(&downloadName))); | |
| 2721 | + /*NOTREACHED*/ | |
| 2722 | + } | |
| 2723 | + if( g.perm.Admin ){ | |
| 2724 | + const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",rid); | |
| 2725 | + if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ | |
| 2726 | + style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#accshun", | |
| 2727 | + g.zTop, zUuid); | |
| 2728 | + }else{ | |
| 2729 | + style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); | |
| 2730 | + } | |
| 2296 | 2731 | } |
| 2297 | 2732 | } |
| 2298 | 2733 | |
| 2299 | 2734 | if( isFile ){ |
| 2300 | 2735 | if( isSymbolicCI ){ |
| 2301 | 2736 | zHeader = mprintf("%s at %s", file_tail(zName), zCI); |
| 2737 | + }else if( bLocalMode ){ | |
| 2738 | + zHeader = mprintf("%s (local changes)", file_tail(zName)); | |
| 2302 | 2739 | }else if( zCI ){ |
| 2303 | 2740 | zHeader = mprintf("%s at [%S]", file_tail(zName), zCIUuid); |
| 2304 | 2741 | }else{ |
| 2305 | 2742 | zHeader = mprintf("%s", file_tail(zName)); |
| 2306 | 2743 | } |
| @@ -2326,13 +2763,17 @@ | ||
| 2326 | 2763 | const char *zIp = db_column_text(&q,2); |
| 2327 | 2764 | @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p> |
| 2328 | 2765 | } |
| 2329 | 2766 | db_finalize(&q); |
| 2330 | 2767 | } |
| 2331 | - style_submenu_element("Download", "%R/raw/%s?at=%T", zUuid, file_tail(zName)); | |
| 2332 | - if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ | |
| 2333 | - style_submenu_element("Check-ins Using", "%R/timeline?n=200&uf=%s", zUuid); | |
| 2768 | + if( !bLocalMode ){ | |
| 2769 | + style_submenu_element("Download", "%R/raw/%s?at=%T", | |
| 2770 | + zUuid, file_tail(zName)); | |
| 2771 | + if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ | |
| 2772 | + style_submenu_element("Check-ins Using", "%R/timeline?n=200&uf=%s", | |
| 2773 | + zUuid); | |
| 2774 | + } | |
| 2334 | 2775 | } |
| 2335 | 2776 | zMime = mimetype_from_name(blob_str(&downloadName)); |
| 2336 | 2777 | if( zMime ){ |
| 2337 | 2778 | if( fossil_strcmp(zMime, "text/html")==0 ){ |
| 2338 | 2779 | if( asText ){ |
| @@ -2348,24 +2789,38 @@ | ||
| 2348 | 2789 | }else{ |
| 2349 | 2790 | renderAsWiki = 1; |
| 2350 | 2791 | style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); |
| 2351 | 2792 | } |
| 2352 | 2793 | } |
| 2353 | - if( fileedit_is_editable(zName) ){ | |
| 2354 | - style_submenu_element("Edit", | |
| 2355 | - "%R/fileedit?filename=%T&checkin=%!S", | |
| 2356 | - zName, zCI); | |
| 2794 | + if( !bLocalMode ){ /* This way madness lies... */ | |
| 2795 | + if( fileedit_is_editable(zName) ){ | |
| 2796 | + style_submenu_element("Edit", | |
| 2797 | + "%R/fileedit?filename=%T&checkin=%!S", | |
| 2798 | + zName, zCI); | |
| 2799 | + } | |
| 2357 | 2800 | } |
| 2358 | 2801 | } |
| 2359 | 2802 | if( (objType & (OBJTYPE_WIKI|OBJTYPE_TICKET))!=0 ){ |
| 2360 | 2803 | style_submenu_element("Parsed", "%R/info/%s", zUuid); |
| 2361 | 2804 | } |
| 2362 | 2805 | if( descOnly ){ |
| 2363 | 2806 | style_submenu_element("Content", "%R/artifact/%s", zUuid); |
| 2364 | 2807 | }else{ |
| 2365 | 2808 | @ <hr /> |
| 2366 | - content_get(rid, &content); | |
| 2809 | + if( bLocalMode ){ | |
| 2810 | + /*TODO | |
| 2811 | + ** Should we handle non-existent local files differently? Currently, | |
| 2812 | + ** they are shown the same as if the file was present but empty. This | |
| 2813 | + ** should never happen through "normal" operation, but someone might | |
| 2814 | + ** craft a link to one. Perhaps have content_from_file() perform an | |
| 2815 | + ** existence-check (rather than relying on blob_read_from_file() which | |
| 2816 | + ** it calls returning an empty blob)? | |
| 2817 | + */ | |
| 2818 | + content_from_file(zName, &content); | |
| 2819 | + }else{ | |
| 2820 | + content_get(rid, &content); | |
| 2821 | + } | |
| 2367 | 2822 | if( renderAsWiki ){ |
| 2368 | 2823 | wiki_render_by_mimetype(&content, zMime); |
| 2369 | 2824 | }else if( renderAsHtml ){ |
| 2370 | 2825 | @ <iframe src="%R/raw/%s(zUuid)" |
| 2371 | 2826 | @ width="100%%" frameborder="0" marginwidth="0" marginheight="0" |
| @@ -2377,11 +2832,15 @@ | ||
| 2377 | 2832 | @ this.height=this.contentDocument.documentElement.scrollHeight + 75; |
| 2378 | 2833 | @ } |
| 2379 | 2834 | @ ); |
| 2380 | 2835 | @ </script> |
| 2381 | 2836 | }else{ |
| 2382 | - style_submenu_element("Hex", "%s/hexdump?name=%s", g.zTop, zUuid); | |
| 2837 | + if( bLocalMode ){ | |
| 2838 | + style_submenu_element("Hex", "%R/hexdump?local=%s", zName); | |
| 2839 | + }else{ | |
| 2840 | + style_submenu_element("Hex", "%s/hexdump?name=%s", g.zTop, zUuid); | |
| 2841 | + } | |
| 2383 | 2842 | if( zLn==0 || atoi(zLn)==0 ){ |
| 2384 | 2843 | style_submenu_checkbox("ln", "Line Numbers", 0, 0); |
| 2385 | 2844 | } |
| 2386 | 2845 | blob_to_utf8_no_bom(&content, 0); |
| 2387 | 2846 | zMime = mimetype_from_content(&content); |
| 2388 | 2847 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1,5 +1,19 @@ | |
| 1 | /* |
| 2 | ** Copyright (c) 2007 D. Richard Hipp |
| 3 | ** |
| 4 | ** This program is free software; you can redistribute it and/or |
| 5 | ** modify it under the terms of the Simplified BSD License (also |
| @@ -322,17 +336,36 @@ | |
| 322 | |TIMELINE_CHPICK, |
| 323 | 0, 0, 0, rid, rid2, 0); |
| 324 | db_finalize(&q); |
| 325 | } |
| 326 | |
| 327 | |
| 328 | /* |
| 329 | ** Append the difference between artifacts to the output |
| 330 | */ |
| 331 | static void append_diff( |
| 332 | const char *zFrom, /* Diff from this artifact */ |
| 333 | const char *zTo, /* ... to this artifact */ |
| 334 | u64 diffFlags, /* Diff formatting flags */ |
| 335 | ReCompiled *pRe /* Only show change matching this regex */ |
| 336 | ){ |
| 337 | int fromid; |
| 338 | int toid; |
| @@ -344,10 +377,12 @@ | |
| 344 | blob_zero(&from); |
| 345 | } |
| 346 | if( zTo ){ |
| 347 | toid = uuid_to_rid(zTo, 0); |
| 348 | content_get(toid, &to); |
| 349 | }else{ |
| 350 | blob_zero(&to); |
| 351 | } |
| 352 | blob_zero(&out); |
| 353 | if( diffFlags & DIFF_SIDEBYSIDE ){ |
| @@ -396,11 +431,11 @@ | |
| 396 | } |
| 397 | }else{ |
| 398 | @ Changes to %h(zName). |
| 399 | } |
| 400 | if( diffFlags ){ |
| 401 | append_diff(zOld, zNew, diffFlags, pRe); |
| 402 | } |
| 403 | }else{ |
| 404 | if( zOld && zNew ){ |
| 405 | if( fossil_strcmp(zOld, zNew)!=0 ){ |
| 406 | @ Modified %z(href("%R/finfo?name=%T&m=%!S",zName,zNew))%h(zName)</a> |
| @@ -427,18 +462,172 @@ | |
| 427 | }else{ |
| 428 | @ Added %z(href("%R/finfo?name=%T&m=%!S",zName,zNew))%h(zName)</a> |
| 429 | @ version %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>. |
| 430 | } |
| 431 | if( diffFlags ){ |
| 432 | append_diff(zOld, zNew, diffFlags, pRe); |
| 433 | }else if( zOld && zNew && fossil_strcmp(zOld,zNew)!=0 ){ |
| 434 | @ |
| 435 | @ %z(href("%R/fdiff?v1=%!S&v2=%!S",zOld,zNew))[diff]</a> |
| 436 | } |
| 437 | } |
| 438 | @ </p> |
| 439 | } |
| 440 | |
| 441 | /* |
| 442 | ** Generate javascript to enhance HTML diffs. |
| 443 | */ |
| 444 | void append_diff_javascript(int sideBySide){ |
| @@ -596,19 +785,24 @@ | |
| 596 | } |
| 597 | |
| 598 | /* |
| 599 | ** WEBPAGE: vinfo |
| 600 | ** WEBPAGE: ci |
| 601 | ** URL: /ci/ARTIFACTID |
| 602 | ** OR: /ci?name=ARTIFACTID |
| 603 | ** |
| 604 | ** Display information about a particular check-in. The exact |
| 605 | ** same information is shown on the /info page if the name query |
| 606 | ** parameter to /info describes a check-in. |
| 607 | ** |
| 608 | ** The ARTIFACTID can be a unique prefix for the HASH of the check-in, |
| 609 | ** or a tag or branch name that identifies the check-in. |
| 610 | */ |
| 611 | void ci_page(void){ |
| 612 | Stmt q1, q2, q3; |
| 613 | int rid; |
| 614 | int isLeaf; |
| @@ -621,14 +815,37 @@ | |
| 621 | ReCompiled *pRe = 0; /* regex */ |
| 622 | const char *zW; /* URL param for ignoring whitespace */ |
| 623 | const char *zPage = "vinfo"; /* Page that shows diffs */ |
| 624 | const char *zPageHide = "ci"; /* Page that hides diffs */ |
| 625 | const char *zBrName; /* Branch name */ |
| 626 | |
| 627 | login_check_credentials(); |
| 628 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 629 | zName = P("name"); |
| 630 | rid = name_to_rid_www("name"); |
| 631 | if( rid==0 ){ |
| 632 | style_header("Check-in Information Error"); |
| 633 | @ No such object: %h(g.argv[2]) |
| 634 | style_footer(); |
| @@ -667,11 +884,15 @@ | |
| 667 | int okWiki = 0; |
| 668 | Blob wiki_read_links = BLOB_INITIALIZER; |
| 669 | Blob wiki_add_links = BLOB_INITIALIZER; |
| 670 | |
| 671 | Th_Store("current_checkin", zName); |
| 672 | style_header("Check-in [%S]", zUuid); |
| 673 | login_anonymous_available(); |
| 674 | zEUser = db_text(0, |
| 675 | "SELECT value FROM tagxref" |
| 676 | " WHERE tagid=%d AND rid=%d AND tagtype>0", |
| 677 | TAG_USER, rid); |
| @@ -873,14 +1094,25 @@ | |
| 873 | wiki_render_associated("checkin", zUuid, 0); |
| 874 | } |
| 875 | render_backlink_graph(zUuid, "<div class=\"section\">References</div>\n"); |
| 876 | @ <div class="section">Context</div> |
| 877 | render_checkin_context(rid, 0, 0); |
| 878 | @ <div class="section">Changes</div> |
| 879 | @ <div class="sectionmenu"> |
| 880 | diffFlags = construct_diff_flags(diffType); |
| 881 | zW = (diffFlags&DIFF_IGNORE_ALLWS)?"&w":""; |
| 882 | if( diffType!=0 ){ |
| 883 | @ %z(chref("button","%R/%s/%T?diff=0",zPageHide,zName))\ |
| 884 | @ Hide Diffs</a> |
| 885 | } |
| 886 | if( diffType!=1 ){ |
| @@ -898,48 +1130,163 @@ | |
| 898 | }else{ |
| 899 | @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType)) |
| 900 | @ Ignore Whitespace</a> |
| 901 | } |
| 902 | } |
| 903 | if( zParent ){ |
| 904 | @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) |
| 905 | @ Patch</a> |
| 906 | } |
| 907 | if( g.perm.Admin ){ |
| 908 | @ %z(chref("button","%R/mlink?ci=%!S",zUuid))MLink Table</a> |
| 909 | } |
| 910 | @</div> |
| 911 | if( pRe ){ |
| 912 | @ <p><b>Only differences that match regular expression "%h(zRe)" |
| 913 | @ are shown.</b></p> |
| 914 | } |
| 915 | db_prepare(&q3, |
| 916 | "SELECT name," |
| 917 | " mperm," |
| 918 | " (SELECT uuid FROM blob WHERE rid=mlink.pid)," |
| 919 | " (SELECT uuid FROM blob WHERE rid=mlink.fid)," |
| 920 | " (SELECT name FROM filename WHERE filename.fnid=mlink.pfnid)" |
| 921 | " FROM mlink JOIN filename ON filename.fnid=mlink.fnid" |
| 922 | " WHERE mlink.mid=%d AND NOT mlink.isaux" |
| 923 | " AND (mlink.fid>0" |
| 924 | " OR mlink.fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=%d))" |
| 925 | " ORDER BY name /*sort*/", |
| 926 | rid, rid |
| 927 | ); |
| 928 | while( db_step(&q3)==SQLITE_ROW ){ |
| 929 | const char *zName = db_column_text(&q3,0); |
| 930 | int mperm = db_column_int(&q3, 1); |
| 931 | const char *zOld = db_column_text(&q3,2); |
| 932 | const char *zNew = db_column_text(&q3,3); |
| 933 | const char *zOldName = db_column_text(&q3, 4); |
| 934 | append_file_change_line(zName, zOld, zNew, zOldName, diffFlags,pRe,mperm); |
| 935 | } |
| 936 | db_finalize(&q3); |
| 937 | append_diff_javascript(diffType==2); |
| 938 | cookie_render(); |
| 939 | style_footer(); |
| 940 | } |
| 941 | |
| 942 | /* |
| 943 | ** WEBPAGE: winfo |
| 944 | ** URL: /winfo?name=UUID |
| 945 | ** |
| @@ -1216,11 +1563,11 @@ | |
| 1216 | style_submenu_element("Side-by-Side Diff", |
| 1217 | "%R/vdiff?%s&diff=2%s%T%s", |
| 1218 | zQuery, |
| 1219 | zGlob ? "&glob=" : "", zGlob ? zGlob : "", zW); |
| 1220 | } |
| 1221 | if( diffType!=1 ) { |
| 1222 | style_submenu_element("Unified Diff", |
| 1223 | "%R/vdiff?%s&diff=1%s%T%s", |
| 1224 | zQuery, |
| 1225 | zGlob ? "&glob=" : "", zGlob ? zGlob : "", zW); |
| 1226 | } |
| @@ -1400,11 +1747,11 @@ | |
| 1400 | } |
| 1401 | cnt++; |
| 1402 | continue; |
| 1403 | } |
| 1404 | if( !sameFilename ){ |
| 1405 | if( prevName && showDetail ) { |
| 1406 | @ </ul> |
| 1407 | } |
| 1408 | if( mPerm==PERM_LNK ){ |
| 1409 | @ <li>Symbolic link |
| 1410 | objType |= OBJTYPE_SYMLINK; |
| @@ -1520,11 +1867,11 @@ | |
| 1520 | objType |= OBJTYPE_TICKET; |
| 1521 | }else if( zType[0]=='c' ){ |
| 1522 | @ Manifest of check-in |
| 1523 | objType |= OBJTYPE_CHECKIN; |
| 1524 | }else if( zType[0]=='e' ){ |
| 1525 | if( eventTagId != 0) { |
| 1526 | @ Instance of technote |
| 1527 | objType |= OBJTYPE_EVENT; |
| 1528 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1529 | }else{ |
| 1530 | @ Attachment to technote |
| @@ -1567,11 +1914,11 @@ | |
| 1567 | }else{ |
| 1568 | @ Attachment "%h(zFilename)" to |
| 1569 | } |
| 1570 | objType |= OBJTYPE_ATTACHMENT; |
| 1571 | if( fossil_is_uuid(zTarget) ){ |
| 1572 | if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", |
| 1573 | zTarget) |
| 1574 | ){ |
| 1575 | if( g.perm.Hyperlink && g.anon.RdTkt ){ |
| 1576 | @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] |
| 1577 | }else{ |
| @@ -1625,11 +1972,13 @@ | |
| 1625 | } |
| 1626 | |
| 1627 | |
| 1628 | /* |
| 1629 | ** WEBPAGE: fdiff |
| 1630 | ** URL: fdiff?v1=UUID&v2=UUID |
| 1631 | ** |
| 1632 | ** Two arguments, v1 and v2, identify the artifacts to be diffed. |
| 1633 | ** Show diff side by side unless sbs is 0. Generate plain text if |
| 1634 | ** "patch" is present, otherwise generate "pretty" HTML. |
| 1635 | ** |
| @@ -1637,10 +1986,14 @@ | |
| 1637 | ** |
| 1638 | ** If the "from" and "to" query parameters are both present, then they are |
| 1639 | ** the names of two files within the check-in "ci" that are diffed. If the |
| 1640 | ** "ci" parameter is omitted, then the most recent check-in ("tip") is |
| 1641 | ** used. |
| 1642 | ** |
| 1643 | ** Additional parameters: |
| 1644 | ** |
| 1645 | ** dc=N Show N lines of context around each diff |
| 1646 | ** patch Use the patch diff format |
| @@ -1658,17 +2011,23 @@ | |
| 1658 | const char *zRe; |
| 1659 | ReCompiled *pRe = 0; |
| 1660 | u64 diffFlags; |
| 1661 | u32 objdescFlags = 0; |
| 1662 | int verbose = PB("verbose"); |
| 1663 | |
| 1664 | login_check_credentials(); |
| 1665 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 1666 | cookie_link_parameter("diff","diff","2"); |
| 1667 | diffType = atoi(PD("diff","2")); |
| 1668 | cookie_render(); |
| 1669 | if( P("from") && P("to") ){ |
| 1670 | v1 = artifact_from_ci_and_filename("from"); |
| 1671 | v2 = artifact_from_ci_and_filename("to"); |
| 1672 | }else{ |
| 1673 | Stmt q; |
| 1674 | v1 = name_to_rid_www("v1"); |
| @@ -1707,14 +2066,19 @@ | |
| 1707 | if( zRe ) re_compile(&pRe, zRe, 0); |
| 1708 | if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
| 1709 | if( isPatch ){ |
| 1710 | Blob c1, c2, *pOut; |
| 1711 | pOut = cgi_output_blob(); |
| 1712 | cgi_set_content_type("text/plain"); |
| 1713 | diffFlags = 4; |
| 1714 | content_get(v1, &c1); |
| 1715 | content_get(v2, &c2); |
| 1716 | text_diff(&c1, &c2, pOut, pRe, diffFlags); |
| 1717 | blob_reset(&c1); |
| 1718 | blob_reset(&c2); |
| 1719 | return; |
| 1720 | } |
| @@ -1723,38 +2087,62 @@ | |
| 1723 | zV2 = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", v2); |
| 1724 | diffFlags = construct_diff_flags(diffType) | DIFF_HTML; |
| 1725 | |
| 1726 | style_header("Diff"); |
| 1727 | style_submenu_checkbox("w", "Ignore Whitespace", 0, 0); |
| 1728 | if( diffType==2 ){ |
| 1729 | style_submenu_element("Unified Diff", "%R/fdiff?v1=%T&v2=%T&diff=1", |
| 1730 | P("v1"), P("v2")); |
| 1731 | }else{ |
| 1732 | style_submenu_element("Side-by-side Diff", "%R/fdiff?v1=%T&v2=%T&diff=2", |
| 1733 | P("v1"), P("v2")); |
| 1734 | } |
| 1735 | style_submenu_checkbox("verbose", "Verbose", 0, 0); |
| 1736 | style_submenu_element("Patch", "%R/fdiff?v1=%T&v2=%T&patch", |
| 1737 | P("v1"), P("v2")); |
| 1738 | |
| 1739 | if( P("smhdr")!=0 ){ |
| 1740 | @ <h2>Differences From Artifact |
| 1741 | @ %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a> To |
| 1742 | @ %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>.</h2> |
| 1743 | }else{ |
| 1744 | @ <h2>Differences From |
| 1745 | @ Artifact %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a>:</h2> |
| 1746 | object_description(v1, objdescFlags,0, 0); |
| 1747 | @ <h2>To Artifact %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>:</h2> |
| 1748 | object_description(v2, objdescFlags,0, 0); |
| 1749 | } |
| 1750 | if( pRe ){ |
| 1751 | @ <b>Only differences that match regular expression "%h(zRe)" |
| 1752 | @ are shown.</b> |
| 1753 | } |
| 1754 | @ <hr /> |
| 1755 | append_diff(zV1, zV2, diffFlags, pRe); |
| 1756 | append_diff_javascript(diffType); |
| 1757 | style_footer(); |
| 1758 | } |
| 1759 | |
| 1760 | /* |
| @@ -1908,13 +2296,16 @@ | |
| 1908 | } |
| 1909 | |
| 1910 | /* |
| 1911 | ** WEBPAGE: hexdump |
| 1912 | ** URL: /hexdump?name=ARTIFACTID |
| 1913 | ** |
| 1914 | ** Show the complete content of a file identified by ARTIFACTID |
| 1915 | ** as preformatted text. |
| 1916 | ** |
| 1917 | ** Other parameters: |
| 1918 | ** |
| 1919 | ** verbose Show more detail when describing the object |
| 1920 | */ |
| @@ -1922,42 +2313,61 @@ | |
| 1922 | int rid; |
| 1923 | Blob content; |
| 1924 | Blob downloadName; |
| 1925 | char *zUuid; |
| 1926 | u32 objdescFlags = 0; |
| 1927 | |
| 1928 | rid = name_to_rid_www("name"); |
| 1929 | login_check_credentials(); |
| 1930 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 1931 | if( rid==0 ) fossil_redirect_home(); |
| 1932 | if( g.perm.Admin ){ |
| 1933 | const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 1934 | if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
| 1935 | style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#delshun", |
| 1936 | g.zTop, zUuid); |
| 1937 | }else{ |
| 1938 | style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); |
| 1939 | } |
| 1940 | } |
| 1941 | style_header("Hex Artifact Content"); |
| 1942 | zUuid = db_text("?","SELECT uuid FROM blob WHERE rid=%d", rid); |
| 1943 | @ <h2>Artifact |
| 1944 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| 1945 | if( g.perm.Setup ){ |
| 1946 | @ (%d(rid)):</h2> |
| 1947 | }else{ |
| 1948 | @ :</h2> |
| 1949 | } |
| 1950 | blob_zero(&downloadName); |
| 1951 | if( P("verbose")!=0 ) objdescFlags |= OBJDESC_DETAIL; |
| 1952 | object_description(rid, objdescFlags, 0, &downloadName); |
| 1953 | style_submenu_element("Download", "%R/raw/%s?at=%T", |
| 1954 | zUuid, file_tail(blob_str(&downloadName))); |
| 1955 | @ <hr /> |
| 1956 | content_get(rid, &content); |
| 1957 | @ <blockquote><pre> |
| 1958 | hexdump(&content); |
| 1959 | @ </pre></blockquote> |
| 1960 | style_footer(); |
| 1961 | } |
| 1962 | |
| 1963 | /* |
| @@ -2131,10 +2541,13 @@ | |
| 2131 | ** if name= cannot be understood as a hash, a default "tip" value is |
| 2132 | ** used for ci=. |
| 2133 | ** |
| 2134 | ** For /file, name= can only be interpreted as a filename. As before, |
| 2135 | ** a default value of "tip" is used for ci= if ci= is omitted. |
| 2136 | */ |
| 2137 | void artifact_page(void){ |
| 2138 | int rid = 0; |
| 2139 | Blob content; |
| 2140 | const char *zMime; |
| @@ -2153,10 +2566,11 @@ | |
| 2153 | HQuery url; |
| 2154 | char *zCIUuid = 0; |
| 2155 | int isSymbolicCI = 0; /* ci= exists and is a symbolic name, not a hash */ |
| 2156 | int isBranchCI = 0; /* ci= refers to a branch name */ |
| 2157 | char *zHeader = 0; |
| 2158 | |
| 2159 | login_check_credentials(); |
| 2160 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2161 | |
| 2162 | /* Capture and normalize the name= and ci= query parameters */ |
| @@ -2200,11 +2614,14 @@ | |
| 2200 | ** name= as a hash for /artifact and /whatis. But for not for /file. |
| 2201 | ** For /file, a name= without a ci= while prefer to use the default |
| 2202 | ** "tip" value for ci=. */ |
| 2203 | rid = name_to_rid(zName); |
| 2204 | } |
| 2205 | if( rid==0 ){ |
| 2206 | rid = artifact_from_ci_and_filename(0); |
| 2207 | } |
| 2208 | |
| 2209 | if( rid==0 ){ /* Artifact not found */ |
| 2210 | if( isFile ){ |
| @@ -2237,11 +2654,25 @@ | |
| 2237 | } |
| 2238 | zUuid = db_text("?", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2239 | |
| 2240 | asText = P("txt")!=0; |
| 2241 | if( isFile ){ |
| 2242 | if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ |
| 2243 | zCI = "tip"; |
| 2244 | isSymbolicCI = 1; /* Mark default-to-"tip" as symbolic */ |
| 2245 | @ <h2>File %z(href("%R/finfo?name=%T&m=tip",zName))%h(zName)</a> |
| 2246 | @ from the %z(href("%R/info/tip"))latest check-in</a></h2> |
| 2247 | }else{ |
| @@ -2258,15 +2689,17 @@ | |
| 2258 | }else{ |
| 2259 | @ as of check-in [%z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a>]</h2> |
| 2260 | } |
| 2261 | blob_reset(&path); |
| 2262 | } |
| 2263 | style_submenu_element("Artifact", "%R/artifact/%S", zUuid); |
| 2264 | style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", |
| 2265 | zName, zCI); |
| 2266 | style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", |
| 2267 | zName, zCI); |
| 2268 | blob_init(&downloadName, zName, -1); |
| 2269 | objType = OBJTYPE_CONTENT; |
| 2270 | }else{ |
| 2271 | @ <h2>Artifact |
| 2272 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| @@ -2278,29 +2711,33 @@ | |
| 2278 | blob_zero(&downloadName); |
| 2279 | if( asText ) objdescFlags &= ~OBJDESC_BASE; |
| 2280 | objType = object_description(rid, objdescFlags, |
| 2281 | (isFile?zName:0), &downloadName); |
| 2282 | } |
| 2283 | if( !descOnly && P("download")!=0 ){ |
| 2284 | cgi_redirectf("%R/raw/%s?at=%T", |
| 2285 | db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), |
| 2286 | file_tail(blob_str(&downloadName))); |
| 2287 | /*NOTREACHED*/ |
| 2288 | } |
| 2289 | if( g.perm.Admin ){ |
| 2290 | const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2291 | if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
| 2292 | style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#accshun", |
| 2293 | g.zTop, zUuid); |
| 2294 | }else{ |
| 2295 | style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); |
| 2296 | } |
| 2297 | } |
| 2298 | |
| 2299 | if( isFile ){ |
| 2300 | if( isSymbolicCI ){ |
| 2301 | zHeader = mprintf("%s at %s", file_tail(zName), zCI); |
| 2302 | }else if( zCI ){ |
| 2303 | zHeader = mprintf("%s at [%S]", file_tail(zName), zCIUuid); |
| 2304 | }else{ |
| 2305 | zHeader = mprintf("%s", file_tail(zName)); |
| 2306 | } |
| @@ -2326,13 +2763,17 @@ | |
| 2326 | const char *zIp = db_column_text(&q,2); |
| 2327 | @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p> |
| 2328 | } |
| 2329 | db_finalize(&q); |
| 2330 | } |
| 2331 | style_submenu_element("Download", "%R/raw/%s?at=%T", zUuid, file_tail(zName)); |
| 2332 | if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ |
| 2333 | style_submenu_element("Check-ins Using", "%R/timeline?n=200&uf=%s", zUuid); |
| 2334 | } |
| 2335 | zMime = mimetype_from_name(blob_str(&downloadName)); |
| 2336 | if( zMime ){ |
| 2337 | if( fossil_strcmp(zMime, "text/html")==0 ){ |
| 2338 | if( asText ){ |
| @@ -2348,24 +2789,38 @@ | |
| 2348 | }else{ |
| 2349 | renderAsWiki = 1; |
| 2350 | style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); |
| 2351 | } |
| 2352 | } |
| 2353 | if( fileedit_is_editable(zName) ){ |
| 2354 | style_submenu_element("Edit", |
| 2355 | "%R/fileedit?filename=%T&checkin=%!S", |
| 2356 | zName, zCI); |
| 2357 | } |
| 2358 | } |
| 2359 | if( (objType & (OBJTYPE_WIKI|OBJTYPE_TICKET))!=0 ){ |
| 2360 | style_submenu_element("Parsed", "%R/info/%s", zUuid); |
| 2361 | } |
| 2362 | if( descOnly ){ |
| 2363 | style_submenu_element("Content", "%R/artifact/%s", zUuid); |
| 2364 | }else{ |
| 2365 | @ <hr /> |
| 2366 | content_get(rid, &content); |
| 2367 | if( renderAsWiki ){ |
| 2368 | wiki_render_by_mimetype(&content, zMime); |
| 2369 | }else if( renderAsHtml ){ |
| 2370 | @ <iframe src="%R/raw/%s(zUuid)" |
| 2371 | @ width="100%%" frameborder="0" marginwidth="0" marginheight="0" |
| @@ -2377,11 +2832,15 @@ | |
| 2377 | @ this.height=this.contentDocument.documentElement.scrollHeight + 75; |
| 2378 | @ } |
| 2379 | @ ); |
| 2380 | @ </script> |
| 2381 | }else{ |
| 2382 | style_submenu_element("Hex", "%s/hexdump?name=%s", g.zTop, zUuid); |
| 2383 | if( zLn==0 || atoi(zLn)==0 ){ |
| 2384 | style_submenu_checkbox("ln", "Line Numbers", 0, 0); |
| 2385 | } |
| 2386 | blob_to_utf8_no_bom(&content, 0); |
| 2387 | zMime = mimetype_from_content(&content); |
| 2388 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1,5 +1,19 @@ | |
| 1 | /*TODO |
| 2 | ** o Should /file behave differently for non-existent local files? |
| 3 | ** o Look at adding an "extras" option (non-added, non-ignored files). |
| 4 | ** o Look at sifting out "one line" differences from those with "diff blocks". |
| 5 | ** Perhaps reset the query and re-run, displaying only non-diff entries the |
| 6 | ** first time? Or perhaps buffer the output (probably bad idea). |
| 7 | ** o If I keep the extra <hr/> I've added after a diff-block, is there a way |
| 8 | ** to avoid the double <hr/> if the last entry has a diff-block? |
| 9 | ** o Find a place to add links to /local. |
| 10 | ** o Remove //TODO TESTING HACK TODO |
| 11 | ** ?? In hexdump_page(), should content (and downloadName?) be reset/freed? |
| 12 | ** ?? In the test fossil (\x\$Test\Fossil) there are (at time of writing) two |
| 13 | ** commits under the same artifact... is this normal? |
| 14 | */ |
| 15 | /* |
| 16 | ** Copyright (c) 2007 D. Richard Hipp |
| 17 | ** |
| 18 | ** This program is free software; you can redistribute it and/or |
| 19 | ** modify it under the terms of the Simplified BSD License (also |
| @@ -322,17 +336,36 @@ | |
| 336 | |TIMELINE_CHPICK, |
| 337 | 0, 0, 0, rid, rid2, 0); |
| 338 | db_finalize(&q); |
| 339 | } |
| 340 | |
| 341 | /* |
| 342 | ** Read the content of file zName (prepended with the checkout directory) |
| 343 | ** and put it into the uninitialized blob. The blob is zeroed if the file |
| 344 | ** does not exist (if the file cannot be read, blob_read_from_file() aborts |
| 345 | ** the program). |
| 346 | */ |
| 347 | static void content_from_file( |
| 348 | const char *zName, /* Filename (relative to checkout) of file to be read */ |
| 349 | Blob *pBlob /* Pointer to blob to receive contents */ |
| 350 | ){ |
| 351 | const char *zFullPath = mprintf("%s%s", g.zLocalRoot, zName); |
| 352 | blob_zero(pBlob); |
| 353 | if( file_size(zFullPath, ExtFILE)>=0 ){ |
| 354 | blob_read_from_file(pBlob, zFullPath, ExtFILE); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | /* |
| 359 | ** Append the difference between artifacts to the output |
| 360 | ** If zLocal is not NULL, instead compare against the local |
| 361 | ** copy of the file it names in the repository. |
| 362 | */ |
| 363 | static void append_diff( |
| 364 | const char *zFrom, /* Diff from this artifact */ |
| 365 | const char *zTo, /* ... to this artifact */ |
| 366 | const char *zLocal, /* ... OR to this local file */ |
| 367 | u64 diffFlags, /* Diff formatting flags */ |
| 368 | ReCompiled *pRe /* Only show change matching this regex */ |
| 369 | ){ |
| 370 | int fromid; |
| 371 | int toid; |
| @@ -344,10 +377,12 @@ | |
| 377 | blob_zero(&from); |
| 378 | } |
| 379 | if( zTo ){ |
| 380 | toid = uuid_to_rid(zTo, 0); |
| 381 | content_get(toid, &to); |
| 382 | }else if( zLocal ){ /* Read the file on disk */ |
| 383 | content_from_file(zLocal, &to); |
| 384 | }else{ |
| 385 | blob_zero(&to); |
| 386 | } |
| 387 | blob_zero(&out); |
| 388 | if( diffFlags & DIFF_SIDEBYSIDE ){ |
| @@ -396,11 +431,11 @@ | |
| 431 | } |
| 432 | }else{ |
| 433 | @ Changes to %h(zName). |
| 434 | } |
| 435 | if( diffFlags ){ |
| 436 | append_diff(zOld, zNew, NULL, diffFlags, pRe); |
| 437 | } |
| 438 | }else{ |
| 439 | if( zOld && zNew ){ |
| 440 | if( fossil_strcmp(zOld, zNew)!=0 ){ |
| 441 | @ Modified %z(href("%R/finfo?name=%T&m=%!S",zName,zNew))%h(zName)</a> |
| @@ -427,18 +462,172 @@ | |
| 462 | }else{ |
| 463 | @ Added %z(href("%R/finfo?name=%T&m=%!S",zName,zNew))%h(zName)</a> |
| 464 | @ version %z(href("%R/artifact/%!S",zNew))[%S(zNew)]</a>. |
| 465 | } |
| 466 | if( diffFlags ){ |
| 467 | append_diff(zOld, zNew, NULL, diffFlags, pRe); |
| 468 | }else if( zOld && zNew && fossil_strcmp(zOld,zNew)!=0 ){ |
| 469 | @ |
| 470 | @ %z(href("%R/fdiff?v1=%!S&v2=%!S",zOld,zNew))[diff]</a> |
| 471 | } |
| 472 | } |
| 473 | @ </p> |
| 474 | } |
| 475 | |
| 476 | /* |
| 477 | ** Append notice of executable or symlink being gained or lost. |
| 478 | */ |
| 479 | static void append_status( |
| 480 | const char *zAction, /* Whether status was gained or lost */ |
| 481 | const char *zStatus, /* The status that was gained/lost */ |
| 482 | const char *zName, /* Name of file */ |
| 483 | const char *zOld /* Existing artifact */ |
| 484 | ){ |
| 485 | if( !g.perm.Hyperlink ){ |
| 486 | @ %h(zName) %h(zAction) %h(zStatus) status. |
| 487 | }else{ |
| 488 | @ %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> |
| 489 | @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> |
| 490 | @ %h(zAction) %h(zStatus) status. |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | /* |
| 495 | ** Append web-page output that shows changes between a file at last check-in |
| 496 | ** and its current state on disk (i.e. any uncommitted changes). The status |
| 497 | ** ("changed", "missing" etc.) is a blend of that from append_file_change_line() |
| 498 | ** and diff_against_disk() (in "diffcmd.c"). |
| 499 | ** |
| 500 | ** The file-differences (if being shown) use append_diff() as before, but |
| 501 | ** there is an additional parameter (zLocal) which, if non-NULL, causes it |
| 502 | ** to compare the checked-in version against the named file on disk. |
| 503 | */ |
| 504 | static void append_local_file_change_line( |
| 505 | const char *zName, /* Name of the file that has changed */ |
| 506 | const char *zOld, /* blob.uuid before change. NULL for added files */ |
| 507 | int isDeleted, /* Has the file-on-disk been removed from Fossil? */ |
| 508 | int isChnged, /* Has the file changed in some way (see vfile.c) */ |
| 509 | int isNew, /* Has the file been ADDed (but not yet committed? */ |
| 510 | int isLink, /* Is the file a symbolic link? */ |
| 511 | u64 diffFlags, /* Flags for text_diff(). Zero to omit diffs */ |
| 512 | ReCompiled *pRe /* Only show diffs that match this regex, if not NULL */ |
| 513 | ){ |
| 514 | char *zFullName = mprintf("%s%s", g.zLocalRoot, zName); |
| 515 | int isFilePresent = !file_access(zFullName, F_OK); |
| 516 | int showDiff = 0; |
| 517 | //TODO TESTING HACK TODO |
| 518 | if( strncmp(zName,"aa",2)==0 ){ |
| 519 | isChnged = atoi(zName+2); |
| 520 | } |
| 521 | //TODO TESTING HACK TODO |
| 522 | @ <p> |
| 523 | if( !g.perm.Hyperlink ){ |
| 524 | if( isDeleted ){ |
| 525 | if( isFilePresent ){ |
| 526 | @ Deleted %h(zName) (still present as a local file). |
| 527 | showDiff = 1; |
| 528 | }else{ |
| 529 | @ Deleted %h(zName). |
| 530 | } |
| 531 | }else if( isNew ){ |
| 532 | if( isFilePresent ){ |
| 533 | @ Added %h(zName) but not committed. |
| 534 | }else{ |
| 535 | @ Missing %h(zName) (was added to checkout). |
| 536 | } |
| 537 | }else switch( isChnged ){ |
| 538 | /*TODO |
| 539 | ** These "special cases" have not been properly tested (by creating |
| 540 | ** entries in a in a repository to trigger them), but they do display |
| 541 | ** as expected when "forced" to appear. |
| 542 | */ |
| 543 | case 3: |
| 544 | @ Added %h(zName) due to a merge. |
| 545 | break; |
| 546 | case 5: |
| 547 | @ Added %h(zName) due to an integration merge. |
| 548 | break; |
| 549 | case 6: append_status( "gained", "executable", zName, zOld); break; |
| 550 | case 7: append_status( "gained", "symlink", zName, zOld); break; |
| 551 | case 8: append_status( "lost", "executable", zName, zOld); break; |
| 552 | case 9: append_status( "lost", "symlink", zName, zOld); break; |
| 553 | |
| 554 | default: /* Normal edit */ |
| 555 | @ Local changes of %h(zName). |
| 556 | showDiff = 1; |
| 557 | } |
| 558 | if( showDiff && diffFlags ){ |
| 559 | append_diff(zOld, NULL, zName, diffFlags, pRe); |
| 560 | @ <hr/> |
| 561 | } |
| 562 | }else{ |
| 563 | if( isDeleted ){ |
| 564 | if( isFilePresent ){ /* DELETEd but still on disk */ |
| 565 | @ Deleted %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> |
| 566 | @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> (still present |
| 567 | @ as %z(href("%R/file/%T?ci=ckout&annot=removed from checkout",zName)) |
| 568 | @ [local file]</a>). |
| 569 | showDiff = 1; |
| 570 | }else{ /* DELETEd and deleted from disk */ |
| 571 | @ Deleted %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> |
| 572 | @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a>. |
| 573 | } |
| 574 | }else if( isNew ){ |
| 575 | if( isFilePresent ){ /* ADDed and present on disk */ |
| 576 | @ Added %z(href("%R/file/%T?ci=ckout",zName))%h(zName)</a> |
| 577 | @ but not committed. |
| 578 | }else{ /* ADDed but not present on disk */ |
| 579 | @ Missing %h(zName) (was added to checkout). |
| 580 | } |
| 581 | }else switch( isChnged ){ |
| 582 | /*TODO Not fully tested... see see no-hyperlink version above */ |
| 583 | case 3: /* Added by a merge */ |
| 584 | @ Added |
| 585 | @ %z(href("%R/file/%T?ci=ckout&annot=added by merge",zName))%h(zName) |
| 586 | @ </a> to %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> due to merge. |
| 587 | break; |
| 588 | case 5: /* Added by an integration merge */ |
| 589 | @ Added |
| 590 | @ %z(href("%R/file/%T?ci=ckout&annot=added by integration merge",zName)) |
| 591 | @ %h(zName)</a> to |
| 592 | @ %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> due to integrate merge. |
| 593 | break; |
| 594 | case 6: append_status( "gained", "executable", zName, zOld); break; |
| 595 | case 7: append_status( "gained", "symlink", zName, zOld); break; |
| 596 | case 8: append_status( "lost", "executable", zName, zOld); break; |
| 597 | case 9: append_status( "lost", "symlink", zName, zOld); break; |
| 598 | |
| 599 | default: /* Normal edit */ |
| 600 | @ Local changes of |
| 601 | @ %z(href("%R/finfo?name=%T&m=%!S",zName,zOld))%h(zName)</a> |
| 602 | @ from %z(href("%R/artifact/%!S",zOld))[%S(zOld)]</a> to |
| 603 | @ %z(href("%R/file/%T?ci=ckout&annot=edited locally",zName)) |
| 604 | @ [local file]</a> |
| 605 | showDiff = 1; |
| 606 | } |
| 607 | if( showDiff ){ |
| 608 | if( diffFlags ){ |
| 609 | append_diff(zOld, NULL, zName, diffFlags, pRe); |
| 610 | /*TODO |
| 611 | ** Not related to the local-mode, but if two or more files have been |
| 612 | ** changed in a commit/"local changes", it is sometimes easy to miss |
| 613 | ** the switch from one to the other. The following (IMHO) makes things |
| 614 | ** clearer, but can mean there's a double rule at the bottom of the |
| 615 | ** page. If kept, a similar <hr/> should probably be added to |
| 616 | ** append_file_change_line() (but would need to check how things look |
| 617 | ** when called from /vinfo). |
| 618 | */ |
| 619 | @ <hr/> |
| 620 | }else if( isChnged ){ |
| 621 | @ |
| 622 | @ %z(href("%R/localdiff?name=%T",zName))[diff]</a> |
| 623 | } |
| 624 | } |
| 625 | } |
| 626 | @ </p> |
| 627 | fossil_free(zFullName); |
| 628 | } |
| 629 | |
| 630 | /* |
| 631 | ** Generate javascript to enhance HTML diffs. |
| 632 | */ |
| 633 | void append_diff_javascript(int sideBySide){ |
| @@ -596,19 +785,24 @@ | |
| 785 | } |
| 786 | |
| 787 | /* |
| 788 | ** WEBPAGE: vinfo |
| 789 | ** WEBPAGE: ci |
| 790 | ** WEBPAGE: local |
| 791 | ** URL: /ci/ARTIFACTID |
| 792 | ** OR: /ci?name=ARTIFACTID |
| 793 | ** |
| 794 | ** Display information about a particular check-in. The exact |
| 795 | ** same information is shown on the /info page if the name query |
| 796 | ** parameter to /info describes a check-in. |
| 797 | ** |
| 798 | ** The ARTIFACTID can be a unique prefix for the HASH of the check-in, |
| 799 | ** or a tag or branch name that identifies the check-in. |
| 800 | ** |
| 801 | ** Use of /local (or the use of "ckout" for ARTIFACTID) will show the |
| 802 | ** same header details as /ci/tip, but then displays any (uncommitted) |
| 803 | ** edits made to files in the checkout directory. |
| 804 | */ |
| 805 | void ci_page(void){ |
| 806 | Stmt q1, q2, q3; |
| 807 | int rid; |
| 808 | int isLeaf; |
| @@ -621,14 +815,37 @@ | |
| 815 | ReCompiled *pRe = 0; /* regex */ |
| 816 | const char *zW; /* URL param for ignoring whitespace */ |
| 817 | const char *zPage = "vinfo"; /* Page that shows diffs */ |
| 818 | const char *zPageHide = "ci"; /* Page that hides diffs */ |
| 819 | const char *zBrName; /* Branch name */ |
| 820 | int bLocalMode; /* TRUE for /local; FALSE otherwise */ |
| 821 | int vid; /* Virtual file system? */ |
| 822 | |
| 823 | login_check_credentials(); |
| 824 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 825 | zName = P("name"); |
| 826 | /* Local mode is selected by either "/local" or with a "name" of "ckout". |
| 827 | ** First, check we have access to the checkout (and report to the user if we |
| 828 | ** don't), then refresh the "vfile" table (recording which files in the |
| 829 | ** checkout have changed etc.). We then change the "name" parameter to "tip" |
| 830 | ** so that the "header" section displays info about the check-in that the |
| 831 | ** checkout came from. |
| 832 | */ |
| 833 | bLocalMode = (g.zPath[0]=='l') || (fossil_strcmp(zName,"ckout")==0); |
| 834 | if( bLocalMode ){ |
| 835 | vid = g.localOpen ? db_lget_int("checkout", 0) : 0; |
| 836 | if( vid==0 ){ |
| 837 | /*TODO Is this the right response? */ |
| 838 | style_header("No Local Checkout"); |
| 839 | @ No access to local checkout. |
| 840 | style_footer(); |
| 841 | return; |
| 842 | } |
| 843 | vfile_check_signature(vid, CKSIG_ENOTFILE); |
| 844 | zName = "tip"; |
| 845 | cgi_replace_parameter("name","tip"); /* Needed to get rid below */ |
| 846 | } |
| 847 | rid = name_to_rid_www("name"); |
| 848 | if( rid==0 ){ |
| 849 | style_header("Check-in Information Error"); |
| 850 | @ No such object: %h(g.argv[2]) |
| 851 | style_footer(); |
| @@ -667,11 +884,15 @@ | |
| 884 | int okWiki = 0; |
| 885 | Blob wiki_read_links = BLOB_INITIALIZER; |
| 886 | Blob wiki_add_links = BLOB_INITIALIZER; |
| 887 | |
| 888 | Th_Store("current_checkin", zName); |
| 889 | if( bLocalMode ){ |
| 890 | style_header("Local Changes from Check-in [%S]", zUuid); |
| 891 | }else{ |
| 892 | style_header("Check-in [%S]", zUuid); |
| 893 | } |
| 894 | login_anonymous_available(); |
| 895 | zEUser = db_text(0, |
| 896 | "SELECT value FROM tagxref" |
| 897 | " WHERE tagid=%d AND rid=%d AND tagtype>0", |
| 898 | TAG_USER, rid); |
| @@ -873,14 +1094,25 @@ | |
| 1094 | wiki_render_associated("checkin", zUuid, 0); |
| 1095 | } |
| 1096 | render_backlink_graph(zUuid, "<div class=\"section\">References</div>\n"); |
| 1097 | @ <div class="section">Context</div> |
| 1098 | render_checkin_context(rid, 0, 0); |
| 1099 | if( bLocalMode ){ |
| 1100 | @ <div class="section">Uncommitted Changes</div> |
| 1101 | }else{ |
| 1102 | @ <div class="section">Changes</div> |
| 1103 | } |
| 1104 | @ <div class="sectionmenu"> |
| 1105 | diffFlags = construct_diff_flags(diffType); |
| 1106 | zW = (diffFlags&DIFF_IGNORE_ALLWS)?"&w":""; |
| 1107 | /* In local mode, having displayed the header info for "tip", switch zName |
| 1108 | ** to be "ckout" so the style-altering links (unified or side-by-side etc.) |
| 1109 | ** will correctly re-select local-mode. |
| 1110 | */ |
| 1111 | if( bLocalMode ){ |
| 1112 | zName = "ckout"; |
| 1113 | } |
| 1114 | if( diffType!=0 ){ |
| 1115 | @ %z(chref("button","%R/%s/%T?diff=0",zPageHide,zName))\ |
| 1116 | @ Hide Diffs</a> |
| 1117 | } |
| 1118 | if( diffType!=1 ){ |
| @@ -898,48 +1130,163 @@ | |
| 1130 | }else{ |
| 1131 | @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType)) |
| 1132 | @ Ignore Whitespace</a> |
| 1133 | } |
| 1134 | } |
| 1135 | if( bLocalMode ){ |
| 1136 | @ %z(chref("button","%R/localpatch")) Patch</a> |
| 1137 | }else{ |
| 1138 | if( zParent ){ |
| 1139 | @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) |
| 1140 | @ Patch</a> |
| 1141 | } |
| 1142 | } |
| 1143 | if( g.perm.Admin ){ |
| 1144 | @ %z(chref("button","%R/mlink?ci=%!S",zUuid))MLink Table</a> |
| 1145 | } |
| 1146 | @</div> |
| 1147 | if( pRe ){ |
| 1148 | @ <p><b>Only differences that match regular expression "%h(zRe)" |
| 1149 | @ are shown.</b></p> |
| 1150 | } |
| 1151 | if( bLocalMode ){ |
| 1152 | /* Following SQL taken from diff_against_disk() in diffcmd.c */ |
| 1153 | db_begin_transaction(); |
| 1154 | db_prepare(&q3, |
| 1155 | "SELECT pathname, deleted, chnged , rid==0, rid, islink" |
| 1156 | " FROM vfile" |
| 1157 | " WHERE vid=%d" |
| 1158 | " AND (deleted OR chnged OR rid==0)" |
| 1159 | " ORDER BY pathname /*scan*/", |
| 1160 | vid |
| 1161 | ); |
| 1162 | /* TODO Have the option of showing "extras" (non-ignored files in the |
| 1163 | ** checkout directory that have not been ADDed). If done, they should |
| 1164 | ** be ahead of any potential "diff-blocks" so they don't get lost |
| 1165 | ** (which is the inspiration for...) |
| 1166 | ** TODO Consider making this two-pass, where the first pass skips anything |
| 1167 | ** that would show a diff-block (and the second pass only shows such |
| 1168 | ** entries). This would group all "one-line" entries at the top so |
| 1169 | ** they are less likely to be missed. |
| 1170 | ** TODO Possibly (at some stage) have an option to commit? |
| 1171 | */ |
| 1172 | while( db_step(&q3)==SQLITE_ROW ){ |
| 1173 | const char *zPathname = db_column_text(&q3,0); |
| 1174 | int isDeleted = db_column_int(&q3, 1); |
| 1175 | int isChnged = db_column_int(&q3,2); |
| 1176 | int isNew = db_column_int(&q3,3); |
| 1177 | int srcid = db_column_int(&q3, 4); |
| 1178 | int isLink = db_column_int(&q3, 5); |
| 1179 | char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", srcid); |
| 1180 | append_local_file_change_line(zPathname, zUuid, |
| 1181 | isDeleted, isChnged, isNew, isLink, diffFlags,pRe); |
| 1182 | free(zUuid); |
| 1183 | } |
| 1184 | db_finalize(&q3); |
| 1185 | db_end_transaction(1); /* ROLLBACK */ |
| 1186 | }else{ /* Normal, non-local-mode: show diffs against parent */ |
| 1187 | db_prepare(&q3, |
| 1188 | "SELECT name," |
| 1189 | " mperm," |
| 1190 | " (SELECT uuid FROM blob WHERE rid=mlink.pid)," |
| 1191 | " (SELECT uuid FROM blob WHERE rid=mlink.fid)," |
| 1192 | " (SELECT name FROM filename WHERE filename.fnid=mlink.pfnid)" |
| 1193 | " FROM mlink JOIN filename ON filename.fnid=mlink.fnid" |
| 1194 | " WHERE mlink.mid=%d AND NOT mlink.isaux" |
| 1195 | " AND (mlink.fid>0" |
| 1196 | " OR mlink.fnid NOT IN (SELECT pfnid FROM mlink WHERE mid=%d))" |
| 1197 | " ORDER BY name /*sort*/", |
| 1198 | rid, rid |
| 1199 | ); |
| 1200 | while( db_step(&q3)==SQLITE_ROW ){ |
| 1201 | const char *zName = db_column_text(&q3,0); |
| 1202 | int mperm = db_column_int(&q3, 1); |
| 1203 | const char *zOld = db_column_text(&q3,2); |
| 1204 | const char *zNew = db_column_text(&q3,3); |
| 1205 | const char *zOldName = db_column_text(&q3, 4); |
| 1206 | append_file_change_line(zName, zOld, zNew, zOldName, diffFlags,pRe,mperm); |
| 1207 | } |
| 1208 | db_finalize(&q3); |
| 1209 | } |
| 1210 | append_diff_javascript(diffType==2); |
| 1211 | cookie_render(); |
| 1212 | style_footer(); |
| 1213 | } |
| 1214 | |
| 1215 | /* |
| 1216 | ** WEBPAGE: localpatch |
| 1217 | ** URL: /localpatch |
| 1218 | ** |
| 1219 | ** Shows a patch from the current checkout, incorporating any |
| 1220 | ** uncommitted local edits. |
| 1221 | */ |
| 1222 | void localpatch_page(void){ |
| 1223 | Stmt q3; |
| 1224 | int vid; |
| 1225 | |
| 1226 | login_check_credentials(); |
| 1227 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 1228 | |
| 1229 | vid = g.localOpen ? db_lget_int("checkout", 0) : 0; |
| 1230 | if( vid==0 ){ |
| 1231 | /*TODO Is this the right response? */ |
| 1232 | style_header("No Local Checkout"); |
| 1233 | @ No access to local checkout. |
| 1234 | style_footer(); |
| 1235 | return; |
| 1236 | } |
| 1237 | vfile_check_signature(vid, CKSIG_ENOTFILE); |
| 1238 | |
| 1239 | cgi_set_content_type("text/plain"); |
| 1240 | |
| 1241 | db_begin_transaction(); |
| 1242 | /*TODO |
| 1243 | ** This query is the same as in ci_page() for local-mode (as well as in |
| 1244 | ** diff_against_disk() in diffcmd.c, where it was originally taken from). |
| 1245 | ** Should they be "coalesced" in some way? |
| 1246 | */ |
| 1247 | db_prepare(&q3, |
| 1248 | "SELECT pathname, deleted, chnged , rid==0, rid, islink" |
| 1249 | " FROM vfile" |
| 1250 | " WHERE vid=%d" |
| 1251 | " AND (deleted OR chnged OR rid==0)" |
| 1252 | " ORDER BY pathname /*scan*/", |
| 1253 | vid |
| 1254 | ); |
| 1255 | while( db_step(&q3)==SQLITE_ROW ){ |
| 1256 | const char *zPathname = db_column_text(&q3,0); |
| 1257 | int isDeleted = db_column_int(&q3, 1); |
| 1258 | int isChnged = db_column_int(&q3,2); |
| 1259 | int isNew = db_column_int(&q3,3); |
| 1260 | int srcid = db_column_int(&q3, 4); |
| 1261 | int isLink = db_column_int(&q3, 5); |
| 1262 | char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", srcid); |
| 1263 | |
| 1264 | if( isChnged ){ |
| 1265 | Blob c1, c2; /* Content to diff */ |
| 1266 | Blob out; /* Diff output text */ |
| 1267 | int diffFlags = 4; |
| 1268 | |
| 1269 | content_get(srcid, &c1); |
| 1270 | content_from_file(zPathname, &c2); |
| 1271 | blob_zero(&out); |
| 1272 | text_diff(&c1, &c2, &out, 0, diffFlags); |
| 1273 | blob_reset(&c1); |
| 1274 | blob_reset(&c2); |
| 1275 | if( blob_size(&out) ){ |
| 1276 | diff_print_index(zPathname, diffFlags); |
| 1277 | diff_print_filenames(zPathname, zPathname, diffFlags); |
| 1278 | fossil_print("%s\n", blob_str(&out)); |
| 1279 | } |
| 1280 | /* Release memory resources */ |
| 1281 | blob_reset(&out); |
| 1282 | } |
| 1283 | free(zUuid); |
| 1284 | } |
| 1285 | db_finalize(&q3); |
| 1286 | db_end_transaction(1); /* ROLLBACK */ |
| 1287 | } |
| 1288 | |
| 1289 | /* |
| 1290 | ** WEBPAGE: winfo |
| 1291 | ** URL: /winfo?name=UUID |
| 1292 | ** |
| @@ -1216,11 +1563,11 @@ | |
| 1563 | style_submenu_element("Side-by-Side Diff", |
| 1564 | "%R/vdiff?%s&diff=2%s%T%s", |
| 1565 | zQuery, |
| 1566 | zGlob ? "&glob=" : "", zGlob ? zGlob : "", zW); |
| 1567 | } |
| 1568 | if( diffType!=1 ){ |
| 1569 | style_submenu_element("Unified Diff", |
| 1570 | "%R/vdiff?%s&diff=1%s%T%s", |
| 1571 | zQuery, |
| 1572 | zGlob ? "&glob=" : "", zGlob ? zGlob : "", zW); |
| 1573 | } |
| @@ -1400,11 +1747,11 @@ | |
| 1747 | } |
| 1748 | cnt++; |
| 1749 | continue; |
| 1750 | } |
| 1751 | if( !sameFilename ){ |
| 1752 | if( prevName && showDetail ){ |
| 1753 | @ </ul> |
| 1754 | } |
| 1755 | if( mPerm==PERM_LNK ){ |
| 1756 | @ <li>Symbolic link |
| 1757 | objType |= OBJTYPE_SYMLINK; |
| @@ -1520,11 +1867,11 @@ | |
| 1867 | objType |= OBJTYPE_TICKET; |
| 1868 | }else if( zType[0]=='c' ){ |
| 1869 | @ Manifest of check-in |
| 1870 | objType |= OBJTYPE_CHECKIN; |
| 1871 | }else if( zType[0]=='e' ){ |
| 1872 | if( eventTagId != 0){ |
| 1873 | @ Instance of technote |
| 1874 | objType |= OBJTYPE_EVENT; |
| 1875 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1876 | }else{ |
| 1877 | @ Attachment to technote |
| @@ -1567,11 +1914,11 @@ | |
| 1914 | }else{ |
| 1915 | @ Attachment "%h(zFilename)" to |
| 1916 | } |
| 1917 | objType |= OBJTYPE_ATTACHMENT; |
| 1918 | if( fossil_is_uuid(zTarget) ){ |
| 1919 | if( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", |
| 1920 | zTarget) |
| 1921 | ){ |
| 1922 | if( g.perm.Hyperlink && g.anon.RdTkt ){ |
| 1923 | @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>] |
| 1924 | }else{ |
| @@ -1625,11 +1972,13 @@ | |
| 1972 | } |
| 1973 | |
| 1974 | |
| 1975 | /* |
| 1976 | ** WEBPAGE: fdiff |
| 1977 | ** WEBPAGE: localdiff |
| 1978 | ** URL: fdiff?v1=UUID&v2=UUID |
| 1979 | ** URL: localdiff?name=filename |
| 1980 | ** |
| 1981 | ** Two arguments, v1 and v2, identify the artifacts to be diffed. |
| 1982 | ** Show diff side by side unless sbs is 0. Generate plain text if |
| 1983 | ** "patch" is present, otherwise generate "pretty" HTML. |
| 1984 | ** |
| @@ -1637,10 +1986,14 @@ | |
| 1986 | ** |
| 1987 | ** If the "from" and "to" query parameters are both present, then they are |
| 1988 | ** the names of two files within the check-in "ci" that are diffed. If the |
| 1989 | ** "ci" parameter is omitted, then the most recent check-in ("tip") is |
| 1990 | ** used. |
| 1991 | ** |
| 1992 | ** The /localdiff version will diff the given filename from the most recent |
| 1993 | ** check-in ("tip") against the current (edited) version in the checkout |
| 1994 | ** directory. |
| 1995 | ** |
| 1996 | ** Additional parameters: |
| 1997 | ** |
| 1998 | ** dc=N Show N lines of context around each diff |
| 1999 | ** patch Use the patch diff format |
| @@ -1658,17 +2011,23 @@ | |
| 2011 | const char *zRe; |
| 2012 | ReCompiled *pRe = 0; |
| 2013 | u64 diffFlags; |
| 2014 | u32 objdescFlags = 0; |
| 2015 | int verbose = PB("verbose"); |
| 2016 | int bLocalMode = g.zPath[0]=='l'; /* diff against checkout */ |
| 2017 | const char *zLocalName = NULL; /* Holds local filename */ |
| 2018 | |
| 2019 | login_check_credentials(); |
| 2020 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2021 | cookie_link_parameter("diff","diff","2"); |
| 2022 | diffType = atoi(PD("diff","2")); |
| 2023 | cookie_render(); |
| 2024 | if( bLocalMode ){ |
| 2025 | zLocalName = P("name"); |
| 2026 | v1 = artifact_from_ci_and_filename("name"); |
| 2027 | v2 = (zLocalName!=NULL)?-1:0; /* -1 prevents "not found" check below */ |
| 2028 | }else if( P("from") && P("to") ){ |
| 2029 | v1 = artifact_from_ci_and_filename("from"); |
| 2030 | v2 = artifact_from_ci_and_filename("to"); |
| 2031 | }else{ |
| 2032 | Stmt q; |
| 2033 | v1 = name_to_rid_www("v1"); |
| @@ -1707,14 +2066,19 @@ | |
| 2066 | if( zRe ) re_compile(&pRe, zRe, 0); |
| 2067 | if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
| 2068 | if( isPatch ){ |
| 2069 | Blob c1, c2, *pOut; |
| 2070 | pOut = cgi_output_blob(); |
| 2071 | |
| 2072 | cgi_set_content_type("text/plain"); |
| 2073 | diffFlags = 4; |
| 2074 | content_get(v1, &c1); |
| 2075 | if( bLocalMode ){ |
| 2076 | content_from_file(zLocalName, &c2); |
| 2077 | }else{ |
| 2078 | content_get(v2, &c2); |
| 2079 | } |
| 2080 | text_diff(&c1, &c2, pOut, pRe, diffFlags); |
| 2081 | blob_reset(&c1); |
| 2082 | blob_reset(&c2); |
| 2083 | return; |
| 2084 | } |
| @@ -1723,38 +2087,62 @@ | |
| 2087 | zV2 = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", v2); |
| 2088 | diffFlags = construct_diff_flags(diffType) | DIFF_HTML; |
| 2089 | |
| 2090 | style_header("Diff"); |
| 2091 | style_submenu_checkbox("w", "Ignore Whitespace", 0, 0); |
| 2092 | if( bLocalMode ){ |
| 2093 | if( diffType==2 ){ |
| 2094 | style_submenu_element("Unified Diff", "%R/localdiff?name=%T&diff=1", |
| 2095 | zLocalName); |
| 2096 | }else{ |
| 2097 | style_submenu_element("Side-by-side Diff", "%R/localdiff?name=%T&diff=2", |
| 2098 | zLocalName); |
| 2099 | } |
| 2100 | }else{ /* Normal */ |
| 2101 | if( diffType==2 ){ |
| 2102 | style_submenu_element("Unified Diff", "%R/fdiff?v1=%T&v2=%T&diff=1", |
| 2103 | P("v1"), P("v2")); |
| 2104 | }else{ |
| 2105 | style_submenu_element("Side-by-side Diff", "%R/fdiff?v1=%T&v2=%T&diff=2", |
| 2106 | P("v1"), P("v2")); |
| 2107 | } |
| 2108 | } |
| 2109 | style_submenu_checkbox("verbose", "Verbose", 0, 0); |
| 2110 | if( bLocalMode ){ |
| 2111 | style_submenu_element("Patch", "%R/localdiff?name=%T&patch", zLocalName); |
| 2112 | }else{ |
| 2113 | style_submenu_element("Patch", "%R/fdiff?v1=%T&v2=%T&patch", |
| 2114 | P("v1"), P("v2")); |
| 2115 | } |
| 2116 | |
| 2117 | if( P("smhdr")!=0 ){ |
| 2118 | @ <h2>Differences From Artifact |
| 2119 | @ %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a> To |
| 2120 | if( bLocalMode ){ |
| 2121 | @ %z(href("%R/local"))[Local Changes]</a> of |
| 2122 | @ %z(href("%R/file/%T?ci=ckout",zLocalName))%h(zLocalName)</a>. |
| 2123 | }else{ |
| 2124 | @ %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>.</h2> |
| 2125 | } |
| 2126 | }else{ |
| 2127 | @ <h2>Differences From |
| 2128 | @ Artifact %z(href("%R/artifact/%!S",zV1))[%S(zV1)]</a>:</h2> |
| 2129 | object_description(v1, objdescFlags,0, 0); |
| 2130 | if( bLocalMode ){ |
| 2131 | @ <h2>To %z(href("%R/local"))[Local Changes]</a> |
| 2132 | @ of %z(href("%R/file/%T?ci=ckout",zLocalName))%h(zLocalName)</a>.</h2> |
| 2133 | }else{ |
| 2134 | @ <h2>To Artifact %z(href("%R/artifact/%!S",zV2))[%S(zV2)]</a>:</h2> |
| 2135 | object_description(v2, objdescFlags,0, 0); |
| 2136 | } |
| 2137 | } |
| 2138 | if( pRe ){ |
| 2139 | @ <b>Only differences that match regular expression "%h(zRe)" |
| 2140 | @ are shown.</b> |
| 2141 | } |
| 2142 | @ <hr /> |
| 2143 | append_diff(zV1, zV2, zLocalName, diffFlags, pRe); |
| 2144 | append_diff_javascript(diffType); |
| 2145 | style_footer(); |
| 2146 | } |
| 2147 | |
| 2148 | /* |
| @@ -1908,13 +2296,16 @@ | |
| 2296 | } |
| 2297 | |
| 2298 | /* |
| 2299 | ** WEBPAGE: hexdump |
| 2300 | ** URL: /hexdump?name=ARTIFACTID |
| 2301 | ** URL: /hexdump?local=FILENAME |
| 2302 | ** |
| 2303 | ** Show the complete content of a file identified by ARTIFACTID |
| 2304 | ** as preformatted text. |
| 2305 | ** |
| 2306 | ** The second version does the same for FILENAME from the local checkout. |
| 2307 | ** |
| 2308 | ** Other parameters: |
| 2309 | ** |
| 2310 | ** verbose Show more detail when describing the object |
| 2311 | */ |
| @@ -1922,42 +2313,61 @@ | |
| 2313 | int rid; |
| 2314 | Blob content; |
| 2315 | Blob downloadName; |
| 2316 | char *zUuid; |
| 2317 | u32 objdescFlags = 0; |
| 2318 | const char *zLocalName = P("local"); |
| 2319 | int bLocalMode = zLocalName!=NULL; |
| 2320 | |
| 2321 | rid = name_to_rid_www("name"); |
| 2322 | login_check_credentials(); |
| 2323 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2324 | if( !bLocalMode ){ |
| 2325 | if( rid==0 ) fossil_redirect_home(); |
| 2326 | if( g.perm.Admin ){ |
| 2327 | const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",rid); |
| 2328 | if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
| 2329 | style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#delshun", |
| 2330 | g.zTop, zUuid); |
| 2331 | }else{ |
| 2332 | style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); |
| 2333 | } |
| 2334 | } |
| 2335 | } |
| 2336 | style_header("Hex Artifact Content"); |
| 2337 | /* TODO |
| 2338 | ** Could the call to style_header() be moved so these two exclusion |
| 2339 | ** blocks could be merged? I don't think any of them make sense for |
| 2340 | ** a local file. |
| 2341 | */ |
| 2342 | if( !bLocalMode ){ |
| 2343 | zUuid = db_text("?","SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2344 | @ <h2>Artifact |
| 2345 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| 2346 | if( g.perm.Setup ){ |
| 2347 | @ (%d(rid)):</h2> |
| 2348 | }else{ |
| 2349 | @ :</h2> |
| 2350 | } |
| 2351 | blob_zero(&downloadName); |
| 2352 | if( P("verbose")!=0 ) objdescFlags |= OBJDESC_DETAIL; |
| 2353 | object_description(rid, objdescFlags, 0, &downloadName); |
| 2354 | style_submenu_element("Download", "%R/raw/%s?at=%T", |
| 2355 | zUuid, file_tail(blob_str(&downloadName))); |
| 2356 | }else{ |
| 2357 | @ <h2>File %z(href("%R/finfo?name=%T&m=tip",zLocalName))%h(zLocalName)</a> |
| 2358 | @ from %z(href("%R/local"))[Local Changes]</a></h2> |
| 2359 | } |
| 2360 | @ <hr /> |
| 2361 | if( bLocalMode ){ |
| 2362 | content_from_file(zLocalName, &content); |
| 2363 | }else{ |
| 2364 | content_get(rid, &content); |
| 2365 | } |
| 2366 | @ <blockquote><pre> |
| 2367 | hexdump(&content); |
| 2368 | /* TODO: Should content (and downloadName?) be reset/freed? */ |
| 2369 | @ </pre></blockquote> |
| 2370 | style_footer(); |
| 2371 | } |
| 2372 | |
| 2373 | /* |
| @@ -2131,10 +2541,13 @@ | |
| 2541 | ** if name= cannot be understood as a hash, a default "tip" value is |
| 2542 | ** used for ci=. |
| 2543 | ** |
| 2544 | ** For /file, name= can only be interpreted as a filename. As before, |
| 2545 | ** a default value of "tip" is used for ci= if ci= is omitted. |
| 2546 | ** |
| 2547 | ** If ci=ckout then display the content of the file NAME in the local |
| 2548 | ** checkout directory. |
| 2549 | */ |
| 2550 | void artifact_page(void){ |
| 2551 | int rid = 0; |
| 2552 | Blob content; |
| 2553 | const char *zMime; |
| @@ -2153,10 +2566,11 @@ | |
| 2566 | HQuery url; |
| 2567 | char *zCIUuid = 0; |
| 2568 | int isSymbolicCI = 0; /* ci= exists and is a symbolic name, not a hash */ |
| 2569 | int isBranchCI = 0; /* ci= refers to a branch name */ |
| 2570 | char *zHeader = 0; |
| 2571 | int bLocalMode = 0; /* TRUE if trying to show file in local checkout */ |
| 2572 | |
| 2573 | login_check_credentials(); |
| 2574 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2575 | |
| 2576 | /* Capture and normalize the name= and ci= query parameters */ |
| @@ -2200,11 +2614,14 @@ | |
| 2614 | ** name= as a hash for /artifact and /whatis. But for not for /file. |
| 2615 | ** For /file, a name= without a ci= while prefer to use the default |
| 2616 | ** "tip" value for ci=. */ |
| 2617 | rid = name_to_rid(zName); |
| 2618 | } |
| 2619 | if( fossil_strcmp(zCI,"ckout")==0 ){ |
| 2620 | bLocalMode = 1; |
| 2621 | rid = -1; /* Dummy value to make it look found */ |
| 2622 | }else if( rid==0 ){ |
| 2623 | rid = artifact_from_ci_and_filename(0); |
| 2624 | } |
| 2625 | |
| 2626 | if( rid==0 ){ /* Artifact not found */ |
| 2627 | if( isFile ){ |
| @@ -2237,11 +2654,25 @@ | |
| 2654 | } |
| 2655 | zUuid = db_text("?", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2656 | |
| 2657 | asText = P("txt")!=0; |
| 2658 | if( isFile ){ |
| 2659 | if( bLocalMode ){ |
| 2660 | /*TODO |
| 2661 | ** Is this the best way of handling annotations to the description? |
| 2662 | ** If "annot=message" is part of the URL, the message is appended |
| 2663 | ** to the description of the file. Only used for "local" files to |
| 2664 | ** distinguish such files from part of the repository. |
| 2665 | */ |
| 2666 | const char *annot = P("annot"); |
| 2667 | @ <h2>File %z(href("%R/finfo?name=%T&m=tip",zName))%h(zName)</a> |
| 2668 | @ from %z(href("%R/local"))[Local Changes]</a> |
| 2669 | if( annot ){ |
| 2670 | @ (%h(annot)) |
| 2671 | } |
| 2672 | @ </h2> |
| 2673 | }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ |
| 2674 | zCI = "tip"; |
| 2675 | isSymbolicCI = 1; /* Mark default-to-"tip" as symbolic */ |
| 2676 | @ <h2>File %z(href("%R/finfo?name=%T&m=tip",zName))%h(zName)</a> |
| 2677 | @ from the %z(href("%R/info/tip"))latest check-in</a></h2> |
| 2678 | }else{ |
| @@ -2258,15 +2689,17 @@ | |
| 2689 | }else{ |
| 2690 | @ as of check-in [%z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a>]</h2> |
| 2691 | } |
| 2692 | blob_reset(&path); |
| 2693 | } |
| 2694 | if( !bLocalMode ){ |
| 2695 | style_submenu_element("Artifact", "%R/artifact/%S", zUuid); |
| 2696 | style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", |
| 2697 | zName, zCI); |
| 2698 | style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", |
| 2699 | zName, zCI); |
| 2700 | } |
| 2701 | blob_init(&downloadName, zName, -1); |
| 2702 | objType = OBJTYPE_CONTENT; |
| 2703 | }else{ |
| 2704 | @ <h2>Artifact |
| 2705 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| @@ -2278,29 +2711,33 @@ | |
| 2711 | blob_zero(&downloadName); |
| 2712 | if( asText ) objdescFlags &= ~OBJDESC_BASE; |
| 2713 | objType = object_description(rid, objdescFlags, |
| 2714 | (isFile?zName:0), &downloadName); |
| 2715 | } |
| 2716 | if( !bLocalMode ){ |
| 2717 | if( !descOnly && P("download")!=0 ){ |
| 2718 | cgi_redirectf("%R/raw/%s?at=%T", |
| 2719 | db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), |
| 2720 | file_tail(blob_str(&downloadName))); |
| 2721 | /*NOTREACHED*/ |
| 2722 | } |
| 2723 | if( g.perm.Admin ){ |
| 2724 | const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",rid); |
| 2725 | if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
| 2726 | style_submenu_element("Unshun", "%s/shun?accept=%s&sub=1#accshun", |
| 2727 | g.zTop, zUuid); |
| 2728 | }else{ |
| 2729 | style_submenu_element("Shun", "%s/shun?shun=%s#addshun", g.zTop, zUuid); |
| 2730 | } |
| 2731 | } |
| 2732 | } |
| 2733 | |
| 2734 | if( isFile ){ |
| 2735 | if( isSymbolicCI ){ |
| 2736 | zHeader = mprintf("%s at %s", file_tail(zName), zCI); |
| 2737 | }else if( bLocalMode ){ |
| 2738 | zHeader = mprintf("%s (local changes)", file_tail(zName)); |
| 2739 | }else if( zCI ){ |
| 2740 | zHeader = mprintf("%s at [%S]", file_tail(zName), zCIUuid); |
| 2741 | }else{ |
| 2742 | zHeader = mprintf("%s", file_tail(zName)); |
| 2743 | } |
| @@ -2326,13 +2763,17 @@ | |
| 2763 | const char *zIp = db_column_text(&q,2); |
| 2764 | @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p> |
| 2765 | } |
| 2766 | db_finalize(&q); |
| 2767 | } |
| 2768 | if( !bLocalMode ){ |
| 2769 | style_submenu_element("Download", "%R/raw/%s?at=%T", |
| 2770 | zUuid, file_tail(zName)); |
| 2771 | if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ |
| 2772 | style_submenu_element("Check-ins Using", "%R/timeline?n=200&uf=%s", |
| 2773 | zUuid); |
| 2774 | } |
| 2775 | } |
| 2776 | zMime = mimetype_from_name(blob_str(&downloadName)); |
| 2777 | if( zMime ){ |
| 2778 | if( fossil_strcmp(zMime, "text/html")==0 ){ |
| 2779 | if( asText ){ |
| @@ -2348,24 +2789,38 @@ | |
| 2789 | }else{ |
| 2790 | renderAsWiki = 1; |
| 2791 | style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); |
| 2792 | } |
| 2793 | } |
| 2794 | if( !bLocalMode ){ /* This way madness lies... */ |
| 2795 | if( fileedit_is_editable(zName) ){ |
| 2796 | style_submenu_element("Edit", |
| 2797 | "%R/fileedit?filename=%T&checkin=%!S", |
| 2798 | zName, zCI); |
| 2799 | } |
| 2800 | } |
| 2801 | } |
| 2802 | if( (objType & (OBJTYPE_WIKI|OBJTYPE_TICKET))!=0 ){ |
| 2803 | style_submenu_element("Parsed", "%R/info/%s", zUuid); |
| 2804 | } |
| 2805 | if( descOnly ){ |
| 2806 | style_submenu_element("Content", "%R/artifact/%s", zUuid); |
| 2807 | }else{ |
| 2808 | @ <hr /> |
| 2809 | if( bLocalMode ){ |
| 2810 | /*TODO |
| 2811 | ** Should we handle non-existent local files differently? Currently, |
| 2812 | ** they are shown the same as if the file was present but empty. This |
| 2813 | ** should never happen through "normal" operation, but someone might |
| 2814 | ** craft a link to one. Perhaps have content_from_file() perform an |
| 2815 | ** existence-check (rather than relying on blob_read_from_file() which |
| 2816 | ** it calls returning an empty blob)? |
| 2817 | */ |
| 2818 | content_from_file(zName, &content); |
| 2819 | }else{ |
| 2820 | content_get(rid, &content); |
| 2821 | } |
| 2822 | if( renderAsWiki ){ |
| 2823 | wiki_render_by_mimetype(&content, zMime); |
| 2824 | }else if( renderAsHtml ){ |
| 2825 | @ <iframe src="%R/raw/%s(zUuid)" |
| 2826 | @ width="100%%" frameborder="0" marginwidth="0" marginheight="0" |
| @@ -2377,11 +2832,15 @@ | |
| 2832 | @ this.height=this.contentDocument.documentElement.scrollHeight + 75; |
| 2833 | @ } |
| 2834 | @ ); |
| 2835 | @ </script> |
| 2836 | }else{ |
| 2837 | if( bLocalMode ){ |
| 2838 | style_submenu_element("Hex", "%R/hexdump?local=%s", zName); |
| 2839 | }else{ |
| 2840 | style_submenu_element("Hex", "%s/hexdump?name=%s", g.zTop, zUuid); |
| 2841 | } |
| 2842 | if( zLn==0 || atoi(zLn)==0 ){ |
| 2843 | style_submenu_checkbox("ln", "Line Numbers", 0, 0); |
| 2844 | } |
| 2845 | blob_to_utf8_no_bom(&content, 0); |
| 2846 | zMime = mimetype_from_content(&content); |
| 2847 |