Fossil SCM
Merge trunk, since cloning local filesystem repositories still does not work
Commit
719dcd29cd8a99d592793211fe60f6c0d39a450087ab5aa440ed193372802a01
Parent
cbd52334efe299e…
99 files changed
+1
-1
+4
-1
+1
-1
+2
-2
+16
-16
+15
-2
+1
+1
-1
+2
-1
+5
-6
+1
-1
+459
-10
+2
-16
+8
-8
+5
-5
+3
-3
+9
-7
-1
+4
-1
+1
-1
+131
-19
+375
-21
+2
-2
+3
-3
+80
-8
+16
-11
+4
-1
+1
-1
+26
+221
-144
+2
-2
+7
-11
+1
-1
+33
-3
+41
-8
+3
+93
+23
-9
+8
+126
-104
+46
-10
+43
+65
+45
-8
+1
-1
+33
+1
-1
+1
-1
+1
-1
+104
-48
+2
+5
-4
+50
-4
+9
-13
+13
-5
+7
-1
+4
-1
+2
-2
-11
-6
-6
+9
-1
+1
-1
+58
-12
+1
-1
+2
+2
+54
-22
+1
-1
+9
+772
-531
+1
-1
-13
+28
-10
+69
-231
+47
-190
+185
+5
-4
+5
-4
+6
-2
+1
-1
+2
-2
+716
-236
-328
+2
-2
+6
-6
+1
-1
+5
-5
+1
+6
+4
-10
+9
-13
+15
-12
+19
-3
+1
-1
+1
-1
+10
-6
~
VERSION
~
skins/ardoise/css.txt
~
skins/eagle/css.txt
~
skins/xekri/css.txt
~
src/add.c
~
src/ajax.c
~
src/allrepo.c
~
src/attach.c
~
src/bisect.c
~
src/blob.c
~
src/branch.c
~
src/browse.c
~
src/builtin.c
~
src/bundle.c
~
src/cgi.c
~
src/checkin.c
~
src/checkout.c
~
src/clone.c
~
src/codecheck1.c
~
src/configure.c
~
src/content.c
~
src/db.c
~
src/default.css
~
src/descendants.c
~
src/diff.c
~
src/dispatch.c
~
src/doc.c
~
src/etag.c
~
src/export.c
~
src/file.c
~
src/fileedit.c
~
src/finfo.c
~
src/forum.c
~
src/forum.js
~
src/fossil.bootstrap.js
~
src/fossil.confirmer.js
~
src/fossil.copybutton.js
~
src/fossil.dom.js
~
src/fossil.fetch.js
~
src/fossil.numbered-lines.js
~
src/fossil.page.fileedit.js
~
src/fossil.page.forumpost.js
~
src/fossil.page.wikiedit.js
~
src/fossil.popupwidget.js
~
src/fossil.tabs.js
~
src/fusefs.c
~
src/glob.c
~
src/graph.js
~
src/hook.c
~
src/import.c
~
src/info.c
~
src/json_config.c
~
src/login.c
~
src/main.c
~
src/main.mk
~
src/makemake.tcl
~
src/mkbuiltin.c
~
src/mkindex.c
~
src/piechart.c
~
src/publish.c
~
src/purge.c
~
src/rebuild.c
~
src/rebuild.c
~
src/sbsdiff.js
~
src/scroll.js
~
src/setup.c
~
src/setupuser.c
~
src/sha1.c
~
src/sha3.c
~
src/shell.c
~
src/skins.c
~
src/sqlcmd.c
~
src/sqlite3.c
~
src/sqlite3.h
~
src/stash.c
~
src/stat.c
~
src/style.c
~
src/style.fileedit.css
~
src/style.wikiedit.css
~
src/sync.c
~
src/sync.c
~
src/timeline.c
~
src/undo.c
~
src/update.c
~
src/wiki.c
-
src/wysiwyg.c
~
src/xfer.c
~
test/merge1.test
~
test/merge3.test
~
test/merge4.test
~
test/tester.tcl
~
test/wiki.test
~
win/Makefile.dmc
~
win/Makefile.mingw
~
win/Makefile.msc
~
www/changes.wiki
~
www/fileedit-page.md
~
www/gitusers.md
~
www/server/openbsd/fastcgi.md
M
VERSION
+1
-1
| --- VERSION | ||
| +++ VERSION | ||
| @@ -1,1 +1,1 @@ | ||
| 1 | -2.12 | |
| 1 | +2.13 | |
| 2 | 2 |
| --- VERSION | |
| +++ VERSION | |
| @@ -1,1 +1,1 @@ | |
| 1 | 2.12 |
| 2 |
| --- VERSION | |
| +++ VERSION | |
| @@ -1,1 +1,1 @@ | |
| 1 | 2.13 |
| 2 |
+4
-1
| --- skins/ardoise/css.txt | ||
| +++ skins/ardoise/css.txt | ||
| @@ -497,11 +497,11 @@ | ||
| 497 | 497 | border: 0; |
| 498 | 498 | border-radius: 5px; |
| 499 | 499 | box-shadow: none; |
| 500 | 500 | box-sizing: border-box |
| 501 | 501 | } |
| 502 | -textarea { | |
| 502 | +textarea, select { | |
| 503 | 503 | height: initial; |
| 504 | 504 | } |
| 505 | 505 | input[type=email]:hover, |
| 506 | 506 | input[type=number]:hover, |
| 507 | 507 | input[type=password]:hover, |
| @@ -572,10 +572,13 @@ | ||
| 572 | 572 | white-space: nowrap; |
| 573 | 573 | background: #000; |
| 574 | 574 | border: 2px solid #bbb; |
| 575 | 575 | border-radius: 5px |
| 576 | 576 | } |
| 577 | +table.numbered-lines td.file-content > pre { | |
| 578 | + margin-top: -2px/*offset CODE tag border*/; | |
| 579 | +} | |
| 577 | 580 | pre > code { |
| 578 | 581 | padding: 1rem 1.5rem; |
| 579 | 582 | white-space: pre |
| 580 | 583 | } |
| 581 | 584 | td, |
| 582 | 585 |
| --- skins/ardoise/css.txt | |
| +++ skins/ardoise/css.txt | |
| @@ -497,11 +497,11 @@ | |
| 497 | border: 0; |
| 498 | border-radius: 5px; |
| 499 | box-shadow: none; |
| 500 | box-sizing: border-box |
| 501 | } |
| 502 | textarea { |
| 503 | height: initial; |
| 504 | } |
| 505 | input[type=email]:hover, |
| 506 | input[type=number]:hover, |
| 507 | input[type=password]:hover, |
| @@ -572,10 +572,13 @@ | |
| 572 | white-space: nowrap; |
| 573 | background: #000; |
| 574 | border: 2px solid #bbb; |
| 575 | border-radius: 5px |
| 576 | } |
| 577 | pre > code { |
| 578 | padding: 1rem 1.5rem; |
| 579 | white-space: pre |
| 580 | } |
| 581 | td, |
| 582 |
| --- skins/ardoise/css.txt | |
| +++ skins/ardoise/css.txt | |
| @@ -497,11 +497,11 @@ | |
| 497 | border: 0; |
| 498 | border-radius: 5px; |
| 499 | box-shadow: none; |
| 500 | box-sizing: border-box |
| 501 | } |
| 502 | textarea, select { |
| 503 | height: initial; |
| 504 | } |
| 505 | input[type=email]:hover, |
| 506 | input[type=number]:hover, |
| 507 | input[type=password]:hover, |
| @@ -572,10 +572,13 @@ | |
| 572 | white-space: nowrap; |
| 573 | background: #000; |
| 574 | border: 2px solid #bbb; |
| 575 | border-radius: 5px |
| 576 | } |
| 577 | table.numbered-lines td.file-content > pre { |
| 578 | margin-top: -2px/*offset CODE tag border*/; |
| 579 | } |
| 580 | pre > code { |
| 581 | padding: 1rem 1.5rem; |
| 582 | white-space: pre |
| 583 | } |
| 584 | td, |
| 585 |
+1
-1
| --- skins/eagle/css.txt | ||
| +++ skins/eagle/css.txt | ||
| @@ -399,11 +399,11 @@ | ||
| 399 | 399 | |
| 400 | 400 | div.filetreeline:hover { |
| 401 | 401 | background-color: #7EA2D9; |
| 402 | 402 | } |
| 403 | 403 | |
| 404 | -div.selectedText { | |
| 404 | +table.numbered-lines td.line-numbers span.selected-line { | |
| 405 | 405 | background-color: #7EA2D9; |
| 406 | 406 | } |
| 407 | 407 | |
| 408 | 408 | .statistics-report-graph-line { |
| 409 | 409 | background-color: #7EA2D9; |
| 410 | 410 |
| --- skins/eagle/css.txt | |
| +++ skins/eagle/css.txt | |
| @@ -399,11 +399,11 @@ | |
| 399 | |
| 400 | div.filetreeline:hover { |
| 401 | background-color: #7EA2D9; |
| 402 | } |
| 403 | |
| 404 | div.selectedText { |
| 405 | background-color: #7EA2D9; |
| 406 | } |
| 407 | |
| 408 | .statistics-report-graph-line { |
| 409 | background-color: #7EA2D9; |
| 410 |
| --- skins/eagle/css.txt | |
| +++ skins/eagle/css.txt | |
| @@ -399,11 +399,11 @@ | |
| 399 | |
| 400 | div.filetreeline:hover { |
| 401 | background-color: #7EA2D9; |
| 402 | } |
| 403 | |
| 404 | table.numbered-lines td.line-numbers span.selected-line { |
| 405 | background-color: #7EA2D9; |
| 406 | } |
| 407 | |
| 408 | .statistics-report-graph-line { |
| 409 | background-color: #7EA2D9; |
| 410 |
+2
-2
| --- skins/xekri/css.txt | ||
| +++ skins/xekri/css.txt | ||
| @@ -1000,15 +1000,15 @@ | ||
| 1000 | 1000 | /************************************** |
| 1001 | 1001 | * Did not encounter these |
| 1002 | 1002 | */ |
| 1003 | 1003 | |
| 1004 | 1004 | /* selected lines of text within a linenumbered artifact display */ |
| 1005 | -div.selectedText { | |
| 1005 | +table.numbered-lines td.line-numbers span.selected-line { | |
| 1006 | 1006 | font-weight: bold; |
| 1007 | 1007 | color: #00f; |
| 1008 | 1008 | background-color: #d5d5ff; |
| 1009 | - border: 1px #00f solid; | |
| 1009 | + border-color: #00f; | |
| 1010 | 1010 | } |
| 1011 | 1011 | |
| 1012 | 1012 | /* format for missing privileges note on user setup page */ |
| 1013 | 1013 | p.missingPriv { |
| 1014 | 1014 | color: #00f; |
| 1015 | 1015 |
| --- skins/xekri/css.txt | |
| +++ skins/xekri/css.txt | |
| @@ -1000,15 +1000,15 @@ | |
| 1000 | /************************************** |
| 1001 | * Did not encounter these |
| 1002 | */ |
| 1003 | |
| 1004 | /* selected lines of text within a linenumbered artifact display */ |
| 1005 | div.selectedText { |
| 1006 | font-weight: bold; |
| 1007 | color: #00f; |
| 1008 | background-color: #d5d5ff; |
| 1009 | border: 1px #00f solid; |
| 1010 | } |
| 1011 | |
| 1012 | /* format for missing privileges note on user setup page */ |
| 1013 | p.missingPriv { |
| 1014 | color: #00f; |
| 1015 |
| --- skins/xekri/css.txt | |
| +++ skins/xekri/css.txt | |
| @@ -1000,15 +1000,15 @@ | |
| 1000 | /************************************** |
| 1001 | * Did not encounter these |
| 1002 | */ |
| 1003 | |
| 1004 | /* selected lines of text within a linenumbered artifact display */ |
| 1005 | table.numbered-lines td.line-numbers span.selected-line { |
| 1006 | font-weight: bold; |
| 1007 | color: #00f; |
| 1008 | background-color: #d5d5ff; |
| 1009 | border-color: #00f; |
| 1010 | } |
| 1011 | |
| 1012 | /* format for missing privileges note on user setup page */ |
| 1013 | p.missingPriv { |
| 1014 | color: #00f; |
| 1015 |
+16
-16
| --- src/add.c | ||
| +++ src/add.c | ||
| @@ -320,11 +320,11 @@ | ||
| 320 | 320 | ** COMMAND: add |
| 321 | 321 | ** |
| 322 | 322 | ** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...? |
| 323 | 323 | ** |
| 324 | 324 | ** Make arrangements to add one or more files or directories to the |
| 325 | -** current checkout at the next commit. | |
| 325 | +** current checkout at the next [[commit]]. | |
| 326 | 326 | ** |
| 327 | 327 | ** When adding files or directories recursively, filenames that begin |
| 328 | 328 | ** with "." are excluded by default. To include such files, add |
| 329 | 329 | ** the "--dotfiles" option to the command-line. |
| 330 | 330 | ** |
| @@ -359,11 +359,11 @@ | ||
| 359 | 359 | ** |
| 360 | 360 | ** The following options are only valid with --reset: |
| 361 | 361 | ** -v|--verbose Outputs information about each --reset file. |
| 362 | 362 | ** -n|--dry-run Display instead of run actions. |
| 363 | 363 | ** |
| 364 | -** See also: addremove, rm | |
| 364 | +** See also: [[addremove]], [[rm]] | |
| 365 | 365 | */ |
| 366 | 366 | void add_cmd(void){ |
| 367 | 367 | int i; /* Loop counter */ |
| 368 | 368 | int vid; /* Currently checked out version */ |
| 369 | 369 | int nRoot; /* Full path characters in g.zLocalRoot */ |
| @@ -541,11 +541,11 @@ | ||
| 541 | 541 | ** than --verbose or --dry-run may be used with |
| 542 | 542 | ** --reset. |
| 543 | 543 | ** --verbose|-v Outputs information about each --reset file. |
| 544 | 544 | ** Only usable with --reset. |
| 545 | 545 | ** |
| 546 | -** See also: addremove, add | |
| 546 | +** See also: [[addremove]], [[add]] | |
| 547 | 547 | */ |
| 548 | 548 | void delete_cmd(void){ |
| 549 | 549 | int i; |
| 550 | 550 | int removeFiles; |
| 551 | 551 | int dryRunFlag = find_option("dry-run","n",0)!=0; |
| @@ -689,22 +689,22 @@ | ||
| 689 | 689 | /* |
| 690 | 690 | ** COMMAND: addremove |
| 691 | 691 | ** |
| 692 | 692 | ** Usage: %fossil addremove ?OPTIONS? |
| 693 | 693 | ** |
| 694 | -** Do all necessary "add" and "rm" commands to synchronize the repository | |
| 695 | -** with the content of the working checkout: | |
| 694 | +** Do all necessary "[[add]]" and "[[rm]]" commands to synchronize the | |
| 695 | +** repository with the content of the working checkout: | |
| 696 | 696 | ** |
| 697 | 697 | ** * All files in the checkout but not in the repository (that is, |
| 698 | 698 | ** all files displayed using the "extras" command) are added as |
| 699 | -** if by the "add" command. | |
| 699 | +** if by the "[[add]]" command. | |
| 700 | 700 | ** |
| 701 | 701 | ** * All files in the repository but missing from the checkout (that is, |
| 702 | 702 | ** all files that show as MISSING with the "status" command) are |
| 703 | -** removed as if by the "rm" command. | |
| 703 | +** removed as if by the "[[rm]]" command. | |
| 704 | 704 | ** |
| 705 | -** The command does not "commit". You must run the "commit" separately | |
| 705 | +** The command does not "[[commit]]". You must run the "[[commit]]" separately | |
| 706 | 706 | ** as a separate step. |
| 707 | 707 | ** |
| 708 | 708 | ** Files and directories whose names begin with "." are ignored unless |
| 709 | 709 | ** the --dotfiles option is used. |
| 710 | 710 | ** |
| @@ -733,11 +733,11 @@ | ||
| 733 | 733 | ** removed. No flags other than --verbose and |
| 734 | 734 | ** --dry-run may be used with --reset. |
| 735 | 735 | ** --verbose|-v Outputs information about each --reset file. |
| 736 | 736 | ** Only usable with --reset. |
| 737 | 737 | ** |
| 738 | -** See also: add, rm | |
| 738 | +** See also: [[add]], [[rm]] | |
| 739 | 739 | */ |
| 740 | 740 | void addremove_cmd(void){ |
| 741 | 741 | Blob path; |
| 742 | 742 | const char *zCleanFlag; |
| 743 | 743 | const char *zIgnoreFlag; |
| @@ -953,11 +953,11 @@ | ||
| 953 | 953 | ** Move or rename one or more files or directories within the repository tree. |
| 954 | 954 | ** You can either rename a file or directory or move it to another subdirectory. |
| 955 | 955 | ** |
| 956 | 956 | ** The 'mv' command does NOT normally rename or move the files on disk. |
| 957 | 957 | ** This command merely records the fact that file names have changed so |
| 958 | -** that appropriate notations can be made at the next commit/check-in. | |
| 958 | +** that appropriate notations can be made at the next [[commit]]. | |
| 959 | 959 | ** However, the default behavior of this command may be overridden via |
| 960 | 960 | ** command line options listed below and/or the 'mv-rm-files' setting. |
| 961 | 961 | ** |
| 962 | 962 | ** The 'rename' command never renames or moves files on disk, even when the |
| 963 | 963 | ** command line options and/or the 'mv-rm-files' setting would otherwise |
| @@ -966,17 +966,17 @@ | ||
| 966 | 966 | ** WARNING: If the "--hard" option is specified -OR- the "mv-rm-files" |
| 967 | 967 | ** setting is non-zero, files WILL BE renamed or moved on disk |
| 968 | 968 | ** as well. This does NOT apply to the 'rename' command. |
| 969 | 969 | ** |
| 970 | 970 | ** Options: |
| 971 | -** --soft Skip moving files within the checkout. | |
| 972 | -** This supersedes the --hard option. | |
| 973 | -** --hard Move files within the checkout. | |
| 974 | -** --case-sensitive <BOOL> Override the case-sensitive setting. | |
| 975 | -** -n|--dry-run If given, display instead of run actions. | |
| 971 | +** --soft Skip moving files within the checkout. | |
| 972 | +** This supersedes the --hard option. | |
| 973 | +** --hard Move files within the checkout. | |
| 974 | +** --case-sensitive <BOOL> Override the case-sensitive setting. | |
| 975 | +** -n|--dry-run If given, display instead of run actions. | |
| 976 | 976 | ** |
| 977 | -** See also: changes, status | |
| 977 | +** See also: [[changes]], [[status]] | |
| 978 | 978 | */ |
| 979 | 979 | void mv_cmd(void){ |
| 980 | 980 | int i; |
| 981 | 981 | int vid; |
| 982 | 982 | int moveFiles; |
| 983 | 983 |
| --- src/add.c | |
| +++ src/add.c | |
| @@ -320,11 +320,11 @@ | |
| 320 | ** COMMAND: add |
| 321 | ** |
| 322 | ** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...? |
| 323 | ** |
| 324 | ** Make arrangements to add one or more files or directories to the |
| 325 | ** current checkout at the next commit. |
| 326 | ** |
| 327 | ** When adding files or directories recursively, filenames that begin |
| 328 | ** with "." are excluded by default. To include such files, add |
| 329 | ** the "--dotfiles" option to the command-line. |
| 330 | ** |
| @@ -359,11 +359,11 @@ | |
| 359 | ** |
| 360 | ** The following options are only valid with --reset: |
| 361 | ** -v|--verbose Outputs information about each --reset file. |
| 362 | ** -n|--dry-run Display instead of run actions. |
| 363 | ** |
| 364 | ** See also: addremove, rm |
| 365 | */ |
| 366 | void add_cmd(void){ |
| 367 | int i; /* Loop counter */ |
| 368 | int vid; /* Currently checked out version */ |
| 369 | int nRoot; /* Full path characters in g.zLocalRoot */ |
| @@ -541,11 +541,11 @@ | |
| 541 | ** than --verbose or --dry-run may be used with |
| 542 | ** --reset. |
| 543 | ** --verbose|-v Outputs information about each --reset file. |
| 544 | ** Only usable with --reset. |
| 545 | ** |
| 546 | ** See also: addremove, add |
| 547 | */ |
| 548 | void delete_cmd(void){ |
| 549 | int i; |
| 550 | int removeFiles; |
| 551 | int dryRunFlag = find_option("dry-run","n",0)!=0; |
| @@ -689,22 +689,22 @@ | |
| 689 | /* |
| 690 | ** COMMAND: addremove |
| 691 | ** |
| 692 | ** Usage: %fossil addremove ?OPTIONS? |
| 693 | ** |
| 694 | ** Do all necessary "add" and "rm" commands to synchronize the repository |
| 695 | ** with the content of the working checkout: |
| 696 | ** |
| 697 | ** * All files in the checkout but not in the repository (that is, |
| 698 | ** all files displayed using the "extras" command) are added as |
| 699 | ** if by the "add" command. |
| 700 | ** |
| 701 | ** * All files in the repository but missing from the checkout (that is, |
| 702 | ** all files that show as MISSING with the "status" command) are |
| 703 | ** removed as if by the "rm" command. |
| 704 | ** |
| 705 | ** The command does not "commit". You must run the "commit" separately |
| 706 | ** as a separate step. |
| 707 | ** |
| 708 | ** Files and directories whose names begin with "." are ignored unless |
| 709 | ** the --dotfiles option is used. |
| 710 | ** |
| @@ -733,11 +733,11 @@ | |
| 733 | ** removed. No flags other than --verbose and |
| 734 | ** --dry-run may be used with --reset. |
| 735 | ** --verbose|-v Outputs information about each --reset file. |
| 736 | ** Only usable with --reset. |
| 737 | ** |
| 738 | ** See also: add, rm |
| 739 | */ |
| 740 | void addremove_cmd(void){ |
| 741 | Blob path; |
| 742 | const char *zCleanFlag; |
| 743 | const char *zIgnoreFlag; |
| @@ -953,11 +953,11 @@ | |
| 953 | ** Move or rename one or more files or directories within the repository tree. |
| 954 | ** You can either rename a file or directory or move it to another subdirectory. |
| 955 | ** |
| 956 | ** The 'mv' command does NOT normally rename or move the files on disk. |
| 957 | ** This command merely records the fact that file names have changed so |
| 958 | ** that appropriate notations can be made at the next commit/check-in. |
| 959 | ** However, the default behavior of this command may be overridden via |
| 960 | ** command line options listed below and/or the 'mv-rm-files' setting. |
| 961 | ** |
| 962 | ** The 'rename' command never renames or moves files on disk, even when the |
| 963 | ** command line options and/or the 'mv-rm-files' setting would otherwise |
| @@ -966,17 +966,17 @@ | |
| 966 | ** WARNING: If the "--hard" option is specified -OR- the "mv-rm-files" |
| 967 | ** setting is non-zero, files WILL BE renamed or moved on disk |
| 968 | ** as well. This does NOT apply to the 'rename' command. |
| 969 | ** |
| 970 | ** Options: |
| 971 | ** --soft Skip moving files within the checkout. |
| 972 | ** This supersedes the --hard option. |
| 973 | ** --hard Move files within the checkout. |
| 974 | ** --case-sensitive <BOOL> Override the case-sensitive setting. |
| 975 | ** -n|--dry-run If given, display instead of run actions. |
| 976 | ** |
| 977 | ** See also: changes, status |
| 978 | */ |
| 979 | void mv_cmd(void){ |
| 980 | int i; |
| 981 | int vid; |
| 982 | int moveFiles; |
| 983 |
| --- src/add.c | |
| +++ src/add.c | |
| @@ -320,11 +320,11 @@ | |
| 320 | ** COMMAND: add |
| 321 | ** |
| 322 | ** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...? |
| 323 | ** |
| 324 | ** Make arrangements to add one or more files or directories to the |
| 325 | ** current checkout at the next [[commit]]. |
| 326 | ** |
| 327 | ** When adding files or directories recursively, filenames that begin |
| 328 | ** with "." are excluded by default. To include such files, add |
| 329 | ** the "--dotfiles" option to the command-line. |
| 330 | ** |
| @@ -359,11 +359,11 @@ | |
| 359 | ** |
| 360 | ** The following options are only valid with --reset: |
| 361 | ** -v|--verbose Outputs information about each --reset file. |
| 362 | ** -n|--dry-run Display instead of run actions. |
| 363 | ** |
| 364 | ** See also: [[addremove]], [[rm]] |
| 365 | */ |
| 366 | void add_cmd(void){ |
| 367 | int i; /* Loop counter */ |
| 368 | int vid; /* Currently checked out version */ |
| 369 | int nRoot; /* Full path characters in g.zLocalRoot */ |
| @@ -541,11 +541,11 @@ | |
| 541 | ** than --verbose or --dry-run may be used with |
| 542 | ** --reset. |
| 543 | ** --verbose|-v Outputs information about each --reset file. |
| 544 | ** Only usable with --reset. |
| 545 | ** |
| 546 | ** See also: [[addremove]], [[add]] |
| 547 | */ |
| 548 | void delete_cmd(void){ |
| 549 | int i; |
| 550 | int removeFiles; |
| 551 | int dryRunFlag = find_option("dry-run","n",0)!=0; |
| @@ -689,22 +689,22 @@ | |
| 689 | /* |
| 690 | ** COMMAND: addremove |
| 691 | ** |
| 692 | ** Usage: %fossil addremove ?OPTIONS? |
| 693 | ** |
| 694 | ** Do all necessary "[[add]]" and "[[rm]]" commands to synchronize the |
| 695 | ** repository with the content of the working checkout: |
| 696 | ** |
| 697 | ** * All files in the checkout but not in the repository (that is, |
| 698 | ** all files displayed using the "extras" command) are added as |
| 699 | ** if by the "[[add]]" command. |
| 700 | ** |
| 701 | ** * All files in the repository but missing from the checkout (that is, |
| 702 | ** all files that show as MISSING with the "status" command) are |
| 703 | ** removed as if by the "[[rm]]" command. |
| 704 | ** |
| 705 | ** The command does not "[[commit]]". You must run the "[[commit]]" separately |
| 706 | ** as a separate step. |
| 707 | ** |
| 708 | ** Files and directories whose names begin with "." are ignored unless |
| 709 | ** the --dotfiles option is used. |
| 710 | ** |
| @@ -733,11 +733,11 @@ | |
| 733 | ** removed. No flags other than --verbose and |
| 734 | ** --dry-run may be used with --reset. |
| 735 | ** --verbose|-v Outputs information about each --reset file. |
| 736 | ** Only usable with --reset. |
| 737 | ** |
| 738 | ** See also: [[add]], [[rm]] |
| 739 | */ |
| 740 | void addremove_cmd(void){ |
| 741 | Blob path; |
| 742 | const char *zCleanFlag; |
| 743 | const char *zIgnoreFlag; |
| @@ -953,11 +953,11 @@ | |
| 953 | ** Move or rename one or more files or directories within the repository tree. |
| 954 | ** You can either rename a file or directory or move it to another subdirectory. |
| 955 | ** |
| 956 | ** The 'mv' command does NOT normally rename or move the files on disk. |
| 957 | ** This command merely records the fact that file names have changed so |
| 958 | ** that appropriate notations can be made at the next [[commit]]. |
| 959 | ** However, the default behavior of this command may be overridden via |
| 960 | ** command line options listed below and/or the 'mv-rm-files' setting. |
| 961 | ** |
| 962 | ** The 'rename' command never renames or moves files on disk, even when the |
| 963 | ** command line options and/or the 'mv-rm-files' setting would otherwise |
| @@ -966,17 +966,17 @@ | |
| 966 | ** WARNING: If the "--hard" option is specified -OR- the "mv-rm-files" |
| 967 | ** setting is non-zero, files WILL BE renamed or moved on disk |
| 968 | ** as well. This does NOT apply to the 'rename' command. |
| 969 | ** |
| 970 | ** Options: |
| 971 | ** --soft Skip moving files within the checkout. |
| 972 | ** This supersedes the --hard option. |
| 973 | ** --hard Move files within the checkout. |
| 974 | ** --case-sensitive <BOOL> Override the case-sensitive setting. |
| 975 | ** -n|--dry-run If given, display instead of run actions. |
| 976 | ** |
| 977 | ** See also: [[changes]], [[status]] |
| 978 | */ |
| 979 | void mv_cmd(void){ |
| 980 | int i; |
| 981 | int vid; |
| 982 | int moveFiles; |
| 983 |
+15
-2
| --- src/ajax.c | ||
| +++ src/ajax.c | ||
| @@ -130,11 +130,12 @@ | ||
| 130 | 130 | wiki_render_by_mimetype(pContent, zMime); |
| 131 | 131 | break; |
| 132 | 132 | default:{ |
| 133 | 133 | const char *zContent = blob_str(pContent); |
| 134 | 134 | if(AJAX_PREVIEW_LINE_NUMBERS & flags){ |
| 135 | - output_text_with_line_numbers(zContent, "on"); | |
| 135 | + output_text_with_line_numbers(zContent, blob_size(pContent), | |
| 136 | + zName, "on"); | |
| 136 | 137 | }else{ |
| 137 | 138 | const char *zExt = strrchr(zName,'.'); |
| 138 | 139 | if(zExt && zExt[1]){ |
| 139 | 140 | CX("<pre><code class='language-%s'>%h</code></pre>", |
| 140 | 141 | zExt+1, zContent); |
| @@ -163,10 +164,20 @@ | ||
| 163 | 164 | }else{ |
| 164 | 165 | CX("<pre class='udiff'>%b</pre>",&out); |
| 165 | 166 | } |
| 166 | 167 | blob_reset(&out); |
| 167 | 168 | } |
| 169 | + | |
| 170 | +/* | |
| 171 | +** Uses P(zKey) to fetch a CGI environment variable. If that var is | |
| 172 | +** NULL or starts with '0' or 'f' then this function returns false, | |
| 173 | +** else it returns true. | |
| 174 | +*/ | |
| 175 | +int ajax_p_bool(char const *zKey){ | |
| 176 | + const char * zVal = P(zKey); | |
| 177 | + return (!zVal || '0'==*zVal || 'f'==*zVal) ? 0 : 1; | |
| 178 | +} | |
| 168 | 179 | |
| 169 | 180 | /* |
| 170 | 181 | ** Helper for /ajax routes. Clears the CGI content buffer, sets an |
| 171 | 182 | ** HTTP error status code, and queues up a JSON response in the form |
| 172 | 183 | ** of an object: |
| @@ -323,10 +334,11 @@ | ||
| 323 | 334 | if(zRenderMode!=0){ |
| 324 | 335 | cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); |
| 325 | 336 | } |
| 326 | 337 | } |
| 327 | 338 | |
| 339 | +#if INTERFACE | |
| 328 | 340 | /* |
| 329 | 341 | ** Internal mapping of ajax sub-route names to various metadata. |
| 330 | 342 | */ |
| 331 | 343 | struct AjaxRoute { |
| 332 | 344 | const char *zName; /* Name part of the route after "ajax/" */ |
| @@ -334,16 +346,17 @@ | ||
| 334 | 346 | int bWriteMode; /* True if requires write mode */ |
| 335 | 347 | int bPost; /* True if requires POST (i.e. CSRF |
| 336 | 348 | ** verification) */ |
| 337 | 349 | }; |
| 338 | 350 | typedef struct AjaxRoute AjaxRoute; |
| 351 | +#endif /*INTERFACE*/ | |
| 339 | 352 | |
| 340 | 353 | /* |
| 341 | 354 | ** Comparison function for bsearch() for searching an AjaxRoute |
| 342 | 355 | ** list for a matching name. |
| 343 | 356 | */ |
| 344 | -static int cmp_ajax_route_name(const void *a, const void *b){ | |
| 357 | +int cmp_ajax_route_name(const void *a, const void *b){ | |
| 345 | 358 | const AjaxRoute * rA = (const AjaxRoute*)a; |
| 346 | 359 | const AjaxRoute * rB = (const AjaxRoute*)b; |
| 347 | 360 | return fossil_strcmp(rA->zName, rB->zName); |
| 348 | 361 | } |
| 349 | 362 | |
| 350 | 363 |
| --- src/ajax.c | |
| +++ src/ajax.c | |
| @@ -130,11 +130,12 @@ | |
| 130 | wiki_render_by_mimetype(pContent, zMime); |
| 131 | break; |
| 132 | default:{ |
| 133 | const char *zContent = blob_str(pContent); |
| 134 | if(AJAX_PREVIEW_LINE_NUMBERS & flags){ |
| 135 | output_text_with_line_numbers(zContent, "on"); |
| 136 | }else{ |
| 137 | const char *zExt = strrchr(zName,'.'); |
| 138 | if(zExt && zExt[1]){ |
| 139 | CX("<pre><code class='language-%s'>%h</code></pre>", |
| 140 | zExt+1, zContent); |
| @@ -163,10 +164,20 @@ | |
| 163 | }else{ |
| 164 | CX("<pre class='udiff'>%b</pre>",&out); |
| 165 | } |
| 166 | blob_reset(&out); |
| 167 | } |
| 168 | |
| 169 | /* |
| 170 | ** Helper for /ajax routes. Clears the CGI content buffer, sets an |
| 171 | ** HTTP error status code, and queues up a JSON response in the form |
| 172 | ** of an object: |
| @@ -323,10 +334,11 @@ | |
| 323 | if(zRenderMode!=0){ |
| 324 | cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | /* |
| 329 | ** Internal mapping of ajax sub-route names to various metadata. |
| 330 | */ |
| 331 | struct AjaxRoute { |
| 332 | const char *zName; /* Name part of the route after "ajax/" */ |
| @@ -334,16 +346,17 @@ | |
| 334 | int bWriteMode; /* True if requires write mode */ |
| 335 | int bPost; /* True if requires POST (i.e. CSRF |
| 336 | ** verification) */ |
| 337 | }; |
| 338 | typedef struct AjaxRoute AjaxRoute; |
| 339 | |
| 340 | /* |
| 341 | ** Comparison function for bsearch() for searching an AjaxRoute |
| 342 | ** list for a matching name. |
| 343 | */ |
| 344 | static int cmp_ajax_route_name(const void *a, const void *b){ |
| 345 | const AjaxRoute * rA = (const AjaxRoute*)a; |
| 346 | const AjaxRoute * rB = (const AjaxRoute*)b; |
| 347 | return fossil_strcmp(rA->zName, rB->zName); |
| 348 | } |
| 349 | |
| 350 |
| --- src/ajax.c | |
| +++ src/ajax.c | |
| @@ -130,11 +130,12 @@ | |
| 130 | wiki_render_by_mimetype(pContent, zMime); |
| 131 | break; |
| 132 | default:{ |
| 133 | const char *zContent = blob_str(pContent); |
| 134 | if(AJAX_PREVIEW_LINE_NUMBERS & flags){ |
| 135 | output_text_with_line_numbers(zContent, blob_size(pContent), |
| 136 | zName, "on"); |
| 137 | }else{ |
| 138 | const char *zExt = strrchr(zName,'.'); |
| 139 | if(zExt && zExt[1]){ |
| 140 | CX("<pre><code class='language-%s'>%h</code></pre>", |
| 141 | zExt+1, zContent); |
| @@ -163,10 +164,20 @@ | |
| 164 | }else{ |
| 165 | CX("<pre class='udiff'>%b</pre>",&out); |
| 166 | } |
| 167 | blob_reset(&out); |
| 168 | } |
| 169 | |
| 170 | /* |
| 171 | ** Uses P(zKey) to fetch a CGI environment variable. If that var is |
| 172 | ** NULL or starts with '0' or 'f' then this function returns false, |
| 173 | ** else it returns true. |
| 174 | */ |
| 175 | int ajax_p_bool(char const *zKey){ |
| 176 | const char * zVal = P(zKey); |
| 177 | return (!zVal || '0'==*zVal || 'f'==*zVal) ? 0 : 1; |
| 178 | } |
| 179 | |
| 180 | /* |
| 181 | ** Helper for /ajax routes. Clears the CGI content buffer, sets an |
| 182 | ** HTTP error status code, and queues up a JSON response in the form |
| 183 | ** of an object: |
| @@ -323,10 +334,11 @@ | |
| 334 | if(zRenderMode!=0){ |
| 335 | cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | #if INTERFACE |
| 340 | /* |
| 341 | ** Internal mapping of ajax sub-route names to various metadata. |
| 342 | */ |
| 343 | struct AjaxRoute { |
| 344 | const char *zName; /* Name part of the route after "ajax/" */ |
| @@ -334,16 +346,17 @@ | |
| 346 | int bWriteMode; /* True if requires write mode */ |
| 347 | int bPost; /* True if requires POST (i.e. CSRF |
| 348 | ** verification) */ |
| 349 | }; |
| 350 | typedef struct AjaxRoute AjaxRoute; |
| 351 | #endif /*INTERFACE*/ |
| 352 | |
| 353 | /* |
| 354 | ** Comparison function for bsearch() for searching an AjaxRoute |
| 355 | ** list for a matching name. |
| 356 | */ |
| 357 | int cmp_ajax_route_name(const void *a, const void *b){ |
| 358 | const AjaxRoute * rA = (const AjaxRoute*)a; |
| 359 | const AjaxRoute * rB = (const AjaxRoute*)b; |
| 360 | return fossil_strcmp(rA->zName, rB->zName); |
| 361 | } |
| 362 | |
| 363 |
+1
| --- src/allrepo.c | ||
| +++ src/allrepo.c | ||
| @@ -219,10 +219,11 @@ | ||
| 219 | 219 | zCmd = "dbstat --omit-version-info -R"; |
| 220 | 220 | showLabel = 1; |
| 221 | 221 | quiet = 1; |
| 222 | 222 | collect_argument(&extra, "brief", "b"); |
| 223 | 223 | collect_argument(&extra, "db-check", 0); |
| 224 | + collect_argument(&extra, "db-verify", 0); | |
| 224 | 225 | }else if( strncmp(zCmd, "extras", n)==0 ){ |
| 225 | 226 | if( showFile ){ |
| 226 | 227 | zCmd = "extras --chdir"; |
| 227 | 228 | }else{ |
| 228 | 229 | zCmd = "extras --header --chdir"; |
| 229 | 230 |
| --- src/allrepo.c | |
| +++ src/allrepo.c | |
| @@ -219,10 +219,11 @@ | |
| 219 | zCmd = "dbstat --omit-version-info -R"; |
| 220 | showLabel = 1; |
| 221 | quiet = 1; |
| 222 | collect_argument(&extra, "brief", "b"); |
| 223 | collect_argument(&extra, "db-check", 0); |
| 224 | }else if( strncmp(zCmd, "extras", n)==0 ){ |
| 225 | if( showFile ){ |
| 226 | zCmd = "extras --chdir"; |
| 227 | }else{ |
| 228 | zCmd = "extras --header --chdir"; |
| 229 |
| --- src/allrepo.c | |
| +++ src/allrepo.c | |
| @@ -219,10 +219,11 @@ | |
| 219 | zCmd = "dbstat --omit-version-info -R"; |
| 220 | showLabel = 1; |
| 221 | quiet = 1; |
| 222 | collect_argument(&extra, "brief", "b"); |
| 223 | collect_argument(&extra, "db-check", 0); |
| 224 | collect_argument(&extra, "db-verify", 0); |
| 225 | }else if( strncmp(zCmd, "extras", n)==0 ){ |
| 226 | if( showFile ){ |
| 227 | zCmd = "extras --chdir"; |
| 228 | }else{ |
| 229 | zCmd = "extras --header --chdir"; |
| 230 |
+1
-1
| --- src/attach.c | ||
| +++ src/attach.c | ||
| @@ -617,11 +617,11 @@ | ||
| 617 | 617 | const char *z; |
| 618 | 618 | content_get(ridSrc, &attach); |
| 619 | 619 | blob_to_utf8_no_bom(&attach, 0); |
| 620 | 620 | z = blob_str(&attach); |
| 621 | 621 | if( zLn ){ |
| 622 | - output_text_with_line_numbers(z, zLn); | |
| 622 | + output_text_with_line_numbers(z, blob_size(&attach), zName, zLn); | |
| 623 | 623 | }else{ |
| 624 | 624 | @ <pre> |
| 625 | 625 | @ %h(z) |
| 626 | 626 | @ </pre> |
| 627 | 627 | } |
| 628 | 628 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -617,11 +617,11 @@ | |
| 617 | const char *z; |
| 618 | content_get(ridSrc, &attach); |
| 619 | blob_to_utf8_no_bom(&attach, 0); |
| 620 | z = blob_str(&attach); |
| 621 | if( zLn ){ |
| 622 | output_text_with_line_numbers(z, zLn); |
| 623 | }else{ |
| 624 | @ <pre> |
| 625 | @ %h(z) |
| 626 | @ </pre> |
| 627 | } |
| 628 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -617,11 +617,11 @@ | |
| 617 | const char *z; |
| 618 | content_get(ridSrc, &attach); |
| 619 | blob_to_utf8_no_bom(&attach, 0); |
| 620 | z = blob_str(&attach); |
| 621 | if( zLn ){ |
| 622 | output_text_with_line_numbers(z, blob_size(&attach), zName, zLn); |
| 623 | }else{ |
| 624 | @ <pre> |
| 625 | @ %h(z) |
| 626 | @ </pre> |
| 627 | } |
| 628 |
+2
-1
| --- src/bisect.c | ||
| +++ src/bisect.c | ||
| @@ -381,11 +381,12 @@ | ||
| 381 | 381 | /* |
| 382 | 382 | ** COMMAND: bisect |
| 383 | 383 | ** |
| 384 | 384 | ** Usage: %fossil bisect SUBCOMMAND ... |
| 385 | 385 | ** |
| 386 | -** Run various subcommands useful for searching for bugs. | |
| 386 | +** Run various subcommands useful for searching back through the change | |
| 387 | +** history for a particular checkin that causes or fixes a problem. | |
| 387 | 388 | ** |
| 388 | 389 | ** > fossil bisect bad ?VERSION? |
| 389 | 390 | ** |
| 390 | 391 | ** Identify version VERSION as non-working. If VERSION is omitted, |
| 391 | 392 | ** the current checkout is marked as non-working. |
| 392 | 393 |
| --- src/bisect.c | |
| +++ src/bisect.c | |
| @@ -381,11 +381,12 @@ | |
| 381 | /* |
| 382 | ** COMMAND: bisect |
| 383 | ** |
| 384 | ** Usage: %fossil bisect SUBCOMMAND ... |
| 385 | ** |
| 386 | ** Run various subcommands useful for searching for bugs. |
| 387 | ** |
| 388 | ** > fossil bisect bad ?VERSION? |
| 389 | ** |
| 390 | ** Identify version VERSION as non-working. If VERSION is omitted, |
| 391 | ** the current checkout is marked as non-working. |
| 392 |
| --- src/bisect.c | |
| +++ src/bisect.c | |
| @@ -381,11 +381,12 @@ | |
| 381 | /* |
| 382 | ** COMMAND: bisect |
| 383 | ** |
| 384 | ** Usage: %fossil bisect SUBCOMMAND ... |
| 385 | ** |
| 386 | ** Run various subcommands useful for searching back through the change |
| 387 | ** history for a particular checkin that causes or fixes a problem. |
| 388 | ** |
| 389 | ** > fossil bisect bad ?VERSION? |
| 390 | ** |
| 391 | ** Identify version VERSION as non-working. If VERSION is omitted, |
| 392 | ** the current checkout is marked as non-working. |
| 393 |
+5
| --- src/blob.c | ||
| +++ src/blob.c | ||
| @@ -487,10 +487,15 @@ | ||
| 487 | 487 | ** |
| 488 | 488 | ** For semantic compatibility with blob_append_full(), if newSize is |
| 489 | 489 | ** >=0x7fff000 (~2GB) then this function will trigger blob_panic(). If |
| 490 | 490 | ** it didn't, it would be possible to bypass that hard-coded limit via |
| 491 | 491 | ** this function. |
| 492 | +** | |
| 493 | +** We've had at least one report: | |
| 494 | +** https://fossil-scm.org/forum/forumpost/b7bbd28db4 | |
| 495 | +** which implies that this is unconditionally failing on mingw 32-bit | |
| 496 | +** builds. | |
| 492 | 497 | */ |
| 493 | 498 | void blob_reserve(Blob *pBlob, unsigned int newSize){ |
| 494 | 499 | if(newSize>=0x7fff0000 ){ |
| 495 | 500 | blob_panic(); |
| 496 | 501 | }else if(newSize>pBlob->nUsed){ |
| 497 | 502 |
| --- src/blob.c | |
| +++ src/blob.c | |
| @@ -487,10 +487,15 @@ | |
| 487 | ** |
| 488 | ** For semantic compatibility with blob_append_full(), if newSize is |
| 489 | ** >=0x7fff000 (~2GB) then this function will trigger blob_panic(). If |
| 490 | ** it didn't, it would be possible to bypass that hard-coded limit via |
| 491 | ** this function. |
| 492 | */ |
| 493 | void blob_reserve(Blob *pBlob, unsigned int newSize){ |
| 494 | if(newSize>=0x7fff0000 ){ |
| 495 | blob_panic(); |
| 496 | }else if(newSize>pBlob->nUsed){ |
| 497 |
| --- src/blob.c | |
| +++ src/blob.c | |
| @@ -487,10 +487,15 @@ | |
| 487 | ** |
| 488 | ** For semantic compatibility with blob_append_full(), if newSize is |
| 489 | ** >=0x7fff000 (~2GB) then this function will trigger blob_panic(). If |
| 490 | ** it didn't, it would be possible to bypass that hard-coded limit via |
| 491 | ** this function. |
| 492 | ** |
| 493 | ** We've had at least one report: |
| 494 | ** https://fossil-scm.org/forum/forumpost/b7bbd28db4 |
| 495 | ** which implies that this is unconditionally failing on mingw 32-bit |
| 496 | ** builds. |
| 497 | */ |
| 498 | void blob_reserve(Blob *pBlob, unsigned int newSize){ |
| 499 | if(newSize>=0x7fff0000 ){ |
| 500 | blob_panic(); |
| 501 | }else if(newSize>pBlob->nUsed){ |
| 502 |
-6
| --- src/branch.c | ||
| +++ src/branch.c | ||
| @@ -382,16 +382,10 @@ | ||
| 382 | 382 | ** Either no timezone suffix or "Z" means UTC. |
| 383 | 383 | ** |
| 384 | 384 | ** Options valid for all subcommands: |
| 385 | 385 | ** |
| 386 | 386 | ** -R|--repository FILE Run commands on repository FILE |
| 387 | -** | |
| 388 | -** Summary: | |
| 389 | -** fossil branch current | |
| 390 | -** fossil branch info BRANCH-NAME | |
| 391 | -** fossil branch [list|ls] | |
| 392 | -** fossil branch new BRANCH-NAME BASIS | |
| 393 | 387 | */ |
| 394 | 388 | void branch_cmd(void){ |
| 395 | 389 | int n; |
| 396 | 390 | const char *zCmd = "list"; |
| 397 | 391 | db_find_and_open_repository(0, 0); |
| 398 | 392 |
| --- src/branch.c | |
| +++ src/branch.c | |
| @@ -382,16 +382,10 @@ | |
| 382 | ** Either no timezone suffix or "Z" means UTC. |
| 383 | ** |
| 384 | ** Options valid for all subcommands: |
| 385 | ** |
| 386 | ** -R|--repository FILE Run commands on repository FILE |
| 387 | ** |
| 388 | ** Summary: |
| 389 | ** fossil branch current |
| 390 | ** fossil branch info BRANCH-NAME |
| 391 | ** fossil branch [list|ls] |
| 392 | ** fossil branch new BRANCH-NAME BASIS |
| 393 | */ |
| 394 | void branch_cmd(void){ |
| 395 | int n; |
| 396 | const char *zCmd = "list"; |
| 397 | db_find_and_open_repository(0, 0); |
| 398 |
| --- src/branch.c | |
| +++ src/branch.c | |
| @@ -382,16 +382,10 @@ | |
| 382 | ** Either no timezone suffix or "Z" means UTC. |
| 383 | ** |
| 384 | ** Options valid for all subcommands: |
| 385 | ** |
| 386 | ** -R|--repository FILE Run commands on repository FILE |
| 387 | */ |
| 388 | void branch_cmd(void){ |
| 389 | int n; |
| 390 | const char *zCmd = "list"; |
| 391 | db_find_and_open_repository(0, 0); |
| 392 |
+1
-1
| --- src/browse.c | ||
| +++ src/browse.c | ||
| @@ -911,11 +911,11 @@ | ||
| 911 | 911 | } |
| 912 | 912 | } |
| 913 | 913 | } |
| 914 | 914 | @ </ul> |
| 915 | 915 | @ </ul></div> |
| 916 | - style_load_one_js_file("tree.js"); | |
| 916 | + builtin_request_js("tree.js"); | |
| 917 | 917 | style_footer(); |
| 918 | 918 | |
| 919 | 919 | /* We could free memory used by sTree here if we needed to. But |
| 920 | 920 | ** the process is about to exit, so doing so would not really accomplish |
| 921 | 921 | ** anything useful. */ |
| 922 | 922 |
| --- src/browse.c | |
| +++ src/browse.c | |
| @@ -911,11 +911,11 @@ | |
| 911 | } |
| 912 | } |
| 913 | } |
| 914 | @ </ul> |
| 915 | @ </ul></div> |
| 916 | style_load_one_js_file("tree.js"); |
| 917 | style_footer(); |
| 918 | |
| 919 | /* We could free memory used by sTree here if we needed to. But |
| 920 | ** the process is about to exit, so doing so would not really accomplish |
| 921 | ** anything useful. */ |
| 922 |
| --- src/browse.c | |
| +++ src/browse.c | |
| @@ -911,11 +911,11 @@ | |
| 911 | } |
| 912 | } |
| 913 | } |
| 914 | @ </ul> |
| 915 | @ </ul></div> |
| 916 | builtin_request_js("tree.js"); |
| 917 | style_footer(); |
| 918 | |
| 919 | /* We could free memory used by sTree here if we needed to. But |
| 920 | ** the process is about to exit, so doing so would not really accomplish |
| 921 | ** anything useful. */ |
| 922 |
+459
-10
| --- src/builtin.c | ||
| +++ src/builtin.c | ||
| @@ -28,13 +28,15 @@ | ||
| 28 | 28 | ** builtin_data.h file. Include that information here: |
| 29 | 29 | */ |
| 30 | 30 | #include "builtin_data.h" |
| 31 | 31 | |
| 32 | 32 | /* |
| 33 | -** Return a pointer to built-in content | |
| 33 | +** Return the index in the aBuiltinFiles[] array for the file | |
| 34 | +** whose name is zFilename. Or return -1 if the file is not | |
| 35 | +** found. | |
| 34 | 36 | */ |
| 35 | -const unsigned char *builtin_file(const char *zFilename, int *piSize){ | |
| 37 | +static int builtin_file_index(const char *zFilename){ | |
| 36 | 38 | int lwr, upr, i, c; |
| 37 | 39 | lwr = 0; |
| 38 | 40 | upr = count(aBuiltinFiles) - 1; |
| 39 | 41 | while( upr>=lwr ){ |
| 40 | 42 | i = (upr+lwr)/2; |
| @@ -42,16 +44,28 @@ | ||
| 42 | 44 | if( c<0 ){ |
| 43 | 45 | lwr = i+1; |
| 44 | 46 | }else if( c>0 ){ |
| 45 | 47 | upr = i-1; |
| 46 | 48 | }else{ |
| 47 | - if( piSize ) *piSize = aBuiltinFiles[i].nByte; | |
| 48 | - return aBuiltinFiles[i].pData; | |
| 49 | + return i; | |
| 49 | 50 | } |
| 50 | 51 | } |
| 51 | - if( piSize ) *piSize = 0; | |
| 52 | - return 0; | |
| 52 | + return -1; | |
| 53 | +} | |
| 54 | + | |
| 55 | +/* | |
| 56 | +** Return a pointer to built-in content | |
| 57 | +*/ | |
| 58 | +const unsigned char *builtin_file(const char *zFilename, int *piSize){ | |
| 59 | + int i = builtin_file_index(zFilename); | |
| 60 | + if( i>=0 ){ | |
| 61 | + if( piSize ) *piSize = aBuiltinFiles[i].nByte; | |
| 62 | + return aBuiltinFiles[i].pData; | |
| 63 | + }else{ | |
| 64 | + if( piSize ) *piSize = 0; | |
| 65 | + return 0; | |
| 66 | + } | |
| 53 | 67 | } |
| 54 | 68 | const char *builtin_text(const char *zFilename){ |
| 55 | 69 | return (char*)builtin_file(zFilename, 0); |
| 56 | 70 | } |
| 57 | 71 | |
| @@ -65,11 +79,11 @@ | ||
| 65 | 79 | */ |
| 66 | 80 | void test_builtin_list(void){ |
| 67 | 81 | int i, size = 0;; |
| 68 | 82 | for(i=0; i<count(aBuiltinFiles); i++){ |
| 69 | 83 | const int n = aBuiltinFiles[i].nByte; |
| 70 | - fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,n); | |
| 84 | + fossil_print("%3d. %-45s %6d\n", i+1, aBuiltinFiles[i].zName,n); | |
| 71 | 85 | size += n; |
| 72 | 86 | } |
| 73 | 87 | if(find_option("verbose","v",0)!=0){ |
| 74 | 88 | fossil_print("%d entries totaling %d bytes\n", i, size); |
| 75 | 89 | } |
| @@ -81,16 +95,18 @@ | ||
| 81 | 95 | ** Show all built-in text files. |
| 82 | 96 | */ |
| 83 | 97 | void test_builtin_list_page(void){ |
| 84 | 98 | int i; |
| 85 | 99 | style_header("Built-in Text Files"); |
| 86 | - @ <ul> | |
| 100 | + @ <ol> | |
| 87 | 101 | for(i=0; i<count(aBuiltinFiles); i++){ |
| 88 | 102 | const char *z = aBuiltinFiles[i].zName; |
| 89 | - @ <li>%z(href("%R/builtin?name=%T&id=%S",z,MANIFEST_UUID))%h(z)</a> | |
| 103 | + char *zUrl = href("%R/builtin?name=%T&id=%.8s&mimetype=text/plain", | |
| 104 | + z,fossil_exe_id()); | |
| 105 | + @ <li>%z(zUrl)%h(z)</a> | |
| 90 | 106 | } |
| 91 | - @ </ul> | |
| 107 | + @ </ol> | |
| 92 | 108 | style_footer(); |
| 93 | 109 | } |
| 94 | 110 | |
| 95 | 111 | /* |
| 96 | 112 | ** COMMAND: test-builtin-get |
| @@ -110,5 +126,438 @@ | ||
| 110 | 126 | } |
| 111 | 127 | blob_init(&x, (const char*)pData, nByte); |
| 112 | 128 | blob_write_to_file(&x, g.argc==4 ? g.argv[3] : "-"); |
| 113 | 129 | blob_reset(&x); |
| 114 | 130 | } |
| 131 | + | |
| 132 | +/* | |
| 133 | +** Input zList is a list of numeric identifiers for files in | |
| 134 | +** aBuiltinFiles[]. Return the concatenation of all of those | |
| 135 | +** files using mimetype zType, or as application/javascript if | |
| 136 | +** zType is 0. | |
| 137 | +*/ | |
| 138 | +static void builtin_deliver_multiple_js_files( | |
| 139 | + const char *zList, /* List of numeric identifiers */ | |
| 140 | + const char *zType /* Override mimetype */ | |
| 141 | +){ | |
| 142 | + Blob *pOut; | |
| 143 | + if( zType==0 ) zType = "application/javascript"; | |
| 144 | + cgi_set_content_type(zType); | |
| 145 | + pOut = cgi_output_blob(); | |
| 146 | + while( zList[0] ){ | |
| 147 | + int i = atoi(zList); | |
| 148 | + if( i>0 && i<=count(aBuiltinFiles) ){ | |
| 149 | + blob_appendf(pOut, "/* %s */\n", aBuiltinFiles[i-1].zName); | |
| 150 | + blob_append(pOut, (const char*)aBuiltinFiles[i-1].pData, | |
| 151 | + aBuiltinFiles[i-1].nByte); | |
| 152 | + } | |
| 153 | + while( fossil_isdigit(zList[0]) ) zList++; | |
| 154 | + if( zList[0]==',' ) zList++; | |
| 155 | + } | |
| 156 | + return; | |
| 157 | +} | |
| 158 | + | |
| 159 | +/* | |
| 160 | +** WEBPAGE: builtin | |
| 161 | +** | |
| 162 | +** Return one of many built-in content files. Query parameters: | |
| 163 | +** | |
| 164 | +** name=FILENAME Return the single file whose name is FILENAME. | |
| 165 | +** mimetype=TYPE Override the mimetype in the returned file to | |
| 166 | +** be TYPE. If this query parameter is omitted | |
| 167 | +** (the usual case) then the mimetype is inferred | |
| 168 | +** from the suffix on FILENAME | |
| 169 | +** m=IDLIST IDLIST is a comma-separated list of integers | |
| 170 | +** that specify multiple javascript files to be | |
| 171 | +** concatenated and returned all at once. | |
| 172 | +** id=UNIQUEID Version number of the "builtin" files. Used | |
| 173 | +** for cache control only. | |
| 174 | +** | |
| 175 | +** At least one of the name= or m= query parameters must be present. | |
| 176 | +** | |
| 177 | +** If the id= query parameter is present, then Fossil assumes that the | |
| 178 | +** result is immutable and sets a very large cache retention time (1 year). | |
| 179 | +*/ | |
| 180 | +void builtin_webpage(void){ | |
| 181 | + Blob out; | |
| 182 | + const char *zName = P("name"); | |
| 183 | + const char *zTxt = 0; | |
| 184 | + const char *zId = P("id"); | |
| 185 | + const char *zType = P("mimetype"); | |
| 186 | + int nId; | |
| 187 | + if( zName ) zTxt = builtin_text(zName); | |
| 188 | + if( zTxt==0 ){ | |
| 189 | + const char *zM = P("m"); | |
| 190 | + if( zM ){ | |
| 191 | + if( zId && (nId = (int)strlen(zId))>=8 | |
| 192 | + && strncmp(zId,fossil_exe_id(),nId)==0 | |
| 193 | + ){ | |
| 194 | + g.isConst = 1; | |
| 195 | + } | |
| 196 | + etag_check(0,0); | |
| 197 | + builtin_deliver_multiple_js_files(zM, zType); | |
| 198 | + return; | |
| 199 | + } | |
| 200 | + cgi_set_status(404, "Not Found"); | |
| 201 | + @ File "%h(zName)" not found | |
| 202 | + return; | |
| 203 | + } | |
| 204 | + if( zType==0 ){ | |
| 205 | + if( sqlite3_strglob("*.js", zName)==0 ){ | |
| 206 | + zType = "application/javascript"; | |
| 207 | + }else{ | |
| 208 | + zType = mimetype_from_name(zName); | |
| 209 | + } | |
| 210 | + } | |
| 211 | + cgi_set_content_type(zType); | |
| 212 | + if( zId | |
| 213 | + && (nId = (int)strlen(zId))>=8 | |
| 214 | + && strncmp(zId,fossil_exe_id(),nId)==0 | |
| 215 | + ){ | |
| 216 | + g.isConst = 1; | |
| 217 | + } | |
| 218 | + etag_check(0,0); | |
| 219 | + blob_init(&out, zTxt, -1); | |
| 220 | + cgi_set_content(&out); | |
| 221 | +} | |
| 222 | + | |
| 223 | +/* Variables controlling the JS cache. | |
| 224 | +*/ | |
| 225 | +static struct { | |
| 226 | + int aReq[30]; /* Indexes of all requested built-in JS files */ | |
| 227 | + int nReq; /* Number of slots in aReq[] currently used */ | |
| 228 | + int nSent; /* Number of slots in aReq[] fulfilled */ | |
| 229 | + int eDelivery; /* Delivery mechanism */ | |
| 230 | +} builtin; | |
| 231 | + | |
| 232 | +#if INTERFACE | |
| 233 | +/* Various delivery mechanisms. The 0 option is the default. | |
| 234 | +*/ | |
| 235 | +#define JS_INLINE 0 /* inline, batched together at end of file */ | |
| 236 | +#define JS_SEPARATE 1 /* Separate HTTP request for each JS file */ | |
| 237 | +#define JS_BUNDLED 2 /* One HTTP request to load all JS files */ | |
| 238 | + /* concatenated together into a bundle */ | |
| 239 | +#endif /* INTERFACE */ | |
| 240 | + | |
| 241 | +/* | |
| 242 | +** The argument is a request to change the javascript delivery mode. | |
| 243 | +** The argument is a string which is a command-line option or CGI | |
| 244 | +** parameter. Try to match it against one of the delivery options | |
| 245 | +** and set things up accordingly. Throw an error if no match unless | |
| 246 | +** bSilent is true. | |
| 247 | +*/ | |
| 248 | +void builtin_set_js_delivery_mode(const char *zMode, int bSilent){ | |
| 249 | + if( zMode==0 ) return; | |
| 250 | + if( strcmp(zMode, "inline")==0 ){ | |
| 251 | + builtin.eDelivery = JS_INLINE; | |
| 252 | + }else | |
| 253 | + if( strcmp(zMode, "separate")==0 ){ | |
| 254 | + builtin.eDelivery = JS_SEPARATE; | |
| 255 | + }else | |
| 256 | + if( strcmp(zMode, "bundled")==0 ){ | |
| 257 | + builtin.eDelivery = JS_BUNDLED; | |
| 258 | + }else if( !bSilent ){ | |
| 259 | + fossil_fatal("unknown javascript delivery mode \"%s\" - should be" | |
| 260 | + " one of: inline separate bundled", zMode); | |
| 261 | + } | |
| 262 | +} | |
| 263 | + | |
| 264 | +/* | |
| 265 | +** The caller wants the Javascript file named by zFilename to be | |
| 266 | +** included in the generated page. Add the file to the queue of | |
| 267 | +** requested javascript resources, if it is not there already. | |
| 268 | +** | |
| 269 | +** The current implementation queues the file to be included in the | |
| 270 | +** output later. However, the caller should not depend on that | |
| 271 | +** behavior. In the future, this routine might decide to insert | |
| 272 | +** the requested javascript inline, immedaitely, or to insert | |
| 273 | +** a <script src=..> element to reference the javascript as a | |
| 274 | +** separate resource. The exact behavior might change in the future | |
| 275 | +** so pages that use this interface must not rely on any particular | |
| 276 | +** behavior. | |
| 277 | +** | |
| 278 | +** All this routine guarantees is that the named javascript file | |
| 279 | +** will be requested by the browser at some point. This routine | |
| 280 | +** does not guarantee when the javascript will be included, and it | |
| 281 | +** does not guarantee whether the javascript will be added inline or | |
| 282 | +** delivered as a separate resource. | |
| 283 | +*/ | |
| 284 | +void builtin_request_js(const char *zFilename){ | |
| 285 | + int i = builtin_file_index(zFilename); | |
| 286 | + int j; | |
| 287 | + if( i<0 ){ | |
| 288 | + fossil_panic("unknown javascript file: \"%s\"", zFilename); | |
| 289 | + } | |
| 290 | + for(j=0; j<builtin.nReq; j++){ | |
| 291 | + if( builtin.aReq[j]==i ) return; /* Already queued or sent */ | |
| 292 | + } | |
| 293 | + if( builtin.nReq>=count(builtin.aReq) ){ | |
| 294 | + fossil_panic("too many javascript files requested"); | |
| 295 | + } | |
| 296 | + builtin.aReq[builtin.nReq++] = i; | |
| 297 | +} | |
| 298 | + | |
| 299 | +/* | |
| 300 | +** Fulfill all pending requests for javascript files. | |
| 301 | +** | |
| 302 | +** The current implementation delivers all javascript in-line. However, | |
| 303 | +** the caller should not depend on this. Future changes to this routine | |
| 304 | +** might choose to deliver javascript as separate resources. | |
| 305 | +*/ | |
| 306 | +void builtin_fulfill_js_requests(void){ | |
| 307 | + if( builtin.nSent>=builtin.nReq ) return; /* nothing to do */ | |
| 308 | + switch( builtin.eDelivery ){ | |
| 309 | + case JS_INLINE: { | |
| 310 | + CX("<script nonce='%h'>\n",style_nonce()); | |
| 311 | + do{ | |
| 312 | + int i = builtin.aReq[builtin.nSent++]; | |
| 313 | + CX("/* %s */\n", aBuiltinFiles[i].zName); | |
| 314 | + cgi_append_content((const char*)aBuiltinFiles[i].pData, | |
| 315 | + aBuiltinFiles[i].nByte); | |
| 316 | + }while( builtin.nSent<builtin.nReq ); | |
| 317 | + CX("</script>\n"); | |
| 318 | + break; | |
| 319 | + } | |
| 320 | + case JS_BUNDLED: { | |
| 321 | + if( builtin.nSent+1<builtin.nReq ){ | |
| 322 | + Blob aList; | |
| 323 | + blob_init(&aList,0,0); | |
| 324 | + while( builtin.nSent<builtin.nReq ){ | |
| 325 | + blob_appendf(&aList, ",%d", builtin.aReq[builtin.nSent++]+1); | |
| 326 | + } | |
| 327 | + CX("<script src='%R/builtin?m=%s&id=%.8s'></script>\n", | |
| 328 | + blob_str(&aList)+1, fossil_exe_id()); | |
| 329 | + blob_reset(&aList); | |
| 330 | + break; | |
| 331 | + } | |
| 332 | + /* If there is only one JS file, fall through into the | |
| 333 | + ** JS_SEPARATE case below. */ | |
| 334 | + /*FALLTHROUGH*/ | |
| 335 | + } | |
| 336 | + case JS_SEPARATE: { | |
| 337 | + /* Each JS file as a separate resource */ | |
| 338 | + while( builtin.nSent<builtin.nReq ){ | |
| 339 | + int i = builtin.aReq[builtin.nSent++]; | |
| 340 | + CX("<script src='%R/builtin?name=%t&id=%.8s'></script>\n", | |
| 341 | + aBuiltinFiles[i].zName, fossil_exe_id()); | |
| 342 | + } | |
| 343 | + break; | |
| 344 | + } | |
| 345 | + } | |
| 346 | +} | |
| 347 | + | |
| 348 | +/***************************************************************************** | |
| 349 | +** A virtual table for accessing the information in aBuiltinFiles[]. | |
| 350 | +*/ | |
| 351 | + | |
| 352 | +/* builtinVtab_vtab is a subclass of sqlite3_vtab which is | |
| 353 | +** underlying representation of the virtual table | |
| 354 | +*/ | |
| 355 | +typedef struct builtinVtab_vtab builtinVtab_vtab; | |
| 356 | +struct builtinVtab_vtab { | |
| 357 | + sqlite3_vtab base; /* Base class - must be first */ | |
| 358 | + /* Add new fields here, as necessary */ | |
| 359 | +}; | |
| 360 | + | |
| 361 | +/* builtinVtab_cursor is a subclass of sqlite3_vtab_cursor which will | |
| 362 | +** serve as the underlying representation of a cursor that scans | |
| 363 | +** over rows of the result | |
| 364 | +*/ | |
| 365 | +typedef struct builtinVtab_cursor builtinVtab_cursor; | |
| 366 | +struct builtinVtab_cursor { | |
| 367 | + sqlite3_vtab_cursor base; /* Base class - must be first */ | |
| 368 | + /* Insert new fields here. For this builtinVtab we only keep track | |
| 369 | + ** of the rowid */ | |
| 370 | + sqlite3_int64 iRowid; /* The rowid */ | |
| 371 | +}; | |
| 372 | + | |
| 373 | +/* | |
| 374 | +** The builtinVtabConnect() method is invoked to create a new | |
| 375 | +** builtin virtual table. | |
| 376 | +** | |
| 377 | +** Think of this routine as the constructor for builtinVtab_vtab objects. | |
| 378 | +** | |
| 379 | +** All this routine needs to do is: | |
| 380 | +** | |
| 381 | +** (1) Allocate the builtinVtab_vtab object and initialize all fields. | |
| 382 | +** | |
| 383 | +** (2) Tell SQLite (via the sqlite3_declare_vtab() interface) what the | |
| 384 | +** result set of queries against the virtual table will look like. | |
| 385 | +*/ | |
| 386 | +static int builtinVtabConnect( | |
| 387 | + sqlite3 *db, | |
| 388 | + void *pAux, | |
| 389 | + int argc, const char *const*argv, | |
| 390 | + sqlite3_vtab **ppVtab, | |
| 391 | + char **pzErr | |
| 392 | +){ | |
| 393 | + builtinVtab_vtab *pNew; | |
| 394 | + int rc; | |
| 395 | + | |
| 396 | + rc = sqlite3_declare_vtab(db, | |
| 397 | + "CREATE TABLE x(name,size,data)" | |
| 398 | + ); | |
| 399 | + if( rc==SQLITE_OK ){ | |
| 400 | + pNew = sqlite3_malloc( sizeof(*pNew) ); | |
| 401 | + *ppVtab = (sqlite3_vtab*)pNew; | |
| 402 | + if( pNew==0 ) return SQLITE_NOMEM; | |
| 403 | + memset(pNew, 0, sizeof(*pNew)); | |
| 404 | + } | |
| 405 | + return rc; | |
| 406 | +} | |
| 407 | + | |
| 408 | +/* | |
| 409 | +** This method is the destructor for builtinVtab_vtab objects. | |
| 410 | +*/ | |
| 411 | +static int builtinVtabDisconnect(sqlite3_vtab *pVtab){ | |
| 412 | + builtinVtab_vtab *p = (builtinVtab_vtab*)pVtab; | |
| 413 | + sqlite3_free(p); | |
| 414 | + return SQLITE_OK; | |
| 415 | +} | |
| 416 | + | |
| 417 | +/* | |
| 418 | +** Constructor for a new builtinVtab_cursor object. | |
| 419 | +*/ | |
| 420 | +static int builtinVtabOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor){ | |
| 421 | + builtinVtab_cursor *pCur; | |
| 422 | + pCur = sqlite3_malloc( sizeof(*pCur) ); | |
| 423 | + if( pCur==0 ) return SQLITE_NOMEM; | |
| 424 | + memset(pCur, 0, sizeof(*pCur)); | |
| 425 | + *ppCursor = &pCur->base; | |
| 426 | + return SQLITE_OK; | |
| 427 | +} | |
| 428 | + | |
| 429 | +/* | |
| 430 | +** Destructor for a builtinVtab_cursor. | |
| 431 | +*/ | |
| 432 | +static int builtinVtabClose(sqlite3_vtab_cursor *cur){ | |
| 433 | + builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; | |
| 434 | + sqlite3_free(pCur); | |
| 435 | + return SQLITE_OK; | |
| 436 | +} | |
| 437 | + | |
| 438 | + | |
| 439 | +/* | |
| 440 | +** Advance a builtinVtab_cursor to its next row of output. | |
| 441 | +*/ | |
| 442 | +static int builtinVtabNext(sqlite3_vtab_cursor *cur){ | |
| 443 | + builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; | |
| 444 | + pCur->iRowid++; | |
| 445 | + return SQLITE_OK; | |
| 446 | +} | |
| 447 | + | |
| 448 | +/* | |
| 449 | +** Return values of columns for the row at which the builtinVtab_cursor | |
| 450 | +** is currently pointing. | |
| 451 | +*/ | |
| 452 | +static int builtinVtabColumn( | |
| 453 | + sqlite3_vtab_cursor *cur, /* The cursor */ | |
| 454 | + sqlite3_context *ctx, /* First argument to sqlite3_result_...() */ | |
| 455 | + int i /* Which column to return */ | |
| 456 | +){ | |
| 457 | + builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; | |
| 458 | + const struct BuiltinFileTable *pFile = aBuiltinFiles + pCur->iRowid; | |
| 459 | + switch( i ){ | |
| 460 | + case 0: /* name */ | |
| 461 | + sqlite3_result_text(ctx, pFile->zName, -1, SQLITE_STATIC); | |
| 462 | + break; | |
| 463 | + case 1: /* size */ | |
| 464 | + sqlite3_result_int(ctx, pFile->nByte); | |
| 465 | + break; | |
| 466 | + case 2: /* data */ | |
| 467 | + sqlite3_result_blob(ctx, pFile->pData, pFile->nByte, SQLITE_STATIC); | |
| 468 | + break; | |
| 469 | + } | |
| 470 | + return SQLITE_OK; | |
| 471 | +} | |
| 472 | + | |
| 473 | +/* | |
| 474 | +** Return the rowid for the current row. In this implementation, the | |
| 475 | +** rowid is the same as the output value. | |
| 476 | +*/ | |
| 477 | +static int builtinVtabRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ | |
| 478 | + builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; | |
| 479 | + *pRowid = pCur->iRowid; | |
| 480 | + return SQLITE_OK; | |
| 481 | +} | |
| 482 | + | |
| 483 | +/* | |
| 484 | +** Return TRUE if the cursor has been moved off of the last | |
| 485 | +** row of output. | |
| 486 | +*/ | |
| 487 | +static int builtinVtabEof(sqlite3_vtab_cursor *cur){ | |
| 488 | + builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; | |
| 489 | + return pCur->iRowid>=count(aBuiltinFiles); | |
| 490 | +} | |
| 491 | + | |
| 492 | +/* | |
| 493 | +** This method is called to "rewind" the builtinVtab_cursor object back | |
| 494 | +** to the first row of output. This method is always called at least | |
| 495 | +** once prior to any call to builtinVtabColumn() or builtinVtabRowid() or | |
| 496 | +** builtinVtabEof(). | |
| 497 | +*/ | |
| 498 | +static int builtinVtabFilter( | |
| 499 | + sqlite3_vtab_cursor *pVtabCursor, | |
| 500 | + int idxNum, const char *idxStr, | |
| 501 | + int argc, sqlite3_value **argv | |
| 502 | +){ | |
| 503 | + builtinVtab_cursor *pCur = (builtinVtab_cursor *)pVtabCursor; | |
| 504 | + pCur->iRowid = 1; | |
| 505 | + return SQLITE_OK; | |
| 506 | +} | |
| 507 | + | |
| 508 | +/* | |
| 509 | +** SQLite will invoke this method one or more times while planning a query | |
| 510 | +** that uses the virtual table. This routine needs to create | |
| 511 | +** a query plan for each invocation and compute an estimated cost for that | |
| 512 | +** plan. | |
| 513 | +*/ | |
| 514 | +static int builtinVtabBestIndex( | |
| 515 | + sqlite3_vtab *tab, | |
| 516 | + sqlite3_index_info *pIdxInfo | |
| 517 | +){ | |
| 518 | + pIdxInfo->estimatedCost = (double)count(aBuiltinFiles); | |
| 519 | + pIdxInfo->estimatedRows = count(aBuiltinFiles); | |
| 520 | + return SQLITE_OK; | |
| 521 | +} | |
| 522 | + | |
| 523 | +/* | |
| 524 | +** This following structure defines all the methods for the | |
| 525 | +** virtual table. | |
| 526 | +*/ | |
| 527 | +static sqlite3_module builtinVtabModule = { | |
| 528 | + /* iVersion */ 0, | |
| 529 | + /* xCreate */ 0, /* The builtin vtab is eponymous and read-only */ | |
| 530 | + /* xConnect */ builtinVtabConnect, | |
| 531 | + /* xBestIndex */ builtinVtabBestIndex, | |
| 532 | + /* xDisconnect */ builtinVtabDisconnect, | |
| 533 | + /* xDestroy */ 0, | |
| 534 | + /* xOpen */ builtinVtabOpen, | |
| 535 | + /* xClose */ builtinVtabClose, | |
| 536 | + /* xFilter */ builtinVtabFilter, | |
| 537 | + /* xNext */ builtinVtabNext, | |
| 538 | + /* xEof */ builtinVtabEof, | |
| 539 | + /* xColumn */ builtinVtabColumn, | |
| 540 | + /* xRowid */ builtinVtabRowid, | |
| 541 | + /* xUpdate */ 0, | |
| 542 | + /* xBegin */ 0, | |
| 543 | + /* xSync */ 0, | |
| 544 | + /* xCommit */ 0, | |
| 545 | + /* xRollback */ 0, | |
| 546 | + /* xFindMethod */ 0, | |
| 547 | + /* xRename */ 0, | |
| 548 | + /* xSavepoint */ 0, | |
| 549 | + /* xRelease */ 0, | |
| 550 | + /* xRollbackTo */ 0, | |
| 551 | + /* xShadowName */ 0 | |
| 552 | +}; | |
| 553 | + | |
| 554 | + | |
| 555 | +/* | |
| 556 | +** Register the builtin virtual table | |
| 557 | +*/ | |
| 558 | +int builtin_vtab_register(sqlite3 *db){ | |
| 559 | + int rc = sqlite3_create_module(db, "builtin", &builtinVtabModule, 0); | |
| 560 | + return rc; | |
| 561 | +} | |
| 562 | +/* End of the builtin virtual table | |
| 563 | +******************************************************************************/ | |
| 115 | 564 |
| --- src/builtin.c | |
| +++ src/builtin.c | |
| @@ -28,13 +28,15 @@ | |
| 28 | ** builtin_data.h file. Include that information here: |
| 29 | */ |
| 30 | #include "builtin_data.h" |
| 31 | |
| 32 | /* |
| 33 | ** Return a pointer to built-in content |
| 34 | */ |
| 35 | const unsigned char *builtin_file(const char *zFilename, int *piSize){ |
| 36 | int lwr, upr, i, c; |
| 37 | lwr = 0; |
| 38 | upr = count(aBuiltinFiles) - 1; |
| 39 | while( upr>=lwr ){ |
| 40 | i = (upr+lwr)/2; |
| @@ -42,16 +44,28 @@ | |
| 42 | if( c<0 ){ |
| 43 | lwr = i+1; |
| 44 | }else if( c>0 ){ |
| 45 | upr = i-1; |
| 46 | }else{ |
| 47 | if( piSize ) *piSize = aBuiltinFiles[i].nByte; |
| 48 | return aBuiltinFiles[i].pData; |
| 49 | } |
| 50 | } |
| 51 | if( piSize ) *piSize = 0; |
| 52 | return 0; |
| 53 | } |
| 54 | const char *builtin_text(const char *zFilename){ |
| 55 | return (char*)builtin_file(zFilename, 0); |
| 56 | } |
| 57 | |
| @@ -65,11 +79,11 @@ | |
| 65 | */ |
| 66 | void test_builtin_list(void){ |
| 67 | int i, size = 0;; |
| 68 | for(i=0; i<count(aBuiltinFiles); i++){ |
| 69 | const int n = aBuiltinFiles[i].nByte; |
| 70 | fossil_print("%-30s %6d\n", aBuiltinFiles[i].zName,n); |
| 71 | size += n; |
| 72 | } |
| 73 | if(find_option("verbose","v",0)!=0){ |
| 74 | fossil_print("%d entries totaling %d bytes\n", i, size); |
| 75 | } |
| @@ -81,16 +95,18 @@ | |
| 81 | ** Show all built-in text files. |
| 82 | */ |
| 83 | void test_builtin_list_page(void){ |
| 84 | int i; |
| 85 | style_header("Built-in Text Files"); |
| 86 | @ <ul> |
| 87 | for(i=0; i<count(aBuiltinFiles); i++){ |
| 88 | const char *z = aBuiltinFiles[i].zName; |
| 89 | @ <li>%z(href("%R/builtin?name=%T&id=%S",z,MANIFEST_UUID))%h(z)</a> |
| 90 | } |
| 91 | @ </ul> |
| 92 | style_footer(); |
| 93 | } |
| 94 | |
| 95 | /* |
| 96 | ** COMMAND: test-builtin-get |
| @@ -110,5 +126,438 @@ | |
| 110 | } |
| 111 | blob_init(&x, (const char*)pData, nByte); |
| 112 | blob_write_to_file(&x, g.argc==4 ? g.argv[3] : "-"); |
| 113 | blob_reset(&x); |
| 114 | } |
| 115 |
| --- src/builtin.c | |
| +++ src/builtin.c | |
| @@ -28,13 +28,15 @@ | |
| 28 | ** builtin_data.h file. Include that information here: |
| 29 | */ |
| 30 | #include "builtin_data.h" |
| 31 | |
| 32 | /* |
| 33 | ** Return the index in the aBuiltinFiles[] array for the file |
| 34 | ** whose name is zFilename. Or return -1 if the file is not |
| 35 | ** found. |
| 36 | */ |
| 37 | static int builtin_file_index(const char *zFilename){ |
| 38 | int lwr, upr, i, c; |
| 39 | lwr = 0; |
| 40 | upr = count(aBuiltinFiles) - 1; |
| 41 | while( upr>=lwr ){ |
| 42 | i = (upr+lwr)/2; |
| @@ -42,16 +44,28 @@ | |
| 44 | if( c<0 ){ |
| 45 | lwr = i+1; |
| 46 | }else if( c>0 ){ |
| 47 | upr = i-1; |
| 48 | }else{ |
| 49 | return i; |
| 50 | } |
| 51 | } |
| 52 | return -1; |
| 53 | } |
| 54 | |
| 55 | /* |
| 56 | ** Return a pointer to built-in content |
| 57 | */ |
| 58 | const unsigned char *builtin_file(const char *zFilename, int *piSize){ |
| 59 | int i = builtin_file_index(zFilename); |
| 60 | if( i>=0 ){ |
| 61 | if( piSize ) *piSize = aBuiltinFiles[i].nByte; |
| 62 | return aBuiltinFiles[i].pData; |
| 63 | }else{ |
| 64 | if( piSize ) *piSize = 0; |
| 65 | return 0; |
| 66 | } |
| 67 | } |
| 68 | const char *builtin_text(const char *zFilename){ |
| 69 | return (char*)builtin_file(zFilename, 0); |
| 70 | } |
| 71 | |
| @@ -65,11 +79,11 @@ | |
| 79 | */ |
| 80 | void test_builtin_list(void){ |
| 81 | int i, size = 0;; |
| 82 | for(i=0; i<count(aBuiltinFiles); i++){ |
| 83 | const int n = aBuiltinFiles[i].nByte; |
| 84 | fossil_print("%3d. %-45s %6d\n", i+1, aBuiltinFiles[i].zName,n); |
| 85 | size += n; |
| 86 | } |
| 87 | if(find_option("verbose","v",0)!=0){ |
| 88 | fossil_print("%d entries totaling %d bytes\n", i, size); |
| 89 | } |
| @@ -81,16 +95,18 @@ | |
| 95 | ** Show all built-in text files. |
| 96 | */ |
| 97 | void test_builtin_list_page(void){ |
| 98 | int i; |
| 99 | style_header("Built-in Text Files"); |
| 100 | @ <ol> |
| 101 | for(i=0; i<count(aBuiltinFiles); i++){ |
| 102 | const char *z = aBuiltinFiles[i].zName; |
| 103 | char *zUrl = href("%R/builtin?name=%T&id=%.8s&mimetype=text/plain", |
| 104 | z,fossil_exe_id()); |
| 105 | @ <li>%z(zUrl)%h(z)</a> |
| 106 | } |
| 107 | @ </ol> |
| 108 | style_footer(); |
| 109 | } |
| 110 | |
| 111 | /* |
| 112 | ** COMMAND: test-builtin-get |
| @@ -110,5 +126,438 @@ | |
| 126 | } |
| 127 | blob_init(&x, (const char*)pData, nByte); |
| 128 | blob_write_to_file(&x, g.argc==4 ? g.argv[3] : "-"); |
| 129 | blob_reset(&x); |
| 130 | } |
| 131 | |
| 132 | /* |
| 133 | ** Input zList is a list of numeric identifiers for files in |
| 134 | ** aBuiltinFiles[]. Return the concatenation of all of those |
| 135 | ** files using mimetype zType, or as application/javascript if |
| 136 | ** zType is 0. |
| 137 | */ |
| 138 | static void builtin_deliver_multiple_js_files( |
| 139 | const char *zList, /* List of numeric identifiers */ |
| 140 | const char *zType /* Override mimetype */ |
| 141 | ){ |
| 142 | Blob *pOut; |
| 143 | if( zType==0 ) zType = "application/javascript"; |
| 144 | cgi_set_content_type(zType); |
| 145 | pOut = cgi_output_blob(); |
| 146 | while( zList[0] ){ |
| 147 | int i = atoi(zList); |
| 148 | if( i>0 && i<=count(aBuiltinFiles) ){ |
| 149 | blob_appendf(pOut, "/* %s */\n", aBuiltinFiles[i-1].zName); |
| 150 | blob_append(pOut, (const char*)aBuiltinFiles[i-1].pData, |
| 151 | aBuiltinFiles[i-1].nByte); |
| 152 | } |
| 153 | while( fossil_isdigit(zList[0]) ) zList++; |
| 154 | if( zList[0]==',' ) zList++; |
| 155 | } |
| 156 | return; |
| 157 | } |
| 158 | |
| 159 | /* |
| 160 | ** WEBPAGE: builtin |
| 161 | ** |
| 162 | ** Return one of many built-in content files. Query parameters: |
| 163 | ** |
| 164 | ** name=FILENAME Return the single file whose name is FILENAME. |
| 165 | ** mimetype=TYPE Override the mimetype in the returned file to |
| 166 | ** be TYPE. If this query parameter is omitted |
| 167 | ** (the usual case) then the mimetype is inferred |
| 168 | ** from the suffix on FILENAME |
| 169 | ** m=IDLIST IDLIST is a comma-separated list of integers |
| 170 | ** that specify multiple javascript files to be |
| 171 | ** concatenated and returned all at once. |
| 172 | ** id=UNIQUEID Version number of the "builtin" files. Used |
| 173 | ** for cache control only. |
| 174 | ** |
| 175 | ** At least one of the name= or m= query parameters must be present. |
| 176 | ** |
| 177 | ** If the id= query parameter is present, then Fossil assumes that the |
| 178 | ** result is immutable and sets a very large cache retention time (1 year). |
| 179 | */ |
| 180 | void builtin_webpage(void){ |
| 181 | Blob out; |
| 182 | const char *zName = P("name"); |
| 183 | const char *zTxt = 0; |
| 184 | const char *zId = P("id"); |
| 185 | const char *zType = P("mimetype"); |
| 186 | int nId; |
| 187 | if( zName ) zTxt = builtin_text(zName); |
| 188 | if( zTxt==0 ){ |
| 189 | const char *zM = P("m"); |
| 190 | if( zM ){ |
| 191 | if( zId && (nId = (int)strlen(zId))>=8 |
| 192 | && strncmp(zId,fossil_exe_id(),nId)==0 |
| 193 | ){ |
| 194 | g.isConst = 1; |
| 195 | } |
| 196 | etag_check(0,0); |
| 197 | builtin_deliver_multiple_js_files(zM, zType); |
| 198 | return; |
| 199 | } |
| 200 | cgi_set_status(404, "Not Found"); |
| 201 | @ File "%h(zName)" not found |
| 202 | return; |
| 203 | } |
| 204 | if( zType==0 ){ |
| 205 | if( sqlite3_strglob("*.js", zName)==0 ){ |
| 206 | zType = "application/javascript"; |
| 207 | }else{ |
| 208 | zType = mimetype_from_name(zName); |
| 209 | } |
| 210 | } |
| 211 | cgi_set_content_type(zType); |
| 212 | if( zId |
| 213 | && (nId = (int)strlen(zId))>=8 |
| 214 | && strncmp(zId,fossil_exe_id(),nId)==0 |
| 215 | ){ |
| 216 | g.isConst = 1; |
| 217 | } |
| 218 | etag_check(0,0); |
| 219 | blob_init(&out, zTxt, -1); |
| 220 | cgi_set_content(&out); |
| 221 | } |
| 222 | |
| 223 | /* Variables controlling the JS cache. |
| 224 | */ |
| 225 | static struct { |
| 226 | int aReq[30]; /* Indexes of all requested built-in JS files */ |
| 227 | int nReq; /* Number of slots in aReq[] currently used */ |
| 228 | int nSent; /* Number of slots in aReq[] fulfilled */ |
| 229 | int eDelivery; /* Delivery mechanism */ |
| 230 | } builtin; |
| 231 | |
| 232 | #if INTERFACE |
| 233 | /* Various delivery mechanisms. The 0 option is the default. |
| 234 | */ |
| 235 | #define JS_INLINE 0 /* inline, batched together at end of file */ |
| 236 | #define JS_SEPARATE 1 /* Separate HTTP request for each JS file */ |
| 237 | #define JS_BUNDLED 2 /* One HTTP request to load all JS files */ |
| 238 | /* concatenated together into a bundle */ |
| 239 | #endif /* INTERFACE */ |
| 240 | |
| 241 | /* |
| 242 | ** The argument is a request to change the javascript delivery mode. |
| 243 | ** The argument is a string which is a command-line option or CGI |
| 244 | ** parameter. Try to match it against one of the delivery options |
| 245 | ** and set things up accordingly. Throw an error if no match unless |
| 246 | ** bSilent is true. |
| 247 | */ |
| 248 | void builtin_set_js_delivery_mode(const char *zMode, int bSilent){ |
| 249 | if( zMode==0 ) return; |
| 250 | if( strcmp(zMode, "inline")==0 ){ |
| 251 | builtin.eDelivery = JS_INLINE; |
| 252 | }else |
| 253 | if( strcmp(zMode, "separate")==0 ){ |
| 254 | builtin.eDelivery = JS_SEPARATE; |
| 255 | }else |
| 256 | if( strcmp(zMode, "bundled")==0 ){ |
| 257 | builtin.eDelivery = JS_BUNDLED; |
| 258 | }else if( !bSilent ){ |
| 259 | fossil_fatal("unknown javascript delivery mode \"%s\" - should be" |
| 260 | " one of: inline separate bundled", zMode); |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | /* |
| 265 | ** The caller wants the Javascript file named by zFilename to be |
| 266 | ** included in the generated page. Add the file to the queue of |
| 267 | ** requested javascript resources, if it is not there already. |
| 268 | ** |
| 269 | ** The current implementation queues the file to be included in the |
| 270 | ** output later. However, the caller should not depend on that |
| 271 | ** behavior. In the future, this routine might decide to insert |
| 272 | ** the requested javascript inline, immedaitely, or to insert |
| 273 | ** a <script src=..> element to reference the javascript as a |
| 274 | ** separate resource. The exact behavior might change in the future |
| 275 | ** so pages that use this interface must not rely on any particular |
| 276 | ** behavior. |
| 277 | ** |
| 278 | ** All this routine guarantees is that the named javascript file |
| 279 | ** will be requested by the browser at some point. This routine |
| 280 | ** does not guarantee when the javascript will be included, and it |
| 281 | ** does not guarantee whether the javascript will be added inline or |
| 282 | ** delivered as a separate resource. |
| 283 | */ |
| 284 | void builtin_request_js(const char *zFilename){ |
| 285 | int i = builtin_file_index(zFilename); |
| 286 | int j; |
| 287 | if( i<0 ){ |
| 288 | fossil_panic("unknown javascript file: \"%s\"", zFilename); |
| 289 | } |
| 290 | for(j=0; j<builtin.nReq; j++){ |
| 291 | if( builtin.aReq[j]==i ) return; /* Already queued or sent */ |
| 292 | } |
| 293 | if( builtin.nReq>=count(builtin.aReq) ){ |
| 294 | fossil_panic("too many javascript files requested"); |
| 295 | } |
| 296 | builtin.aReq[builtin.nReq++] = i; |
| 297 | } |
| 298 | |
| 299 | /* |
| 300 | ** Fulfill all pending requests for javascript files. |
| 301 | ** |
| 302 | ** The current implementation delivers all javascript in-line. However, |
| 303 | ** the caller should not depend on this. Future changes to this routine |
| 304 | ** might choose to deliver javascript as separate resources. |
| 305 | */ |
| 306 | void builtin_fulfill_js_requests(void){ |
| 307 | if( builtin.nSent>=builtin.nReq ) return; /* nothing to do */ |
| 308 | switch( builtin.eDelivery ){ |
| 309 | case JS_INLINE: { |
| 310 | CX("<script nonce='%h'>\n",style_nonce()); |
| 311 | do{ |
| 312 | int i = builtin.aReq[builtin.nSent++]; |
| 313 | CX("/* %s */\n", aBuiltinFiles[i].zName); |
| 314 | cgi_append_content((const char*)aBuiltinFiles[i].pData, |
| 315 | aBuiltinFiles[i].nByte); |
| 316 | }while( builtin.nSent<builtin.nReq ); |
| 317 | CX("</script>\n"); |
| 318 | break; |
| 319 | } |
| 320 | case JS_BUNDLED: { |
| 321 | if( builtin.nSent+1<builtin.nReq ){ |
| 322 | Blob aList; |
| 323 | blob_init(&aList,0,0); |
| 324 | while( builtin.nSent<builtin.nReq ){ |
| 325 | blob_appendf(&aList, ",%d", builtin.aReq[builtin.nSent++]+1); |
| 326 | } |
| 327 | CX("<script src='%R/builtin?m=%s&id=%.8s'></script>\n", |
| 328 | blob_str(&aList)+1, fossil_exe_id()); |
| 329 | blob_reset(&aList); |
| 330 | break; |
| 331 | } |
| 332 | /* If there is only one JS file, fall through into the |
| 333 | ** JS_SEPARATE case below. */ |
| 334 | /*FALLTHROUGH*/ |
| 335 | } |
| 336 | case JS_SEPARATE: { |
| 337 | /* Each JS file as a separate resource */ |
| 338 | while( builtin.nSent<builtin.nReq ){ |
| 339 | int i = builtin.aReq[builtin.nSent++]; |
| 340 | CX("<script src='%R/builtin?name=%t&id=%.8s'></script>\n", |
| 341 | aBuiltinFiles[i].zName, fossil_exe_id()); |
| 342 | } |
| 343 | break; |
| 344 | } |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | /***************************************************************************** |
| 349 | ** A virtual table for accessing the information in aBuiltinFiles[]. |
| 350 | */ |
| 351 | |
| 352 | /* builtinVtab_vtab is a subclass of sqlite3_vtab which is |
| 353 | ** underlying representation of the virtual table |
| 354 | */ |
| 355 | typedef struct builtinVtab_vtab builtinVtab_vtab; |
| 356 | struct builtinVtab_vtab { |
| 357 | sqlite3_vtab base; /* Base class - must be first */ |
| 358 | /* Add new fields here, as necessary */ |
| 359 | }; |
| 360 | |
| 361 | /* builtinVtab_cursor is a subclass of sqlite3_vtab_cursor which will |
| 362 | ** serve as the underlying representation of a cursor that scans |
| 363 | ** over rows of the result |
| 364 | */ |
| 365 | typedef struct builtinVtab_cursor builtinVtab_cursor; |
| 366 | struct builtinVtab_cursor { |
| 367 | sqlite3_vtab_cursor base; /* Base class - must be first */ |
| 368 | /* Insert new fields here. For this builtinVtab we only keep track |
| 369 | ** of the rowid */ |
| 370 | sqlite3_int64 iRowid; /* The rowid */ |
| 371 | }; |
| 372 | |
| 373 | /* |
| 374 | ** The builtinVtabConnect() method is invoked to create a new |
| 375 | ** builtin virtual table. |
| 376 | ** |
| 377 | ** Think of this routine as the constructor for builtinVtab_vtab objects. |
| 378 | ** |
| 379 | ** All this routine needs to do is: |
| 380 | ** |
| 381 | ** (1) Allocate the builtinVtab_vtab object and initialize all fields. |
| 382 | ** |
| 383 | ** (2) Tell SQLite (via the sqlite3_declare_vtab() interface) what the |
| 384 | ** result set of queries against the virtual table will look like. |
| 385 | */ |
| 386 | static int builtinVtabConnect( |
| 387 | sqlite3 *db, |
| 388 | void *pAux, |
| 389 | int argc, const char *const*argv, |
| 390 | sqlite3_vtab **ppVtab, |
| 391 | char **pzErr |
| 392 | ){ |
| 393 | builtinVtab_vtab *pNew; |
| 394 | int rc; |
| 395 | |
| 396 | rc = sqlite3_declare_vtab(db, |
| 397 | "CREATE TABLE x(name,size,data)" |
| 398 | ); |
| 399 | if( rc==SQLITE_OK ){ |
| 400 | pNew = sqlite3_malloc( sizeof(*pNew) ); |
| 401 | *ppVtab = (sqlite3_vtab*)pNew; |
| 402 | if( pNew==0 ) return SQLITE_NOMEM; |
| 403 | memset(pNew, 0, sizeof(*pNew)); |
| 404 | } |
| 405 | return rc; |
| 406 | } |
| 407 | |
| 408 | /* |
| 409 | ** This method is the destructor for builtinVtab_vtab objects. |
| 410 | */ |
| 411 | static int builtinVtabDisconnect(sqlite3_vtab *pVtab){ |
| 412 | builtinVtab_vtab *p = (builtinVtab_vtab*)pVtab; |
| 413 | sqlite3_free(p); |
| 414 | return SQLITE_OK; |
| 415 | } |
| 416 | |
| 417 | /* |
| 418 | ** Constructor for a new builtinVtab_cursor object. |
| 419 | */ |
| 420 | static int builtinVtabOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor){ |
| 421 | builtinVtab_cursor *pCur; |
| 422 | pCur = sqlite3_malloc( sizeof(*pCur) ); |
| 423 | if( pCur==0 ) return SQLITE_NOMEM; |
| 424 | memset(pCur, 0, sizeof(*pCur)); |
| 425 | *ppCursor = &pCur->base; |
| 426 | return SQLITE_OK; |
| 427 | } |
| 428 | |
| 429 | /* |
| 430 | ** Destructor for a builtinVtab_cursor. |
| 431 | */ |
| 432 | static int builtinVtabClose(sqlite3_vtab_cursor *cur){ |
| 433 | builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; |
| 434 | sqlite3_free(pCur); |
| 435 | return SQLITE_OK; |
| 436 | } |
| 437 | |
| 438 | |
| 439 | /* |
| 440 | ** Advance a builtinVtab_cursor to its next row of output. |
| 441 | */ |
| 442 | static int builtinVtabNext(sqlite3_vtab_cursor *cur){ |
| 443 | builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; |
| 444 | pCur->iRowid++; |
| 445 | return SQLITE_OK; |
| 446 | } |
| 447 | |
| 448 | /* |
| 449 | ** Return values of columns for the row at which the builtinVtab_cursor |
| 450 | ** is currently pointing. |
| 451 | */ |
| 452 | static int builtinVtabColumn( |
| 453 | sqlite3_vtab_cursor *cur, /* The cursor */ |
| 454 | sqlite3_context *ctx, /* First argument to sqlite3_result_...() */ |
| 455 | int i /* Which column to return */ |
| 456 | ){ |
| 457 | builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; |
| 458 | const struct BuiltinFileTable *pFile = aBuiltinFiles + pCur->iRowid; |
| 459 | switch( i ){ |
| 460 | case 0: /* name */ |
| 461 | sqlite3_result_text(ctx, pFile->zName, -1, SQLITE_STATIC); |
| 462 | break; |
| 463 | case 1: /* size */ |
| 464 | sqlite3_result_int(ctx, pFile->nByte); |
| 465 | break; |
| 466 | case 2: /* data */ |
| 467 | sqlite3_result_blob(ctx, pFile->pData, pFile->nByte, SQLITE_STATIC); |
| 468 | break; |
| 469 | } |
| 470 | return SQLITE_OK; |
| 471 | } |
| 472 | |
| 473 | /* |
| 474 | ** Return the rowid for the current row. In this implementation, the |
| 475 | ** rowid is the same as the output value. |
| 476 | */ |
| 477 | static int builtinVtabRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ |
| 478 | builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; |
| 479 | *pRowid = pCur->iRowid; |
| 480 | return SQLITE_OK; |
| 481 | } |
| 482 | |
| 483 | /* |
| 484 | ** Return TRUE if the cursor has been moved off of the last |
| 485 | ** row of output. |
| 486 | */ |
| 487 | static int builtinVtabEof(sqlite3_vtab_cursor *cur){ |
| 488 | builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; |
| 489 | return pCur->iRowid>=count(aBuiltinFiles); |
| 490 | } |
| 491 | |
| 492 | /* |
| 493 | ** This method is called to "rewind" the builtinVtab_cursor object back |
| 494 | ** to the first row of output. This method is always called at least |
| 495 | ** once prior to any call to builtinVtabColumn() or builtinVtabRowid() or |
| 496 | ** builtinVtabEof(). |
| 497 | */ |
| 498 | static int builtinVtabFilter( |
| 499 | sqlite3_vtab_cursor *pVtabCursor, |
| 500 | int idxNum, const char *idxStr, |
| 501 | int argc, sqlite3_value **argv |
| 502 | ){ |
| 503 | builtinVtab_cursor *pCur = (builtinVtab_cursor *)pVtabCursor; |
| 504 | pCur->iRowid = 1; |
| 505 | return SQLITE_OK; |
| 506 | } |
| 507 | |
| 508 | /* |
| 509 | ** SQLite will invoke this method one or more times while planning a query |
| 510 | ** that uses the virtual table. This routine needs to create |
| 511 | ** a query plan for each invocation and compute an estimated cost for that |
| 512 | ** plan. |
| 513 | */ |
| 514 | static int builtinVtabBestIndex( |
| 515 | sqlite3_vtab *tab, |
| 516 | sqlite3_index_info *pIdxInfo |
| 517 | ){ |
| 518 | pIdxInfo->estimatedCost = (double)count(aBuiltinFiles); |
| 519 | pIdxInfo->estimatedRows = count(aBuiltinFiles); |
| 520 | return SQLITE_OK; |
| 521 | } |
| 522 | |
| 523 | /* |
| 524 | ** This following structure defines all the methods for the |
| 525 | ** virtual table. |
| 526 | */ |
| 527 | static sqlite3_module builtinVtabModule = { |
| 528 | /* iVersion */ 0, |
| 529 | /* xCreate */ 0, /* The builtin vtab is eponymous and read-only */ |
| 530 | /* xConnect */ builtinVtabConnect, |
| 531 | /* xBestIndex */ builtinVtabBestIndex, |
| 532 | /* xDisconnect */ builtinVtabDisconnect, |
| 533 | /* xDestroy */ 0, |
| 534 | /* xOpen */ builtinVtabOpen, |
| 535 | /* xClose */ builtinVtabClose, |
| 536 | /* xFilter */ builtinVtabFilter, |
| 537 | /* xNext */ builtinVtabNext, |
| 538 | /* xEof */ builtinVtabEof, |
| 539 | /* xColumn */ builtinVtabColumn, |
| 540 | /* xRowid */ builtinVtabRowid, |
| 541 | /* xUpdate */ 0, |
| 542 | /* xBegin */ 0, |
| 543 | /* xSync */ 0, |
| 544 | /* xCommit */ 0, |
| 545 | /* xRollback */ 0, |
| 546 | /* xFindMethod */ 0, |
| 547 | /* xRename */ 0, |
| 548 | /* xSavepoint */ 0, |
| 549 | /* xRelease */ 0, |
| 550 | /* xRollbackTo */ 0, |
| 551 | /* xShadowName */ 0 |
| 552 | }; |
| 553 | |
| 554 | |
| 555 | /* |
| 556 | ** Register the builtin virtual table |
| 557 | */ |
| 558 | int builtin_vtab_register(sqlite3 *db){ |
| 559 | int rc = sqlite3_create_module(db, "builtin", &builtinVtabModule, 0); |
| 560 | return rc; |
| 561 | } |
| 562 | /* End of the builtin virtual table |
| 563 | ******************************************************************************/ |
| 564 |
+2
-16
| --- src/bundle.c | ||
| +++ src/bundle.c | ||
| @@ -716,11 +716,11 @@ | ||
| 716 | 716 | } |
| 717 | 717 | db_end_transaction(0); |
| 718 | 718 | } |
| 719 | 719 | |
| 720 | 720 | /* |
| 721 | -** COMMAND: bundle | |
| 721 | +** COMMAND: bundle* | |
| 722 | 722 | ** |
| 723 | 723 | ** Usage: %fossil bundle SUBCOMMAND ARGS... |
| 724 | 724 | ** |
| 725 | 725 | ** > fossil bundle append BUNDLE FILE... |
| 726 | 726 | ** |
| @@ -768,25 +768,11 @@ | ||
| 768 | 768 | ** |
| 769 | 769 | ** Remove from the repository all files that are used exclusively |
| 770 | 770 | ** by check-ins in BUNDLE. This has the effect of undoing a |
| 771 | 771 | ** "fossil bundle import". |
| 772 | 772 | ** |
| 773 | -** SUMMARY: | |
| 774 | -** fossil bundle append BUNDLE FILE... Add files to BUNDLE | |
| 775 | -** fossil bundle cat BUNDLE HASH... Extract file from BUNDLE | |
| 776 | -** fossil bundle export BUNDLE ?OPTIONS? Create a new BUNDLE | |
| 777 | -** --branch BRANCH --from TAG1 --to TAG2 Check-ins to include | |
| 778 | -** --checkin TAG Use only check-in TAG | |
| 779 | -** --standalone Omit dependencies | |
| 780 | -** fossil bundle extend BUNDLE Update with newer content | |
| 781 | -** fossil bundle import BUNDLE ?OPTIONS? Import a bundle | |
| 782 | -** --publish Publish the import | |
| 783 | -** --force Cross-repo import | |
| 784 | -** fossil bundle ls BUNDLE List content of a bundle | |
| 785 | -** fossil bundle purge BUNDLE Undo an import | |
| 786 | -** | |
| 787 | -** See also: publish | |
| 773 | +** See also: [[publish]] | |
| 788 | 774 | */ |
| 789 | 775 | void bundle_cmd(void){ |
| 790 | 776 | const char *zSubcmd; |
| 791 | 777 | int n; |
| 792 | 778 | if( g.argc<4 ) usage("SUBCOMMAND BUNDLE ?OPTIONS?"); |
| 793 | 779 |
| --- src/bundle.c | |
| +++ src/bundle.c | |
| @@ -716,11 +716,11 @@ | |
| 716 | } |
| 717 | db_end_transaction(0); |
| 718 | } |
| 719 | |
| 720 | /* |
| 721 | ** COMMAND: bundle |
| 722 | ** |
| 723 | ** Usage: %fossil bundle SUBCOMMAND ARGS... |
| 724 | ** |
| 725 | ** > fossil bundle append BUNDLE FILE... |
| 726 | ** |
| @@ -768,25 +768,11 @@ | |
| 768 | ** |
| 769 | ** Remove from the repository all files that are used exclusively |
| 770 | ** by check-ins in BUNDLE. This has the effect of undoing a |
| 771 | ** "fossil bundle import". |
| 772 | ** |
| 773 | ** SUMMARY: |
| 774 | ** fossil bundle append BUNDLE FILE... Add files to BUNDLE |
| 775 | ** fossil bundle cat BUNDLE HASH... Extract file from BUNDLE |
| 776 | ** fossil bundle export BUNDLE ?OPTIONS? Create a new BUNDLE |
| 777 | ** --branch BRANCH --from TAG1 --to TAG2 Check-ins to include |
| 778 | ** --checkin TAG Use only check-in TAG |
| 779 | ** --standalone Omit dependencies |
| 780 | ** fossil bundle extend BUNDLE Update with newer content |
| 781 | ** fossil bundle import BUNDLE ?OPTIONS? Import a bundle |
| 782 | ** --publish Publish the import |
| 783 | ** --force Cross-repo import |
| 784 | ** fossil bundle ls BUNDLE List content of a bundle |
| 785 | ** fossil bundle purge BUNDLE Undo an import |
| 786 | ** |
| 787 | ** See also: publish |
| 788 | */ |
| 789 | void bundle_cmd(void){ |
| 790 | const char *zSubcmd; |
| 791 | int n; |
| 792 | if( g.argc<4 ) usage("SUBCOMMAND BUNDLE ?OPTIONS?"); |
| 793 |
| --- src/bundle.c | |
| +++ src/bundle.c | |
| @@ -716,11 +716,11 @@ | |
| 716 | } |
| 717 | db_end_transaction(0); |
| 718 | } |
| 719 | |
| 720 | /* |
| 721 | ** COMMAND: bundle* |
| 722 | ** |
| 723 | ** Usage: %fossil bundle SUBCOMMAND ARGS... |
| 724 | ** |
| 725 | ** > fossil bundle append BUNDLE FILE... |
| 726 | ** |
| @@ -768,25 +768,11 @@ | |
| 768 | ** |
| 769 | ** Remove from the repository all files that are used exclusively |
| 770 | ** by check-ins in BUNDLE. This has the effect of undoing a |
| 771 | ** "fossil bundle import". |
| 772 | ** |
| 773 | ** See also: [[publish]] |
| 774 | */ |
| 775 | void bundle_cmd(void){ |
| 776 | const char *zSubcmd; |
| 777 | int n; |
| 778 | if( g.argc<4 ) usage("SUBCOMMAND BUNDLE ?OPTIONS?"); |
| 779 |
+8
-8
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -303,24 +303,24 @@ | ||
| 303 | 303 | fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); |
| 304 | 304 | }else{ |
| 305 | 305 | assert( rangeEnd==0 ); |
| 306 | 306 | fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); |
| 307 | 307 | } |
| 308 | - if( g.isConst ){ | |
| 308 | + if( etag_tag()[0]!=0 ){ | |
| 309 | + fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); | |
| 310 | + fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); | |
| 311 | + if( etag_mtime()>0 ){ | |
| 312 | + fprintf(g.httpOut, "Last-Modified: %s\r\n", | |
| 313 | + cgi_rfc822_datestamp(etag_mtime())); | |
| 314 | + } | |
| 315 | + }else if( g.isConst ){ | |
| 309 | 316 | /* isConst means that the reply is guaranteed to be invariant, even |
| 310 | 317 | ** after configuration changes and/or Fossil binary recompiles. */ |
| 311 | 318 | fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n"); |
| 312 | - }else if( etag_tag()[0]!=0 ){ | |
| 313 | - fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); | |
| 314 | - fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); | |
| 315 | 319 | }else{ |
| 316 | 320 | fprintf(g.httpOut, "Cache-control: no-cache\r\n"); |
| 317 | 321 | } |
| 318 | - if( etag_mtime()>0 ){ | |
| 319 | - fprintf(g.httpOut, "Last-Modified: %s\r\n", | |
| 320 | - cgi_rfc822_datestamp(etag_mtime())); | |
| 321 | - } | |
| 322 | 322 | |
| 323 | 323 | if( blob_size(&extraHeader)>0 ){ |
| 324 | 324 | fprintf(g.httpOut, "%s", blob_buffer(&extraHeader)); |
| 325 | 325 | } |
| 326 | 326 | |
| 327 | 327 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -303,24 +303,24 @@ | |
| 303 | fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); |
| 304 | }else{ |
| 305 | assert( rangeEnd==0 ); |
| 306 | fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); |
| 307 | } |
| 308 | if( g.isConst ){ |
| 309 | /* isConst means that the reply is guaranteed to be invariant, even |
| 310 | ** after configuration changes and/or Fossil binary recompiles. */ |
| 311 | fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n"); |
| 312 | }else if( etag_tag()[0]!=0 ){ |
| 313 | fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); |
| 314 | fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); |
| 315 | }else{ |
| 316 | fprintf(g.httpOut, "Cache-control: no-cache\r\n"); |
| 317 | } |
| 318 | if( etag_mtime()>0 ){ |
| 319 | fprintf(g.httpOut, "Last-Modified: %s\r\n", |
| 320 | cgi_rfc822_datestamp(etag_mtime())); |
| 321 | } |
| 322 | |
| 323 | if( blob_size(&extraHeader)>0 ){ |
| 324 | fprintf(g.httpOut, "%s", blob_buffer(&extraHeader)); |
| 325 | } |
| 326 | |
| 327 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -303,24 +303,24 @@ | |
| 303 | fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); |
| 304 | }else{ |
| 305 | assert( rangeEnd==0 ); |
| 306 | fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); |
| 307 | } |
| 308 | if( etag_tag()[0]!=0 ){ |
| 309 | fprintf(g.httpOut, "ETag: %s\r\n", etag_tag()); |
| 310 | fprintf(g.httpOut, "Cache-Control: max-age=%d\r\n", etag_maxage()); |
| 311 | if( etag_mtime()>0 ){ |
| 312 | fprintf(g.httpOut, "Last-Modified: %s\r\n", |
| 313 | cgi_rfc822_datestamp(etag_mtime())); |
| 314 | } |
| 315 | }else if( g.isConst ){ |
| 316 | /* isConst means that the reply is guaranteed to be invariant, even |
| 317 | ** after configuration changes and/or Fossil binary recompiles. */ |
| 318 | fprintf(g.httpOut, "Cache-Control: max-age=31536000\r\n"); |
| 319 | }else{ |
| 320 | fprintf(g.httpOut, "Cache-control: no-cache\r\n"); |
| 321 | } |
| 322 | |
| 323 | if( blob_size(&extraHeader)>0 ){ |
| 324 | fprintf(g.httpOut, "%s", blob_buffer(&extraHeader)); |
| 325 | } |
| 326 | |
| 327 |
+5
-5
| --- src/checkin.c | ||
| +++ src/checkin.c | ||
| @@ -440,11 +440,11 @@ | ||
| 440 | 440 | ** --extra Display unmanaged files. |
| 441 | 441 | ** --differ Display modified and extra files. |
| 442 | 442 | ** --merge Display merge contributors. |
| 443 | 443 | ** --no-merge Do not display merge contributors. |
| 444 | 444 | ** |
| 445 | -** See also: extras, ls | |
| 445 | +** See also: [[extras]], [[ls]] | |
| 446 | 446 | */ |
| 447 | 447 | void status_cmd(void){ |
| 448 | 448 | /* Affirmative and negative flag option tables. */ |
| 449 | 449 | static const struct { |
| 450 | 450 | const char *option; /* Flag name. */ |
| @@ -676,11 +676,11 @@ | ||
| 676 | 676 | ** -v|--verbose Provide extra information about each file. |
| 677 | 677 | ** -t Sort output in time order. |
| 678 | 678 | ** -r VERSION The specific check-in to list. |
| 679 | 679 | ** -R|--repository FILE Extract info from repository FILE. |
| 680 | 680 | ** |
| 681 | -** See also: changes, extras, status | |
| 681 | +** See also: [[changes]], [[extras]], [[status]] | |
| 682 | 682 | */ |
| 683 | 683 | void ls_cmd(void){ |
| 684 | 684 | int vid; |
| 685 | 685 | Stmt q; |
| 686 | 686 | int verboseFlag; |
| @@ -830,11 +830,11 @@ | ||
| 830 | 830 | ** --header Identify the repository if there are extras |
| 831 | 831 | ** --ignore CSG Ignore files matching patterns from the argument |
| 832 | 832 | ** --rel-paths Display pathnames relative to the current working |
| 833 | 833 | ** directory. |
| 834 | 834 | ** |
| 835 | -** See also: changes, clean, status | |
| 835 | +** See also: [[changes]], [[clean]], [[status]] | |
| 836 | 836 | */ |
| 837 | 837 | void extras_cmd(void){ |
| 838 | 838 | Blob report = BLOB_INITIALIZER; |
| 839 | 839 | const char *zIgnoreFlag = find_option("ignore",0,1); |
| 840 | 840 | unsigned scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0; |
| @@ -954,11 +954,11 @@ | ||
| 954 | 954 | ** --no-prompt This option disables prompting the user for input |
| 955 | 955 | ** and assumes an answer of 'No' for every question. |
| 956 | 956 | ** --temp Remove only Fossil-generated temporary files. |
| 957 | 957 | ** -v|--verbose Show all files as they are removed. |
| 958 | 958 | ** |
| 959 | -** See also: addremove, extras, status | |
| 959 | +** See also: [[addremove]], [[extras]], [[status]] | |
| 960 | 960 | */ |
| 961 | 961 | void clean_cmd(void){ |
| 962 | 962 | int allFileFlag, allDirFlag, dryRunFlag, verboseFlag; |
| 963 | 963 | int emptyDirsFlag, dirsOnlyFlag; |
| 964 | 964 | int disableUndo, noPrompt; |
| @@ -2091,11 +2091,11 @@ | ||
| 2091 | 2091 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2092 | 2092 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| 2093 | 2093 | ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
| 2094 | 2094 | ** means UTC. |
| 2095 | 2095 | ** |
| 2096 | -** See also: branch, changes, checkout, extras, sync | |
| 2096 | +** See also: [[branch]], [[changes]], [[update]], [[extras]], [[sync]] | |
| 2097 | 2097 | */ |
| 2098 | 2098 | void commit_cmd(void){ |
| 2099 | 2099 | int hasChanges; /* True if unsaved changes exist */ |
| 2100 | 2100 | int vid; /* blob-id of parent version */ |
| 2101 | 2101 | int nrid; /* blob-id of a modified file */ |
| 2102 | 2102 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -440,11 +440,11 @@ | |
| 440 | ** --extra Display unmanaged files. |
| 441 | ** --differ Display modified and extra files. |
| 442 | ** --merge Display merge contributors. |
| 443 | ** --no-merge Do not display merge contributors. |
| 444 | ** |
| 445 | ** See also: extras, ls |
| 446 | */ |
| 447 | void status_cmd(void){ |
| 448 | /* Affirmative and negative flag option tables. */ |
| 449 | static const struct { |
| 450 | const char *option; /* Flag name. */ |
| @@ -676,11 +676,11 @@ | |
| 676 | ** -v|--verbose Provide extra information about each file. |
| 677 | ** -t Sort output in time order. |
| 678 | ** -r VERSION The specific check-in to list. |
| 679 | ** -R|--repository FILE Extract info from repository FILE. |
| 680 | ** |
| 681 | ** See also: changes, extras, status |
| 682 | */ |
| 683 | void ls_cmd(void){ |
| 684 | int vid; |
| 685 | Stmt q; |
| 686 | int verboseFlag; |
| @@ -830,11 +830,11 @@ | |
| 830 | ** --header Identify the repository if there are extras |
| 831 | ** --ignore CSG Ignore files matching patterns from the argument |
| 832 | ** --rel-paths Display pathnames relative to the current working |
| 833 | ** directory. |
| 834 | ** |
| 835 | ** See also: changes, clean, status |
| 836 | */ |
| 837 | void extras_cmd(void){ |
| 838 | Blob report = BLOB_INITIALIZER; |
| 839 | const char *zIgnoreFlag = find_option("ignore",0,1); |
| 840 | unsigned scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0; |
| @@ -954,11 +954,11 @@ | |
| 954 | ** --no-prompt This option disables prompting the user for input |
| 955 | ** and assumes an answer of 'No' for every question. |
| 956 | ** --temp Remove only Fossil-generated temporary files. |
| 957 | ** -v|--verbose Show all files as they are removed. |
| 958 | ** |
| 959 | ** See also: addremove, extras, status |
| 960 | */ |
| 961 | void clean_cmd(void){ |
| 962 | int allFileFlag, allDirFlag, dryRunFlag, verboseFlag; |
| 963 | int emptyDirsFlag, dirsOnlyFlag; |
| 964 | int disableUndo, noPrompt; |
| @@ -2091,11 +2091,11 @@ | |
| 2091 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2092 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| 2093 | ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
| 2094 | ** means UTC. |
| 2095 | ** |
| 2096 | ** See also: branch, changes, checkout, extras, sync |
| 2097 | */ |
| 2098 | void commit_cmd(void){ |
| 2099 | int hasChanges; /* True if unsaved changes exist */ |
| 2100 | int vid; /* blob-id of parent version */ |
| 2101 | int nrid; /* blob-id of a modified file */ |
| 2102 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -440,11 +440,11 @@ | |
| 440 | ** --extra Display unmanaged files. |
| 441 | ** --differ Display modified and extra files. |
| 442 | ** --merge Display merge contributors. |
| 443 | ** --no-merge Do not display merge contributors. |
| 444 | ** |
| 445 | ** See also: [[extras]], [[ls]] |
| 446 | */ |
| 447 | void status_cmd(void){ |
| 448 | /* Affirmative and negative flag option tables. */ |
| 449 | static const struct { |
| 450 | const char *option; /* Flag name. */ |
| @@ -676,11 +676,11 @@ | |
| 676 | ** -v|--verbose Provide extra information about each file. |
| 677 | ** -t Sort output in time order. |
| 678 | ** -r VERSION The specific check-in to list. |
| 679 | ** -R|--repository FILE Extract info from repository FILE. |
| 680 | ** |
| 681 | ** See also: [[changes]], [[extras]], [[status]] |
| 682 | */ |
| 683 | void ls_cmd(void){ |
| 684 | int vid; |
| 685 | Stmt q; |
| 686 | int verboseFlag; |
| @@ -830,11 +830,11 @@ | |
| 830 | ** --header Identify the repository if there are extras |
| 831 | ** --ignore CSG Ignore files matching patterns from the argument |
| 832 | ** --rel-paths Display pathnames relative to the current working |
| 833 | ** directory. |
| 834 | ** |
| 835 | ** See also: [[changes]], [[clean]], [[status]] |
| 836 | */ |
| 837 | void extras_cmd(void){ |
| 838 | Blob report = BLOB_INITIALIZER; |
| 839 | const char *zIgnoreFlag = find_option("ignore",0,1); |
| 840 | unsigned scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0; |
| @@ -954,11 +954,11 @@ | |
| 954 | ** --no-prompt This option disables prompting the user for input |
| 955 | ** and assumes an answer of 'No' for every question. |
| 956 | ** --temp Remove only Fossil-generated temporary files. |
| 957 | ** -v|--verbose Show all files as they are removed. |
| 958 | ** |
| 959 | ** See also: [[addremove]], [[extras]], [[status]] |
| 960 | */ |
| 961 | void clean_cmd(void){ |
| 962 | int allFileFlag, allDirFlag, dryRunFlag, verboseFlag; |
| 963 | int emptyDirsFlag, dirsOnlyFlag; |
| 964 | int disableUndo, noPrompt; |
| @@ -2091,11 +2091,11 @@ | |
| 2091 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2092 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| 2093 | ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
| 2094 | ** means UTC. |
| 2095 | ** |
| 2096 | ** See also: [[branch]], [[changes]], [[update]], [[extras]], [[sync]] |
| 2097 | */ |
| 2098 | void commit_cmd(void){ |
| 2099 | int hasChanges; /* True if unsaved changes exist */ |
| 2100 | int vid; /* blob-id of parent version */ |
| 2101 | int nrid; /* blob-id of a modified file */ |
| 2102 |
+3
-3
| --- src/checkout.c | ||
| +++ src/checkout.c | ||
| @@ -279,11 +279,11 @@ | ||
| 279 | 279 | ** --force-missing Force checkout even if content is missing |
| 280 | 280 | ** --setmtime Set timestamps of all files to match their SCM-side |
| 281 | 281 | ** times (the timestamp of the last checkin which modified |
| 282 | 282 | ** them). |
| 283 | 283 | ** |
| 284 | -** See also: update | |
| 284 | +** See also: [[update]] | |
| 285 | 285 | */ |
| 286 | 286 | void checkout_cmd(void){ |
| 287 | 287 | int forceFlag; /* Force checkout even if edits exist */ |
| 288 | 288 | int forceMissingFlag; /* Force checkout even if missing content */ |
| 289 | 289 | int keepFlag; /* Do not change any files on disk */ |
| @@ -387,18 +387,18 @@ | ||
| 387 | 387 | /* |
| 388 | 388 | ** COMMAND: close* |
| 389 | 389 | ** |
| 390 | 390 | ** Usage: %fossil close ?OPTIONS? |
| 391 | 391 | ** |
| 392 | -** The opposite of "open". Close the current database connection. | |
| 392 | +** The opposite of "[[open]]". Close the current database connection. | |
| 393 | 393 | ** Require a -f or --force flag if there are unsaved changes in the |
| 394 | 394 | ** current check-out or if there is non-empty stash. |
| 395 | 395 | ** |
| 396 | 396 | ** Options: |
| 397 | 397 | ** --force|-f necessary to close a check out with uncommitted changes |
| 398 | 398 | ** |
| 399 | -** See also: open | |
| 399 | +** See also: [[open]] | |
| 400 | 400 | */ |
| 401 | 401 | void close_cmd(void){ |
| 402 | 402 | int forceFlag = find_option("force","f",0)!=0; |
| 403 | 403 | db_must_be_within_tree(); |
| 404 | 404 | |
| 405 | 405 |
| --- src/checkout.c | |
| +++ src/checkout.c | |
| @@ -279,11 +279,11 @@ | |
| 279 | ** --force-missing Force checkout even if content is missing |
| 280 | ** --setmtime Set timestamps of all files to match their SCM-side |
| 281 | ** times (the timestamp of the last checkin which modified |
| 282 | ** them). |
| 283 | ** |
| 284 | ** See also: update |
| 285 | */ |
| 286 | void checkout_cmd(void){ |
| 287 | int forceFlag; /* Force checkout even if edits exist */ |
| 288 | int forceMissingFlag; /* Force checkout even if missing content */ |
| 289 | int keepFlag; /* Do not change any files on disk */ |
| @@ -387,18 +387,18 @@ | |
| 387 | /* |
| 388 | ** COMMAND: close* |
| 389 | ** |
| 390 | ** Usage: %fossil close ?OPTIONS? |
| 391 | ** |
| 392 | ** The opposite of "open". Close the current database connection. |
| 393 | ** Require a -f or --force flag if there are unsaved changes in the |
| 394 | ** current check-out or if there is non-empty stash. |
| 395 | ** |
| 396 | ** Options: |
| 397 | ** --force|-f necessary to close a check out with uncommitted changes |
| 398 | ** |
| 399 | ** See also: open |
| 400 | */ |
| 401 | void close_cmd(void){ |
| 402 | int forceFlag = find_option("force","f",0)!=0; |
| 403 | db_must_be_within_tree(); |
| 404 | |
| 405 |
| --- src/checkout.c | |
| +++ src/checkout.c | |
| @@ -279,11 +279,11 @@ | |
| 279 | ** --force-missing Force checkout even if content is missing |
| 280 | ** --setmtime Set timestamps of all files to match their SCM-side |
| 281 | ** times (the timestamp of the last checkin which modified |
| 282 | ** them). |
| 283 | ** |
| 284 | ** See also: [[update]] |
| 285 | */ |
| 286 | void checkout_cmd(void){ |
| 287 | int forceFlag; /* Force checkout even if edits exist */ |
| 288 | int forceMissingFlag; /* Force checkout even if missing content */ |
| 289 | int keepFlag; /* Do not change any files on disk */ |
| @@ -387,18 +387,18 @@ | |
| 387 | /* |
| 388 | ** COMMAND: close* |
| 389 | ** |
| 390 | ** Usage: %fossil close ?OPTIONS? |
| 391 | ** |
| 392 | ** The opposite of "[[open]]". Close the current database connection. |
| 393 | ** Require a -f or --force flag if there are unsaved changes in the |
| 394 | ** current check-out or if there is non-empty stash. |
| 395 | ** |
| 396 | ** Options: |
| 397 | ** --force|-f necessary to close a check out with uncommitted changes |
| 398 | ** |
| 399 | ** See also: [[open]] |
| 400 | */ |
| 401 | void close_cmd(void){ |
| 402 | int forceFlag = find_option("force","f",0)!=0; |
| 403 | db_must_be_within_tree(); |
| 404 | |
| 405 |
+9
-7
| --- src/clone.c | ||
| +++ src/clone.c | ||
| @@ -83,13 +83,11 @@ | ||
| 83 | 83 | ** COMMAND: clone |
| 84 | 84 | ** |
| 85 | 85 | ** Usage: %fossil clone ?OPTIONS? URI FILENAME |
| 86 | 86 | ** |
| 87 | 87 | ** Make a clone of a repository specified by URI in the local |
| 88 | -** file named FILENAME. | |
| 89 | -** | |
| 90 | -** URI may be one of the following forms: | |
| 88 | +** file named FILENAME. URI may be one of the following forms: | |
| 91 | 89 | ** ([...] denotes optional elements): |
| 92 | 90 | ** |
| 93 | 91 | ** * HTTP/HTTPS protocol: |
| 94 | 92 | ** |
| 95 | 93 | ** http[s]://[userid[:password]@]host[:port][/path] |
| @@ -100,18 +98,22 @@ | ||
| 100 | 98 | ** |
| 101 | 99 | ** * Filesystem: |
| 102 | 100 | ** |
| 103 | 101 | ** [file://]path/to/repo.fossil |
| 104 | 102 | ** |
| 105 | -** Note 1: For ssh and filesystem, path must have an extra leading | |
| 103 | +** Note that in Fossil (in contrast to some other DVCSes) a repository | |
| 104 | +** is distinct from a checkout. This command create a clone of a repository. | |
| 105 | +** Use the separate [[open]] command to open a checkout from that repository. | |
| 106 | +** | |
| 107 | +** For ssh and filesystem, path must have an extra leading | |
| 106 | 108 | ** '/' to use an absolute path. |
| 107 | 109 | ** |
| 108 | -** Note 2: Use %HH escapes for special characters in the userid and | |
| 110 | +** Use %HH escapes for special characters in the userid and | |
| 109 | 111 | ** password. For example "%40" in place of "@", "%2f" in place |
| 110 | 112 | ** of "/", and "%3a" in place of ":". |
| 111 | 113 | ** |
| 112 | -** By default, your current login name is used to create the default | |
| 114 | +** By default, the current login name is used to create the default | |
| 113 | 115 | ** admin user. This can be overridden using the -A|--admin-user |
| 114 | 116 | ** parameter. |
| 115 | 117 | ** |
| 116 | 118 | ** Options: |
| 117 | 119 | ** --admin-user|-A USERNAME Make USERNAME the administrator |
| @@ -123,11 +125,11 @@ | ||
| 123 | 125 | ** --ssh-command|-c SSH Use SSH as the "ssh" command |
| 124 | 126 | ** --ssl-identity FILENAME Use the SSL identity if requested by the server |
| 125 | 127 | ** -u|--unversioned Also sync unversioned content |
| 126 | 128 | ** -v|--verbose Show more statistics in output |
| 127 | 129 | ** |
| 128 | -** See also: init | |
| 130 | +** See also: [[init]], [[open]] | |
| 129 | 131 | */ |
| 130 | 132 | void clone_cmd(void){ |
| 131 | 133 | char *zPassword; |
| 132 | 134 | const char *zDefaultUser; /* Optional name of the default user */ |
| 133 | 135 | const char *zHttpAuth; /* HTTP Authorization user:pass information */ |
| 134 | 136 |
| --- src/clone.c | |
| +++ src/clone.c | |
| @@ -83,13 +83,11 @@ | |
| 83 | ** COMMAND: clone |
| 84 | ** |
| 85 | ** Usage: %fossil clone ?OPTIONS? URI FILENAME |
| 86 | ** |
| 87 | ** Make a clone of a repository specified by URI in the local |
| 88 | ** file named FILENAME. |
| 89 | ** |
| 90 | ** URI may be one of the following forms: |
| 91 | ** ([...] denotes optional elements): |
| 92 | ** |
| 93 | ** * HTTP/HTTPS protocol: |
| 94 | ** |
| 95 | ** http[s]://[userid[:password]@]host[:port][/path] |
| @@ -100,18 +98,22 @@ | |
| 100 | ** |
| 101 | ** * Filesystem: |
| 102 | ** |
| 103 | ** [file://]path/to/repo.fossil |
| 104 | ** |
| 105 | ** Note 1: For ssh and filesystem, path must have an extra leading |
| 106 | ** '/' to use an absolute path. |
| 107 | ** |
| 108 | ** Note 2: Use %HH escapes for special characters in the userid and |
| 109 | ** password. For example "%40" in place of "@", "%2f" in place |
| 110 | ** of "/", and "%3a" in place of ":". |
| 111 | ** |
| 112 | ** By default, your current login name is used to create the default |
| 113 | ** admin user. This can be overridden using the -A|--admin-user |
| 114 | ** parameter. |
| 115 | ** |
| 116 | ** Options: |
| 117 | ** --admin-user|-A USERNAME Make USERNAME the administrator |
| @@ -123,11 +125,11 @@ | |
| 123 | ** --ssh-command|-c SSH Use SSH as the "ssh" command |
| 124 | ** --ssl-identity FILENAME Use the SSL identity if requested by the server |
| 125 | ** -u|--unversioned Also sync unversioned content |
| 126 | ** -v|--verbose Show more statistics in output |
| 127 | ** |
| 128 | ** See also: init |
| 129 | */ |
| 130 | void clone_cmd(void){ |
| 131 | char *zPassword; |
| 132 | const char *zDefaultUser; /* Optional name of the default user */ |
| 133 | const char *zHttpAuth; /* HTTP Authorization user:pass information */ |
| 134 |
| --- src/clone.c | |
| +++ src/clone.c | |
| @@ -83,13 +83,11 @@ | |
| 83 | ** COMMAND: clone |
| 84 | ** |
| 85 | ** Usage: %fossil clone ?OPTIONS? URI FILENAME |
| 86 | ** |
| 87 | ** Make a clone of a repository specified by URI in the local |
| 88 | ** file named FILENAME. URI may be one of the following forms: |
| 89 | ** ([...] denotes optional elements): |
| 90 | ** |
| 91 | ** * HTTP/HTTPS protocol: |
| 92 | ** |
| 93 | ** http[s]://[userid[:password]@]host[:port][/path] |
| @@ -100,18 +98,22 @@ | |
| 98 | ** |
| 99 | ** * Filesystem: |
| 100 | ** |
| 101 | ** [file://]path/to/repo.fossil |
| 102 | ** |
| 103 | ** Note that in Fossil (in contrast to some other DVCSes) a repository |
| 104 | ** is distinct from a checkout. This command create a clone of a repository. |
| 105 | ** Use the separate [[open]] command to open a checkout from that repository. |
| 106 | ** |
| 107 | ** For ssh and filesystem, path must have an extra leading |
| 108 | ** '/' to use an absolute path. |
| 109 | ** |
| 110 | ** Use %HH escapes for special characters in the userid and |
| 111 | ** password. For example "%40" in place of "@", "%2f" in place |
| 112 | ** of "/", and "%3a" in place of ":". |
| 113 | ** |
| 114 | ** By default, the current login name is used to create the default |
| 115 | ** admin user. This can be overridden using the -A|--admin-user |
| 116 | ** parameter. |
| 117 | ** |
| 118 | ** Options: |
| 119 | ** --admin-user|-A USERNAME Make USERNAME the administrator |
| @@ -123,11 +125,11 @@ | |
| 125 | ** --ssh-command|-c SSH Use SSH as the "ssh" command |
| 126 | ** --ssl-identity FILENAME Use the SSL identity if requested by the server |
| 127 | ** -u|--unversioned Also sync unversioned content |
| 128 | ** -v|--verbose Show more statistics in output |
| 129 | ** |
| 130 | ** See also: [[init]], [[open]] |
| 131 | */ |
| 132 | void clone_cmd(void){ |
| 133 | char *zPassword; |
| 134 | const char *zDefaultUser; /* Optional name of the default user */ |
| 135 | const char *zHttpAuth; /* HTTP Authorization user:pass information */ |
| 136 |
-1
| --- src/codecheck1.c | ||
| +++ src/codecheck1.c | ||
| @@ -407,11 +407,10 @@ | ||
| 407 | 407 | { "smtp_send_line", 2, FMT_SAFE }, |
| 408 | 408 | { "smtp_server_send", 2, FMT_SAFE }, |
| 409 | 409 | { "socket_set_errmsg", 1, FMT_SAFE }, |
| 410 | 410 | { "ssl_set_errmsg", 1, FMT_SAFE }, |
| 411 | 411 | { "style_header", 1, FMT_HTML }, |
| 412 | - { "style_js_onload", 1, FMT_HTML }, | |
| 413 | 412 | { "style_set_current_page", 1, FMT_URL }, |
| 414 | 413 | { "style_submenu_element", 2, FMT_URL }, |
| 415 | 414 | { "style_submenu_sql", 3, FMT_SQL }, |
| 416 | 415 | { "webpage_error", 1, FMT_SAFE }, |
| 417 | 416 | { "xhref", 2, FMT_URL }, |
| 418 | 417 |
| --- src/codecheck1.c | |
| +++ src/codecheck1.c | |
| @@ -407,11 +407,10 @@ | |
| 407 | { "smtp_send_line", 2, FMT_SAFE }, |
| 408 | { "smtp_server_send", 2, FMT_SAFE }, |
| 409 | { "socket_set_errmsg", 1, FMT_SAFE }, |
| 410 | { "ssl_set_errmsg", 1, FMT_SAFE }, |
| 411 | { "style_header", 1, FMT_HTML }, |
| 412 | { "style_js_onload", 1, FMT_HTML }, |
| 413 | { "style_set_current_page", 1, FMT_URL }, |
| 414 | { "style_submenu_element", 2, FMT_URL }, |
| 415 | { "style_submenu_sql", 3, FMT_SQL }, |
| 416 | { "webpage_error", 1, FMT_SAFE }, |
| 417 | { "xhref", 2, FMT_URL }, |
| 418 |
| --- src/codecheck1.c | |
| +++ src/codecheck1.c | |
| @@ -407,11 +407,10 @@ | |
| 407 | { "smtp_send_line", 2, FMT_SAFE }, |
| 408 | { "smtp_server_send", 2, FMT_SAFE }, |
| 409 | { "socket_set_errmsg", 1, FMT_SAFE }, |
| 410 | { "ssl_set_errmsg", 1, FMT_SAFE }, |
| 411 | { "style_header", 1, FMT_HTML }, |
| 412 | { "style_set_current_page", 1, FMT_URL }, |
| 413 | { "style_submenu_element", 2, FMT_URL }, |
| 414 | { "style_submenu_sql", 3, FMT_SQL }, |
| 415 | { "webpage_error", 1, FMT_SAFE }, |
| 416 | { "xhref", 2, FMT_URL }, |
| 417 |
+4
-1
| --- src/configure.c | ||
| +++ src/configure.c | ||
| @@ -94,10 +94,12 @@ | ||
| 94 | 94 | { "js", CONFIGSET_SKIN }, |
| 95 | 95 | { "logo-mimetype", CONFIGSET_SKIN }, |
| 96 | 96 | { "logo-image", CONFIGSET_SKIN }, |
| 97 | 97 | { "background-mimetype", CONFIGSET_SKIN }, |
| 98 | 98 | { "background-image", CONFIGSET_SKIN }, |
| 99 | + { "icon-mimetype", CONFIGSET_SKIN }, | |
| 100 | + { "icon-image", CONFIGSET_SKIN }, | |
| 99 | 101 | { "timeline-block-markup", CONFIGSET_SKIN }, |
| 100 | 102 | { "timeline-date-format", CONFIGSET_SKIN }, |
| 101 | 103 | { "timeline-default-style", CONFIGSET_SKIN }, |
| 102 | 104 | { "timeline-dwelltime", CONFIGSET_SKIN }, |
| 103 | 105 | { "timeline-closetime", CONFIGSET_SKIN }, |
| @@ -112,10 +114,11 @@ | ||
| 112 | 114 | { "default-csp", CONFIGSET_SKIN }, |
| 113 | 115 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 114 | 116 | { "sitemap-download", CONFIGSET_SKIN }, |
| 115 | 117 | { "sitemap-license", CONFIGSET_SKIN }, |
| 116 | 118 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 119 | + { "safe-html", CONFIGSET_SKIN }, | |
| 117 | 120 | |
| 118 | 121 | #ifdef FOSSIL_ENABLE_TH1_DOCS |
| 119 | 122 | { "th1-docs", CONFIGSET_TH1 }, |
| 120 | 123 | #endif |
| 121 | 124 | #ifdef FOSSIL_ENABLE_TH1_HOOKS |
| @@ -744,11 +747,11 @@ | ||
| 744 | 747 | ** the remote repository at URL. |
| 745 | 748 | ** |
| 746 | 749 | ** Options: |
| 747 | 750 | ** -R|--repository FILE Extract info from repository FILE |
| 748 | 751 | ** |
| 749 | -** See also: settings, unset | |
| 752 | +** See also: [[settings]], [[unset]] | |
| 750 | 753 | */ |
| 751 | 754 | void configuration_cmd(void){ |
| 752 | 755 | int n; |
| 753 | 756 | const char *zMethod; |
| 754 | 757 | db_find_and_open_repository(0, 0); |
| 755 | 758 |
| --- src/configure.c | |
| +++ src/configure.c | |
| @@ -94,10 +94,12 @@ | |
| 94 | { "js", CONFIGSET_SKIN }, |
| 95 | { "logo-mimetype", CONFIGSET_SKIN }, |
| 96 | { "logo-image", CONFIGSET_SKIN }, |
| 97 | { "background-mimetype", CONFIGSET_SKIN }, |
| 98 | { "background-image", CONFIGSET_SKIN }, |
| 99 | { "timeline-block-markup", CONFIGSET_SKIN }, |
| 100 | { "timeline-date-format", CONFIGSET_SKIN }, |
| 101 | { "timeline-default-style", CONFIGSET_SKIN }, |
| 102 | { "timeline-dwelltime", CONFIGSET_SKIN }, |
| 103 | { "timeline-closetime", CONFIGSET_SKIN }, |
| @@ -112,10 +114,11 @@ | |
| 112 | { "default-csp", CONFIGSET_SKIN }, |
| 113 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 114 | { "sitemap-download", CONFIGSET_SKIN }, |
| 115 | { "sitemap-license", CONFIGSET_SKIN }, |
| 116 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 117 | |
| 118 | #ifdef FOSSIL_ENABLE_TH1_DOCS |
| 119 | { "th1-docs", CONFIGSET_TH1 }, |
| 120 | #endif |
| 121 | #ifdef FOSSIL_ENABLE_TH1_HOOKS |
| @@ -744,11 +747,11 @@ | |
| 744 | ** the remote repository at URL. |
| 745 | ** |
| 746 | ** Options: |
| 747 | ** -R|--repository FILE Extract info from repository FILE |
| 748 | ** |
| 749 | ** See also: settings, unset |
| 750 | */ |
| 751 | void configuration_cmd(void){ |
| 752 | int n; |
| 753 | const char *zMethod; |
| 754 | db_find_and_open_repository(0, 0); |
| 755 |
| --- src/configure.c | |
| +++ src/configure.c | |
| @@ -94,10 +94,12 @@ | |
| 94 | { "js", CONFIGSET_SKIN }, |
| 95 | { "logo-mimetype", CONFIGSET_SKIN }, |
| 96 | { "logo-image", CONFIGSET_SKIN }, |
| 97 | { "background-mimetype", CONFIGSET_SKIN }, |
| 98 | { "background-image", CONFIGSET_SKIN }, |
| 99 | { "icon-mimetype", CONFIGSET_SKIN }, |
| 100 | { "icon-image", CONFIGSET_SKIN }, |
| 101 | { "timeline-block-markup", CONFIGSET_SKIN }, |
| 102 | { "timeline-date-format", CONFIGSET_SKIN }, |
| 103 | { "timeline-default-style", CONFIGSET_SKIN }, |
| 104 | { "timeline-dwelltime", CONFIGSET_SKIN }, |
| 105 | { "timeline-closetime", CONFIGSET_SKIN }, |
| @@ -112,10 +114,11 @@ | |
| 114 | { "default-csp", CONFIGSET_SKIN }, |
| 115 | { "sitemap-docidx", CONFIGSET_SKIN }, |
| 116 | { "sitemap-download", CONFIGSET_SKIN }, |
| 117 | { "sitemap-license", CONFIGSET_SKIN }, |
| 118 | { "sitemap-contact", CONFIGSET_SKIN }, |
| 119 | { "safe-html", CONFIGSET_SKIN }, |
| 120 | |
| 121 | #ifdef FOSSIL_ENABLE_TH1_DOCS |
| 122 | { "th1-docs", CONFIGSET_TH1 }, |
| 123 | #endif |
| 124 | #ifdef FOSSIL_ENABLE_TH1_HOOKS |
| @@ -744,11 +747,11 @@ | |
| 747 | ** the remote repository at URL. |
| 748 | ** |
| 749 | ** Options: |
| 750 | ** -R|--repository FILE Extract info from repository FILE |
| 751 | ** |
| 752 | ** See also: [[settings]], [[unset]] |
| 753 | */ |
| 754 | void configuration_cmd(void){ |
| 755 | int n; |
| 756 | const char *zMethod; |
| 757 | db_find_and_open_repository(0, 0); |
| 758 |
+1
-1
| --- src/content.c | ||
| +++ src/content.c | ||
| @@ -326,11 +326,11 @@ | ||
| 326 | 326 | ** the named output file. |
| 327 | 327 | ** |
| 328 | 328 | ** Options: |
| 329 | 329 | ** -R|--repository FILE Extract artifacts from repository FILE |
| 330 | 330 | ** |
| 331 | -** See also: finfo | |
| 331 | +** See also: [[finfo]] | |
| 332 | 332 | */ |
| 333 | 333 | void artifact_cmd(void){ |
| 334 | 334 | int rid; |
| 335 | 335 | Blob content; |
| 336 | 336 | const char *zFile; |
| 337 | 337 |
| --- src/content.c | |
| +++ src/content.c | |
| @@ -326,11 +326,11 @@ | |
| 326 | ** the named output file. |
| 327 | ** |
| 328 | ** Options: |
| 329 | ** -R|--repository FILE Extract artifacts from repository FILE |
| 330 | ** |
| 331 | ** See also: finfo |
| 332 | */ |
| 333 | void artifact_cmd(void){ |
| 334 | int rid; |
| 335 | Blob content; |
| 336 | const char *zFile; |
| 337 |
| --- src/content.c | |
| +++ src/content.c | |
| @@ -326,11 +326,11 @@ | |
| 326 | ** the named output file. |
| 327 | ** |
| 328 | ** Options: |
| 329 | ** -R|--repository FILE Extract artifacts from repository FILE |
| 330 | ** |
| 331 | ** See also: [[finfo]] |
| 332 | */ |
| 333 | void artifact_cmd(void){ |
| 334 | int rid; |
| 335 | Blob content; |
| 336 | const char *zFile; |
| 337 |
M
src/db.c
+131
-19
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -116,10 +116,11 @@ | ||
| 116 | 116 | */ |
| 117 | 117 | static struct DbLocalData { |
| 118 | 118 | int nBegin; /* Nesting depth of BEGIN */ |
| 119 | 119 | int doRollback; /* True to force a rollback */ |
| 120 | 120 | int nCommitHook; /* Number of commit hooks */ |
| 121 | + int wrTxn; /* Outer-most TNX is a write */ | |
| 121 | 122 | Stmt *pAllStmt; /* List of all unfinalized statements */ |
| 122 | 123 | int nPrepare; /* Number of calls to sqlite3_prepare_v2() */ |
| 123 | 124 | int nDeleteOnFail; /* Number of entries in azDeleteOnFail[] */ |
| 124 | 125 | struct sCommitHook { |
| 125 | 126 | int (*xHook)(void); /* Functions to call at db_end_transaction() */ |
| @@ -195,10 +196,11 @@ | ||
| 195 | 196 | sqlite3_commit_hook(g.db, db_verify_at_commit, 0); |
| 196 | 197 | db.nPriorChanges = sqlite3_total_changes(g.db); |
| 197 | 198 | db.doRollback = 0; |
| 198 | 199 | db.zStartFile = zStartFile; |
| 199 | 200 | db.iStartLine = iStartLine; |
| 201 | + db.wrTxn = 0; | |
| 200 | 202 | } |
| 201 | 203 | db.nBegin++; |
| 202 | 204 | } |
| 203 | 205 | /* |
| 204 | 206 | ** Begin a new transaction for writing. |
| @@ -209,11 +211,12 @@ | ||
| 209 | 211 | sqlite3_commit_hook(g.db, db_verify_at_commit, 0); |
| 210 | 212 | db.nPriorChanges = sqlite3_total_changes(g.db); |
| 211 | 213 | db.doRollback = 0; |
| 212 | 214 | db.zStartFile = zStartFile; |
| 213 | 215 | db.iStartLine = iStartLine; |
| 214 | - }else{ | |
| 216 | + db.wrTxn = 1; | |
| 217 | + }else if( !db.wrTxn ){ | |
| 215 | 218 | fossil_warning("read txn at %s:%d might cause SQLITE_BUSY " |
| 216 | 219 | "for the write txn at %s:%d", |
| 217 | 220 | db.zStartFile, db.iStartLine, zStartFile, iStartLine); |
| 218 | 221 | } |
| 219 | 222 | db.nBegin++; |
| @@ -1328,11 +1331,11 @@ | ||
| 1328 | 1331 | ); |
| 1329 | 1332 | if( rc!=SQLITE_OK ){ |
| 1330 | 1333 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 1331 | 1334 | } |
| 1332 | 1335 | db_maybe_set_encryption_key(db, zDbName); |
| 1333 | - sqlite3_busy_timeout(db, 5000); | |
| 1336 | + sqlite3_busy_timeout(db, 15000); | |
| 1334 | 1337 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| 1335 | 1338 | sqlite3_create_function(db, "user", 0, SQLITE_UTF8, 0, db_sql_user, 0, 0); |
| 1336 | 1339 | sqlite3_create_function(db, "cgi", 1, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); |
| 1337 | 1340 | sqlite3_create_function(db, "cgi", 2, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); |
| 1338 | 1341 | sqlite3_create_function(db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); |
| @@ -1344,11 +1347,11 @@ | ||
| 1344 | 1347 | ); |
| 1345 | 1348 | if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); |
| 1346 | 1349 | db_add_aux_functions(db); |
| 1347 | 1350 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1348 | 1351 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1349 | - sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); | |
| 1352 | + sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_FKEY, 0, &rc); | |
| 1350 | 1353 | return db; |
| 1351 | 1354 | } |
| 1352 | 1355 | |
| 1353 | 1356 | |
| 1354 | 1357 | /* |
| @@ -1714,16 +1717,20 @@ | ||
| 1714 | 1717 | ** |
| 1715 | 1718 | ** If no valid _FOSSIL_ or .fslckout file is found, we move up one level and |
| 1716 | 1719 | ** try again. Once the file is found, the g.zLocalRoot variable is set |
| 1717 | 1720 | ** to the root of the repository tree and this routine returns 1. If |
| 1718 | 1721 | ** no database is found, then this routine return 0. |
| 1722 | +** | |
| 1723 | +** In db_open_local_v2(), if the bRootOnly flag is true, then only | |
| 1724 | +** look in the CWD for the checkout database. Do not scan upwards in | |
| 1725 | +** the file hierarchy. | |
| 1719 | 1726 | ** |
| 1720 | 1727 | ** This routine always opens the user database regardless of whether or |
| 1721 | 1728 | ** not the repository database is found. If the _FOSSIL_ or .fslckout file |
| 1722 | 1729 | ** is found, it is attached to the open database connection too. |
| 1723 | 1730 | */ |
| 1724 | -int db_open_local(const char *zDbName){ | |
| 1731 | +int db_open_local_v2(const char *zDbName, int bRootOnly){ | |
| 1725 | 1732 | int i, n; |
| 1726 | 1733 | char zPwd[2000]; |
| 1727 | 1734 | static const char *(aDbName[]) = { "_FOSSIL_", ".fslckout", ".fos" }; |
| 1728 | 1735 | |
| 1729 | 1736 | if( g.localOpen ) return 1; |
| @@ -1747,18 +1754,22 @@ | ||
| 1747 | 1754 | g.localOpen = 1; |
| 1748 | 1755 | db_open_repository(zDbName); |
| 1749 | 1756 | return 1; |
| 1750 | 1757 | } |
| 1751 | 1758 | } |
| 1759 | + if( bRootOnly ) break; | |
| 1752 | 1760 | n--; |
| 1753 | 1761 | while( n>1 && zPwd[n]!='/' ){ n--; } |
| 1754 | 1762 | while( n>1 && zPwd[n-1]=='/' ){ n--; } |
| 1755 | 1763 | zPwd[n] = 0; |
| 1756 | 1764 | } |
| 1757 | 1765 | |
| 1758 | 1766 | /* A checkout database file could not be found */ |
| 1759 | 1767 | return 0; |
| 1768 | +} | |
| 1769 | +int db_open_local(const char *zDbName){ | |
| 1770 | + return db_open_local_v2(zDbName, 0); | |
| 1760 | 1771 | } |
| 1761 | 1772 | |
| 1762 | 1773 | /* |
| 1763 | 1774 | ** Get the full pathname to the repository database file. The |
| 1764 | 1775 | ** local database (the _FOSSIL_ or .fslckout database) must have already |
| @@ -2388,11 +2399,11 @@ | ||
| 2388 | 2399 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2389 | 2400 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| 2390 | 2401 | ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
| 2391 | 2402 | ** means UTC. |
| 2392 | 2403 | ** |
| 2393 | -** See also: clone | |
| 2404 | +** See also: [[clone]] | |
| 2394 | 2405 | */ |
| 2395 | 2406 | void create_repository_cmd(void){ |
| 2396 | 2407 | char *zPassword; |
| 2397 | 2408 | const char *zTemplate; /* Repository from which to copy settings */ |
| 2398 | 2409 | const char *zDate; /* Date of the initial check-in */ |
| @@ -3070,57 +3081,158 @@ | ||
| 3070 | 3081 | } |
| 3071 | 3082 | |
| 3072 | 3083 | /* |
| 3073 | 3084 | ** COMMAND: open |
| 3074 | 3085 | ** |
| 3075 | -** Usage: %fossil open FILENAME ?VERSION? ?OPTIONS? | |
| 3086 | +** Usage: %fossil open REPOSITORY ?VERSION? ?OPTIONS? | |
| 3087 | +** | |
| 3088 | +** Open a new connection to the repository name REPOSITORY. A checkout | |
| 3089 | +** for the repository is created with its root at the current working | |
| 3090 | +** directory, or in DIR if the "--workdir DIR" is used. If VERSION is | |
| 3091 | +** specified then that version is checked out. Otherwise the most recent | |
| 3092 | +** check-in on the main branch (usually "trunk") is used. | |
| 3093 | +** | |
| 3094 | +** REPOSITORY can be the filename for a repository that already exists on the | |
| 3095 | +** local machine or it can be a URI for a remote repository. If REPOSITORY | |
| 3096 | +** is a URI in one of the formats recognized by the [[clone]] command, then | |
| 3097 | +** remote repo is first cloned, then the clone is opened. The clone will be | |
| 3098 | +** stored in the current directory, or in DIR if the "--repodir DIR" option | |
| 3099 | +** is used. The name of the clone will be taken from the last term of the URI. | |
| 3100 | +** For "http:" and "https:" URIs, you can append an extra term to the end of | |
| 3101 | +** the URI to get any repository name you like. For example: | |
| 3102 | +** | |
| 3103 | +** fossil open https://fossil-scm.org/home/new-name | |
| 3076 | 3104 | ** |
| 3077 | -** Open a connection to the local repository in FILENAME. A checkout | |
| 3078 | -** for the repository is created with its root at the working directory. | |
| 3079 | -** If VERSION is specified then that version is checked out. Otherwise | |
| 3080 | -** the latest version is checked out. No files other than "manifest" | |
| 3081 | -** and "manifest.uuid" are modified if the --keep option is present. | |
| 3105 | +** The base URI for cloning is "https://fossil-scm.org/home". The extra | |
| 3106 | +** "new-name" term means that the cloned repository will be called | |
| 3107 | +** "new-name.fossil". | |
| 3082 | 3108 | ** |
| 3083 | 3109 | ** Options: |
| 3084 | 3110 | ** --empty Initialize checkout as being empty, but still connected |
| 3085 | 3111 | ** with the local repository. If you commit this checkout, |
| 3086 | 3112 | ** it will become a new "initial" commit in the repository. |
| 3113 | +** --force Continue with the open even if the working directory is | |
| 3114 | +** not empty. | |
| 3115 | +** --force-missing Force opening a repository with missing content | |
| 3087 | 3116 | ** --keep Only modify the manifest and manifest.uuid files |
| 3088 | 3117 | ** --nested Allow opening a repository inside an opened checkout |
| 3089 | -** --force-missing Force opening a repository with missing content | |
| 3118 | +** --repodir DIR If REPOSITORY is a URI that will be cloned, store | |
| 3119 | +** the clone in DIR rather than in "." | |
| 3090 | 3120 | ** --setmtime Set timestamps of all files to match their SCM-side |
| 3091 | 3121 | ** times (the timestamp of the last checkin which modified |
| 3092 | 3122 | ** them). |
| 3123 | +** --workdir DIR Use DIR as the working directory instead of ".". The DIR | |
| 3124 | +** directory is created if it does not exist. | |
| 3093 | 3125 | ** |
| 3094 | -** See also: close | |
| 3126 | +** See also: [[close]], [[clone]] | |
| 3095 | 3127 | */ |
| 3096 | 3128 | void cmd_open(void){ |
| 3097 | 3129 | int emptyFlag; |
| 3098 | 3130 | int keepFlag; |
| 3099 | 3131 | int forceMissingFlag; |
| 3100 | 3132 | int allowNested; |
| 3101 | 3133 | int allowSymlinks; |
| 3102 | 3134 | int setmtimeFlag; /* --setmtime. Set mtimes on files */ |
| 3135 | + int bForce = 0; /* --force. Open even if non-empty dir */ | |
| 3103 | 3136 | static char *azNewArgv[] = { 0, "checkout", "--prompt", 0, 0, 0, 0 }; |
| 3137 | + const char *zWorkDir; /* --workdir value */ | |
| 3138 | + const char *zRepo = 0; /* Name of the repository file */ | |
| 3139 | + const char *zRepoDir = 0; /* --repodir value */ | |
| 3140 | + char *zPwd; /* Initial working directory */ | |
| 3141 | + int isUri = 0; /* True if REPOSITORY is a URI */ | |
| 3104 | 3142 | |
| 3105 | 3143 | url_proxy_options(); |
| 3106 | 3144 | emptyFlag = find_option("empty",0,0)!=0; |
| 3107 | 3145 | keepFlag = find_option("keep",0,0)!=0; |
| 3108 | 3146 | forceMissingFlag = find_option("force-missing",0,0)!=0; |
| 3109 | 3147 | allowNested = find_option("nested",0,0)!=0; |
| 3110 | 3148 | setmtimeFlag = find_option("setmtime",0,0)!=0; |
| 3149 | + zWorkDir = find_option("workdir",0,1); | |
| 3150 | + zRepoDir = find_option("repodir",0,1); | |
| 3151 | + bForce = find_option("force",0,0)!=0; | |
| 3152 | + zPwd = file_getcwd(0,0); | |
| 3153 | + | |
| 3111 | 3154 | |
| 3112 | 3155 | /* We should be done with options.. */ |
| 3113 | 3156 | verify_all_options(); |
| 3114 | 3157 | |
| 3115 | 3158 | if( g.argc!=3 && g.argc!=4 ){ |
| 3116 | 3159 | usage("REPOSITORY-FILENAME ?VERSION?"); |
| 3117 | 3160 | } |
| 3118 | - if( !allowNested && db_open_local(0) ){ | |
| 3119 | - fossil_fatal("already within an open tree rooted at %s", g.zLocalRoot); | |
| 3161 | + zRepo = g.argv[2]; | |
| 3162 | + if( sqlite3_strglob("http://*", zRepo)==0 | |
| 3163 | + || sqlite3_strglob("https://*", zRepo)==0 | |
| 3164 | + || sqlite3_strglob("ssh:*", zRepo)==0 | |
| 3165 | + || sqlite3_strglob("file:*", zRepo)==0 | |
| 3166 | + ){ | |
| 3167 | + isUri = 1; | |
| 3168 | + } | |
| 3169 | + | |
| 3170 | + /* If --workdir is specified, change to the requested working directory */ | |
| 3171 | + if( zWorkDir ){ | |
| 3172 | + if( !isUri ){ | |
| 3173 | + zRepo = file_canonical_name_dup(zRepo); | |
| 3174 | + } | |
| 3175 | + if( zRepoDir ){ | |
| 3176 | + zRepoDir = file_canonical_name_dup(zRepoDir); | |
| 3177 | + } | |
| 3178 | + if( file_isdir(zWorkDir, ExtFILE)!=1 ){ | |
| 3179 | + file_mkfolder(zWorkDir, ExtFILE, 0, 0); | |
| 3180 | + if( file_mkdir(zWorkDir, ExtFILE, 0) ){ | |
| 3181 | + fossil_fatal("cannot create directory %s", zWorkDir); | |
| 3182 | + } | |
| 3183 | + } | |
| 3184 | + if( file_chdir(zWorkDir, 0) ){ | |
| 3185 | + fossil_fatal("unable to make %s the working directory", zWorkDir); | |
| 3186 | + } | |
| 3187 | + } | |
| 3188 | + if( keepFlag==0 && bForce==0 && file_directory_size(".", 0, 1)>0 ){ | |
| 3189 | + fossil_fatal("directory %s is not empty\n" | |
| 3190 | + "use the --force option to override", file_getcwd(0,0)); | |
| 3191 | + } | |
| 3192 | + | |
| 3193 | + if( db_open_local_v2(0, allowNested) ){ | |
| 3194 | + fossil_fatal("there is already an open tree at %s", g.zLocalRoot); | |
| 3195 | + } | |
| 3196 | + | |
| 3197 | + /* If REPOSITORY looks like a URI, then try to clone it first */ | |
| 3198 | + if( isUri ){ | |
| 3199 | + char *zNewBase; /* Base name of the cloned repository file */ | |
| 3200 | + const char *zUri; /* URI to clone */ | |
| 3201 | + int i; /* Loop counter */ | |
| 3202 | + int rc; /* Result code from fossil_system() */ | |
| 3203 | + Blob cmd; /* Clone command to be run */ | |
| 3204 | + char *zCmd; /* String version of the clone command */ | |
| 3205 | + | |
| 3206 | + zUri = zRepo; | |
| 3207 | + zNewBase = fossil_strdup(file_tail(zUri)); | |
| 3208 | + for(i=(int)strlen(zNewBase)-1; i>1 && zNewBase[i]!='.'; i--){} | |
| 3209 | + if( zNewBase[i]=='.' ) zNewBase[i] = 0; | |
| 3210 | + if( zRepoDir==0 ) zRepoDir = zPwd; | |
| 3211 | + zRepo = mprintf("%s/%s.fossil", zRepoDir, zNewBase); | |
| 3212 | + fossil_free(zNewBase); | |
| 3213 | + blob_init(&cmd, 0, 0); | |
| 3214 | + blob_append_escaped_arg(&cmd, g.nameOfExe); | |
| 3215 | + blob_append(&cmd, " clone", -1); | |
| 3216 | + blob_append_escaped_arg(&cmd, zUri); | |
| 3217 | + blob_append_escaped_arg(&cmd, zRepo); | |
| 3218 | + zCmd = blob_str(&cmd); | |
| 3219 | + fossil_print("%s\n", zCmd); | |
| 3220 | + if( zWorkDir ) file_chdir(zPwd, 0); | |
| 3221 | + rc = fossil_system(zCmd); | |
| 3222 | + if( rc ){ | |
| 3223 | + fossil_fatal("clone of %s failed", zUri); | |
| 3224 | + } | |
| 3225 | + blob_reset(&cmd); | |
| 3226 | + if( zWorkDir ) file_chdir(zWorkDir, 0); | |
| 3227 | + }else if( zRepoDir ){ | |
| 3228 | + fossil_fatal("the --repodir option only makes sense if the REPOSITORY " | |
| 3229 | + "argument is a URI that begins with http:, https:, ssh:, " | |
| 3230 | + "or file:"); | |
| 3120 | 3231 | } |
| 3121 | - db_open_repository(g.argv[2]); | |
| 3232 | + | |
| 3233 | + db_open_repository(zRepo); | |
| 3122 | 3234 | |
| 3123 | 3235 | /* Figure out which revision to open. */ |
| 3124 | 3236 | if( !emptyFlag ){ |
| 3125 | 3237 | if( g.argc==4 ){ |
| 3126 | 3238 | g.zOpenRevision = g.argv[3]; |
| @@ -3169,12 +3281,12 @@ | ||
| 3169 | 3281 | ** point, this will probably be the setting value from the |
| 3170 | 3282 | ** repository or global configuration databases. */ |
| 3171 | 3283 | g.allowSymlinks = db_get_boolean("allow-symlinks", |
| 3172 | 3284 | db_allow_symlinks_by_default()); |
| 3173 | 3285 | } |
| 3174 | - db_lset("repository", g.argv[2]); | |
| 3175 | - db_record_repository_filename(g.argv[2]); | |
| 3286 | + db_lset("repository", zRepo); | |
| 3287 | + db_record_repository_filename(zRepo); | |
| 3176 | 3288 | db_set_checkout(0); |
| 3177 | 3289 | azNewArgv[0] = g.argv[0]; |
| 3178 | 3290 | g.argv = azNewArgv; |
| 3179 | 3291 | if( !emptyFlag ){ |
| 3180 | 3292 | g.argc = 3; |
| @@ -3887,11 +3999,11 @@ | ||
| 3887 | 3999 | ** --global set or unset the given property globally instead of |
| 3888 | 4000 | ** setting or unsetting it for the open repository only. |
| 3889 | 4001 | ** |
| 3890 | 4002 | ** --exact only consider exact name matches. |
| 3891 | 4003 | ** |
| 3892 | -** See also: configuration | |
| 4004 | +** See also: [[configuration]] | |
| 3893 | 4005 | */ |
| 3894 | 4006 | void setting_cmd(void){ |
| 3895 | 4007 | int i; |
| 3896 | 4008 | int globalFlag = find_option("global","g",0)!=0; |
| 3897 | 4009 | int exactFlag = find_option("exact",0,0)!=0; |
| 3898 | 4010 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -116,10 +116,11 @@ | |
| 116 | */ |
| 117 | static struct DbLocalData { |
| 118 | int nBegin; /* Nesting depth of BEGIN */ |
| 119 | int doRollback; /* True to force a rollback */ |
| 120 | int nCommitHook; /* Number of commit hooks */ |
| 121 | Stmt *pAllStmt; /* List of all unfinalized statements */ |
| 122 | int nPrepare; /* Number of calls to sqlite3_prepare_v2() */ |
| 123 | int nDeleteOnFail; /* Number of entries in azDeleteOnFail[] */ |
| 124 | struct sCommitHook { |
| 125 | int (*xHook)(void); /* Functions to call at db_end_transaction() */ |
| @@ -195,10 +196,11 @@ | |
| 195 | sqlite3_commit_hook(g.db, db_verify_at_commit, 0); |
| 196 | db.nPriorChanges = sqlite3_total_changes(g.db); |
| 197 | db.doRollback = 0; |
| 198 | db.zStartFile = zStartFile; |
| 199 | db.iStartLine = iStartLine; |
| 200 | } |
| 201 | db.nBegin++; |
| 202 | } |
| 203 | /* |
| 204 | ** Begin a new transaction for writing. |
| @@ -209,11 +211,12 @@ | |
| 209 | sqlite3_commit_hook(g.db, db_verify_at_commit, 0); |
| 210 | db.nPriorChanges = sqlite3_total_changes(g.db); |
| 211 | db.doRollback = 0; |
| 212 | db.zStartFile = zStartFile; |
| 213 | db.iStartLine = iStartLine; |
| 214 | }else{ |
| 215 | fossil_warning("read txn at %s:%d might cause SQLITE_BUSY " |
| 216 | "for the write txn at %s:%d", |
| 217 | db.zStartFile, db.iStartLine, zStartFile, iStartLine); |
| 218 | } |
| 219 | db.nBegin++; |
| @@ -1328,11 +1331,11 @@ | |
| 1328 | ); |
| 1329 | if( rc!=SQLITE_OK ){ |
| 1330 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 1331 | } |
| 1332 | db_maybe_set_encryption_key(db, zDbName); |
| 1333 | sqlite3_busy_timeout(db, 5000); |
| 1334 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| 1335 | sqlite3_create_function(db, "user", 0, SQLITE_UTF8, 0, db_sql_user, 0, 0); |
| 1336 | sqlite3_create_function(db, "cgi", 1, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); |
| 1337 | sqlite3_create_function(db, "cgi", 2, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); |
| 1338 | sqlite3_create_function(db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); |
| @@ -1344,11 +1347,11 @@ | |
| 1344 | ); |
| 1345 | if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); |
| 1346 | db_add_aux_functions(db); |
| 1347 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1348 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1349 | sqlite3_exec(db, "PRAGMA foreign_keys=OFF;", 0, 0, 0); |
| 1350 | return db; |
| 1351 | } |
| 1352 | |
| 1353 | |
| 1354 | /* |
| @@ -1714,16 +1717,20 @@ | |
| 1714 | ** |
| 1715 | ** If no valid _FOSSIL_ or .fslckout file is found, we move up one level and |
| 1716 | ** try again. Once the file is found, the g.zLocalRoot variable is set |
| 1717 | ** to the root of the repository tree and this routine returns 1. If |
| 1718 | ** no database is found, then this routine return 0. |
| 1719 | ** |
| 1720 | ** This routine always opens the user database regardless of whether or |
| 1721 | ** not the repository database is found. If the _FOSSIL_ or .fslckout file |
| 1722 | ** is found, it is attached to the open database connection too. |
| 1723 | */ |
| 1724 | int db_open_local(const char *zDbName){ |
| 1725 | int i, n; |
| 1726 | char zPwd[2000]; |
| 1727 | static const char *(aDbName[]) = { "_FOSSIL_", ".fslckout", ".fos" }; |
| 1728 | |
| 1729 | if( g.localOpen ) return 1; |
| @@ -1747,18 +1754,22 @@ | |
| 1747 | g.localOpen = 1; |
| 1748 | db_open_repository(zDbName); |
| 1749 | return 1; |
| 1750 | } |
| 1751 | } |
| 1752 | n--; |
| 1753 | while( n>1 && zPwd[n]!='/' ){ n--; } |
| 1754 | while( n>1 && zPwd[n-1]=='/' ){ n--; } |
| 1755 | zPwd[n] = 0; |
| 1756 | } |
| 1757 | |
| 1758 | /* A checkout database file could not be found */ |
| 1759 | return 0; |
| 1760 | } |
| 1761 | |
| 1762 | /* |
| 1763 | ** Get the full pathname to the repository database file. The |
| 1764 | ** local database (the _FOSSIL_ or .fslckout database) must have already |
| @@ -2388,11 +2399,11 @@ | |
| 2388 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2389 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| 2390 | ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
| 2391 | ** means UTC. |
| 2392 | ** |
| 2393 | ** See also: clone |
| 2394 | */ |
| 2395 | void create_repository_cmd(void){ |
| 2396 | char *zPassword; |
| 2397 | const char *zTemplate; /* Repository from which to copy settings */ |
| 2398 | const char *zDate; /* Date of the initial check-in */ |
| @@ -3070,57 +3081,158 @@ | |
| 3070 | } |
| 3071 | |
| 3072 | /* |
| 3073 | ** COMMAND: open |
| 3074 | ** |
| 3075 | ** Usage: %fossil open FILENAME ?VERSION? ?OPTIONS? |
| 3076 | ** |
| 3077 | ** Open a connection to the local repository in FILENAME. A checkout |
| 3078 | ** for the repository is created with its root at the working directory. |
| 3079 | ** If VERSION is specified then that version is checked out. Otherwise |
| 3080 | ** the latest version is checked out. No files other than "manifest" |
| 3081 | ** and "manifest.uuid" are modified if the --keep option is present. |
| 3082 | ** |
| 3083 | ** Options: |
| 3084 | ** --empty Initialize checkout as being empty, but still connected |
| 3085 | ** with the local repository. If you commit this checkout, |
| 3086 | ** it will become a new "initial" commit in the repository. |
| 3087 | ** --keep Only modify the manifest and manifest.uuid files |
| 3088 | ** --nested Allow opening a repository inside an opened checkout |
| 3089 | ** --force-missing Force opening a repository with missing content |
| 3090 | ** --setmtime Set timestamps of all files to match their SCM-side |
| 3091 | ** times (the timestamp of the last checkin which modified |
| 3092 | ** them). |
| 3093 | ** |
| 3094 | ** See also: close |
| 3095 | */ |
| 3096 | void cmd_open(void){ |
| 3097 | int emptyFlag; |
| 3098 | int keepFlag; |
| 3099 | int forceMissingFlag; |
| 3100 | int allowNested; |
| 3101 | int allowSymlinks; |
| 3102 | int setmtimeFlag; /* --setmtime. Set mtimes on files */ |
| 3103 | static char *azNewArgv[] = { 0, "checkout", "--prompt", 0, 0, 0, 0 }; |
| 3104 | |
| 3105 | url_proxy_options(); |
| 3106 | emptyFlag = find_option("empty",0,0)!=0; |
| 3107 | keepFlag = find_option("keep",0,0)!=0; |
| 3108 | forceMissingFlag = find_option("force-missing",0,0)!=0; |
| 3109 | allowNested = find_option("nested",0,0)!=0; |
| 3110 | setmtimeFlag = find_option("setmtime",0,0)!=0; |
| 3111 | |
| 3112 | /* We should be done with options.. */ |
| 3113 | verify_all_options(); |
| 3114 | |
| 3115 | if( g.argc!=3 && g.argc!=4 ){ |
| 3116 | usage("REPOSITORY-FILENAME ?VERSION?"); |
| 3117 | } |
| 3118 | if( !allowNested && db_open_local(0) ){ |
| 3119 | fossil_fatal("already within an open tree rooted at %s", g.zLocalRoot); |
| 3120 | } |
| 3121 | db_open_repository(g.argv[2]); |
| 3122 | |
| 3123 | /* Figure out which revision to open. */ |
| 3124 | if( !emptyFlag ){ |
| 3125 | if( g.argc==4 ){ |
| 3126 | g.zOpenRevision = g.argv[3]; |
| @@ -3169,12 +3281,12 @@ | |
| 3169 | ** point, this will probably be the setting value from the |
| 3170 | ** repository or global configuration databases. */ |
| 3171 | g.allowSymlinks = db_get_boolean("allow-symlinks", |
| 3172 | db_allow_symlinks_by_default()); |
| 3173 | } |
| 3174 | db_lset("repository", g.argv[2]); |
| 3175 | db_record_repository_filename(g.argv[2]); |
| 3176 | db_set_checkout(0); |
| 3177 | azNewArgv[0] = g.argv[0]; |
| 3178 | g.argv = azNewArgv; |
| 3179 | if( !emptyFlag ){ |
| 3180 | g.argc = 3; |
| @@ -3887,11 +3999,11 @@ | |
| 3887 | ** --global set or unset the given property globally instead of |
| 3888 | ** setting or unsetting it for the open repository only. |
| 3889 | ** |
| 3890 | ** --exact only consider exact name matches. |
| 3891 | ** |
| 3892 | ** See also: configuration |
| 3893 | */ |
| 3894 | void setting_cmd(void){ |
| 3895 | int i; |
| 3896 | int globalFlag = find_option("global","g",0)!=0; |
| 3897 | int exactFlag = find_option("exact",0,0)!=0; |
| 3898 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -116,10 +116,11 @@ | |
| 116 | */ |
| 117 | static struct DbLocalData { |
| 118 | int nBegin; /* Nesting depth of BEGIN */ |
| 119 | int doRollback; /* True to force a rollback */ |
| 120 | int nCommitHook; /* Number of commit hooks */ |
| 121 | int wrTxn; /* Outer-most TNX is a write */ |
| 122 | Stmt *pAllStmt; /* List of all unfinalized statements */ |
| 123 | int nPrepare; /* Number of calls to sqlite3_prepare_v2() */ |
| 124 | int nDeleteOnFail; /* Number of entries in azDeleteOnFail[] */ |
| 125 | struct sCommitHook { |
| 126 | int (*xHook)(void); /* Functions to call at db_end_transaction() */ |
| @@ -195,10 +196,11 @@ | |
| 196 | sqlite3_commit_hook(g.db, db_verify_at_commit, 0); |
| 197 | db.nPriorChanges = sqlite3_total_changes(g.db); |
| 198 | db.doRollback = 0; |
| 199 | db.zStartFile = zStartFile; |
| 200 | db.iStartLine = iStartLine; |
| 201 | db.wrTxn = 0; |
| 202 | } |
| 203 | db.nBegin++; |
| 204 | } |
| 205 | /* |
| 206 | ** Begin a new transaction for writing. |
| @@ -209,11 +211,12 @@ | |
| 211 | sqlite3_commit_hook(g.db, db_verify_at_commit, 0); |
| 212 | db.nPriorChanges = sqlite3_total_changes(g.db); |
| 213 | db.doRollback = 0; |
| 214 | db.zStartFile = zStartFile; |
| 215 | db.iStartLine = iStartLine; |
| 216 | db.wrTxn = 1; |
| 217 | }else if( !db.wrTxn ){ |
| 218 | fossil_warning("read txn at %s:%d might cause SQLITE_BUSY " |
| 219 | "for the write txn at %s:%d", |
| 220 | db.zStartFile, db.iStartLine, zStartFile, iStartLine); |
| 221 | } |
| 222 | db.nBegin++; |
| @@ -1328,11 +1331,11 @@ | |
| 1331 | ); |
| 1332 | if( rc!=SQLITE_OK ){ |
| 1333 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 1334 | } |
| 1335 | db_maybe_set_encryption_key(db, zDbName); |
| 1336 | sqlite3_busy_timeout(db, 15000); |
| 1337 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| 1338 | sqlite3_create_function(db, "user", 0, SQLITE_UTF8, 0, db_sql_user, 0, 0); |
| 1339 | sqlite3_create_function(db, "cgi", 1, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); |
| 1340 | sqlite3_create_function(db, "cgi", 2, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); |
| 1341 | sqlite3_create_function(db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); |
| @@ -1344,11 +1347,11 @@ | |
| 1347 | ); |
| 1348 | if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); |
| 1349 | db_add_aux_functions(db); |
| 1350 | re_add_sql_func(db); /* The REGEXP operator */ |
| 1351 | foci_register(db); /* The "files_of_checkin" virtual table */ |
| 1352 | sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_FKEY, 0, &rc); |
| 1353 | return db; |
| 1354 | } |
| 1355 | |
| 1356 | |
| 1357 | /* |
| @@ -1714,16 +1717,20 @@ | |
| 1717 | ** |
| 1718 | ** If no valid _FOSSIL_ or .fslckout file is found, we move up one level and |
| 1719 | ** try again. Once the file is found, the g.zLocalRoot variable is set |
| 1720 | ** to the root of the repository tree and this routine returns 1. If |
| 1721 | ** no database is found, then this routine return 0. |
| 1722 | ** |
| 1723 | ** In db_open_local_v2(), if the bRootOnly flag is true, then only |
| 1724 | ** look in the CWD for the checkout database. Do not scan upwards in |
| 1725 | ** the file hierarchy. |
| 1726 | ** |
| 1727 | ** This routine always opens the user database regardless of whether or |
| 1728 | ** not the repository database is found. If the _FOSSIL_ or .fslckout file |
| 1729 | ** is found, it is attached to the open database connection too. |
| 1730 | */ |
| 1731 | int db_open_local_v2(const char *zDbName, int bRootOnly){ |
| 1732 | int i, n; |
| 1733 | char zPwd[2000]; |
| 1734 | static const char *(aDbName[]) = { "_FOSSIL_", ".fslckout", ".fos" }; |
| 1735 | |
| 1736 | if( g.localOpen ) return 1; |
| @@ -1747,18 +1754,22 @@ | |
| 1754 | g.localOpen = 1; |
| 1755 | db_open_repository(zDbName); |
| 1756 | return 1; |
| 1757 | } |
| 1758 | } |
| 1759 | if( bRootOnly ) break; |
| 1760 | n--; |
| 1761 | while( n>1 && zPwd[n]!='/' ){ n--; } |
| 1762 | while( n>1 && zPwd[n-1]=='/' ){ n--; } |
| 1763 | zPwd[n] = 0; |
| 1764 | } |
| 1765 | |
| 1766 | /* A checkout database file could not be found */ |
| 1767 | return 0; |
| 1768 | } |
| 1769 | int db_open_local(const char *zDbName){ |
| 1770 | return db_open_local_v2(zDbName, 0); |
| 1771 | } |
| 1772 | |
| 1773 | /* |
| 1774 | ** Get the full pathname to the repository database file. The |
| 1775 | ** local database (the _FOSSIL_ or .fslckout database) must have already |
| @@ -2388,11 +2399,11 @@ | |
| 2399 | ** year-month-day form, it may be truncated, the "T" may be replaced by |
| 2400 | ** a space, and it may also name a timezone offset from UTC as "-HH:MM" |
| 2401 | ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" |
| 2402 | ** means UTC. |
| 2403 | ** |
| 2404 | ** See also: [[clone]] |
| 2405 | */ |
| 2406 | void create_repository_cmd(void){ |
| 2407 | char *zPassword; |
| 2408 | const char *zTemplate; /* Repository from which to copy settings */ |
| 2409 | const char *zDate; /* Date of the initial check-in */ |
| @@ -3070,57 +3081,158 @@ | |
| 3081 | } |
| 3082 | |
| 3083 | /* |
| 3084 | ** COMMAND: open |
| 3085 | ** |
| 3086 | ** Usage: %fossil open REPOSITORY ?VERSION? ?OPTIONS? |
| 3087 | ** |
| 3088 | ** Open a new connection to the repository name REPOSITORY. A checkout |
| 3089 | ** for the repository is created with its root at the current working |
| 3090 | ** directory, or in DIR if the "--workdir DIR" is used. If VERSION is |
| 3091 | ** specified then that version is checked out. Otherwise the most recent |
| 3092 | ** check-in on the main branch (usually "trunk") is used. |
| 3093 | ** |
| 3094 | ** REPOSITORY can be the filename for a repository that already exists on the |
| 3095 | ** local machine or it can be a URI for a remote repository. If REPOSITORY |
| 3096 | ** is a URI in one of the formats recognized by the [[clone]] command, then |
| 3097 | ** remote repo is first cloned, then the clone is opened. The clone will be |
| 3098 | ** stored in the current directory, or in DIR if the "--repodir DIR" option |
| 3099 | ** is used. The name of the clone will be taken from the last term of the URI. |
| 3100 | ** For "http:" and "https:" URIs, you can append an extra term to the end of |
| 3101 | ** the URI to get any repository name you like. For example: |
| 3102 | ** |
| 3103 | ** fossil open https://fossil-scm.org/home/new-name |
| 3104 | ** |
| 3105 | ** The base URI for cloning is "https://fossil-scm.org/home". The extra |
| 3106 | ** "new-name" term means that the cloned repository will be called |
| 3107 | ** "new-name.fossil". |
| 3108 | ** |
| 3109 | ** Options: |
| 3110 | ** --empty Initialize checkout as being empty, but still connected |
| 3111 | ** with the local repository. If you commit this checkout, |
| 3112 | ** it will become a new "initial" commit in the repository. |
| 3113 | ** --force Continue with the open even if the working directory is |
| 3114 | ** not empty. |
| 3115 | ** --force-missing Force opening a repository with missing content |
| 3116 | ** --keep Only modify the manifest and manifest.uuid files |
| 3117 | ** --nested Allow opening a repository inside an opened checkout |
| 3118 | ** --repodir DIR If REPOSITORY is a URI that will be cloned, store |
| 3119 | ** the clone in DIR rather than in "." |
| 3120 | ** --setmtime Set timestamps of all files to match their SCM-side |
| 3121 | ** times (the timestamp of the last checkin which modified |
| 3122 | ** them). |
| 3123 | ** --workdir DIR Use DIR as the working directory instead of ".". The DIR |
| 3124 | ** directory is created if it does not exist. |
| 3125 | ** |
| 3126 | ** See also: [[close]], [[clone]] |
| 3127 | */ |
| 3128 | void cmd_open(void){ |
| 3129 | int emptyFlag; |
| 3130 | int keepFlag; |
| 3131 | int forceMissingFlag; |
| 3132 | int allowNested; |
| 3133 | int allowSymlinks; |
| 3134 | int setmtimeFlag; /* --setmtime. Set mtimes on files */ |
| 3135 | int bForce = 0; /* --force. Open even if non-empty dir */ |
| 3136 | static char *azNewArgv[] = { 0, "checkout", "--prompt", 0, 0, 0, 0 }; |
| 3137 | const char *zWorkDir; /* --workdir value */ |
| 3138 | const char *zRepo = 0; /* Name of the repository file */ |
| 3139 | const char *zRepoDir = 0; /* --repodir value */ |
| 3140 | char *zPwd; /* Initial working directory */ |
| 3141 | int isUri = 0; /* True if REPOSITORY is a URI */ |
| 3142 | |
| 3143 | url_proxy_options(); |
| 3144 | emptyFlag = find_option("empty",0,0)!=0; |
| 3145 | keepFlag = find_option("keep",0,0)!=0; |
| 3146 | forceMissingFlag = find_option("force-missing",0,0)!=0; |
| 3147 | allowNested = find_option("nested",0,0)!=0; |
| 3148 | setmtimeFlag = find_option("setmtime",0,0)!=0; |
| 3149 | zWorkDir = find_option("workdir",0,1); |
| 3150 | zRepoDir = find_option("repodir",0,1); |
| 3151 | bForce = find_option("force",0,0)!=0; |
| 3152 | zPwd = file_getcwd(0,0); |
| 3153 | |
| 3154 | |
| 3155 | /* We should be done with options.. */ |
| 3156 | verify_all_options(); |
| 3157 | |
| 3158 | if( g.argc!=3 && g.argc!=4 ){ |
| 3159 | usage("REPOSITORY-FILENAME ?VERSION?"); |
| 3160 | } |
| 3161 | zRepo = g.argv[2]; |
| 3162 | if( sqlite3_strglob("http://*", zRepo)==0 |
| 3163 | || sqlite3_strglob("https://*", zRepo)==0 |
| 3164 | || sqlite3_strglob("ssh:*", zRepo)==0 |
| 3165 | || sqlite3_strglob("file:*", zRepo)==0 |
| 3166 | ){ |
| 3167 | isUri = 1; |
| 3168 | } |
| 3169 | |
| 3170 | /* If --workdir is specified, change to the requested working directory */ |
| 3171 | if( zWorkDir ){ |
| 3172 | if( !isUri ){ |
| 3173 | zRepo = file_canonical_name_dup(zRepo); |
| 3174 | } |
| 3175 | if( zRepoDir ){ |
| 3176 | zRepoDir = file_canonical_name_dup(zRepoDir); |
| 3177 | } |
| 3178 | if( file_isdir(zWorkDir, ExtFILE)!=1 ){ |
| 3179 | file_mkfolder(zWorkDir, ExtFILE, 0, 0); |
| 3180 | if( file_mkdir(zWorkDir, ExtFILE, 0) ){ |
| 3181 | fossil_fatal("cannot create directory %s", zWorkDir); |
| 3182 | } |
| 3183 | } |
| 3184 | if( file_chdir(zWorkDir, 0) ){ |
| 3185 | fossil_fatal("unable to make %s the working directory", zWorkDir); |
| 3186 | } |
| 3187 | } |
| 3188 | if( keepFlag==0 && bForce==0 && file_directory_size(".", 0, 1)>0 ){ |
| 3189 | fossil_fatal("directory %s is not empty\n" |
| 3190 | "use the --force option to override", file_getcwd(0,0)); |
| 3191 | } |
| 3192 | |
| 3193 | if( db_open_local_v2(0, allowNested) ){ |
| 3194 | fossil_fatal("there is already an open tree at %s", g.zLocalRoot); |
| 3195 | } |
| 3196 | |
| 3197 | /* If REPOSITORY looks like a URI, then try to clone it first */ |
| 3198 | if( isUri ){ |
| 3199 | char *zNewBase; /* Base name of the cloned repository file */ |
| 3200 | const char *zUri; /* URI to clone */ |
| 3201 | int i; /* Loop counter */ |
| 3202 | int rc; /* Result code from fossil_system() */ |
| 3203 | Blob cmd; /* Clone command to be run */ |
| 3204 | char *zCmd; /* String version of the clone command */ |
| 3205 | |
| 3206 | zUri = zRepo; |
| 3207 | zNewBase = fossil_strdup(file_tail(zUri)); |
| 3208 | for(i=(int)strlen(zNewBase)-1; i>1 && zNewBase[i]!='.'; i--){} |
| 3209 | if( zNewBase[i]=='.' ) zNewBase[i] = 0; |
| 3210 | if( zRepoDir==0 ) zRepoDir = zPwd; |
| 3211 | zRepo = mprintf("%s/%s.fossil", zRepoDir, zNewBase); |
| 3212 | fossil_free(zNewBase); |
| 3213 | blob_init(&cmd, 0, 0); |
| 3214 | blob_append_escaped_arg(&cmd, g.nameOfExe); |
| 3215 | blob_append(&cmd, " clone", -1); |
| 3216 | blob_append_escaped_arg(&cmd, zUri); |
| 3217 | blob_append_escaped_arg(&cmd, zRepo); |
| 3218 | zCmd = blob_str(&cmd); |
| 3219 | fossil_print("%s\n", zCmd); |
| 3220 | if( zWorkDir ) file_chdir(zPwd, 0); |
| 3221 | rc = fossil_system(zCmd); |
| 3222 | if( rc ){ |
| 3223 | fossil_fatal("clone of %s failed", zUri); |
| 3224 | } |
| 3225 | blob_reset(&cmd); |
| 3226 | if( zWorkDir ) file_chdir(zWorkDir, 0); |
| 3227 | }else if( zRepoDir ){ |
| 3228 | fossil_fatal("the --repodir option only makes sense if the REPOSITORY " |
| 3229 | "argument is a URI that begins with http:, https:, ssh:, " |
| 3230 | "or file:"); |
| 3231 | } |
| 3232 | |
| 3233 | db_open_repository(zRepo); |
| 3234 | |
| 3235 | /* Figure out which revision to open. */ |
| 3236 | if( !emptyFlag ){ |
| 3237 | if( g.argc==4 ){ |
| 3238 | g.zOpenRevision = g.argv[3]; |
| @@ -3169,12 +3281,12 @@ | |
| 3281 | ** point, this will probably be the setting value from the |
| 3282 | ** repository or global configuration databases. */ |
| 3283 | g.allowSymlinks = db_get_boolean("allow-symlinks", |
| 3284 | db_allow_symlinks_by_default()); |
| 3285 | } |
| 3286 | db_lset("repository", zRepo); |
| 3287 | db_record_repository_filename(zRepo); |
| 3288 | db_set_checkout(0); |
| 3289 | azNewArgv[0] = g.argv[0]; |
| 3290 | g.argv = azNewArgv; |
| 3291 | if( !emptyFlag ){ |
| 3292 | g.argc = 3; |
| @@ -3887,11 +3999,11 @@ | |
| 3999 | ** --global set or unset the given property globally instead of |
| 4000 | ** setting or unsetting it for the open repository only. |
| 4001 | ** |
| 4002 | ** --exact only consider exact name matches. |
| 4003 | ** |
| 4004 | ** See also: [[configuration]] |
| 4005 | */ |
| 4006 | void setting_cmd(void){ |
| 4007 | int i; |
| 4008 | int globalFlag = find_option("global","g",0)!=0; |
| 4009 | int exactFlag = find_option("exact",0,0)!=0; |
| 4010 |
+375
-21
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -440,16 +440,10 @@ | ||
| 440 | 440 | content:"'"; |
| 441 | 441 | } |
| 442 | 442 | span.usertype:after { |
| 443 | 443 | content:"'"; |
| 444 | 444 | } |
| 445 | -div.selectedText { | |
| 446 | - font-weight: bold; | |
| 447 | - color: blue; | |
| 448 | - background-color: #d5d5ff; | |
| 449 | - border: 1px blue solid; | |
| 450 | -} | |
| 451 | 445 | p.missingPriv { |
| 452 | 446 | color: blue; |
| 453 | 447 | } |
| 454 | 448 | span.wikiruleHead { |
| 455 | 449 | font-weight: bold; |
| @@ -774,44 +768,93 @@ | ||
| 774 | 768 | div.forumTime > div > form, |
| 775 | 769 | div.forumHierRoot > div > form { |
| 776 | 770 | margin: 0.5em 0; |
| 777 | 771 | } |
| 778 | 772 | .forum-post-collapser { |
| 773 | + /* Common style for the bottom-of-post and right-of-post | |
| 774 | + expand/collapse widgets. */ | |
| 779 | 775 | font-size: 0.8em; |
| 780 | - margin-top: 0.2em; | |
| 781 | 776 | padding: 0; |
| 777 | + border: 1px solid rgba(0, 0, 0, 0.2); | |
| 778 | + border-radius: 0 0 0.5em 0.5em; | |
| 779 | + background-color: rgba(0, 0, 0, 0.05); | |
| 780 | + opacity: 0.8; | |
| 781 | + cursor: pointer; | |
| 782 | +} | |
| 783 | +.forum-post-collapser.bottom { | |
| 784 | + margin: 0 0 0.4em 0; | |
| 782 | 785 | height: 1.75em; |
| 783 | 786 | line-height: 1.75em; |
| 784 | 787 | /* ^^^ Those sizes are finely tuned for the current selection of |
| 785 | 788 | arrow characters. If those change, these should, too. Remember that |
| 786 | 789 | FF/Chrome simply do not agree on alignment with most values :/. */ |
| 787 | - border-width: 1px; | |
| 788 | - border-style: solid; | |
| 789 | - border-radius: 0.25em; | |
| 790 | - opacity: 0.8; | |
| 791 | - cursor: pointer; | |
| 792 | 790 | display: flex; |
| 793 | 791 | flex-direction: row; |
| 794 | 792 | justify-content: space-between; |
| 795 | 793 | } |
| 796 | -.forum-post-collapser > span { | |
| 794 | +.forum-post-collapser.bottom > span { | |
| 797 | 795 | margin: 0 1em 0 1em; |
| 798 | 796 | vertical-align: middle; |
| 799 | 797 | } |
| 800 | -.forum-post-collapser.expanded > span::before { | |
| 801 | - content: "⇡⇡⇡" /*reminder: FF/Chrome cannot agree on alignment of ⮝*/; | |
| 802 | -} | |
| 803 | -.forum-post-collapser:not(.expanded) > span::before { | |
| 798 | +.forum-post-collapser.bottom > span::before { | |
| 804 | 799 | content: "⇣⇣⇣"; |
| 805 | 800 | } |
| 801 | +.forum-post-collapser.bottom.expanded > span::before { | |
| 802 | + content: "⇡⇡⇡" /*reminder: FF/Chrome cannot agree on alignment of ⮝*/; | |
| 803 | +} | |
| 806 | 804 | div.forumPostBody{ |
| 807 | 805 | max-height: 50em; |
| 808 | 806 | overflow: auto; |
| 807 | +} | |
| 808 | +div.forumPostBody.with-expander { | |
| 809 | + display: flex; | |
| 810 | + flex-direction: row; | |
| 811 | + overflow: auto; | |
| 812 | +} | |
| 813 | +div.forumPostBody.with-expander:not(.expanded) > :first-child { | |
| 814 | + overflow-y: hidden; | |
| 815 | +} | |
| 816 | +div.forumPostBody.with-expander > *:first-child { | |
| 817 | + /* Main content DIV/PRE */ | |
| 818 | + overflow: auto; | |
| 819 | + flex: 10 1 auto; | |
| 820 | +} | |
| 821 | +div.forumPostBody.with-expander.expanded > *:first-child { | |
| 822 | + margin-bottom: 0.5em /* try to suppress scroll bar */; | |
| 823 | +} | |
| 824 | +div.forumPostBody.with-expander .forum-post-collapser.right { | |
| 825 | + /* "Tap zone" for expansion of the post, sits to the right of the | |
| 826 | + post's content. */ | |
| 827 | + flex: 1 10 auto; | |
| 828 | + min-width: 1.25em; | |
| 829 | + max-width: 1.25em; | |
| 830 | + margin: 0 0 0 0.2em; | |
| 831 | + overflow: hidden; | |
| 832 | + display: flex; | |
| 833 | + flex-direction: column; | |
| 834 | + justify-content: space-around; | |
| 835 | + align-items: center; | |
| 836 | + border-radius: 0.1em; | |
| 837 | + cursor: pointer; | |
| 838 | + border-bottom: 0; | |
| 839 | + border-radius: 0 0.5em 0 0; | |
| 840 | +} | |
| 841 | +div.forumPostBody.with-expander .forum-post-collapser.right > span:before { | |
| 842 | + content: "⇣"; | |
| 843 | +} | |
| 844 | +div.forumPostBody.with-expander.expanded .forum-post-collapser.right > span:before { | |
| 845 | + content: "⇡"; | |
| 809 | 846 | } |
| 810 | 847 | div.forumPostBody.expanded { |
| 811 | 848 | max-height: initial; |
| 812 | 849 | } |
| 850 | +div.forumPostBody.shrunken { | |
| 851 | + /* When an expandable post is un-expanded, it is shrunkend down | |
| 852 | + to this size instead of its original size. */ | |
| 853 | + max-height: 8em; | |
| 854 | +} | |
| 855 | + | |
| 813 | 856 | div.forumSel { |
| 814 | 857 | background-color: #cef; |
| 815 | 858 | } |
| 816 | 859 | div.forumObs { |
| 817 | 860 | color: #bbb; |
| @@ -904,14 +947,19 @@ | ||
| 904 | 947 | color: darkred; |
| 905 | 948 | background: yellow; |
| 906 | 949 | opacity: 0.7; |
| 907 | 950 | } |
| 908 | 951 | .hidden { |
| 909 | - position: absolute; | |
| 910 | - opacity: 0; | |
| 911 | - pointer-events: none; | |
| 912 | - display: none; | |
| 952 | + /* The framework-wide way of hiding elements is to assign them this | |
| 953 | + CSS class. To make them visible again, remove it. The !important | |
| 954 | + qualifiers are unfortunate but sometimes necessary when hidden | |
| 955 | + element has other classes which specify visibility-related | |
| 956 | + options. */ | |
| 957 | + position: absolute !important; | |
| 958 | + opacity: 0 !important; | |
| 959 | + pointer-events: none !important; | |
| 960 | + display: none !important; | |
| 913 | 961 | } |
| 914 | 962 | input { |
| 915 | 963 | max-width: 95%; |
| 916 | 964 | } |
| 917 | 965 | textarea { |
| @@ -919,5 +967,311 @@ | ||
| 919 | 967 | } |
| 920 | 968 | img { |
| 921 | 969 | max-width: 100%; |
| 922 | 970 | height: auto; |
| 923 | 971 | } |
| 972 | +hr { | |
| 973 | + /* Needed to keep /dir README.txt from floating right in some skins */ | |
| 974 | + clear: both; | |
| 975 | +} | |
| 976 | + | |
| 977 | +/** | |
| 978 | + .tab-xxx: styles for fossil.tabs.js. | |
| 979 | +*/ | |
| 980 | +.tab-container { | |
| 981 | + width: 100%; | |
| 982 | + display: flex; | |
| 983 | + flex-direction: column; | |
| 984 | + align-items: stretch; | |
| 985 | +} | |
| 986 | +.tab-container > #fossil-status-bar { | |
| 987 | + margin-top: 0; | |
| 988 | +} | |
| 989 | +.tab-container > .tabs { | |
| 990 | + padding: 0.25em; | |
| 991 | + margin: 0; | |
| 992 | + display: flex; | |
| 993 | + flex-direction: column; | |
| 994 | + border-width: 1px; | |
| 995 | + border-style: outset; | |
| 996 | + border-color: inherit; | |
| 997 | +} | |
| 998 | +.tab-container > .tabs > .tab-panel { | |
| 999 | + align-self: stretch; | |
| 1000 | + flex: 10 1 auto; | |
| 1001 | + display: block; | |
| 1002 | + border: 0; | |
| 1003 | + padding: 0; | |
| 1004 | + margin: 0; | |
| 1005 | +} | |
| 1006 | +.tab-container > .tab-bar { | |
| 1007 | + display: flex; | |
| 1008 | + flex-direction: row; | |
| 1009 | + flex: 1 10 auto; | |
| 1010 | + align-self: stretch; | |
| 1011 | + flex-wrap: wrap; | |
| 1012 | +} | |
| 1013 | +.tab-container > .tab-bar > .tab-button { | |
| 1014 | + display: inline-block; | |
| 1015 | + border-radius: 0.25em 0.25em 0 0; | |
| 1016 | + margin: 0 0.1em; | |
| 1017 | + padding: 0.25em 0.75em; | |
| 1018 | + align-self: baseline; | |
| 1019 | + border-color: inherit; | |
| 1020 | + border-width: 1px; | |
| 1021 | + border-bottom: none; | |
| 1022 | + border-top-style: inset; | |
| 1023 | + border-left-style: inset; | |
| 1024 | + border-right-style: inset; | |
| 1025 | + cursor: pointer; | |
| 1026 | + opacity: 0.6; | |
| 1027 | +} | |
| 1028 | +.tab-container > .tab-bar > .tab-button.selected { | |
| 1029 | + text-decoration: underline; | |
| 1030 | + opacity: 1.0; | |
| 1031 | + border-top-style: outset; | |
| 1032 | + border-left-style: outset; | |
| 1033 | + border-right-style: outset; | |
| 1034 | +} | |
| 1035 | + | |
| 1036 | +/** | |
| 1037 | + The flex-xxx classes can be used to create basic flexbox layouts | |
| 1038 | + through the application of classes to the containing/contained | |
| 1039 | + objects. | |
| 1040 | +*/ | |
| 1041 | +.flex-container { | |
| 1042 | + display: flex; | |
| 1043 | +} | |
| 1044 | +.flex-container.flex-row { | |
| 1045 | + flex-direction: row; | |
| 1046 | + flex-wrap: wrap; | |
| 1047 | + justify-content: center; | |
| 1048 | + align-items: center; | |
| 1049 | +} | |
| 1050 | +.flex-container .flex-grow { | |
| 1051 | + flex-grow: 10; | |
| 1052 | + flex-shrink: 0; | |
| 1053 | +} | |
| 1054 | +.flex-container .flex-shrink { | |
| 1055 | + flex-grow: 0; | |
| 1056 | + flex-shrink: 10; | |
| 1057 | +} | |
| 1058 | +.flex-container.flex-row.stretch { | |
| 1059 | + flex-wrap: wrap; | |
| 1060 | + align-items: baseline; | |
| 1061 | + justify-content: stretch; | |
| 1062 | + margin: 0; | |
| 1063 | +} | |
| 1064 | +.flex-container.flex-column { | |
| 1065 | + flex-direction: column; | |
| 1066 | + flex-wrap: wrap; | |
| 1067 | + justify-content: center; | |
| 1068 | + align-items: center; | |
| 1069 | +} | |
| 1070 | +.flex-container.flex-column.stretch { | |
| 1071 | + align-items: stretch; | |
| 1072 | + margin: 0; | |
| 1073 | +} | |
| 1074 | +.flex-container.child-gap-small > * { | |
| 1075 | + margin: 0.25em; | |
| 1076 | +} | |
| 1077 | +#fossil-status-bar { | |
| 1078 | + display: block; | |
| 1079 | + font-family: monospace; | |
| 1080 | + border-width: 1px; | |
| 1081 | + border-style: inset; | |
| 1082 | + border-color: inherit; | |
| 1083 | + min-height: 1.5em; | |
| 1084 | + font-size: 1.2em; | |
| 1085 | + padding: 0.2em; | |
| 1086 | + margin: 0.25em 0; | |
| 1087 | + flex: 0 0 auto; | |
| 1088 | +} | |
| 1089 | +.font-size-100 { | |
| 1090 | + font-size: 100%; | |
| 1091 | +} | |
| 1092 | +.font-size-125 { | |
| 1093 | + font-size: 125%; | |
| 1094 | +} | |
| 1095 | +.font-size-150 { | |
| 1096 | + font-size: 150%; | |
| 1097 | +} | |
| 1098 | +.font-size-175 { | |
| 1099 | + font-size: 175%; | |
| 1100 | +} | |
| 1101 | +.font-size-200 { | |
| 1102 | + font-size: 200%; | |
| 1103 | +} | |
| 1104 | + | |
| 1105 | +/** | |
| 1106 | + .input-with-label is intended to be a wrapper element which | |
| 1107 | + contain both a LABEL tag and an INPUT or SELECT control. | |
| 1108 | + The wrapper is "necessary", as opposed to placing the INPUT | |
| 1109 | + in the LABEL, so that we can include multiple INPUT | |
| 1110 | + elements (e.g. a set of radio buttons). | |
| 1111 | +*/ | |
| 1112 | +.input-with-label { | |
| 1113 | + border: 1px inset #808080; | |
| 1114 | + border-radius: 0.25em; | |
| 1115 | + padding: 0.25em 0.4em; | |
| 1116 | + margin: 0 0.5em; | |
| 1117 | + display: inline-block; | |
| 1118 | + cursor: default; | |
| 1119 | +} | |
| 1120 | +.input-with-label > * { | |
| 1121 | + vertical-align: middle; | |
| 1122 | +} | |
| 1123 | +.input-with-label > label { | |
| 1124 | + display: inline; /* some skins set label display to block! */ | |
| 1125 | +} | |
| 1126 | +.input-with-label > input { | |
| 1127 | + margin: 0; | |
| 1128 | +} | |
| 1129 | +.input-with-label > button { | |
| 1130 | + margin: 0; | |
| 1131 | +} | |
| 1132 | +.input-with-label > select { | |
| 1133 | + margin: 0; | |
| 1134 | +} | |
| 1135 | +.input-with-label > input[type=text] { | |
| 1136 | + margin: 0; | |
| 1137 | +} | |
| 1138 | +.input-with-label > textarea { | |
| 1139 | + margin: 0; | |
| 1140 | +} | |
| 1141 | +.input-with-label > input[type=checkbox] { | |
| 1142 | + vertical-align: sub; | |
| 1143 | +} | |
| 1144 | +.input-with-label > input[type=radio] { | |
| 1145 | + vertical-align: sub; | |
| 1146 | +} | |
| 1147 | +.input-with-label > label { | |
| 1148 | + font-weight: initial; | |
| 1149 | + margin: 0 0.25em 0 0.25em; | |
| 1150 | + vertical-align: middle; | |
| 1151 | +} | |
| 1152 | + | |
| 1153 | +table.numbered-lines { | |
| 1154 | + width: 100%; | |
| 1155 | + table-layout: fixed /* required to keep ultra-wide code from exceeding | |
| 1156 | + window width, and instead force a scrollbar | |
| 1157 | + on them. */; | |
| 1158 | +} | |
| 1159 | +table.numbered-lines > tbody > tr { | |
| 1160 | + font-family: monospace; | |
| 1161 | + font-size: 1.2em; | |
| 1162 | + line-height: 1.35; | |
| 1163 | + white-space: pre; | |
| 1164 | +} | |
| 1165 | +table.numbered-lines > tbody > tr > td { | |
| 1166 | + font-family: inherit; | |
| 1167 | + font-size: inherit; | |
| 1168 | + line-height: inherit; | |
| 1169 | + white-space: inherit; | |
| 1170 | + margin: 0; | |
| 1171 | + vertical-align: top; | |
| 1172 | + padding: 0.25em 0 0 0 /*prevents slight overlap at top */; | |
| 1173 | +} | |
| 1174 | +table.numbered-lines td.line-numbers { | |
| 1175 | + width: 4.5em; | |
| 1176 | +} | |
| 1177 | +table.numbered-lines td.line-numbers > span:first-of-type { | |
| 1178 | + margin-top: 0.25em/*must match top PADDING of | |
| 1179 | + td.file-content > pre > code*/; | |
| 1180 | +} | |
| 1181 | +table.numbered-lines td.line-numbers > span { | |
| 1182 | + display: block; | |
| 1183 | + margin: 0; | |
| 1184 | + padding: 0; | |
| 1185 | + line-height: inherit; | |
| 1186 | + font-size: inherit; | |
| 1187 | + font-family: inherit; | |
| 1188 | + cursor: pointer; | |
| 1189 | + white-space: pre; | |
| 1190 | + margin-right: 2px/*keep selection from nudging the right column */; | |
| 1191 | + text-align: right; | |
| 1192 | +} | |
| 1193 | +table.numbered-lines td.line-numbers > span:hover { | |
| 1194 | + background-color: rgba(112, 112, 112, 0.25); | |
| 1195 | +} | |
| 1196 | +table.numbered-lines td.file-content { | |
| 1197 | + padding-left: 0.25em; | |
| 1198 | +} | |
| 1199 | +table.numbered-lines td.file-content > pre, | |
| 1200 | +table.numbered-lines td.file-content > pre > code { | |
| 1201 | + margin: 0; | |
| 1202 | + padding: 0; | |
| 1203 | + line-height: inherit; | |
| 1204 | + font-size: inherit; | |
| 1205 | + font-family: inherit; | |
| 1206 | + white-space: pre; | |
| 1207 | + display: block/*necessary for certain skins!*/; | |
| 1208 | +} | |
| 1209 | +table.numbered-lines td.file-content > pre { | |
| 1210 | +} | |
| 1211 | +table.numbered-lines td.file-content > pre > code { | |
| 1212 | + overflow: auto; | |
| 1213 | + padding-left: 0.5em; | |
| 1214 | + padding-right: 0.5em; | |
| 1215 | + padding-top: 0.25em/*any top padding here must match the top MARGIN of | |
| 1216 | + td.line-numbers's first span child or the | |
| 1217 | + lines/code will get misaligned. */; | |
| 1218 | + padding-bottom: 0.25em/*prevents a slight overlap at bottom from | |
| 1219 | + triggering a scroller*/; | |
| 1220 | +} | |
| 1221 | +table.numbered-lines td.file-content > pre > code > * { | |
| 1222 | + /* Defense against syntax highlighters indirectly messing up these | |
| 1223 | + properties... */ | |
| 1224 | + line-height: inherit; | |
| 1225 | + font-size: inherit; | |
| 1226 | + font-family: inherit; | |
| 1227 | +} | |
| 1228 | +table.numbered-lines td.line-numbers span.selected-line/*replacement*/ { | |
| 1229 | + font-weight: bold; | |
| 1230 | + color: blue; | |
| 1231 | + background-color: #d5d5ff; | |
| 1232 | + border: 1px blue solid; | |
| 1233 | + border-top-width: 0; | |
| 1234 | + border-bottom-width: 0; | |
| 1235 | + padding: 0; | |
| 1236 | + margin: 0; | |
| 1237 | +} | |
| 1238 | +table.numbered-lines td.line-numbers span.selected-line.start { | |
| 1239 | + border-top-width: 1px; | |
| 1240 | + margin-top: -1px/*restore alignment*/; | |
| 1241 | +} | |
| 1242 | +table.numbered-lines td.line-numbers span.selected-line.end { | |
| 1243 | + border-bottom-width: 1px; | |
| 1244 | + margin-top: -1px/*restore alignment*/; | |
| 1245 | +} | |
| 1246 | +table.numbered-lines td.line-numbers span.selected-line.start.end { | |
| 1247 | + margin-top: -2px/*restore alignment*/; | |
| 1248 | +} | |
| 1249 | + | |
| 1250 | +.fossil-tooltip { | |
| 1251 | + text-align: center; | |
| 1252 | + padding: 0.2em 1em; | |
| 1253 | + border: 1px solid black; | |
| 1254 | + border-radius: 0.25em; | |
| 1255 | + position: absolute; | |
| 1256 | + display: inline-block; | |
| 1257 | + z-index: 100; | |
| 1258 | + box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75); | |
| 1259 | + background-color: inherit; | |
| 1260 | + font-size: 80%; | |
| 1261 | +} | |
| 1262 | + | |
| 1263 | +.fossil-toast {/* "toast"-style popup message */ | |
| 1264 | + padding: 0.25em 0.5em; | |
| 1265 | + margin: 0; | |
| 1266 | + border-radius: 0.25em; | |
| 1267 | + font-size: 1em; | |
| 1268 | + opacity: 0.8; | |
| 1269 | + border-size: 1px; | |
| 1270 | + border-style: dotted; | |
| 1271 | + border-color: rgb( 127, 127, 127, 0.5 ); | |
| 1272 | +} | |
| 1273 | + | |
| 1274 | +blockquote.file-content { | |
| 1275 | + /* file content block in the /file page */ | |
| 1276 | + margin: 0 1em; | |
| 1277 | +} | |
| 924 | 1278 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -440,16 +440,10 @@ | |
| 440 | content:"'"; |
| 441 | } |
| 442 | span.usertype:after { |
| 443 | content:"'"; |
| 444 | } |
| 445 | div.selectedText { |
| 446 | font-weight: bold; |
| 447 | color: blue; |
| 448 | background-color: #d5d5ff; |
| 449 | border: 1px blue solid; |
| 450 | } |
| 451 | p.missingPriv { |
| 452 | color: blue; |
| 453 | } |
| 454 | span.wikiruleHead { |
| 455 | font-weight: bold; |
| @@ -774,44 +768,93 @@ | |
| 774 | div.forumTime > div > form, |
| 775 | div.forumHierRoot > div > form { |
| 776 | margin: 0.5em 0; |
| 777 | } |
| 778 | .forum-post-collapser { |
| 779 | font-size: 0.8em; |
| 780 | margin-top: 0.2em; |
| 781 | padding: 0; |
| 782 | height: 1.75em; |
| 783 | line-height: 1.75em; |
| 784 | /* ^^^ Those sizes are finely tuned for the current selection of |
| 785 | arrow characters. If those change, these should, too. Remember that |
| 786 | FF/Chrome simply do not agree on alignment with most values :/. */ |
| 787 | border-width: 1px; |
| 788 | border-style: solid; |
| 789 | border-radius: 0.25em; |
| 790 | opacity: 0.8; |
| 791 | cursor: pointer; |
| 792 | display: flex; |
| 793 | flex-direction: row; |
| 794 | justify-content: space-between; |
| 795 | } |
| 796 | .forum-post-collapser > span { |
| 797 | margin: 0 1em 0 1em; |
| 798 | vertical-align: middle; |
| 799 | } |
| 800 | .forum-post-collapser.expanded > span::before { |
| 801 | content: "⇡⇡⇡" /*reminder: FF/Chrome cannot agree on alignment of ⮝*/; |
| 802 | } |
| 803 | .forum-post-collapser:not(.expanded) > span::before { |
| 804 | content: "⇣⇣⇣"; |
| 805 | } |
| 806 | div.forumPostBody{ |
| 807 | max-height: 50em; |
| 808 | overflow: auto; |
| 809 | } |
| 810 | div.forumPostBody.expanded { |
| 811 | max-height: initial; |
| 812 | } |
| 813 | div.forumSel { |
| 814 | background-color: #cef; |
| 815 | } |
| 816 | div.forumObs { |
| 817 | color: #bbb; |
| @@ -904,14 +947,19 @@ | |
| 904 | color: darkred; |
| 905 | background: yellow; |
| 906 | opacity: 0.7; |
| 907 | } |
| 908 | .hidden { |
| 909 | position: absolute; |
| 910 | opacity: 0; |
| 911 | pointer-events: none; |
| 912 | display: none; |
| 913 | } |
| 914 | input { |
| 915 | max-width: 95%; |
| 916 | } |
| 917 | textarea { |
| @@ -919,5 +967,311 @@ | |
| 919 | } |
| 920 | img { |
| 921 | max-width: 100%; |
| 922 | height: auto; |
| 923 | } |
| 924 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -440,16 +440,10 @@ | |
| 440 | content:"'"; |
| 441 | } |
| 442 | span.usertype:after { |
| 443 | content:"'"; |
| 444 | } |
| 445 | p.missingPriv { |
| 446 | color: blue; |
| 447 | } |
| 448 | span.wikiruleHead { |
| 449 | font-weight: bold; |
| @@ -774,44 +768,93 @@ | |
| 768 | div.forumTime > div > form, |
| 769 | div.forumHierRoot > div > form { |
| 770 | margin: 0.5em 0; |
| 771 | } |
| 772 | .forum-post-collapser { |
| 773 | /* Common style for the bottom-of-post and right-of-post |
| 774 | expand/collapse widgets. */ |
| 775 | font-size: 0.8em; |
| 776 | padding: 0; |
| 777 | border: 1px solid rgba(0, 0, 0, 0.2); |
| 778 | border-radius: 0 0 0.5em 0.5em; |
| 779 | background-color: rgba(0, 0, 0, 0.05); |
| 780 | opacity: 0.8; |
| 781 | cursor: pointer; |
| 782 | } |
| 783 | .forum-post-collapser.bottom { |
| 784 | margin: 0 0 0.4em 0; |
| 785 | height: 1.75em; |
| 786 | line-height: 1.75em; |
| 787 | /* ^^^ Those sizes are finely tuned for the current selection of |
| 788 | arrow characters. If those change, these should, too. Remember that |
| 789 | FF/Chrome simply do not agree on alignment with most values :/. */ |
| 790 | display: flex; |
| 791 | flex-direction: row; |
| 792 | justify-content: space-between; |
| 793 | } |
| 794 | .forum-post-collapser.bottom > span { |
| 795 | margin: 0 1em 0 1em; |
| 796 | vertical-align: middle; |
| 797 | } |
| 798 | .forum-post-collapser.bottom > span::before { |
| 799 | content: "⇣⇣⇣"; |
| 800 | } |
| 801 | .forum-post-collapser.bottom.expanded > span::before { |
| 802 | content: "⇡⇡⇡" /*reminder: FF/Chrome cannot agree on alignment of ⮝*/; |
| 803 | } |
| 804 | div.forumPostBody{ |
| 805 | max-height: 50em; |
| 806 | overflow: auto; |
| 807 | } |
| 808 | div.forumPostBody.with-expander { |
| 809 | display: flex; |
| 810 | flex-direction: row; |
| 811 | overflow: auto; |
| 812 | } |
| 813 | div.forumPostBody.with-expander:not(.expanded) > :first-child { |
| 814 | overflow-y: hidden; |
| 815 | } |
| 816 | div.forumPostBody.with-expander > *:first-child { |
| 817 | /* Main content DIV/PRE */ |
| 818 | overflow: auto; |
| 819 | flex: 10 1 auto; |
| 820 | } |
| 821 | div.forumPostBody.with-expander.expanded > *:first-child { |
| 822 | margin-bottom: 0.5em /* try to suppress scroll bar */; |
| 823 | } |
| 824 | div.forumPostBody.with-expander .forum-post-collapser.right { |
| 825 | /* "Tap zone" for expansion of the post, sits to the right of the |
| 826 | post's content. */ |
| 827 | flex: 1 10 auto; |
| 828 | min-width: 1.25em; |
| 829 | max-width: 1.25em; |
| 830 | margin: 0 0 0 0.2em; |
| 831 | overflow: hidden; |
| 832 | display: flex; |
| 833 | flex-direction: column; |
| 834 | justify-content: space-around; |
| 835 | align-items: center; |
| 836 | border-radius: 0.1em; |
| 837 | cursor: pointer; |
| 838 | border-bottom: 0; |
| 839 | border-radius: 0 0.5em 0 0; |
| 840 | } |
| 841 | div.forumPostBody.with-expander .forum-post-collapser.right > span:before { |
| 842 | content: "⇣"; |
| 843 | } |
| 844 | div.forumPostBody.with-expander.expanded .forum-post-collapser.right > span:before { |
| 845 | content: "⇡"; |
| 846 | } |
| 847 | div.forumPostBody.expanded { |
| 848 | max-height: initial; |
| 849 | } |
| 850 | div.forumPostBody.shrunken { |
| 851 | /* When an expandable post is un-expanded, it is shrunkend down |
| 852 | to this size instead of its original size. */ |
| 853 | max-height: 8em; |
| 854 | } |
| 855 | |
| 856 | div.forumSel { |
| 857 | background-color: #cef; |
| 858 | } |
| 859 | div.forumObs { |
| 860 | color: #bbb; |
| @@ -904,14 +947,19 @@ | |
| 947 | color: darkred; |
| 948 | background: yellow; |
| 949 | opacity: 0.7; |
| 950 | } |
| 951 | .hidden { |
| 952 | /* The framework-wide way of hiding elements is to assign them this |
| 953 | CSS class. To make them visible again, remove it. The !important |
| 954 | qualifiers are unfortunate but sometimes necessary when hidden |
| 955 | element has other classes which specify visibility-related |
| 956 | options. */ |
| 957 | position: absolute !important; |
| 958 | opacity: 0 !important; |
| 959 | pointer-events: none !important; |
| 960 | display: none !important; |
| 961 | } |
| 962 | input { |
| 963 | max-width: 95%; |
| 964 | } |
| 965 | textarea { |
| @@ -919,5 +967,311 @@ | |
| 967 | } |
| 968 | img { |
| 969 | max-width: 100%; |
| 970 | height: auto; |
| 971 | } |
| 972 | hr { |
| 973 | /* Needed to keep /dir README.txt from floating right in some skins */ |
| 974 | clear: both; |
| 975 | } |
| 976 | |
| 977 | /** |
| 978 | .tab-xxx: styles for fossil.tabs.js. |
| 979 | */ |
| 980 | .tab-container { |
| 981 | width: 100%; |
| 982 | display: flex; |
| 983 | flex-direction: column; |
| 984 | align-items: stretch; |
| 985 | } |
| 986 | .tab-container > #fossil-status-bar { |
| 987 | margin-top: 0; |
| 988 | } |
| 989 | .tab-container > .tabs { |
| 990 | padding: 0.25em; |
| 991 | margin: 0; |
| 992 | display: flex; |
| 993 | flex-direction: column; |
| 994 | border-width: 1px; |
| 995 | border-style: outset; |
| 996 | border-color: inherit; |
| 997 | } |
| 998 | .tab-container > .tabs > .tab-panel { |
| 999 | align-self: stretch; |
| 1000 | flex: 10 1 auto; |
| 1001 | display: block; |
| 1002 | border: 0; |
| 1003 | padding: 0; |
| 1004 | margin: 0; |
| 1005 | } |
| 1006 | .tab-container > .tab-bar { |
| 1007 | display: flex; |
| 1008 | flex-direction: row; |
| 1009 | flex: 1 10 auto; |
| 1010 | align-self: stretch; |
| 1011 | flex-wrap: wrap; |
| 1012 | } |
| 1013 | .tab-container > .tab-bar > .tab-button { |
| 1014 | display: inline-block; |
| 1015 | border-radius: 0.25em 0.25em 0 0; |
| 1016 | margin: 0 0.1em; |
| 1017 | padding: 0.25em 0.75em; |
| 1018 | align-self: baseline; |
| 1019 | border-color: inherit; |
| 1020 | border-width: 1px; |
| 1021 | border-bottom: none; |
| 1022 | border-top-style: inset; |
| 1023 | border-left-style: inset; |
| 1024 | border-right-style: inset; |
| 1025 | cursor: pointer; |
| 1026 | opacity: 0.6; |
| 1027 | } |
| 1028 | .tab-container > .tab-bar > .tab-button.selected { |
| 1029 | text-decoration: underline; |
| 1030 | opacity: 1.0; |
| 1031 | border-top-style: outset; |
| 1032 | border-left-style: outset; |
| 1033 | border-right-style: outset; |
| 1034 | } |
| 1035 | |
| 1036 | /** |
| 1037 | The flex-xxx classes can be used to create basic flexbox layouts |
| 1038 | through the application of classes to the containing/contained |
| 1039 | objects. |
| 1040 | */ |
| 1041 | .flex-container { |
| 1042 | display: flex; |
| 1043 | } |
| 1044 | .flex-container.flex-row { |
| 1045 | flex-direction: row; |
| 1046 | flex-wrap: wrap; |
| 1047 | justify-content: center; |
| 1048 | align-items: center; |
| 1049 | } |
| 1050 | .flex-container .flex-grow { |
| 1051 | flex-grow: 10; |
| 1052 | flex-shrink: 0; |
| 1053 | } |
| 1054 | .flex-container .flex-shrink { |
| 1055 | flex-grow: 0; |
| 1056 | flex-shrink: 10; |
| 1057 | } |
| 1058 | .flex-container.flex-row.stretch { |
| 1059 | flex-wrap: wrap; |
| 1060 | align-items: baseline; |
| 1061 | justify-content: stretch; |
| 1062 | margin: 0; |
| 1063 | } |
| 1064 | .flex-container.flex-column { |
| 1065 | flex-direction: column; |
| 1066 | flex-wrap: wrap; |
| 1067 | justify-content: center; |
| 1068 | align-items: center; |
| 1069 | } |
| 1070 | .flex-container.flex-column.stretch { |
| 1071 | align-items: stretch; |
| 1072 | margin: 0; |
| 1073 | } |
| 1074 | .flex-container.child-gap-small > * { |
| 1075 | margin: 0.25em; |
| 1076 | } |
| 1077 | #fossil-status-bar { |
| 1078 | display: block; |
| 1079 | font-family: monospace; |
| 1080 | border-width: 1px; |
| 1081 | border-style: inset; |
| 1082 | border-color: inherit; |
| 1083 | min-height: 1.5em; |
| 1084 | font-size: 1.2em; |
| 1085 | padding: 0.2em; |
| 1086 | margin: 0.25em 0; |
| 1087 | flex: 0 0 auto; |
| 1088 | } |
| 1089 | .font-size-100 { |
| 1090 | font-size: 100%; |
| 1091 | } |
| 1092 | .font-size-125 { |
| 1093 | font-size: 125%; |
| 1094 | } |
| 1095 | .font-size-150 { |
| 1096 | font-size: 150%; |
| 1097 | } |
| 1098 | .font-size-175 { |
| 1099 | font-size: 175%; |
| 1100 | } |
| 1101 | .font-size-200 { |
| 1102 | font-size: 200%; |
| 1103 | } |
| 1104 | |
| 1105 | /** |
| 1106 | .input-with-label is intended to be a wrapper element which |
| 1107 | contain both a LABEL tag and an INPUT or SELECT control. |
| 1108 | The wrapper is "necessary", as opposed to placing the INPUT |
| 1109 | in the LABEL, so that we can include multiple INPUT |
| 1110 | elements (e.g. a set of radio buttons). |
| 1111 | */ |
| 1112 | .input-with-label { |
| 1113 | border: 1px inset #808080; |
| 1114 | border-radius: 0.25em; |
| 1115 | padding: 0.25em 0.4em; |
| 1116 | margin: 0 0.5em; |
| 1117 | display: inline-block; |
| 1118 | cursor: default; |
| 1119 | } |
| 1120 | .input-with-label > * { |
| 1121 | vertical-align: middle; |
| 1122 | } |
| 1123 | .input-with-label > label { |
| 1124 | display: inline; /* some skins set label display to block! */ |
| 1125 | } |
| 1126 | .input-with-label > input { |
| 1127 | margin: 0; |
| 1128 | } |
| 1129 | .input-with-label > button { |
| 1130 | margin: 0; |
| 1131 | } |
| 1132 | .input-with-label > select { |
| 1133 | margin: 0; |
| 1134 | } |
| 1135 | .input-with-label > input[type=text] { |
| 1136 | margin: 0; |
| 1137 | } |
| 1138 | .input-with-label > textarea { |
| 1139 | margin: 0; |
| 1140 | } |
| 1141 | .input-with-label > input[type=checkbox] { |
| 1142 | vertical-align: sub; |
| 1143 | } |
| 1144 | .input-with-label > input[type=radio] { |
| 1145 | vertical-align: sub; |
| 1146 | } |
| 1147 | .input-with-label > label { |
| 1148 | font-weight: initial; |
| 1149 | margin: 0 0.25em 0 0.25em; |
| 1150 | vertical-align: middle; |
| 1151 | } |
| 1152 | |
| 1153 | table.numbered-lines { |
| 1154 | width: 100%; |
| 1155 | table-layout: fixed /* required to keep ultra-wide code from exceeding |
| 1156 | window width, and instead force a scrollbar |
| 1157 | on them. */; |
| 1158 | } |
| 1159 | table.numbered-lines > tbody > tr { |
| 1160 | font-family: monospace; |
| 1161 | font-size: 1.2em; |
| 1162 | line-height: 1.35; |
| 1163 | white-space: pre; |
| 1164 | } |
| 1165 | table.numbered-lines > tbody > tr > td { |
| 1166 | font-family: inherit; |
| 1167 | font-size: inherit; |
| 1168 | line-height: inherit; |
| 1169 | white-space: inherit; |
| 1170 | margin: 0; |
| 1171 | vertical-align: top; |
| 1172 | padding: 0.25em 0 0 0 /*prevents slight overlap at top */; |
| 1173 | } |
| 1174 | table.numbered-lines td.line-numbers { |
| 1175 | width: 4.5em; |
| 1176 | } |
| 1177 | table.numbered-lines td.line-numbers > span:first-of-type { |
| 1178 | margin-top: 0.25em/*must match top PADDING of |
| 1179 | td.file-content > pre > code*/; |
| 1180 | } |
| 1181 | table.numbered-lines td.line-numbers > span { |
| 1182 | display: block; |
| 1183 | margin: 0; |
| 1184 | padding: 0; |
| 1185 | line-height: inherit; |
| 1186 | font-size: inherit; |
| 1187 | font-family: inherit; |
| 1188 | cursor: pointer; |
| 1189 | white-space: pre; |
| 1190 | margin-right: 2px/*keep selection from nudging the right column */; |
| 1191 | text-align: right; |
| 1192 | } |
| 1193 | table.numbered-lines td.line-numbers > span:hover { |
| 1194 | background-color: rgba(112, 112, 112, 0.25); |
| 1195 | } |
| 1196 | table.numbered-lines td.file-content { |
| 1197 | padding-left: 0.25em; |
| 1198 | } |
| 1199 | table.numbered-lines td.file-content > pre, |
| 1200 | table.numbered-lines td.file-content > pre > code { |
| 1201 | margin: 0; |
| 1202 | padding: 0; |
| 1203 | line-height: inherit; |
| 1204 | font-size: inherit; |
| 1205 | font-family: inherit; |
| 1206 | white-space: pre; |
| 1207 | display: block/*necessary for certain skins!*/; |
| 1208 | } |
| 1209 | table.numbered-lines td.file-content > pre { |
| 1210 | } |
| 1211 | table.numbered-lines td.file-content > pre > code { |
| 1212 | overflow: auto; |
| 1213 | padding-left: 0.5em; |
| 1214 | padding-right: 0.5em; |
| 1215 | padding-top: 0.25em/*any top padding here must match the top MARGIN of |
| 1216 | td.line-numbers's first span child or the |
| 1217 | lines/code will get misaligned. */; |
| 1218 | padding-bottom: 0.25em/*prevents a slight overlap at bottom from |
| 1219 | triggering a scroller*/; |
| 1220 | } |
| 1221 | table.numbered-lines td.file-content > pre > code > * { |
| 1222 | /* Defense against syntax highlighters indirectly messing up these |
| 1223 | properties... */ |
| 1224 | line-height: inherit; |
| 1225 | font-size: inherit; |
| 1226 | font-family: inherit; |
| 1227 | } |
| 1228 | table.numbered-lines td.line-numbers span.selected-line/*replacement*/ { |
| 1229 | font-weight: bold; |
| 1230 | color: blue; |
| 1231 | background-color: #d5d5ff; |
| 1232 | border: 1px blue solid; |
| 1233 | border-top-width: 0; |
| 1234 | border-bottom-width: 0; |
| 1235 | padding: 0; |
| 1236 | margin: 0; |
| 1237 | } |
| 1238 | table.numbered-lines td.line-numbers span.selected-line.start { |
| 1239 | border-top-width: 1px; |
| 1240 | margin-top: -1px/*restore alignment*/; |
| 1241 | } |
| 1242 | table.numbered-lines td.line-numbers span.selected-line.end { |
| 1243 | border-bottom-width: 1px; |
| 1244 | margin-top: -1px/*restore alignment*/; |
| 1245 | } |
| 1246 | table.numbered-lines td.line-numbers span.selected-line.start.end { |
| 1247 | margin-top: -2px/*restore alignment*/; |
| 1248 | } |
| 1249 | |
| 1250 | .fossil-tooltip { |
| 1251 | text-align: center; |
| 1252 | padding: 0.2em 1em; |
| 1253 | border: 1px solid black; |
| 1254 | border-radius: 0.25em; |
| 1255 | position: absolute; |
| 1256 | display: inline-block; |
| 1257 | z-index: 100; |
| 1258 | box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75); |
| 1259 | background-color: inherit; |
| 1260 | font-size: 80%; |
| 1261 | } |
| 1262 | |
| 1263 | .fossil-toast {/* "toast"-style popup message */ |
| 1264 | padding: 0.25em 0.5em; |
| 1265 | margin: 0; |
| 1266 | border-radius: 0.25em; |
| 1267 | font-size: 1em; |
| 1268 | opacity: 0.8; |
| 1269 | border-size: 1px; |
| 1270 | border-style: dotted; |
| 1271 | border-color: rgb( 127, 127, 127, 0.5 ); |
| 1272 | } |
| 1273 | |
| 1274 | blockquote.file-content { |
| 1275 | /* file content block in the /file page */ |
| 1276 | margin: 0 1em; |
| 1277 | } |
| 1278 |
+2
-2
| --- src/descendants.c | ||
| +++ src/descendants.c | ||
| @@ -348,11 +348,11 @@ | ||
| 348 | 348 | ** -R|--repository FILE Extract info from repository FILE |
| 349 | 349 | ** -W|--width <num> Width of lines (default is to auto-detect). |
| 350 | 350 | ** Must be >20 or 0 (= no limit, resulting in a |
| 351 | 351 | ** single line per entry). |
| 352 | 352 | ** |
| 353 | -** See also: finfo, info, leaves | |
| 353 | +** See also: [[finfo]], [[info]], [[leaves]] | |
| 354 | 354 | */ |
| 355 | 355 | void descendants_cmd(void){ |
| 356 | 356 | Stmt q; |
| 357 | 357 | int base, width; |
| 358 | 358 | const char *zWidth; |
| @@ -408,11 +408,11 @@ | ||
| 408 | 408 | ** --recompute recompute the "leaf" table in the repository DB |
| 409 | 409 | ** -W|--width <num> Width of lines (default is to auto-detect). Must be |
| 410 | 410 | ** >39 or 0 (= no limit, resulting in a single line per |
| 411 | 411 | ** entry). |
| 412 | 412 | ** |
| 413 | -** See also: descendants, finfo, info, branch | |
| 413 | +** See also: [[descendants]], [[finfo]], [[info]], [[branch]] | |
| 414 | 414 | */ |
| 415 | 415 | void leaves_cmd(void){ |
| 416 | 416 | Stmt q; |
| 417 | 417 | Blob sql; |
| 418 | 418 | int showAll = find_option("all", "a", 0)!=0; |
| 419 | 419 |
| --- src/descendants.c | |
| +++ src/descendants.c | |
| @@ -348,11 +348,11 @@ | |
| 348 | ** -R|--repository FILE Extract info from repository FILE |
| 349 | ** -W|--width <num> Width of lines (default is to auto-detect). |
| 350 | ** Must be >20 or 0 (= no limit, resulting in a |
| 351 | ** single line per entry). |
| 352 | ** |
| 353 | ** See also: finfo, info, leaves |
| 354 | */ |
| 355 | void descendants_cmd(void){ |
| 356 | Stmt q; |
| 357 | int base, width; |
| 358 | const char *zWidth; |
| @@ -408,11 +408,11 @@ | |
| 408 | ** --recompute recompute the "leaf" table in the repository DB |
| 409 | ** -W|--width <num> Width of lines (default is to auto-detect). Must be |
| 410 | ** >39 or 0 (= no limit, resulting in a single line per |
| 411 | ** entry). |
| 412 | ** |
| 413 | ** See also: descendants, finfo, info, branch |
| 414 | */ |
| 415 | void leaves_cmd(void){ |
| 416 | Stmt q; |
| 417 | Blob sql; |
| 418 | int showAll = find_option("all", "a", 0)!=0; |
| 419 |
| --- src/descendants.c | |
| +++ src/descendants.c | |
| @@ -348,11 +348,11 @@ | |
| 348 | ** -R|--repository FILE Extract info from repository FILE |
| 349 | ** -W|--width <num> Width of lines (default is to auto-detect). |
| 350 | ** Must be >20 or 0 (= no limit, resulting in a |
| 351 | ** single line per entry). |
| 352 | ** |
| 353 | ** See also: [[finfo]], [[info]], [[leaves]] |
| 354 | */ |
| 355 | void descendants_cmd(void){ |
| 356 | Stmt q; |
| 357 | int base, width; |
| 358 | const char *zWidth; |
| @@ -408,11 +408,11 @@ | |
| 408 | ** --recompute recompute the "leaf" table in the repository DB |
| 409 | ** -W|--width <num> Width of lines (default is to auto-detect). Must be |
| 410 | ** >39 or 0 (= no limit, resulting in a single line per |
| 411 | ** entry). |
| 412 | ** |
| 413 | ** See also: [[descendants]], [[finfo]], [[info]], [[branch]] |
| 414 | */ |
| 415 | void leaves_cmd(void){ |
| 416 | Stmt q; |
| 417 | Blob sql; |
| 418 | int showAll = find_option("all", "a", 0)!=0; |
| 419 |
+3
-3
| --- src/diff.c | ||
| +++ src/diff.c | ||
| @@ -125,11 +125,11 @@ | ||
| 125 | 125 | ** in the count even if it lacks the \n terminator. If an empty string |
| 126 | 126 | ** is specified, the number of lines is zero. For the purposes of this |
| 127 | 127 | ** function, a string is considered empty if it contains no characters |
| 128 | 128 | ** -OR- it contains only NUL characters. |
| 129 | 129 | */ |
| 130 | -static int count_lines( | |
| 130 | +int count_lines( | |
| 131 | 131 | const char *z, |
| 132 | 132 | int n, |
| 133 | 133 | int *pnLine |
| 134 | 134 | ){ |
| 135 | 135 | int nLine; |
| @@ -2562,11 +2562,11 @@ | ||
| 2562 | 2562 | } |
| 2563 | 2563 | |
| 2564 | 2564 | /* |
| 2565 | 2565 | ** COMMAND: annotate |
| 2566 | 2566 | ** COMMAND: blame |
| 2567 | -** COMMAND: praise | |
| 2567 | +** COMMAND: praise* | |
| 2568 | 2568 | ** |
| 2569 | 2569 | ** Usage: %fossil annotate|blame|praise ?OPTIONS? FILENAME |
| 2570 | 2570 | ** |
| 2571 | 2571 | ** Output the text of a file with markings to show when each line of the file |
| 2572 | 2572 | ** was last modified. The version currently checked out is shown by default. |
| @@ -2596,11 +2596,11 @@ | ||
| 2596 | 2596 | ** root of the repository. Set to "trunk" or |
| 2597 | 2597 | ** similar for a reverse annotation. |
| 2598 | 2598 | ** -w|--ignore-all-space Ignore white space when comparing lines |
| 2599 | 2599 | ** -Z|--ignore-trailing-space Ignore whitespace at line end |
| 2600 | 2600 | ** |
| 2601 | -** See also: info, finfo, timeline | |
| 2601 | +** See also: [[info]], [[finfo]], [[timeline]] | |
| 2602 | 2602 | */ |
| 2603 | 2603 | void annotate_cmd(void){ |
| 2604 | 2604 | const char *zRevision; /* Revision name, or NULL for current check-in */ |
| 2605 | 2605 | Annotator ann; /* The annotation of the file */ |
| 2606 | 2606 | int i; /* Loop counter */ |
| 2607 | 2607 |
| --- src/diff.c | |
| +++ src/diff.c | |
| @@ -125,11 +125,11 @@ | |
| 125 | ** in the count even if it lacks the \n terminator. If an empty string |
| 126 | ** is specified, the number of lines is zero. For the purposes of this |
| 127 | ** function, a string is considered empty if it contains no characters |
| 128 | ** -OR- it contains only NUL characters. |
| 129 | */ |
| 130 | static int count_lines( |
| 131 | const char *z, |
| 132 | int n, |
| 133 | int *pnLine |
| 134 | ){ |
| 135 | int nLine; |
| @@ -2562,11 +2562,11 @@ | |
| 2562 | } |
| 2563 | |
| 2564 | /* |
| 2565 | ** COMMAND: annotate |
| 2566 | ** COMMAND: blame |
| 2567 | ** COMMAND: praise |
| 2568 | ** |
| 2569 | ** Usage: %fossil annotate|blame|praise ?OPTIONS? FILENAME |
| 2570 | ** |
| 2571 | ** Output the text of a file with markings to show when each line of the file |
| 2572 | ** was last modified. The version currently checked out is shown by default. |
| @@ -2596,11 +2596,11 @@ | |
| 2596 | ** root of the repository. Set to "trunk" or |
| 2597 | ** similar for a reverse annotation. |
| 2598 | ** -w|--ignore-all-space Ignore white space when comparing lines |
| 2599 | ** -Z|--ignore-trailing-space Ignore whitespace at line end |
| 2600 | ** |
| 2601 | ** See also: info, finfo, timeline |
| 2602 | */ |
| 2603 | void annotate_cmd(void){ |
| 2604 | const char *zRevision; /* Revision name, or NULL for current check-in */ |
| 2605 | Annotator ann; /* The annotation of the file */ |
| 2606 | int i; /* Loop counter */ |
| 2607 |
| --- src/diff.c | |
| +++ src/diff.c | |
| @@ -125,11 +125,11 @@ | |
| 125 | ** in the count even if it lacks the \n terminator. If an empty string |
| 126 | ** is specified, the number of lines is zero. For the purposes of this |
| 127 | ** function, a string is considered empty if it contains no characters |
| 128 | ** -OR- it contains only NUL characters. |
| 129 | */ |
| 130 | int count_lines( |
| 131 | const char *z, |
| 132 | int n, |
| 133 | int *pnLine |
| 134 | ){ |
| 135 | int nLine; |
| @@ -2562,11 +2562,11 @@ | |
| 2562 | } |
| 2563 | |
| 2564 | /* |
| 2565 | ** COMMAND: annotate |
| 2566 | ** COMMAND: blame |
| 2567 | ** COMMAND: praise* |
| 2568 | ** |
| 2569 | ** Usage: %fossil annotate|blame|praise ?OPTIONS? FILENAME |
| 2570 | ** |
| 2571 | ** Output the text of a file with markings to show when each line of the file |
| 2572 | ** was last modified. The version currently checked out is shown by default. |
| @@ -2596,11 +2596,11 @@ | |
| 2596 | ** root of the repository. Set to "trunk" or |
| 2597 | ** similar for a reverse annotation. |
| 2598 | ** -w|--ignore-all-space Ignore white space when comparing lines |
| 2599 | ** -Z|--ignore-trailing-space Ignore whitespace at line end |
| 2600 | ** |
| 2601 | ** See also: [[info]], [[finfo]], [[timeline]] |
| 2602 | */ |
| 2603 | void annotate_cmd(void){ |
| 2604 | const char *zRevision; /* Revision name, or NULL for current check-in */ |
| 2605 | Annotator ann; /* The annotation of the file */ |
| 2606 | int i; /* Loop counter */ |
| 2607 |
+80
-8
| --- src/dispatch.c | ||
| +++ src/dispatch.c | ||
| @@ -263,10 +263,48 @@ | ||
| 263 | 263 | if( zEnd[0] ) blob_append(pOut, zEnd, -1); |
| 264 | 264 | i = j; |
| 265 | 265 | } |
| 266 | 266 | } |
| 267 | 267 | } |
| 268 | + | |
| 269 | +/* | |
| 270 | +** Input string zIn starts with '['. If the content is a hyperlink of the | |
| 271 | +** form [[...]] then return the index of the closing ']'. Otherwise return 0. | |
| 272 | +*/ | |
| 273 | +static int help_is_link(const char *z, int n){ | |
| 274 | + int i; | |
| 275 | + char c; | |
| 276 | + if( n<5 ) return 0; | |
| 277 | + if( z[1]!='[' ) return 0; | |
| 278 | + for(i=3; i<n && (c = z[i])!=0; i++){ | |
| 279 | + if( c==']' && z[i-1]==']' ) return i; | |
| 280 | + } | |
| 281 | + return 0; | |
| 282 | +} | |
| 283 | + | |
| 284 | +/* | |
| 285 | +** Append text to pOut, adding hyperlink markup for [...]. | |
| 286 | +*/ | |
| 287 | +static void appendLinked(Blob *pOut, const char *z, int n){ | |
| 288 | + int i = 0; | |
| 289 | + int j; | |
| 290 | + while( i<n ){ | |
| 291 | + if( z[i]=='[' && (j = help_is_link(z+i, n-i))>0 ){ | |
| 292 | + if( i ) blob_append(pOut, z, i); | |
| 293 | + z += i+2; | |
| 294 | + n -= i+2; | |
| 295 | + blob_appendf(pOut, "<a href='%R/help?cmd=%.*s'>%.*s</a>", | |
| 296 | + j-3, z, j-3, z); | |
| 297 | + z += j-1; | |
| 298 | + n -= j-1; | |
| 299 | + i = 0; | |
| 300 | + }else{ | |
| 301 | + i++; | |
| 302 | + } | |
| 303 | + } | |
| 304 | + blob_append(pOut, z, i); | |
| 305 | +} | |
| 268 | 306 | |
| 269 | 307 | /* |
| 270 | 308 | ** Attempt to reformat plain-text help into HTML for display on a webpage. |
| 271 | 309 | ** |
| 272 | 310 | ** The HTML output is appended to Blob pHtml, which should already be |
| @@ -305,17 +343,22 @@ | ||
| 305 | 343 | azEnd[0] = ""; |
| 306 | 344 | while( zHelp[0] ){ |
| 307 | 345 | i = 0; |
| 308 | 346 | while( (c = zHelp[i])!=0 |
| 309 | 347 | && c!='\n' |
| 348 | + && c!='<' | |
| 310 | 349 | && (c!='%' || strncmp(zHelp+i,"%fossil",7)!=0) |
| 311 | 350 | ){ i++; } |
| 312 | 351 | if( c=='%' ){ |
| 313 | 352 | if( i ) blob_appendf(pHtml, "%#h", i, zHelp); |
| 314 | 353 | zHelp += i + 1; |
| 315 | - i = 0; | |
| 316 | 354 | wantBR = 1; |
| 355 | + continue; | |
| 356 | + }else if( c=='<' ){ | |
| 357 | + if( i ) blob_appendf(pHtml, "%#h", i, zHelp); | |
| 358 | + blob_append(pHtml, "&", 5); | |
| 359 | + zHelp += i + 1; | |
| 317 | 360 | continue; |
| 318 | 361 | } |
| 319 | 362 | if( i>2 && zHelp[0]=='>' && zHelp[1]==' ' ){ |
| 320 | 363 | isDT = 1; |
| 321 | 364 | for(nIndent=1; nIndent<i && zHelp[nIndent]==' '; nIndent++){} |
| @@ -394,11 +437,12 @@ | ||
| 394 | 437 | }else if( wantBR ){ |
| 395 | 438 | appendMixedFont(pHtml, zHelp+nIndent, i-nIndent); |
| 396 | 439 | blob_append(pHtml, "<br>\n", 5); |
| 397 | 440 | wantBR = 0; |
| 398 | 441 | }else{ |
| 399 | - blob_appendf(pHtml, "%#h\n", i-nIndent, zHelp+nIndent); | |
| 442 | + appendLinked(pHtml, zHelp+nIndent, i-nIndent); | |
| 443 | + blob_append_char(pHtml, '\n'); | |
| 400 | 444 | } |
| 401 | 445 | zHelp += i+1; |
| 402 | 446 | i = 0; |
| 403 | 447 | if( c==0 ) break; |
| 404 | 448 | } |
| @@ -409,11 +453,11 @@ | ||
| 409 | 453 | |
| 410 | 454 | /* |
| 411 | 455 | ** Format help text for TTY display. |
| 412 | 456 | */ |
| 413 | 457 | static void help_to_text(const char *zHelp, Blob *pText){ |
| 414 | - int i; | |
| 458 | + int i, x; | |
| 415 | 459 | char c; |
| 416 | 460 | for(i=0; (c = zHelp[i])!=0; i++){ |
| 417 | 461 | if( c=='%' && strncmp(zHelp+i,"%fossil",7)==0 ){ |
| 418 | 462 | if( i>0 ) blob_append(pText, zHelp, i); |
| 419 | 463 | blob_append(pText, "fossil", 6); |
| @@ -426,10 +470,18 @@ | ||
| 426 | 470 | blob_append(pText, " ", 1); |
| 427 | 471 | zHelp += i+2; |
| 428 | 472 | i = -1; |
| 429 | 473 | continue; |
| 430 | 474 | } |
| 475 | + if( c=='[' && (x = help_is_link(zHelp+i, 100000))!=0 ){ | |
| 476 | + if( i>0 ) blob_append(pText, zHelp, i); | |
| 477 | + zHelp += i+2; | |
| 478 | + blob_append(pText, zHelp, x-3); | |
| 479 | + zHelp += x-1; | |
| 480 | + i = -1; | |
| 481 | + continue; | |
| 482 | + } | |
| 431 | 483 | } |
| 432 | 484 | if( i>0 ){ |
| 433 | 485 | blob_append(pText, zHelp, i); |
| 434 | 486 | } |
| 435 | 487 | } |
| @@ -447,15 +499,17 @@ | ||
| 447 | 499 | ** -e|--everything Show all commands and pages. |
| 448 | 500 | ** -t|--test Include test- commands |
| 449 | 501 | ** -w|--www Show WWW pages. |
| 450 | 502 | ** -s|--settings Show settings. |
| 451 | 503 | ** -h|--html Transform output to HTML. |
| 504 | +** -r|--raw No output formatting. | |
| 452 | 505 | */ |
| 453 | 506 | void test_all_help_cmd(void){ |
| 454 | 507 | int i; |
| 455 | 508 | int mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER; |
| 456 | 509 | int useHtml = find_option("html","h",0)!=0; |
| 510 | + int rawOut = find_option("raw","r",0)!=0; | |
| 457 | 511 | |
| 458 | 512 | if( find_option("www","w",0) ){ |
| 459 | 513 | mask = CMDFLAG_WEBPAGE; |
| 460 | 514 | } |
| 461 | 515 | if( find_option("everything","e",0) ){ |
| @@ -488,10 +542,13 @@ | ||
| 488 | 542 | blob_init(&html, 0, 0); |
| 489 | 543 | help_to_html(aCommand[i].zHelp, &html); |
| 490 | 544 | fossil_print("<h1>%h</h1>\n", aCommand[i].zName); |
| 491 | 545 | fossil_print("%s\n<hr>\n", blob_str(&html)); |
| 492 | 546 | blob_reset(&html); |
| 547 | + }else if( rawOut ){ | |
| 548 | + fossil_print("# %s\n", aCommand[i].zName); | |
| 549 | + fossil_print("%s\n\n", aCommand[i].zHelp); | |
| 493 | 550 | }else{ |
| 494 | 551 | Blob txt; |
| 495 | 552 | blob_init(&txt, 0, 0); |
| 496 | 553 | help_to_text(aCommand[i].zHelp, &txt); |
| 497 | 554 | fossil_print("# %s\n", aCommand[i].zName); |
| @@ -665,11 +722,11 @@ | ||
| 665 | 722 | const CmdOrPage *pCmd = 0; |
| 666 | 723 | |
| 667 | 724 | style_header("Help: %s", zCmd); |
| 668 | 725 | |
| 669 | 726 | style_submenu_element("Command-List", "%s/help", g.zTop); |
| 670 | - rc = dispatch_name_search(zCmd, CMDFLAG_ANY, &pCmd); | |
| 727 | + rc = dispatch_name_search(zCmd, CMDFLAG_ANY|CMDFLAG_PREFIX, &pCmd); | |
| 671 | 728 | if( *zCmd=='/' ){ |
| 672 | 729 | /* Some of the webpages require query parameters in order to work. |
| 673 | 730 | ** @ <h1>The "<a href='%R%s(zCmd)'>%s(zCmd)</a>" page:</h1> */ |
| 674 | 731 | @ <h1>The "%h(zCmd)" page:</h1> |
| 675 | 732 | }else if( rc==0 && (pCmd->eCmdFlags & CMDFLAG_SETTING)!=0 ){ |
| @@ -913,11 +970,11 @@ | ||
| 913 | 970 | ** setting. Webpage names begin with "/". If TOPIC is omitted, a list of |
| 914 | 971 | ** topics is returned. |
| 915 | 972 | ** |
| 916 | 973 | ** The following options can be used when TOPIC is omitted: |
| 917 | 974 | ** |
| 918 | -** -a|--all List both command and auxiliary commands | |
| 975 | +** -a|--all List both common and auxiliary commands | |
| 919 | 976 | ** -o|--options List command-line options common to all commands |
| 920 | 977 | ** -s|--setting List setting names |
| 921 | 978 | ** -t|--test List unsupported "test" commands |
| 922 | 979 | ** -x|--aux List only auxiliary commands |
| 923 | 980 | ** -w|--www List all web pages |
| @@ -936,12 +993,13 @@ | ||
| 936 | 993 | Blob txt; |
| 937 | 994 | if( g.argc<3 ){ |
| 938 | 995 | z = g.argv[0]; |
| 939 | 996 | fossil_print( |
| 940 | 997 | "Usage: %s help TOPIC\n" |
| 941 | - "Common commands: (use \"%s help help\" for more options)\n", | |
| 942 | - z, z); | |
| 998 | + "Try \"%s help help\" or \"%s help -a\" for more options\n" | |
| 999 | + "Frequently used commands:\n", | |
| 1000 | + z, z, z); | |
| 943 | 1001 | command_list(0, CMDFLAG_1ST_TIER); |
| 944 | 1002 | version_cmd(); |
| 945 | 1003 | return; |
| 946 | 1004 | } |
| 947 | 1005 | if( find_option("options","o",0) ){ |
| @@ -1076,11 +1134,11 @@ | ||
| 1076 | 1134 | ){ |
| 1077 | 1135 | helptextVtab_vtab *pNew; |
| 1078 | 1136 | int rc; |
| 1079 | 1137 | |
| 1080 | 1138 | rc = sqlite3_declare_vtab(db, |
| 1081 | - "CREATE TABLE x(name,type,flags,helptext)" | |
| 1139 | + "CREATE TABLE x(name,type,flags,helptext,formatted,html)" | |
| 1082 | 1140 | ); |
| 1083 | 1141 | if( rc==SQLITE_OK ){ |
| 1084 | 1142 | pNew = sqlite3_malloc( sizeof(*pNew) ); |
| 1085 | 1143 | *ppVtab = (sqlite3_vtab*)pNew; |
| 1086 | 1144 | if( pNew==0 ) return SQLITE_NOMEM; |
| @@ -1160,10 +1218,24 @@ | ||
| 1160 | 1218 | sqlite3_result_int(ctx, pPage->eCmdFlags); |
| 1161 | 1219 | break; |
| 1162 | 1220 | case 3: /* helptext */ |
| 1163 | 1221 | sqlite3_result_text(ctx, pPage->zHelp, -1, SQLITE_STATIC); |
| 1164 | 1222 | break; |
| 1223 | + case 4: { /* formatted */ | |
| 1224 | + Blob txt; | |
| 1225 | + blob_init(&txt, 0, 0); | |
| 1226 | + help_to_text(pPage->zHelp, &txt); | |
| 1227 | + sqlite3_result_text(ctx, blob_str(&txt), -1, fossil_free); | |
| 1228 | + break; | |
| 1229 | + } | |
| 1230 | + case 5: { /* formatted */ | |
| 1231 | + Blob txt; | |
| 1232 | + blob_init(&txt, 0, 0); | |
| 1233 | + help_to_html(pPage->zHelp, &txt); | |
| 1234 | + sqlite3_result_text(ctx, blob_str(&txt), -1, fossil_free); | |
| 1235 | + break; | |
| 1236 | + } | |
| 1165 | 1237 | } |
| 1166 | 1238 | return SQLITE_OK; |
| 1167 | 1239 | } |
| 1168 | 1240 | |
| 1169 | 1241 | /* |
| 1170 | 1242 |
| --- src/dispatch.c | |
| +++ src/dispatch.c | |
| @@ -263,10 +263,48 @@ | |
| 263 | if( zEnd[0] ) blob_append(pOut, zEnd, -1); |
| 264 | i = j; |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | /* |
| 270 | ** Attempt to reformat plain-text help into HTML for display on a webpage. |
| 271 | ** |
| 272 | ** The HTML output is appended to Blob pHtml, which should already be |
| @@ -305,17 +343,22 @@ | |
| 305 | azEnd[0] = ""; |
| 306 | while( zHelp[0] ){ |
| 307 | i = 0; |
| 308 | while( (c = zHelp[i])!=0 |
| 309 | && c!='\n' |
| 310 | && (c!='%' || strncmp(zHelp+i,"%fossil",7)!=0) |
| 311 | ){ i++; } |
| 312 | if( c=='%' ){ |
| 313 | if( i ) blob_appendf(pHtml, "%#h", i, zHelp); |
| 314 | zHelp += i + 1; |
| 315 | i = 0; |
| 316 | wantBR = 1; |
| 317 | continue; |
| 318 | } |
| 319 | if( i>2 && zHelp[0]=='>' && zHelp[1]==' ' ){ |
| 320 | isDT = 1; |
| 321 | for(nIndent=1; nIndent<i && zHelp[nIndent]==' '; nIndent++){} |
| @@ -394,11 +437,12 @@ | |
| 394 | }else if( wantBR ){ |
| 395 | appendMixedFont(pHtml, zHelp+nIndent, i-nIndent); |
| 396 | blob_append(pHtml, "<br>\n", 5); |
| 397 | wantBR = 0; |
| 398 | }else{ |
| 399 | blob_appendf(pHtml, "%#h\n", i-nIndent, zHelp+nIndent); |
| 400 | } |
| 401 | zHelp += i+1; |
| 402 | i = 0; |
| 403 | if( c==0 ) break; |
| 404 | } |
| @@ -409,11 +453,11 @@ | |
| 409 | |
| 410 | /* |
| 411 | ** Format help text for TTY display. |
| 412 | */ |
| 413 | static void help_to_text(const char *zHelp, Blob *pText){ |
| 414 | int i; |
| 415 | char c; |
| 416 | for(i=0; (c = zHelp[i])!=0; i++){ |
| 417 | if( c=='%' && strncmp(zHelp+i,"%fossil",7)==0 ){ |
| 418 | if( i>0 ) blob_append(pText, zHelp, i); |
| 419 | blob_append(pText, "fossil", 6); |
| @@ -426,10 +470,18 @@ | |
| 426 | blob_append(pText, " ", 1); |
| 427 | zHelp += i+2; |
| 428 | i = -1; |
| 429 | continue; |
| 430 | } |
| 431 | } |
| 432 | if( i>0 ){ |
| 433 | blob_append(pText, zHelp, i); |
| 434 | } |
| 435 | } |
| @@ -447,15 +499,17 @@ | |
| 447 | ** -e|--everything Show all commands and pages. |
| 448 | ** -t|--test Include test- commands |
| 449 | ** -w|--www Show WWW pages. |
| 450 | ** -s|--settings Show settings. |
| 451 | ** -h|--html Transform output to HTML. |
| 452 | */ |
| 453 | void test_all_help_cmd(void){ |
| 454 | int i; |
| 455 | int mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER; |
| 456 | int useHtml = find_option("html","h",0)!=0; |
| 457 | |
| 458 | if( find_option("www","w",0) ){ |
| 459 | mask = CMDFLAG_WEBPAGE; |
| 460 | } |
| 461 | if( find_option("everything","e",0) ){ |
| @@ -488,10 +542,13 @@ | |
| 488 | blob_init(&html, 0, 0); |
| 489 | help_to_html(aCommand[i].zHelp, &html); |
| 490 | fossil_print("<h1>%h</h1>\n", aCommand[i].zName); |
| 491 | fossil_print("%s\n<hr>\n", blob_str(&html)); |
| 492 | blob_reset(&html); |
| 493 | }else{ |
| 494 | Blob txt; |
| 495 | blob_init(&txt, 0, 0); |
| 496 | help_to_text(aCommand[i].zHelp, &txt); |
| 497 | fossil_print("# %s\n", aCommand[i].zName); |
| @@ -665,11 +722,11 @@ | |
| 665 | const CmdOrPage *pCmd = 0; |
| 666 | |
| 667 | style_header("Help: %s", zCmd); |
| 668 | |
| 669 | style_submenu_element("Command-List", "%s/help", g.zTop); |
| 670 | rc = dispatch_name_search(zCmd, CMDFLAG_ANY, &pCmd); |
| 671 | if( *zCmd=='/' ){ |
| 672 | /* Some of the webpages require query parameters in order to work. |
| 673 | ** @ <h1>The "<a href='%R%s(zCmd)'>%s(zCmd)</a>" page:</h1> */ |
| 674 | @ <h1>The "%h(zCmd)" page:</h1> |
| 675 | }else if( rc==0 && (pCmd->eCmdFlags & CMDFLAG_SETTING)!=0 ){ |
| @@ -913,11 +970,11 @@ | |
| 913 | ** setting. Webpage names begin with "/". If TOPIC is omitted, a list of |
| 914 | ** topics is returned. |
| 915 | ** |
| 916 | ** The following options can be used when TOPIC is omitted: |
| 917 | ** |
| 918 | ** -a|--all List both command and auxiliary commands |
| 919 | ** -o|--options List command-line options common to all commands |
| 920 | ** -s|--setting List setting names |
| 921 | ** -t|--test List unsupported "test" commands |
| 922 | ** -x|--aux List only auxiliary commands |
| 923 | ** -w|--www List all web pages |
| @@ -936,12 +993,13 @@ | |
| 936 | Blob txt; |
| 937 | if( g.argc<3 ){ |
| 938 | z = g.argv[0]; |
| 939 | fossil_print( |
| 940 | "Usage: %s help TOPIC\n" |
| 941 | "Common commands: (use \"%s help help\" for more options)\n", |
| 942 | z, z); |
| 943 | command_list(0, CMDFLAG_1ST_TIER); |
| 944 | version_cmd(); |
| 945 | return; |
| 946 | } |
| 947 | if( find_option("options","o",0) ){ |
| @@ -1076,11 +1134,11 @@ | |
| 1076 | ){ |
| 1077 | helptextVtab_vtab *pNew; |
| 1078 | int rc; |
| 1079 | |
| 1080 | rc = sqlite3_declare_vtab(db, |
| 1081 | "CREATE TABLE x(name,type,flags,helptext)" |
| 1082 | ); |
| 1083 | if( rc==SQLITE_OK ){ |
| 1084 | pNew = sqlite3_malloc( sizeof(*pNew) ); |
| 1085 | *ppVtab = (sqlite3_vtab*)pNew; |
| 1086 | if( pNew==0 ) return SQLITE_NOMEM; |
| @@ -1160,10 +1218,24 @@ | |
| 1160 | sqlite3_result_int(ctx, pPage->eCmdFlags); |
| 1161 | break; |
| 1162 | case 3: /* helptext */ |
| 1163 | sqlite3_result_text(ctx, pPage->zHelp, -1, SQLITE_STATIC); |
| 1164 | break; |
| 1165 | } |
| 1166 | return SQLITE_OK; |
| 1167 | } |
| 1168 | |
| 1169 | /* |
| 1170 |
| --- src/dispatch.c | |
| +++ src/dispatch.c | |
| @@ -263,10 +263,48 @@ | |
| 263 | if( zEnd[0] ) blob_append(pOut, zEnd, -1); |
| 264 | i = j; |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | /* |
| 270 | ** Input string zIn starts with '['. If the content is a hyperlink of the |
| 271 | ** form [[...]] then return the index of the closing ']'. Otherwise return 0. |
| 272 | */ |
| 273 | static int help_is_link(const char *z, int n){ |
| 274 | int i; |
| 275 | char c; |
| 276 | if( n<5 ) return 0; |
| 277 | if( z[1]!='[' ) return 0; |
| 278 | for(i=3; i<n && (c = z[i])!=0; i++){ |
| 279 | if( c==']' && z[i-1]==']' ) return i; |
| 280 | } |
| 281 | return 0; |
| 282 | } |
| 283 | |
| 284 | /* |
| 285 | ** Append text to pOut, adding hyperlink markup for [...]. |
| 286 | */ |
| 287 | static void appendLinked(Blob *pOut, const char *z, int n){ |
| 288 | int i = 0; |
| 289 | int j; |
| 290 | while( i<n ){ |
| 291 | if( z[i]=='[' && (j = help_is_link(z+i, n-i))>0 ){ |
| 292 | if( i ) blob_append(pOut, z, i); |
| 293 | z += i+2; |
| 294 | n -= i+2; |
| 295 | blob_appendf(pOut, "<a href='%R/help?cmd=%.*s'>%.*s</a>", |
| 296 | j-3, z, j-3, z); |
| 297 | z += j-1; |
| 298 | n -= j-1; |
| 299 | i = 0; |
| 300 | }else{ |
| 301 | i++; |
| 302 | } |
| 303 | } |
| 304 | blob_append(pOut, z, i); |
| 305 | } |
| 306 | |
| 307 | /* |
| 308 | ** Attempt to reformat plain-text help into HTML for display on a webpage. |
| 309 | ** |
| 310 | ** The HTML output is appended to Blob pHtml, which should already be |
| @@ -305,17 +343,22 @@ | |
| 343 | azEnd[0] = ""; |
| 344 | while( zHelp[0] ){ |
| 345 | i = 0; |
| 346 | while( (c = zHelp[i])!=0 |
| 347 | && c!='\n' |
| 348 | && c!='<' |
| 349 | && (c!='%' || strncmp(zHelp+i,"%fossil",7)!=0) |
| 350 | ){ i++; } |
| 351 | if( c=='%' ){ |
| 352 | if( i ) blob_appendf(pHtml, "%#h", i, zHelp); |
| 353 | zHelp += i + 1; |
| 354 | wantBR = 1; |
| 355 | continue; |
| 356 | }else if( c=='<' ){ |
| 357 | if( i ) blob_appendf(pHtml, "%#h", i, zHelp); |
| 358 | blob_append(pHtml, "&", 5); |
| 359 | zHelp += i + 1; |
| 360 | continue; |
| 361 | } |
| 362 | if( i>2 && zHelp[0]=='>' && zHelp[1]==' ' ){ |
| 363 | isDT = 1; |
| 364 | for(nIndent=1; nIndent<i && zHelp[nIndent]==' '; nIndent++){} |
| @@ -394,11 +437,12 @@ | |
| 437 | }else if( wantBR ){ |
| 438 | appendMixedFont(pHtml, zHelp+nIndent, i-nIndent); |
| 439 | blob_append(pHtml, "<br>\n", 5); |
| 440 | wantBR = 0; |
| 441 | }else{ |
| 442 | appendLinked(pHtml, zHelp+nIndent, i-nIndent); |
| 443 | blob_append_char(pHtml, '\n'); |
| 444 | } |
| 445 | zHelp += i+1; |
| 446 | i = 0; |
| 447 | if( c==0 ) break; |
| 448 | } |
| @@ -409,11 +453,11 @@ | |
| 453 | |
| 454 | /* |
| 455 | ** Format help text for TTY display. |
| 456 | */ |
| 457 | static void help_to_text(const char *zHelp, Blob *pText){ |
| 458 | int i, x; |
| 459 | char c; |
| 460 | for(i=0; (c = zHelp[i])!=0; i++){ |
| 461 | if( c=='%' && strncmp(zHelp+i,"%fossil",7)==0 ){ |
| 462 | if( i>0 ) blob_append(pText, zHelp, i); |
| 463 | blob_append(pText, "fossil", 6); |
| @@ -426,10 +470,18 @@ | |
| 470 | blob_append(pText, " ", 1); |
| 471 | zHelp += i+2; |
| 472 | i = -1; |
| 473 | continue; |
| 474 | } |
| 475 | if( c=='[' && (x = help_is_link(zHelp+i, 100000))!=0 ){ |
| 476 | if( i>0 ) blob_append(pText, zHelp, i); |
| 477 | zHelp += i+2; |
| 478 | blob_append(pText, zHelp, x-3); |
| 479 | zHelp += x-1; |
| 480 | i = -1; |
| 481 | continue; |
| 482 | } |
| 483 | } |
| 484 | if( i>0 ){ |
| 485 | blob_append(pText, zHelp, i); |
| 486 | } |
| 487 | } |
| @@ -447,15 +499,17 @@ | |
| 499 | ** -e|--everything Show all commands and pages. |
| 500 | ** -t|--test Include test- commands |
| 501 | ** -w|--www Show WWW pages. |
| 502 | ** -s|--settings Show settings. |
| 503 | ** -h|--html Transform output to HTML. |
| 504 | ** -r|--raw No output formatting. |
| 505 | */ |
| 506 | void test_all_help_cmd(void){ |
| 507 | int i; |
| 508 | int mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER; |
| 509 | int useHtml = find_option("html","h",0)!=0; |
| 510 | int rawOut = find_option("raw","r",0)!=0; |
| 511 | |
| 512 | if( find_option("www","w",0) ){ |
| 513 | mask = CMDFLAG_WEBPAGE; |
| 514 | } |
| 515 | if( find_option("everything","e",0) ){ |
| @@ -488,10 +542,13 @@ | |
| 542 | blob_init(&html, 0, 0); |
| 543 | help_to_html(aCommand[i].zHelp, &html); |
| 544 | fossil_print("<h1>%h</h1>\n", aCommand[i].zName); |
| 545 | fossil_print("%s\n<hr>\n", blob_str(&html)); |
| 546 | blob_reset(&html); |
| 547 | }else if( rawOut ){ |
| 548 | fossil_print("# %s\n", aCommand[i].zName); |
| 549 | fossil_print("%s\n\n", aCommand[i].zHelp); |
| 550 | }else{ |
| 551 | Blob txt; |
| 552 | blob_init(&txt, 0, 0); |
| 553 | help_to_text(aCommand[i].zHelp, &txt); |
| 554 | fossil_print("# %s\n", aCommand[i].zName); |
| @@ -665,11 +722,11 @@ | |
| 722 | const CmdOrPage *pCmd = 0; |
| 723 | |
| 724 | style_header("Help: %s", zCmd); |
| 725 | |
| 726 | style_submenu_element("Command-List", "%s/help", g.zTop); |
| 727 | rc = dispatch_name_search(zCmd, CMDFLAG_ANY|CMDFLAG_PREFIX, &pCmd); |
| 728 | if( *zCmd=='/' ){ |
| 729 | /* Some of the webpages require query parameters in order to work. |
| 730 | ** @ <h1>The "<a href='%R%s(zCmd)'>%s(zCmd)</a>" page:</h1> */ |
| 731 | @ <h1>The "%h(zCmd)" page:</h1> |
| 732 | }else if( rc==0 && (pCmd->eCmdFlags & CMDFLAG_SETTING)!=0 ){ |
| @@ -913,11 +970,11 @@ | |
| 970 | ** setting. Webpage names begin with "/". If TOPIC is omitted, a list of |
| 971 | ** topics is returned. |
| 972 | ** |
| 973 | ** The following options can be used when TOPIC is omitted: |
| 974 | ** |
| 975 | ** -a|--all List both common and auxiliary commands |
| 976 | ** -o|--options List command-line options common to all commands |
| 977 | ** -s|--setting List setting names |
| 978 | ** -t|--test List unsupported "test" commands |
| 979 | ** -x|--aux List only auxiliary commands |
| 980 | ** -w|--www List all web pages |
| @@ -936,12 +993,13 @@ | |
| 993 | Blob txt; |
| 994 | if( g.argc<3 ){ |
| 995 | z = g.argv[0]; |
| 996 | fossil_print( |
| 997 | "Usage: %s help TOPIC\n" |
| 998 | "Try \"%s help help\" or \"%s help -a\" for more options\n" |
| 999 | "Frequently used commands:\n", |
| 1000 | z, z, z); |
| 1001 | command_list(0, CMDFLAG_1ST_TIER); |
| 1002 | version_cmd(); |
| 1003 | return; |
| 1004 | } |
| 1005 | if( find_option("options","o",0) ){ |
| @@ -1076,11 +1134,11 @@ | |
| 1134 | ){ |
| 1135 | helptextVtab_vtab *pNew; |
| 1136 | int rc; |
| 1137 | |
| 1138 | rc = sqlite3_declare_vtab(db, |
| 1139 | "CREATE TABLE x(name,type,flags,helptext,formatted,html)" |
| 1140 | ); |
| 1141 | if( rc==SQLITE_OK ){ |
| 1142 | pNew = sqlite3_malloc( sizeof(*pNew) ); |
| 1143 | *ppVtab = (sqlite3_vtab*)pNew; |
| 1144 | if( pNew==0 ) return SQLITE_NOMEM; |
| @@ -1160,10 +1218,24 @@ | |
| 1218 | sqlite3_result_int(ctx, pPage->eCmdFlags); |
| 1219 | break; |
| 1220 | case 3: /* helptext */ |
| 1221 | sqlite3_result_text(ctx, pPage->zHelp, -1, SQLITE_STATIC); |
| 1222 | break; |
| 1223 | case 4: { /* formatted */ |
| 1224 | Blob txt; |
| 1225 | blob_init(&txt, 0, 0); |
| 1226 | help_to_text(pPage->zHelp, &txt); |
| 1227 | sqlite3_result_text(ctx, blob_str(&txt), -1, fossil_free); |
| 1228 | break; |
| 1229 | } |
| 1230 | case 5: { /* formatted */ |
| 1231 | Blob txt; |
| 1232 | blob_init(&txt, 0, 0); |
| 1233 | help_to_html(pPage->zHelp, &txt); |
| 1234 | sqlite3_result_text(ctx, blob_str(&txt), -1, fossil_free); |
| 1235 | break; |
| 1236 | } |
| 1237 | } |
| 1238 | return SQLITE_OK; |
| 1239 | } |
| 1240 | |
| 1241 | /* |
| 1242 |
+16
-11
| --- src/doc.c | ||
| +++ src/doc.c | ||
| @@ -147,11 +147,11 @@ | ||
| 147 | 147 | { "jad", 3, "text/vnd.sun.j2me.app-descriptor" }, |
| 148 | 148 | { "jar", 3, "application/java-archive" }, |
| 149 | 149 | { "jpe", 3, "image/jpeg" }, |
| 150 | 150 | { "jpeg", 4, "image/jpeg" }, |
| 151 | 151 | { "jpg", 3, "image/jpeg" }, |
| 152 | - { "js", 2, "application/x-javascript" }, | |
| 152 | + { "js", 2, "application/javascript" }, | |
| 153 | 153 | { "kar", 3, "audio/midi" }, |
| 154 | 154 | { "latex", 5, "application/x-latex" }, |
| 155 | 155 | { "lha", 3, "application/octet-stream" }, |
| 156 | 156 | { "lsp", 3, "application/x-lisp" }, |
| 157 | 157 | { "lzh", 3, "application/octet-stream" }, |
| @@ -1132,29 +1132,34 @@ | ||
| 1132 | 1132 | |
| 1133 | 1133 | |
| 1134 | 1134 | /* |
| 1135 | 1135 | ** WEBPAGE: favicon.ico |
| 1136 | 1136 | ** |
| 1137 | -** Return the default favicon.ico image. The returned image is for the | |
| 1138 | -** Fossil lizard icon. | |
| 1137 | +** Return the configured "favicon.ico" image. If no "favicon.ico" image | |
| 1138 | +** is defined, the returned image is for the Fossil lizard icon. | |
| 1139 | 1139 | ** |
| 1140 | -** The intended use case here is to supply a favicon for the "fossil ui" | |
| 1140 | +** The intended use case here is to supply an icon for the "fossil ui" | |
| 1141 | 1141 | ** command. For a permanent website, the recommended process is for |
| 1142 | -** the admin to set up a project-specific favicon and reference that | |
| 1143 | -** icon in the HTML header using a line like: | |
| 1142 | +** the admin to set up a project-specific icon and reference that icon | |
| 1143 | +** in the HTML header using a line like: | |
| 1144 | 1144 | ** |
| 1145 | 1145 | ** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/> |
| 1146 | 1146 | ** |
| 1147 | 1147 | */ |
| 1148 | 1148 | void favicon_page(void){ |
| 1149 | - Blob favicon; | |
| 1149 | + Blob icon; | |
| 1150 | + char *zMime; | |
| 1150 | 1151 | |
| 1151 | 1152 | etag_check(ETAG_CONFIG, 0); |
| 1152 | - blob_zero(&favicon); | |
| 1153 | - blob_init(&favicon, (char*)aLogo, sizeof(aLogo)); | |
| 1154 | - cgi_set_content_type("image/gif"); | |
| 1155 | - cgi_set_content(&favicon); | |
| 1153 | + zMime = db_get("icon-mimetype", "image/gif"); | |
| 1154 | + blob_zero(&icon); | |
| 1155 | + db_blob(&icon, "SELECT value FROM config WHERE name='icon-image'"); | |
| 1156 | + if( blob_size(&icon)==0 ){ | |
| 1157 | + blob_init(&icon, (char*)aLogo, sizeof(aLogo)); | |
| 1158 | + } | |
| 1159 | + cgi_set_content_type(zMime); | |
| 1160 | + cgi_set_content(&icon); | |
| 1156 | 1161 | } |
| 1157 | 1162 | |
| 1158 | 1163 | /* |
| 1159 | 1164 | ** WEBPAGE: docsrch |
| 1160 | 1165 | ** |
| 1161 | 1166 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -147,11 +147,11 @@ | |
| 147 | { "jad", 3, "text/vnd.sun.j2me.app-descriptor" }, |
| 148 | { "jar", 3, "application/java-archive" }, |
| 149 | { "jpe", 3, "image/jpeg" }, |
| 150 | { "jpeg", 4, "image/jpeg" }, |
| 151 | { "jpg", 3, "image/jpeg" }, |
| 152 | { "js", 2, "application/x-javascript" }, |
| 153 | { "kar", 3, "audio/midi" }, |
| 154 | { "latex", 5, "application/x-latex" }, |
| 155 | { "lha", 3, "application/octet-stream" }, |
| 156 | { "lsp", 3, "application/x-lisp" }, |
| 157 | { "lzh", 3, "application/octet-stream" }, |
| @@ -1132,29 +1132,34 @@ | |
| 1132 | |
| 1133 | |
| 1134 | /* |
| 1135 | ** WEBPAGE: favicon.ico |
| 1136 | ** |
| 1137 | ** Return the default favicon.ico image. The returned image is for the |
| 1138 | ** Fossil lizard icon. |
| 1139 | ** |
| 1140 | ** The intended use case here is to supply a favicon for the "fossil ui" |
| 1141 | ** command. For a permanent website, the recommended process is for |
| 1142 | ** the admin to set up a project-specific favicon and reference that |
| 1143 | ** icon in the HTML header using a line like: |
| 1144 | ** |
| 1145 | ** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/> |
| 1146 | ** |
| 1147 | */ |
| 1148 | void favicon_page(void){ |
| 1149 | Blob favicon; |
| 1150 | |
| 1151 | etag_check(ETAG_CONFIG, 0); |
| 1152 | blob_zero(&favicon); |
| 1153 | blob_init(&favicon, (char*)aLogo, sizeof(aLogo)); |
| 1154 | cgi_set_content_type("image/gif"); |
| 1155 | cgi_set_content(&favicon); |
| 1156 | } |
| 1157 | |
| 1158 | /* |
| 1159 | ** WEBPAGE: docsrch |
| 1160 | ** |
| 1161 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -147,11 +147,11 @@ | |
| 147 | { "jad", 3, "text/vnd.sun.j2me.app-descriptor" }, |
| 148 | { "jar", 3, "application/java-archive" }, |
| 149 | { "jpe", 3, "image/jpeg" }, |
| 150 | { "jpeg", 4, "image/jpeg" }, |
| 151 | { "jpg", 3, "image/jpeg" }, |
| 152 | { "js", 2, "application/javascript" }, |
| 153 | { "kar", 3, "audio/midi" }, |
| 154 | { "latex", 5, "application/x-latex" }, |
| 155 | { "lha", 3, "application/octet-stream" }, |
| 156 | { "lsp", 3, "application/x-lisp" }, |
| 157 | { "lzh", 3, "application/octet-stream" }, |
| @@ -1132,29 +1132,34 @@ | |
| 1132 | |
| 1133 | |
| 1134 | /* |
| 1135 | ** WEBPAGE: favicon.ico |
| 1136 | ** |
| 1137 | ** Return the configured "favicon.ico" image. If no "favicon.ico" image |
| 1138 | ** is defined, the returned image is for the Fossil lizard icon. |
| 1139 | ** |
| 1140 | ** The intended use case here is to supply an icon for the "fossil ui" |
| 1141 | ** command. For a permanent website, the recommended process is for |
| 1142 | ** the admin to set up a project-specific icon and reference that icon |
| 1143 | ** in the HTML header using a line like: |
| 1144 | ** |
| 1145 | ** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/> |
| 1146 | ** |
| 1147 | */ |
| 1148 | void favicon_page(void){ |
| 1149 | Blob icon; |
| 1150 | char *zMime; |
| 1151 | |
| 1152 | etag_check(ETAG_CONFIG, 0); |
| 1153 | zMime = db_get("icon-mimetype", "image/gif"); |
| 1154 | blob_zero(&icon); |
| 1155 | db_blob(&icon, "SELECT value FROM config WHERE name='icon-image'"); |
| 1156 | if( blob_size(&icon)==0 ){ |
| 1157 | blob_init(&icon, (char*)aLogo, sizeof(aLogo)); |
| 1158 | } |
| 1159 | cgi_set_content_type(zMime); |
| 1160 | cgi_set_content(&icon); |
| 1161 | } |
| 1162 | |
| 1163 | /* |
| 1164 | ** WEBPAGE: docsrch |
| 1165 | ** |
| 1166 |
+4
-1
| --- src/etag.c | ||
| +++ src/etag.c | ||
| @@ -97,11 +97,14 @@ | ||
| 97 | 97 | const char *zIfNoneMatch; |
| 98 | 98 | char zBuf[50]; |
| 99 | 99 | assert( zETag[0]==0 ); /* Only call this routine once! */ |
| 100 | 100 | |
| 101 | 101 | if( etagCancelled ) return; |
| 102 | - iMaxAge = 86400; | |
| 102 | + | |
| 103 | + /* By default, ETagged URLs never expire since the ETag will change | |
| 104 | + * when the content changes. Approximate this policy as 10 years. */ | |
| 105 | + iMaxAge = 10 * 365 * 24 * 60 * 60; | |
| 103 | 106 | md5sum_init(); |
| 104 | 107 | |
| 105 | 108 | /* Always include the executable ID as part of the hash */ |
| 106 | 109 | md5sum_step_text("exe-id: ", -1); |
| 107 | 110 | md5sum_step_text(fossil_exe_id(), -1); |
| 108 | 111 |
| --- src/etag.c | |
| +++ src/etag.c | |
| @@ -97,11 +97,14 @@ | |
| 97 | const char *zIfNoneMatch; |
| 98 | char zBuf[50]; |
| 99 | assert( zETag[0]==0 ); /* Only call this routine once! */ |
| 100 | |
| 101 | if( etagCancelled ) return; |
| 102 | iMaxAge = 86400; |
| 103 | md5sum_init(); |
| 104 | |
| 105 | /* Always include the executable ID as part of the hash */ |
| 106 | md5sum_step_text("exe-id: ", -1); |
| 107 | md5sum_step_text(fossil_exe_id(), -1); |
| 108 |
| --- src/etag.c | |
| +++ src/etag.c | |
| @@ -97,11 +97,14 @@ | |
| 97 | const char *zIfNoneMatch; |
| 98 | char zBuf[50]; |
| 99 | assert( zETag[0]==0 ); /* Only call this routine once! */ |
| 100 | |
| 101 | if( etagCancelled ) return; |
| 102 | |
| 103 | /* By default, ETagged URLs never expire since the ETag will change |
| 104 | * when the content changes. Approximate this policy as 10 years. */ |
| 105 | iMaxAge = 10 * 365 * 24 * 60 * 60; |
| 106 | md5sum_init(); |
| 107 | |
| 108 | /* Always include the executable ID as part of the hash */ |
| 109 | md5sum_step_text("exe-id: ", -1); |
| 110 | md5sum_step_text(fossil_exe_id(), -1); |
| 111 |
+1
-1
| --- src/export.c | ||
| +++ src/export.c | ||
| @@ -1642,11 +1642,11 @@ | ||
| 1642 | 1642 | k = db_int(0, "SELECT count(*) FROm mmark WHERE NOT isfile"); |
| 1643 | 1643 | fossil_print("Exported: %d check-ins and %d file blobs\n", k, n); |
| 1644 | 1644 | } |
| 1645 | 1645 | |
| 1646 | 1646 | /* |
| 1647 | -** COMMAND: git | |
| 1647 | +** COMMAND: git* | |
| 1648 | 1648 | ** |
| 1649 | 1649 | ** Usage: %fossil git SUBCOMMAND |
| 1650 | 1650 | ** |
| 1651 | 1651 | ** Do incremental import or export operations between Fossil and Git. |
| 1652 | 1652 | ** Subcommands: |
| 1653 | 1653 |
| --- src/export.c | |
| +++ src/export.c | |
| @@ -1642,11 +1642,11 @@ | |
| 1642 | k = db_int(0, "SELECT count(*) FROm mmark WHERE NOT isfile"); |
| 1643 | fossil_print("Exported: %d check-ins and %d file blobs\n", k, n); |
| 1644 | } |
| 1645 | |
| 1646 | /* |
| 1647 | ** COMMAND: git |
| 1648 | ** |
| 1649 | ** Usage: %fossil git SUBCOMMAND |
| 1650 | ** |
| 1651 | ** Do incremental import or export operations between Fossil and Git. |
| 1652 | ** Subcommands: |
| 1653 |
| --- src/export.c | |
| +++ src/export.c | |
| @@ -1642,11 +1642,11 @@ | |
| 1642 | k = db_int(0, "SELECT count(*) FROm mmark WHERE NOT isfile"); |
| 1643 | fossil_print("Exported: %d check-ins and %d file blobs\n", k, n); |
| 1644 | } |
| 1645 | |
| 1646 | /* |
| 1647 | ** COMMAND: git* |
| 1648 | ** |
| 1649 | ** Usage: %fossil git SUBCOMMAND |
| 1650 | ** |
| 1651 | ** Do incremental import or export operations between Fossil and Git. |
| 1652 | ** Subcommands: |
| 1653 |
+26
| --- src/file.c | ||
| +++ src/file.c | ||
| @@ -1105,10 +1105,12 @@ | ||
| 1105 | 1105 | ** Remove redundant / characters |
| 1106 | 1106 | ** Remove all /./ path elements. |
| 1107 | 1107 | ** Convert /A/../ to just / |
| 1108 | 1108 | ** If the slash parameter is non-zero, the trailing slash, if any, |
| 1109 | 1109 | ** is retained. |
| 1110 | +** | |
| 1111 | +** See also: file_canonical_name_dup() | |
| 1110 | 1112 | */ |
| 1111 | 1113 | void file_canonical_name(const char *zOrigName, Blob *pOut, int slash){ |
| 1112 | 1114 | blob_zero(pOut); |
| 1113 | 1115 | if( file_is_absolute_path(zOrigName) ){ |
| 1114 | 1116 | blob_appendf(pOut, "%/", zOrigName); |
| @@ -1140,10 +1142,25 @@ | ||
| 1140 | 1142 | } |
| 1141 | 1143 | #endif |
| 1142 | 1144 | blob_resize(pOut, file_simplify_name(blob_buffer(pOut), |
| 1143 | 1145 | blob_size(pOut), slash)); |
| 1144 | 1146 | } |
| 1147 | + | |
| 1148 | +/* | |
| 1149 | +** Compute the canonical name of a file. Store that name in | |
| 1150 | +** memory obtained from fossil_malloc() and return a pointer to the | |
| 1151 | +** name. | |
| 1152 | +** | |
| 1153 | +** See also: file_canonical_name() | |
| 1154 | +*/ | |
| 1155 | +char *file_canonical_name_dup(const char *zOrigName){ | |
| 1156 | + Blob x; | |
| 1157 | + if( zOrigName==0 ) return 0; | |
| 1158 | + blob_init(&x, 0, 0); | |
| 1159 | + file_canonical_name(zOrigName, &x, 0); | |
| 1160 | + return blob_str(&x); | |
| 1161 | +} | |
| 1145 | 1162 | |
| 1146 | 1163 | /* |
| 1147 | 1164 | ** The input is the name of an executable, such as one might |
| 1148 | 1165 | ** type on a command-line. This routine resolves that name into |
| 1149 | 1166 | ** a full pathname. The result is obtained from fossil_malloc() |
| @@ -2380,5 +2397,14 @@ | ||
| 2380 | 2397 | changeCount); |
| 2381 | 2398 | }else{ |
| 2382 | 2399 | fossil_print("Touched %d file(s)\n", changeCount); |
| 2383 | 2400 | } |
| 2384 | 2401 | } |
| 2402 | + | |
| 2403 | +/* | |
| 2404 | +** If zFileName is not NULL and contains a '.', this returns a pointer | |
| 2405 | +** to the position after the final '.', else it returns NULL. | |
| 2406 | +*/ | |
| 2407 | +const char * file_extension(const char *zFileName){ | |
| 2408 | + const char * zExt = strrchr(zFileName, '.'); | |
| 2409 | + return zExt ? &zExt[1] : 0; | |
| 2410 | +} | |
| 2385 | 2411 |
| --- src/file.c | |
| +++ src/file.c | |
| @@ -1105,10 +1105,12 @@ | |
| 1105 | ** Remove redundant / characters |
| 1106 | ** Remove all /./ path elements. |
| 1107 | ** Convert /A/../ to just / |
| 1108 | ** If the slash parameter is non-zero, the trailing slash, if any, |
| 1109 | ** is retained. |
| 1110 | */ |
| 1111 | void file_canonical_name(const char *zOrigName, Blob *pOut, int slash){ |
| 1112 | blob_zero(pOut); |
| 1113 | if( file_is_absolute_path(zOrigName) ){ |
| 1114 | blob_appendf(pOut, "%/", zOrigName); |
| @@ -1140,10 +1142,25 @@ | |
| 1140 | } |
| 1141 | #endif |
| 1142 | blob_resize(pOut, file_simplify_name(blob_buffer(pOut), |
| 1143 | blob_size(pOut), slash)); |
| 1144 | } |
| 1145 | |
| 1146 | /* |
| 1147 | ** The input is the name of an executable, such as one might |
| 1148 | ** type on a command-line. This routine resolves that name into |
| 1149 | ** a full pathname. The result is obtained from fossil_malloc() |
| @@ -2380,5 +2397,14 @@ | |
| 2380 | changeCount); |
| 2381 | }else{ |
| 2382 | fossil_print("Touched %d file(s)\n", changeCount); |
| 2383 | } |
| 2384 | } |
| 2385 |
| --- src/file.c | |
| +++ src/file.c | |
| @@ -1105,10 +1105,12 @@ | |
| 1105 | ** Remove redundant / characters |
| 1106 | ** Remove all /./ path elements. |
| 1107 | ** Convert /A/../ to just / |
| 1108 | ** If the slash parameter is non-zero, the trailing slash, if any, |
| 1109 | ** is retained. |
| 1110 | ** |
| 1111 | ** See also: file_canonical_name_dup() |
| 1112 | */ |
| 1113 | void file_canonical_name(const char *zOrigName, Blob *pOut, int slash){ |
| 1114 | blob_zero(pOut); |
| 1115 | if( file_is_absolute_path(zOrigName) ){ |
| 1116 | blob_appendf(pOut, "%/", zOrigName); |
| @@ -1140,10 +1142,25 @@ | |
| 1142 | } |
| 1143 | #endif |
| 1144 | blob_resize(pOut, file_simplify_name(blob_buffer(pOut), |
| 1145 | blob_size(pOut), slash)); |
| 1146 | } |
| 1147 | |
| 1148 | /* |
| 1149 | ** Compute the canonical name of a file. Store that name in |
| 1150 | ** memory obtained from fossil_malloc() and return a pointer to the |
| 1151 | ** name. |
| 1152 | ** |
| 1153 | ** See also: file_canonical_name() |
| 1154 | */ |
| 1155 | char *file_canonical_name_dup(const char *zOrigName){ |
| 1156 | Blob x; |
| 1157 | if( zOrigName==0 ) return 0; |
| 1158 | blob_init(&x, 0, 0); |
| 1159 | file_canonical_name(zOrigName, &x, 0); |
| 1160 | return blob_str(&x); |
| 1161 | } |
| 1162 | |
| 1163 | /* |
| 1164 | ** The input is the name of an executable, such as one might |
| 1165 | ** type on a command-line. This routine resolves that name into |
| 1166 | ** a full pathname. The result is obtained from fossil_malloc() |
| @@ -2380,5 +2397,14 @@ | |
| 2397 | changeCount); |
| 2398 | }else{ |
| 2399 | fossil_print("Touched %d file(s)\n", changeCount); |
| 2400 | } |
| 2401 | } |
| 2402 | |
| 2403 | /* |
| 2404 | ** If zFileName is not NULL and contains a '.', this returns a pointer |
| 2405 | ** to the position after the final '.', else it returns NULL. |
| 2406 | */ |
| 2407 | const char * file_extension(const char *zFileName){ |
| 2408 | const char * zExt = strrchr(zFileName, '.'); |
| 2409 | return zExt ? &zExt[1] : 0; |
| 2410 | } |
| 2411 |
+221
-144
| --- src/fileedit.c | ||
| +++ src/fileedit.c | ||
| @@ -364,14 +364,10 @@ | ||
| 364 | 364 | blob_appendf(pOut, "B %s\n", |
| 365 | 365 | pCI->pParent->zBaseline |
| 366 | 366 | ? pCI->pParent->zBaseline |
| 367 | 367 | : pCI->zParentUuid); |
| 368 | 368 | } |
| 369 | - blob_reserve(pOut, 1024 * | |
| 370 | - (asDelta ? 2 : pCI->pParent->nFile/11+1 | |
| 371 | - /* In the fossil core repo, each 12-ish F-cards (on | |
| 372 | - ** average) take up roughly 1kb */)); | |
| 373 | 369 | if(blob_size(&pCI->comment)!=0){ |
| 374 | 370 | blob_appendf(pOut, "C %F\n", blob_str(&pCI->comment)); |
| 375 | 371 | }else{ |
| 376 | 372 | blob_append(pOut, "C (no\\scomment)\n", 16); |
| 377 | 373 | } |
| @@ -965,11 +961,12 @@ | ||
| 965 | 961 | ** populates: |
| 966 | 962 | ** |
| 967 | 963 | ** - *zRevUuid = the fully-expanded value of zRev (owned by the |
| 968 | 964 | ** caller). zRevUuid may be NULL. |
| 969 | 965 | ** |
| 970 | -** - *vid = the RID of zRevUuid. May not be NULL. | |
| 966 | +** - *pVid = the RID of zRevUuid. pVid May be NULL. If the vid | |
| 967 | +** cannot be resolved or is ambiguous, pVid is not assigned. | |
| 971 | 968 | ** |
| 972 | 969 | ** - *frid = the RID of zFilename's blob content. May not be NULL |
| 973 | 970 | ** unless zFilename is also NULL. If BOTH of zFilename and frid are |
| 974 | 971 | ** NULL then no confirmation is done on the filename argument - only |
| 975 | 972 | ** zRev is checked. |
| @@ -979,38 +976,41 @@ | ||
| 979 | 976 | ** returns false, it queues up an error response and the caller must |
| 980 | 977 | ** return immediately. |
| 981 | 978 | */ |
| 982 | 979 | static int fileedit_ajax_setup_filerev(const char * zRev, |
| 983 | 980 | char ** zRevUuid, |
| 984 | - int * vid, | |
| 981 | + int * pVid, | |
| 985 | 982 | const char * zFilename, |
| 986 | 983 | int * frid){ |
| 987 | 984 | char * zFileUuid = 0; /* file content UUID */ |
| 988 | 985 | const int checkFile = zFilename!=0 || frid!=0; |
| 986 | + int vid = 0; | |
| 989 | 987 | |
| 990 | 988 | if(checkFile && !fileedit_ajax_check_filename(zFilename)){ |
| 991 | 989 | return 0; |
| 992 | 990 | } |
| 993 | - *vid = symbolic_name_to_rid(zRev, "ci"); | |
| 994 | - if(0==*vid){ | |
| 991 | + vid = symbolic_name_to_rid(zRev, "ci"); | |
| 992 | + if(0==vid){ | |
| 995 | 993 | ajax_route_error(404,"Cannot resolve name as a checkin: %s", |
| 996 | 994 | zRev); |
| 997 | 995 | return 0; |
| 998 | - }else if(*vid<0){ | |
| 996 | + }else if(vid<0){ | |
| 999 | 997 | ajax_route_error(400,"Checkin name is ambiguous: %s", |
| 1000 | 998 | zRev); |
| 1001 | 999 | return 0; |
| 1000 | + }else if(pVid!=0){ | |
| 1001 | + *pVid = vid; | |
| 1002 | 1002 | } |
| 1003 | 1003 | if(checkFile){ |
| 1004 | - zFileUuid = fileedit_file_uuid(zFilename, *vid, 0); | |
| 1004 | + zFileUuid = fileedit_file_uuid(zFilename, vid, 0); | |
| 1005 | 1005 | if(zFileUuid==0){ |
| 1006 | 1006 | ajax_route_error(404, "Checkin does not contain file."); |
| 1007 | 1007 | return 0; |
| 1008 | 1008 | } |
| 1009 | 1009 | } |
| 1010 | 1010 | if(zRevUuid!=0){ |
| 1011 | - *zRevUuid = rid_to_uuid(*vid); | |
| 1011 | + *zRevUuid = rid_to_uuid(vid); | |
| 1012 | 1012 | } |
| 1013 | 1013 | if(checkFile){ |
| 1014 | 1014 | assert(zFileUuid!=0); |
| 1015 | 1015 | if(frid!=0){ |
| 1016 | 1016 | *frid = fast_uuid_to_rid(zFileUuid); |
| @@ -1290,10 +1290,92 @@ | ||
| 1290 | 1290 | end_fail: |
| 1291 | 1291 | #undef fail |
| 1292 | 1292 | fossil_free(zFileUuid); |
| 1293 | 1293 | return rc ? rc : 500; |
| 1294 | 1294 | } |
| 1295 | + | |
| 1296 | +/* | |
| 1297 | +** Renders a list of all open leaves in JSON form: | |
| 1298 | +** | |
| 1299 | +** [ | |
| 1300 | +** {checkin: UUID, branch: branchName, timestamp: string} | |
| 1301 | +** ] | |
| 1302 | +** | |
| 1303 | +** The entries are ordered newest first. | |
| 1304 | +** | |
| 1305 | +** If zFirstUuid is not NULL then *zFirstUuid is set to a copy of the | |
| 1306 | +** full UUID of the first (most recent) leaf, which must be freed by | |
| 1307 | +** the caller. It is set to 0 if there are no leaves. | |
| 1308 | +*/ | |
| 1309 | +static void fileedit_render_leaves_list(char ** zFirstUuid){ | |
| 1310 | + Blob sql = empty_blob; | |
| 1311 | + Stmt q = empty_Stmt; | |
| 1312 | + int i = 0; | |
| 1313 | + | |
| 1314 | + if(zFirstUuid){ | |
| 1315 | + *zFirstUuid = 0; | |
| 1316 | + } | |
| 1317 | + blob_append(&sql, timeline_query_for_tty(), -1); | |
| 1318 | + blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " | |
| 1319 | + "WHERE NOT EXISTS(" | |
| 1320 | + "SELECT 1 from tagxref WHERE tagid=%d AND " | |
| 1321 | + "tagtype>0 AND rid=leaf.rid" | |
| 1322 | + ")) " | |
| 1323 | + "ORDER BY mtime DESC", TAG_CLOSED); | |
| 1324 | + db_prepare_blob(&q, &sql); | |
| 1325 | + CX("["); | |
| 1326 | + while( SQLITE_ROW==db_step(&q) ){ | |
| 1327 | + const char * zUuid = db_column_text(&q, 1); | |
| 1328 | + if(i++){ | |
| 1329 | + CX(","); | |
| 1330 | + }else if(zFirstUuid){ | |
| 1331 | + *zFirstUuid = fossil_strdup(zUuid); | |
| 1332 | + } | |
| 1333 | + CX("{"); | |
| 1334 | + CX("\"checkin\":%!j,", zUuid); | |
| 1335 | + CX("\"branch\":%!j,", db_column_text(&q, 7)); | |
| 1336 | + CX("\"timestamp\":%!j", db_column_text(&q, 2)); | |
| 1337 | + CX("}"); | |
| 1338 | + } | |
| 1339 | + CX("]"); | |
| 1340 | + db_finalize(&q); | |
| 1341 | +} | |
| 1342 | + | |
| 1343 | +/* | |
| 1344 | +** For the given fully resolved UUID, renders a JSON object containing | |
| 1345 | +** the fileeedit-editable files in that checkin: | |
| 1346 | +** | |
| 1347 | +** { | |
| 1348 | +** checkin: UUID, | |
| 1349 | +** editableFiles: [ filename1, ... filenameN ] | |
| 1350 | +** } | |
| 1351 | +** | |
| 1352 | +** They are sorted by name using filename_collation(). | |
| 1353 | +*/ | |
| 1354 | +static void fileedit_render_checkin_files(const char * zFullUuid){ | |
| 1355 | + Blob sql = empty_blob; | |
| 1356 | + Stmt q = empty_Stmt; | |
| 1357 | + int i = 0; | |
| 1358 | + | |
| 1359 | + CX("{\"checkin\":%!j," | |
| 1360 | + "\"editableFiles\":[", zFullUuid); | |
| 1361 | + blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " | |
| 1362 | + "ORDER BY filename %s", | |
| 1363 | + zFullUuid, filename_collation()); | |
| 1364 | + db_prepare_blob(&q, &sql); | |
| 1365 | + while( SQLITE_ROW==db_step(&q) ){ | |
| 1366 | + const char * zFilename = db_column_text(&q, 0); | |
| 1367 | + if(fileedit_is_editable(zFilename)){ | |
| 1368 | + if(i++){ | |
| 1369 | + CX(","); | |
| 1370 | + } | |
| 1371 | + CX("%!j", zFilename); | |
| 1372 | + } | |
| 1373 | + } | |
| 1374 | + db_finalize(&q); | |
| 1375 | + CX("]}"); | |
| 1376 | +} | |
| 1295 | 1377 | |
| 1296 | 1378 | /* |
| 1297 | 1379 | ** AJAX route /fileedit?ajax=filelist |
| 1298 | 1380 | ** |
| 1299 | 1381 | ** Fetches a JSON-format list of leaves and/or filenames for use in |
| @@ -1318,66 +1400,27 @@ | ||
| 1318 | 1400 | ** } |
| 1319 | 1401 | ** |
| 1320 | 1402 | ** On error it produces a JSON response as documented for |
| 1321 | 1403 | ** ajax_route_error(). |
| 1322 | 1404 | */ |
| 1323 | -static void fileedit_ajax_filelist(void){ | |
| 1405 | +static void fileedit_ajax_filelist(){ | |
| 1324 | 1406 | const char * zCi = PD("checkin",P("ci")); |
| 1325 | - Blob sql = empty_blob; | |
| 1326 | - Stmt q = empty_Stmt; | |
| 1327 | - int i = 0; | |
| 1328 | 1407 | |
| 1329 | 1408 | if(!ajax_route_bootstrap(1,0)){ |
| 1330 | 1409 | return; |
| 1331 | 1410 | } |
| 1332 | 1411 | cgi_set_content_type("application/json"); |
| 1333 | 1412 | if(zCi!=0){ |
| 1334 | 1413 | char * zCiFull = 0; |
| 1335 | - int vid = 0; | |
| 1336 | - if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, &vid, 0, 0)){ | |
| 1414 | + if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, 0, 0, 0)){ | |
| 1337 | 1415 | /* Error already reported */ |
| 1338 | 1416 | return; |
| 1339 | 1417 | } |
| 1340 | - CX("{\"checkin\":%!j," | |
| 1341 | - "\"editableFiles\":[", zCiFull); | |
| 1342 | - blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " | |
| 1343 | - "ORDER BY filename %s", | |
| 1344 | - zCiFull, filename_collation()); | |
| 1345 | - db_prepare_blob(&q, &sql); | |
| 1346 | - while( SQLITE_ROW==db_step(&q) ){ | |
| 1347 | - const char * zFilename = db_column_text(&q, 0); | |
| 1348 | - if(fileedit_is_editable(zFilename)){ | |
| 1349 | - if(i++){ | |
| 1350 | - CX(","); | |
| 1351 | - } | |
| 1352 | - CX("%!j", zFilename); | |
| 1353 | - } | |
| 1354 | - } | |
| 1355 | - db_finalize(&q); | |
| 1356 | - CX("]}"); | |
| 1418 | + fileedit_render_checkin_files(zCiFull); | |
| 1419 | + fossil_free(zCiFull); | |
| 1357 | 1420 | }else if(P("leaves")!=0){ |
| 1358 | - blob_append(&sql, timeline_query_for_tty(), -1); | |
| 1359 | - blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " | |
| 1360 | - "WHERE NOT EXISTS(" | |
| 1361 | - "SELECT 1 from tagxref WHERE tagid=%d AND " | |
| 1362 | - "tagtype>0 AND rid=leaf.rid" | |
| 1363 | - ")) " | |
| 1364 | - "ORDER BY mtime DESC", TAG_CLOSED); | |
| 1365 | - db_prepare_blob(&q, &sql); | |
| 1366 | - CX("["); | |
| 1367 | - while( SQLITE_ROW==db_step(&q) ){ | |
| 1368 | - if(i++){ | |
| 1369 | - CX(","); | |
| 1370 | - } | |
| 1371 | - CX("{"); | |
| 1372 | - CX("\"checkin\":%!j,", db_column_text(&q, 1)); | |
| 1373 | - CX("\"branch\":%!j,", db_column_text(&q, 7)); | |
| 1374 | - CX("\"timestamp\":%!j", db_column_text(&q, 2)); | |
| 1375 | - CX("}"); | |
| 1376 | - } | |
| 1377 | - CX("]"); | |
| 1378 | - db_finalize(&q); | |
| 1421 | + fileedit_render_leaves_list(0); | |
| 1379 | 1422 | }else{ |
| 1380 | 1423 | ajax_route_error(500, "Unhandled URL argument."); |
| 1381 | 1424 | } |
| 1382 | 1425 | } |
| 1383 | 1426 | |
| @@ -1484,52 +1527,60 @@ | ||
| 1484 | 1527 | } |
| 1485 | 1528 | |
| 1486 | 1529 | /* |
| 1487 | 1530 | ** WEBPAGE: fileedit |
| 1488 | 1531 | ** |
| 1489 | -** Enables the online editing and committing of individual text files. | |
| 1490 | -** Requires that the user have Write permissions. | |
| 1532 | +** Enables the online editing and committing of text files. Requires | |
| 1533 | +** that the user have Write permissions and that a user with setup | |
| 1534 | +** permissions has set the fileedit-glob setting to a list of glob | |
| 1535 | +** patterns matching files which may be edited (e.g. "*.wiki,*.md"). | |
| 1536 | +** Note that fileedit-glob, by design, is a local-only setting. | |
| 1537 | +** It does not sync across repository clones, and must be explicitly | |
| 1538 | +** set on any repositories where this page should be activated. | |
| 1491 | 1539 | ** |
| 1492 | 1540 | ** Optional query parameters: |
| 1493 | 1541 | ** |
| 1494 | 1542 | ** filename=FILENAME Repo-relative path to the file. |
| 1495 | 1543 | ** checkin=VERSION Checkin version, using any unambiguous |
| 1496 | -** supported symbolic version name. | |
| 1497 | -** | |
| 1498 | -** Internal-use parameters: | |
| 1499 | -** | |
| 1500 | -** name=string The name of a page-specific AJAX operation. | |
| 1501 | -** | |
| 1502 | -** Noting that fossil internally stores all URL path components after | |
| 1503 | -** the first as the "name" value. Thus /fileedit?name=blah is | |
| 1504 | -** equivalent to /fileedit/blah. The latter is the preferred | |
| 1505 | -** form. This means, however, that no fileedit ajax routes may make | |
| 1506 | -** use of the name parameter. | |
| 1507 | -** | |
| 1508 | -** Which additional parameters are used by each distinct ajax value is | |
| 1509 | -** an internal implementation detail and may change with any given | |
| 1510 | -** build of this code. An unknown "name" value triggers an error, as | |
| 1511 | -** documented for ajax_route_error(). | |
| 1544 | +** symbolic version name. | |
| 1545 | +** | |
| 1546 | +** If passed a filename but no checkin then it will attempt to | |
| 1547 | +** load that file from the most recent leaf checkin. | |
| 1548 | +** | |
| 1549 | +** Once the page is loaded, files may be selected from any open leaf | |
| 1550 | +** version. The only way to edit files from non-leaf checkins is to | |
| 1551 | +** pass both the filename and checkin as URL parameters to the page. | |
| 1552 | +** Users with the proper permissions will be presented with "Edit" | |
| 1553 | +** links in various file-specific contexts for files which match the | |
| 1554 | +** fileedit-glob, regardless of whether they refer to leaf versions or | |
| 1555 | +** not. | |
| 1512 | 1556 | */ |
| 1513 | 1557 | void fileedit_page(void){ |
| 1514 | - const char * zFilename = 0; /* filename. We'll accept 'name' | |
| 1515 | - because that param is handled | |
| 1516 | - specially by the core. */ | |
| 1517 | - const char * zRev = 0; /* checkin version */ | |
| 1518 | 1558 | const char * zFileMime = 0; /* File mime type guess */ |
| 1519 | 1559 | CheckinMiniInfo cimi; /* Checkin state */ |
| 1520 | 1560 | int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */ |
| 1521 | 1561 | Blob err = empty_blob; /* Error report */ |
| 1522 | - Blob endScript = empty_blob; /* Script code to run at the | |
| 1523 | - end. This content will be | |
| 1524 | - combined into a single JS | |
| 1525 | - function call, thus each | |
| 1526 | - entry must end with a | |
| 1527 | - semicolon. */ | |
| 1528 | 1562 | const char *zAjax = P("name"); /* Name of AJAX route for |
| 1529 | 1563 | sub-dispatching. */ |
| 1530 | 1564 | |
| 1565 | + /* | |
| 1566 | + ** Internal-use URL parameters: | |
| 1567 | + ** | |
| 1568 | + ** name=string The name of a page-specific AJAX operation. | |
| 1569 | + ** | |
| 1570 | + ** Noting that fossil internally stores all URL path components | |
| 1571 | + ** after the first as the "name" value. Thus /fileedit?name=blah is | |
| 1572 | + ** equivalent to /fileedit/blah. The latter is the preferred | |
| 1573 | + ** form. This means, however, that no fileedit ajax routes may make | |
| 1574 | + ** use of the name parameter. | |
| 1575 | + ** | |
| 1576 | + ** Which additional parameters are used by each distinct ajax route | |
| 1577 | + ** is an internal implementation detail and may change with any | |
| 1578 | + ** given build of this code. An unknown "name" value triggers an | |
| 1579 | + ** error, as documented for ajax_route_error(). | |
| 1580 | + */ | |
| 1581 | + | |
| 1531 | 1582 | /* Allow no access to this page without check-in privilege */ |
| 1532 | 1583 | login_check_credentials(); |
| 1533 | 1584 | if( !g.perm.Write ){ |
| 1534 | 1585 | if(zAjax!=0){ |
| 1535 | 1586 | ajax_route_error(403, "Write permissions required."); |
| @@ -1590,14 +1641,11 @@ | ||
| 1590 | 1641 | ** transaction cleanly. |
| 1591 | 1642 | */ |
| 1592 | 1643 | { |
| 1593 | 1644 | int isMissingArg = 0; |
| 1594 | 1645 | if(fileedit_setup_cimi_from_p(&cimi, &err, &isMissingArg)==0){ |
| 1595 | - zFilename = cimi.zFilename; | |
| 1596 | - zRev = cimi.zParentUuid; | |
| 1597 | - assert(zRev); | |
| 1598 | - assert(zFilename); | |
| 1646 | + assert(cimi.zFilename); | |
| 1599 | 1647 | zFileMime = mimetype_from_name(cimi.zFilename); |
| 1600 | 1648 | }else if(isMissingArg!=0){ |
| 1601 | 1649 | /* Squelch these startup warnings - they're non-fatal now but |
| 1602 | 1650 | ** used to be fatal. */ |
| 1603 | 1651 | blob_reset(&err); |
| @@ -1626,34 +1674,39 @@ | ||
| 1626 | 1674 | /* Status bar */ |
| 1627 | 1675 | CX("<div id='fossil-status-bar' " |
| 1628 | 1676 | "title='Status message area. Double-click to clear them.'>" |
| 1629 | 1677 | "Status messages will go here.</div>\n" |
| 1630 | 1678 | /* will be moved into the tab container via JS */); |
| 1679 | + | |
| 1680 | + CX("<div id='fileedit-edit-status'>" | |
| 1681 | + "<span class='name'>(no file loaded)</span>" | |
| 1682 | + "<span class='links'></span>" | |
| 1683 | + "</div>"); | |
| 1631 | 1684 | |
| 1632 | 1685 | /* Main tab container... */ |
| 1633 | 1686 | CX("<div id='fileedit-tabs' class='tab-container'></div>"); |
| 1687 | + | |
| 1688 | + /* The .hidden class on the following tab elements is to help lessen | |
| 1689 | + the FOUC effect of the tabs before JS re-assembles them. */ | |
| 1634 | 1690 | |
| 1635 | 1691 | /***** File/version info tab *****/ |
| 1636 | 1692 | { |
| 1637 | 1693 | CX("<div id='fileedit-tab-fileselect' " |
| 1638 | 1694 | "data-tab-parent='fileedit-tabs' " |
| 1639 | - "data-tab-label='File Info & Selection'" | |
| 1695 | + "data-tab-label='File Selection' " | |
| 1696 | + "class='hidden'" | |
| 1640 | 1697 | ">"); |
| 1641 | - CX("<fieldset id='file-version-details'>" | |
| 1642 | - "<legend>File/Version</legend>" | |
| 1643 | - "<div>No file loaded.</div>" | |
| 1644 | - "</fieldset>"); | |
| 1645 | - CX("<h1>Select a file to edit:</h1>"); | |
| 1646 | 1698 | CX("<div id='fileedit-file-selector'></div>"); |
| 1647 | 1699 | CX("</div>"/*#fileedit-tab-fileselect*/); |
| 1648 | 1700 | } |
| 1649 | 1701 | |
| 1650 | 1702 | /******* Content tab *******/ |
| 1651 | 1703 | { |
| 1652 | 1704 | CX("<div id='fileedit-tab-content' " |
| 1653 | 1705 | "data-tab-parent='fileedit-tabs' " |
| 1654 | - "data-tab-label='File Content'" | |
| 1706 | + "data-tab-label='File Content' " | |
| 1707 | + "class='hidden'" | |
| 1655 | 1708 | ">"); |
| 1656 | 1709 | CX("<div class='flex-container flex-row child-gap-small'>"); |
| 1657 | 1710 | CX("<button class='fileedit-content-reload confirmer' " |
| 1658 | 1711 | "title='Reload the file from the server, discarding " |
| 1659 | 1712 | "any local edits. To help avoid accidental loss of " |
| @@ -1668,22 +1721,22 @@ | ||
| 1668 | 1721 | "150%", 150, "175%", 175, |
| 1669 | 1722 | "200%", 200, NULL); |
| 1670 | 1723 | CX("</div>"); |
| 1671 | 1724 | CX("<div class='flex-container flex-column stretch'>"); |
| 1672 | 1725 | CX("<textarea name='content' id='fileedit-content-editor' " |
| 1673 | - "class='fileedit' " | |
| 1674 | - "rows='20' cols='80'>"); | |
| 1726 | + "class='fileedit' rows='25'>"); | |
| 1675 | 1727 | CX("</textarea>"); |
| 1676 | 1728 | CX("</div>"/*textarea wrapper*/); |
| 1677 | 1729 | CX("</div>"/*#tab-file-content*/); |
| 1678 | 1730 | } |
| 1679 | 1731 | |
| 1680 | 1732 | /****** Preview tab ******/ |
| 1681 | 1733 | { |
| 1682 | 1734 | CX("<div id='fileedit-tab-preview' " |
| 1683 | 1735 | "data-tab-parent='fileedit-tabs' " |
| 1684 | - "data-tab-label='Preview'" | |
| 1736 | + "data-tab-label='Preview' " | |
| 1737 | + "class='hidden'" | |
| 1685 | 1738 | ">"); |
| 1686 | 1739 | CX("<div class='fileedit-options flex-container flex-row'>"); |
| 1687 | 1740 | CX("<button id='btn-preview-refresh' " |
| 1688 | 1741 | "data-f-preview-from='fileContent' " |
| 1689 | 1742 | /* ^^^ fossil.page[methodName]() OR text source elem ID, |
| @@ -1741,14 +1794,16 @@ | ||
| 1741 | 1794 | |
| 1742 | 1795 | /****** Diff tab ******/ |
| 1743 | 1796 | { |
| 1744 | 1797 | CX("<div id='fileedit-tab-diff' " |
| 1745 | 1798 | "data-tab-parent='fileedit-tabs' " |
| 1746 | - "data-tab-label='Diff'" | |
| 1799 | + "data-tab-label='Diff' " | |
| 1800 | + "class='hidden'" | |
| 1747 | 1801 | ">"); |
| 1748 | 1802 | |
| 1749 | - CX("<div class='fileedit-options flex-container flex-row' " | |
| 1803 | + CX("<div class='fileedit-options flex-container " | |
| 1804 | + "flex-row child-gap-small' " | |
| 1750 | 1805 | "id='fileedit-tab-diff-buttons'>"); |
| 1751 | 1806 | CX("<button class='sbs'>Side-by-side</button>" |
| 1752 | 1807 | "<button class='unified'>Unified</button>"); |
| 1753 | 1808 | if(0){ |
| 1754 | 1809 | /* For the time being let's just ignore all whitespace |
| @@ -1773,11 +1828,12 @@ | ||
| 1773 | 1828 | } |
| 1774 | 1829 | |
| 1775 | 1830 | /****** Commit ******/ |
| 1776 | 1831 | CX("<div id='fileedit-tab-commit' " |
| 1777 | 1832 | "data-tab-parent='fileedit-tabs' " |
| 1778 | - "data-tab-label='Commit'" | |
| 1833 | + "data-tab-label='Commit' " | |
| 1834 | + "class='hidden'" | |
| 1779 | 1835 | ">"); |
| 1780 | 1836 | { |
| 1781 | 1837 | /******* Commit flags/options *******/ |
| 1782 | 1838 | CX("<div class='fileedit-options flex-container flex-row'>"); |
| 1783 | 1839 | style_labeled_checkbox("cb-dry-run", |
| @@ -1894,11 +1950,12 @@ | ||
| 1894 | 1950 | CX("</div>"/*#fileedit-tab-commit*/); |
| 1895 | 1951 | |
| 1896 | 1952 | /****** Help/Tips ******/ |
| 1897 | 1953 | CX("<div id='fileedit-tab-help' " |
| 1898 | 1954 | "data-tab-parent='fileedit-tabs' " |
| 1899 | - "data-tab-label='Help'" | |
| 1955 | + "data-tab-label='Help' " | |
| 1956 | + "class='hidden'" | |
| 1900 | 1957 | ">"); |
| 1901 | 1958 | { |
| 1902 | 1959 | CX("<h1>Help & Tips</h1>"); |
| 1903 | 1960 | CX("<ul>"); |
| 1904 | 1961 | CX("<li><strong>Only files matching the <code>fileedit-glob</code> " |
| @@ -1922,61 +1979,81 @@ | ||
| 1922 | 1979 | "file/check-in combination are discarded.</li>"); |
| 1923 | 1980 | CX("</ul>"); |
| 1924 | 1981 | } |
| 1925 | 1982 | CX("</div>"/*#fileedit-tab-help*/); |
| 1926 | 1983 | |
| 1927 | - { | |
| 1928 | - /* Dynamically populate the editor, display any error in the err | |
| 1929 | - ** blob, and/or switch to tab #0, where the file selector | |
| 1930 | - ** lives... */ | |
| 1931 | - blob_appendf(&endScript, | |
| 1932 | - "fossil.onPageLoad("); | |
| 1933 | - if(zRev && zFilename){ | |
| 1934 | - assert(0==blob_size(&err)); | |
| 1935 | - blob_appendf(&endScript, | |
| 1936 | - "()=>fossil.page.loadFile(%!j,%!j)", | |
| 1937 | - zFilename, cimi.zParentUuid); | |
| 1938 | - }else{ | |
| 1939 | - blob_appendf(&endScript,"function(){\n"); | |
| 1940 | - if(blob_size(&err)>0){ | |
| 1941 | - blob_appendf(&endScript, | |
| 1942 | - "fossil.error(%!j);\n", | |
| 1943 | - blob_str(&err)); | |
| 1944 | - } | |
| 1945 | - blob_appendf(&endScript, | |
| 1946 | - "fossil.page.tabs.switchToTab(0);\n"); | |
| 1947 | - blob_appendf(&endScript,"}"); | |
| 1948 | - } | |
| 1949 | - blob_appendf(&endScript,");\n"); | |
| 1950 | - } | |
| 1951 | - | |
| 1952 | - blob_reset(&err); | |
| 1953 | - CheckinMiniInfo_cleanup(&cimi); | |
| 1954 | - style_emit_script_fossil_bootstrap(0); | |
| 1955 | - append_diff_javascript(1); | |
| 1956 | - style_emit_script_fetch(0); | |
| 1957 | - style_emit_script_tabs(0)/*also emits fossil.dom*/; | |
| 1958 | - style_emit_script_confirmer(0); | |
| 1959 | - style_emit_script_builtin(0, "fossil.storage.js"); | |
| 1960 | - | |
| 1984 | + builtin_request_js("sbsdiff.js"); | |
| 1985 | + style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer", | |
| 1986 | + "storage", 0); | |
| 1987 | + builtin_fulfill_js_requests(); | |
| 1961 | 1988 | /* |
| 1962 | 1989 | ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is |
| 1963 | 1990 | ** used for dynamically toggling certain UI components on and off. |
| 1964 | - ** Must come before fossil.page.fileedit.js. | |
| 1991 | + ** Must come after window.fossil has been intialized and before | |
| 1992 | + ** fossil.page.fileedit.js. Potential TODO: move this into the | |
| 1993 | + ** window.fossil bootstrapping so that we don't have to "fulfill" | |
| 1994 | + ** the JS multiple times. | |
| 1965 | 1995 | */ |
| 1966 | 1996 | ajax_emit_js_preview_modes(1); |
| 1967 | - | |
| 1968 | - style_emit_script_builtin(0, "fossil.page.fileedit.js"); | |
| 1969 | - if(blob_size(&endScript)>0){ | |
| 1997 | + builtin_request_js("fossil.page.fileedit.js"); | |
| 1998 | + builtin_fulfill_js_requests(); | |
| 1999 | + { | |
| 2000 | + /* Dynamically populate the editor, display any error in the err | |
| 2001 | + ** blob, and/or switch to tab #0, where the file selector | |
| 2002 | + ** lives. The extra C scopes here correspond to JS-level scopes, | |
| 2003 | + ** to improve grokability. */ | |
| 1970 | 2004 | style_emit_script_tag(0,0); |
| 1971 | 2005 | CX("\n(function(){\n"); |
| 1972 | - CX("try{\n%b}\n" | |
| 1973 | - "catch(e){" | |
| 2006 | + CX("try{\n"); | |
| 2007 | + { | |
| 2008 | + char * zFirstLeafUuid = 0; | |
| 2009 | + CX("fossil.config['fileedit-glob'] = "); | |
| 2010 | + glob_render_json_to_cgi(fileedit_glob()); | |
| 2011 | + CX(";\n"); | |
| 2012 | + if(blob_size(&err)>0){ | |
| 2013 | + CX("fossil.error(%!j);\n", blob_str(&err)); | |
| 2014 | + } | |
| 2015 | + /* Populate the page with the current leaves and, if available, | |
| 2016 | + the selected checkin's file list, to save 1 or 2 XHR requests | |
| 2017 | + at startup. That makes this page uncacheable, but compressed | |
| 2018 | + delivery of this page is currently less than 6k. */ | |
| 2019 | + CX("fossil.page.initialLeaves = "); | |
| 2020 | + fileedit_render_leaves_list(cimi.zParentUuid ? 0 : &zFirstLeafUuid); | |
| 2021 | + CX(";\n"); | |
| 2022 | + if(zFirstLeafUuid){ | |
| 2023 | + assert(!cimi.zParentUuid); | |
| 2024 | + cimi.zParentUuid = zFirstLeafUuid; | |
| 2025 | + zFirstLeafUuid = 0; | |
| 2026 | + } | |
| 2027 | + if(cimi.zParentUuid){ | |
| 2028 | + CX("fossil.page.initialFiles = "); | |
| 2029 | + fileedit_render_checkin_files(cimi.zParentUuid); | |
| 2030 | + CX(";\n"); | |
| 2031 | + } | |
| 2032 | + CX("fossil.onPageLoad(function(){\n"); | |
| 2033 | + { | |
| 2034 | + if(blob_size(&err)>0){ | |
| 2035 | + CX("fossil.error(%!j);\n", | |
| 2036 | + blob_str(&err)); | |
| 2037 | + CX("fossil.page.tabs.switchToTab(0);\n"); | |
| 2038 | + } | |
| 2039 | + if(cimi.zParentUuid && cimi.zFilename){ | |
| 2040 | + CX("fossil.page.loadFile(%!j,%!j);\n", | |
| 2041 | + cimi.zFilename, cimi.zParentUuid) | |
| 2042 | + /* Reminder we cannot embed the JSON-format | |
| 2043 | + content of the file here because if it contains | |
| 2044 | + a SCRIPT tag then it will break the whole page. */; | |
| 2045 | + } | |
| 2046 | + } | |
| 2047 | + CX("});\n")/*fossil.onPageLoad()*/; | |
| 2048 | + } | |
| 2049 | + CX("}catch(e){" | |
| 1974 | 2050 | "fossil.error(e); console.error('Exception:',e);" |
| 1975 | - "}\n", | |
| 1976 | - &endScript); | |
| 1977 | - CX("})();"); | |
| 2051 | + "}\n"); | |
| 2052 | + CX("})();")/*anonymous function*/; | |
| 1978 | 2053 | style_emit_script_tag(1,0); |
| 1979 | 2054 | } |
| 2055 | + blob_reset(&err); | |
| 2056 | + CheckinMiniInfo_cleanup(&cimi); | |
| 1980 | 2057 | db_end_transaction(0); |
| 1981 | 2058 | style_footer(); |
| 1982 | 2059 | } |
| 1983 | 2060 |
| --- src/fileedit.c | |
| +++ src/fileedit.c | |
| @@ -364,14 +364,10 @@ | |
| 364 | blob_appendf(pOut, "B %s\n", |
| 365 | pCI->pParent->zBaseline |
| 366 | ? pCI->pParent->zBaseline |
| 367 | : pCI->zParentUuid); |
| 368 | } |
| 369 | blob_reserve(pOut, 1024 * |
| 370 | (asDelta ? 2 : pCI->pParent->nFile/11+1 |
| 371 | /* In the fossil core repo, each 12-ish F-cards (on |
| 372 | ** average) take up roughly 1kb */)); |
| 373 | if(blob_size(&pCI->comment)!=0){ |
| 374 | blob_appendf(pOut, "C %F\n", blob_str(&pCI->comment)); |
| 375 | }else{ |
| 376 | blob_append(pOut, "C (no\\scomment)\n", 16); |
| 377 | } |
| @@ -965,11 +961,12 @@ | |
| 965 | ** populates: |
| 966 | ** |
| 967 | ** - *zRevUuid = the fully-expanded value of zRev (owned by the |
| 968 | ** caller). zRevUuid may be NULL. |
| 969 | ** |
| 970 | ** - *vid = the RID of zRevUuid. May not be NULL. |
| 971 | ** |
| 972 | ** - *frid = the RID of zFilename's blob content. May not be NULL |
| 973 | ** unless zFilename is also NULL. If BOTH of zFilename and frid are |
| 974 | ** NULL then no confirmation is done on the filename argument - only |
| 975 | ** zRev is checked. |
| @@ -979,38 +976,41 @@ | |
| 979 | ** returns false, it queues up an error response and the caller must |
| 980 | ** return immediately. |
| 981 | */ |
| 982 | static int fileedit_ajax_setup_filerev(const char * zRev, |
| 983 | char ** zRevUuid, |
| 984 | int * vid, |
| 985 | const char * zFilename, |
| 986 | int * frid){ |
| 987 | char * zFileUuid = 0; /* file content UUID */ |
| 988 | const int checkFile = zFilename!=0 || frid!=0; |
| 989 | |
| 990 | if(checkFile && !fileedit_ajax_check_filename(zFilename)){ |
| 991 | return 0; |
| 992 | } |
| 993 | *vid = symbolic_name_to_rid(zRev, "ci"); |
| 994 | if(0==*vid){ |
| 995 | ajax_route_error(404,"Cannot resolve name as a checkin: %s", |
| 996 | zRev); |
| 997 | return 0; |
| 998 | }else if(*vid<0){ |
| 999 | ajax_route_error(400,"Checkin name is ambiguous: %s", |
| 1000 | zRev); |
| 1001 | return 0; |
| 1002 | } |
| 1003 | if(checkFile){ |
| 1004 | zFileUuid = fileedit_file_uuid(zFilename, *vid, 0); |
| 1005 | if(zFileUuid==0){ |
| 1006 | ajax_route_error(404, "Checkin does not contain file."); |
| 1007 | return 0; |
| 1008 | } |
| 1009 | } |
| 1010 | if(zRevUuid!=0){ |
| 1011 | *zRevUuid = rid_to_uuid(*vid); |
| 1012 | } |
| 1013 | if(checkFile){ |
| 1014 | assert(zFileUuid!=0); |
| 1015 | if(frid!=0){ |
| 1016 | *frid = fast_uuid_to_rid(zFileUuid); |
| @@ -1290,10 +1290,92 @@ | |
| 1290 | end_fail: |
| 1291 | #undef fail |
| 1292 | fossil_free(zFileUuid); |
| 1293 | return rc ? rc : 500; |
| 1294 | } |
| 1295 | |
| 1296 | /* |
| 1297 | ** AJAX route /fileedit?ajax=filelist |
| 1298 | ** |
| 1299 | ** Fetches a JSON-format list of leaves and/or filenames for use in |
| @@ -1318,66 +1400,27 @@ | |
| 1318 | ** } |
| 1319 | ** |
| 1320 | ** On error it produces a JSON response as documented for |
| 1321 | ** ajax_route_error(). |
| 1322 | */ |
| 1323 | static void fileedit_ajax_filelist(void){ |
| 1324 | const char * zCi = PD("checkin",P("ci")); |
| 1325 | Blob sql = empty_blob; |
| 1326 | Stmt q = empty_Stmt; |
| 1327 | int i = 0; |
| 1328 | |
| 1329 | if(!ajax_route_bootstrap(1,0)){ |
| 1330 | return; |
| 1331 | } |
| 1332 | cgi_set_content_type("application/json"); |
| 1333 | if(zCi!=0){ |
| 1334 | char * zCiFull = 0; |
| 1335 | int vid = 0; |
| 1336 | if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, &vid, 0, 0)){ |
| 1337 | /* Error already reported */ |
| 1338 | return; |
| 1339 | } |
| 1340 | CX("{\"checkin\":%!j," |
| 1341 | "\"editableFiles\":[", zCiFull); |
| 1342 | blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " |
| 1343 | "ORDER BY filename %s", |
| 1344 | zCiFull, filename_collation()); |
| 1345 | db_prepare_blob(&q, &sql); |
| 1346 | while( SQLITE_ROW==db_step(&q) ){ |
| 1347 | const char * zFilename = db_column_text(&q, 0); |
| 1348 | if(fileedit_is_editable(zFilename)){ |
| 1349 | if(i++){ |
| 1350 | CX(","); |
| 1351 | } |
| 1352 | CX("%!j", zFilename); |
| 1353 | } |
| 1354 | } |
| 1355 | db_finalize(&q); |
| 1356 | CX("]}"); |
| 1357 | }else if(P("leaves")!=0){ |
| 1358 | blob_append(&sql, timeline_query_for_tty(), -1); |
| 1359 | blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " |
| 1360 | "WHERE NOT EXISTS(" |
| 1361 | "SELECT 1 from tagxref WHERE tagid=%d AND " |
| 1362 | "tagtype>0 AND rid=leaf.rid" |
| 1363 | ")) " |
| 1364 | "ORDER BY mtime DESC", TAG_CLOSED); |
| 1365 | db_prepare_blob(&q, &sql); |
| 1366 | CX("["); |
| 1367 | while( SQLITE_ROW==db_step(&q) ){ |
| 1368 | if(i++){ |
| 1369 | CX(","); |
| 1370 | } |
| 1371 | CX("{"); |
| 1372 | CX("\"checkin\":%!j,", db_column_text(&q, 1)); |
| 1373 | CX("\"branch\":%!j,", db_column_text(&q, 7)); |
| 1374 | CX("\"timestamp\":%!j", db_column_text(&q, 2)); |
| 1375 | CX("}"); |
| 1376 | } |
| 1377 | CX("]"); |
| 1378 | db_finalize(&q); |
| 1379 | }else{ |
| 1380 | ajax_route_error(500, "Unhandled URL argument."); |
| 1381 | } |
| 1382 | } |
| 1383 | |
| @@ -1484,52 +1527,60 @@ | |
| 1484 | } |
| 1485 | |
| 1486 | /* |
| 1487 | ** WEBPAGE: fileedit |
| 1488 | ** |
| 1489 | ** Enables the online editing and committing of individual text files. |
| 1490 | ** Requires that the user have Write permissions. |
| 1491 | ** |
| 1492 | ** Optional query parameters: |
| 1493 | ** |
| 1494 | ** filename=FILENAME Repo-relative path to the file. |
| 1495 | ** checkin=VERSION Checkin version, using any unambiguous |
| 1496 | ** supported symbolic version name. |
| 1497 | ** |
| 1498 | ** Internal-use parameters: |
| 1499 | ** |
| 1500 | ** name=string The name of a page-specific AJAX operation. |
| 1501 | ** |
| 1502 | ** Noting that fossil internally stores all URL path components after |
| 1503 | ** the first as the "name" value. Thus /fileedit?name=blah is |
| 1504 | ** equivalent to /fileedit/blah. The latter is the preferred |
| 1505 | ** form. This means, however, that no fileedit ajax routes may make |
| 1506 | ** use of the name parameter. |
| 1507 | ** |
| 1508 | ** Which additional parameters are used by each distinct ajax value is |
| 1509 | ** an internal implementation detail and may change with any given |
| 1510 | ** build of this code. An unknown "name" value triggers an error, as |
| 1511 | ** documented for ajax_route_error(). |
| 1512 | */ |
| 1513 | void fileedit_page(void){ |
| 1514 | const char * zFilename = 0; /* filename. We'll accept 'name' |
| 1515 | because that param is handled |
| 1516 | specially by the core. */ |
| 1517 | const char * zRev = 0; /* checkin version */ |
| 1518 | const char * zFileMime = 0; /* File mime type guess */ |
| 1519 | CheckinMiniInfo cimi; /* Checkin state */ |
| 1520 | int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */ |
| 1521 | Blob err = empty_blob; /* Error report */ |
| 1522 | Blob endScript = empty_blob; /* Script code to run at the |
| 1523 | end. This content will be |
| 1524 | combined into a single JS |
| 1525 | function call, thus each |
| 1526 | entry must end with a |
| 1527 | semicolon. */ |
| 1528 | const char *zAjax = P("name"); /* Name of AJAX route for |
| 1529 | sub-dispatching. */ |
| 1530 | |
| 1531 | /* Allow no access to this page without check-in privilege */ |
| 1532 | login_check_credentials(); |
| 1533 | if( !g.perm.Write ){ |
| 1534 | if(zAjax!=0){ |
| 1535 | ajax_route_error(403, "Write permissions required."); |
| @@ -1590,14 +1641,11 @@ | |
| 1590 | ** transaction cleanly. |
| 1591 | */ |
| 1592 | { |
| 1593 | int isMissingArg = 0; |
| 1594 | if(fileedit_setup_cimi_from_p(&cimi, &err, &isMissingArg)==0){ |
| 1595 | zFilename = cimi.zFilename; |
| 1596 | zRev = cimi.zParentUuid; |
| 1597 | assert(zRev); |
| 1598 | assert(zFilename); |
| 1599 | zFileMime = mimetype_from_name(cimi.zFilename); |
| 1600 | }else if(isMissingArg!=0){ |
| 1601 | /* Squelch these startup warnings - they're non-fatal now but |
| 1602 | ** used to be fatal. */ |
| 1603 | blob_reset(&err); |
| @@ -1626,34 +1674,39 @@ | |
| 1626 | /* Status bar */ |
| 1627 | CX("<div id='fossil-status-bar' " |
| 1628 | "title='Status message area. Double-click to clear them.'>" |
| 1629 | "Status messages will go here.</div>\n" |
| 1630 | /* will be moved into the tab container via JS */); |
| 1631 | |
| 1632 | /* Main tab container... */ |
| 1633 | CX("<div id='fileedit-tabs' class='tab-container'></div>"); |
| 1634 | |
| 1635 | /***** File/version info tab *****/ |
| 1636 | { |
| 1637 | CX("<div id='fileedit-tab-fileselect' " |
| 1638 | "data-tab-parent='fileedit-tabs' " |
| 1639 | "data-tab-label='File Info & Selection'" |
| 1640 | ">"); |
| 1641 | CX("<fieldset id='file-version-details'>" |
| 1642 | "<legend>File/Version</legend>" |
| 1643 | "<div>No file loaded.</div>" |
| 1644 | "</fieldset>"); |
| 1645 | CX("<h1>Select a file to edit:</h1>"); |
| 1646 | CX("<div id='fileedit-file-selector'></div>"); |
| 1647 | CX("</div>"/*#fileedit-tab-fileselect*/); |
| 1648 | } |
| 1649 | |
| 1650 | /******* Content tab *******/ |
| 1651 | { |
| 1652 | CX("<div id='fileedit-tab-content' " |
| 1653 | "data-tab-parent='fileedit-tabs' " |
| 1654 | "data-tab-label='File Content'" |
| 1655 | ">"); |
| 1656 | CX("<div class='flex-container flex-row child-gap-small'>"); |
| 1657 | CX("<button class='fileedit-content-reload confirmer' " |
| 1658 | "title='Reload the file from the server, discarding " |
| 1659 | "any local edits. To help avoid accidental loss of " |
| @@ -1668,22 +1721,22 @@ | |
| 1668 | "150%", 150, "175%", 175, |
| 1669 | "200%", 200, NULL); |
| 1670 | CX("</div>"); |
| 1671 | CX("<div class='flex-container flex-column stretch'>"); |
| 1672 | CX("<textarea name='content' id='fileedit-content-editor' " |
| 1673 | "class='fileedit' " |
| 1674 | "rows='20' cols='80'>"); |
| 1675 | CX("</textarea>"); |
| 1676 | CX("</div>"/*textarea wrapper*/); |
| 1677 | CX("</div>"/*#tab-file-content*/); |
| 1678 | } |
| 1679 | |
| 1680 | /****** Preview tab ******/ |
| 1681 | { |
| 1682 | CX("<div id='fileedit-tab-preview' " |
| 1683 | "data-tab-parent='fileedit-tabs' " |
| 1684 | "data-tab-label='Preview'" |
| 1685 | ">"); |
| 1686 | CX("<div class='fileedit-options flex-container flex-row'>"); |
| 1687 | CX("<button id='btn-preview-refresh' " |
| 1688 | "data-f-preview-from='fileContent' " |
| 1689 | /* ^^^ fossil.page[methodName]() OR text source elem ID, |
| @@ -1741,14 +1794,16 @@ | |
| 1741 | |
| 1742 | /****** Diff tab ******/ |
| 1743 | { |
| 1744 | CX("<div id='fileedit-tab-diff' " |
| 1745 | "data-tab-parent='fileedit-tabs' " |
| 1746 | "data-tab-label='Diff'" |
| 1747 | ">"); |
| 1748 | |
| 1749 | CX("<div class='fileedit-options flex-container flex-row' " |
| 1750 | "id='fileedit-tab-diff-buttons'>"); |
| 1751 | CX("<button class='sbs'>Side-by-side</button>" |
| 1752 | "<button class='unified'>Unified</button>"); |
| 1753 | if(0){ |
| 1754 | /* For the time being let's just ignore all whitespace |
| @@ -1773,11 +1828,12 @@ | |
| 1773 | } |
| 1774 | |
| 1775 | /****** Commit ******/ |
| 1776 | CX("<div id='fileedit-tab-commit' " |
| 1777 | "data-tab-parent='fileedit-tabs' " |
| 1778 | "data-tab-label='Commit'" |
| 1779 | ">"); |
| 1780 | { |
| 1781 | /******* Commit flags/options *******/ |
| 1782 | CX("<div class='fileedit-options flex-container flex-row'>"); |
| 1783 | style_labeled_checkbox("cb-dry-run", |
| @@ -1894,11 +1950,12 @@ | |
| 1894 | CX("</div>"/*#fileedit-tab-commit*/); |
| 1895 | |
| 1896 | /****** Help/Tips ******/ |
| 1897 | CX("<div id='fileedit-tab-help' " |
| 1898 | "data-tab-parent='fileedit-tabs' " |
| 1899 | "data-tab-label='Help'" |
| 1900 | ">"); |
| 1901 | { |
| 1902 | CX("<h1>Help & Tips</h1>"); |
| 1903 | CX("<ul>"); |
| 1904 | CX("<li><strong>Only files matching the <code>fileedit-glob</code> " |
| @@ -1922,61 +1979,81 @@ | |
| 1922 | "file/check-in combination are discarded.</li>"); |
| 1923 | CX("</ul>"); |
| 1924 | } |
| 1925 | CX("</div>"/*#fileedit-tab-help*/); |
| 1926 | |
| 1927 | { |
| 1928 | /* Dynamically populate the editor, display any error in the err |
| 1929 | ** blob, and/or switch to tab #0, where the file selector |
| 1930 | ** lives... */ |
| 1931 | blob_appendf(&endScript, |
| 1932 | "fossil.onPageLoad("); |
| 1933 | if(zRev && zFilename){ |
| 1934 | assert(0==blob_size(&err)); |
| 1935 | blob_appendf(&endScript, |
| 1936 | "()=>fossil.page.loadFile(%!j,%!j)", |
| 1937 | zFilename, cimi.zParentUuid); |
| 1938 | }else{ |
| 1939 | blob_appendf(&endScript,"function(){\n"); |
| 1940 | if(blob_size(&err)>0){ |
| 1941 | blob_appendf(&endScript, |
| 1942 | "fossil.error(%!j);\n", |
| 1943 | blob_str(&err)); |
| 1944 | } |
| 1945 | blob_appendf(&endScript, |
| 1946 | "fossil.page.tabs.switchToTab(0);\n"); |
| 1947 | blob_appendf(&endScript,"}"); |
| 1948 | } |
| 1949 | blob_appendf(&endScript,");\n"); |
| 1950 | } |
| 1951 | |
| 1952 | blob_reset(&err); |
| 1953 | CheckinMiniInfo_cleanup(&cimi); |
| 1954 | style_emit_script_fossil_bootstrap(0); |
| 1955 | append_diff_javascript(1); |
| 1956 | style_emit_script_fetch(0); |
| 1957 | style_emit_script_tabs(0)/*also emits fossil.dom*/; |
| 1958 | style_emit_script_confirmer(0); |
| 1959 | style_emit_script_builtin(0, "fossil.storage.js"); |
| 1960 | |
| 1961 | /* |
| 1962 | ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is |
| 1963 | ** used for dynamically toggling certain UI components on and off. |
| 1964 | ** Must come before fossil.page.fileedit.js. |
| 1965 | */ |
| 1966 | ajax_emit_js_preview_modes(1); |
| 1967 | |
| 1968 | style_emit_script_builtin(0, "fossil.page.fileedit.js"); |
| 1969 | if(blob_size(&endScript)>0){ |
| 1970 | style_emit_script_tag(0,0); |
| 1971 | CX("\n(function(){\n"); |
| 1972 | CX("try{\n%b}\n" |
| 1973 | "catch(e){" |
| 1974 | "fossil.error(e); console.error('Exception:',e);" |
| 1975 | "}\n", |
| 1976 | &endScript); |
| 1977 | CX("})();"); |
| 1978 | style_emit_script_tag(1,0); |
| 1979 | } |
| 1980 | db_end_transaction(0); |
| 1981 | style_footer(); |
| 1982 | } |
| 1983 |
| --- src/fileedit.c | |
| +++ src/fileedit.c | |
| @@ -364,14 +364,10 @@ | |
| 364 | blob_appendf(pOut, "B %s\n", |
| 365 | pCI->pParent->zBaseline |
| 366 | ? pCI->pParent->zBaseline |
| 367 | : pCI->zParentUuid); |
| 368 | } |
| 369 | if(blob_size(&pCI->comment)!=0){ |
| 370 | blob_appendf(pOut, "C %F\n", blob_str(&pCI->comment)); |
| 371 | }else{ |
| 372 | blob_append(pOut, "C (no\\scomment)\n", 16); |
| 373 | } |
| @@ -965,11 +961,12 @@ | |
| 961 | ** populates: |
| 962 | ** |
| 963 | ** - *zRevUuid = the fully-expanded value of zRev (owned by the |
| 964 | ** caller). zRevUuid may be NULL. |
| 965 | ** |
| 966 | ** - *pVid = the RID of zRevUuid. pVid May be NULL. If the vid |
| 967 | ** cannot be resolved or is ambiguous, pVid is not assigned. |
| 968 | ** |
| 969 | ** - *frid = the RID of zFilename's blob content. May not be NULL |
| 970 | ** unless zFilename is also NULL. If BOTH of zFilename and frid are |
| 971 | ** NULL then no confirmation is done on the filename argument - only |
| 972 | ** zRev is checked. |
| @@ -979,38 +976,41 @@ | |
| 976 | ** returns false, it queues up an error response and the caller must |
| 977 | ** return immediately. |
| 978 | */ |
| 979 | static int fileedit_ajax_setup_filerev(const char * zRev, |
| 980 | char ** zRevUuid, |
| 981 | int * pVid, |
| 982 | const char * zFilename, |
| 983 | int * frid){ |
| 984 | char * zFileUuid = 0; /* file content UUID */ |
| 985 | const int checkFile = zFilename!=0 || frid!=0; |
| 986 | int vid = 0; |
| 987 | |
| 988 | if(checkFile && !fileedit_ajax_check_filename(zFilename)){ |
| 989 | return 0; |
| 990 | } |
| 991 | vid = symbolic_name_to_rid(zRev, "ci"); |
| 992 | if(0==vid){ |
| 993 | ajax_route_error(404,"Cannot resolve name as a checkin: %s", |
| 994 | zRev); |
| 995 | return 0; |
| 996 | }else if(vid<0){ |
| 997 | ajax_route_error(400,"Checkin name is ambiguous: %s", |
| 998 | zRev); |
| 999 | return 0; |
| 1000 | }else if(pVid!=0){ |
| 1001 | *pVid = vid; |
| 1002 | } |
| 1003 | if(checkFile){ |
| 1004 | zFileUuid = fileedit_file_uuid(zFilename, vid, 0); |
| 1005 | if(zFileUuid==0){ |
| 1006 | ajax_route_error(404, "Checkin does not contain file."); |
| 1007 | return 0; |
| 1008 | } |
| 1009 | } |
| 1010 | if(zRevUuid!=0){ |
| 1011 | *zRevUuid = rid_to_uuid(vid); |
| 1012 | } |
| 1013 | if(checkFile){ |
| 1014 | assert(zFileUuid!=0); |
| 1015 | if(frid!=0){ |
| 1016 | *frid = fast_uuid_to_rid(zFileUuid); |
| @@ -1290,10 +1290,92 @@ | |
| 1290 | end_fail: |
| 1291 | #undef fail |
| 1292 | fossil_free(zFileUuid); |
| 1293 | return rc ? rc : 500; |
| 1294 | } |
| 1295 | |
| 1296 | /* |
| 1297 | ** Renders a list of all open leaves in JSON form: |
| 1298 | ** |
| 1299 | ** [ |
| 1300 | ** {checkin: UUID, branch: branchName, timestamp: string} |
| 1301 | ** ] |
| 1302 | ** |
| 1303 | ** The entries are ordered newest first. |
| 1304 | ** |
| 1305 | ** If zFirstUuid is not NULL then *zFirstUuid is set to a copy of the |
| 1306 | ** full UUID of the first (most recent) leaf, which must be freed by |
| 1307 | ** the caller. It is set to 0 if there are no leaves. |
| 1308 | */ |
| 1309 | static void fileedit_render_leaves_list(char ** zFirstUuid){ |
| 1310 | Blob sql = empty_blob; |
| 1311 | Stmt q = empty_Stmt; |
| 1312 | int i = 0; |
| 1313 | |
| 1314 | if(zFirstUuid){ |
| 1315 | *zFirstUuid = 0; |
| 1316 | } |
| 1317 | blob_append(&sql, timeline_query_for_tty(), -1); |
| 1318 | blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " |
| 1319 | "WHERE NOT EXISTS(" |
| 1320 | "SELECT 1 from tagxref WHERE tagid=%d AND " |
| 1321 | "tagtype>0 AND rid=leaf.rid" |
| 1322 | ")) " |
| 1323 | "ORDER BY mtime DESC", TAG_CLOSED); |
| 1324 | db_prepare_blob(&q, &sql); |
| 1325 | CX("["); |
| 1326 | while( SQLITE_ROW==db_step(&q) ){ |
| 1327 | const char * zUuid = db_column_text(&q, 1); |
| 1328 | if(i++){ |
| 1329 | CX(","); |
| 1330 | }else if(zFirstUuid){ |
| 1331 | *zFirstUuid = fossil_strdup(zUuid); |
| 1332 | } |
| 1333 | CX("{"); |
| 1334 | CX("\"checkin\":%!j,", zUuid); |
| 1335 | CX("\"branch\":%!j,", db_column_text(&q, 7)); |
| 1336 | CX("\"timestamp\":%!j", db_column_text(&q, 2)); |
| 1337 | CX("}"); |
| 1338 | } |
| 1339 | CX("]"); |
| 1340 | db_finalize(&q); |
| 1341 | } |
| 1342 | |
| 1343 | /* |
| 1344 | ** For the given fully resolved UUID, renders a JSON object containing |
| 1345 | ** the fileeedit-editable files in that checkin: |
| 1346 | ** |
| 1347 | ** { |
| 1348 | ** checkin: UUID, |
| 1349 | ** editableFiles: [ filename1, ... filenameN ] |
| 1350 | ** } |
| 1351 | ** |
| 1352 | ** They are sorted by name using filename_collation(). |
| 1353 | */ |
| 1354 | static void fileedit_render_checkin_files(const char * zFullUuid){ |
| 1355 | Blob sql = empty_blob; |
| 1356 | Stmt q = empty_Stmt; |
| 1357 | int i = 0; |
| 1358 | |
| 1359 | CX("{\"checkin\":%!j," |
| 1360 | "\"editableFiles\":[", zFullUuid); |
| 1361 | blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " |
| 1362 | "ORDER BY filename %s", |
| 1363 | zFullUuid, filename_collation()); |
| 1364 | db_prepare_blob(&q, &sql); |
| 1365 | while( SQLITE_ROW==db_step(&q) ){ |
| 1366 | const char * zFilename = db_column_text(&q, 0); |
| 1367 | if(fileedit_is_editable(zFilename)){ |
| 1368 | if(i++){ |
| 1369 | CX(","); |
| 1370 | } |
| 1371 | CX("%!j", zFilename); |
| 1372 | } |
| 1373 | } |
| 1374 | db_finalize(&q); |
| 1375 | CX("]}"); |
| 1376 | } |
| 1377 | |
| 1378 | /* |
| 1379 | ** AJAX route /fileedit?ajax=filelist |
| 1380 | ** |
| 1381 | ** Fetches a JSON-format list of leaves and/or filenames for use in |
| @@ -1318,66 +1400,27 @@ | |
| 1400 | ** } |
| 1401 | ** |
| 1402 | ** On error it produces a JSON response as documented for |
| 1403 | ** ajax_route_error(). |
| 1404 | */ |
| 1405 | static void fileedit_ajax_filelist(){ |
| 1406 | const char * zCi = PD("checkin",P("ci")); |
| 1407 | |
| 1408 | if(!ajax_route_bootstrap(1,0)){ |
| 1409 | return; |
| 1410 | } |
| 1411 | cgi_set_content_type("application/json"); |
| 1412 | if(zCi!=0){ |
| 1413 | char * zCiFull = 0; |
| 1414 | if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, 0, 0, 0)){ |
| 1415 | /* Error already reported */ |
| 1416 | return; |
| 1417 | } |
| 1418 | fileedit_render_checkin_files(zCiFull); |
| 1419 | fossil_free(zCiFull); |
| 1420 | }else if(P("leaves")!=0){ |
| 1421 | fileedit_render_leaves_list(0); |
| 1422 | }else{ |
| 1423 | ajax_route_error(500, "Unhandled URL argument."); |
| 1424 | } |
| 1425 | } |
| 1426 | |
| @@ -1484,52 +1527,60 @@ | |
| 1527 | } |
| 1528 | |
| 1529 | /* |
| 1530 | ** WEBPAGE: fileedit |
| 1531 | ** |
| 1532 | ** Enables the online editing and committing of text files. Requires |
| 1533 | ** that the user have Write permissions and that a user with setup |
| 1534 | ** permissions has set the fileedit-glob setting to a list of glob |
| 1535 | ** patterns matching files which may be edited (e.g. "*.wiki,*.md"). |
| 1536 | ** Note that fileedit-glob, by design, is a local-only setting. |
| 1537 | ** It does not sync across repository clones, and must be explicitly |
| 1538 | ** set on any repositories where this page should be activated. |
| 1539 | ** |
| 1540 | ** Optional query parameters: |
| 1541 | ** |
| 1542 | ** filename=FILENAME Repo-relative path to the file. |
| 1543 | ** checkin=VERSION Checkin version, using any unambiguous |
| 1544 | ** symbolic version name. |
| 1545 | ** |
| 1546 | ** If passed a filename but no checkin then it will attempt to |
| 1547 | ** load that file from the most recent leaf checkin. |
| 1548 | ** |
| 1549 | ** Once the page is loaded, files may be selected from any open leaf |
| 1550 | ** version. The only way to edit files from non-leaf checkins is to |
| 1551 | ** pass both the filename and checkin as URL parameters to the page. |
| 1552 | ** Users with the proper permissions will be presented with "Edit" |
| 1553 | ** links in various file-specific contexts for files which match the |
| 1554 | ** fileedit-glob, regardless of whether they refer to leaf versions or |
| 1555 | ** not. |
| 1556 | */ |
| 1557 | void fileedit_page(void){ |
| 1558 | const char * zFileMime = 0; /* File mime type guess */ |
| 1559 | CheckinMiniInfo cimi; /* Checkin state */ |
| 1560 | int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */ |
| 1561 | Blob err = empty_blob; /* Error report */ |
| 1562 | const char *zAjax = P("name"); /* Name of AJAX route for |
| 1563 | sub-dispatching. */ |
| 1564 | |
| 1565 | /* |
| 1566 | ** Internal-use URL parameters: |
| 1567 | ** |
| 1568 | ** name=string The name of a page-specific AJAX operation. |
| 1569 | ** |
| 1570 | ** Noting that fossil internally stores all URL path components |
| 1571 | ** after the first as the "name" value. Thus /fileedit?name=blah is |
| 1572 | ** equivalent to /fileedit/blah. The latter is the preferred |
| 1573 | ** form. This means, however, that no fileedit ajax routes may make |
| 1574 | ** use of the name parameter. |
| 1575 | ** |
| 1576 | ** Which additional parameters are used by each distinct ajax route |
| 1577 | ** is an internal implementation detail and may change with any |
| 1578 | ** given build of this code. An unknown "name" value triggers an |
| 1579 | ** error, as documented for ajax_route_error(). |
| 1580 | */ |
| 1581 | |
| 1582 | /* Allow no access to this page without check-in privilege */ |
| 1583 | login_check_credentials(); |
| 1584 | if( !g.perm.Write ){ |
| 1585 | if(zAjax!=0){ |
| 1586 | ajax_route_error(403, "Write permissions required."); |
| @@ -1590,14 +1641,11 @@ | |
| 1641 | ** transaction cleanly. |
| 1642 | */ |
| 1643 | { |
| 1644 | int isMissingArg = 0; |
| 1645 | if(fileedit_setup_cimi_from_p(&cimi, &err, &isMissingArg)==0){ |
| 1646 | assert(cimi.zFilename); |
| 1647 | zFileMime = mimetype_from_name(cimi.zFilename); |
| 1648 | }else if(isMissingArg!=0){ |
| 1649 | /* Squelch these startup warnings - they're non-fatal now but |
| 1650 | ** used to be fatal. */ |
| 1651 | blob_reset(&err); |
| @@ -1626,34 +1674,39 @@ | |
| 1674 | /* Status bar */ |
| 1675 | CX("<div id='fossil-status-bar' " |
| 1676 | "title='Status message area. Double-click to clear them.'>" |
| 1677 | "Status messages will go here.</div>\n" |
| 1678 | /* will be moved into the tab container via JS */); |
| 1679 | |
| 1680 | CX("<div id='fileedit-edit-status'>" |
| 1681 | "<span class='name'>(no file loaded)</span>" |
| 1682 | "<span class='links'></span>" |
| 1683 | "</div>"); |
| 1684 | |
| 1685 | /* Main tab container... */ |
| 1686 | CX("<div id='fileedit-tabs' class='tab-container'></div>"); |
| 1687 | |
| 1688 | /* The .hidden class on the following tab elements is to help lessen |
| 1689 | the FOUC effect of the tabs before JS re-assembles them. */ |
| 1690 | |
| 1691 | /***** File/version info tab *****/ |
| 1692 | { |
| 1693 | CX("<div id='fileedit-tab-fileselect' " |
| 1694 | "data-tab-parent='fileedit-tabs' " |
| 1695 | "data-tab-label='File Selection' " |
| 1696 | "class='hidden'" |
| 1697 | ">"); |
| 1698 | CX("<div id='fileedit-file-selector'></div>"); |
| 1699 | CX("</div>"/*#fileedit-tab-fileselect*/); |
| 1700 | } |
| 1701 | |
| 1702 | /******* Content tab *******/ |
| 1703 | { |
| 1704 | CX("<div id='fileedit-tab-content' " |
| 1705 | "data-tab-parent='fileedit-tabs' " |
| 1706 | "data-tab-label='File Content' " |
| 1707 | "class='hidden'" |
| 1708 | ">"); |
| 1709 | CX("<div class='flex-container flex-row child-gap-small'>"); |
| 1710 | CX("<button class='fileedit-content-reload confirmer' " |
| 1711 | "title='Reload the file from the server, discarding " |
| 1712 | "any local edits. To help avoid accidental loss of " |
| @@ -1668,22 +1721,22 @@ | |
| 1721 | "150%", 150, "175%", 175, |
| 1722 | "200%", 200, NULL); |
| 1723 | CX("</div>"); |
| 1724 | CX("<div class='flex-container flex-column stretch'>"); |
| 1725 | CX("<textarea name='content' id='fileedit-content-editor' " |
| 1726 | "class='fileedit' rows='25'>"); |
| 1727 | CX("</textarea>"); |
| 1728 | CX("</div>"/*textarea wrapper*/); |
| 1729 | CX("</div>"/*#tab-file-content*/); |
| 1730 | } |
| 1731 | |
| 1732 | /****** Preview tab ******/ |
| 1733 | { |
| 1734 | CX("<div id='fileedit-tab-preview' " |
| 1735 | "data-tab-parent='fileedit-tabs' " |
| 1736 | "data-tab-label='Preview' " |
| 1737 | "class='hidden'" |
| 1738 | ">"); |
| 1739 | CX("<div class='fileedit-options flex-container flex-row'>"); |
| 1740 | CX("<button id='btn-preview-refresh' " |
| 1741 | "data-f-preview-from='fileContent' " |
| 1742 | /* ^^^ fossil.page[methodName]() OR text source elem ID, |
| @@ -1741,14 +1794,16 @@ | |
| 1794 | |
| 1795 | /****** Diff tab ******/ |
| 1796 | { |
| 1797 | CX("<div id='fileedit-tab-diff' " |
| 1798 | "data-tab-parent='fileedit-tabs' " |
| 1799 | "data-tab-label='Diff' " |
| 1800 | "class='hidden'" |
| 1801 | ">"); |
| 1802 | |
| 1803 | CX("<div class='fileedit-options flex-container " |
| 1804 | "flex-row child-gap-small' " |
| 1805 | "id='fileedit-tab-diff-buttons'>"); |
| 1806 | CX("<button class='sbs'>Side-by-side</button>" |
| 1807 | "<button class='unified'>Unified</button>"); |
| 1808 | if(0){ |
| 1809 | /* For the time being let's just ignore all whitespace |
| @@ -1773,11 +1828,12 @@ | |
| 1828 | } |
| 1829 | |
| 1830 | /****** Commit ******/ |
| 1831 | CX("<div id='fileedit-tab-commit' " |
| 1832 | "data-tab-parent='fileedit-tabs' " |
| 1833 | "data-tab-label='Commit' " |
| 1834 | "class='hidden'" |
| 1835 | ">"); |
| 1836 | { |
| 1837 | /******* Commit flags/options *******/ |
| 1838 | CX("<div class='fileedit-options flex-container flex-row'>"); |
| 1839 | style_labeled_checkbox("cb-dry-run", |
| @@ -1894,11 +1950,12 @@ | |
| 1950 | CX("</div>"/*#fileedit-tab-commit*/); |
| 1951 | |
| 1952 | /****** Help/Tips ******/ |
| 1953 | CX("<div id='fileedit-tab-help' " |
| 1954 | "data-tab-parent='fileedit-tabs' " |
| 1955 | "data-tab-label='Help' " |
| 1956 | "class='hidden'" |
| 1957 | ">"); |
| 1958 | { |
| 1959 | CX("<h1>Help & Tips</h1>"); |
| 1960 | CX("<ul>"); |
| 1961 | CX("<li><strong>Only files matching the <code>fileedit-glob</code> " |
| @@ -1922,61 +1979,81 @@ | |
| 1979 | "file/check-in combination are discarded.</li>"); |
| 1980 | CX("</ul>"); |
| 1981 | } |
| 1982 | CX("</div>"/*#fileedit-tab-help*/); |
| 1983 | |
| 1984 | builtin_request_js("sbsdiff.js"); |
| 1985 | style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer", |
| 1986 | "storage", 0); |
| 1987 | builtin_fulfill_js_requests(); |
| 1988 | /* |
| 1989 | ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is |
| 1990 | ** used for dynamically toggling certain UI components on and off. |
| 1991 | ** Must come after window.fossil has been intialized and before |
| 1992 | ** fossil.page.fileedit.js. Potential TODO: move this into the |
| 1993 | ** window.fossil bootstrapping so that we don't have to "fulfill" |
| 1994 | ** the JS multiple times. |
| 1995 | */ |
| 1996 | ajax_emit_js_preview_modes(1); |
| 1997 | builtin_request_js("fossil.page.fileedit.js"); |
| 1998 | builtin_fulfill_js_requests(); |
| 1999 | { |
| 2000 | /* Dynamically populate the editor, display any error in the err |
| 2001 | ** blob, and/or switch to tab #0, where the file selector |
| 2002 | ** lives. The extra C scopes here correspond to JS-level scopes, |
| 2003 | ** to improve grokability. */ |
| 2004 | style_emit_script_tag(0,0); |
| 2005 | CX("\n(function(){\n"); |
| 2006 | CX("try{\n"); |
| 2007 | { |
| 2008 | char * zFirstLeafUuid = 0; |
| 2009 | CX("fossil.config['fileedit-glob'] = "); |
| 2010 | glob_render_json_to_cgi(fileedit_glob()); |
| 2011 | CX(";\n"); |
| 2012 | if(blob_size(&err)>0){ |
| 2013 | CX("fossil.error(%!j);\n", blob_str(&err)); |
| 2014 | } |
| 2015 | /* Populate the page with the current leaves and, if available, |
| 2016 | the selected checkin's file list, to save 1 or 2 XHR requests |
| 2017 | at startup. That makes this page uncacheable, but compressed |
| 2018 | delivery of this page is currently less than 6k. */ |
| 2019 | CX("fossil.page.initialLeaves = "); |
| 2020 | fileedit_render_leaves_list(cimi.zParentUuid ? 0 : &zFirstLeafUuid); |
| 2021 | CX(";\n"); |
| 2022 | if(zFirstLeafUuid){ |
| 2023 | assert(!cimi.zParentUuid); |
| 2024 | cimi.zParentUuid = zFirstLeafUuid; |
| 2025 | zFirstLeafUuid = 0; |
| 2026 | } |
| 2027 | if(cimi.zParentUuid){ |
| 2028 | CX("fossil.page.initialFiles = "); |
| 2029 | fileedit_render_checkin_files(cimi.zParentUuid); |
| 2030 | CX(";\n"); |
| 2031 | } |
| 2032 | CX("fossil.onPageLoad(function(){\n"); |
| 2033 | { |
| 2034 | if(blob_size(&err)>0){ |
| 2035 | CX("fossil.error(%!j);\n", |
| 2036 | blob_str(&err)); |
| 2037 | CX("fossil.page.tabs.switchToTab(0);\n"); |
| 2038 | } |
| 2039 | if(cimi.zParentUuid && cimi.zFilename){ |
| 2040 | CX("fossil.page.loadFile(%!j,%!j);\n", |
| 2041 | cimi.zFilename, cimi.zParentUuid) |
| 2042 | /* Reminder we cannot embed the JSON-format |
| 2043 | content of the file here because if it contains |
| 2044 | a SCRIPT tag then it will break the whole page. */; |
| 2045 | } |
| 2046 | } |
| 2047 | CX("});\n")/*fossil.onPageLoad()*/; |
| 2048 | } |
| 2049 | CX("}catch(e){" |
| 2050 | "fossil.error(e); console.error('Exception:',e);" |
| 2051 | "}\n"); |
| 2052 | CX("})();")/*anonymous function*/; |
| 2053 | style_emit_script_tag(1,0); |
| 2054 | } |
| 2055 | blob_reset(&err); |
| 2056 | CheckinMiniInfo_cleanup(&cimi); |
| 2057 | db_end_transaction(0); |
| 2058 | style_footer(); |
| 2059 | } |
| 2060 |
+2
-2
| --- src/finfo.c | ||
| +++ src/finfo.c | ||
| @@ -54,11 +54,11 @@ | ||
| 54 | 54 | ** -s|--status select status mode (print a status indicator for FILE) |
| 55 | 55 | ** -W|--width <num> Width of lines (default is to auto-detect). Must be |
| 56 | 56 | ** >22 or 0 (= no limit, resulting in a single line per |
| 57 | 57 | ** entry). |
| 58 | 58 | ** |
| 59 | -** See also: artifact, cat, descendants, info, leaves | |
| 59 | +** See also: [[artifact]], [[cat]], [[descendants]], [[info]], [[leaves]] | |
| 60 | 60 | */ |
| 61 | 61 | void finfo_cmd(void){ |
| 62 | 62 | db_must_be_within_tree(); |
| 63 | 63 | if( find_option("status","s",0) ){ |
| 64 | 64 | Stmt q; |
| @@ -245,11 +245,11 @@ | ||
| 245 | 245 | ** |
| 246 | 246 | ** Options: |
| 247 | 247 | ** -R|--repository FILE Extract artifacts from repository FILE |
| 248 | 248 | ** -r VERSION The specific check-in containing the file |
| 249 | 249 | ** |
| 250 | -** See also: finfo | |
| 250 | +** See also: [[finfo]] | |
| 251 | 251 | */ |
| 252 | 252 | void cat_cmd(void){ |
| 253 | 253 | int i; |
| 254 | 254 | Blob content, fname; |
| 255 | 255 | const char *zRev; |
| 256 | 256 |
| --- src/finfo.c | |
| +++ src/finfo.c | |
| @@ -54,11 +54,11 @@ | |
| 54 | ** -s|--status select status mode (print a status indicator for FILE) |
| 55 | ** -W|--width <num> Width of lines (default is to auto-detect). Must be |
| 56 | ** >22 or 0 (= no limit, resulting in a single line per |
| 57 | ** entry). |
| 58 | ** |
| 59 | ** See also: artifact, cat, descendants, info, leaves |
| 60 | */ |
| 61 | void finfo_cmd(void){ |
| 62 | db_must_be_within_tree(); |
| 63 | if( find_option("status","s",0) ){ |
| 64 | Stmt q; |
| @@ -245,11 +245,11 @@ | |
| 245 | ** |
| 246 | ** Options: |
| 247 | ** -R|--repository FILE Extract artifacts from repository FILE |
| 248 | ** -r VERSION The specific check-in containing the file |
| 249 | ** |
| 250 | ** See also: finfo |
| 251 | */ |
| 252 | void cat_cmd(void){ |
| 253 | int i; |
| 254 | Blob content, fname; |
| 255 | const char *zRev; |
| 256 |
| --- src/finfo.c | |
| +++ src/finfo.c | |
| @@ -54,11 +54,11 @@ | |
| 54 | ** -s|--status select status mode (print a status indicator for FILE) |
| 55 | ** -W|--width <num> Width of lines (default is to auto-detect). Must be |
| 56 | ** >22 or 0 (= no limit, resulting in a single line per |
| 57 | ** entry). |
| 58 | ** |
| 59 | ** See also: [[artifact]], [[cat]], [[descendants]], [[info]], [[leaves]] |
| 60 | */ |
| 61 | void finfo_cmd(void){ |
| 62 | db_must_be_within_tree(); |
| 63 | if( find_option("status","s",0) ){ |
| 64 | Stmt q; |
| @@ -245,11 +245,11 @@ | |
| 245 | ** |
| 246 | ** Options: |
| 247 | ** -R|--repository FILE Extract artifacts from repository FILE |
| 248 | ** -r VERSION The specific check-in containing the file |
| 249 | ** |
| 250 | ** See also: [[finfo]] |
| 251 | */ |
| 252 | void cat_cmd(void){ |
| 253 | int i; |
| 254 | Blob content, fname; |
| 255 | const char *zRev; |
| 256 |
+7
-11
| --- src/forum.c | ||
| +++ src/forum.c | ||
| @@ -749,24 +749,20 @@ | ||
| 749 | 749 | forumthread_delete(pThread); |
| 750 | 750 | return target; |
| 751 | 751 | } |
| 752 | 752 | |
| 753 | 753 | /* |
| 754 | -** The first time this is called, it emits SCRIPT tags to load various | |
| 755 | -** forum-related JavaScript. Ideally it should be called near the end | |
| 756 | -** of the page, immediately before the call to style_footer() (which | |
| 757 | -** closes the document's <BODY> and <HTML> tags). Calls after the first | |
| 758 | -** are a no-op. | |
| 754 | +** Emits all JS code required by /forumpost. | |
| 759 | 755 | */ |
| 760 | -static void forum_emit_page_js(){ | |
| 756 | +static void forumpost_emit_page_js(){ | |
| 761 | 757 | static int once = 0; |
| 762 | 758 | if(0==once){ |
| 763 | 759 | once = 1; |
| 764 | - style_load_js("forum.js"); | |
| 765 | - style_emit_script_fossil_bootstrap(0); | |
| 766 | - style_emit_script_dom(0); | |
| 767 | - style_emit_script_builtin(0, "fossil.page.forumpost.js"); | |
| 760 | + style_emit_script_fossil_bootstrap(1); | |
| 761 | + builtin_request_js("forum.js"); | |
| 762 | + builtin_request_js("fossil.dom.js"); | |
| 763 | + builtin_request_js("fossil.page.forumpost.js"); | |
| 768 | 764 | } |
| 769 | 765 | } |
| 770 | 766 | |
| 771 | 767 | /* |
| 772 | 768 | ** WEBPAGE: forumpost |
| @@ -894,11 +890,11 @@ | ||
| 894 | 890 | }else{ |
| 895 | 891 | style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName); |
| 896 | 892 | style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName); |
| 897 | 893 | forum_display_hierarchical(froot, fpid); |
| 898 | 894 | } |
| 899 | - forum_emit_page_js(); | |
| 895 | + forumpost_emit_page_js(); | |
| 900 | 896 | style_footer(); |
| 901 | 897 | } |
| 902 | 898 | |
| 903 | 899 | /* |
| 904 | 900 | ** Return true if a forum post should be moderated. |
| 905 | 901 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -749,24 +749,20 @@ | |
| 749 | forumthread_delete(pThread); |
| 750 | return target; |
| 751 | } |
| 752 | |
| 753 | /* |
| 754 | ** The first time this is called, it emits SCRIPT tags to load various |
| 755 | ** forum-related JavaScript. Ideally it should be called near the end |
| 756 | ** of the page, immediately before the call to style_footer() (which |
| 757 | ** closes the document's <BODY> and <HTML> tags). Calls after the first |
| 758 | ** are a no-op. |
| 759 | */ |
| 760 | static void forum_emit_page_js(){ |
| 761 | static int once = 0; |
| 762 | if(0==once){ |
| 763 | once = 1; |
| 764 | style_load_js("forum.js"); |
| 765 | style_emit_script_fossil_bootstrap(0); |
| 766 | style_emit_script_dom(0); |
| 767 | style_emit_script_builtin(0, "fossil.page.forumpost.js"); |
| 768 | } |
| 769 | } |
| 770 | |
| 771 | /* |
| 772 | ** WEBPAGE: forumpost |
| @@ -894,11 +890,11 @@ | |
| 894 | }else{ |
| 895 | style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName); |
| 896 | style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName); |
| 897 | forum_display_hierarchical(froot, fpid); |
| 898 | } |
| 899 | forum_emit_page_js(); |
| 900 | style_footer(); |
| 901 | } |
| 902 | |
| 903 | /* |
| 904 | ** Return true if a forum post should be moderated. |
| 905 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -749,24 +749,20 @@ | |
| 749 | forumthread_delete(pThread); |
| 750 | return target; |
| 751 | } |
| 752 | |
| 753 | /* |
| 754 | ** Emits all JS code required by /forumpost. |
| 755 | */ |
| 756 | static void forumpost_emit_page_js(){ |
| 757 | static int once = 0; |
| 758 | if(0==once){ |
| 759 | once = 1; |
| 760 | style_emit_script_fossil_bootstrap(1); |
| 761 | builtin_request_js("forum.js"); |
| 762 | builtin_request_js("fossil.dom.js"); |
| 763 | builtin_request_js("fossil.page.forumpost.js"); |
| 764 | } |
| 765 | } |
| 766 | |
| 767 | /* |
| 768 | ** WEBPAGE: forumpost |
| @@ -894,11 +890,11 @@ | |
| 890 | }else{ |
| 891 | style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName); |
| 892 | style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName); |
| 893 | forum_display_hierarchical(froot, fpid); |
| 894 | } |
| 895 | forumpost_emit_page_js(); |
| 896 | style_footer(); |
| 897 | } |
| 898 | |
| 899 | /* |
| 900 | ** Return true if a forum post should be moderated. |
| 901 |
+1
-1
| --- src/forum.js | ||
| +++ src/forum.js | ||
| @@ -14,6 +14,6 @@ | ||
| 14 | 14 | var h = x[0].scrollHeight; |
| 15 | 15 | var y = absoluteY(x[0]); |
| 16 | 16 | if( w>h ) y = y + (h-w)/2; |
| 17 | 17 | if( y>0 ) window.scrollTo(0, y); |
| 18 | 18 | } |
| 19 | -})() | |
| 19 | +})(); | |
| 20 | 20 |
| --- src/forum.js | |
| +++ src/forum.js | |
| @@ -14,6 +14,6 @@ | |
| 14 | var h = x[0].scrollHeight; |
| 15 | var y = absoluteY(x[0]); |
| 16 | if( w>h ) y = y + (h-w)/2; |
| 17 | if( y>0 ) window.scrollTo(0, y); |
| 18 | } |
| 19 | })() |
| 20 |
| --- src/forum.js | |
| +++ src/forum.js | |
| @@ -14,6 +14,6 @@ | |
| 14 | var h = x[0].scrollHeight; |
| 15 | var y = absoluteY(x[0]); |
| 16 | if( w>h ) y = y + (h-w)/2; |
| 17 | if( y>0 ) window.scrollTo(0, y); |
| 18 | } |
| 19 | })(); |
| 20 |
+33
-3
| --- src/fossil.bootstrap.js | ||
| +++ src/fossil.bootstrap.js | ||
| @@ -1,6 +1,18 @@ | ||
| 1 | 1 | "use strict"; |
| 2 | +(function () { | |
| 3 | + /* CustomEvent polyfill, courtesy of Mozilla: | |
| 4 | + https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent | |
| 5 | + */ | |
| 6 | + if(typeof window.CustomEvent === "function") return false; | |
| 7 | + window.CustomEvent = function(event, params) { | |
| 8 | + if(!params) params = {bubbles: false, cancelable: false, detail: null}; | |
| 9 | + const evt = document.createEvent('CustomEvent'); | |
| 10 | + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); | |
| 11 | + return evt; | |
| 12 | + }; | |
| 13 | +})(); | |
| 2 | 14 | (function(global){ |
| 3 | 15 | /* Bootstrapping bits for the global.fossil object. Must be |
| 4 | 16 | loaded after style.c:style_emit_script_tag() has initialized |
| 5 | 17 | that object. |
| 6 | 18 | */ |
| @@ -17,10 +29,25 @@ | ||
| 17 | 29 | } |
| 18 | 30 | const d = new Date(); |
| 19 | 31 | return d.toISOString().replace(f.rx1,'').split('T').join(' '); |
| 20 | 32 | }; |
| 21 | 33 | |
| 34 | + /** Returns the local time string of Date object d, defaulting | |
| 35 | + to the current time. */ | |
| 36 | + const localTimeString = function ff(d){ | |
| 37 | + if(!ff.pad){ | |
| 38 | + ff.pad = (x)=>(''+x).length>1 ? x : '0'+x; | |
| 39 | + } | |
| 40 | + d || (d = new Date()); | |
| 41 | + return [ | |
| 42 | + d.getFullYear(),'-',ff.pad(d.getMonth()+1/*sigh*/), | |
| 43 | + '-',ff.pad(d.getDate()), | |
| 44 | + ' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()), | |
| 45 | + ':',ff.pad(d.getSeconds()) | |
| 46 | + ].join(''); | |
| 47 | + }; | |
| 48 | + | |
| 22 | 49 | /* |
| 23 | 50 | ** By default fossil.message() sends its arguments console.debug(). If |
| 24 | 51 | ** fossil.message.targetElement is set, it is assumed to be a DOM |
| 25 | 52 | ** element, its innerText gets assigned to the concatenation of all |
| 26 | 53 | ** arguments (with a space between each), and the CSS 'error' class is |
| @@ -30,11 +57,14 @@ | ||
| 30 | 57 | ** Returns this object. |
| 31 | 58 | */ |
| 32 | 59 | F.message = function f(msg){ |
| 33 | 60 | const args = Array.prototype.slice.call(arguments,0); |
| 34 | 61 | const tgt = f.targetElement; |
| 35 | - if(args.length) args.unshift(timestring(),'UTC:'); | |
| 62 | + if(args.length) args.unshift( | |
| 63 | + localTimeString()+':' | |
| 64 | + //timestring(),'UTC:' | |
| 65 | + ); | |
| 36 | 66 | if(tgt){ |
| 37 | 67 | tgt.classList.remove('error'); |
| 38 | 68 | tgt.innerText = args.join(' '); |
| 39 | 69 | } |
| 40 | 70 | else{ |
| @@ -107,20 +137,20 @@ | ||
| 107 | 137 | repoUrl( repoRelativePath [,urlParams] ) |
| 108 | 138 | |
| 109 | 139 | Creates a URL by prepending this.rootPath to the given path |
| 110 | 140 | (which must be relative from the top of the site, without a |
| 111 | 141 | leading slash). If urlParams is a string, it must be |
| 112 | - paramters encoded in the form "key=val&key2=val2...", WITHOUT | |
| 142 | + paramters encoded in the form "key=val&key2=val2..." WITHOUT | |
| 113 | 143 | a leading '?'. If it's an object, all of its properties get |
| 114 | 144 | appended to the URL in that form. |
| 115 | 145 | */ |
| 116 | 146 | F.repoUrl = function(path,urlParams){ |
| 117 | 147 | if(!urlParams) return this.rootPath+path; |
| 118 | 148 | const url=[this.rootPath,path]; |
| 119 | 149 | url.push('?'); |
| 120 | 150 | if('string'===typeof urlParams) url.push(urlParams); |
| 121 | - else if('object'===typeof urlParams){ | |
| 151 | + else if(urlParams && 'object'===typeof urlParams){ | |
| 122 | 152 | this.encodeUrlArgs(urlParams, url); |
| 123 | 153 | } |
| 124 | 154 | return url.join(''); |
| 125 | 155 | }; |
| 126 | 156 | |
| 127 | 157 |
| --- src/fossil.bootstrap.js | |
| +++ src/fossil.bootstrap.js | |
| @@ -1,6 +1,18 @@ | |
| 1 | "use strict"; |
| 2 | (function(global){ |
| 3 | /* Bootstrapping bits for the global.fossil object. Must be |
| 4 | loaded after style.c:style_emit_script_tag() has initialized |
| 5 | that object. |
| 6 | */ |
| @@ -17,10 +29,25 @@ | |
| 17 | } |
| 18 | const d = new Date(); |
| 19 | return d.toISOString().replace(f.rx1,'').split('T').join(' '); |
| 20 | }; |
| 21 | |
| 22 | /* |
| 23 | ** By default fossil.message() sends its arguments console.debug(). If |
| 24 | ** fossil.message.targetElement is set, it is assumed to be a DOM |
| 25 | ** element, its innerText gets assigned to the concatenation of all |
| 26 | ** arguments (with a space between each), and the CSS 'error' class is |
| @@ -30,11 +57,14 @@ | |
| 30 | ** Returns this object. |
| 31 | */ |
| 32 | F.message = function f(msg){ |
| 33 | const args = Array.prototype.slice.call(arguments,0); |
| 34 | const tgt = f.targetElement; |
| 35 | if(args.length) args.unshift(timestring(),'UTC:'); |
| 36 | if(tgt){ |
| 37 | tgt.classList.remove('error'); |
| 38 | tgt.innerText = args.join(' '); |
| 39 | } |
| 40 | else{ |
| @@ -107,20 +137,20 @@ | |
| 107 | repoUrl( repoRelativePath [,urlParams] ) |
| 108 | |
| 109 | Creates a URL by prepending this.rootPath to the given path |
| 110 | (which must be relative from the top of the site, without a |
| 111 | leading slash). If urlParams is a string, it must be |
| 112 | paramters encoded in the form "key=val&key2=val2...", WITHOUT |
| 113 | a leading '?'. If it's an object, all of its properties get |
| 114 | appended to the URL in that form. |
| 115 | */ |
| 116 | F.repoUrl = function(path,urlParams){ |
| 117 | if(!urlParams) return this.rootPath+path; |
| 118 | const url=[this.rootPath,path]; |
| 119 | url.push('?'); |
| 120 | if('string'===typeof urlParams) url.push(urlParams); |
| 121 | else if('object'===typeof urlParams){ |
| 122 | this.encodeUrlArgs(urlParams, url); |
| 123 | } |
| 124 | return url.join(''); |
| 125 | }; |
| 126 | |
| 127 |
| --- src/fossil.bootstrap.js | |
| +++ src/fossil.bootstrap.js | |
| @@ -1,6 +1,18 @@ | |
| 1 | "use strict"; |
| 2 | (function () { |
| 3 | /* CustomEvent polyfill, courtesy of Mozilla: |
| 4 | https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent |
| 5 | */ |
| 6 | if(typeof window.CustomEvent === "function") return false; |
| 7 | window.CustomEvent = function(event, params) { |
| 8 | if(!params) params = {bubbles: false, cancelable: false, detail: null}; |
| 9 | const evt = document.createEvent('CustomEvent'); |
| 10 | evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); |
| 11 | return evt; |
| 12 | }; |
| 13 | })(); |
| 14 | (function(global){ |
| 15 | /* Bootstrapping bits for the global.fossil object. Must be |
| 16 | loaded after style.c:style_emit_script_tag() has initialized |
| 17 | that object. |
| 18 | */ |
| @@ -17,10 +29,25 @@ | |
| 29 | } |
| 30 | const d = new Date(); |
| 31 | return d.toISOString().replace(f.rx1,'').split('T').join(' '); |
| 32 | }; |
| 33 | |
| 34 | /** Returns the local time string of Date object d, defaulting |
| 35 | to the current time. */ |
| 36 | const localTimeString = function ff(d){ |
| 37 | if(!ff.pad){ |
| 38 | ff.pad = (x)=>(''+x).length>1 ? x : '0'+x; |
| 39 | } |
| 40 | d || (d = new Date()); |
| 41 | return [ |
| 42 | d.getFullYear(),'-',ff.pad(d.getMonth()+1/*sigh*/), |
| 43 | '-',ff.pad(d.getDate()), |
| 44 | ' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()), |
| 45 | ':',ff.pad(d.getSeconds()) |
| 46 | ].join(''); |
| 47 | }; |
| 48 | |
| 49 | /* |
| 50 | ** By default fossil.message() sends its arguments console.debug(). If |
| 51 | ** fossil.message.targetElement is set, it is assumed to be a DOM |
| 52 | ** element, its innerText gets assigned to the concatenation of all |
| 53 | ** arguments (with a space between each), and the CSS 'error' class is |
| @@ -30,11 +57,14 @@ | |
| 57 | ** Returns this object. |
| 58 | */ |
| 59 | F.message = function f(msg){ |
| 60 | const args = Array.prototype.slice.call(arguments,0); |
| 61 | const tgt = f.targetElement; |
| 62 | if(args.length) args.unshift( |
| 63 | localTimeString()+':' |
| 64 | //timestring(),'UTC:' |
| 65 | ); |
| 66 | if(tgt){ |
| 67 | tgt.classList.remove('error'); |
| 68 | tgt.innerText = args.join(' '); |
| 69 | } |
| 70 | else{ |
| @@ -107,20 +137,20 @@ | |
| 137 | repoUrl( repoRelativePath [,urlParams] ) |
| 138 | |
| 139 | Creates a URL by prepending this.rootPath to the given path |
| 140 | (which must be relative from the top of the site, without a |
| 141 | leading slash). If urlParams is a string, it must be |
| 142 | paramters encoded in the form "key=val&key2=val2..." WITHOUT |
| 143 | a leading '?'. If it's an object, all of its properties get |
| 144 | appended to the URL in that form. |
| 145 | */ |
| 146 | F.repoUrl = function(path,urlParams){ |
| 147 | if(!urlParams) return this.rootPath+path; |
| 148 | const url=[this.rootPath,path]; |
| 149 | url.push('?'); |
| 150 | if('string'===typeof urlParams) url.push(urlParams); |
| 151 | else if(urlParams && 'object'===typeof urlParams){ |
| 152 | this.encodeUrlArgs(urlParams, url); |
| 153 | } |
| 154 | return url.join(''); |
| 155 | }; |
| 156 | |
| 157 |
+41
-8
| --- src/fossil.confirmer.js | ||
| +++ src/fossil.confirmer.js | ||
| @@ -81,10 +81,21 @@ | ||
| 81 | 81 | action has been confirmed, immediately before the onconfirm or |
| 82 | 82 | ontimeout callback. The intention of the callback is to update the |
| 83 | 83 | label of the target element. If .ticks is set but .ontick is not |
| 84 | 84 | then a default implementation is used which updates the element with |
| 85 | 85 | the .confirmText, prepending a countdown to it. |
| 86 | + | |
| 87 | + .pinSize = if true AND confirmText is set, calculate the larger of | |
| 88 | + the element's original and confirmed size and pin it to the larger | |
| 89 | + of those sizes to avoid layout reflows when confirmation is | |
| 90 | + running. The pinning is implemented by setting its minWidth and | |
| 91 | + maxWidth style properties to the same value. This does not work if | |
| 92 | + the element text is updated dynamically via ontick(). This ONLY | |
| 93 | + works if the element is in the DOM and is not hidden (e.g. via | |
| 94 | + display:none) at the time this routine is called, otherwise we | |
| 95 | + cannot calculate its size. If the element needs to be hidden, hide | |
| 96 | + it after initializing the confirmer. | |
| 86 | 97 | |
| 87 | 98 | .debug = boolean. If truthy, it sends some debug output to the dev |
| 88 | 99 | console to track what it's doing. |
| 89 | 100 | |
| 90 | 101 | Various notes: |
| @@ -99,17 +110,26 @@ | ||
| 99 | 110 | - Due to the nature of multi-threaded code, it is potentially possible |
| 100 | 111 | that confirmation and timeout actions BOTH happen if the user |
| 101 | 112 | triggers the associated action at "just the right millisecond" |
| 102 | 113 | before the timeout is triggered. |
| 103 | 114 | |
| 104 | -TODO: add an invert option which activates if the timeout is reached | |
| 105 | -and "times out" if the element is clicked again. e.g. a button which | |
| 106 | -says "Saving..." and cancels the op if it's clicked again, else it | |
| 107 | -saves after X time/ticks. | |
| 115 | +TODO: | |
| 116 | + | |
| 117 | +- Add an invert option which activates if the timeout is reached and | |
| 118 | +"times out" if the element is clicked again. e.g. a button which says | |
| 119 | +"Saving..." and cancels the op if it's clicked again, else it saves | |
| 120 | +after X time/ticks. | |
| 121 | + | |
| 122 | +- Internally we save/restore the initial text of non-INPUT elements | |
| 123 | +using innerHTML. We should instead move their child nodes aside (into | |
| 124 | +an internal out-of-DOM element) and restore them as needed. | |
| 108 | 125 | |
| 109 | 126 | Terse Change history: |
| 110 | 127 | |
| 128 | +- 20200811 | |
| 129 | + - Added pinSize option. | |
| 130 | + | |
| 111 | 131 | - 20200507: |
| 112 | 132 | - Add a tick-based countdown in order to more easily support |
| 113 | 133 | updating the target element with the countdown. |
| 114 | 134 | |
| 115 | 135 | - 20200506: |
| @@ -138,14 +158,26 @@ | ||
| 138 | 158 | const isInput = f.isInput(target); |
| 139 | 159 | const updateText = function(msg){ |
| 140 | 160 | if(isInput) target.value = msg; |
| 141 | 161 | else target.innerHTML = msg; |
| 142 | 162 | } |
| 163 | + const formatCountdown = (txt, number) => txt + " ["+number+"]"; | |
| 164 | + if(opt.pinSize && opt.confirmText){ | |
| 165 | + /* Try to pin the element's width the the greater of its | |
| 166 | + current width or its waiting-on-confirmation width | |
| 167 | + to avoid layout reflow when it's activated. */ | |
| 168 | + const digits = (''+(opt.timeout/1000 || opt.ticks)).length; | |
| 169 | + const lblLong = formatCountdown(opt.confirmText, "00000000".substr(0,digits)); | |
| 170 | + const w1 = parseFloat(window.getComputedStyle(target).width); | |
| 171 | + updateText(lblLong); | |
| 172 | + const w2 = parseFloat(window.getComputedStyle(target).width); | |
| 173 | + target.style.minWidth = target.style.maxWidth = (w1>w2 ? w1 : w2)+"px"; | |
| 174 | + } | |
| 143 | 175 | updateText(this.opt.initialText); |
| 144 | 176 | if(this.opt.ticks && !this.opt.ontick){ |
| 145 | 177 | this.opt.ontick = function(tick){ |
| 146 | - updateText("("+tick+") "+self.opt.confirmText); | |
| 178 | + updateText(formatCountdown(self.opt.confirmText,tick)); | |
| 147 | 179 | }; |
| 148 | 180 | } |
| 149 | 181 | this.setClasses(false); |
| 150 | 182 | this.doTimeout = function() { |
| 151 | 183 | if(this.timerID){ |
| @@ -268,15 +300,16 @@ | ||
| 268 | 300 | }; |
| 269 | 301 | /** |
| 270 | 302 | The default options for initConfirmer(). Tweak them to set the |
| 271 | 303 | defaults. A couple of them (initialText and confirmText) are |
| 272 | 304 | dynamically-generated, and can't reasonably be set in the |
| 273 | - defaults. | |
| 305 | + defaults. Some, like ticks, cannot be set here because that would | |
| 306 | + end up indirectly replacing non-tick timeouts with ticks. | |
| 274 | 307 | */ |
| 275 | 308 | F.confirmer.defaultOpts = { |
| 276 | - timeout:3000, | |
| 277 | - ticks: undefined, | |
| 309 | + timeout:undefined, | |
| 310 | + ticks: 3, | |
| 278 | 311 | ticktime: 998/*not *quite* 1000*/, |
| 279 | 312 | onconfirm: undefined, |
| 280 | 313 | ontimeout: undefined, |
| 281 | 314 | onactivate: undefined, |
| 282 | 315 | classInitial: '', |
| 283 | 316 | |
| 284 | 317 | ADDED src/fossil.copybutton.js |
| --- src/fossil.confirmer.js | |
| +++ src/fossil.confirmer.js | |
| @@ -81,10 +81,21 @@ | |
| 81 | action has been confirmed, immediately before the onconfirm or |
| 82 | ontimeout callback. The intention of the callback is to update the |
| 83 | label of the target element. If .ticks is set but .ontick is not |
| 84 | then a default implementation is used which updates the element with |
| 85 | the .confirmText, prepending a countdown to it. |
| 86 | |
| 87 | .debug = boolean. If truthy, it sends some debug output to the dev |
| 88 | console to track what it's doing. |
| 89 | |
| 90 | Various notes: |
| @@ -99,17 +110,26 @@ | |
| 99 | - Due to the nature of multi-threaded code, it is potentially possible |
| 100 | that confirmation and timeout actions BOTH happen if the user |
| 101 | triggers the associated action at "just the right millisecond" |
| 102 | before the timeout is triggered. |
| 103 | |
| 104 | TODO: add an invert option which activates if the timeout is reached |
| 105 | and "times out" if the element is clicked again. e.g. a button which |
| 106 | says "Saving..." and cancels the op if it's clicked again, else it |
| 107 | saves after X time/ticks. |
| 108 | |
| 109 | Terse Change history: |
| 110 | |
| 111 | - 20200507: |
| 112 | - Add a tick-based countdown in order to more easily support |
| 113 | updating the target element with the countdown. |
| 114 | |
| 115 | - 20200506: |
| @@ -138,14 +158,26 @@ | |
| 138 | const isInput = f.isInput(target); |
| 139 | const updateText = function(msg){ |
| 140 | if(isInput) target.value = msg; |
| 141 | else target.innerHTML = msg; |
| 142 | } |
| 143 | updateText(this.opt.initialText); |
| 144 | if(this.opt.ticks && !this.opt.ontick){ |
| 145 | this.opt.ontick = function(tick){ |
| 146 | updateText("("+tick+") "+self.opt.confirmText); |
| 147 | }; |
| 148 | } |
| 149 | this.setClasses(false); |
| 150 | this.doTimeout = function() { |
| 151 | if(this.timerID){ |
| @@ -268,15 +300,16 @@ | |
| 268 | }; |
| 269 | /** |
| 270 | The default options for initConfirmer(). Tweak them to set the |
| 271 | defaults. A couple of them (initialText and confirmText) are |
| 272 | dynamically-generated, and can't reasonably be set in the |
| 273 | defaults. |
| 274 | */ |
| 275 | F.confirmer.defaultOpts = { |
| 276 | timeout:3000, |
| 277 | ticks: undefined, |
| 278 | ticktime: 998/*not *quite* 1000*/, |
| 279 | onconfirm: undefined, |
| 280 | ontimeout: undefined, |
| 281 | onactivate: undefined, |
| 282 | classInitial: '', |
| 283 | |
| 284 | DDED src/fossil.copybutton.js |
| --- src/fossil.confirmer.js | |
| +++ src/fossil.confirmer.js | |
| @@ -81,10 +81,21 @@ | |
| 81 | action has been confirmed, immediately before the onconfirm or |
| 82 | ontimeout callback. The intention of the callback is to update the |
| 83 | label of the target element. If .ticks is set but .ontick is not |
| 84 | then a default implementation is used which updates the element with |
| 85 | the .confirmText, prepending a countdown to it. |
| 86 | |
| 87 | .pinSize = if true AND confirmText is set, calculate the larger of |
| 88 | the element's original and confirmed size and pin it to the larger |
| 89 | of those sizes to avoid layout reflows when confirmation is |
| 90 | running. The pinning is implemented by setting its minWidth and |
| 91 | maxWidth style properties to the same value. This does not work if |
| 92 | the element text is updated dynamically via ontick(). This ONLY |
| 93 | works if the element is in the DOM and is not hidden (e.g. via |
| 94 | display:none) at the time this routine is called, otherwise we |
| 95 | cannot calculate its size. If the element needs to be hidden, hide |
| 96 | it after initializing the confirmer. |
| 97 | |
| 98 | .debug = boolean. If truthy, it sends some debug output to the dev |
| 99 | console to track what it's doing. |
| 100 | |
| 101 | Various notes: |
| @@ -99,17 +110,26 @@ | |
| 110 | - Due to the nature of multi-threaded code, it is potentially possible |
| 111 | that confirmation and timeout actions BOTH happen if the user |
| 112 | triggers the associated action at "just the right millisecond" |
| 113 | before the timeout is triggered. |
| 114 | |
| 115 | TODO: |
| 116 | |
| 117 | - Add an invert option which activates if the timeout is reached and |
| 118 | "times out" if the element is clicked again. e.g. a button which says |
| 119 | "Saving..." and cancels the op if it's clicked again, else it saves |
| 120 | after X time/ticks. |
| 121 | |
| 122 | - Internally we save/restore the initial text of non-INPUT elements |
| 123 | using innerHTML. We should instead move their child nodes aside (into |
| 124 | an internal out-of-DOM element) and restore them as needed. |
| 125 | |
| 126 | Terse Change history: |
| 127 | |
| 128 | - 20200811 |
| 129 | - Added pinSize option. |
| 130 | |
| 131 | - 20200507: |
| 132 | - Add a tick-based countdown in order to more easily support |
| 133 | updating the target element with the countdown. |
| 134 | |
| 135 | - 20200506: |
| @@ -138,14 +158,26 @@ | |
| 158 | const isInput = f.isInput(target); |
| 159 | const updateText = function(msg){ |
| 160 | if(isInput) target.value = msg; |
| 161 | else target.innerHTML = msg; |
| 162 | } |
| 163 | const formatCountdown = (txt, number) => txt + " ["+number+"]"; |
| 164 | if(opt.pinSize && opt.confirmText){ |
| 165 | /* Try to pin the element's width the the greater of its |
| 166 | current width or its waiting-on-confirmation width |
| 167 | to avoid layout reflow when it's activated. */ |
| 168 | const digits = (''+(opt.timeout/1000 || opt.ticks)).length; |
| 169 | const lblLong = formatCountdown(opt.confirmText, "00000000".substr(0,digits)); |
| 170 | const w1 = parseFloat(window.getComputedStyle(target).width); |
| 171 | updateText(lblLong); |
| 172 | const w2 = parseFloat(window.getComputedStyle(target).width); |
| 173 | target.style.minWidth = target.style.maxWidth = (w1>w2 ? w1 : w2)+"px"; |
| 174 | } |
| 175 | updateText(this.opt.initialText); |
| 176 | if(this.opt.ticks && !this.opt.ontick){ |
| 177 | this.opt.ontick = function(tick){ |
| 178 | updateText(formatCountdown(self.opt.confirmText,tick)); |
| 179 | }; |
| 180 | } |
| 181 | this.setClasses(false); |
| 182 | this.doTimeout = function() { |
| 183 | if(this.timerID){ |
| @@ -268,15 +300,16 @@ | |
| 300 | }; |
| 301 | /** |
| 302 | The default options for initConfirmer(). Tweak them to set the |
| 303 | defaults. A couple of them (initialText and confirmText) are |
| 304 | dynamically-generated, and can't reasonably be set in the |
| 305 | defaults. Some, like ticks, cannot be set here because that would |
| 306 | end up indirectly replacing non-tick timeouts with ticks. |
| 307 | */ |
| 308 | F.confirmer.defaultOpts = { |
| 309 | timeout:undefined, |
| 310 | ticks: 3, |
| 311 | ticktime: 998/*not *quite* 1000*/, |
| 312 | onconfirm: undefined, |
| 313 | ontimeout: undefined, |
| 314 | onactivate: undefined, |
| 315 | classInitial: '', |
| 316 | |
| 317 | DDED src/fossil.copybutton.js |
| --- a/src/fossil.copybutton.js | ||
| +++ b/src/fossil.copybutton.js | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +(function(F/*fossil object*/){ | |
| 2 | + /** | |
| 3 | + A basic API for creating and |
| --- a/src/fossil.copybutton.js | |
| +++ b/src/fossil.copybutton.js | |
| @@ -0,0 +1,3 @@ | |
| --- a/src/fossil.copybutton.js | |
| +++ b/src/fossil.copybutton.js | |
| @@ -0,0 +1,3 @@ | |
| 1 | (function(F/*fossil object*/){ |
| 2 | /** |
| 3 | A basic API for creating and |
+93
| --- src/fossil.dom.js | ||
| +++ src/fossil.dom.js | ||
| @@ -465,10 +465,103 @@ | ||
| 465 | 465 | e = src.querySelector(x); |
| 466 | 466 | if(!e){ |
| 467 | 467 | e = new Error("Cannot find DOM element: "+x); |
| 468 | 468 | console.error(e, src); |
| 469 | 469 | throw e; |
| 470 | + } | |
| 471 | + return e; | |
| 472 | + }; | |
| 473 | + | |
| 474 | + /** | |
| 475 | + "Blinks" the given element a single time for the given number of | |
| 476 | + milliseconds, defaulting (if the 2nd argument is falsy or not a | |
| 477 | + number) to flashOnce.defaultTimeMs. If a 3rd argument is passed | |
| 478 | + in, it must be a function, and it gets callback back at the end | |
| 479 | + of the asynchronous flashing processes. | |
| 480 | + | |
| 481 | + This will only activate once per element during that timeframe - | |
| 482 | + further calls will become no-ops until the blink is | |
| 483 | + completed. This routine adds a dataset member to the element for | |
| 484 | + the duration of the blink, to allow it to block multiple blinks. | |
| 485 | + | |
| 486 | + If passed 2 arguments and the 2nd is a function, it behaves as if | |
| 487 | + it were called as (arg1, undefined, arg2). | |
| 488 | + | |
| 489 | + Returns e, noting that the flash itself is asynchronous and may | |
| 490 | + still be running, or not yet started, when this function returns. | |
| 491 | + */ | |
| 492 | + dom.flashOnce = function f(e,howLongMs,afterFlashCallback){ | |
| 493 | + if(e.dataset.isBlinking){ | |
| 494 | + return; | |
| 495 | + } | |
| 496 | + if(2===arguments.length && 'function' ===typeof howLongMs){ | |
| 497 | + afterFlashCallback = howLongMs; | |
| 498 | + howLongMs = f.defaultTimeMs; | |
| 499 | + } | |
| 500 | + if(!howLongMs || 'number'!==typeof howLongMs){ | |
| 501 | + howLongMs = f.defaultTimeMs; | |
| 502 | + } | |
| 503 | + e.dataset.isBlinking = true; | |
| 504 | + const transition = e.style.transition; | |
| 505 | + e.style.transition = "opacity "+howLongMs+"ms ease-in-out"; | |
| 506 | + const opacity = e.style.opacity; | |
| 507 | + e.style.opacity = 0; | |
| 508 | + setTimeout(function(){ | |
| 509 | + e.style.transition = transition; | |
| 510 | + e.style.opacity = opacity; | |
| 511 | + delete e.dataset.isBlinking; | |
| 512 | + if(afterFlashCallback) afterFlashCallback(); | |
| 513 | + }, howLongMs); | |
| 514 | + return e; | |
| 515 | + }; | |
| 516 | + dom.flashOnce.defaultTimeMs = 400; | |
| 517 | + | |
| 518 | + /** | |
| 519 | + Attempts to copy the given text to the system clipboard. Returns | |
| 520 | + true if it succeeds, else false. | |
| 521 | + */ | |
| 522 | + dom.copyTextToClipboard = function(text){ | |
| 523 | + if( window.clipboardData && window.clipboardData.setData ){ | |
| 524 | + clipboardData.setData('Text',text); | |
| 525 | + return true; | |
| 526 | + }else{ | |
| 527 | + const x = document.createElement("textarea"); | |
| 528 | + x.style.position = 'fixed'; | |
| 529 | + x.value = text; | |
| 530 | + document.body.appendChild(x); | |
| 531 | + x.select(); | |
| 532 | + var rc; | |
| 533 | + try{ | |
| 534 | + document.execCommand('copy'); | |
| 535 | + rc = true; | |
| 536 | + }catch(err){ | |
| 537 | + rc = false; | |
| 538 | + }finally{ | |
| 539 | + document.body.removeChild(x); | |
| 540 | + } | |
| 541 | + return rc; | |
| 542 | + } | |
| 543 | + }; | |
| 544 | + | |
| 545 | + /** | |
| 546 | + Copies all properties from the 2nd argument (a plain object) into | |
| 547 | + the style member of the first argument (DOM element or a | |
| 548 | + forEach-capable list of elements). If the 2nd argument is falsy | |
| 549 | + or empty, this is a no-op. | |
| 550 | + | |
| 551 | + Returns its first argument. | |
| 552 | + */ | |
| 553 | + dom.copyStyle = function f(e, style){ | |
| 554 | + if(e.forEach){ | |
| 555 | + e.forEach((x)=>f(x, style)); | |
| 556 | + return e; | |
| 557 | + } | |
| 558 | + if(style){ | |
| 559 | + let k; | |
| 560 | + for(k in style){ | |
| 561 | + if(style.hasOwnProperty(k)) e.style[k] = style[k]; | |
| 562 | + } | |
| 470 | 563 | } |
| 471 | 564 | return e; |
| 472 | 565 | }; |
| 473 | 566 | |
| 474 | 567 | return F.dom = dom; |
| 475 | 568 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -465,10 +465,103 @@ | |
| 465 | e = src.querySelector(x); |
| 466 | if(!e){ |
| 467 | e = new Error("Cannot find DOM element: "+x); |
| 468 | console.error(e, src); |
| 469 | throw e; |
| 470 | } |
| 471 | return e; |
| 472 | }; |
| 473 | |
| 474 | return F.dom = dom; |
| 475 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -465,10 +465,103 @@ | |
| 465 | e = src.querySelector(x); |
| 466 | if(!e){ |
| 467 | e = new Error("Cannot find DOM element: "+x); |
| 468 | console.error(e, src); |
| 469 | throw e; |
| 470 | } |
| 471 | return e; |
| 472 | }; |
| 473 | |
| 474 | /** |
| 475 | "Blinks" the given element a single time for the given number of |
| 476 | milliseconds, defaulting (if the 2nd argument is falsy or not a |
| 477 | number) to flashOnce.defaultTimeMs. If a 3rd argument is passed |
| 478 | in, it must be a function, and it gets callback back at the end |
| 479 | of the asynchronous flashing processes. |
| 480 | |
| 481 | This will only activate once per element during that timeframe - |
| 482 | further calls will become no-ops until the blink is |
| 483 | completed. This routine adds a dataset member to the element for |
| 484 | the duration of the blink, to allow it to block multiple blinks. |
| 485 | |
| 486 | If passed 2 arguments and the 2nd is a function, it behaves as if |
| 487 | it were called as (arg1, undefined, arg2). |
| 488 | |
| 489 | Returns e, noting that the flash itself is asynchronous and may |
| 490 | still be running, or not yet started, when this function returns. |
| 491 | */ |
| 492 | dom.flashOnce = function f(e,howLongMs,afterFlashCallback){ |
| 493 | if(e.dataset.isBlinking){ |
| 494 | return; |
| 495 | } |
| 496 | if(2===arguments.length && 'function' ===typeof howLongMs){ |
| 497 | afterFlashCallback = howLongMs; |
| 498 | howLongMs = f.defaultTimeMs; |
| 499 | } |
| 500 | if(!howLongMs || 'number'!==typeof howLongMs){ |
| 501 | howLongMs = f.defaultTimeMs; |
| 502 | } |
| 503 | e.dataset.isBlinking = true; |
| 504 | const transition = e.style.transition; |
| 505 | e.style.transition = "opacity "+howLongMs+"ms ease-in-out"; |
| 506 | const opacity = e.style.opacity; |
| 507 | e.style.opacity = 0; |
| 508 | setTimeout(function(){ |
| 509 | e.style.transition = transition; |
| 510 | e.style.opacity = opacity; |
| 511 | delete e.dataset.isBlinking; |
| 512 | if(afterFlashCallback) afterFlashCallback(); |
| 513 | }, howLongMs); |
| 514 | return e; |
| 515 | }; |
| 516 | dom.flashOnce.defaultTimeMs = 400; |
| 517 | |
| 518 | /** |
| 519 | Attempts to copy the given text to the system clipboard. Returns |
| 520 | true if it succeeds, else false. |
| 521 | */ |
| 522 | dom.copyTextToClipboard = function(text){ |
| 523 | if( window.clipboardData && window.clipboardData.setData ){ |
| 524 | clipboardData.setData('Text',text); |
| 525 | return true; |
| 526 | }else{ |
| 527 | const x = document.createElement("textarea"); |
| 528 | x.style.position = 'fixed'; |
| 529 | x.value = text; |
| 530 | document.body.appendChild(x); |
| 531 | x.select(); |
| 532 | var rc; |
| 533 | try{ |
| 534 | document.execCommand('copy'); |
| 535 | rc = true; |
| 536 | }catch(err){ |
| 537 | rc = false; |
| 538 | }finally{ |
| 539 | document.body.removeChild(x); |
| 540 | } |
| 541 | return rc; |
| 542 | } |
| 543 | }; |
| 544 | |
| 545 | /** |
| 546 | Copies all properties from the 2nd argument (a plain object) into |
| 547 | the style member of the first argument (DOM element or a |
| 548 | forEach-capable list of elements). If the 2nd argument is falsy |
| 549 | or empty, this is a no-op. |
| 550 | |
| 551 | Returns its first argument. |
| 552 | */ |
| 553 | dom.copyStyle = function f(e, style){ |
| 554 | if(e.forEach){ |
| 555 | e.forEach((x)=>f(x, style)); |
| 556 | return e; |
| 557 | } |
| 558 | if(style){ |
| 559 | let k; |
| 560 | for(k in style){ |
| 561 | if(style.hasOwnProperty(k)) e.style[k] = style[k]; |
| 562 | } |
| 563 | } |
| 564 | return e; |
| 565 | }; |
| 566 | |
| 567 | return F.dom = dom; |
| 568 |
+23
-9
| --- src/fossil.fetch.js | ||
| +++ src/fossil.fetch.js | ||
| @@ -1,10 +1,13 @@ | ||
| 1 | 1 | "use strict"; |
| 2 | 2 | /** |
| 3 | 3 | Requires that window.fossil has already been set up. |
| 4 | - | |
| 5 | - window.fossil.fetch() is an HTTP request/response mini-framework | |
| 4 | +*/ | |
| 5 | +(function(namespace){ | |
| 6 | +const fossil = namespace; | |
| 7 | + /** | |
| 8 | + fetch() is an HTTP request/response mini-framework | |
| 6 | 9 | similar (but not identical) to the not-quite-ubiquitous |
| 7 | 10 | window.fetch(). |
| 8 | 11 | |
| 9 | 12 | JS usages: |
| 10 | 13 | |
| @@ -87,19 +90,21 @@ | ||
| 87 | 90 | function with the same name, e.g. fossil.fetch.onload(). The |
| 88 | 91 | default onload/onerror implementations route the data through the |
| 89 | 92 | dev console and (for onerror()) through fossil.error(). The default |
| 90 | 93 | beforesend/aftersend are no-ops. Individual pages may overwrite |
| 91 | 94 | those members to provide default implementations suitable for the |
| 92 | - page's use, e.g. keeping track of how many in-flight | |
| 95 | + page's use, e.g. keeping track of how many in-flight ajax requests | |
| 96 | + are pending. Any exceptions thrown in an beforesend/aftersend | |
| 97 | + handler are current ignored (feature or bug?). | |
| 93 | 98 | |
| 94 | 99 | Note that this routine may add properties to the 2nd argument, so |
| 95 | 100 | that instance should not be kept around for later use. |
| 96 | 101 | |
| 97 | 102 | Returns this object, noting that the XHR request is asynchronous, |
| 98 | 103 | and still in transit (or has yet to be sent) when that happens. |
| 99 | 104 | */ |
| 100 | -window.fossil.fetch = function f(uri,opt){ | |
| 105 | +fossil.fetch = function f(uri,opt){ | |
| 101 | 106 | const F = fossil; |
| 102 | 107 | if(!f.onload){ |
| 103 | 108 | f.onload = (r)=>console.debug('fossil.fetch() XHR response:',r); |
| 104 | 109 | } |
| 105 | 110 | if(!f.onerror){ |
| @@ -106,11 +111,11 @@ | ||
| 106 | 111 | f.onerror = function(e/*exception*/){ |
| 107 | 112 | console.error("fossil.fetch() XHR error:",e); |
| 108 | 113 | if(e instanceof Error) F.error('Exception:',e); |
| 109 | 114 | else F.error("Unknown error in handling of XHR request."); |
| 110 | 115 | }; |
| 111 | - }/*f.onerror()*/ | |
| 116 | + } | |
| 112 | 117 | if(!f.parseResponseHeaders){ |
| 113 | 118 | f.parseResponseHeaders = function(h){ |
| 114 | 119 | const rc = {}; |
| 115 | 120 | if(!h) return rc; |
| 116 | 121 | const ar = h.trim().split(/[\r\n]+/); |
| @@ -142,11 +147,11 @@ | ||
| 142 | 147 | || payload instanceof Array)){ |
| 143 | 148 | payload = JSON.stringify(payload); |
| 144 | 149 | opt.contentType = 'application/json'; |
| 145 | 150 | } |
| 146 | 151 | } |
| 147 | - const url=[F.repoUrl(uri,opt.urlParams)], | |
| 152 | + const url=[f.urlTransform(uri,opt.urlParams)], | |
| 148 | 153 | x=new XMLHttpRequest(); |
| 149 | 154 | if('json'===opt.responseType){ |
| 150 | 155 | /* 'json' is an extension to the supported XHR.responseType |
| 151 | 156 | list. We use it as a flag to tell us to JSON.parse() |
| 152 | 157 | the response. */ |
| @@ -201,8 +206,17 @@ | ||
| 201 | 206 | if(undefined!==payload) x.send(payload); |
| 202 | 207 | else x.send(); |
| 203 | 208 | return this; |
| 204 | 209 | }; |
| 205 | 210 | |
| 206 | -window.fossil.fetch.beforesend = function(){}; | |
| 207 | -window.fossil.fetch.aftersend = function(){}; | |
| 208 | -window.fossil.fetch.timeout = 15000/* Default timeout, in ms. */; | |
| 211 | +/** | |
| 212 | + urlTransform() must refer to a function which accepts a relative path | |
| 213 | + to the same site as fetch() is served from and an optional set of | |
| 214 | + URL parameters to pass with it (in the form a of a string | |
| 215 | + ("a=b&c=d...") or an object of key/value pairs (which it converts | |
| 216 | + to such a string), and returns the resulting URL or URI as a string. | |
| 217 | +*/ | |
| 218 | +fossil.fetch.urlTransform = (u,p)=>fossil.repoUrl(u,p); | |
| 219 | +fossil.fetch.beforesend = function(){}; | |
| 220 | +fossil.fetch.aftersend = function(){}; | |
| 221 | +fossil.fetch.timeout = 15000/* Default timeout, in ms. */; | |
| 222 | +})(window.fossil); | |
| 209 | 223 | |
| 210 | 224 | ADDED src/fossil.numbered-lines.js |
| --- src/fossil.fetch.js | |
| +++ src/fossil.fetch.js | |
| @@ -1,10 +1,13 @@ | |
| 1 | "use strict"; |
| 2 | /** |
| 3 | Requires that window.fossil has already been set up. |
| 4 | |
| 5 | window.fossil.fetch() is an HTTP request/response mini-framework |
| 6 | similar (but not identical) to the not-quite-ubiquitous |
| 7 | window.fetch(). |
| 8 | |
| 9 | JS usages: |
| 10 | |
| @@ -87,19 +90,21 @@ | |
| 87 | function with the same name, e.g. fossil.fetch.onload(). The |
| 88 | default onload/onerror implementations route the data through the |
| 89 | dev console and (for onerror()) through fossil.error(). The default |
| 90 | beforesend/aftersend are no-ops. Individual pages may overwrite |
| 91 | those members to provide default implementations suitable for the |
| 92 | page's use, e.g. keeping track of how many in-flight |
| 93 | |
| 94 | Note that this routine may add properties to the 2nd argument, so |
| 95 | that instance should not be kept around for later use. |
| 96 | |
| 97 | Returns this object, noting that the XHR request is asynchronous, |
| 98 | and still in transit (or has yet to be sent) when that happens. |
| 99 | */ |
| 100 | window.fossil.fetch = function f(uri,opt){ |
| 101 | const F = fossil; |
| 102 | if(!f.onload){ |
| 103 | f.onload = (r)=>console.debug('fossil.fetch() XHR response:',r); |
| 104 | } |
| 105 | if(!f.onerror){ |
| @@ -106,11 +111,11 @@ | |
| 106 | f.onerror = function(e/*exception*/){ |
| 107 | console.error("fossil.fetch() XHR error:",e); |
| 108 | if(e instanceof Error) F.error('Exception:',e); |
| 109 | else F.error("Unknown error in handling of XHR request."); |
| 110 | }; |
| 111 | }/*f.onerror()*/ |
| 112 | if(!f.parseResponseHeaders){ |
| 113 | f.parseResponseHeaders = function(h){ |
| 114 | const rc = {}; |
| 115 | if(!h) return rc; |
| 116 | const ar = h.trim().split(/[\r\n]+/); |
| @@ -142,11 +147,11 @@ | |
| 142 | || payload instanceof Array)){ |
| 143 | payload = JSON.stringify(payload); |
| 144 | opt.contentType = 'application/json'; |
| 145 | } |
| 146 | } |
| 147 | const url=[F.repoUrl(uri,opt.urlParams)], |
| 148 | x=new XMLHttpRequest(); |
| 149 | if('json'===opt.responseType){ |
| 150 | /* 'json' is an extension to the supported XHR.responseType |
| 151 | list. We use it as a flag to tell us to JSON.parse() |
| 152 | the response. */ |
| @@ -201,8 +206,17 @@ | |
| 201 | if(undefined!==payload) x.send(payload); |
| 202 | else x.send(); |
| 203 | return this; |
| 204 | }; |
| 205 | |
| 206 | window.fossil.fetch.beforesend = function(){}; |
| 207 | window.fossil.fetch.aftersend = function(){}; |
| 208 | window.fossil.fetch.timeout = 15000/* Default timeout, in ms. */; |
| 209 | |
| 210 | DDED src/fossil.numbered-lines.js |
| --- src/fossil.fetch.js | |
| +++ src/fossil.fetch.js | |
| @@ -1,10 +1,13 @@ | |
| 1 | "use strict"; |
| 2 | /** |
| 3 | Requires that window.fossil has already been set up. |
| 4 | */ |
| 5 | (function(namespace){ |
| 6 | const fossil = namespace; |
| 7 | /** |
| 8 | fetch() is an HTTP request/response mini-framework |
| 9 | similar (but not identical) to the not-quite-ubiquitous |
| 10 | window.fetch(). |
| 11 | |
| 12 | JS usages: |
| 13 | |
| @@ -87,19 +90,21 @@ | |
| 90 | function with the same name, e.g. fossil.fetch.onload(). The |
| 91 | default onload/onerror implementations route the data through the |
| 92 | dev console and (for onerror()) through fossil.error(). The default |
| 93 | beforesend/aftersend are no-ops. Individual pages may overwrite |
| 94 | those members to provide default implementations suitable for the |
| 95 | page's use, e.g. keeping track of how many in-flight ajax requests |
| 96 | are pending. Any exceptions thrown in an beforesend/aftersend |
| 97 | handler are current ignored (feature or bug?). |
| 98 | |
| 99 | Note that this routine may add properties to the 2nd argument, so |
| 100 | that instance should not be kept around for later use. |
| 101 | |
| 102 | Returns this object, noting that the XHR request is asynchronous, |
| 103 | and still in transit (or has yet to be sent) when that happens. |
| 104 | */ |
| 105 | fossil.fetch = function f(uri,opt){ |
| 106 | const F = fossil; |
| 107 | if(!f.onload){ |
| 108 | f.onload = (r)=>console.debug('fossil.fetch() XHR response:',r); |
| 109 | } |
| 110 | if(!f.onerror){ |
| @@ -106,11 +111,11 @@ | |
| 111 | f.onerror = function(e/*exception*/){ |
| 112 | console.error("fossil.fetch() XHR error:",e); |
| 113 | if(e instanceof Error) F.error('Exception:',e); |
| 114 | else F.error("Unknown error in handling of XHR request."); |
| 115 | }; |
| 116 | } |
| 117 | if(!f.parseResponseHeaders){ |
| 118 | f.parseResponseHeaders = function(h){ |
| 119 | const rc = {}; |
| 120 | if(!h) return rc; |
| 121 | const ar = h.trim().split(/[\r\n]+/); |
| @@ -142,11 +147,11 @@ | |
| 147 | || payload instanceof Array)){ |
| 148 | payload = JSON.stringify(payload); |
| 149 | opt.contentType = 'application/json'; |
| 150 | } |
| 151 | } |
| 152 | const url=[f.urlTransform(uri,opt.urlParams)], |
| 153 | x=new XMLHttpRequest(); |
| 154 | if('json'===opt.responseType){ |
| 155 | /* 'json' is an extension to the supported XHR.responseType |
| 156 | list. We use it as a flag to tell us to JSON.parse() |
| 157 | the response. */ |
| @@ -201,8 +206,17 @@ | |
| 206 | if(undefined!==payload) x.send(payload); |
| 207 | else x.send(); |
| 208 | return this; |
| 209 | }; |
| 210 | |
| 211 | /** |
| 212 | urlTransform() must refer to a function which accepts a relative path |
| 213 | to the same site as fetch() is served from and an optional set of |
| 214 | URL parameters to pass with it (in the form a of a string |
| 215 | ("a=b&c=d...") or an object of key/value pairs (which it converts |
| 216 | to such a string), and returns the resulting URL or URI as a string. |
| 217 | */ |
| 218 | fossil.fetch.urlTransform = (u,p)=>fossil.repoUrl(u,p); |
| 219 | fossil.fetch.beforesend = function(){}; |
| 220 | fossil.fetch.aftersend = function(){}; |
| 221 | fossil.fetch.timeout = 15000/* Default timeout, in ms. */; |
| 222 | })(window.fossil); |
| 223 | |
| 224 | DDED src/fossil.numbered-lines.js |
| --- a/src/fossil.numbered-lines.js | ||
| +++ b/src/fossil.numbered-lines.js | ||
| @@ -0,0 +1,8 @@ | ||
| 1 | +(function callee(arg){ | |
| 2 | + /* | |
| 3 | + JSelse if(!arg){tion callee(arg){ | |
| 4 | + /* | |
| 5 | + JS(function callee(arg){ | |
| 6 | + /* | |
| 7 | + JSfossil', | |
| 8 | + ( |
| --- a/src/fossil.numbered-lines.js | |
| +++ b/src/fossil.numbered-lines.js | |
| @@ -0,0 +1,8 @@ | |
| --- a/src/fossil.numbered-lines.js | |
| +++ b/src/fossil.numbered-lines.js | |
| @@ -0,0 +1,8 @@ | |
| 1 | (function callee(arg){ |
| 2 | /* |
| 3 | JSelse if(!arg){tion callee(arg){ |
| 4 | /* |
| 5 | JS(function callee(arg){ |
| 6 | /* |
| 7 | JSfossil', |
| 8 | ( |
+126
-104
| --- src/fossil.page.fileedit.js | ||
| +++ src/fossil.page.fileedit.js | ||
| @@ -121,11 +121,11 @@ | ||
| 121 | 121 | and that would be horribly inefficient (meaning "battery-consuming" |
| 122 | 122 | on mobile devices). |
| 123 | 123 | */ |
| 124 | 124 | const $stash = { |
| 125 | 125 | keys: { |
| 126 | - index: F.page.name+'/index' | |
| 126 | + index: F.page.name+'.index' | |
| 127 | 127 | }, |
| 128 | 128 | /** |
| 129 | 129 | index: { |
| 130 | 130 | "CHECKIN_HASH:FILENAME": {file info w/o content} |
| 131 | 131 | ... |
| @@ -149,27 +149,12 @@ | ||
| 149 | 149 | /** Returns the index object, fetching it from the stash or creating |
| 150 | 150 | it anew on the first call. */ |
| 151 | 151 | getIndex: function(){ |
| 152 | 152 | if(!this.index){ |
| 153 | 153 | this.index = F.storage.getJSON( |
| 154 | - this.keys.index, undefined | |
| 154 | + this.keys.index, {} | |
| 155 | 155 | ); |
| 156 | - if(!this.index){ | |
| 157 | - /*check for and remove/replace older name. This whole block | |
| 158 | - can be removed once the test phase is done (don't want to | |
| 159 | - invalidate the testers' edits on the test server). When | |
| 160 | - doing so, be sure to replace undefined in the above | |
| 161 | - getJSON() call with {}. */ | |
| 162 | - const oldName = F.page.name+':index'; | |
| 163 | - this.index = F.storage.getJSON(oldName,undefined); | |
| 164 | - if(this.index){ | |
| 165 | - F.storage.remove(oldName); | |
| 166 | - this.storeIndex(); | |
| 167 | - }else{ | |
| 168 | - this.index = {}; | |
| 169 | - } | |
| 170 | - } | |
| 171 | 156 | } |
| 172 | 157 | return this.index; |
| 173 | 158 | }, |
| 174 | 159 | _fireStashEvent: function(){ |
| 175 | 160 | if(this._disableNextEvent) delete this._disableNextEvent; |
| @@ -300,30 +285,44 @@ | ||
| 300 | 285 | this.e.selectCi, |
| 301 | 286 | this.e.selectFiles |
| 302 | 287 | ),"Loading leaves..."); |
| 303 | 288 | D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi); |
| 304 | 289 | const self = this; |
| 305 | - F.fetch('fileedit/filelist',{ | |
| 306 | - urlParams:'leaves', | |
| 307 | - responseType: 'json', | |
| 308 | - onload: function(list){ | |
| 309 | - D.append(D.clearElement(self.e.ciListLabel), | |
| 310 | - "Open leaves (newest first):"); | |
| 311 | - self.cache.checkins = list; | |
| 312 | - D.clearElement(D.enable(self.e.selectCi)); | |
| 313 | - let loadThisOne; | |
| 314 | - list.forEach(function(o,n){ | |
| 315 | - if(!n) loadThisOne = o; | |
| 316 | - self.cache.branchNames[F.hashDigits(o.checkin,true)] = o.branch; | |
| 317 | - D.option(self.e.selectCi, o.checkin, | |
| 318 | - o.timestamp+' ['+o.branch+']: ' | |
| 319 | - +F.hashDigits(o.checkin)); | |
| 320 | - }); | |
| 321 | - F.storage.setJSON(self.cache.branchKey, self.cache.branchNames); | |
| 322 | - self.loadFiles(loadThisOne ? loadThisOne.checkin : false); | |
| 323 | - } | |
| 324 | - }); | |
| 290 | + const onload = function(list){ | |
| 291 | + D.append(D.clearElement(self.e.ciListLabel), | |
| 292 | + "Open leaves (newest first):"); | |
| 293 | + self.cache.checkins = list; | |
| 294 | + D.clearElement(D.enable(self.e.selectCi)); | |
| 295 | + let loadThisOne = P.initialFiles/*possibly injected at page-load time*/; | |
| 296 | + if(loadThisOne){ | |
| 297 | + self.cache.files[loadThisOne.checkin] = loadThisOne; | |
| 298 | + delete P.initialFiles; | |
| 299 | + } | |
| 300 | + list.forEach(function(o,n){ | |
| 301 | + if(!n && !loadThisOne) loadThisOne = o; | |
| 302 | + self.cache.branchNames[F.hashDigits(o.checkin,true)] = o.branch; | |
| 303 | + D.option(self.e.selectCi, o.checkin, | |
| 304 | + o.timestamp+' ['+o.branch+']: ' | |
| 305 | + +F.hashDigits(o.checkin)); | |
| 306 | + }); | |
| 307 | + F.storage.setJSON(self.cache.branchKey, self.cache.branchNames); | |
| 308 | + if(loadThisOne){ | |
| 309 | + self.e.selectCi.value = loadThisOne.checkin; | |
| 310 | + } | |
| 311 | + self.loadFiles(loadThisOne ? loadThisOne.checkin : false); | |
| 312 | + }; | |
| 313 | + if(P.initialLeaves/*injected at page-load time.*/){ | |
| 314 | + const lv = P.initialLeaves; | |
| 315 | + delete P.initialLeaves; | |
| 316 | + onload(lv); | |
| 317 | + }else{ | |
| 318 | + F.fetch('fileedit/filelist',{ | |
| 319 | + urlParams:'leaves', | |
| 320 | + responseType: 'json', | |
| 321 | + onload: onload | |
| 322 | + }); | |
| 323 | + } | |
| 325 | 324 | }, |
| 326 | 325 | /** |
| 327 | 326 | Loads the file list for the given checkin UUID. It uses a |
| 328 | 327 | cached copy on subsequent calls for the same UUID. If passed a |
| 329 | 328 | falsy value, it instead clears and disables the file selection |
| @@ -387,20 +386,20 @@ | ||
| 387 | 386 | Initializes the checkin/file selector widget. Must only be |
| 388 | 387 | called once. |
| 389 | 388 | */ |
| 390 | 389 | init: function(){ |
| 391 | 390 | this.cache.branchNames = F.storage.getJSON(this.cache.branchKey, {}); |
| 392 | - const selCi = this.e.selectCi = D.select(), | |
| 391 | + const selCi = this.e.selectCi = D.addClass(D.select(), 'flex-grow'), | |
| 393 | 392 | selFiles = this.e.selectFiles |
| 394 | 393 | = D.addClass(D.select(), 'file-list'), |
| 395 | 394 | btnLoad = this.e.btnLoadFile = |
| 396 | 395 | D.addClass(D.button("Load file"), "flex-shrink"), |
| 397 | 396 | filesLabel = this.e.fileListLabel = |
| 398 | 397 | D.addClass(D.div(),'flex-shrink','file-list-label'), |
| 399 | 398 | ciLabelWrapper = D.addClass( |
| 400 | 399 | D.div(), 'flex-container','flex-row', 'flex-shrink', |
| 401 | - 'stretch' | |
| 400 | + 'stretch', 'child-gap-small' | |
| 402 | 401 | ), |
| 403 | 402 | btnReload = D.addClass( |
| 404 | 403 | D.button('Reload'), 'flex-shrink' |
| 405 | 404 | ), |
| 406 | 405 | ciLabel = this.e.ciListLabel = |
| @@ -410,34 +409,47 @@ | ||
| 410 | 409 | D.attr(selFiles, 'title', |
| 411 | 410 | "The list of editable files for the selected checkin."); |
| 412 | 411 | D.attr(btnLoad, 'title', |
| 413 | 412 | "Load the selected file into the editor."); |
| 414 | 413 | D.disable(selCi, selFiles, btnLoad); |
| 415 | - D.attr(selFiles, 'size', 10); | |
| 414 | + D.attr(selFiles, 'size', 12); | |
| 416 | 415 | D.append( |
| 417 | 416 | this.e.container, |
| 417 | + ciLabel, | |
| 418 | 418 | D.append(ciLabelWrapper, |
| 419 | - btnReload, ciLabel), | |
| 420 | - selCi, | |
| 419 | + selCi, | |
| 420 | + btnReload), | |
| 421 | 421 | filesLabel, |
| 422 | 422 | selFiles, |
| 423 | 423 | /* Use a wrapper for btnLoad so that the button itself does not |
| 424 | 424 | stretch to fill the parent width: */ |
| 425 | 425 | D.append(D.addClass(D.div(), 'flex-shrink'), btnLoad) |
| 426 | 426 | ); |
| 427 | + if(F.config['fileedit-glob']){ | |
| 428 | + D.append( | |
| 429 | + this.e.container, | |
| 430 | + D.append( | |
| 431 | + D.span(), | |
| 432 | + D.append(D.code(),"fileedit-glob"), | |
| 433 | + " config setting = ", | |
| 434 | + D.append(D.code(), JSON.stringify(F.config['fileedit-glob'])) | |
| 435 | + ) | |
| 436 | + ); | |
| 437 | + } | |
| 438 | + | |
| 427 | 439 | this.loadLeaves(); |
| 428 | 440 | selCi.addEventListener( |
| 429 | 441 | 'change', (e)=>this.loadFiles(e.target.value), false |
| 430 | 442 | ); |
| 431 | - btnLoad.addEventListener( | |
| 432 | - 'click', (e)=>{ | |
| 433 | - this.finfo.filename = selFiles.value; | |
| 434 | - if(this.finfo.filename){ | |
| 435 | - P.loadFile(this.finfo.filename, this.finfo.checkin); | |
| 436 | - } | |
| 437 | - }, false | |
| 438 | - ); | |
| 443 | + const doLoad = (e)=>{ | |
| 444 | + this.finfo.filename = selFiles.value; | |
| 445 | + if(this.finfo.filename){ | |
| 446 | + P.loadFile(this.finfo.filename, this.finfo.checkin); | |
| 447 | + } | |
| 448 | + }; | |
| 449 | + btnLoad.addEventListener('click', doLoad, false); | |
| 450 | + selFiles.addEventListener('dblclick', doLoad, false); | |
| 439 | 451 | btnReload.addEventListener( |
| 440 | 452 | 'click', (e)=>this.loadLeaves(), false |
| 441 | 453 | ); |
| 442 | 454 | delete this.init; |
| 443 | 455 | } |
| @@ -453,11 +465,11 @@ | ||
| 453 | 465 | D.attr(D.div(),'id','fileedit-stash-selector'), |
| 454 | 466 | 'input-with-label' |
| 455 | 467 | ); |
| 456 | 468 | const sel = this.e.select = D.select(); |
| 457 | 469 | const btnClear = this.e.btnClear |
| 458 | - = D.addClass(D.button("Clear"),'hidden'); | |
| 470 | + = D.button("Discard Edits"); | |
| 459 | 471 | D.append(wrapper, "Local edits (", |
| 460 | 472 | D.append(D.code(), |
| 461 | 473 | F.storage.storageImplName()), |
| 462 | 474 | "):", |
| 463 | 475 | sel, btnClear); |
| @@ -472,23 +484,34 @@ | ||
| 472 | 484 | F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail)); |
| 473 | 485 | sel.addEventListener('change',function(e){ |
| 474 | 486 | const opt = this.selectedOptions[0]; |
| 475 | 487 | if(opt && opt._finfo) P.loadFile(opt._finfo); |
| 476 | 488 | }); |
| 477 | - F.confirmer(btnClear, { | |
| 478 | - confirmText: "REALLY delete ALL local edits?", | |
| 479 | - onconfirm: (e)=>P.clearStash().loadFile(/*in case P.finfo() was in the stash*/), | |
| 480 | - ticks: 3 | |
| 481 | - }); | |
| 482 | 489 | if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/ |
| 483 | 490 | D.append(wrapper, D.append( |
| 484 | 491 | D.addClass(D.span(),'warning'), |
| 485 | 492 | "Warning: persistent storage is not available, "+ |
| 486 | 493 | "so uncomitted edits will not survive a page reload." |
| 487 | 494 | )); |
| 488 | 495 | } |
| 489 | 496 | domInsertPoint.parentNode.insertBefore(wrapper, domInsertPoint); |
| 497 | + F.confirmer(btnClear, { | |
| 498 | + /* must come after insertion into the DOM for the pinSize option to work. */ | |
| 499 | + pinSize: true, | |
| 500 | + confirmText: "DISCARD all local edits?", | |
| 501 | + onconfirm: function(e){ | |
| 502 | + if(P.finfo){ | |
| 503 | + const stashed = P.getStashedFinfo(P.finfo); | |
| 504 | + P.clearStash(); | |
| 505 | + if(stashed) P.loadFile(/*reload after discarding edits*/); | |
| 506 | + }else{ | |
| 507 | + P.clearStash(); | |
| 508 | + } | |
| 509 | + }, | |
| 510 | + ticks: F.config.confirmerButtonTicks | |
| 511 | + }); | |
| 512 | + D.addClass(this.e.btnClear,'hidden' /* must not be set until after confirmer is set up!*/); | |
| 490 | 513 | $stash._fireStashEvent(/*read the page-load-time stash*/); |
| 491 | 514 | delete this.init; |
| 492 | 515 | }, |
| 493 | 516 | /** |
| 494 | 517 | Regenerates the edit selection list. |
| @@ -521,20 +544,20 @@ | ||
| 521 | 544 | return; |
| 522 | 545 | } |
| 523 | 546 | D.enable(this.e.select); |
| 524 | 547 | D.removeClass(this.e.btnClear, 'hidden'); |
| 525 | 548 | D.disable(D.option(this.e.select,0,"Select a local edit...")); |
| 526 | - const currentFinfo = theFinfo || P.finfo || {}; | |
| 549 | + const currentFinfo = theFinfo || P.finfo || {filename:''}; | |
| 527 | 550 | ilist.sort(f.compare).forEach(function(finfo,n){ |
| 528 | 551 | const key = stasher.indexKey(finfo), |
| 529 | 552 | branch = finfo.branch |
| 530 | 553 | || P.fileSelectWidget.checkinBranchName(finfo.checkin)||''; |
| 531 | 554 | /* Remember that we don't know the branch name for non-leaf versions |
| 532 | 555 | which P.fileSelectWidget() has never seen/cached. */ |
| 533 | 556 | const opt = D.option( |
| 534 | 557 | self.e.select, n+1/*value is (almost) irrelevant*/, |
| 535 | - [F.hashDigits(finfo.checkin, 6), ' [',branch||'?branch?','] ', | |
| 558 | + [F.hashDigits(finfo.checkin), ' [',branch||'?branch?','] ', | |
| 536 | 559 | f.timestring(new Date(finfo.stashTime)),' ', |
| 537 | 560 | false ? finfo.filename : F.shortenFilename(finfo.filename) |
| 538 | 561 | ].join('') |
| 539 | 562 | ); |
| 540 | 563 | opt._finfo = finfo; |
| @@ -637,11 +660,11 @@ | ||
| 637 | 660 | previewTarget: E('#fileedit-tab-preview-wrapper'), |
| 638 | 661 | manifestTarget: E('#fileedit-manifest'), |
| 639 | 662 | diffTarget: E('#fileedit-tab-diff-wrapper'), |
| 640 | 663 | cbIsExe: E('input[type=checkbox][name=exec_bit]'), |
| 641 | 664 | cbManifest: E('input[type=checkbox][name=include_manifest]'), |
| 642 | - fsFileVersionDetails: E('#file-version-details'), | |
| 665 | + editStatus: E('#fileedit-edit-status'), | |
| 643 | 666 | tabs:{ |
| 644 | 667 | content: E('#fileedit-tab-content'), |
| 645 | 668 | preview: E('#fileedit-tab-preview'), |
| 646 | 669 | diff: E('#fileedit-tab-diff'), |
| 647 | 670 | commit: E('#fileedit-tab-commit'), |
| @@ -659,24 +682,24 @@ | ||
| 659 | 682 | }else{ |
| 660 | 683 | P.e.taComment = P.e.taCommentSmall; |
| 661 | 684 | D.addClass(P.e.taCommentBig, 'hidden'); |
| 662 | 685 | } |
| 663 | 686 | D.removeClass(P.e.taComment, 'hidden'); |
| 664 | - | |
| 665 | 687 | P.tabs.e.container.insertBefore( |
| 666 | 688 | /* Move the status bar between the tab buttons and |
| 667 | 689 | tab panels. Seems to be the best fit in terms of |
| 668 | 690 | functionality and visibility. */ |
| 669 | 691 | E('#fossil-status-bar'), P.tabs.e.tabs |
| 670 | 692 | ); |
| 693 | + P.tabs.e.container.insertBefore(P.e.editStatus, P.tabs.e.tabs); | |
| 671 | 694 | |
| 672 | 695 | P.tabs.addEventListener( |
| 673 | 696 | /* Set up auto-refresh of the preview tab... */ |
| 674 | 697 | 'before-switch-to', function(ev){ |
| 675 | 698 | if(ev.detail===P.e.tabs.preview){ |
| 676 | 699 | P.baseHrefForFile(); |
| 677 | - if(P.e.cbAutoPreview.checked) P.preview(); | |
| 700 | + if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview(); | |
| 678 | 701 | }else if(ev.detail===P.e.tabs.diff){ |
| 679 | 702 | /* Work around a weird bug where the page gets wider than |
| 680 | 703 | the window when the diff tab is NOT in view and the |
| 681 | 704 | current SBS diff widget is wider than the window. When |
| 682 | 705 | the diff IS in view then CSS overflow magically reduces |
| @@ -715,13 +738,14 @@ | ||
| 715 | 738 | ); |
| 716 | 739 | P.e.btnCommit.addEventListener( |
| 717 | 740 | "click",(e)=>P.commit(), false |
| 718 | 741 | ); |
| 719 | 742 | F.confirmer(P.e.btnReload, { |
| 743 | + pinSize: true, | |
| 720 | 744 | confirmText: "Really reload, losing edits?", |
| 721 | 745 | onconfirm: (e)=>P.unstashContent().loadFile(), |
| 722 | - ticks: 3 | |
| 746 | + ticks: F.config.confirmerButtonTicks | |
| 723 | 747 | }); |
| 724 | 748 | E('#comment-toggle').addEventListener( |
| 725 | 749 | "click",(e)=>P.toggleCommentMode(), false |
| 726 | 750 | ); |
| 727 | 751 | |
| @@ -774,11 +798,14 @@ | ||
| 774 | 798 | } |
| 775 | 799 | |
| 776 | 800 | P.addEventListener( |
| 777 | 801 | // Clear certain views when new content is loaded/set |
| 778 | 802 | 'fileedit-content-replaced', |
| 779 | - ()=>D.clearElement(P.e.diffTarget, P.e.previewTarget, P.e.manifestTarget) | |
| 803 | + ()=>{ | |
| 804 | + P.previewNeedsUpdate = true; | |
| 805 | + D.clearElement(P.e.diffTarget, P.e.previewTarget, P.e.manifestTarget); | |
| 806 | + } | |
| 780 | 807 | ); |
| 781 | 808 | P.addEventListener( |
| 782 | 809 | // Clear certain views after a non-dry-run commit |
| 783 | 810 | 'fileedit-committed', |
| 784 | 811 | (e)=>{ |
| @@ -789,11 +816,10 @@ | ||
| 789 | 816 | ); |
| 790 | 817 | |
| 791 | 818 | P.fileSelectWidget.init(); |
| 792 | 819 | P.stashWidget.init( |
| 793 | 820 | P.e.tabs.content.lastElementChild |
| 794 | - //P.e.tabs.fileSelect.querySelector("h1") | |
| 795 | 821 | ); |
| 796 | 822 | }/*F.onPageLoad()*/); |
| 797 | 823 | |
| 798 | 824 | /** |
| 799 | 825 | Getter (if called with no args) or setter (if passed an arg) for |
| @@ -819,11 +845,11 @@ | ||
| 819 | 845 | For use when installing a custom editor widget. Pass it the |
| 820 | 846 | getter and setter callbacks to fetch resp. set the content of the |
| 821 | 847 | custom widget. They will be triggered via |
| 822 | 848 | P.fileContent(). Returns this object. |
| 823 | 849 | */ |
| 824 | - P.setFileContentMethods = function(getter, setter){ | |
| 850 | + P.setContentMethods = function(getter, setter){ | |
| 825 | 851 | this.fileContent.get = getter; |
| 826 | 852 | this.fileContent.set = setter; |
| 827 | 853 | return this; |
| 828 | 854 | }; |
| 829 | 855 | |
| @@ -929,63 +955,55 @@ | ||
| 929 | 955 | updateVersion() updates the filename and version in various UI |
| 930 | 956 | elements... |
| 931 | 957 | |
| 932 | 958 | Returns this object. |
| 933 | 959 | */ |
| 934 | - P.updateVersion = function(file,rev){ | |
| 960 | + P.updateVersion = function f(file,rev){ | |
| 961 | + if(!f.eLinks){ | |
| 962 | + f.eName = P.e.editStatus.querySelector('span.name'); | |
| 963 | + f.eLinks = P.e.editStatus.querySelector('span.links'); | |
| 964 | + } | |
| 935 | 965 | if(1===arguments.length){/*assume object*/ |
| 936 | 966 | this.finfo = arguments[0]; |
| 937 | 967 | file = this.finfo.filename; |
| 938 | 968 | rev = this.finfo.checkin; |
| 939 | 969 | }else if(0===arguments.length){ |
| 940 | - if(!affirmHasFile()) return this; | |
| 941 | - file = this.finfo.filename; | |
| 942 | - rev = this.finfo.checkin; | |
| 970 | + if(affirmHasFile()){ | |
| 971 | + file = this.finfo.filename; | |
| 972 | + rev = this.finfo.checkin; | |
| 973 | + } | |
| 943 | 974 | }else{ |
| 944 | 975 | this.finfo = {filename:file,checkin:rev}; |
| 945 | 976 | } |
| 946 | - const eTgt = this.e.fsFileVersionDetails.querySelector('div'), | |
| 947 | - rHuman = F.hashDigits(rev), | |
| 948 | - rUrl = F.hashDigits(rev,true); | |
| 949 | - D.clearElement(eTgt); | |
| 950 | - D.append( | |
| 951 | - eTgt, "File: ", | |
| 952 | - D.append(D.code(), | |
| 953 | - D.a(F.repoUrl('finfo',{name:file, m:rUrl}), file)), | |
| 954 | - D.br() | |
| 955 | - ); | |
| 956 | - D.append( | |
| 957 | - eTgt, "Checkin: ", | |
| 958 | - D.append(D.code(), D.a(F.repoUrl('info/'+rUrl), rHuman)), | |
| 959 | - " [",D.a(F.repoUrl('timeline',{m:rUrl}), "timeline"),"]", | |
| 960 | - D.br() | |
| 961 | - ); | |
| 962 | - D.append( | |
| 963 | - eTgt, "Mimetype: ", | |
| 964 | - D.append(D.code(), this.finfo.mimetype||'???'), | |
| 965 | - D.br() | |
| 966 | - ); | |
| 967 | - D.append( | |
| 968 | - eTgt, | |
| 969 | - D.append(D.code(), "[", | |
| 970 | - D.a(F.repoUrl('annotate',{filename:file, checkin:rUrl}), | |
| 971 | - 'annotate'), "]"), | |
| 972 | - D.append(D.code(), "[", | |
| 973 | - D.a(F.repoUrl('blame',{filename:file, checkin:rUrl}), | |
| 974 | - 'blame'), "]") | |
| 977 | + const fi = this.finfo; | |
| 978 | + D.clearElement(f.eName, f.eLinks); | |
| 979 | + if(!fi){ | |
| 980 | + D.append(f.eName, '(no file loaded)'); | |
| 981 | + return this; | |
| 982 | + } | |
| 983 | + const rHuman = F.hashDigits(rev), | |
| 984 | + rUrl = F.hashDigits(rev,true); | |
| 985 | + | |
| 986 | + //TODO? port over is-edited marker from /wikiedit | |
| 987 | + //var marker = getEditMarker(wi, false); | |
| 988 | + D.append(f.eName/*,marker*/,D.a(F.repoUrl('finfo',{name:file, m:rUrl}), file)); | |
| 989 | + | |
| 990 | + D.append( | |
| 991 | + f.eLinks, | |
| 992 | + D.append(D.span(), fi.mimetype||'?mimetype?'), | |
| 993 | + D.a(F.repoUrl('info/'+rUrl), rHuman), | |
| 994 | + D.a(F.repoUrl('timeline',{m:rUrl}), "timeline"), | |
| 995 | + D.a(F.repoUrl('annotate',{filename:file, checkin:rUrl}),'annotate'), | |
| 996 | + D.a(F.repoUrl('blame',{filename:file, checkin:rUrl}),'blame') | |
| 975 | 997 | ); |
| 976 | 998 | const purlArgs = F.encodeUrlArgs({ |
| 977 | 999 | filename: this.finfo.filename, |
| 978 | 1000 | checkin: rUrl |
| 979 | 1001 | },false,true); |
| 980 | 1002 | const purl = F.repoUrl('fileedit',purlArgs); |
| 981 | - D.append( | |
| 982 | - eTgt, | |
| 983 | - D.append(D.code(), | |
| 984 | - "[",D.a(purl,"Editor permalink"),"]") | |
| 985 | - ); | |
| 986 | - this.setPageTitle("Edit: "+this.finfo.filename); | |
| 1003 | + D.append( f.eLinks, D.a(purl,"editor permalink") ); | |
| 1004 | + this.setPageTitle("Edit: "+fi.filename); | |
| 987 | 1005 | return this; |
| 988 | 1006 | }; |
| 989 | 1007 | |
| 990 | 1008 | /** |
| 991 | 1009 | loadFile() loads (file,checkinVersion) and updates the relevant |
| @@ -1032,10 +1050,11 @@ | ||
| 1032 | 1050 | mimetype: headers['content-type'].split(';').shift() |
| 1033 | 1051 | }); |
| 1034 | 1052 | self.tabs.switchToTab(self.e.tabs.content); |
| 1035 | 1053 | self.e.cbIsExe.checked = self.finfo.isExe; |
| 1036 | 1054 | self.fileContent(r); |
| 1055 | + P.previewNeedsUpdate = true; | |
| 1037 | 1056 | self.dispatchEvent('fileedit-file-loaded', self.finfo); |
| 1038 | 1057 | }; |
| 1039 | 1058 | const semiFinfo = {filename: file, checkin: rev}; |
| 1040 | 1059 | const stashFinfo = this.getStashedFinfo(semiFinfo); |
| 1041 | 1060 | if(stashFinfo){ // fake a response from the stash... |
| @@ -1115,10 +1134,11 @@ | ||
| 1115 | 1134 | P.selectPreviewMode(P.previewModes[header]); |
| 1116 | 1135 | if('wiki'===header) P.baseHrefForFile(); |
| 1117 | 1136 | else P.baseHrefRestore(); |
| 1118 | 1137 | callback(r); |
| 1119 | 1138 | F.message('Updated preview.'); |
| 1139 | + P.previewNeedsUpdate = false; | |
| 1120 | 1140 | P.dispatchEvent('fileedit-preview-updated',{ |
| 1121 | 1141 | previewMode: P.previewModes.current, |
| 1122 | 1142 | mimetype: P.finfo.mimetype, |
| 1123 | 1143 | element: P.e.previewTarget |
| 1124 | 1144 | }); |
| @@ -1290,10 +1310,11 @@ | ||
| 1290 | 1310 | }else{ |
| 1291 | 1311 | $stash.updateFile(fi, P.fileContent()); |
| 1292 | 1312 | } |
| 1293 | 1313 | F.message("Stashed change to",F.hashDigits(fi.checkin),fi.filename); |
| 1294 | 1314 | $stash.prune(); |
| 1315 | + this.previewNeedsUpdate = true; | |
| 1295 | 1316 | } |
| 1296 | 1317 | return this; |
| 1297 | 1318 | }; |
| 1298 | 1319 | |
| 1299 | 1320 | /** |
| @@ -1301,10 +1322,11 @@ | ||
| 1301 | 1322 | F.storage. Returns this. |
| 1302 | 1323 | */ |
| 1303 | 1324 | P.unstashContent = function(){ |
| 1304 | 1325 | const finfo = arguments[0] || this.finfo; |
| 1305 | 1326 | if(finfo){ |
| 1327 | + this.previewNeedsUpdate = true; | |
| 1306 | 1328 | $stash.unstash(finfo); |
| 1307 | 1329 | //console.debug("Unstashed",finfo); |
| 1308 | 1330 | F.message("Unstashed",F.hashDigits(finfo.checkin),finfo.filename); |
| 1309 | 1331 | } |
| 1310 | 1332 | return this; |
| 1311 | 1333 |
| --- src/fossil.page.fileedit.js | |
| +++ src/fossil.page.fileedit.js | |
| @@ -121,11 +121,11 @@ | |
| 121 | and that would be horribly inefficient (meaning "battery-consuming" |
| 122 | on mobile devices). |
| 123 | */ |
| 124 | const $stash = { |
| 125 | keys: { |
| 126 | index: F.page.name+'/index' |
| 127 | }, |
| 128 | /** |
| 129 | index: { |
| 130 | "CHECKIN_HASH:FILENAME": {file info w/o content} |
| 131 | ... |
| @@ -149,27 +149,12 @@ | |
| 149 | /** Returns the index object, fetching it from the stash or creating |
| 150 | it anew on the first call. */ |
| 151 | getIndex: function(){ |
| 152 | if(!this.index){ |
| 153 | this.index = F.storage.getJSON( |
| 154 | this.keys.index, undefined |
| 155 | ); |
| 156 | if(!this.index){ |
| 157 | /*check for and remove/replace older name. This whole block |
| 158 | can be removed once the test phase is done (don't want to |
| 159 | invalidate the testers' edits on the test server). When |
| 160 | doing so, be sure to replace undefined in the above |
| 161 | getJSON() call with {}. */ |
| 162 | const oldName = F.page.name+':index'; |
| 163 | this.index = F.storage.getJSON(oldName,undefined); |
| 164 | if(this.index){ |
| 165 | F.storage.remove(oldName); |
| 166 | this.storeIndex(); |
| 167 | }else{ |
| 168 | this.index = {}; |
| 169 | } |
| 170 | } |
| 171 | } |
| 172 | return this.index; |
| 173 | }, |
| 174 | _fireStashEvent: function(){ |
| 175 | if(this._disableNextEvent) delete this._disableNextEvent; |
| @@ -300,30 +285,44 @@ | |
| 300 | this.e.selectCi, |
| 301 | this.e.selectFiles |
| 302 | ),"Loading leaves..."); |
| 303 | D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi); |
| 304 | const self = this; |
| 305 | F.fetch('fileedit/filelist',{ |
| 306 | urlParams:'leaves', |
| 307 | responseType: 'json', |
| 308 | onload: function(list){ |
| 309 | D.append(D.clearElement(self.e.ciListLabel), |
| 310 | "Open leaves (newest first):"); |
| 311 | self.cache.checkins = list; |
| 312 | D.clearElement(D.enable(self.e.selectCi)); |
| 313 | let loadThisOne; |
| 314 | list.forEach(function(o,n){ |
| 315 | if(!n) loadThisOne = o; |
| 316 | self.cache.branchNames[F.hashDigits(o.checkin,true)] = o.branch; |
| 317 | D.option(self.e.selectCi, o.checkin, |
| 318 | o.timestamp+' ['+o.branch+']: ' |
| 319 | +F.hashDigits(o.checkin)); |
| 320 | }); |
| 321 | F.storage.setJSON(self.cache.branchKey, self.cache.branchNames); |
| 322 | self.loadFiles(loadThisOne ? loadThisOne.checkin : false); |
| 323 | } |
| 324 | }); |
| 325 | }, |
| 326 | /** |
| 327 | Loads the file list for the given checkin UUID. It uses a |
| 328 | cached copy on subsequent calls for the same UUID. If passed a |
| 329 | falsy value, it instead clears and disables the file selection |
| @@ -387,20 +386,20 @@ | |
| 387 | Initializes the checkin/file selector widget. Must only be |
| 388 | called once. |
| 389 | */ |
| 390 | init: function(){ |
| 391 | this.cache.branchNames = F.storage.getJSON(this.cache.branchKey, {}); |
| 392 | const selCi = this.e.selectCi = D.select(), |
| 393 | selFiles = this.e.selectFiles |
| 394 | = D.addClass(D.select(), 'file-list'), |
| 395 | btnLoad = this.e.btnLoadFile = |
| 396 | D.addClass(D.button("Load file"), "flex-shrink"), |
| 397 | filesLabel = this.e.fileListLabel = |
| 398 | D.addClass(D.div(),'flex-shrink','file-list-label'), |
| 399 | ciLabelWrapper = D.addClass( |
| 400 | D.div(), 'flex-container','flex-row', 'flex-shrink', |
| 401 | 'stretch' |
| 402 | ), |
| 403 | btnReload = D.addClass( |
| 404 | D.button('Reload'), 'flex-shrink' |
| 405 | ), |
| 406 | ciLabel = this.e.ciListLabel = |
| @@ -410,34 +409,47 @@ | |
| 410 | D.attr(selFiles, 'title', |
| 411 | "The list of editable files for the selected checkin."); |
| 412 | D.attr(btnLoad, 'title', |
| 413 | "Load the selected file into the editor."); |
| 414 | D.disable(selCi, selFiles, btnLoad); |
| 415 | D.attr(selFiles, 'size', 10); |
| 416 | D.append( |
| 417 | this.e.container, |
| 418 | D.append(ciLabelWrapper, |
| 419 | btnReload, ciLabel), |
| 420 | selCi, |
| 421 | filesLabel, |
| 422 | selFiles, |
| 423 | /* Use a wrapper for btnLoad so that the button itself does not |
| 424 | stretch to fill the parent width: */ |
| 425 | D.append(D.addClass(D.div(), 'flex-shrink'), btnLoad) |
| 426 | ); |
| 427 | this.loadLeaves(); |
| 428 | selCi.addEventListener( |
| 429 | 'change', (e)=>this.loadFiles(e.target.value), false |
| 430 | ); |
| 431 | btnLoad.addEventListener( |
| 432 | 'click', (e)=>{ |
| 433 | this.finfo.filename = selFiles.value; |
| 434 | if(this.finfo.filename){ |
| 435 | P.loadFile(this.finfo.filename, this.finfo.checkin); |
| 436 | } |
| 437 | }, false |
| 438 | ); |
| 439 | btnReload.addEventListener( |
| 440 | 'click', (e)=>this.loadLeaves(), false |
| 441 | ); |
| 442 | delete this.init; |
| 443 | } |
| @@ -453,11 +465,11 @@ | |
| 453 | D.attr(D.div(),'id','fileedit-stash-selector'), |
| 454 | 'input-with-label' |
| 455 | ); |
| 456 | const sel = this.e.select = D.select(); |
| 457 | const btnClear = this.e.btnClear |
| 458 | = D.addClass(D.button("Clear"),'hidden'); |
| 459 | D.append(wrapper, "Local edits (", |
| 460 | D.append(D.code(), |
| 461 | F.storage.storageImplName()), |
| 462 | "):", |
| 463 | sel, btnClear); |
| @@ -472,23 +484,34 @@ | |
| 472 | F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail)); |
| 473 | sel.addEventListener('change',function(e){ |
| 474 | const opt = this.selectedOptions[0]; |
| 475 | if(opt && opt._finfo) P.loadFile(opt._finfo); |
| 476 | }); |
| 477 | F.confirmer(btnClear, { |
| 478 | confirmText: "REALLY delete ALL local edits?", |
| 479 | onconfirm: (e)=>P.clearStash().loadFile(/*in case P.finfo() was in the stash*/), |
| 480 | ticks: 3 |
| 481 | }); |
| 482 | if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/ |
| 483 | D.append(wrapper, D.append( |
| 484 | D.addClass(D.span(),'warning'), |
| 485 | "Warning: persistent storage is not available, "+ |
| 486 | "so uncomitted edits will not survive a page reload." |
| 487 | )); |
| 488 | } |
| 489 | domInsertPoint.parentNode.insertBefore(wrapper, domInsertPoint); |
| 490 | $stash._fireStashEvent(/*read the page-load-time stash*/); |
| 491 | delete this.init; |
| 492 | }, |
| 493 | /** |
| 494 | Regenerates the edit selection list. |
| @@ -521,20 +544,20 @@ | |
| 521 | return; |
| 522 | } |
| 523 | D.enable(this.e.select); |
| 524 | D.removeClass(this.e.btnClear, 'hidden'); |
| 525 | D.disable(D.option(this.e.select,0,"Select a local edit...")); |
| 526 | const currentFinfo = theFinfo || P.finfo || {}; |
| 527 | ilist.sort(f.compare).forEach(function(finfo,n){ |
| 528 | const key = stasher.indexKey(finfo), |
| 529 | branch = finfo.branch |
| 530 | || P.fileSelectWidget.checkinBranchName(finfo.checkin)||''; |
| 531 | /* Remember that we don't know the branch name for non-leaf versions |
| 532 | which P.fileSelectWidget() has never seen/cached. */ |
| 533 | const opt = D.option( |
| 534 | self.e.select, n+1/*value is (almost) irrelevant*/, |
| 535 | [F.hashDigits(finfo.checkin, 6), ' [',branch||'?branch?','] ', |
| 536 | f.timestring(new Date(finfo.stashTime)),' ', |
| 537 | false ? finfo.filename : F.shortenFilename(finfo.filename) |
| 538 | ].join('') |
| 539 | ); |
| 540 | opt._finfo = finfo; |
| @@ -637,11 +660,11 @@ | |
| 637 | previewTarget: E('#fileedit-tab-preview-wrapper'), |
| 638 | manifestTarget: E('#fileedit-manifest'), |
| 639 | diffTarget: E('#fileedit-tab-diff-wrapper'), |
| 640 | cbIsExe: E('input[type=checkbox][name=exec_bit]'), |
| 641 | cbManifest: E('input[type=checkbox][name=include_manifest]'), |
| 642 | fsFileVersionDetails: E('#file-version-details'), |
| 643 | tabs:{ |
| 644 | content: E('#fileedit-tab-content'), |
| 645 | preview: E('#fileedit-tab-preview'), |
| 646 | diff: E('#fileedit-tab-diff'), |
| 647 | commit: E('#fileedit-tab-commit'), |
| @@ -659,24 +682,24 @@ | |
| 659 | }else{ |
| 660 | P.e.taComment = P.e.taCommentSmall; |
| 661 | D.addClass(P.e.taCommentBig, 'hidden'); |
| 662 | } |
| 663 | D.removeClass(P.e.taComment, 'hidden'); |
| 664 | |
| 665 | P.tabs.e.container.insertBefore( |
| 666 | /* Move the status bar between the tab buttons and |
| 667 | tab panels. Seems to be the best fit in terms of |
| 668 | functionality and visibility. */ |
| 669 | E('#fossil-status-bar'), P.tabs.e.tabs |
| 670 | ); |
| 671 | |
| 672 | P.tabs.addEventListener( |
| 673 | /* Set up auto-refresh of the preview tab... */ |
| 674 | 'before-switch-to', function(ev){ |
| 675 | if(ev.detail===P.e.tabs.preview){ |
| 676 | P.baseHrefForFile(); |
| 677 | if(P.e.cbAutoPreview.checked) P.preview(); |
| 678 | }else if(ev.detail===P.e.tabs.diff){ |
| 679 | /* Work around a weird bug where the page gets wider than |
| 680 | the window when the diff tab is NOT in view and the |
| 681 | current SBS diff widget is wider than the window. When |
| 682 | the diff IS in view then CSS overflow magically reduces |
| @@ -715,13 +738,14 @@ | |
| 715 | ); |
| 716 | P.e.btnCommit.addEventListener( |
| 717 | "click",(e)=>P.commit(), false |
| 718 | ); |
| 719 | F.confirmer(P.e.btnReload, { |
| 720 | confirmText: "Really reload, losing edits?", |
| 721 | onconfirm: (e)=>P.unstashContent().loadFile(), |
| 722 | ticks: 3 |
| 723 | }); |
| 724 | E('#comment-toggle').addEventListener( |
| 725 | "click",(e)=>P.toggleCommentMode(), false |
| 726 | ); |
| 727 | |
| @@ -774,11 +798,14 @@ | |
| 774 | } |
| 775 | |
| 776 | P.addEventListener( |
| 777 | // Clear certain views when new content is loaded/set |
| 778 | 'fileedit-content-replaced', |
| 779 | ()=>D.clearElement(P.e.diffTarget, P.e.previewTarget, P.e.manifestTarget) |
| 780 | ); |
| 781 | P.addEventListener( |
| 782 | // Clear certain views after a non-dry-run commit |
| 783 | 'fileedit-committed', |
| 784 | (e)=>{ |
| @@ -789,11 +816,10 @@ | |
| 789 | ); |
| 790 | |
| 791 | P.fileSelectWidget.init(); |
| 792 | P.stashWidget.init( |
| 793 | P.e.tabs.content.lastElementChild |
| 794 | //P.e.tabs.fileSelect.querySelector("h1") |
| 795 | ); |
| 796 | }/*F.onPageLoad()*/); |
| 797 | |
| 798 | /** |
| 799 | Getter (if called with no args) or setter (if passed an arg) for |
| @@ -819,11 +845,11 @@ | |
| 819 | For use when installing a custom editor widget. Pass it the |
| 820 | getter and setter callbacks to fetch resp. set the content of the |
| 821 | custom widget. They will be triggered via |
| 822 | P.fileContent(). Returns this object. |
| 823 | */ |
| 824 | P.setFileContentMethods = function(getter, setter){ |
| 825 | this.fileContent.get = getter; |
| 826 | this.fileContent.set = setter; |
| 827 | return this; |
| 828 | }; |
| 829 | |
| @@ -929,63 +955,55 @@ | |
| 929 | updateVersion() updates the filename and version in various UI |
| 930 | elements... |
| 931 | |
| 932 | Returns this object. |
| 933 | */ |
| 934 | P.updateVersion = function(file,rev){ |
| 935 | if(1===arguments.length){/*assume object*/ |
| 936 | this.finfo = arguments[0]; |
| 937 | file = this.finfo.filename; |
| 938 | rev = this.finfo.checkin; |
| 939 | }else if(0===arguments.length){ |
| 940 | if(!affirmHasFile()) return this; |
| 941 | file = this.finfo.filename; |
| 942 | rev = this.finfo.checkin; |
| 943 | }else{ |
| 944 | this.finfo = {filename:file,checkin:rev}; |
| 945 | } |
| 946 | const eTgt = this.e.fsFileVersionDetails.querySelector('div'), |
| 947 | rHuman = F.hashDigits(rev), |
| 948 | rUrl = F.hashDigits(rev,true); |
| 949 | D.clearElement(eTgt); |
| 950 | D.append( |
| 951 | eTgt, "File: ", |
| 952 | D.append(D.code(), |
| 953 | D.a(F.repoUrl('finfo',{name:file, m:rUrl}), file)), |
| 954 | D.br() |
| 955 | ); |
| 956 | D.append( |
| 957 | eTgt, "Checkin: ", |
| 958 | D.append(D.code(), D.a(F.repoUrl('info/'+rUrl), rHuman)), |
| 959 | " [",D.a(F.repoUrl('timeline',{m:rUrl}), "timeline"),"]", |
| 960 | D.br() |
| 961 | ); |
| 962 | D.append( |
| 963 | eTgt, "Mimetype: ", |
| 964 | D.append(D.code(), this.finfo.mimetype||'???'), |
| 965 | D.br() |
| 966 | ); |
| 967 | D.append( |
| 968 | eTgt, |
| 969 | D.append(D.code(), "[", |
| 970 | D.a(F.repoUrl('annotate',{filename:file, checkin:rUrl}), |
| 971 | 'annotate'), "]"), |
| 972 | D.append(D.code(), "[", |
| 973 | D.a(F.repoUrl('blame',{filename:file, checkin:rUrl}), |
| 974 | 'blame'), "]") |
| 975 | ); |
| 976 | const purlArgs = F.encodeUrlArgs({ |
| 977 | filename: this.finfo.filename, |
| 978 | checkin: rUrl |
| 979 | },false,true); |
| 980 | const purl = F.repoUrl('fileedit',purlArgs); |
| 981 | D.append( |
| 982 | eTgt, |
| 983 | D.append(D.code(), |
| 984 | "[",D.a(purl,"Editor permalink"),"]") |
| 985 | ); |
| 986 | this.setPageTitle("Edit: "+this.finfo.filename); |
| 987 | return this; |
| 988 | }; |
| 989 | |
| 990 | /** |
| 991 | loadFile() loads (file,checkinVersion) and updates the relevant |
| @@ -1032,10 +1050,11 @@ | |
| 1032 | mimetype: headers['content-type'].split(';').shift() |
| 1033 | }); |
| 1034 | self.tabs.switchToTab(self.e.tabs.content); |
| 1035 | self.e.cbIsExe.checked = self.finfo.isExe; |
| 1036 | self.fileContent(r); |
| 1037 | self.dispatchEvent('fileedit-file-loaded', self.finfo); |
| 1038 | }; |
| 1039 | const semiFinfo = {filename: file, checkin: rev}; |
| 1040 | const stashFinfo = this.getStashedFinfo(semiFinfo); |
| 1041 | if(stashFinfo){ // fake a response from the stash... |
| @@ -1115,10 +1134,11 @@ | |
| 1115 | P.selectPreviewMode(P.previewModes[header]); |
| 1116 | if('wiki'===header) P.baseHrefForFile(); |
| 1117 | else P.baseHrefRestore(); |
| 1118 | callback(r); |
| 1119 | F.message('Updated preview.'); |
| 1120 | P.dispatchEvent('fileedit-preview-updated',{ |
| 1121 | previewMode: P.previewModes.current, |
| 1122 | mimetype: P.finfo.mimetype, |
| 1123 | element: P.e.previewTarget |
| 1124 | }); |
| @@ -1290,10 +1310,11 @@ | |
| 1290 | }else{ |
| 1291 | $stash.updateFile(fi, P.fileContent()); |
| 1292 | } |
| 1293 | F.message("Stashed change to",F.hashDigits(fi.checkin),fi.filename); |
| 1294 | $stash.prune(); |
| 1295 | } |
| 1296 | return this; |
| 1297 | }; |
| 1298 | |
| 1299 | /** |
| @@ -1301,10 +1322,11 @@ | |
| 1301 | F.storage. Returns this. |
| 1302 | */ |
| 1303 | P.unstashContent = function(){ |
| 1304 | const finfo = arguments[0] || this.finfo; |
| 1305 | if(finfo){ |
| 1306 | $stash.unstash(finfo); |
| 1307 | //console.debug("Unstashed",finfo); |
| 1308 | F.message("Unstashed",F.hashDigits(finfo.checkin),finfo.filename); |
| 1309 | } |
| 1310 | return this; |
| 1311 |
| --- src/fossil.page.fileedit.js | |
| +++ src/fossil.page.fileedit.js | |
| @@ -121,11 +121,11 @@ | |
| 121 | and that would be horribly inefficient (meaning "battery-consuming" |
| 122 | on mobile devices). |
| 123 | */ |
| 124 | const $stash = { |
| 125 | keys: { |
| 126 | index: F.page.name+'.index' |
| 127 | }, |
| 128 | /** |
| 129 | index: { |
| 130 | "CHECKIN_HASH:FILENAME": {file info w/o content} |
| 131 | ... |
| @@ -149,27 +149,12 @@ | |
| 149 | /** Returns the index object, fetching it from the stash or creating |
| 150 | it anew on the first call. */ |
| 151 | getIndex: function(){ |
| 152 | if(!this.index){ |
| 153 | this.index = F.storage.getJSON( |
| 154 | this.keys.index, {} |
| 155 | ); |
| 156 | } |
| 157 | return this.index; |
| 158 | }, |
| 159 | _fireStashEvent: function(){ |
| 160 | if(this._disableNextEvent) delete this._disableNextEvent; |
| @@ -300,30 +285,44 @@ | |
| 285 | this.e.selectCi, |
| 286 | this.e.selectFiles |
| 287 | ),"Loading leaves..."); |
| 288 | D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi); |
| 289 | const self = this; |
| 290 | const onload = function(list){ |
| 291 | D.append(D.clearElement(self.e.ciListLabel), |
| 292 | "Open leaves (newest first):"); |
| 293 | self.cache.checkins = list; |
| 294 | D.clearElement(D.enable(self.e.selectCi)); |
| 295 | let loadThisOne = P.initialFiles/*possibly injected at page-load time*/; |
| 296 | if(loadThisOne){ |
| 297 | self.cache.files[loadThisOne.checkin] = loadThisOne; |
| 298 | delete P.initialFiles; |
| 299 | } |
| 300 | list.forEach(function(o,n){ |
| 301 | if(!n && !loadThisOne) loadThisOne = o; |
| 302 | self.cache.branchNames[F.hashDigits(o.checkin,true)] = o.branch; |
| 303 | D.option(self.e.selectCi, o.checkin, |
| 304 | o.timestamp+' ['+o.branch+']: ' |
| 305 | +F.hashDigits(o.checkin)); |
| 306 | }); |
| 307 | F.storage.setJSON(self.cache.branchKey, self.cache.branchNames); |
| 308 | if(loadThisOne){ |
| 309 | self.e.selectCi.value = loadThisOne.checkin; |
| 310 | } |
| 311 | self.loadFiles(loadThisOne ? loadThisOne.checkin : false); |
| 312 | }; |
| 313 | if(P.initialLeaves/*injected at page-load time.*/){ |
| 314 | const lv = P.initialLeaves; |
| 315 | delete P.initialLeaves; |
| 316 | onload(lv); |
| 317 | }else{ |
| 318 | F.fetch('fileedit/filelist',{ |
| 319 | urlParams:'leaves', |
| 320 | responseType: 'json', |
| 321 | onload: onload |
| 322 | }); |
| 323 | } |
| 324 | }, |
| 325 | /** |
| 326 | Loads the file list for the given checkin UUID. It uses a |
| 327 | cached copy on subsequent calls for the same UUID. If passed a |
| 328 | falsy value, it instead clears and disables the file selection |
| @@ -387,20 +386,20 @@ | |
| 386 | Initializes the checkin/file selector widget. Must only be |
| 387 | called once. |
| 388 | */ |
| 389 | init: function(){ |
| 390 | this.cache.branchNames = F.storage.getJSON(this.cache.branchKey, {}); |
| 391 | const selCi = this.e.selectCi = D.addClass(D.select(), 'flex-grow'), |
| 392 | selFiles = this.e.selectFiles |
| 393 | = D.addClass(D.select(), 'file-list'), |
| 394 | btnLoad = this.e.btnLoadFile = |
| 395 | D.addClass(D.button("Load file"), "flex-shrink"), |
| 396 | filesLabel = this.e.fileListLabel = |
| 397 | D.addClass(D.div(),'flex-shrink','file-list-label'), |
| 398 | ciLabelWrapper = D.addClass( |
| 399 | D.div(), 'flex-container','flex-row', 'flex-shrink', |
| 400 | 'stretch', 'child-gap-small' |
| 401 | ), |
| 402 | btnReload = D.addClass( |
| 403 | D.button('Reload'), 'flex-shrink' |
| 404 | ), |
| 405 | ciLabel = this.e.ciListLabel = |
| @@ -410,34 +409,47 @@ | |
| 409 | D.attr(selFiles, 'title', |
| 410 | "The list of editable files for the selected checkin."); |
| 411 | D.attr(btnLoad, 'title', |
| 412 | "Load the selected file into the editor."); |
| 413 | D.disable(selCi, selFiles, btnLoad); |
| 414 | D.attr(selFiles, 'size', 12); |
| 415 | D.append( |
| 416 | this.e.container, |
| 417 | ciLabel, |
| 418 | D.append(ciLabelWrapper, |
| 419 | selCi, |
| 420 | btnReload), |
| 421 | filesLabel, |
| 422 | selFiles, |
| 423 | /* Use a wrapper for btnLoad so that the button itself does not |
| 424 | stretch to fill the parent width: */ |
| 425 | D.append(D.addClass(D.div(), 'flex-shrink'), btnLoad) |
| 426 | ); |
| 427 | if(F.config['fileedit-glob']){ |
| 428 | D.append( |
| 429 | this.e.container, |
| 430 | D.append( |
| 431 | D.span(), |
| 432 | D.append(D.code(),"fileedit-glob"), |
| 433 | " config setting = ", |
| 434 | D.append(D.code(), JSON.stringify(F.config['fileedit-glob'])) |
| 435 | ) |
| 436 | ); |
| 437 | } |
| 438 | |
| 439 | this.loadLeaves(); |
| 440 | selCi.addEventListener( |
| 441 | 'change', (e)=>this.loadFiles(e.target.value), false |
| 442 | ); |
| 443 | const doLoad = (e)=>{ |
| 444 | this.finfo.filename = selFiles.value; |
| 445 | if(this.finfo.filename){ |
| 446 | P.loadFile(this.finfo.filename, this.finfo.checkin); |
| 447 | } |
| 448 | }; |
| 449 | btnLoad.addEventListener('click', doLoad, false); |
| 450 | selFiles.addEventListener('dblclick', doLoad, false); |
| 451 | btnReload.addEventListener( |
| 452 | 'click', (e)=>this.loadLeaves(), false |
| 453 | ); |
| 454 | delete this.init; |
| 455 | } |
| @@ -453,11 +465,11 @@ | |
| 465 | D.attr(D.div(),'id','fileedit-stash-selector'), |
| 466 | 'input-with-label' |
| 467 | ); |
| 468 | const sel = this.e.select = D.select(); |
| 469 | const btnClear = this.e.btnClear |
| 470 | = D.button("Discard Edits"); |
| 471 | D.append(wrapper, "Local edits (", |
| 472 | D.append(D.code(), |
| 473 | F.storage.storageImplName()), |
| 474 | "):", |
| 475 | sel, btnClear); |
| @@ -472,23 +484,34 @@ | |
| 484 | F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail)); |
| 485 | sel.addEventListener('change',function(e){ |
| 486 | const opt = this.selectedOptions[0]; |
| 487 | if(opt && opt._finfo) P.loadFile(opt._finfo); |
| 488 | }); |
| 489 | if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/ |
| 490 | D.append(wrapper, D.append( |
| 491 | D.addClass(D.span(),'warning'), |
| 492 | "Warning: persistent storage is not available, "+ |
| 493 | "so uncomitted edits will not survive a page reload." |
| 494 | )); |
| 495 | } |
| 496 | domInsertPoint.parentNode.insertBefore(wrapper, domInsertPoint); |
| 497 | F.confirmer(btnClear, { |
| 498 | /* must come after insertion into the DOM for the pinSize option to work. */ |
| 499 | pinSize: true, |
| 500 | confirmText: "DISCARD all local edits?", |
| 501 | onconfirm: function(e){ |
| 502 | if(P.finfo){ |
| 503 | const stashed = P.getStashedFinfo(P.finfo); |
| 504 | P.clearStash(); |
| 505 | if(stashed) P.loadFile(/*reload after discarding edits*/); |
| 506 | }else{ |
| 507 | P.clearStash(); |
| 508 | } |
| 509 | }, |
| 510 | ticks: F.config.confirmerButtonTicks |
| 511 | }); |
| 512 | D.addClass(this.e.btnClear,'hidden' /* must not be set until after confirmer is set up!*/); |
| 513 | $stash._fireStashEvent(/*read the page-load-time stash*/); |
| 514 | delete this.init; |
| 515 | }, |
| 516 | /** |
| 517 | Regenerates the edit selection list. |
| @@ -521,20 +544,20 @@ | |
| 544 | return; |
| 545 | } |
| 546 | D.enable(this.e.select); |
| 547 | D.removeClass(this.e.btnClear, 'hidden'); |
| 548 | D.disable(D.option(this.e.select,0,"Select a local edit...")); |
| 549 | const currentFinfo = theFinfo || P.finfo || {filename:''}; |
| 550 | ilist.sort(f.compare).forEach(function(finfo,n){ |
| 551 | const key = stasher.indexKey(finfo), |
| 552 | branch = finfo.branch |
| 553 | || P.fileSelectWidget.checkinBranchName(finfo.checkin)||''; |
| 554 | /* Remember that we don't know the branch name for non-leaf versions |
| 555 | which P.fileSelectWidget() has never seen/cached. */ |
| 556 | const opt = D.option( |
| 557 | self.e.select, n+1/*value is (almost) irrelevant*/, |
| 558 | [F.hashDigits(finfo.checkin), ' [',branch||'?branch?','] ', |
| 559 | f.timestring(new Date(finfo.stashTime)),' ', |
| 560 | false ? finfo.filename : F.shortenFilename(finfo.filename) |
| 561 | ].join('') |
| 562 | ); |
| 563 | opt._finfo = finfo; |
| @@ -637,11 +660,11 @@ | |
| 660 | previewTarget: E('#fileedit-tab-preview-wrapper'), |
| 661 | manifestTarget: E('#fileedit-manifest'), |
| 662 | diffTarget: E('#fileedit-tab-diff-wrapper'), |
| 663 | cbIsExe: E('input[type=checkbox][name=exec_bit]'), |
| 664 | cbManifest: E('input[type=checkbox][name=include_manifest]'), |
| 665 | editStatus: E('#fileedit-edit-status'), |
| 666 | tabs:{ |
| 667 | content: E('#fileedit-tab-content'), |
| 668 | preview: E('#fileedit-tab-preview'), |
| 669 | diff: E('#fileedit-tab-diff'), |
| 670 | commit: E('#fileedit-tab-commit'), |
| @@ -659,24 +682,24 @@ | |
| 682 | }else{ |
| 683 | P.e.taComment = P.e.taCommentSmall; |
| 684 | D.addClass(P.e.taCommentBig, 'hidden'); |
| 685 | } |
| 686 | D.removeClass(P.e.taComment, 'hidden'); |
| 687 | P.tabs.e.container.insertBefore( |
| 688 | /* Move the status bar between the tab buttons and |
| 689 | tab panels. Seems to be the best fit in terms of |
| 690 | functionality and visibility. */ |
| 691 | E('#fossil-status-bar'), P.tabs.e.tabs |
| 692 | ); |
| 693 | P.tabs.e.container.insertBefore(P.e.editStatus, P.tabs.e.tabs); |
| 694 | |
| 695 | P.tabs.addEventListener( |
| 696 | /* Set up auto-refresh of the preview tab... */ |
| 697 | 'before-switch-to', function(ev){ |
| 698 | if(ev.detail===P.e.tabs.preview){ |
| 699 | P.baseHrefForFile(); |
| 700 | if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview(); |
| 701 | }else if(ev.detail===P.e.tabs.diff){ |
| 702 | /* Work around a weird bug where the page gets wider than |
| 703 | the window when the diff tab is NOT in view and the |
| 704 | current SBS diff widget is wider than the window. When |
| 705 | the diff IS in view then CSS overflow magically reduces |
| @@ -715,13 +738,14 @@ | |
| 738 | ); |
| 739 | P.e.btnCommit.addEventListener( |
| 740 | "click",(e)=>P.commit(), false |
| 741 | ); |
| 742 | F.confirmer(P.e.btnReload, { |
| 743 | pinSize: true, |
| 744 | confirmText: "Really reload, losing edits?", |
| 745 | onconfirm: (e)=>P.unstashContent().loadFile(), |
| 746 | ticks: F.config.confirmerButtonTicks |
| 747 | }); |
| 748 | E('#comment-toggle').addEventListener( |
| 749 | "click",(e)=>P.toggleCommentMode(), false |
| 750 | ); |
| 751 | |
| @@ -774,11 +798,14 @@ | |
| 798 | } |
| 799 | |
| 800 | P.addEventListener( |
| 801 | // Clear certain views when new content is loaded/set |
| 802 | 'fileedit-content-replaced', |
| 803 | ()=>{ |
| 804 | P.previewNeedsUpdate = true; |
| 805 | D.clearElement(P.e.diffTarget, P.e.previewTarget, P.e.manifestTarget); |
| 806 | } |
| 807 | ); |
| 808 | P.addEventListener( |
| 809 | // Clear certain views after a non-dry-run commit |
| 810 | 'fileedit-committed', |
| 811 | (e)=>{ |
| @@ -789,11 +816,10 @@ | |
| 816 | ); |
| 817 | |
| 818 | P.fileSelectWidget.init(); |
| 819 | P.stashWidget.init( |
| 820 | P.e.tabs.content.lastElementChild |
| 821 | ); |
| 822 | }/*F.onPageLoad()*/); |
| 823 | |
| 824 | /** |
| 825 | Getter (if called with no args) or setter (if passed an arg) for |
| @@ -819,11 +845,11 @@ | |
| 845 | For use when installing a custom editor widget. Pass it the |
| 846 | getter and setter callbacks to fetch resp. set the content of the |
| 847 | custom widget. They will be triggered via |
| 848 | P.fileContent(). Returns this object. |
| 849 | */ |
| 850 | P.setContentMethods = function(getter, setter){ |
| 851 | this.fileContent.get = getter; |
| 852 | this.fileContent.set = setter; |
| 853 | return this; |
| 854 | }; |
| 855 | |
| @@ -929,63 +955,55 @@ | |
| 955 | updateVersion() updates the filename and version in various UI |
| 956 | elements... |
| 957 | |
| 958 | Returns this object. |
| 959 | */ |
| 960 | P.updateVersion = function f(file,rev){ |
| 961 | if(!f.eLinks){ |
| 962 | f.eName = P.e.editStatus.querySelector('span.name'); |
| 963 | f.eLinks = P.e.editStatus.querySelector('span.links'); |
| 964 | } |
| 965 | if(1===arguments.length){/*assume object*/ |
| 966 | this.finfo = arguments[0]; |
| 967 | file = this.finfo.filename; |
| 968 | rev = this.finfo.checkin; |
| 969 | }else if(0===arguments.length){ |
| 970 | if(affirmHasFile()){ |
| 971 | file = this.finfo.filename; |
| 972 | rev = this.finfo.checkin; |
| 973 | } |
| 974 | }else{ |
| 975 | this.finfo = {filename:file,checkin:rev}; |
| 976 | } |
| 977 | const fi = this.finfo; |
| 978 | D.clearElement(f.eName, f.eLinks); |
| 979 | if(!fi){ |
| 980 | D.append(f.eName, '(no file loaded)'); |
| 981 | return this; |
| 982 | } |
| 983 | const rHuman = F.hashDigits(rev), |
| 984 | rUrl = F.hashDigits(rev,true); |
| 985 | |
| 986 | //TODO? port over is-edited marker from /wikiedit |
| 987 | //var marker = getEditMarker(wi, false); |
| 988 | D.append(f.eName/*,marker*/,D.a(F.repoUrl('finfo',{name:file, m:rUrl}), file)); |
| 989 | |
| 990 | D.append( |
| 991 | f.eLinks, |
| 992 | D.append(D.span(), fi.mimetype||'?mimetype?'), |
| 993 | D.a(F.repoUrl('info/'+rUrl), rHuman), |
| 994 | D.a(F.repoUrl('timeline',{m:rUrl}), "timeline"), |
| 995 | D.a(F.repoUrl('annotate',{filename:file, checkin:rUrl}),'annotate'), |
| 996 | D.a(F.repoUrl('blame',{filename:file, checkin:rUrl}),'blame') |
| 997 | ); |
| 998 | const purlArgs = F.encodeUrlArgs({ |
| 999 | filename: this.finfo.filename, |
| 1000 | checkin: rUrl |
| 1001 | },false,true); |
| 1002 | const purl = F.repoUrl('fileedit',purlArgs); |
| 1003 | D.append( f.eLinks, D.a(purl,"editor permalink") ); |
| 1004 | this.setPageTitle("Edit: "+fi.filename); |
| 1005 | return this; |
| 1006 | }; |
| 1007 | |
| 1008 | /** |
| 1009 | loadFile() loads (file,checkinVersion) and updates the relevant |
| @@ -1032,10 +1050,11 @@ | |
| 1050 | mimetype: headers['content-type'].split(';').shift() |
| 1051 | }); |
| 1052 | self.tabs.switchToTab(self.e.tabs.content); |
| 1053 | self.e.cbIsExe.checked = self.finfo.isExe; |
| 1054 | self.fileContent(r); |
| 1055 | P.previewNeedsUpdate = true; |
| 1056 | self.dispatchEvent('fileedit-file-loaded', self.finfo); |
| 1057 | }; |
| 1058 | const semiFinfo = {filename: file, checkin: rev}; |
| 1059 | const stashFinfo = this.getStashedFinfo(semiFinfo); |
| 1060 | if(stashFinfo){ // fake a response from the stash... |
| @@ -1115,10 +1134,11 @@ | |
| 1134 | P.selectPreviewMode(P.previewModes[header]); |
| 1135 | if('wiki'===header) P.baseHrefForFile(); |
| 1136 | else P.baseHrefRestore(); |
| 1137 | callback(r); |
| 1138 | F.message('Updated preview.'); |
| 1139 | P.previewNeedsUpdate = false; |
| 1140 | P.dispatchEvent('fileedit-preview-updated',{ |
| 1141 | previewMode: P.previewModes.current, |
| 1142 | mimetype: P.finfo.mimetype, |
| 1143 | element: P.e.previewTarget |
| 1144 | }); |
| @@ -1290,10 +1310,11 @@ | |
| 1310 | }else{ |
| 1311 | $stash.updateFile(fi, P.fileContent()); |
| 1312 | } |
| 1313 | F.message("Stashed change to",F.hashDigits(fi.checkin),fi.filename); |
| 1314 | $stash.prune(); |
| 1315 | this.previewNeedsUpdate = true; |
| 1316 | } |
| 1317 | return this; |
| 1318 | }; |
| 1319 | |
| 1320 | /** |
| @@ -1301,10 +1322,11 @@ | |
| 1322 | F.storage. Returns this. |
| 1323 | */ |
| 1324 | P.unstashContent = function(){ |
| 1325 | const finfo = arguments[0] || this.finfo; |
| 1326 | if(finfo){ |
| 1327 | this.previewNeedsUpdate = true; |
| 1328 | $stash.unstash(finfo); |
| 1329 | //console.debug("Unstashed",finfo); |
| 1330 | F.message("Unstashed",F.hashDigits(finfo.checkin),finfo.filename); |
| 1331 | } |
| 1332 | return this; |
| 1333 |
+46
-10
| --- src/fossil.page.forumpost.js | ||
| +++ src/fossil.page.forumpost.js | ||
| @@ -11,26 +11,64 @@ | ||
| 11 | 11 | return function(ev){ |
| 12 | 12 | if(ev) ev.preventDefault(); |
| 13 | 13 | const wasExpanded = widget.classList.contains('expanded'); |
| 14 | 14 | widget.classList.toggle('expanded'); |
| 15 | 15 | contentElem.classList.toggle('expanded'); |
| 16 | - if(wasExpanded) widget.scrollIntoView(); | |
| 16 | + if(wasExpanded){ | |
| 17 | + contentElem.classList.add('shrunken'); | |
| 18 | + contentElem.parentElement.scrollIntoView({ | |
| 19 | + /* This is non-standard, but !(MSIE, Safari) supposedly support it: | |
| 20 | + https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#Browser_compatibility | |
| 21 | + */ behavior: 'smooth' | |
| 22 | + }); | |
| 23 | + }else{ | |
| 24 | + contentElem.classList.remove('shrunken'); | |
| 25 | + } | |
| 17 | 26 | return false; |
| 18 | 27 | }; |
| 19 | 28 | }; |
| 29 | + | |
| 20 | 30 | /* Adds an Expand/Collapse toggle to all div.forumPostBody |
| 21 | 31 | elements which are deemed "too large" (those for which |
| 22 | 32 | scrolling is currently activated because they are taller than |
| 23 | 33 | their max-height). */ |
| 24 | 34 | document.querySelectorAll( |
| 25 | 35 | 'div.forumHier, div.forumTime, div.forumHierRoot' |
| 26 | - ).forEach(function(forumPostWrapper){ | |
| 36 | + ).forEach(function f(forumPostWrapper){ | |
| 27 | 37 | const content = forumPostWrapper.querySelector('div.forumPostBody'); |
| 28 | 38 | if(!content || !scrollbarIsVisible(content)) return; |
| 29 | - const widget = D.div(), | |
| 30 | - widgetEventHandler = getWidgetHandler(widget, content); | |
| 31 | - widget.classList.add('forum-post-collapser'); | |
| 39 | + const parent = content.parentElement, | |
| 40 | + widget = D.addClass( | |
| 41 | + D.div(), | |
| 42 | + 'forum-post-collapser','bottom' | |
| 43 | + ), | |
| 44 | + rightTapZone = D.addClass( | |
| 45 | + D.div(), | |
| 46 | + 'forum-post-collapser','right' | |
| 47 | + ); | |
| 48 | + /* Repopulates the rightTapZone with arrow indicators. Because | |
| 49 | + of the wildly varying height of these elements, This has to | |
| 50 | + be done dynamically at init time and upon collapse/expand. Will not | |
| 51 | + work until the rightTapZone has been added to the DOM. */ | |
| 52 | + const refillTapZone = function f(){ | |
| 53 | + if(!f.baseTapIndicatorHeight){ | |
| 54 | + /* To figure out how often to place an arrow in the rightTapZone, | |
| 55 | + we simply grab the first header element from the page and use | |
| 56 | + its hight as our basis for calculation. */ | |
| 57 | + const h1 = document.querySelector('h1, h2'); | |
| 58 | + f.baseTapIndicatorHeight = h1.getBoundingClientRect().height; | |
| 59 | + } | |
| 60 | + D.clearElement(rightTapZone); | |
| 61 | + var rtzHeight = parseInt(window.getComputedStyle(rightTapZone).height); | |
| 62 | + do { | |
| 63 | + D.append(rightTapZone, D.span()); | |
| 64 | + rtzHeight -= f.baseTapIndicatorHeight * 8; | |
| 65 | + }while(rtzHeight>0); | |
| 66 | + }; | |
| 67 | + const handlerStep1 = getWidgetHandler(widget, content); | |
| 68 | + const widgetEventHandler = ()=>{ handlerStep1(); refillTapZone(); }; | |
| 69 | + content.classList.add('with-expander'); | |
| 32 | 70 | widget.addEventListener('click', widgetEventHandler, false); |
| 33 | 71 | /** Append 3 children, which CSS will evenly space across the |
| 34 | 72 | widget. This improves visibility over having the label |
| 35 | 73 | in only the left, right, or center. */ |
| 36 | 74 | var i = 0; |
| @@ -38,14 +76,12 @@ | ||
| 38 | 76 | if(content.nextSibling){ |
| 39 | 77 | forumPostWrapper.insertBefore(widget, content.nextSibling); |
| 40 | 78 | }else{ |
| 41 | 79 | forumPostWrapper.appendChild(widget); |
| 42 | 80 | } |
| 43 | - /** A double-click toggle will select "the current word" on the | |
| 44 | - post, which is minorly annoying but otherwise harmless. Such | |
| 45 | - a toggle has proven convenient on "excessive" posts, | |
| 46 | - though. */ | |
| 47 | - content.addEventListener('dblclick', widgetEventHandler); | |
| 81 | + content.appendChild(rightTapZone); | |
| 82 | + rightTapZone.addEventListener('click', widgetEventHandler, false); | |
| 83 | + refillTapZone(); | |
| 48 | 84 | }); |
| 49 | 85 | })/*onload callback*/; |
| 50 | 86 | |
| 51 | 87 | })(window.fossil); |
| 52 | 88 | |
| 53 | 89 | ADDED src/fossil.page.wikiedit.js |
| 54 | 90 | ADDED src/fossil.popupwidget.js |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -11,26 +11,64 @@ | |
| 11 | return function(ev){ |
| 12 | if(ev) ev.preventDefault(); |
| 13 | const wasExpanded = widget.classList.contains('expanded'); |
| 14 | widget.classList.toggle('expanded'); |
| 15 | contentElem.classList.toggle('expanded'); |
| 16 | if(wasExpanded) widget.scrollIntoView(); |
| 17 | return false; |
| 18 | }; |
| 19 | }; |
| 20 | /* Adds an Expand/Collapse toggle to all div.forumPostBody |
| 21 | elements which are deemed "too large" (those for which |
| 22 | scrolling is currently activated because they are taller than |
| 23 | their max-height). */ |
| 24 | document.querySelectorAll( |
| 25 | 'div.forumHier, div.forumTime, div.forumHierRoot' |
| 26 | ).forEach(function(forumPostWrapper){ |
| 27 | const content = forumPostWrapper.querySelector('div.forumPostBody'); |
| 28 | if(!content || !scrollbarIsVisible(content)) return; |
| 29 | const widget = D.div(), |
| 30 | widgetEventHandler = getWidgetHandler(widget, content); |
| 31 | widget.classList.add('forum-post-collapser'); |
| 32 | widget.addEventListener('click', widgetEventHandler, false); |
| 33 | /** Append 3 children, which CSS will evenly space across the |
| 34 | widget. This improves visibility over having the label |
| 35 | in only the left, right, or center. */ |
| 36 | var i = 0; |
| @@ -38,14 +76,12 @@ | |
| 38 | if(content.nextSibling){ |
| 39 | forumPostWrapper.insertBefore(widget, content.nextSibling); |
| 40 | }else{ |
| 41 | forumPostWrapper.appendChild(widget); |
| 42 | } |
| 43 | /** A double-click toggle will select "the current word" on the |
| 44 | post, which is minorly annoying but otherwise harmless. Such |
| 45 | a toggle has proven convenient on "excessive" posts, |
| 46 | though. */ |
| 47 | content.addEventListener('dblclick', widgetEventHandler); |
| 48 | }); |
| 49 | })/*onload callback*/; |
| 50 | |
| 51 | })(window.fossil); |
| 52 | |
| 53 | DDED src/fossil.page.wikiedit.js |
| 54 | DDED src/fossil.popupwidget.js |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -11,26 +11,64 @@ | |
| 11 | return function(ev){ |
| 12 | if(ev) ev.preventDefault(); |
| 13 | const wasExpanded = widget.classList.contains('expanded'); |
| 14 | widget.classList.toggle('expanded'); |
| 15 | contentElem.classList.toggle('expanded'); |
| 16 | if(wasExpanded){ |
| 17 | contentElem.classList.add('shrunken'); |
| 18 | contentElem.parentElement.scrollIntoView({ |
| 19 | /* This is non-standard, but !(MSIE, Safari) supposedly support it: |
| 20 | https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#Browser_compatibility |
| 21 | */ behavior: 'smooth' |
| 22 | }); |
| 23 | }else{ |
| 24 | contentElem.classList.remove('shrunken'); |
| 25 | } |
| 26 | return false; |
| 27 | }; |
| 28 | }; |
| 29 | |
| 30 | /* Adds an Expand/Collapse toggle to all div.forumPostBody |
| 31 | elements which are deemed "too large" (those for which |
| 32 | scrolling is currently activated because they are taller than |
| 33 | their max-height). */ |
| 34 | document.querySelectorAll( |
| 35 | 'div.forumHier, div.forumTime, div.forumHierRoot' |
| 36 | ).forEach(function f(forumPostWrapper){ |
| 37 | const content = forumPostWrapper.querySelector('div.forumPostBody'); |
| 38 | if(!content || !scrollbarIsVisible(content)) return; |
| 39 | const parent = content.parentElement, |
| 40 | widget = D.addClass( |
| 41 | D.div(), |
| 42 | 'forum-post-collapser','bottom' |
| 43 | ), |
| 44 | rightTapZone = D.addClass( |
| 45 | D.div(), |
| 46 | 'forum-post-collapser','right' |
| 47 | ); |
| 48 | /* Repopulates the rightTapZone with arrow indicators. Because |
| 49 | of the wildly varying height of these elements, This has to |
| 50 | be done dynamically at init time and upon collapse/expand. Will not |
| 51 | work until the rightTapZone has been added to the DOM. */ |
| 52 | const refillTapZone = function f(){ |
| 53 | if(!f.baseTapIndicatorHeight){ |
| 54 | /* To figure out how often to place an arrow in the rightTapZone, |
| 55 | we simply grab the first header element from the page and use |
| 56 | its hight as our basis for calculation. */ |
| 57 | const h1 = document.querySelector('h1, h2'); |
| 58 | f.baseTapIndicatorHeight = h1.getBoundingClientRect().height; |
| 59 | } |
| 60 | D.clearElement(rightTapZone); |
| 61 | var rtzHeight = parseInt(window.getComputedStyle(rightTapZone).height); |
| 62 | do { |
| 63 | D.append(rightTapZone, D.span()); |
| 64 | rtzHeight -= f.baseTapIndicatorHeight * 8; |
| 65 | }while(rtzHeight>0); |
| 66 | }; |
| 67 | const handlerStep1 = getWidgetHandler(widget, content); |
| 68 | const widgetEventHandler = ()=>{ handlerStep1(); refillTapZone(); }; |
| 69 | content.classList.add('with-expander'); |
| 70 | widget.addEventListener('click', widgetEventHandler, false); |
| 71 | /** Append 3 children, which CSS will evenly space across the |
| 72 | widget. This improves visibility over having the label |
| 73 | in only the left, right, or center. */ |
| 74 | var i = 0; |
| @@ -38,14 +76,12 @@ | |
| 76 | if(content.nextSibling){ |
| 77 | forumPostWrapper.insertBefore(widget, content.nextSibling); |
| 78 | }else{ |
| 79 | forumPostWrapper.appendChild(widget); |
| 80 | } |
| 81 | content.appendChild(rightTapZone); |
| 82 | rightTapZone.addEventListener('click', widgetEventHandler, false); |
| 83 | refillTapZone(); |
| 84 | }); |
| 85 | })/*onload callback*/; |
| 86 | |
| 87 | })(window.fossil); |
| 88 | |
| 89 | DDED src/fossil.page.wikiedit.js |
| 90 | DDED src/fossil.popupwidget.js |
| --- a/src/fossil.page.wikiedit.js | ||
| +++ b/src/fossil.page.wikiedit.js | ||
| @@ -0,0 +1,43 @@ | ||
| 1 | +notag" |(function(F/*ttarget.innerHTML =for a sandbox page or new page, | |
| 2 | + parent: parent UUID string or null if no parent, | |
| 3 | + isEmpty: true if page has no content (is "dele ctrnormal" | "tag" |(function(F/*the fossil object*/){ | |
| 4 | + "use strict"; | |
| 5 | + /** | |
| 6 | + Clienuires that | |
| 7 | + the fossil JS bootstrapping is complete and that several fossil | |
| 8 | + JS APIs have been installed: fossil.fetch, fossil.dom, | |
| 9 | + fossil.tabs, fossil.storage, fossil.confirmer, fossil.popupwidget. | |
| 10 | + | |
| 11 | + Custom events which can be listened for via | |
| 12 | + fossil.page.addEventListener(): | |
| 13 | + | |
| 14 | + - Event 'wiki-page-loaded': passes on information when it | |
| 15 | + loads a wiki (whether from the network or its internal local-edit | |
| 16 | + cache), in the form of an "winfo" objec | |
| 17 | + 'change',n information when it | |
| 18 | + loads a wiki (whether from the network or its internal local-edit | |
| 19 | + cache), in the form of an "winfo" objecnot'localStorage'),' uses browD.append(D.code(),'sessionStorage'),' uses storage local to this browser tab.'update > input[type=checkbox], 'title);cb; | |
| 20 | + constD.attr(wrapper, "title", [ | |
| 21 | +', | |
| 22 | + 'Only themost recent pages', | |
| 23 | +' | |
| 24 | + ].join(' ')); | |
| 25 | +D.attr(title' 'Save changes and Pfunction(F/*ttargenotag" |(function(F/*ttarget.innerHTML =for a sandbox page or new page, | |
| 26 | + parent: parent UUID string or null if no parent, | |
| 27 | + isEmpty: true if page has no content (is "dele ctrnormal" | "tag" |(function(F/*the fossil object*/){ | |
| 28 | + "use strict"; | |
| 29 | + /** | |
| 30 | + Client-side implementation of the /wikiedit app. Requires that | |
| 31 | + the fossil JS bootstrapping is complete and that several fossil | |
| 32 | + JS APIs have been installed: fossil.fetch, fossil.dom, | |
| 33 | + fossil.tabs, fossil.storage, fossil.confirmer, fossil.popupwidget. | |
| 34 | + | |
| 35 | + Custom events which can be listened for via | |
| 36 | + fossil.page.addEventListener(): | |
| 37 | + | |
| 38 | + - Event 'wiki-page-loaded': passes on information when it | |
| 39 | + loads a wiki (whether from the network or its internal local-edit | |
| 40 | + cache), in the form of an "winfo" objec | |
| 41 | + 'change',n information when it | |
| 42 | + loads a wiki (whether from the network or its internal local-edit | |
| 43 | + cache), in the form of an "winfo" objecnot'localStorage'),' uses */fossilfossil |
| --- a/src/fossil.page.wikiedit.js | |
| +++ b/src/fossil.page.wikiedit.js | |
| @@ -0,0 +1,43 @@ | |
| --- a/src/fossil.page.wikiedit.js | |
| +++ b/src/fossil.page.wikiedit.js | |
| @@ -0,0 +1,43 @@ | |
| 1 | notag" |(function(F/*ttarget.innerHTML =for a sandbox page or new page, |
| 2 | parent: parent UUID string or null if no parent, |
| 3 | isEmpty: true if page has no content (is "dele ctrnormal" | "tag" |(function(F/*the fossil object*/){ |
| 4 | "use strict"; |
| 5 | /** |
| 6 | Clienuires that |
| 7 | the fossil JS bootstrapping is complete and that several fossil |
| 8 | JS APIs have been installed: fossil.fetch, fossil.dom, |
| 9 | fossil.tabs, fossil.storage, fossil.confirmer, fossil.popupwidget. |
| 10 | |
| 11 | Custom events which can be listened for via |
| 12 | fossil.page.addEventListener(): |
| 13 | |
| 14 | - Event 'wiki-page-loaded': passes on information when it |
| 15 | loads a wiki (whether from the network or its internal local-edit |
| 16 | cache), in the form of an "winfo" objec |
| 17 | 'change',n information when it |
| 18 | loads a wiki (whether from the network or its internal local-edit |
| 19 | cache), in the form of an "winfo" objecnot'localStorage'),' uses browD.append(D.code(),'sessionStorage'),' uses storage local to this browser tab.'update > input[type=checkbox], 'title);cb; |
| 20 | constD.attr(wrapper, "title", [ |
| 21 | ', |
| 22 | 'Only themost recent pages', |
| 23 | ' |
| 24 | ].join(' ')); |
| 25 | D.attr(title' 'Save changes and Pfunction(F/*ttargenotag" |(function(F/*ttarget.innerHTML =for a sandbox page or new page, |
| 26 | parent: parent UUID string or null if no parent, |
| 27 | isEmpty: true if page has no content (is "dele ctrnormal" | "tag" |(function(F/*the fossil object*/){ |
| 28 | "use strict"; |
| 29 | /** |
| 30 | Client-side implementation of the /wikiedit app. Requires that |
| 31 | the fossil JS bootstrapping is complete and that several fossil |
| 32 | JS APIs have been installed: fossil.fetch, fossil.dom, |
| 33 | fossil.tabs, fossil.storage, fossil.confirmer, fossil.popupwidget. |
| 34 | |
| 35 | Custom events which can be listened for via |
| 36 | fossil.page.addEventListener(): |
| 37 | |
| 38 | - Event 'wiki-page-loaded': passes on information when it |
| 39 | loads a wiki (whether from the network or its internal local-edit |
| 40 | cache), in the form of an "winfo" objec |
| 41 | 'change',n information when it |
| 42 | loads a wiki (whether from the network or its internal local-edit |
| 43 | cache), in the form of an "winfo" objecnot'localStorage'),' uses */fossilfossil |
| --- a/src/fossil.popupwidget.js | ||
| +++ b/src/fossil.popupwidget.js | ||
| @@ -0,0 +1,65 @@ | ||
| 1 | +(function(F/*fossil object*/){ | |
| 2 | + 30 */ | |
| 3 | + installClickToHiden or basic user intera(function(F/*fossil object*/){ | |
| 4 | + 3000unction(e){ | |
| 5 | + iunct}, true);isplay basic information ClickToHide(nction(F/*fossil object*/fossil object*/){ | |
| 6 | + 3000unction(e){ | |
| 7 | + iunction(F/*fossil object*/){ | |
| 8 | + /** | |
| 9 | + A very basic tooltip-like widget. It's intended to be popped up | |
| 10 | + to display basic information or basic user interaction | |
| 11 | + components, e.g. a cop or movopy-to-clipboard butt | |
| 12 | + if needed,l.dom | |
| 13 | + */ | |
| 14 | + const D = F.dom | |
| 15 | + base DOMwidget using the | |
| 16 | +). If theallback whic | |
| 17 | + h is called just before*/){ | |
| 18 | + 30 */ | |
| 19 | + instfunction(F/*fossil object*/){ | |
| 20 | + 30 */ | |
| 21 | + installClickToHiden or basic user intera(function(F/*fossil object*/){ | |
| 22 | + 3000unction(e){ | |
| 23 | + iunct}, true);isplay basic information ClickToHide(nction(F/*fossil object*/fossil object*/){ | |
| 24 | + 3000unction(e){ | |
| 25 | + iunction(F/*fossil object*/){ | |
| 26 | + /** | |
| 27 | + A very basic tooltip-like widget. It's intended to be popped up | |
| 28 | + to display basic information or basic user interaction | |
| 29 | + components, e.g. a copy-to-clipboard button. | |
| 30 | + | |
| 31 | + Requires: fossil.bootstrap, fossil.dom | |
| 32 | + */ | |
| 33 | + conbjecthe popup when either | |
| 34 | + call this | |
| 35 | + show(falseshow(falseshow(falseconst hide Just be careful to mess only with the X coordinate | |
| 36 | + and the width. The browser will try to keep the widget | |
| 37 | + from being truncated off-screen on the right, shifting it | |
| 38 | + to the left if needed, and we cannot generically be sure | |
| 39 | + that an enforced fully on-screen size will actually fit | |
| 40 | + the current help textclickHandler){const rect1unction(F/*fossil object*(function(F/*fossil object*/){})(window.fossil); | |
| 41 | +deleteleft'); | |
| 42 | + deletea toas(function(F/*fossil o*fossil object*/){ | |
| 43 | +function(F/*fossil ct*/){ | |
| 44 | + 30 */ | |
| 45 | + installClickToHiden or basic user intera(fun*/ | |
| 46 | + F.toast = function f(/*...*/){ | |
| 47 | + f.toast = function ff(argsObject){ | |
| 48 | + if(!ff.toaster) f ['fossil-tooltip', 'fossil-toast'] | |
| 49 | + }); | |
| 50 | + i@1hi,X: D.clearElement(ff.toaster.e);9@1SA,2w:var i = 0; | |
| 51 | + for( ; i < argsObject.length; ++i ){ | |
| 52 | + D.append(ff.toaster.e, argsObject[i]); | |
| 53 | + }; | |
| 54 | + ff.toaster.show(f.config.position.x, f.config.position.y); | |
| 55 | + Q@1mw,1m:()=>ff.toaster.hide(), f.config.displayTimeMs); | |
| 56 | + }; | |
| 57 | + } | |
| 58 | + f.toast(arguments); | |
| 59 | + }; | |
| 60 | + F.toast.config = { | |
| 61 | +10@1pR,i:displayTimeMs: 2500 | |
| 62 | + }; | |
| 63 | + | |
| 64 | +})(window.fossil); | |
| 65 | +ZfWzX; |
| --- a/src/fossil.popupwidget.js | |
| +++ b/src/fossil.popupwidget.js | |
| @@ -0,0 +1,65 @@ | |
| --- a/src/fossil.popupwidget.js | |
| +++ b/src/fossil.popupwidget.js | |
| @@ -0,0 +1,65 @@ | |
| 1 | (function(F/*fossil object*/){ |
| 2 | 30 */ |
| 3 | installClickToHiden or basic user intera(function(F/*fossil object*/){ |
| 4 | 3000unction(e){ |
| 5 | iunct}, true);isplay basic information ClickToHide(nction(F/*fossil object*/fossil object*/){ |
| 6 | 3000unction(e){ |
| 7 | iunction(F/*fossil object*/){ |
| 8 | /** |
| 9 | A very basic tooltip-like widget. It's intended to be popped up |
| 10 | to display basic information or basic user interaction |
| 11 | components, e.g. a cop or movopy-to-clipboard butt |
| 12 | if needed,l.dom |
| 13 | */ |
| 14 | const D = F.dom |
| 15 | base DOMwidget using the |
| 16 | ). If theallback whic |
| 17 | h is called just before*/){ |
| 18 | 30 */ |
| 19 | instfunction(F/*fossil object*/){ |
| 20 | 30 */ |
| 21 | installClickToHiden or basic user intera(function(F/*fossil object*/){ |
| 22 | 3000unction(e){ |
| 23 | iunct}, true);isplay basic information ClickToHide(nction(F/*fossil object*/fossil object*/){ |
| 24 | 3000unction(e){ |
| 25 | iunction(F/*fossil object*/){ |
| 26 | /** |
| 27 | A very basic tooltip-like widget. It's intended to be popped up |
| 28 | to display basic information or basic user interaction |
| 29 | components, e.g. a copy-to-clipboard button. |
| 30 | |
| 31 | Requires: fossil.bootstrap, fossil.dom |
| 32 | */ |
| 33 | conbjecthe popup when either |
| 34 | call this |
| 35 | show(falseshow(falseshow(falseconst hide Just be careful to mess only with the X coordinate |
| 36 | and the width. The browser will try to keep the widget |
| 37 | from being truncated off-screen on the right, shifting it |
| 38 | to the left if needed, and we cannot generically be sure |
| 39 | that an enforced fully on-screen size will actually fit |
| 40 | the current help textclickHandler){const rect1unction(F/*fossil object*(function(F/*fossil object*/){})(window.fossil); |
| 41 | deleteleft'); |
| 42 | deletea toas(function(F/*fossil o*fossil object*/){ |
| 43 | function(F/*fossil ct*/){ |
| 44 | 30 */ |
| 45 | installClickToHiden or basic user intera(fun*/ |
| 46 | F.toast = function f(/*...*/){ |
| 47 | f.toast = function ff(argsObject){ |
| 48 | if(!ff.toaster) f ['fossil-tooltip', 'fossil-toast'] |
| 49 | }); |
| 50 | i@1hi,X: D.clearElement(ff.toaster.e);9@1SA,2w:var i = 0; |
| 51 | for( ; i < argsObject.length; ++i ){ |
| 52 | D.append(ff.toaster.e, argsObject[i]); |
| 53 | }; |
| 54 | ff.toaster.show(f.config.position.x, f.config.position.y); |
| 55 | Q@1mw,1m:()=>ff.toaster.hide(), f.config.displayTimeMs); |
| 56 | }; |
| 57 | } |
| 58 | f.toast(arguments); |
| 59 | }; |
| 60 | F.toast.config = { |
| 61 | 10@1pR,i:displayTimeMs: 2500 |
| 62 | }; |
| 63 | |
| 64 | })(window.fossil); |
| 65 | ZfWzX; |
+45
-8
| --- src/fossil.tabs.js | ||
| +++ src/fossil.tabs.js | ||
| @@ -3,30 +3,53 @@ | ||
| 3 | 3 | const E = (s)=>document.querySelector(s), |
| 4 | 4 | EA = (s)=>document.querySelectorAll(s), |
| 5 | 5 | D = F.dom; |
| 6 | 6 | |
| 7 | 7 | /** |
| 8 | - Creates a TabManager. If passed an argument, it is | |
| 9 | - passed to init(). | |
| 8 | + Creates a TabManager. If passed a truthy first argument, it is | |
| 9 | + passed to init(). If passed a truthy second argument, it must be | |
| 10 | + an Object holding configuration options: | |
| 11 | + | |
| 12 | + { | |
| 13 | + tabAccessKeys: boolean (=true) | |
| 14 | + If true, tab buttons are assigned "accesskey" values | |
| 15 | + equal to their 1-based tab number. | |
| 16 | + } | |
| 10 | 17 | */ |
| 11 | - const TabManager = function(domElem){ | |
| 18 | + const TabManager = function(domElem, options){ | |
| 12 | 19 | this.e = {}; |
| 20 | + this.options = F.mergeLastWins(TabManager.defaultOptions , options); | |
| 13 | 21 | if(domElem) this.init(domElem); |
| 14 | 22 | }; |
| 15 | 23 | |
| 16 | 24 | /** |
| 17 | - Internal helper to normalize a method argument | |
| 18 | - to a tab element. | |
| 25 | + Default values for the options object passed to the TabManager | |
| 26 | + constructor. Changing these affects the defaults of all | |
| 27 | + TabManager instances instantiated after that point. | |
| 28 | + */ | |
| 29 | + TabManager.defaultOptions = { | |
| 30 | + tabAccessKeys: true | |
| 31 | + }; | |
| 32 | + | |
| 33 | + /** | |
| 34 | + Internal helper to normalize a method argument to a tab | |
| 35 | + element. arg may be a tab DOM element, a selector string, or an | |
| 36 | + index into tabMgr.e.tabs.childNodes. Returns the corresponding | |
| 37 | + tab element. | |
| 19 | 38 | */ |
| 20 | 39 | const tabArg = function(arg,tabMgr){ |
| 21 | 40 | if('string'===typeof arg) arg = E(arg); |
| 22 | 41 | else if(tabMgr && 'number'===typeof arg && arg>=0){ |
| 23 | 42 | arg = tabMgr.e.tabs.childNodes[arg]; |
| 24 | 43 | } |
| 25 | 44 | return arg; |
| 26 | 45 | }; |
| 27 | 46 | |
| 47 | + /** | |
| 48 | + Sets sets the visibility of tab element e to on or off. e MUST be | |
| 49 | + a TabManager tab element. | |
| 50 | + */ | |
| 28 | 51 | const setVisible = function(e,yes){ |
| 29 | 52 | D[yes ? 'removeClass' : 'addClass'](e, 'hidden'); |
| 30 | 53 | }; |
| 31 | 54 | |
| 32 | 55 | TabManager.prototype = { |
| @@ -33,16 +56,18 @@ | ||
| 33 | 56 | /** |
| 34 | 57 | Initializes the tabs associated with the given tab container |
| 35 | 58 | (DOM element or selector for a single element). This must be |
| 36 | 59 | called once before using any other member functions of a given |
| 37 | 60 | instance, noting that the constructor will call this if it is |
| 38 | - passed an argument. | |
| 61 | + passed an argument. | |
| 39 | 62 | |
| 40 | 63 | The tab container must have an 'id' attribute. This function |
| 41 | 64 | looks through the DOM for all elements which have |
| 42 | 65 | data-tab-parent=thatId. For each one it creates a button to |
| 43 | - switch to that tab and moves the element into this.e.tabs. | |
| 66 | + switch to that tab and moves the element into this.e.tabs, | |
| 67 | + *possibly* injecting an intermediary element between | |
| 68 | + this.e.tabs and the element. | |
| 44 | 69 | |
| 45 | 70 | The label for each tab is set by the data-tab-label attribute |
| 46 | 71 | of each element, defaulting to something not terribly useful. |
| 47 | 72 | |
| 48 | 73 | When it's done, it auto-selects the first tab unless a tab has |
| @@ -108,10 +133,18 @@ | ||
| 108 | 133 | }, |
| 109 | 134 | /** |
| 110 | 135 | Adds the given DOM element or unique selector as the next |
| 111 | 136 | tab in the tab container, adding a button to switch to |
| 112 | 137 | the tab. Returns this object. |
| 138 | + | |
| 139 | + If this object's options include a truthy tabAccessKeys then | |
| 140 | + each tab button gets assigned an accesskey attribute equal to | |
| 141 | + its 1-based index in the tab list. e.g. key 1 is the first tab | |
| 142 | + and key 5 is the 5th. Whether/how that accesskey is accessed is | |
| 143 | + dependent on the browser and its OS: | |
| 144 | + | |
| 145 | + https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey | |
| 113 | 146 | */ |
| 114 | 147 | addTab: function f(tab){ |
| 115 | 148 | if(!f.click){ |
| 116 | 149 | f.click = function(e){ |
| 117 | 150 | e.target.$manager.switchToTab(e.target.$tab); |
| @@ -118,15 +151,19 @@ | ||
| 118 | 151 | }; |
| 119 | 152 | } |
| 120 | 153 | tab = tabArg(tab); |
| 121 | 154 | tab.remove(); |
| 122 | 155 | D.append(this.e.tabs, D.addClass(tab,'tab-panel')); |
| 123 | - const lbl = tab.dataset.tabLabel || 'Tab #'+(this.e.tabs.childNodes.length-1); | |
| 156 | + const tabCount = this.e.tabBar.childNodes.length+1; | |
| 157 | + const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount; | |
| 124 | 158 | const btn = D.addClass(D.append(D.span(), lbl), 'tab-button'); |
| 125 | 159 | D.append(this.e.tabBar,btn); |
| 126 | 160 | btn.$manager = this; |
| 127 | 161 | btn.$tab = tab; |
| 162 | + if(this.options.tabAccessKeys){ | |
| 163 | + D.attr(btn, 'accesskey', tabCount); | |
| 164 | + } | |
| 128 | 165 | btn.addEventListener('click', f.click, false); |
| 129 | 166 | return this; |
| 130 | 167 | }, |
| 131 | 168 | |
| 132 | 169 | /** |
| 133 | 170 |
| --- src/fossil.tabs.js | |
| +++ src/fossil.tabs.js | |
| @@ -3,30 +3,53 @@ | |
| 3 | const E = (s)=>document.querySelector(s), |
| 4 | EA = (s)=>document.querySelectorAll(s), |
| 5 | D = F.dom; |
| 6 | |
| 7 | /** |
| 8 | Creates a TabManager. If passed an argument, it is |
| 9 | passed to init(). |
| 10 | */ |
| 11 | const TabManager = function(domElem){ |
| 12 | this.e = {}; |
| 13 | if(domElem) this.init(domElem); |
| 14 | }; |
| 15 | |
| 16 | /** |
| 17 | Internal helper to normalize a method argument |
| 18 | to a tab element. |
| 19 | */ |
| 20 | const tabArg = function(arg,tabMgr){ |
| 21 | if('string'===typeof arg) arg = E(arg); |
| 22 | else if(tabMgr && 'number'===typeof arg && arg>=0){ |
| 23 | arg = tabMgr.e.tabs.childNodes[arg]; |
| 24 | } |
| 25 | return arg; |
| 26 | }; |
| 27 | |
| 28 | const setVisible = function(e,yes){ |
| 29 | D[yes ? 'removeClass' : 'addClass'](e, 'hidden'); |
| 30 | }; |
| 31 | |
| 32 | TabManager.prototype = { |
| @@ -33,16 +56,18 @@ | |
| 33 | /** |
| 34 | Initializes the tabs associated with the given tab container |
| 35 | (DOM element or selector for a single element). This must be |
| 36 | called once before using any other member functions of a given |
| 37 | instance, noting that the constructor will call this if it is |
| 38 | passed an argument. |
| 39 | |
| 40 | The tab container must have an 'id' attribute. This function |
| 41 | looks through the DOM for all elements which have |
| 42 | data-tab-parent=thatId. For each one it creates a button to |
| 43 | switch to that tab and moves the element into this.e.tabs. |
| 44 | |
| 45 | The label for each tab is set by the data-tab-label attribute |
| 46 | of each element, defaulting to something not terribly useful. |
| 47 | |
| 48 | When it's done, it auto-selects the first tab unless a tab has |
| @@ -108,10 +133,18 @@ | |
| 108 | }, |
| 109 | /** |
| 110 | Adds the given DOM element or unique selector as the next |
| 111 | tab in the tab container, adding a button to switch to |
| 112 | the tab. Returns this object. |
| 113 | */ |
| 114 | addTab: function f(tab){ |
| 115 | if(!f.click){ |
| 116 | f.click = function(e){ |
| 117 | e.target.$manager.switchToTab(e.target.$tab); |
| @@ -118,15 +151,19 @@ | |
| 118 | }; |
| 119 | } |
| 120 | tab = tabArg(tab); |
| 121 | tab.remove(); |
| 122 | D.append(this.e.tabs, D.addClass(tab,'tab-panel')); |
| 123 | const lbl = tab.dataset.tabLabel || 'Tab #'+(this.e.tabs.childNodes.length-1); |
| 124 | const btn = D.addClass(D.append(D.span(), lbl), 'tab-button'); |
| 125 | D.append(this.e.tabBar,btn); |
| 126 | btn.$manager = this; |
| 127 | btn.$tab = tab; |
| 128 | btn.addEventListener('click', f.click, false); |
| 129 | return this; |
| 130 | }, |
| 131 | |
| 132 | /** |
| 133 |
| --- src/fossil.tabs.js | |
| +++ src/fossil.tabs.js | |
| @@ -3,30 +3,53 @@ | |
| 3 | const E = (s)=>document.querySelector(s), |
| 4 | EA = (s)=>document.querySelectorAll(s), |
| 5 | D = F.dom; |
| 6 | |
| 7 | /** |
| 8 | Creates a TabManager. If passed a truthy first argument, it is |
| 9 | passed to init(). If passed a truthy second argument, it must be |
| 10 | an Object holding configuration options: |
| 11 | |
| 12 | { |
| 13 | tabAccessKeys: boolean (=true) |
| 14 | If true, tab buttons are assigned "accesskey" values |
| 15 | equal to their 1-based tab number. |
| 16 | } |
| 17 | */ |
| 18 | const TabManager = function(domElem, options){ |
| 19 | this.e = {}; |
| 20 | this.options = F.mergeLastWins(TabManager.defaultOptions , options); |
| 21 | if(domElem) this.init(domElem); |
| 22 | }; |
| 23 | |
| 24 | /** |
| 25 | Default values for the options object passed to the TabManager |
| 26 | constructor. Changing these affects the defaults of all |
| 27 | TabManager instances instantiated after that point. |
| 28 | */ |
| 29 | TabManager.defaultOptions = { |
| 30 | tabAccessKeys: true |
| 31 | }; |
| 32 | |
| 33 | /** |
| 34 | Internal helper to normalize a method argument to a tab |
| 35 | element. arg may be a tab DOM element, a selector string, or an |
| 36 | index into tabMgr.e.tabs.childNodes. Returns the corresponding |
| 37 | tab element. |
| 38 | */ |
| 39 | const tabArg = function(arg,tabMgr){ |
| 40 | if('string'===typeof arg) arg = E(arg); |
| 41 | else if(tabMgr && 'number'===typeof arg && arg>=0){ |
| 42 | arg = tabMgr.e.tabs.childNodes[arg]; |
| 43 | } |
| 44 | return arg; |
| 45 | }; |
| 46 | |
| 47 | /** |
| 48 | Sets sets the visibility of tab element e to on or off. e MUST be |
| 49 | a TabManager tab element. |
| 50 | */ |
| 51 | const setVisible = function(e,yes){ |
| 52 | D[yes ? 'removeClass' : 'addClass'](e, 'hidden'); |
| 53 | }; |
| 54 | |
| 55 | TabManager.prototype = { |
| @@ -33,16 +56,18 @@ | |
| 56 | /** |
| 57 | Initializes the tabs associated with the given tab container |
| 58 | (DOM element or selector for a single element). This must be |
| 59 | called once before using any other member functions of a given |
| 60 | instance, noting that the constructor will call this if it is |
| 61 | passed an argument. |
| 62 | |
| 63 | The tab container must have an 'id' attribute. This function |
| 64 | looks through the DOM for all elements which have |
| 65 | data-tab-parent=thatId. For each one it creates a button to |
| 66 | switch to that tab and moves the element into this.e.tabs, |
| 67 | *possibly* injecting an intermediary element between |
| 68 | this.e.tabs and the element. |
| 69 | |
| 70 | The label for each tab is set by the data-tab-label attribute |
| 71 | of each element, defaulting to something not terribly useful. |
| 72 | |
| 73 | When it's done, it auto-selects the first tab unless a tab has |
| @@ -108,10 +133,18 @@ | |
| 133 | }, |
| 134 | /** |
| 135 | Adds the given DOM element or unique selector as the next |
| 136 | tab in the tab container, adding a button to switch to |
| 137 | the tab. Returns this object. |
| 138 | |
| 139 | If this object's options include a truthy tabAccessKeys then |
| 140 | each tab button gets assigned an accesskey attribute equal to |
| 141 | its 1-based index in the tab list. e.g. key 1 is the first tab |
| 142 | and key 5 is the 5th. Whether/how that accesskey is accessed is |
| 143 | dependent on the browser and its OS: |
| 144 | |
| 145 | https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey |
| 146 | */ |
| 147 | addTab: function f(tab){ |
| 148 | if(!f.click){ |
| 149 | f.click = function(e){ |
| 150 | e.target.$manager.switchToTab(e.target.$tab); |
| @@ -118,15 +151,19 @@ | |
| 151 | }; |
| 152 | } |
| 153 | tab = tabArg(tab); |
| 154 | tab.remove(); |
| 155 | D.append(this.e.tabs, D.addClass(tab,'tab-panel')); |
| 156 | const tabCount = this.e.tabBar.childNodes.length+1; |
| 157 | const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount; |
| 158 | const btn = D.addClass(D.append(D.span(), lbl), 'tab-button'); |
| 159 | D.append(this.e.tabBar,btn); |
| 160 | btn.$manager = this; |
| 161 | btn.$tab = tab; |
| 162 | if(this.options.tabAccessKeys){ |
| 163 | D.attr(btn, 'accesskey', tabCount); |
| 164 | } |
| 165 | btn.addEventListener('click', f.click, false); |
| 166 | return this; |
| 167 | }, |
| 168 | |
| 169 | /** |
| 170 |
+1
-1
| --- src/fusefs.c | ||
| +++ src/fusefs.c | ||
| @@ -285,11 +285,11 @@ | ||
| 285 | 285 | .readdir = fusefs_readdir, |
| 286 | 286 | .read = fusefs_read, |
| 287 | 287 | }; |
| 288 | 288 | |
| 289 | 289 | /* |
| 290 | -** COMMAND: fusefs | |
| 290 | +** COMMAND: fusefs* | |
| 291 | 291 | ** |
| 292 | 292 | ** Usage: %fossil fusefs [--debug] DIRECTORY |
| 293 | 293 | ** |
| 294 | 294 | ** This command uses the Fuse Filesystem (FuseFS) to mount a directory |
| 295 | 295 | ** at DIRECTORY that contains the content of all check-ins in the |
| 296 | 296 |
| --- src/fusefs.c | |
| +++ src/fusefs.c | |
| @@ -285,11 +285,11 @@ | |
| 285 | .readdir = fusefs_readdir, |
| 286 | .read = fusefs_read, |
| 287 | }; |
| 288 | |
| 289 | /* |
| 290 | ** COMMAND: fusefs |
| 291 | ** |
| 292 | ** Usage: %fossil fusefs [--debug] DIRECTORY |
| 293 | ** |
| 294 | ** This command uses the Fuse Filesystem (FuseFS) to mount a directory |
| 295 | ** at DIRECTORY that contains the content of all check-ins in the |
| 296 |
| --- src/fusefs.c | |
| +++ src/fusefs.c | |
| @@ -285,11 +285,11 @@ | |
| 285 | .readdir = fusefs_readdir, |
| 286 | .read = fusefs_read, |
| 287 | }; |
| 288 | |
| 289 | /* |
| 290 | ** COMMAND: fusefs* |
| 291 | ** |
| 292 | ** Usage: %fossil fusefs [--debug] DIRECTORY |
| 293 | ** |
| 294 | ** This command uses the Fuse Filesystem (FuseFS) to mount a directory |
| 295 | ** at DIRECTORY that contains the content of all check-ins in the |
| 296 |
+33
| --- src/glob.c | ||
| +++ src/glob.c | ||
| @@ -163,10 +163,43 @@ | ||
| 163 | 163 | if( pGlob ){ |
| 164 | 164 | fossil_free(pGlob->azPattern); |
| 165 | 165 | fossil_free(pGlob); |
| 166 | 166 | } |
| 167 | 167 | } |
| 168 | + | |
| 169 | +/* | |
| 170 | +** Appends the given glob to the given buffer in the form of a | |
| 171 | +** JS/JSON-compatible array. It requires that pDest have been | |
| 172 | +** initialized. If pGlob is NULL or empty it emits [] (an empty | |
| 173 | +** array). | |
| 174 | +*/ | |
| 175 | +void glob_render_json_to_blob(Glob *pGlob, Blob *pDest){ | |
| 176 | + int i = 0; | |
| 177 | + blob_append(pDest, "[", 1); | |
| 178 | + for( ; pGlob && i < pGlob->nPattern; ++i ){ | |
| 179 | + if(i){ | |
| 180 | + blob_append(pDest, ",", 1); | |
| 181 | + } | |
| 182 | + blob_appendf(pDest, "%!j", pGlob->azPattern[i]); | |
| 183 | + } | |
| 184 | + blob_append(pDest, "]", 1); | |
| 185 | +} | |
| 186 | +/* | |
| 187 | +** Functionally equivalent to glob_render_json_to_blob() | |
| 188 | +** but outputs via cgi_print(). | |
| 189 | +*/ | |
| 190 | +void glob_render_json_to_cgi(Glob *pGlob){ | |
| 191 | + int i = 0; | |
| 192 | + CX("["); | |
| 193 | + for( ; pGlob && i < pGlob->nPattern; ++i ){ | |
| 194 | + if(i){ | |
| 195 | + CX(","); | |
| 196 | + } | |
| 197 | + CX("%!j", pGlob->azPattern[i]); | |
| 198 | + } | |
| 199 | + CX("]"); | |
| 200 | +} | |
| 168 | 201 | |
| 169 | 202 | /* |
| 170 | 203 | ** COMMAND: test-glob |
| 171 | 204 | ** |
| 172 | 205 | ** Usage: %fossil test-glob PATTERN STRING... |
| 173 | 206 |
| --- src/glob.c | |
| +++ src/glob.c | |
| @@ -163,10 +163,43 @@ | |
| 163 | if( pGlob ){ |
| 164 | fossil_free(pGlob->azPattern); |
| 165 | fossil_free(pGlob); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | /* |
| 170 | ** COMMAND: test-glob |
| 171 | ** |
| 172 | ** Usage: %fossil test-glob PATTERN STRING... |
| 173 |
| --- src/glob.c | |
| +++ src/glob.c | |
| @@ -163,10 +163,43 @@ | |
| 163 | if( pGlob ){ |
| 164 | fossil_free(pGlob->azPattern); |
| 165 | fossil_free(pGlob); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | /* |
| 170 | ** Appends the given glob to the given buffer in the form of a |
| 171 | ** JS/JSON-compatible array. It requires that pDest have been |
| 172 | ** initialized. If pGlob is NULL or empty it emits [] (an empty |
| 173 | ** array). |
| 174 | */ |
| 175 | void glob_render_json_to_blob(Glob *pGlob, Blob *pDest){ |
| 176 | int i = 0; |
| 177 | blob_append(pDest, "[", 1); |
| 178 | for( ; pGlob && i < pGlob->nPattern; ++i ){ |
| 179 | if(i){ |
| 180 | blob_append(pDest, ",", 1); |
| 181 | } |
| 182 | blob_appendf(pDest, "%!j", pGlob->azPattern[i]); |
| 183 | } |
| 184 | blob_append(pDest, "]", 1); |
| 185 | } |
| 186 | /* |
| 187 | ** Functionally equivalent to glob_render_json_to_blob() |
| 188 | ** but outputs via cgi_print(). |
| 189 | */ |
| 190 | void glob_render_json_to_cgi(Glob *pGlob){ |
| 191 | int i = 0; |
| 192 | CX("["); |
| 193 | for( ; pGlob && i < pGlob->nPattern; ++i ){ |
| 194 | if(i){ |
| 195 | CX(","); |
| 196 | } |
| 197 | CX("%!j", pGlob->azPattern[i]); |
| 198 | } |
| 199 | CX("]"); |
| 200 | } |
| 201 | |
| 202 | /* |
| 203 | ** COMMAND: test-glob |
| 204 | ** |
| 205 | ** Usage: %fossil test-glob PATTERN STRING... |
| 206 |
+1
-1
| --- src/graph.js | ||
| +++ src/graph.js | ||
| @@ -779,6 +779,6 @@ | ||
| 779 | 779 | if(!dataObj) break; |
| 780 | 780 | var txJson = dataObj.textContent || dataObj.innerText; |
| 781 | 781 | var tx = JSON.parse(txJson); |
| 782 | 782 | TimelineGraph(tx); |
| 783 | 783 | } |
| 784 | -}()) | |
| 784 | +}()); | |
| 785 | 785 |
| --- src/graph.js | |
| +++ src/graph.js | |
| @@ -779,6 +779,6 @@ | |
| 779 | if(!dataObj) break; |
| 780 | var txJson = dataObj.textContent || dataObj.innerText; |
| 781 | var tx = JSON.parse(txJson); |
| 782 | TimelineGraph(tx); |
| 783 | } |
| 784 | }()) |
| 785 |
| --- src/graph.js | |
| +++ src/graph.js | |
| @@ -779,6 +779,6 @@ | |
| 779 | if(!dataObj) break; |
| 780 | var txJson = dataObj.textContent || dataObj.innerText; |
| 781 | var tx = JSON.parse(txJson); |
| 782 | TimelineGraph(tx); |
| 783 | } |
| 784 | }()); |
| 785 |
+1
-1
| --- src/hook.c | ||
| +++ src/hook.c | ||
| @@ -174,11 +174,11 @@ | ||
| 174 | 174 | } |
| 175 | 175 | db_finalize(&q); |
| 176 | 176 | } |
| 177 | 177 | |
| 178 | 178 | /* |
| 179 | -** COMMAND: hook | |
| 179 | +** COMMAND: hook* | |
| 180 | 180 | ** |
| 181 | 181 | ** Usage: %fossil hook COMMAND ... |
| 182 | 182 | ** |
| 183 | 183 | ** Commands include: |
| 184 | 184 | ** |
| 185 | 185 |
| --- src/hook.c | |
| +++ src/hook.c | |
| @@ -174,11 +174,11 @@ | |
| 174 | } |
| 175 | db_finalize(&q); |
| 176 | } |
| 177 | |
| 178 | /* |
| 179 | ** COMMAND: hook |
| 180 | ** |
| 181 | ** Usage: %fossil hook COMMAND ... |
| 182 | ** |
| 183 | ** Commands include: |
| 184 | ** |
| 185 |
| --- src/hook.c | |
| +++ src/hook.c | |
| @@ -174,11 +174,11 @@ | |
| 174 | } |
| 175 | db_finalize(&q); |
| 176 | } |
| 177 | |
| 178 | /* |
| 179 | ** COMMAND: hook* |
| 180 | ** |
| 181 | ** Usage: %fossil hook COMMAND ... |
| 182 | ** |
| 183 | ** Commands include: |
| 184 | ** |
| 185 |
+1
-1
| --- src/import.c | ||
| +++ src/import.c | ||
| @@ -1594,11 +1594,11 @@ | ||
| 1594 | 1594 | db_finalize(&revSrc); |
| 1595 | 1595 | fossil_print(" Done!\n"); |
| 1596 | 1596 | } |
| 1597 | 1597 | |
| 1598 | 1598 | /* |
| 1599 | -** COMMAND: import | |
| 1599 | +** COMMAND: import* | |
| 1600 | 1600 | ** |
| 1601 | 1601 | ** Usage: %fossil import ?--git? ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE? |
| 1602 | 1602 | ** or: %fossil import --svn ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE? |
| 1603 | 1603 | ** |
| 1604 | 1604 | ** Read interchange format generated by another VCS and use it to |
| 1605 | 1605 |
| --- src/import.c | |
| +++ src/import.c | |
| @@ -1594,11 +1594,11 @@ | |
| 1594 | db_finalize(&revSrc); |
| 1595 | fossil_print(" Done!\n"); |
| 1596 | } |
| 1597 | |
| 1598 | /* |
| 1599 | ** COMMAND: import |
| 1600 | ** |
| 1601 | ** Usage: %fossil import ?--git? ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE? |
| 1602 | ** or: %fossil import --svn ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE? |
| 1603 | ** |
| 1604 | ** Read interchange format generated by another VCS and use it to |
| 1605 |
| --- src/import.c | |
| +++ src/import.c | |
| @@ -1594,11 +1594,11 @@ | |
| 1594 | db_finalize(&revSrc); |
| 1595 | fossil_print(" Done!\n"); |
| 1596 | } |
| 1597 | |
| 1598 | /* |
| 1599 | ** COMMAND: import* |
| 1600 | ** |
| 1601 | ** Usage: %fossil import ?--git? ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE? |
| 1602 | ** or: %fossil import --svn ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE? |
| 1603 | ** |
| 1604 | ** Read interchange format generated by another VCS and use it to |
| 1605 |
+104
-48
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -191,11 +191,11 @@ | ||
| 191 | 191 | ** Options: |
| 192 | 192 | ** |
| 193 | 193 | ** -R|--repository FILE Extract info from repository FILE |
| 194 | 194 | ** -v|--verbose Show extra information about repositories |
| 195 | 195 | ** |
| 196 | -** See also: annotate, artifact, finfo, timeline | |
| 196 | +** See also: [[annotate]], [[artifact]], [[finfo]], [[timeline]] | |
| 197 | 197 | */ |
| 198 | 198 | void info_cmd(void){ |
| 199 | 199 | i64 fsize; |
| 200 | 200 | int verboseFlag = find_option("verbose","v",0)!=0; |
| 201 | 201 | if( !verboseFlag ){ |
| @@ -441,11 +441,11 @@ | ||
| 441 | 441 | /* |
| 442 | 442 | ** Generate javascript to enhance HTML diffs. |
| 443 | 443 | */ |
| 444 | 444 | void append_diff_javascript(int sideBySide){ |
| 445 | 445 | if( !sideBySide ) return; |
| 446 | - style_load_one_js_file("sbsdiff.js"); | |
| 446 | + builtin_request_js("sbsdiff.js"); | |
| 447 | 447 | } |
| 448 | 448 | |
| 449 | 449 | /* |
| 450 | 450 | ** Construct an appropriate diffFlag for text_diff() based on query |
| 451 | 451 | ** parameters and the to boolean arguments. |
| @@ -2011,27 +2011,37 @@ | ||
| 2011 | 2011 | manifest_destroy(pManifest); |
| 2012 | 2012 | return rid; |
| 2013 | 2013 | } |
| 2014 | 2014 | |
| 2015 | 2015 | /* |
| 2016 | -** The "z" argument is a string that contains the text of a source code | |
| 2017 | -** file. This routine appends that text to the HTTP reply with line numbering. | |
| 2016 | +** The "z" argument is a string that contains the text of a source | |
| 2017 | +** code file and nZ is its length in bytes. This routine appends that | |
| 2018 | +** text to the HTTP reply with line numbering. | |
| 2019 | +** | |
| 2020 | +** zName is the content's file name, if any (it may be NULL). If that | |
| 2021 | +** name contains a '.' then the part after the final '.' is used as | |
| 2022 | +** the X part of a "language-X" CSS class on the generated CODE block. | |
| 2018 | 2023 | ** |
| 2019 | 2024 | ** zLn is the ?ln= parameter for the HTTP query. If there is an argument, |
| 2020 | 2025 | ** then highlight that line number and scroll to it once the page loads. |
| 2021 | 2026 | ** If there are two line numbers, highlight the range of lines. |
| 2022 | 2027 | ** Multiple ranges can be highlighed by adding additional line numbers |
| 2023 | 2028 | ** separated by a non-digit character (also not one of [-,.]). |
| 2024 | 2029 | */ |
| 2025 | 2030 | void output_text_with_line_numbers( |
| 2026 | 2031 | const char *z, |
| 2032 | + int nZ, | |
| 2033 | + const char *zName, | |
| 2027 | 2034 | const char *zLn |
| 2028 | 2035 | ){ |
| 2029 | 2036 | int iStart, iEnd; /* Start and end of region to highlight */ |
| 2030 | 2037 | int n = 0; /* Current line number */ |
| 2031 | 2038 | int i = 0; /* Loop index */ |
| 2032 | 2039 | int iTop = 0; /* Scroll so that this line is on top of screen. */ |
| 2040 | + int nLine = 0; /* content line count */ | |
| 2041 | + int nSpans = 0; /* number of distinct zLn spans */ | |
| 2042 | + const char *zExt = file_extension(zName); | |
| 2033 | 2043 | Stmt q; |
| 2034 | 2044 | |
| 2035 | 2045 | iStart = iEnd = atoi(zLn); |
| 2036 | 2046 | db_multi_exec( |
| 2037 | 2047 | "CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)"); |
| @@ -2047,56 +2057,101 @@ | ||
| 2047 | 2057 | while( fossil_isdigit(zLn[i]) ) i++; |
| 2048 | 2058 | if( iEnd<iStart ) iEnd = iStart; |
| 2049 | 2059 | db_multi_exec( |
| 2050 | 2060 | "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd |
| 2051 | 2061 | ); |
| 2062 | + ++nSpans; | |
| 2052 | 2063 | iStart = iEnd = atoi(&zLn[i++]); |
| 2053 | 2064 | }while( zLn[i] && iStart && iEnd ); |
| 2054 | 2065 | } |
| 2055 | - db_prepare(&q, "SELECT min(iStart), max(iEnd) FROM lnos"); | |
| 2056 | - if( db_step(&q)==SQLITE_ROW ){ | |
| 2057 | - iStart = db_column_int(&q, 0); | |
| 2058 | - iEnd = db_column_int(&q, 1); | |
| 2059 | - iTop = iStart - 15 + (iEnd-iStart)/4; | |
| 2060 | - if( iTop>iStart - 2 ) iTop = iStart-2; | |
| 2061 | - } | |
| 2062 | - db_finalize(&q); | |
| 2063 | - @ <pre> | |
| 2064 | - while( z[0] ){ | |
| 2065 | - n++; | |
| 2066 | - db_prepare(&q, | |
| 2067 | - "SELECT min(iStart), max(iEnd) FROM lnos" | |
| 2068 | - " WHERE iStart <= %d AND iEnd >= %d", n, n); | |
| 2069 | - if( db_step(&q)==SQLITE_ROW ){ | |
| 2070 | - iStart = db_column_int(&q, 0); | |
| 2071 | - iEnd = db_column_int(&q, 1); | |
| 2072 | - } | |
| 2073 | - db_finalize(&q); | |
| 2074 | - for(i=0; z[i] && z[i]!='\n'; i++){} | |
| 2075 | - if( n==iTop ) cgi_append_content("<span id=\"scrollToMe\">", -1); | |
| 2076 | - if( n==iStart ){ | |
| 2077 | - cgi_append_content("<div class=\"selectedText\">",-1); | |
| 2078 | - } | |
| 2079 | - cgi_printf("%6d ", n); | |
| 2080 | - if( i>0 ){ | |
| 2081 | - char *zHtml = htmlize(z, i); | |
| 2082 | - cgi_append_content(zHtml, -1); | |
| 2083 | - fossil_free(zHtml); | |
| 2084 | - } | |
| 2085 | - if( n==iTop ) cgi_append_content("</span>", -1); | |
| 2086 | - if( n==iEnd ) cgi_append_content("</div>", -1); | |
| 2087 | - else cgi_append_content("\n", 1); | |
| 2088 | - z += i; | |
| 2089 | - if( z[0]=='\n' ) z++; | |
| 2090 | - } | |
| 2091 | - if( n<iEnd ) cgi_printf("</div>"); | |
| 2092 | - @ </pre> | |
| 2066 | + /*cgi_printf("<!-- ln span count=%d -->", nSpans);*/ | |
| 2067 | + cgi_append_content("<table class='numbered-lines'><tbody>" | |
| 2068 | + "<tr><td class='line-numbers'>", -1); | |
| 2069 | + iStart = iEnd = 0; | |
| 2070 | + count_lines(z, nZ, &nLine); | |
| 2071 | + for( n=1 ; n<=nLine; ++n ){ | |
| 2072 | + const char * zAttr = ""; | |
| 2073 | + const char * zId = ""; | |
| 2074 | + if(nSpans>0 && iEnd==0){/*Grab the next range of zLn marking*/ | |
| 2075 | + db_prepare(&q, "SELECT iStart, iEnd FROM lnos " | |
| 2076 | + "WHERE iStart >= %d ORDER BY iStart", n); | |
| 2077 | + if( db_step(&q)==SQLITE_ROW ){ | |
| 2078 | + iStart = db_column_int(&q, 0); | |
| 2079 | + iEnd = db_column_int(&q, 1); | |
| 2080 | + if(!iTop){ | |
| 2081 | + iTop = iStart - 15 + (iEnd-iStart)/4; | |
| 2082 | + if( iTop>iStart - 2 ) iTop = iStart-2; | |
| 2083 | + } | |
| 2084 | + }else{ | |
| 2085 | + /* Note that overlapping multi-spans, e.g. 10-15+12-20, | |
| 2086 | + can cause us to miss a row. */ | |
| 2087 | + iStart = iEnd = 0; | |
| 2088 | + } | |
| 2089 | + db_finalize(&q); | |
| 2090 | + --nSpans; | |
| 2091 | + /*cgi_printf("<!-- iStart=%d, iEnd=%d -->", iStart, iEnd);*/ | |
| 2092 | + } | |
| 2093 | + if(n==iTop) { | |
| 2094 | + zId = " id='scrollToMe'"; | |
| 2095 | + } | |
| 2096 | + if(n==iStart){/*Figure out which CSS class(es) this line needs...*/ | |
| 2097 | + if(n==iEnd){ | |
| 2098 | + zAttr = " class='selected-line start end'"; | |
| 2099 | + iEnd = 0; | |
| 2100 | + }else{ | |
| 2101 | + zAttr = " class='selected-line start'"; | |
| 2102 | + } | |
| 2103 | + iStart = 0; | |
| 2104 | + }else if(n==iEnd){ | |
| 2105 | + zAttr = " class='selected-line end'"; | |
| 2106 | + iEnd = 0; | |
| 2107 | + }else if( n>iStart && n<iEnd ){ | |
| 2108 | + zAttr = " class='selected-line'"; | |
| 2109 | + } | |
| 2110 | + cgi_printf("<span%s%s>%6d</span>", zId, zAttr, n); | |
| 2111 | + } | |
| 2112 | + cgi_append_content("</td><td class='file-content'><pre>",-1); | |
| 2113 | + if(zExt && *zExt){ | |
| 2114 | + cgi_printf("<code class='language-%h'>",zExt); | |
| 2115 | + }else{ | |
| 2116 | + cgi_append_content("<code>", -1); | |
| 2117 | + } | |
| 2118 | + cgi_printf("%z", htmlize(z, nZ)); | |
| 2119 | + CX("</code></pre></td></tr></tbody></table>\n"); | |
| 2093 | 2120 | if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){ |
| 2094 | - style_load_one_js_file("scroll.js"); | |
| 2121 | + builtin_request_js("scroll.js"); | |
| 2095 | 2122 | } |
| 2123 | + style_emit_fossil_js_apis(0, "dom", "copybutton", "popupwidget", | |
| 2124 | + "numbered-lines", 0); | |
| 2096 | 2125 | } |
| 2097 | 2126 | |
| 2127 | +/* | |
| 2128 | +** COMMAND: test-line-numbers | |
| 2129 | +** | |
| 2130 | +** Usage: %fossil test-line-numbers FILE ?LN-SPEC? | |
| 2131 | +** | |
| 2132 | +*/ | |
| 2133 | +void cmd_test_line_numbers(void){ | |
| 2134 | + Blob content = empty_blob; | |
| 2135 | + const char * zLn = ""; | |
| 2136 | + const char * zFilename = 0; | |
| 2137 | + | |
| 2138 | + if(g.argc < 3){ | |
| 2139 | + usage("FILE"); | |
| 2140 | + }else if(g.argc>3){ | |
| 2141 | + zLn = g.argv[3]; | |
| 2142 | + } | |
| 2143 | + db_find_and_open_repository(0,0); | |
| 2144 | + zFilename = g.argv[2]; | |
| 2145 | + fossil_print("%s %s\n", zFilename, zLn); | |
| 2146 | + | |
| 2147 | + blob_read_from_file(&content, zFilename, ExtFILE); | |
| 2148 | + output_text_with_line_numbers(blob_str(&content), blob_size(&content), | |
| 2149 | + zFilename, zLn); | |
| 2150 | + blob_reset(&content); | |
| 2151 | + fossil_print("%b\n", cgi_output_blob()); | |
| 2152 | +} | |
| 2098 | 2153 | |
| 2099 | 2154 | /* |
| 2100 | 2155 | ** WEBPAGE: artifact |
| 2101 | 2156 | ** WEBPAGE: file |
| 2102 | 2157 | ** WEBPAGE: whatis |
| @@ -2387,25 +2442,26 @@ | ||
| 2387 | 2442 | if( zLn==0 || atoi(zLn)==0 ){ |
| 2388 | 2443 | style_submenu_checkbox("ln", "Line Numbers", 0, 0); |
| 2389 | 2444 | } |
| 2390 | 2445 | blob_to_utf8_no_bom(&content, 0); |
| 2391 | 2446 | zMime = mimetype_from_content(&content); |
| 2392 | - @ <blockquote> | |
| 2447 | + @ <blockquote class="file-content"> | |
| 2393 | 2448 | if( zMime==0 ){ |
| 2394 | 2449 | const char *z, *zFileName, *zExt; |
| 2395 | 2450 | z = blob_str(&content); |
| 2396 | 2451 | zFileName = db_text(0, |
| 2397 | 2452 | "SELECT name FROM mlink, filename" |
| 2398 | 2453 | " WHERE filename.fnid=mlink.fnid" |
| 2399 | 2454 | " AND mlink.fid=%d", |
| 2400 | 2455 | rid); |
| 2401 | - zExt = zFileName ? strrchr(zFileName, '.') : 0; | |
| 2456 | + zExt = file_extension(zFileName); | |
| 2402 | 2457 | if( zLn ){ |
| 2403 | - output_text_with_line_numbers(z, zLn); | |
| 2458 | + output_text_with_line_numbers(z, blob_size(&content), | |
| 2459 | + zFileName, zLn); | |
| 2404 | 2460 | }else if( zExt && zExt[1] ){ |
| 2405 | 2461 | @ <pre> |
| 2406 | - @ <code class="language-%s(zExt+1)">%h(z)</code> | |
| 2462 | + @ <code class="language-%s(zExt)">%h(z)</code> | |
| 2407 | 2463 | @ </pre> |
| 2408 | 2464 | }else{ |
| 2409 | 2465 | @ <pre> |
| 2410 | 2466 | @ %h(z) |
| 2411 | 2467 | @ </pre> |
| @@ -3113,11 +3169,11 @@ | ||
| 3113 | 3169 | @ <input type="submit" name="apply" value="Apply Changes" /> |
| 3114 | 3170 | } |
| 3115 | 3171 | @ </td></tr> |
| 3116 | 3172 | @ </table> |
| 3117 | 3173 | @ </div></form> |
| 3118 | - style_load_one_js_file("ci_edit.js"); | |
| 3174 | + builtin_request_js("ci_edit.js"); | |
| 3119 | 3175 | style_footer(); |
| 3120 | 3176 | } |
| 3121 | 3177 | |
| 3122 | 3178 | /* |
| 3123 | 3179 | ** Prepare an ammended commit comment. Let the user modify it using the |
| 3124 | 3180 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -191,11 +191,11 @@ | |
| 191 | ** Options: |
| 192 | ** |
| 193 | ** -R|--repository FILE Extract info from repository FILE |
| 194 | ** -v|--verbose Show extra information about repositories |
| 195 | ** |
| 196 | ** See also: annotate, artifact, finfo, timeline |
| 197 | */ |
| 198 | void info_cmd(void){ |
| 199 | i64 fsize; |
| 200 | int verboseFlag = find_option("verbose","v",0)!=0; |
| 201 | if( !verboseFlag ){ |
| @@ -441,11 +441,11 @@ | |
| 441 | /* |
| 442 | ** Generate javascript to enhance HTML diffs. |
| 443 | */ |
| 444 | void append_diff_javascript(int sideBySide){ |
| 445 | if( !sideBySide ) return; |
| 446 | style_load_one_js_file("sbsdiff.js"); |
| 447 | } |
| 448 | |
| 449 | /* |
| 450 | ** Construct an appropriate diffFlag for text_diff() based on query |
| 451 | ** parameters and the to boolean arguments. |
| @@ -2011,27 +2011,37 @@ | |
| 2011 | manifest_destroy(pManifest); |
| 2012 | return rid; |
| 2013 | } |
| 2014 | |
| 2015 | /* |
| 2016 | ** The "z" argument is a string that contains the text of a source code |
| 2017 | ** file. This routine appends that text to the HTTP reply with line numbering. |
| 2018 | ** |
| 2019 | ** zLn is the ?ln= parameter for the HTTP query. If there is an argument, |
| 2020 | ** then highlight that line number and scroll to it once the page loads. |
| 2021 | ** If there are two line numbers, highlight the range of lines. |
| 2022 | ** Multiple ranges can be highlighed by adding additional line numbers |
| 2023 | ** separated by a non-digit character (also not one of [-,.]). |
| 2024 | */ |
| 2025 | void output_text_with_line_numbers( |
| 2026 | const char *z, |
| 2027 | const char *zLn |
| 2028 | ){ |
| 2029 | int iStart, iEnd; /* Start and end of region to highlight */ |
| 2030 | int n = 0; /* Current line number */ |
| 2031 | int i = 0; /* Loop index */ |
| 2032 | int iTop = 0; /* Scroll so that this line is on top of screen. */ |
| 2033 | Stmt q; |
| 2034 | |
| 2035 | iStart = iEnd = atoi(zLn); |
| 2036 | db_multi_exec( |
| 2037 | "CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)"); |
| @@ -2047,56 +2057,101 @@ | |
| 2047 | while( fossil_isdigit(zLn[i]) ) i++; |
| 2048 | if( iEnd<iStart ) iEnd = iStart; |
| 2049 | db_multi_exec( |
| 2050 | "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd |
| 2051 | ); |
| 2052 | iStart = iEnd = atoi(&zLn[i++]); |
| 2053 | }while( zLn[i] && iStart && iEnd ); |
| 2054 | } |
| 2055 | db_prepare(&q, "SELECT min(iStart), max(iEnd) FROM lnos"); |
| 2056 | if( db_step(&q)==SQLITE_ROW ){ |
| 2057 | iStart = db_column_int(&q, 0); |
| 2058 | iEnd = db_column_int(&q, 1); |
| 2059 | iTop = iStart - 15 + (iEnd-iStart)/4; |
| 2060 | if( iTop>iStart - 2 ) iTop = iStart-2; |
| 2061 | } |
| 2062 | db_finalize(&q); |
| 2063 | @ <pre> |
| 2064 | while( z[0] ){ |
| 2065 | n++; |
| 2066 | db_prepare(&q, |
| 2067 | "SELECT min(iStart), max(iEnd) FROM lnos" |
| 2068 | " WHERE iStart <= %d AND iEnd >= %d", n, n); |
| 2069 | if( db_step(&q)==SQLITE_ROW ){ |
| 2070 | iStart = db_column_int(&q, 0); |
| 2071 | iEnd = db_column_int(&q, 1); |
| 2072 | } |
| 2073 | db_finalize(&q); |
| 2074 | for(i=0; z[i] && z[i]!='\n'; i++){} |
| 2075 | if( n==iTop ) cgi_append_content("<span id=\"scrollToMe\">", -1); |
| 2076 | if( n==iStart ){ |
| 2077 | cgi_append_content("<div class=\"selectedText\">",-1); |
| 2078 | } |
| 2079 | cgi_printf("%6d ", n); |
| 2080 | if( i>0 ){ |
| 2081 | char *zHtml = htmlize(z, i); |
| 2082 | cgi_append_content(zHtml, -1); |
| 2083 | fossil_free(zHtml); |
| 2084 | } |
| 2085 | if( n==iTop ) cgi_append_content("</span>", -1); |
| 2086 | if( n==iEnd ) cgi_append_content("</div>", -1); |
| 2087 | else cgi_append_content("\n", 1); |
| 2088 | z += i; |
| 2089 | if( z[0]=='\n' ) z++; |
| 2090 | } |
| 2091 | if( n<iEnd ) cgi_printf("</div>"); |
| 2092 | @ </pre> |
| 2093 | if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){ |
| 2094 | style_load_one_js_file("scroll.js"); |
| 2095 | } |
| 2096 | } |
| 2097 | |
| 2098 | |
| 2099 | /* |
| 2100 | ** WEBPAGE: artifact |
| 2101 | ** WEBPAGE: file |
| 2102 | ** WEBPAGE: whatis |
| @@ -2387,25 +2442,26 @@ | |
| 2387 | if( zLn==0 || atoi(zLn)==0 ){ |
| 2388 | style_submenu_checkbox("ln", "Line Numbers", 0, 0); |
| 2389 | } |
| 2390 | blob_to_utf8_no_bom(&content, 0); |
| 2391 | zMime = mimetype_from_content(&content); |
| 2392 | @ <blockquote> |
| 2393 | if( zMime==0 ){ |
| 2394 | const char *z, *zFileName, *zExt; |
| 2395 | z = blob_str(&content); |
| 2396 | zFileName = db_text(0, |
| 2397 | "SELECT name FROM mlink, filename" |
| 2398 | " WHERE filename.fnid=mlink.fnid" |
| 2399 | " AND mlink.fid=%d", |
| 2400 | rid); |
| 2401 | zExt = zFileName ? strrchr(zFileName, '.') : 0; |
| 2402 | if( zLn ){ |
| 2403 | output_text_with_line_numbers(z, zLn); |
| 2404 | }else if( zExt && zExt[1] ){ |
| 2405 | @ <pre> |
| 2406 | @ <code class="language-%s(zExt+1)">%h(z)</code> |
| 2407 | @ </pre> |
| 2408 | }else{ |
| 2409 | @ <pre> |
| 2410 | @ %h(z) |
| 2411 | @ </pre> |
| @@ -3113,11 +3169,11 @@ | |
| 3113 | @ <input type="submit" name="apply" value="Apply Changes" /> |
| 3114 | } |
| 3115 | @ </td></tr> |
| 3116 | @ </table> |
| 3117 | @ </div></form> |
| 3118 | style_load_one_js_file("ci_edit.js"); |
| 3119 | style_footer(); |
| 3120 | } |
| 3121 | |
| 3122 | /* |
| 3123 | ** Prepare an ammended commit comment. Let the user modify it using the |
| 3124 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -191,11 +191,11 @@ | |
| 191 | ** Options: |
| 192 | ** |
| 193 | ** -R|--repository FILE Extract info from repository FILE |
| 194 | ** -v|--verbose Show extra information about repositories |
| 195 | ** |
| 196 | ** See also: [[annotate]], [[artifact]], [[finfo]], [[timeline]] |
| 197 | */ |
| 198 | void info_cmd(void){ |
| 199 | i64 fsize; |
| 200 | int verboseFlag = find_option("verbose","v",0)!=0; |
| 201 | if( !verboseFlag ){ |
| @@ -441,11 +441,11 @@ | |
| 441 | /* |
| 442 | ** Generate javascript to enhance HTML diffs. |
| 443 | */ |
| 444 | void append_diff_javascript(int sideBySide){ |
| 445 | if( !sideBySide ) return; |
| 446 | builtin_request_js("sbsdiff.js"); |
| 447 | } |
| 448 | |
| 449 | /* |
| 450 | ** Construct an appropriate diffFlag for text_diff() based on query |
| 451 | ** parameters and the to boolean arguments. |
| @@ -2011,27 +2011,37 @@ | |
| 2011 | manifest_destroy(pManifest); |
| 2012 | return rid; |
| 2013 | } |
| 2014 | |
| 2015 | /* |
| 2016 | ** The "z" argument is a string that contains the text of a source |
| 2017 | ** code file and nZ is its length in bytes. This routine appends that |
| 2018 | ** text to the HTTP reply with line numbering. |
| 2019 | ** |
| 2020 | ** zName is the content's file name, if any (it may be NULL). If that |
| 2021 | ** name contains a '.' then the part after the final '.' is used as |
| 2022 | ** the X part of a "language-X" CSS class on the generated CODE block. |
| 2023 | ** |
| 2024 | ** zLn is the ?ln= parameter for the HTTP query. If there is an argument, |
| 2025 | ** then highlight that line number and scroll to it once the page loads. |
| 2026 | ** If there are two line numbers, highlight the range of lines. |
| 2027 | ** Multiple ranges can be highlighed by adding additional line numbers |
| 2028 | ** separated by a non-digit character (also not one of [-,.]). |
| 2029 | */ |
| 2030 | void output_text_with_line_numbers( |
| 2031 | const char *z, |
| 2032 | int nZ, |
| 2033 | const char *zName, |
| 2034 | const char *zLn |
| 2035 | ){ |
| 2036 | int iStart, iEnd; /* Start and end of region to highlight */ |
| 2037 | int n = 0; /* Current line number */ |
| 2038 | int i = 0; /* Loop index */ |
| 2039 | int iTop = 0; /* Scroll so that this line is on top of screen. */ |
| 2040 | int nLine = 0; /* content line count */ |
| 2041 | int nSpans = 0; /* number of distinct zLn spans */ |
| 2042 | const char *zExt = file_extension(zName); |
| 2043 | Stmt q; |
| 2044 | |
| 2045 | iStart = iEnd = atoi(zLn); |
| 2046 | db_multi_exec( |
| 2047 | "CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)"); |
| @@ -2047,56 +2057,101 @@ | |
| 2057 | while( fossil_isdigit(zLn[i]) ) i++; |
| 2058 | if( iEnd<iStart ) iEnd = iStart; |
| 2059 | db_multi_exec( |
| 2060 | "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd |
| 2061 | ); |
| 2062 | ++nSpans; |
| 2063 | iStart = iEnd = atoi(&zLn[i++]); |
| 2064 | }while( zLn[i] && iStart && iEnd ); |
| 2065 | } |
| 2066 | /*cgi_printf("<!-- ln span count=%d -->", nSpans);*/ |
| 2067 | cgi_append_content("<table class='numbered-lines'><tbody>" |
| 2068 | "<tr><td class='line-numbers'>", -1); |
| 2069 | iStart = iEnd = 0; |
| 2070 | count_lines(z, nZ, &nLine); |
| 2071 | for( n=1 ; n<=nLine; ++n ){ |
| 2072 | const char * zAttr = ""; |
| 2073 | const char * zId = ""; |
| 2074 | if(nSpans>0 && iEnd==0){/*Grab the next range of zLn marking*/ |
| 2075 | db_prepare(&q, "SELECT iStart, iEnd FROM lnos " |
| 2076 | "WHERE iStart >= %d ORDER BY iStart", n); |
| 2077 | if( db_step(&q)==SQLITE_ROW ){ |
| 2078 | iStart = db_column_int(&q, 0); |
| 2079 | iEnd = db_column_int(&q, 1); |
| 2080 | if(!iTop){ |
| 2081 | iTop = iStart - 15 + (iEnd-iStart)/4; |
| 2082 | if( iTop>iStart - 2 ) iTop = iStart-2; |
| 2083 | } |
| 2084 | }else{ |
| 2085 | /* Note that overlapping multi-spans, e.g. 10-15+12-20, |
| 2086 | can cause us to miss a row. */ |
| 2087 | iStart = iEnd = 0; |
| 2088 | } |
| 2089 | db_finalize(&q); |
| 2090 | --nSpans; |
| 2091 | /*cgi_printf("<!-- iStart=%d, iEnd=%d -->", iStart, iEnd);*/ |
| 2092 | } |
| 2093 | if(n==iTop) { |
| 2094 | zId = " id='scrollToMe'"; |
| 2095 | } |
| 2096 | if(n==iStart){/*Figure out which CSS class(es) this line needs...*/ |
| 2097 | if(n==iEnd){ |
| 2098 | zAttr = " class='selected-line start end'"; |
| 2099 | iEnd = 0; |
| 2100 | }else{ |
| 2101 | zAttr = " class='selected-line start'"; |
| 2102 | } |
| 2103 | iStart = 0; |
| 2104 | }else if(n==iEnd){ |
| 2105 | zAttr = " class='selected-line end'"; |
| 2106 | iEnd = 0; |
| 2107 | }else if( n>iStart && n<iEnd ){ |
| 2108 | zAttr = " class='selected-line'"; |
| 2109 | } |
| 2110 | cgi_printf("<span%s%s>%6d</span>", zId, zAttr, n); |
| 2111 | } |
| 2112 | cgi_append_content("</td><td class='file-content'><pre>",-1); |
| 2113 | if(zExt && *zExt){ |
| 2114 | cgi_printf("<code class='language-%h'>",zExt); |
| 2115 | }else{ |
| 2116 | cgi_append_content("<code>", -1); |
| 2117 | } |
| 2118 | cgi_printf("%z", htmlize(z, nZ)); |
| 2119 | CX("</code></pre></td></tr></tbody></table>\n"); |
| 2120 | if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){ |
| 2121 | builtin_request_js("scroll.js"); |
| 2122 | } |
| 2123 | style_emit_fossil_js_apis(0, "dom", "copybutton", "popupwidget", |
| 2124 | "numbered-lines", 0); |
| 2125 | } |
| 2126 | |