Fossil SCM

Merge trunk, since cloning local filesystem repositories still does not work

andygoth 2020-08-16 14:45 UTC multi-remote-fix merge
Commit 719dcd29cd8a99d592793211fe60f6c0d39a450087ab5aa440ed193372802a01
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
+1 -1
--- VERSION
+++ VERSION
@@ -1,1 +1,1 @@
1
-2.12
1
+2.13
22
--- VERSION
+++ VERSION
@@ -1,1 +1,1 @@
1 2.12
2
--- VERSION
+++ VERSION
@@ -1,1 +1,1 @@
1 2.13
2
--- skins/ardoise/css.txt
+++ skins/ardoise/css.txt
@@ -497,11 +497,11 @@
497497
border: 0;
498498
border-radius: 5px;
499499
box-shadow: none;
500500
box-sizing: border-box
501501
}
502
-textarea {
502
+textarea, select {
503503
height: initial;
504504
}
505505
input[type=email]:hover,
506506
input[type=number]:hover,
507507
input[type=password]:hover,
@@ -572,10 +572,13 @@
572572
white-space: nowrap;
573573
background: #000;
574574
border: 2px solid #bbb;
575575
border-radius: 5px
576576
}
577
+table.numbered-lines td.file-content > pre {
578
+ margin-top: -2px/*offset CODE tag border*/;
579
+}
577580
pre > code {
578581
padding: 1rem 1.5rem;
579582
white-space: pre
580583
}
581584
td,
582585
--- 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
--- skins/eagle/css.txt
+++ skins/eagle/css.txt
@@ -399,11 +399,11 @@
399399
400400
div.filetreeline:hover {
401401
background-color: #7EA2D9;
402402
}
403403
404
-div.selectedText {
404
+table.numbered-lines td.line-numbers span.selected-line {
405405
background-color: #7EA2D9;
406406
}
407407
408408
.statistics-report-graph-line {
409409
background-color: #7EA2D9;
410410
--- 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
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -1000,15 +1000,15 @@
10001000
/**************************************
10011001
* Did not encounter these
10021002
*/
10031003
10041004
/* selected lines of text within a linenumbered artifact display */
1005
-div.selectedText {
1005
+table.numbered-lines td.line-numbers span.selected-line {
10061006
font-weight: bold;
10071007
color: #00f;
10081008
background-color: #d5d5ff;
1009
- border: 1px #00f solid;
1009
+ border-color: #00f;
10101010
}
10111011
10121012
/* format for missing privileges note on user setup page */
10131013
p.missingPriv {
10141014
color: #00f;
10151015
--- 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 @@
320320
** COMMAND: add
321321
**
322322
** Usage: %fossil add ?OPTIONS? FILE1 ?FILE2 ...?
323323
**
324324
** 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]].
326326
**
327327
** When adding files or directories recursively, filenames that begin
328328
** with "." are excluded by default. To include such files, add
329329
** the "--dotfiles" option to the command-line.
330330
**
@@ -359,11 +359,11 @@
359359
**
360360
** The following options are only valid with --reset:
361361
** -v|--verbose Outputs information about each --reset file.
362362
** -n|--dry-run Display instead of run actions.
363363
**
364
-** See also: addremove, rm
364
+** See also: [[addremove]], [[rm]]
365365
*/
366366
void add_cmd(void){
367367
int i; /* Loop counter */
368368
int vid; /* Currently checked out version */
369369
int nRoot; /* Full path characters in g.zLocalRoot */
@@ -541,11 +541,11 @@
541541
** than --verbose or --dry-run may be used with
542542
** --reset.
543543
** --verbose|-v Outputs information about each --reset file.
544544
** Only usable with --reset.
545545
**
546
-** See also: addremove, add
546
+** See also: [[addremove]], [[add]]
547547
*/
548548
void delete_cmd(void){
549549
int i;
550550
int removeFiles;
551551
int dryRunFlag = find_option("dry-run","n",0)!=0;
@@ -689,22 +689,22 @@
689689
/*
690690
** COMMAND: addremove
691691
**
692692
** Usage: %fossil addremove ?OPTIONS?
693693
**
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:
696696
**
697697
** * All files in the checkout but not in the repository (that is,
698698
** all files displayed using the "extras" command) are added as
699
-** if by the "add" command.
699
+** if by the "[[add]]" command.
700700
**
701701
** * All files in the repository but missing from the checkout (that is,
702702
** 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.
704704
**
705
-** The command does not "commit". You must run the "commit" separately
705
+** The command does not "[[commit]]". You must run the "[[commit]]" separately
706706
** as a separate step.
707707
**
708708
** Files and directories whose names begin with "." are ignored unless
709709
** the --dotfiles option is used.
710710
**
@@ -733,11 +733,11 @@
733733
** removed. No flags other than --verbose and
734734
** --dry-run may be used with --reset.
735735
** --verbose|-v Outputs information about each --reset file.
736736
** Only usable with --reset.
737737
**
738
-** See also: add, rm
738
+** See also: [[add]], [[rm]]
739739
*/
740740
void addremove_cmd(void){
741741
Blob path;
742742
const char *zCleanFlag;
743743
const char *zIgnoreFlag;
@@ -953,11 +953,11 @@
953953
** Move or rename one or more files or directories within the repository tree.
954954
** You can either rename a file or directory or move it to another subdirectory.
955955
**
956956
** The 'mv' command does NOT normally rename or move the files on disk.
957957
** 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]].
959959
** However, the default behavior of this command may be overridden via
960960
** command line options listed below and/or the 'mv-rm-files' setting.
961961
**
962962
** The 'rename' command never renames or moves files on disk, even when the
963963
** command line options and/or the 'mv-rm-files' setting would otherwise
@@ -966,17 +966,17 @@
966966
** WARNING: If the "--hard" option is specified -OR- the "mv-rm-files"
967967
** setting is non-zero, files WILL BE renamed or moved on disk
968968
** as well. This does NOT apply to the 'rename' command.
969969
**
970970
** 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.
976976
**
977
-** See also: changes, status
977
+** See also: [[changes]], [[status]]
978978
*/
979979
void mv_cmd(void){
980980
int i;
981981
int vid;
982982
int moveFiles;
983983
--- 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 @@
130130
wiki_render_by_mimetype(pContent, zMime);
131131
break;
132132
default:{
133133
const char *zContent = blob_str(pContent);
134134
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");
136137
}else{
137138
const char *zExt = strrchr(zName,'.');
138139
if(zExt && zExt[1]){
139140
CX("<pre><code class='language-%s'>%h</code></pre>",
140141
zExt+1, zContent);
@@ -163,10 +164,20 @@
163164
}else{
164165
CX("<pre class='udiff'>%b</pre>",&out);
165166
}
166167
blob_reset(&out);
167168
}
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
+}
168179
169180
/*
170181
** Helper for /ajax routes. Clears the CGI content buffer, sets an
171182
** HTTP error status code, and queues up a JSON response in the form
172183
** of an object:
@@ -323,10 +334,11 @@
323334
if(zRenderMode!=0){
324335
cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode);
325336
}
326337
}
327338
339
+#if INTERFACE
328340
/*
329341
** Internal mapping of ajax sub-route names to various metadata.
330342
*/
331343
struct AjaxRoute {
332344
const char *zName; /* Name part of the route after "ajax/" */
@@ -334,16 +346,17 @@
334346
int bWriteMode; /* True if requires write mode */
335347
int bPost; /* True if requires POST (i.e. CSRF
336348
** verification) */
337349
};
338350
typedef struct AjaxRoute AjaxRoute;
351
+#endif /*INTERFACE*/
339352
340353
/*
341354
** Comparison function for bsearch() for searching an AjaxRoute
342355
** list for a matching name.
343356
*/
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){
345358
const AjaxRoute * rA = (const AjaxRoute*)a;
346359
const AjaxRoute * rB = (const AjaxRoute*)b;
347360
return fossil_strcmp(rA->zName, rB->zName);
348361
}
349362
350363
--- 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
--- src/allrepo.c
+++ src/allrepo.c
@@ -219,10 +219,11 @@
219219
zCmd = "dbstat --omit-version-info -R";
220220
showLabel = 1;
221221
quiet = 1;
222222
collect_argument(&extra, "brief", "b");
223223
collect_argument(&extra, "db-check", 0);
224
+ collect_argument(&extra, "db-verify", 0);
224225
}else if( strncmp(zCmd, "extras", n)==0 ){
225226
if( showFile ){
226227
zCmd = "extras --chdir";
227228
}else{
228229
zCmd = "extras --header --chdir";
229230
--- 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 @@
617617
const char *z;
618618
content_get(ridSrc, &attach);
619619
blob_to_utf8_no_bom(&attach, 0);
620620
z = blob_str(&attach);
621621
if( zLn ){
622
- output_text_with_line_numbers(z, zLn);
622
+ output_text_with_line_numbers(z, blob_size(&attach), zName, zLn);
623623
}else{
624624
@ <pre>
625625
@ %h(z)
626626
@ </pre>
627627
}
628628
--- 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 @@
381381
/*
382382
** COMMAND: bisect
383383
**
384384
** Usage: %fossil bisect SUBCOMMAND ...
385385
**
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.
387388
**
388389
** > fossil bisect bad ?VERSION?
389390
**
390391
** Identify version VERSION as non-working. If VERSION is omitted,
391392
** the current checkout is marked as non-working.
392393
--- 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 @@
487487
**
488488
** For semantic compatibility with blob_append_full(), if newSize is
489489
** >=0x7fff000 (~2GB) then this function will trigger blob_panic(). If
490490
** it didn't, it would be possible to bypass that hard-coded limit via
491491
** 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.
492497
*/
493498
void blob_reserve(Blob *pBlob, unsigned int newSize){
494499
if(newSize>=0x7fff0000 ){
495500
blob_panic();
496501
}else if(newSize>pBlob->nUsed){
497502
--- 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
--- src/branch.c
+++ src/branch.c
@@ -382,16 +382,10 @@
382382
** Either no timezone suffix or "Z" means UTC.
383383
**
384384
** Options valid for all subcommands:
385385
**
386386
** -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
393387
*/
394388
void branch_cmd(void){
395389
int n;
396390
const char *zCmd = "list";
397391
db_find_and_open_repository(0, 0);
398392
--- 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 @@
911911
}
912912
}
913913
}
914914
@ </ul>
915915
@ </ul></div>
916
- style_load_one_js_file("tree.js");
916
+ builtin_request_js("tree.js");
917917
style_footer();
918918
919919
/* We could free memory used by sTree here if we needed to. But
920920
** the process is about to exit, so doing so would not really accomplish
921921
** anything useful. */
922922
--- 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 @@
2828
** builtin_data.h file. Include that information here:
2929
*/
3030
#include "builtin_data.h"
3131
3232
/*
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.
3436
*/
35
-const unsigned char *builtin_file(const char *zFilename, int *piSize){
37
+static int builtin_file_index(const char *zFilename){
3638
int lwr, upr, i, c;
3739
lwr = 0;
3840
upr = count(aBuiltinFiles) - 1;
3941
while( upr>=lwr ){
4042
i = (upr+lwr)/2;
@@ -42,16 +44,28 @@
4244
if( c<0 ){
4345
lwr = i+1;
4446
}else if( c>0 ){
4547
upr = i-1;
4648
}else{
47
- if( piSize ) *piSize = aBuiltinFiles[i].nByte;
48
- return aBuiltinFiles[i].pData;
49
+ return i;
4950
}
5051
}
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
+ }
5367
}
5468
const char *builtin_text(const char *zFilename){
5569
return (char*)builtin_file(zFilename, 0);
5670
}
5771
@@ -65,11 +79,11 @@
6579
*/
6680
void test_builtin_list(void){
6781
int i, size = 0;;
6882
for(i=0; i<count(aBuiltinFiles); i++){
6983
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);
7185
size += n;
7286
}
7387
if(find_option("verbose","v",0)!=0){
7488
fossil_print("%d entries totaling %d bytes\n", i, size);
7589
}
@@ -81,16 +95,18 @@
8195
** Show all built-in text files.
8296
*/
8397
void test_builtin_list_page(void){
8498
int i;
8599
style_header("Built-in Text Files");
86
- @ <ul>
100
+ @ <ol>
87101
for(i=0; i<count(aBuiltinFiles); i++){
88102
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>
90106
}
91
- @ </ul>
107
+ @ </ol>
92108
style_footer();
93109
}
94110
95111
/*
96112
** COMMAND: test-builtin-get
@@ -110,5 +126,438 @@
110126
}
111127
blob_init(&x, (const char*)pData, nByte);
112128
blob_write_to_file(&x, g.argc==4 ? g.argv[3] : "-");
113129
blob_reset(&x);
114130
}
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
+******************************************************************************/
115564
--- 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 @@
716716
}
717717
db_end_transaction(0);
718718
}
719719
720720
/*
721
-** COMMAND: bundle
721
+** COMMAND: bundle*
722722
**
723723
** Usage: %fossil bundle SUBCOMMAND ARGS...
724724
**
725725
** > fossil bundle append BUNDLE FILE...
726726
**
@@ -768,25 +768,11 @@
768768
**
769769
** Remove from the repository all files that are used exclusively
770770
** by check-ins in BUNDLE. This has the effect of undoing a
771771
** "fossil bundle import".
772772
**
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]]
788774
*/
789775
void bundle_cmd(void){
790776
const char *zSubcmd;
791777
int n;
792778
if( g.argc<4 ) usage("SUBCOMMAND BUNDLE ?OPTIONS?");
793779
--- 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 @@
303303
fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n");
304304
}else{
305305
assert( rangeEnd==0 );
306306
fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus);
307307
}
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 ){
309316
/* isConst means that the reply is guaranteed to be invariant, even
310317
** after configuration changes and/or Fossil binary recompiles. */
311318
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());
315319
}else{
316320
fprintf(g.httpOut, "Cache-control: no-cache\r\n");
317321
}
318
- if( etag_mtime()>0 ){
319
- fprintf(g.httpOut, "Last-Modified: %s\r\n",
320
- cgi_rfc822_datestamp(etag_mtime()));
321
- }
322322
323323
if( blob_size(&extraHeader)>0 ){
324324
fprintf(g.httpOut, "%s", blob_buffer(&extraHeader));
325325
}
326326
327327
--- 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 @@
440440
** --extra Display unmanaged files.
441441
** --differ Display modified and extra files.
442442
** --merge Display merge contributors.
443443
** --no-merge Do not display merge contributors.
444444
**
445
-** See also: extras, ls
445
+** See also: [[extras]], [[ls]]
446446
*/
447447
void status_cmd(void){
448448
/* Affirmative and negative flag option tables. */
449449
static const struct {
450450
const char *option; /* Flag name. */
@@ -676,11 +676,11 @@
676676
** -v|--verbose Provide extra information about each file.
677677
** -t Sort output in time order.
678678
** -r VERSION The specific check-in to list.
679679
** -R|--repository FILE Extract info from repository FILE.
680680
**
681
-** See also: changes, extras, status
681
+** See also: [[changes]], [[extras]], [[status]]
682682
*/
683683
void ls_cmd(void){
684684
int vid;
685685
Stmt q;
686686
int verboseFlag;
@@ -830,11 +830,11 @@
830830
** --header Identify the repository if there are extras
831831
** --ignore CSG Ignore files matching patterns from the argument
832832
** --rel-paths Display pathnames relative to the current working
833833
** directory.
834834
**
835
-** See also: changes, clean, status
835
+** See also: [[changes]], [[clean]], [[status]]
836836
*/
837837
void extras_cmd(void){
838838
Blob report = BLOB_INITIALIZER;
839839
const char *zIgnoreFlag = find_option("ignore",0,1);
840840
unsigned scanFlags = find_option("dotfiles",0,0)!=0 ? SCAN_ALL : 0;
@@ -954,11 +954,11 @@
954954
** --no-prompt This option disables prompting the user for input
955955
** and assumes an answer of 'No' for every question.
956956
** --temp Remove only Fossil-generated temporary files.
957957
** -v|--verbose Show all files as they are removed.
958958
**
959
-** See also: addremove, extras, status
959
+** See also: [[addremove]], [[extras]], [[status]]
960960
*/
961961
void clean_cmd(void){
962962
int allFileFlag, allDirFlag, dryRunFlag, verboseFlag;
963963
int emptyDirsFlag, dirsOnlyFlag;
964964
int disableUndo, noPrompt;
@@ -2091,11 +2091,11 @@
20912091
** year-month-day form, it may be truncated, the "T" may be replaced by
20922092
** a space, and it may also name a timezone offset from UTC as "-HH:MM"
20932093
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
20942094
** means UTC.
20952095
**
2096
-** See also: branch, changes, checkout, extras, sync
2096
+** See also: [[branch]], [[changes]], [[update]], [[extras]], [[sync]]
20972097
*/
20982098
void commit_cmd(void){
20992099
int hasChanges; /* True if unsaved changes exist */
21002100
int vid; /* blob-id of parent version */
21012101
int nrid; /* blob-id of a modified file */
21022102
--- 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 @@
279279
** --force-missing Force checkout even if content is missing
280280
** --setmtime Set timestamps of all files to match their SCM-side
281281
** times (the timestamp of the last checkin which modified
282282
** them).
283283
**
284
-** See also: update
284
+** See also: [[update]]
285285
*/
286286
void checkout_cmd(void){
287287
int forceFlag; /* Force checkout even if edits exist */
288288
int forceMissingFlag; /* Force checkout even if missing content */
289289
int keepFlag; /* Do not change any files on disk */
@@ -387,18 +387,18 @@
387387
/*
388388
** COMMAND: close*
389389
**
390390
** Usage: %fossil close ?OPTIONS?
391391
**
392
-** The opposite of "open". Close the current database connection.
392
+** The opposite of "[[open]]". Close the current database connection.
393393
** Require a -f or --force flag if there are unsaved changes in the
394394
** current check-out or if there is non-empty stash.
395395
**
396396
** Options:
397397
** --force|-f necessary to close a check out with uncommitted changes
398398
**
399
-** See also: open
399
+** See also: [[open]]
400400
*/
401401
void close_cmd(void){
402402
int forceFlag = find_option("force","f",0)!=0;
403403
db_must_be_within_tree();
404404
405405
--- 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 @@
8383
** COMMAND: clone
8484
**
8585
** Usage: %fossil clone ?OPTIONS? URI FILENAME
8686
**
8787
** 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:
9189
** ([...] denotes optional elements):
9290
**
9391
** * HTTP/HTTPS protocol:
9492
**
9593
** http[s]://[userid[:password]@]host[:port][/path]
@@ -100,18 +98,22 @@
10098
**
10199
** * Filesystem:
102100
**
103101
** [file://]path/to/repo.fossil
104102
**
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
106108
** '/' to use an absolute path.
107109
**
108
-** Note 2: Use %HH escapes for special characters in the userid and
110
+** Use %HH escapes for special characters in the userid and
109111
** password. For example "%40" in place of "@", "%2f" in place
110112
** of "/", and "%3a" in place of ":".
111113
**
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
113115
** admin user. This can be overridden using the -A|--admin-user
114116
** parameter.
115117
**
116118
** Options:
117119
** --admin-user|-A USERNAME Make USERNAME the administrator
@@ -123,11 +125,11 @@
123125
** --ssh-command|-c SSH Use SSH as the "ssh" command
124126
** --ssl-identity FILENAME Use the SSL identity if requested by the server
125127
** -u|--unversioned Also sync unversioned content
126128
** -v|--verbose Show more statistics in output
127129
**
128
-** See also: init
130
+** See also: [[init]], [[open]]
129131
*/
130132
void clone_cmd(void){
131133
char *zPassword;
132134
const char *zDefaultUser; /* Optional name of the default user */
133135
const char *zHttpAuth; /* HTTP Authorization user:pass information */
134136
--- 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
--- src/codecheck1.c
+++ src/codecheck1.c
@@ -407,11 +407,10 @@
407407
{ "smtp_send_line", 2, FMT_SAFE },
408408
{ "smtp_server_send", 2, FMT_SAFE },
409409
{ "socket_set_errmsg", 1, FMT_SAFE },
410410
{ "ssl_set_errmsg", 1, FMT_SAFE },
411411
{ "style_header", 1, FMT_HTML },
412
- { "style_js_onload", 1, FMT_HTML },
413412
{ "style_set_current_page", 1, FMT_URL },
414413
{ "style_submenu_element", 2, FMT_URL },
415414
{ "style_submenu_sql", 3, FMT_SQL },
416415
{ "webpage_error", 1, FMT_SAFE },
417416
{ "xhref", 2, FMT_URL },
418417
--- 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 @@
9494
{ "js", CONFIGSET_SKIN },
9595
{ "logo-mimetype", CONFIGSET_SKIN },
9696
{ "logo-image", CONFIGSET_SKIN },
9797
{ "background-mimetype", CONFIGSET_SKIN },
9898
{ "background-image", CONFIGSET_SKIN },
99
+ { "icon-mimetype", CONFIGSET_SKIN },
100
+ { "icon-image", CONFIGSET_SKIN },
99101
{ "timeline-block-markup", CONFIGSET_SKIN },
100102
{ "timeline-date-format", CONFIGSET_SKIN },
101103
{ "timeline-default-style", CONFIGSET_SKIN },
102104
{ "timeline-dwelltime", CONFIGSET_SKIN },
103105
{ "timeline-closetime", CONFIGSET_SKIN },
@@ -112,10 +114,11 @@
112114
{ "default-csp", CONFIGSET_SKIN },
113115
{ "sitemap-docidx", CONFIGSET_SKIN },
114116
{ "sitemap-download", CONFIGSET_SKIN },
115117
{ "sitemap-license", CONFIGSET_SKIN },
116118
{ "sitemap-contact", CONFIGSET_SKIN },
119
+ { "safe-html", CONFIGSET_SKIN },
117120
118121
#ifdef FOSSIL_ENABLE_TH1_DOCS
119122
{ "th1-docs", CONFIGSET_TH1 },
120123
#endif
121124
#ifdef FOSSIL_ENABLE_TH1_HOOKS
@@ -744,11 +747,11 @@
744747
** the remote repository at URL.
745748
**
746749
** Options:
747750
** -R|--repository FILE Extract info from repository FILE
748751
**
749
-** See also: settings, unset
752
+** See also: [[settings]], [[unset]]
750753
*/
751754
void configuration_cmd(void){
752755
int n;
753756
const char *zMethod;
754757
db_find_and_open_repository(0, 0);
755758
--- 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 @@
326326
** the named output file.
327327
**
328328
** Options:
329329
** -R|--repository FILE Extract artifacts from repository FILE
330330
**
331
-** See also: finfo
331
+** See also: [[finfo]]
332332
*/
333333
void artifact_cmd(void){
334334
int rid;
335335
Blob content;
336336
const char *zFile;
337337
--- 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
+131 -19
--- src/db.c
+++ src/db.c
@@ -116,10 +116,11 @@
116116
*/
117117
static struct DbLocalData {
118118
int nBegin; /* Nesting depth of BEGIN */
119119
int doRollback; /* True to force a rollback */
120120
int nCommitHook; /* Number of commit hooks */
121
+ int wrTxn; /* Outer-most TNX is a write */
121122
Stmt *pAllStmt; /* List of all unfinalized statements */
122123
int nPrepare; /* Number of calls to sqlite3_prepare_v2() */
123124
int nDeleteOnFail; /* Number of entries in azDeleteOnFail[] */
124125
struct sCommitHook {
125126
int (*xHook)(void); /* Functions to call at db_end_transaction() */
@@ -195,10 +196,11 @@
195196
sqlite3_commit_hook(g.db, db_verify_at_commit, 0);
196197
db.nPriorChanges = sqlite3_total_changes(g.db);
197198
db.doRollback = 0;
198199
db.zStartFile = zStartFile;
199200
db.iStartLine = iStartLine;
201
+ db.wrTxn = 0;
200202
}
201203
db.nBegin++;
202204
}
203205
/*
204206
** Begin a new transaction for writing.
@@ -209,11 +211,12 @@
209211
sqlite3_commit_hook(g.db, db_verify_at_commit, 0);
210212
db.nPriorChanges = sqlite3_total_changes(g.db);
211213
db.doRollback = 0;
212214
db.zStartFile = zStartFile;
213215
db.iStartLine = iStartLine;
214
- }else{
216
+ db.wrTxn = 1;
217
+ }else if( !db.wrTxn ){
215218
fossil_warning("read txn at %s:%d might cause SQLITE_BUSY "
216219
"for the write txn at %s:%d",
217220
db.zStartFile, db.iStartLine, zStartFile, iStartLine);
218221
}
219222
db.nBegin++;
@@ -1328,11 +1331,11 @@
13281331
);
13291332
if( rc!=SQLITE_OK ){
13301333
db_err("[%s]: %s", zDbName, sqlite3_errmsg(db));
13311334
}
13321335
db_maybe_set_encryption_key(db, zDbName);
1333
- sqlite3_busy_timeout(db, 5000);
1336
+ sqlite3_busy_timeout(db, 15000);
13341337
sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */
13351338
sqlite3_create_function(db, "user", 0, SQLITE_UTF8, 0, db_sql_user, 0, 0);
13361339
sqlite3_create_function(db, "cgi", 1, SQLITE_UTF8, 0, db_sql_cgi, 0, 0);
13371340
sqlite3_create_function(db, "cgi", 2, SQLITE_UTF8, 0, db_sql_cgi, 0, 0);
13381341
sqlite3_create_function(db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0);
@@ -1344,11 +1347,11 @@
13441347
);
13451348
if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0);
13461349
db_add_aux_functions(db);
13471350
re_add_sql_func(db); /* The REGEXP operator */
13481351
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);
13501353
return db;
13511354
}
13521355
13531356
13541357
/*
@@ -1714,16 +1717,20 @@
17141717
**
17151718
** If no valid _FOSSIL_ or .fslckout file is found, we move up one level and
17161719
** try again. Once the file is found, the g.zLocalRoot variable is set
17171720
** to the root of the repository tree and this routine returns 1. If
17181721
** 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.
17191726
**
17201727
** This routine always opens the user database regardless of whether or
17211728
** not the repository database is found. If the _FOSSIL_ or .fslckout file
17221729
** is found, it is attached to the open database connection too.
17231730
*/
1724
-int db_open_local(const char *zDbName){
1731
+int db_open_local_v2(const char *zDbName, int bRootOnly){
17251732
int i, n;
17261733
char zPwd[2000];
17271734
static const char *(aDbName[]) = { "_FOSSIL_", ".fslckout", ".fos" };
17281735
17291736
if( g.localOpen ) return 1;
@@ -1747,18 +1754,22 @@
17471754
g.localOpen = 1;
17481755
db_open_repository(zDbName);
17491756
return 1;
17501757
}
17511758
}
1759
+ if( bRootOnly ) break;
17521760
n--;
17531761
while( n>1 && zPwd[n]!='/' ){ n--; }
17541762
while( n>1 && zPwd[n-1]=='/' ){ n--; }
17551763
zPwd[n] = 0;
17561764
}
17571765
17581766
/* A checkout database file could not be found */
17591767
return 0;
1768
+}
1769
+int db_open_local(const char *zDbName){
1770
+ return db_open_local_v2(zDbName, 0);
17601771
}
17611772
17621773
/*
17631774
** Get the full pathname to the repository database file. The
17641775
** local database (the _FOSSIL_ or .fslckout database) must have already
@@ -2388,11 +2399,11 @@
23882399
** year-month-day form, it may be truncated, the "T" may be replaced by
23892400
** a space, and it may also name a timezone offset from UTC as "-HH:MM"
23902401
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
23912402
** means UTC.
23922403
**
2393
-** See also: clone
2404
+** See also: [[clone]]
23942405
*/
23952406
void create_repository_cmd(void){
23962407
char *zPassword;
23972408
const char *zTemplate; /* Repository from which to copy settings */
23982409
const char *zDate; /* Date of the initial check-in */
@@ -3070,57 +3081,158 @@
30703081
}
30713082
30723083
/*
30733084
** COMMAND: open
30743085
**
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
30763104
**
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".
30823108
**
30833109
** Options:
30843110
** --empty Initialize checkout as being empty, but still connected
30853111
** with the local repository. If you commit this checkout,
30863112
** 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
30873116
** --keep Only modify the manifest and manifest.uuid files
30883117
** --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 "."
30903120
** --setmtime Set timestamps of all files to match their SCM-side
30913121
** times (the timestamp of the last checkin which modified
30923122
** them).
3123
+** --workdir DIR Use DIR as the working directory instead of ".". The DIR
3124
+** directory is created if it does not exist.
30933125
**
3094
-** See also: close
3126
+** See also: [[close]], [[clone]]
30953127
*/
30963128
void cmd_open(void){
30973129
int emptyFlag;
30983130
int keepFlag;
30993131
int forceMissingFlag;
31003132
int allowNested;
31013133
int allowSymlinks;
31023134
int setmtimeFlag; /* --setmtime. Set mtimes on files */
3135
+ int bForce = 0; /* --force. Open even if non-empty dir */
31033136
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 */
31043142
31053143
url_proxy_options();
31063144
emptyFlag = find_option("empty",0,0)!=0;
31073145
keepFlag = find_option("keep",0,0)!=0;
31083146
forceMissingFlag = find_option("force-missing",0,0)!=0;
31093147
allowNested = find_option("nested",0,0)!=0;
31103148
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
+
31113154
31123155
/* We should be done with options.. */
31133156
verify_all_options();
31143157
31153158
if( g.argc!=3 && g.argc!=4 ){
31163159
usage("REPOSITORY-FILENAME ?VERSION?");
31173160
}
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:");
31203231
}
3121
- db_open_repository(g.argv[2]);
3232
+
3233
+ db_open_repository(zRepo);
31223234
31233235
/* Figure out which revision to open. */
31243236
if( !emptyFlag ){
31253237
if( g.argc==4 ){
31263238
g.zOpenRevision = g.argv[3];
@@ -3169,12 +3281,12 @@
31693281
** point, this will probably be the setting value from the
31703282
** repository or global configuration databases. */
31713283
g.allowSymlinks = db_get_boolean("allow-symlinks",
31723284
db_allow_symlinks_by_default());
31733285
}
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);
31763288
db_set_checkout(0);
31773289
azNewArgv[0] = g.argv[0];
31783290
g.argv = azNewArgv;
31793291
if( !emptyFlag ){
31803292
g.argc = 3;
@@ -3887,11 +3999,11 @@
38873999
** --global set or unset the given property globally instead of
38884000
** setting or unsetting it for the open repository only.
38894001
**
38904002
** --exact only consider exact name matches.
38914003
**
3892
-** See also: configuration
4004
+** See also: [[configuration]]
38934005
*/
38944006
void setting_cmd(void){
38954007
int i;
38964008
int globalFlag = find_option("global","g",0)!=0;
38974009
int exactFlag = find_option("exact",0,0)!=0;
38984010
--- 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 @@
440440
content:"'";
441441
}
442442
span.usertype:after {
443443
content:"'";
444444
}
445
-div.selectedText {
446
- font-weight: bold;
447
- color: blue;
448
- background-color: #d5d5ff;
449
- border: 1px blue solid;
450
-}
451445
p.missingPriv {
452446
color: blue;
453447
}
454448
span.wikiruleHead {
455449
font-weight: bold;
@@ -774,44 +768,93 @@
774768
div.forumTime > div > form,
775769
div.forumHierRoot > div > form {
776770
margin: 0.5em 0;
777771
}
778772
.forum-post-collapser {
773
+ /* Common style for the bottom-of-post and right-of-post
774
+ expand/collapse widgets. */
779775
font-size: 0.8em;
780
- margin-top: 0.2em;
781776
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;
782785
height: 1.75em;
783786
line-height: 1.75em;
784787
/* ^^^ Those sizes are finely tuned for the current selection of
785788
arrow characters. If those change, these should, too. Remember that
786789
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;
792790
display: flex;
793791
flex-direction: row;
794792
justify-content: space-between;
795793
}
796
-.forum-post-collapser > span {
794
+.forum-post-collapser.bottom > span {
797795
margin: 0 1em 0 1em;
798796
vertical-align: middle;
799797
}
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 {
804799
content: "⇣⇣⇣";
805800
}
801
+.forum-post-collapser.bottom.expanded > span::before {
802
+ content: "⇡⇡⇡" /*reminder: FF/Chrome cannot agree on alignment of ⮝*/;
803
+}
806804
div.forumPostBody{
807805
max-height: 50em;
808806
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: "⇡";
809846
}
810847
div.forumPostBody.expanded {
811848
max-height: initial;
812849
}
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
+
813856
div.forumSel {
814857
background-color: #cef;
815858
}
816859
div.forumObs {
817860
color: #bbb;
@@ -904,14 +947,19 @@
904947
color: darkred;
905948
background: yellow;
906949
opacity: 0.7;
907950
}
908951
.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;
913961
}
914962
input {
915963
max-width: 95%;
916964
}
917965
textarea {
@@ -919,5 +967,311 @@
919967
}
920968
img {
921969
max-width: 100%;
922970
height: auto;
923971
}
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
+}
9241278
--- 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
--- src/descendants.c
+++ src/descendants.c
@@ -348,11 +348,11 @@
348348
** -R|--repository FILE Extract info from repository FILE
349349
** -W|--width <num> Width of lines (default is to auto-detect).
350350
** Must be >20 or 0 (= no limit, resulting in a
351351
** single line per entry).
352352
**
353
-** See also: finfo, info, leaves
353
+** See also: [[finfo]], [[info]], [[leaves]]
354354
*/
355355
void descendants_cmd(void){
356356
Stmt q;
357357
int base, width;
358358
const char *zWidth;
@@ -408,11 +408,11 @@
408408
** --recompute recompute the "leaf" table in the repository DB
409409
** -W|--width <num> Width of lines (default is to auto-detect). Must be
410410
** >39 or 0 (= no limit, resulting in a single line per
411411
** entry).
412412
**
413
-** See also: descendants, finfo, info, branch
413
+** See also: [[descendants]], [[finfo]], [[info]], [[branch]]
414414
*/
415415
void leaves_cmd(void){
416416
Stmt q;
417417
Blob sql;
418418
int showAll = find_option("all", "a", 0)!=0;
419419
--- 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 @@
125125
** in the count even if it lacks the \n terminator. If an empty string
126126
** is specified, the number of lines is zero. For the purposes of this
127127
** function, a string is considered empty if it contains no characters
128128
** -OR- it contains only NUL characters.
129129
*/
130
-static int count_lines(
130
+int count_lines(
131131
const char *z,
132132
int n,
133133
int *pnLine
134134
){
135135
int nLine;
@@ -2562,11 +2562,11 @@
25622562
}
25632563
25642564
/*
25652565
** COMMAND: annotate
25662566
** COMMAND: blame
2567
-** COMMAND: praise
2567
+** COMMAND: praise*
25682568
**
25692569
** Usage: %fossil annotate|blame|praise ?OPTIONS? FILENAME
25702570
**
25712571
** Output the text of a file with markings to show when each line of the file
25722572
** was last modified. The version currently checked out is shown by default.
@@ -2596,11 +2596,11 @@
25962596
** root of the repository. Set to "trunk" or
25972597
** similar for a reverse annotation.
25982598
** -w|--ignore-all-space Ignore white space when comparing lines
25992599
** -Z|--ignore-trailing-space Ignore whitespace at line end
26002600
**
2601
-** See also: info, finfo, timeline
2601
+** See also: [[info]], [[finfo]], [[timeline]]
26022602
*/
26032603
void annotate_cmd(void){
26042604
const char *zRevision; /* Revision name, or NULL for current check-in */
26052605
Annotator ann; /* The annotation of the file */
26062606
int i; /* Loop counter */
26072607
--- 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 @@
263263
if( zEnd[0] ) blob_append(pOut, zEnd, -1);
264264
i = j;
265265
}
266266
}
267267
}
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
+}
268306
269307
/*
270308
** Attempt to reformat plain-text help into HTML for display on a webpage.
271309
**
272310
** The HTML output is appended to Blob pHtml, which should already be
@@ -305,17 +343,22 @@
305343
azEnd[0] = "";
306344
while( zHelp[0] ){
307345
i = 0;
308346
while( (c = zHelp[i])!=0
309347
&& c!='\n'
348
+ && c!='<'
310349
&& (c!='%' || strncmp(zHelp+i,"%fossil",7)!=0)
311350
){ i++; }
312351
if( c=='%' ){
313352
if( i ) blob_appendf(pHtml, "%#h", i, zHelp);
314353
zHelp += i + 1;
315
- i = 0;
316354
wantBR = 1;
355
+ continue;
356
+ }else if( c=='<' ){
357
+ if( i ) blob_appendf(pHtml, "%#h", i, zHelp);
358
+ blob_append(pHtml, "&amp;", 5);
359
+ zHelp += i + 1;
317360
continue;
318361
}
319362
if( i>2 && zHelp[0]=='>' && zHelp[1]==' ' ){
320363
isDT = 1;
321364
for(nIndent=1; nIndent<i && zHelp[nIndent]==' '; nIndent++){}
@@ -394,11 +437,12 @@
394437
}else if( wantBR ){
395438
appendMixedFont(pHtml, zHelp+nIndent, i-nIndent);
396439
blob_append(pHtml, "<br>\n", 5);
397440
wantBR = 0;
398441
}else{
399
- blob_appendf(pHtml, "%#h\n", i-nIndent, zHelp+nIndent);
442
+ appendLinked(pHtml, zHelp+nIndent, i-nIndent);
443
+ blob_append_char(pHtml, '\n');
400444
}
401445
zHelp += i+1;
402446
i = 0;
403447
if( c==0 ) break;
404448
}
@@ -409,11 +453,11 @@
409453
410454
/*
411455
** Format help text for TTY display.
412456
*/
413457
static void help_to_text(const char *zHelp, Blob *pText){
414
- int i;
458
+ int i, x;
415459
char c;
416460
for(i=0; (c = zHelp[i])!=0; i++){
417461
if( c=='%' && strncmp(zHelp+i,"%fossil",7)==0 ){
418462
if( i>0 ) blob_append(pText, zHelp, i);
419463
blob_append(pText, "fossil", 6);
@@ -426,10 +470,18 @@
426470
blob_append(pText, " ", 1);
427471
zHelp += i+2;
428472
i = -1;
429473
continue;
430474
}
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
+ }
431483
}
432484
if( i>0 ){
433485
blob_append(pText, zHelp, i);
434486
}
435487
}
@@ -447,15 +499,17 @@
447499
** -e|--everything Show all commands and pages.
448500
** -t|--test Include test- commands
449501
** -w|--www Show WWW pages.
450502
** -s|--settings Show settings.
451503
** -h|--html Transform output to HTML.
504
+** -r|--raw No output formatting.
452505
*/
453506
void test_all_help_cmd(void){
454507
int i;
455508
int mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER;
456509
int useHtml = find_option("html","h",0)!=0;
510
+ int rawOut = find_option("raw","r",0)!=0;
457511
458512
if( find_option("www","w",0) ){
459513
mask = CMDFLAG_WEBPAGE;
460514
}
461515
if( find_option("everything","e",0) ){
@@ -488,10 +542,13 @@
488542
blob_init(&html, 0, 0);
489543
help_to_html(aCommand[i].zHelp, &html);
490544
fossil_print("<h1>%h</h1>\n", aCommand[i].zName);
491545
fossil_print("%s\n<hr>\n", blob_str(&html));
492546
blob_reset(&html);
547
+ }else if( rawOut ){
548
+ fossil_print("# %s\n", aCommand[i].zName);
549
+ fossil_print("%s\n\n", aCommand[i].zHelp);
493550
}else{
494551
Blob txt;
495552
blob_init(&txt, 0, 0);
496553
help_to_text(aCommand[i].zHelp, &txt);
497554
fossil_print("# %s\n", aCommand[i].zName);
@@ -665,11 +722,11 @@
665722
const CmdOrPage *pCmd = 0;
666723
667724
style_header("Help: %s", zCmd);
668725
669726
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);
671728
if( *zCmd=='/' ){
672729
/* Some of the webpages require query parameters in order to work.
673730
** @ <h1>The "<a href='%R%s(zCmd)'>%s(zCmd)</a>" page:</h1> */
674731
@ <h1>The "%h(zCmd)" page:</h1>
675732
}else if( rc==0 && (pCmd->eCmdFlags & CMDFLAG_SETTING)!=0 ){
@@ -913,11 +970,11 @@
913970
** setting. Webpage names begin with "/". If TOPIC is omitted, a list of
914971
** topics is returned.
915972
**
916973
** The following options can be used when TOPIC is omitted:
917974
**
918
-** -a|--all List both command and auxiliary commands
975
+** -a|--all List both common and auxiliary commands
919976
** -o|--options List command-line options common to all commands
920977
** -s|--setting List setting names
921978
** -t|--test List unsupported "test" commands
922979
** -x|--aux List only auxiliary commands
923980
** -w|--www List all web pages
@@ -936,12 +993,13 @@
936993
Blob txt;
937994
if( g.argc<3 ){
938995
z = g.argv[0];
939996
fossil_print(
940997
"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);
9431001
command_list(0, CMDFLAG_1ST_TIER);
9441002
version_cmd();
9451003
return;
9461004
}
9471005
if( find_option("options","o",0) ){
@@ -1076,11 +1134,11 @@
10761134
){
10771135
helptextVtab_vtab *pNew;
10781136
int rc;
10791137
10801138
rc = sqlite3_declare_vtab(db,
1081
- "CREATE TABLE x(name,type,flags,helptext)"
1139
+ "CREATE TABLE x(name,type,flags,helptext,formatted,html)"
10821140
);
10831141
if( rc==SQLITE_OK ){
10841142
pNew = sqlite3_malloc( sizeof(*pNew) );
10851143
*ppVtab = (sqlite3_vtab*)pNew;
10861144
if( pNew==0 ) return SQLITE_NOMEM;
@@ -1160,10 +1218,24 @@
11601218
sqlite3_result_int(ctx, pPage->eCmdFlags);
11611219
break;
11621220
case 3: /* helptext */
11631221
sqlite3_result_text(ctx, pPage->zHelp, -1, SQLITE_STATIC);
11641222
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
+ }
11651237
}
11661238
return SQLITE_OK;
11671239
}
11681240
11691241
/*
11701242
--- 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, "&amp;", 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 @@
147147
{ "jad", 3, "text/vnd.sun.j2me.app-descriptor" },
148148
{ "jar", 3, "application/java-archive" },
149149
{ "jpe", 3, "image/jpeg" },
150150
{ "jpeg", 4, "image/jpeg" },
151151
{ "jpg", 3, "image/jpeg" },
152
- { "js", 2, "application/x-javascript" },
152
+ { "js", 2, "application/javascript" },
153153
{ "kar", 3, "audio/midi" },
154154
{ "latex", 5, "application/x-latex" },
155155
{ "lha", 3, "application/octet-stream" },
156156
{ "lsp", 3, "application/x-lisp" },
157157
{ "lzh", 3, "application/octet-stream" },
@@ -1132,29 +1132,34 @@
11321132
11331133
11341134
/*
11351135
** WEBPAGE: favicon.ico
11361136
**
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.
11391139
**
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"
11411141
** 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:
11441144
**
11451145
** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/>
11461146
**
11471147
*/
11481148
void favicon_page(void){
1149
- Blob favicon;
1149
+ Blob icon;
1150
+ char *zMime;
11501151
11511152
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);
11561161
}
11571162
11581163
/*
11591164
** WEBPAGE: docsrch
11601165
**
11611166
--- 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 @@
9797
const char *zIfNoneMatch;
9898
char zBuf[50];
9999
assert( zETag[0]==0 ); /* Only call this routine once! */
100100
101101
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;
103106
md5sum_init();
104107
105108
/* Always include the executable ID as part of the hash */
106109
md5sum_step_text("exe-id: ", -1);
107110
md5sum_step_text(fossil_exe_id(), -1);
108111
--- 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 @@
16421642
k = db_int(0, "SELECT count(*) FROm mmark WHERE NOT isfile");
16431643
fossil_print("Exported: %d check-ins and %d file blobs\n", k, n);
16441644
}
16451645
16461646
/*
1647
-** COMMAND: git
1647
+** COMMAND: git*
16481648
**
16491649
** Usage: %fossil git SUBCOMMAND
16501650
**
16511651
** Do incremental import or export operations between Fossil and Git.
16521652
** Subcommands:
16531653
--- 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 @@
11051105
** Remove redundant / characters
11061106
** Remove all /./ path elements.
11071107
** Convert /A/../ to just /
11081108
** If the slash parameter is non-zero, the trailing slash, if any,
11091109
** is retained.
1110
+**
1111
+** See also: file_canonical_name_dup()
11101112
*/
11111113
void file_canonical_name(const char *zOrigName, Blob *pOut, int slash){
11121114
blob_zero(pOut);
11131115
if( file_is_absolute_path(zOrigName) ){
11141116
blob_appendf(pOut, "%/", zOrigName);
@@ -1140,10 +1142,25 @@
11401142
}
11411143
#endif
11421144
blob_resize(pOut, file_simplify_name(blob_buffer(pOut),
11431145
blob_size(pOut), slash));
11441146
}
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
+}
11451162
11461163
/*
11471164
** The input is the name of an executable, such as one might
11481165
** type on a command-line. This routine resolves that name into
11491166
** a full pathname. The result is obtained from fossil_malloc()
@@ -2380,5 +2397,14 @@
23802397
changeCount);
23812398
}else{
23822399
fossil_print("Touched %d file(s)\n", changeCount);
23832400
}
23842401
}
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
+}
23852411
--- 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 @@
364364
blob_appendf(pOut, "B %s\n",
365365
pCI->pParent->zBaseline
366366
? pCI->pParent->zBaseline
367367
: pCI->zParentUuid);
368368
}
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 */));
373369
if(blob_size(&pCI->comment)!=0){
374370
blob_appendf(pOut, "C %F\n", blob_str(&pCI->comment));
375371
}else{
376372
blob_append(pOut, "C (no\\scomment)\n", 16);
377373
}
@@ -965,11 +961,12 @@
965961
** populates:
966962
**
967963
** - *zRevUuid = the fully-expanded value of zRev (owned by the
968964
** caller). zRevUuid may be NULL.
969965
**
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.
971968
**
972969
** - *frid = the RID of zFilename's blob content. May not be NULL
973970
** unless zFilename is also NULL. If BOTH of zFilename and frid are
974971
** NULL then no confirmation is done on the filename argument - only
975972
** zRev is checked.
@@ -979,38 +976,41 @@
979976
** returns false, it queues up an error response and the caller must
980977
** return immediately.
981978
*/
982979
static int fileedit_ajax_setup_filerev(const char * zRev,
983980
char ** zRevUuid,
984
- int * vid,
981
+ int * pVid,
985982
const char * zFilename,
986983
int * frid){
987984
char * zFileUuid = 0; /* file content UUID */
988985
const int checkFile = zFilename!=0 || frid!=0;
986
+ int vid = 0;
989987
990988
if(checkFile && !fileedit_ajax_check_filename(zFilename)){
991989
return 0;
992990
}
993
- *vid = symbolic_name_to_rid(zRev, "ci");
994
- if(0==*vid){
991
+ vid = symbolic_name_to_rid(zRev, "ci");
992
+ if(0==vid){
995993
ajax_route_error(404,"Cannot resolve name as a checkin: %s",
996994
zRev);
997995
return 0;
998
- }else if(*vid<0){
996
+ }else if(vid<0){
999997
ajax_route_error(400,"Checkin name is ambiguous: %s",
1000998
zRev);
1001999
return 0;
1000
+ }else if(pVid!=0){
1001
+ *pVid = vid;
10021002
}
10031003
if(checkFile){
1004
- zFileUuid = fileedit_file_uuid(zFilename, *vid, 0);
1004
+ zFileUuid = fileedit_file_uuid(zFilename, vid, 0);
10051005
if(zFileUuid==0){
10061006
ajax_route_error(404, "Checkin does not contain file.");
10071007
return 0;
10081008
}
10091009
}
10101010
if(zRevUuid!=0){
1011
- *zRevUuid = rid_to_uuid(*vid);
1011
+ *zRevUuid = rid_to_uuid(vid);
10121012
}
10131013
if(checkFile){
10141014
assert(zFileUuid!=0);
10151015
if(frid!=0){
10161016
*frid = fast_uuid_to_rid(zFileUuid);
@@ -1290,10 +1290,92 @@
12901290
end_fail:
12911291
#undef fail
12921292
fossil_free(zFileUuid);
12931293
return rc ? rc : 500;
12941294
}
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
+}
12951377
12961378
/*
12971379
** AJAX route /fileedit?ajax=filelist
12981380
**
12991381
** Fetches a JSON-format list of leaves and/or filenames for use in
@@ -1318,66 +1400,27 @@
13181400
** }
13191401
**
13201402
** On error it produces a JSON response as documented for
13211403
** ajax_route_error().
13221404
*/
1323
-static void fileedit_ajax_filelist(void){
1405
+static void fileedit_ajax_filelist(){
13241406
const char * zCi = PD("checkin",P("ci"));
1325
- Blob sql = empty_blob;
1326
- Stmt q = empty_Stmt;
1327
- int i = 0;
13281407
13291408
if(!ajax_route_bootstrap(1,0)){
13301409
return;
13311410
}
13321411
cgi_set_content_type("application/json");
13331412
if(zCi!=0){
13341413
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)){
13371415
/* Error already reported */
13381416
return;
13391417
}
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);
13571420
}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);
13791422
}else{
13801423
ajax_route_error(500, "Unhandled URL argument.");
13811424
}
13821425
}
13831426
@@ -1484,52 +1527,60 @@
14841527
}
14851528
14861529
/*
14871530
** WEBPAGE: fileedit
14881531
**
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.
14911539
**
14921540
** Optional query parameters:
14931541
**
14941542
** filename=FILENAME Repo-relative path to the file.
14951543
** 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.
15121556
*/
15131557
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 */
15181558
const char * zFileMime = 0; /* File mime type guess */
15191559
CheckinMiniInfo cimi; /* Checkin state */
15201560
int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */
15211561
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. */
15281562
const char *zAjax = P("name"); /* Name of AJAX route for
15291563
sub-dispatching. */
15301564
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
+
15311582
/* Allow no access to this page without check-in privilege */
15321583
login_check_credentials();
15331584
if( !g.perm.Write ){
15341585
if(zAjax!=0){
15351586
ajax_route_error(403, "Write permissions required.");
@@ -1590,14 +1641,11 @@
15901641
** transaction cleanly.
15911642
*/
15921643
{
15931644
int isMissingArg = 0;
15941645
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);
15991647
zFileMime = mimetype_from_name(cimi.zFilename);
16001648
}else if(isMissingArg!=0){
16011649
/* Squelch these startup warnings - they're non-fatal now but
16021650
** used to be fatal. */
16031651
blob_reset(&err);
@@ -1626,34 +1674,39 @@
16261674
/* Status bar */
16271675
CX("<div id='fossil-status-bar' "
16281676
"title='Status message area. Double-click to clear them.'>"
16291677
"Status messages will go here.</div>\n"
16301678
/* 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>");
16311684
16321685
/* Main tab container... */
16331686
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. */
16341690
16351691
/***** File/version info tab *****/
16361692
{
16371693
CX("<div id='fileedit-tab-fileselect' "
16381694
"data-tab-parent='fileedit-tabs' "
1639
- "data-tab-label='File Info &amp; Selection'"
1695
+ "data-tab-label='File Selection' "
1696
+ "class='hidden'"
16401697
">");
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>");
16461698
CX("<div id='fileedit-file-selector'></div>");
16471699
CX("</div>"/*#fileedit-tab-fileselect*/);
16481700
}
16491701
16501702
/******* Content tab *******/
16511703
{
16521704
CX("<div id='fileedit-tab-content' "
16531705
"data-tab-parent='fileedit-tabs' "
1654
- "data-tab-label='File Content'"
1706
+ "data-tab-label='File Content' "
1707
+ "class='hidden'"
16551708
">");
16561709
CX("<div class='flex-container flex-row child-gap-small'>");
16571710
CX("<button class='fileedit-content-reload confirmer' "
16581711
"title='Reload the file from the server, discarding "
16591712
"any local edits. To help avoid accidental loss of "
@@ -1668,22 +1721,22 @@
16681721
"150%", 150, "175%", 175,
16691722
"200%", 200, NULL);
16701723
CX("</div>");
16711724
CX("<div class='flex-container flex-column stretch'>");
16721725
CX("<textarea name='content' id='fileedit-content-editor' "
1673
- "class='fileedit' "
1674
- "rows='20' cols='80'>");
1726
+ "class='fileedit' rows='25'>");
16751727
CX("</textarea>");
16761728
CX("</div>"/*textarea wrapper*/);
16771729
CX("</div>"/*#tab-file-content*/);
16781730
}
16791731
16801732
/****** Preview tab ******/
16811733
{
16821734
CX("<div id='fileedit-tab-preview' "
16831735
"data-tab-parent='fileedit-tabs' "
1684
- "data-tab-label='Preview'"
1736
+ "data-tab-label='Preview' "
1737
+ "class='hidden'"
16851738
">");
16861739
CX("<div class='fileedit-options flex-container flex-row'>");
16871740
CX("<button id='btn-preview-refresh' "
16881741
"data-f-preview-from='fileContent' "
16891742
/* ^^^ fossil.page[methodName]() OR text source elem ID,
@@ -1741,14 +1794,16 @@
17411794
17421795
/****** Diff tab ******/
17431796
{
17441797
CX("<div id='fileedit-tab-diff' "
17451798
"data-tab-parent='fileedit-tabs' "
1746
- "data-tab-label='Diff'"
1799
+ "data-tab-label='Diff' "
1800
+ "class='hidden'"
17471801
">");
17481802
1749
- CX("<div class='fileedit-options flex-container flex-row' "
1803
+ CX("<div class='fileedit-options flex-container "
1804
+ "flex-row child-gap-small' "
17501805
"id='fileedit-tab-diff-buttons'>");
17511806
CX("<button class='sbs'>Side-by-side</button>"
17521807
"<button class='unified'>Unified</button>");
17531808
if(0){
17541809
/* For the time being let's just ignore all whitespace
@@ -1773,11 +1828,12 @@
17731828
}
17741829
17751830
/****** Commit ******/
17761831
CX("<div id='fileedit-tab-commit' "
17771832
"data-tab-parent='fileedit-tabs' "
1778
- "data-tab-label='Commit'"
1833
+ "data-tab-label='Commit' "
1834
+ "class='hidden'"
17791835
">");
17801836
{
17811837
/******* Commit flags/options *******/
17821838
CX("<div class='fileedit-options flex-container flex-row'>");
17831839
style_labeled_checkbox("cb-dry-run",
@@ -1894,11 +1950,12 @@
18941950
CX("</div>"/*#fileedit-tab-commit*/);
18951951
18961952
/****** Help/Tips ******/
18971953
CX("<div id='fileedit-tab-help' "
18981954
"data-tab-parent='fileedit-tabs' "
1899
- "data-tab-label='Help'"
1955
+ "data-tab-label='Help' "
1956
+ "class='hidden'"
19001957
">");
19011958
{
19021959
CX("<h1>Help &amp; Tips</h1>");
19031960
CX("<ul>");
19041961
CX("<li><strong>Only files matching the <code>fileedit-glob</code> "
@@ -1922,61 +1979,81 @@
19221979
"file/check-in combination are discarded.</li>");
19231980
CX("</ul>");
19241981
}
19251982
CX("</div>"/*#fileedit-tab-help*/);
19261983
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();
19611988
/*
19621989
** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is
19631990
** 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.
19651995
*/
19661996
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. */
19702004
style_emit_script_tag(0,0);
19712005
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){"
19742050
"fossil.error(e); console.error('Exception:',e);"
1975
- "}\n",
1976
- &endScript);
1977
- CX("})();");
2051
+ "}\n");
2052
+ CX("})();")/*anonymous function*/;
19782053
style_emit_script_tag(1,0);
19792054
}
2055
+ blob_reset(&err);
2056
+ CheckinMiniInfo_cleanup(&cimi);
19802057
db_end_transaction(0);
19812058
style_footer();
19822059
}
19832060
--- 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 &amp; 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 &amp; 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 &amp; 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 @@
5454
** -s|--status select status mode (print a status indicator for FILE)
5555
** -W|--width <num> Width of lines (default is to auto-detect). Must be
5656
** >22 or 0 (= no limit, resulting in a single line per
5757
** entry).
5858
**
59
-** See also: artifact, cat, descendants, info, leaves
59
+** See also: [[artifact]], [[cat]], [[descendants]], [[info]], [[leaves]]
6060
*/
6161
void finfo_cmd(void){
6262
db_must_be_within_tree();
6363
if( find_option("status","s",0) ){
6464
Stmt q;
@@ -245,11 +245,11 @@
245245
**
246246
** Options:
247247
** -R|--repository FILE Extract artifacts from repository FILE
248248
** -r VERSION The specific check-in containing the file
249249
**
250
-** See also: finfo
250
+** See also: [[finfo]]
251251
*/
252252
void cat_cmd(void){
253253
int i;
254254
Blob content, fname;
255255
const char *zRev;
256256
--- 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 @@
749749
forumthread_delete(pThread);
750750
return target;
751751
}
752752
753753
/*
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.
759755
*/
760
-static void forum_emit_page_js(){
756
+static void forumpost_emit_page_js(){
761757
static int once = 0;
762758
if(0==once){
763759
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");
768764
}
769765
}
770766
771767
/*
772768
** WEBPAGE: forumpost
@@ -894,11 +890,11 @@
894890
}else{
895891
style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName);
896892
style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName);
897893
forum_display_hierarchical(froot, fpid);
898894
}
899
- forum_emit_page_js();
895
+ forumpost_emit_page_js();
900896
style_footer();
901897
}
902898
903899
/*
904900
** Return true if a forum post should be moderated.
905901
--- 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 @@
1414
var h = x[0].scrollHeight;
1515
var y = absoluteY(x[0]);
1616
if( w>h ) y = y + (h-w)/2;
1717
if( y>0 ) window.scrollTo(0, y);
1818
}
19
-})()
19
+})();
2020
--- 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
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -1,6 +1,18 @@
11
"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
+})();
214
(function(global){
315
/* Bootstrapping bits for the global.fossil object. Must be
416
loaded after style.c:style_emit_script_tag() has initialized
517
that object.
618
*/
@@ -17,10 +29,25 @@
1729
}
1830
const d = new Date();
1931
return d.toISOString().replace(f.rx1,'').split('T').join(' ');
2032
};
2133
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
+
2249
/*
2350
** By default fossil.message() sends its arguments console.debug(). If
2451
** fossil.message.targetElement is set, it is assumed to be a DOM
2552
** element, its innerText gets assigned to the concatenation of all
2653
** arguments (with a space between each), and the CSS 'error' class is
@@ -30,11 +57,14 @@
3057
** Returns this object.
3158
*/
3259
F.message = function f(msg){
3360
const args = Array.prototype.slice.call(arguments,0);
3461
const tgt = f.targetElement;
35
- if(args.length) args.unshift(timestring(),'UTC:');
62
+ if(args.length) args.unshift(
63
+ localTimeString()+':'
64
+ //timestring(),'UTC:'
65
+ );
3666
if(tgt){
3767
tgt.classList.remove('error');
3868
tgt.innerText = args.join(' ');
3969
}
4070
else{
@@ -107,20 +137,20 @@
107137
repoUrl( repoRelativePath [,urlParams] )
108138
109139
Creates a URL by prepending this.rootPath to the given path
110140
(which must be relative from the top of the site, without a
111141
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
113143
a leading '?'. If it's an object, all of its properties get
114144
appended to the URL in that form.
115145
*/
116146
F.repoUrl = function(path,urlParams){
117147
if(!urlParams) return this.rootPath+path;
118148
const url=[this.rootPath,path];
119149
url.push('?');
120150
if('string'===typeof urlParams) url.push(urlParams);
121
- else if('object'===typeof urlParams){
151
+ else if(urlParams && 'object'===typeof urlParams){
122152
this.encodeUrlArgs(urlParams, url);
123153
}
124154
return url.join('');
125155
};
126156
127157
--- 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
--- src/fossil.confirmer.js
+++ src/fossil.confirmer.js
@@ -81,10 +81,21 @@
8181
action has been confirmed, immediately before the onconfirm or
8282
ontimeout callback. The intention of the callback is to update the
8383
label of the target element. If .ticks is set but .ontick is not
8484
then a default implementation is used which updates the element with
8585
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.
8697
8798
.debug = boolean. If truthy, it sends some debug output to the dev
8899
console to track what it's doing.
89100
90101
Various notes:
@@ -99,17 +110,26 @@
99110
- Due to the nature of multi-threaded code, it is potentially possible
100111
that confirmation and timeout actions BOTH happen if the user
101112
triggers the associated action at "just the right millisecond"
102113
before the timeout is triggered.
103114
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.
108125
109126
Terse Change history:
110127
128
+- 20200811
129
+ - Added pinSize option.
130
+
111131
- 20200507:
112132
- Add a tick-based countdown in order to more easily support
113133
updating the target element with the countdown.
114134
115135
- 20200506:
@@ -138,14 +158,26 @@
138158
const isInput = f.isInput(target);
139159
const updateText = function(msg){
140160
if(isInput) target.value = msg;
141161
else target.innerHTML = msg;
142162
}
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
+ }
143175
updateText(this.opt.initialText);
144176
if(this.opt.ticks && !this.opt.ontick){
145177
this.opt.ontick = function(tick){
146
- updateText("("+tick+") "+self.opt.confirmText);
178
+ updateText(formatCountdown(self.opt.confirmText,tick));
147179
};
148180
}
149181
this.setClasses(false);
150182
this.doTimeout = function() {
151183
if(this.timerID){
@@ -268,15 +300,16 @@
268300
};
269301
/**
270302
The default options for initConfirmer(). Tweak them to set the
271303
defaults. A couple of them (initialText and confirmText) are
272304
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.
274307
*/
275308
F.confirmer.defaultOpts = {
276
- timeout:3000,
277
- ticks: undefined,
309
+ timeout:undefined,
310
+ ticks: 3,
278311
ticktime: 998/*not *quite* 1000*/,
279312
onconfirm: undefined,
280313
ontimeout: undefined,
281314
onactivate: undefined,
282315
classInitial: '',
283316
284317
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
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -465,10 +465,103 @@
465465
e = src.querySelector(x);
466466
if(!e){
467467
e = new Error("Cannot find DOM element: "+x);
468468
console.error(e, src);
469469
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
+ }
470563
}
471564
return e;
472565
};
473566
474567
return F.dom = dom;
475568
--- 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
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -1,10 +1,13 @@
11
"use strict";
22
/**
33
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
69
similar (but not identical) to the not-quite-ubiquitous
710
window.fetch().
811
912
JS usages:
1013
@@ -87,19 +90,21 @@
8790
function with the same name, e.g. fossil.fetch.onload(). The
8891
default onload/onerror implementations route the data through the
8992
dev console and (for onerror()) through fossil.error(). The default
9093
beforesend/aftersend are no-ops. Individual pages may overwrite
9194
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?).
9398
9499
Note that this routine may add properties to the 2nd argument, so
95100
that instance should not be kept around for later use.
96101
97102
Returns this object, noting that the XHR request is asynchronous,
98103
and still in transit (or has yet to be sent) when that happens.
99104
*/
100
-window.fossil.fetch = function f(uri,opt){
105
+fossil.fetch = function f(uri,opt){
101106
const F = fossil;
102107
if(!f.onload){
103108
f.onload = (r)=>console.debug('fossil.fetch() XHR response:',r);
104109
}
105110
if(!f.onerror){
@@ -106,11 +111,11 @@
106111
f.onerror = function(e/*exception*/){
107112
console.error("fossil.fetch() XHR error:",e);
108113
if(e instanceof Error) F.error('Exception:',e);
109114
else F.error("Unknown error in handling of XHR request.");
110115
};
111
- }/*f.onerror()*/
116
+ }
112117
if(!f.parseResponseHeaders){
113118
f.parseResponseHeaders = function(h){
114119
const rc = {};
115120
if(!h) return rc;
116121
const ar = h.trim().split(/[\r\n]+/);
@@ -142,11 +147,11 @@
142147
|| payload instanceof Array)){
143148
payload = JSON.stringify(payload);
144149
opt.contentType = 'application/json';
145150
}
146151
}
147
- const url=[F.repoUrl(uri,opt.urlParams)],
152
+ const url=[f.urlTransform(uri,opt.urlParams)],
148153
x=new XMLHttpRequest();
149154
if('json'===opt.responseType){
150155
/* 'json' is an extension to the supported XHR.responseType
151156
list. We use it as a flag to tell us to JSON.parse()
152157
the response. */
@@ -201,8 +206,17 @@
201206
if(undefined!==payload) x.send(payload);
202207
else x.send();
203208
return this;
204209
};
205210
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);
209223
210224
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 (
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -121,11 +121,11 @@
121121
and that would be horribly inefficient (meaning "battery-consuming"
122122
on mobile devices).
123123
*/
124124
const $stash = {
125125
keys: {
126
- index: F.page.name+'/index'
126
+ index: F.page.name+'.index'
127127
},
128128
/**
129129
index: {
130130
"CHECKIN_HASH:FILENAME": {file info w/o content}
131131
...
@@ -149,27 +149,12 @@
149149
/** Returns the index object, fetching it from the stash or creating
150150
it anew on the first call. */
151151
getIndex: function(){
152152
if(!this.index){
153153
this.index = F.storage.getJSON(
154
- this.keys.index, undefined
154
+ this.keys.index, {}
155155
);
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
- }
171156
}
172157
return this.index;
173158
},
174159
_fireStashEvent: function(){
175160
if(this._disableNextEvent) delete this._disableNextEvent;
@@ -300,30 +285,44 @@
300285
this.e.selectCi,
301286
this.e.selectFiles
302287
),"Loading leaves...");
303288
D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi);
304289
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
+ }
325324
},
326325
/**
327326
Loads the file list for the given checkin UUID. It uses a
328327
cached copy on subsequent calls for the same UUID. If passed a
329328
falsy value, it instead clears and disables the file selection
@@ -387,20 +386,20 @@
387386
Initializes the checkin/file selector widget. Must only be
388387
called once.
389388
*/
390389
init: function(){
391390
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'),
393392
selFiles = this.e.selectFiles
394393
= D.addClass(D.select(), 'file-list'),
395394
btnLoad = this.e.btnLoadFile =
396395
D.addClass(D.button("Load file"), "flex-shrink"),
397396
filesLabel = this.e.fileListLabel =
398397
D.addClass(D.div(),'flex-shrink','file-list-label'),
399398
ciLabelWrapper = D.addClass(
400399
D.div(), 'flex-container','flex-row', 'flex-shrink',
401
- 'stretch'
400
+ 'stretch', 'child-gap-small'
402401
),
403402
btnReload = D.addClass(
404403
D.button('Reload'), 'flex-shrink'
405404
),
406405
ciLabel = this.e.ciListLabel =
@@ -410,34 +409,47 @@
410409
D.attr(selFiles, 'title',
411410
"The list of editable files for the selected checkin.");
412411
D.attr(btnLoad, 'title',
413412
"Load the selected file into the editor.");
414413
D.disable(selCi, selFiles, btnLoad);
415
- D.attr(selFiles, 'size', 10);
414
+ D.attr(selFiles, 'size', 12);
416415
D.append(
417416
this.e.container,
417
+ ciLabel,
418418
D.append(ciLabelWrapper,
419
- btnReload, ciLabel),
420
- selCi,
419
+ selCi,
420
+ btnReload),
421421
filesLabel,
422422
selFiles,
423423
/* Use a wrapper for btnLoad so that the button itself does not
424424
stretch to fill the parent width: */
425425
D.append(D.addClass(D.div(), 'flex-shrink'), btnLoad)
426426
);
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
+
427439
this.loadLeaves();
428440
selCi.addEventListener(
429441
'change', (e)=>this.loadFiles(e.target.value), false
430442
);
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);
439451
btnReload.addEventListener(
440452
'click', (e)=>this.loadLeaves(), false
441453
);
442454
delete this.init;
443455
}
@@ -453,11 +465,11 @@
453465
D.attr(D.div(),'id','fileedit-stash-selector'),
454466
'input-with-label'
455467
);
456468
const sel = this.e.select = D.select();
457469
const btnClear = this.e.btnClear
458
- = D.addClass(D.button("Clear"),'hidden');
470
+ = D.button("Discard Edits");
459471
D.append(wrapper, "Local edits (",
460472
D.append(D.code(),
461473
F.storage.storageImplName()),
462474
"):",
463475
sel, btnClear);
@@ -472,23 +484,34 @@
472484
F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
473485
sel.addEventListener('change',function(e){
474486
const opt = this.selectedOptions[0];
475487
if(opt && opt._finfo) P.loadFile(opt._finfo);
476488
});
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
- });
482489
if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/
483490
D.append(wrapper, D.append(
484491
D.addClass(D.span(),'warning'),
485492
"Warning: persistent storage is not available, "+
486493
"so uncomitted edits will not survive a page reload."
487494
));
488495
}
489496
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!*/);
490513
$stash._fireStashEvent(/*read the page-load-time stash*/);
491514
delete this.init;
492515
},
493516
/**
494517
Regenerates the edit selection list.
@@ -521,20 +544,20 @@
521544
return;
522545
}
523546
D.enable(this.e.select);
524547
D.removeClass(this.e.btnClear, 'hidden');
525548
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:''};
527550
ilist.sort(f.compare).forEach(function(finfo,n){
528551
const key = stasher.indexKey(finfo),
529552
branch = finfo.branch
530553
|| P.fileSelectWidget.checkinBranchName(finfo.checkin)||'';
531554
/* Remember that we don't know the branch name for non-leaf versions
532555
which P.fileSelectWidget() has never seen/cached. */
533556
const opt = D.option(
534557
self.e.select, n+1/*value is (almost) irrelevant*/,
535
- [F.hashDigits(finfo.checkin, 6), ' [',branch||'?branch?','] ',
558
+ [F.hashDigits(finfo.checkin), ' [',branch||'?branch?','] ',
536559
f.timestring(new Date(finfo.stashTime)),' ',
537560
false ? finfo.filename : F.shortenFilename(finfo.filename)
538561
].join('')
539562
);
540563
opt._finfo = finfo;
@@ -637,11 +660,11 @@
637660
previewTarget: E('#fileedit-tab-preview-wrapper'),
638661
manifestTarget: E('#fileedit-manifest'),
639662
diffTarget: E('#fileedit-tab-diff-wrapper'),
640663
cbIsExe: E('input[type=checkbox][name=exec_bit]'),
641664
cbManifest: E('input[type=checkbox][name=include_manifest]'),
642
- fsFileVersionDetails: E('#file-version-details'),
665
+ editStatus: E('#fileedit-edit-status'),
643666
tabs:{
644667
content: E('#fileedit-tab-content'),
645668
preview: E('#fileedit-tab-preview'),
646669
diff: E('#fileedit-tab-diff'),
647670
commit: E('#fileedit-tab-commit'),
@@ -659,24 +682,24 @@
659682
}else{
660683
P.e.taComment = P.e.taCommentSmall;
661684
D.addClass(P.e.taCommentBig, 'hidden');
662685
}
663686
D.removeClass(P.e.taComment, 'hidden');
664
-
665687
P.tabs.e.container.insertBefore(
666688
/* Move the status bar between the tab buttons and
667689
tab panels. Seems to be the best fit in terms of
668690
functionality and visibility. */
669691
E('#fossil-status-bar'), P.tabs.e.tabs
670692
);
693
+ P.tabs.e.container.insertBefore(P.e.editStatus, P.tabs.e.tabs);
671694
672695
P.tabs.addEventListener(
673696
/* Set up auto-refresh of the preview tab... */
674697
'before-switch-to', function(ev){
675698
if(ev.detail===P.e.tabs.preview){
676699
P.baseHrefForFile();
677
- if(P.e.cbAutoPreview.checked) P.preview();
700
+ if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview();
678701
}else if(ev.detail===P.e.tabs.diff){
679702
/* Work around a weird bug where the page gets wider than
680703
the window when the diff tab is NOT in view and the
681704
current SBS diff widget is wider than the window. When
682705
the diff IS in view then CSS overflow magically reduces
@@ -715,13 +738,14 @@
715738
);
716739
P.e.btnCommit.addEventListener(
717740
"click",(e)=>P.commit(), false
718741
);
719742
F.confirmer(P.e.btnReload, {
743
+ pinSize: true,
720744
confirmText: "Really reload, losing edits?",
721745
onconfirm: (e)=>P.unstashContent().loadFile(),
722
- ticks: 3
746
+ ticks: F.config.confirmerButtonTicks
723747
});
724748
E('#comment-toggle').addEventListener(
725749
"click",(e)=>P.toggleCommentMode(), false
726750
);
727751
@@ -774,11 +798,14 @@
774798
}
775799
776800
P.addEventListener(
777801
// Clear certain views when new content is loaded/set
778802
'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
+ }
780807
);
781808
P.addEventListener(
782809
// Clear certain views after a non-dry-run commit
783810
'fileedit-committed',
784811
(e)=>{
@@ -789,11 +816,10 @@
789816
);
790817
791818
P.fileSelectWidget.init();
792819
P.stashWidget.init(
793820
P.e.tabs.content.lastElementChild
794
- //P.e.tabs.fileSelect.querySelector("h1")
795821
);
796822
}/*F.onPageLoad()*/);
797823
798824
/**
799825
Getter (if called with no args) or setter (if passed an arg) for
@@ -819,11 +845,11 @@
819845
For use when installing a custom editor widget. Pass it the
820846
getter and setter callbacks to fetch resp. set the content of the
821847
custom widget. They will be triggered via
822848
P.fileContent(). Returns this object.
823849
*/
824
- P.setFileContentMethods = function(getter, setter){
850
+ P.setContentMethods = function(getter, setter){
825851
this.fileContent.get = getter;
826852
this.fileContent.set = setter;
827853
return this;
828854
};
829855
@@ -929,63 +955,55 @@
929955
updateVersion() updates the filename and version in various UI
930956
elements...
931957
932958
Returns this object.
933959
*/
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
+ }
935965
if(1===arguments.length){/*assume object*/
936966
this.finfo = arguments[0];
937967
file = this.finfo.filename;
938968
rev = this.finfo.checkin;
939969
}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
+ }
943974
}else{
944975
this.finfo = {filename:file,checkin:rev};
945976
}
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')
975997
);
976998
const purlArgs = F.encodeUrlArgs({
977999
filename: this.finfo.filename,
9781000
checkin: rUrl
9791001
},false,true);
9801002
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);
9871005
return this;
9881006
};
9891007
9901008
/**
9911009
loadFile() loads (file,checkinVersion) and updates the relevant
@@ -1032,10 +1050,11 @@
10321050
mimetype: headers['content-type'].split(';').shift()
10331051
});
10341052
self.tabs.switchToTab(self.e.tabs.content);
10351053
self.e.cbIsExe.checked = self.finfo.isExe;
10361054
self.fileContent(r);
1055
+ P.previewNeedsUpdate = true;
10371056
self.dispatchEvent('fileedit-file-loaded', self.finfo);
10381057
};
10391058
const semiFinfo = {filename: file, checkin: rev};
10401059
const stashFinfo = this.getStashedFinfo(semiFinfo);
10411060
if(stashFinfo){ // fake a response from the stash...
@@ -1115,10 +1134,11 @@
11151134
P.selectPreviewMode(P.previewModes[header]);
11161135
if('wiki'===header) P.baseHrefForFile();
11171136
else P.baseHrefRestore();
11181137
callback(r);
11191138
F.message('Updated preview.');
1139
+ P.previewNeedsUpdate = false;
11201140
P.dispatchEvent('fileedit-preview-updated',{
11211141
previewMode: P.previewModes.current,
11221142
mimetype: P.finfo.mimetype,
11231143
element: P.e.previewTarget
11241144
});
@@ -1290,10 +1310,11 @@
12901310
}else{
12911311
$stash.updateFile(fi, P.fileContent());
12921312
}
12931313
F.message("Stashed change to",F.hashDigits(fi.checkin),fi.filename);
12941314
$stash.prune();
1315
+ this.previewNeedsUpdate = true;
12951316
}
12961317
return this;
12971318
};
12981319
12991320
/**
@@ -1301,10 +1322,11 @@
13011322
F.storage. Returns this.
13021323
*/
13031324
P.unstashContent = function(){
13041325
const finfo = arguments[0] || this.finfo;
13051326
if(finfo){
1327
+ this.previewNeedsUpdate = true;
13061328
$stash.unstash(finfo);
13071329
//console.debug("Unstashed",finfo);
13081330
F.message("Unstashed",F.hashDigits(finfo.checkin),finfo.filename);
13091331
}
13101332
return this;
13111333
--- 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
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -11,26 +11,64 @@
1111
return function(ev){
1212
if(ev) ev.preventDefault();
1313
const wasExpanded = widget.classList.contains('expanded');
1414
widget.classList.toggle('expanded');
1515
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
+ }
1726
return false;
1827
};
1928
};
29
+
2030
/* Adds an Expand/Collapse toggle to all div.forumPostBody
2131
elements which are deemed "too large" (those for which
2232
scrolling is currently activated because they are taller than
2333
their max-height). */
2434
document.querySelectorAll(
2535
'div.forumHier, div.forumTime, div.forumHierRoot'
26
- ).forEach(function(forumPostWrapper){
36
+ ).forEach(function f(forumPostWrapper){
2737
const content = forumPostWrapper.querySelector('div.forumPostBody');
2838
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');
3270
widget.addEventListener('click', widgetEventHandler, false);
3371
/** Append 3 children, which CSS will evenly space across the
3472
widget. This improves visibility over having the label
3573
in only the left, right, or center. */
3674
var i = 0;
@@ -38,14 +76,12 @@
3876
if(content.nextSibling){
3977
forumPostWrapper.insertBefore(widget, content.nextSibling);
4078
}else{
4179
forumPostWrapper.appendChild(widget);
4280
}
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();
4884
});
4985
})/*onload callback*/;
5086
5187
})(window.fossil);
5288
5389
ADDED src/fossil.page.wikiedit.js
5490
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;
--- src/fossil.tabs.js
+++ src/fossil.tabs.js
@@ -3,30 +3,53 @@
33
const E = (s)=>document.querySelector(s),
44
EA = (s)=>document.querySelectorAll(s),
55
D = F.dom;
66
77
/**
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
+ }
1017
*/
11
- const TabManager = function(domElem){
18
+ const TabManager = function(domElem, options){
1219
this.e = {};
20
+ this.options = F.mergeLastWins(TabManager.defaultOptions , options);
1321
if(domElem) this.init(domElem);
1422
};
1523
1624
/**
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.
1938
*/
2039
const tabArg = function(arg,tabMgr){
2140
if('string'===typeof arg) arg = E(arg);
2241
else if(tabMgr && 'number'===typeof arg && arg>=0){
2342
arg = tabMgr.e.tabs.childNodes[arg];
2443
}
2544
return arg;
2645
};
2746
47
+ /**
48
+ Sets sets the visibility of tab element e to on or off. e MUST be
49
+ a TabManager tab element.
50
+ */
2851
const setVisible = function(e,yes){
2952
D[yes ? 'removeClass' : 'addClass'](e, 'hidden');
3053
};
3154
3255
TabManager.prototype = {
@@ -33,16 +56,18 @@
3356
/**
3457
Initializes the tabs associated with the given tab container
3558
(DOM element or selector for a single element). This must be
3659
called once before using any other member functions of a given
3760
instance, noting that the constructor will call this if it is
38
- passed an argument.
61
+ passed an argument.
3962
4063
The tab container must have an 'id' attribute. This function
4164
looks through the DOM for all elements which have
4265
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.
4469
4570
The label for each tab is set by the data-tab-label attribute
4671
of each element, defaulting to something not terribly useful.
4772
4873
When it's done, it auto-selects the first tab unless a tab has
@@ -108,10 +133,18 @@
108133
},
109134
/**
110135
Adds the given DOM element or unique selector as the next
111136
tab in the tab container, adding a button to switch to
112137
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
113146
*/
114147
addTab: function f(tab){
115148
if(!f.click){
116149
f.click = function(e){
117150
e.target.$manager.switchToTab(e.target.$tab);
@@ -118,15 +151,19 @@
118151
};
119152
}
120153
tab = tabArg(tab);
121154
tab.remove();
122155
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;
124158
const btn = D.addClass(D.append(D.span(), lbl), 'tab-button');
125159
D.append(this.e.tabBar,btn);
126160
btn.$manager = this;
127161
btn.$tab = tab;
162
+ if(this.options.tabAccessKeys){
163
+ D.attr(btn, 'accesskey', tabCount);
164
+ }
128165
btn.addEventListener('click', f.click, false);
129166
return this;
130167
},
131168
132169
/**
133170
--- 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 @@
285285
.readdir = fusefs_readdir,
286286
.read = fusefs_read,
287287
};
288288
289289
/*
290
-** COMMAND: fusefs
290
+** COMMAND: fusefs*
291291
**
292292
** Usage: %fossil fusefs [--debug] DIRECTORY
293293
**
294294
** This command uses the Fuse Filesystem (FuseFS) to mount a directory
295295
** at DIRECTORY that contains the content of all check-ins in the
296296
--- 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 @@
163163
if( pGlob ){
164164
fossil_free(pGlob->azPattern);
165165
fossil_free(pGlob);
166166
}
167167
}
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
+}
168201
169202
/*
170203
** COMMAND: test-glob
171204
**
172205
** Usage: %fossil test-glob PATTERN STRING...
173206
--- 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 @@
779779
if(!dataObj) break;
780780
var txJson = dataObj.textContent || dataObj.innerText;
781781
var tx = JSON.parse(txJson);
782782
TimelineGraph(tx);
783783
}
784
-}())
784
+}());
785785
--- 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 @@
174174
}
175175
db_finalize(&q);
176176
}
177177
178178
/*
179
-** COMMAND: hook
179
+** COMMAND: hook*
180180
**
181181
** Usage: %fossil hook COMMAND ...
182182
**
183183
** Commands include:
184184
**
185185
--- 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 @@
15941594
db_finalize(&revSrc);
15951595
fossil_print(" Done!\n");
15961596
}
15971597
15981598
/*
1599
-** COMMAND: import
1599
+** COMMAND: import*
16001600
**
16011601
** Usage: %fossil import ?--git? ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE?
16021602
** or: %fossil import --svn ?OPTIONS? NEW-REPOSITORY ?INPUT-FILE?
16031603
**
16041604
** Read interchange format generated by another VCS and use it to
16051605
--- 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 @@
191191
** Options:
192192
**
193193
** -R|--repository FILE Extract info from repository FILE
194194
** -v|--verbose Show extra information about repositories
195195
**
196
-** See also: annotate, artifact, finfo, timeline
196
+** See also: [[annotate]], [[artifact]], [[finfo]], [[timeline]]
197197
*/
198198
void info_cmd(void){
199199
i64 fsize;
200200
int verboseFlag = find_option("verbose","v",0)!=0;
201201
if( !verboseFlag ){
@@ -441,11 +441,11 @@
441441
/*
442442
** Generate javascript to enhance HTML diffs.
443443
*/
444444
void append_diff_javascript(int sideBySide){
445445
if( !sideBySide ) return;
446
- style_load_one_js_file("sbsdiff.js");
446
+ builtin_request_js("sbsdiff.js");
447447
}
448448
449449
/*
450450
** Construct an appropriate diffFlag for text_diff() based on query
451451
** parameters and the to boolean arguments.
@@ -2011,27 +2011,37 @@
20112011
manifest_destroy(pManifest);
20122012
return rid;
20132013
}
20142014
20152015
/*
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.
20182023
**
20192024
** zLn is the ?ln= parameter for the HTTP query. If there is an argument,
20202025
** then highlight that line number and scroll to it once the page loads.
20212026
** If there are two line numbers, highlight the range of lines.
20222027
** Multiple ranges can be highlighed by adding additional line numbers
20232028
** separated by a non-digit character (also not one of [-,.]).
20242029
*/
20252030
void output_text_with_line_numbers(
20262031
const char *z,
2032
+ int nZ,
2033
+ const char *zName,
20272034
const char *zLn
20282035
){
20292036
int iStart, iEnd; /* Start and end of region to highlight */
20302037
int n = 0; /* Current line number */
20312038
int i = 0; /* Loop index */
20322039
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);
20332043
Stmt q;
20342044
20352045
iStart = iEnd = atoi(zLn);
20362046
db_multi_exec(
20372047
"CREATE TEMP TABLE lnos(iStart INTEGER PRIMARY KEY, iEnd INTEGER)");
@@ -2047,56 +2057,101 @@
20472057
while( fossil_isdigit(zLn[i]) ) i++;
20482058
if( iEnd<iStart ) iEnd = iStart;
20492059
db_multi_exec(
20502060
"INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd
20512061
);
2062
+ ++nSpans;
20522063
iStart = iEnd = atoi(&zLn[i++]);
20532064
}while( zLn[i] && iStart && iEnd );
20542065
}
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");
20932120
if( db_int(0, "SELECT EXISTS(SELECT 1 FROM lnos)") ){
2094
- style_load_one_js_file("scroll.js");
2121
+ builtin_request_js("scroll.js");
20952122
}
2123
+ style_emit_fossil_js_apis(0, "dom", "copybutton", "popupwidget",
2124
+ "numbered-lines", 0);
20962125
}
20972126
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
+}
20982153
20992154
/*
21002155
** WEBPAGE: artifact
21012156
** WEBPAGE: file
21022157
** WEBPAGE: whatis
@@ -2387,25 +2442,26 @@
23872442
if( zLn==0 || atoi(zLn)==0 ){
23882443
style_submenu_checkbox("ln", "Line Numbers", 0, 0);
23892444
}
23902445
blob_to_utf8_no_bom(&content, 0);
23912446
zMime = mimetype_from_content(&content);
2392
- @ <blockquote>
2447
+ @ <blockquote class="file-content">
23932448
if( zMime==0 ){
23942449
const char *z, *zFileName, *zExt;
23952450
z = blob_str(&content);
23962451
zFileName = db_text(0,
23972452
"SELECT name FROM mlink, filename"
23982453
" WHERE filename.fnid=mlink.fnid"
23992454
" AND mlink.fid=%d",
24002455
rid);
2401
- zExt = zFileName ? strrchr(zFileName, '.') : 0;
2456
+ zExt = file_extension(zFileName);
24022457
if( zLn ){
2403
- output_text_with_line_numbers(z, zLn);
2458
+ output_text_with_line_numbers(z, blob_size(&content),
2459
+ zFileName, zLn);
24042460
}else if( zExt && zExt[1] ){
24052461
@ <pre>
2406
- @ <code class="language-%s(zExt+1)">%h(z)</code>
2462
+ @ <code class="language-%s(zExt)">%h(z)</code>
24072463
@ </pre>
24082464
}else{
24092465
@ <pre>
24102466
@ %h(z)
24112467
@ </pre>
@@ -3113,11 +3169,11 @@
31133169
@ <input type="submit" name="apply" value="Apply Changes" />
31143170
}
31153171
@ </td></tr>
31163172
@ </table>
31173173
@ </div></form>
3118
- style_load_one_js_file("ci_edit.js");
3174
+ builtin_request_js("ci_edit.js");
31193175
style_footer();
31203176
}
31213177
31223178
/*
31233179
** Prepare an ammended commit comment. Let the user modify it using the
31243180
--- 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