Fossil SCM

Merge the latest trunk enhancements into the comment-markdown-links branch.

drh 2025-03-10 14:43 comment-markdown-links merge
Commit 652362e97cbc2ca323c78f4baa608c2c032318e2249e5f299ca772f1729f7535
+1 -1
--- auto.def
+++ auto.def
@@ -36,11 +36,11 @@
3636
}
3737
3838
# Update the minimum required SQLite version number here, and also
3939
# in src/main.c near the sqlite3_libversion_number() call. Take care
4040
# that both places agree!
41
-define MINIMUM_SQLITE_VERSION "3.46.0"
41
+define MINIMUM_SQLITE_VERSION "3.49.0"
4242
4343
# This is useful for people wanting Fossil to use an external SQLite library
4444
# to compare the one they have against the minimum required
4545
if {[opt-bool print-minimum-sqlite-version]} {
4646
puts [get-define MINIMUM_SQLITE_VERSION]
4747
--- auto.def
+++ auto.def
@@ -36,11 +36,11 @@
36 }
37
38 # Update the minimum required SQLite version number here, and also
39 # in src/main.c near the sqlite3_libversion_number() call. Take care
40 # that both places agree!
41 define MINIMUM_SQLITE_VERSION "3.46.0"
42
43 # This is useful for people wanting Fossil to use an external SQLite library
44 # to compare the one they have against the minimum required
45 if {[opt-bool print-minimum-sqlite-version]} {
46 puts [get-define MINIMUM_SQLITE_VERSION]
47
--- auto.def
+++ auto.def
@@ -36,11 +36,11 @@
36 }
37
38 # Update the minimum required SQLite version number here, and also
39 # in src/main.c near the sqlite3_libversion_number() call. Take care
40 # that both places agree!
41 define MINIMUM_SQLITE_VERSION "3.49.0"
42
43 # This is useful for people wanting Fossil to use an external SQLite library
44 # to compare the one they have against the minimum required
45 if {[opt-bool print-minimum-sqlite-version]} {
46 puts [get-define MINIMUM_SQLITE_VERSION]
47
+4 -4
--- src/bisect.c
+++ src/bisect.c
@@ -37,13 +37,13 @@
3737
void bisect_path(void){
3838
PathNode *p;
3939
bisect.bad = db_lget_int("bisect-bad", 0);
4040
bisect.good = db_lget_int("bisect-good", 0);
4141
if( bisect.good>0 && bisect.bad==0 ){
42
- path_shortest(bisect.good, bisect.good, 0, 0, 0);
42
+ path_shortest(bisect.good, bisect.good, 0, 0, 0, 0);
4343
}else if( bisect.bad>0 && bisect.good==0 ){
44
- path_shortest(bisect.bad, bisect.bad, 0, 0, 0);
44
+ path_shortest(bisect.bad, bisect.bad, 0, 0, 0, 0);
4545
}else if( bisect.bad==0 && bisect.good==0 ){
4646
fossil_fatal("neither \"good\" nor \"bad\" versions have been identified");
4747
}else{
4848
Bag skip;
4949
int bDirect = bisect_option("direct-only");
@@ -55,11 +55,11 @@
5555
if( blob_str(&id)[0]=='s' ){
5656
bag_insert(&skip, atoi(blob_str(&id)+1));
5757
}
5858
}
5959
blob_reset(&log);
60
- p = path_shortest(bisect.good, bisect.bad, bDirect, 0, &skip);
60
+ p = path_shortest(bisect.good, bisect.bad, bDirect, 0, &skip, 0);
6161
bag_clear(&skip);
6262
if( p==0 ){
6363
char *zBad = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",bisect.bad);
6464
char *zGood = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",bisect.good);
6565
fossil_fatal("no path from good ([%S]) to bad ([%S]) or back",
@@ -292,11 +292,11 @@
292292
if( iCurrent>0 ){
293293
bisect_log_append(&ins, ++cnt, "CURRENT", iCurrent);
294294
}
295295
if( bDetail && lastGood>0 && lastBad>0 ){
296296
PathNode *p;
297
- p = path_shortest(lastGood, lastBad, bisect_option("direct-only"),0, 0);
297
+ p = path_shortest(lastGood, lastBad, bisect_option("direct-only"),0, 0, 0);
298298
while( p ){
299299
bisect_log_append(&ins, ++cnt, 0, p->rid);
300300
p = p->u.pTo;
301301
}
302302
path_reset();
303303
--- src/bisect.c
+++ src/bisect.c
@@ -37,13 +37,13 @@
37 void bisect_path(void){
38 PathNode *p;
39 bisect.bad = db_lget_int("bisect-bad", 0);
40 bisect.good = db_lget_int("bisect-good", 0);
41 if( bisect.good>0 && bisect.bad==0 ){
42 path_shortest(bisect.good, bisect.good, 0, 0, 0);
43 }else if( bisect.bad>0 && bisect.good==0 ){
44 path_shortest(bisect.bad, bisect.bad, 0, 0, 0);
45 }else if( bisect.bad==0 && bisect.good==0 ){
46 fossil_fatal("neither \"good\" nor \"bad\" versions have been identified");
47 }else{
48 Bag skip;
49 int bDirect = bisect_option("direct-only");
@@ -55,11 +55,11 @@
55 if( blob_str(&id)[0]=='s' ){
56 bag_insert(&skip, atoi(blob_str(&id)+1));
57 }
58 }
59 blob_reset(&log);
60 p = path_shortest(bisect.good, bisect.bad, bDirect, 0, &skip);
61 bag_clear(&skip);
62 if( p==0 ){
63 char *zBad = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",bisect.bad);
64 char *zGood = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",bisect.good);
65 fossil_fatal("no path from good ([%S]) to bad ([%S]) or back",
@@ -292,11 +292,11 @@
292 if( iCurrent>0 ){
293 bisect_log_append(&ins, ++cnt, "CURRENT", iCurrent);
294 }
295 if( bDetail && lastGood>0 && lastBad>0 ){
296 PathNode *p;
297 p = path_shortest(lastGood, lastBad, bisect_option("direct-only"),0, 0);
298 while( p ){
299 bisect_log_append(&ins, ++cnt, 0, p->rid);
300 p = p->u.pTo;
301 }
302 path_reset();
303
--- src/bisect.c
+++ src/bisect.c
@@ -37,13 +37,13 @@
37 void bisect_path(void){
38 PathNode *p;
39 bisect.bad = db_lget_int("bisect-bad", 0);
40 bisect.good = db_lget_int("bisect-good", 0);
41 if( bisect.good>0 && bisect.bad==0 ){
42 path_shortest(bisect.good, bisect.good, 0, 0, 0, 0);
43 }else if( bisect.bad>0 && bisect.good==0 ){
44 path_shortest(bisect.bad, bisect.bad, 0, 0, 0, 0);
45 }else if( bisect.bad==0 && bisect.good==0 ){
46 fossil_fatal("neither \"good\" nor \"bad\" versions have been identified");
47 }else{
48 Bag skip;
49 int bDirect = bisect_option("direct-only");
@@ -55,11 +55,11 @@
55 if( blob_str(&id)[0]=='s' ){
56 bag_insert(&skip, atoi(blob_str(&id)+1));
57 }
58 }
59 blob_reset(&log);
60 p = path_shortest(bisect.good, bisect.bad, bDirect, 0, &skip, 0);
61 bag_clear(&skip);
62 if( p==0 ){
63 char *zBad = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",bisect.bad);
64 char *zGood = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",bisect.good);
65 fossil_fatal("no path from good ([%S]) to bad ([%S]) or back",
@@ -292,11 +292,11 @@
292 if( iCurrent>0 ){
293 bisect_log_append(&ins, ++cnt, "CURRENT", iCurrent);
294 }
295 if( bDetail && lastGood>0 && lastBad>0 ){
296 PathNode *p;
297 p = path_shortest(lastGood, lastBad, bisect_option("direct-only"),0, 0, 0);
298 while( p ){
299 bisect_log_append(&ins, ++cnt, 0, p->rid);
300 p = p->u.pTo;
301 }
302 path_reset();
303
--- src/descendants.c
+++ src/descendants.c
@@ -156,10 +156,27 @@
156156
" AND tagxref.tagtype>0)",
157157
TAG_CLOSED
158158
);
159159
}
160160
}
161
+
162
+/*
163
+** If RID refers to a check-in, return the mtime of that check-in - the
164
+** julian day number of when the check-in occurred.
165
+*/
166
+double mtime_of_rid(int rid, double mtime){
167
+ static Stmt q;
168
+ db_static_prepare(&q,"SELECT mtime FROM event WHERE objid=:rid");
169
+ db_bind_int(&q, ":rid", rid);
170
+ if( db_step(&q)==SQLITE_ROW ){
171
+ mtime = db_column_double(&q,0);
172
+ }
173
+ db_reset(&q);
174
+ return mtime;
175
+}
176
+
177
+
161178
162179
/*
163180
** Load the record ID rid and up to |N|-1 closest ancestors into
164181
** the "ok" table. If N is zero, no limit. If ridBackTo is not zero
165182
** then stop the search upon reaching the ancestor with rid==ridBackTo.
@@ -197,13 +214,11 @@
197214
** (3) Cherrypick merge parents.
198215
** (4) All ancestores of 1 and 2 but not of 3.
199216
*/
200217
double rLimitMtime = 0.0;
201218
if( ridBackTo ){
202
- rLimitMtime = db_double(0.0,
203
- "SELECT mtime FROM event WHERE objid=%d",
204
- ridBackTo);
219
+ rLimitMtime = mtime_of_rid(ridBackTo, 0.0);
205220
}
206221
db_multi_exec(
207222
"WITH RECURSIVE\n"
208223
" parent(pid,cid,isCP) AS (\n"
209224
" SELECT plink.pid, plink.cid, 0 AS xisCP FROM plink\n"
210225
--- src/descendants.c
+++ src/descendants.c
@@ -156,10 +156,27 @@
156 " AND tagxref.tagtype>0)",
157 TAG_CLOSED
158 );
159 }
160 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
162 /*
163 ** Load the record ID rid and up to |N|-1 closest ancestors into
164 ** the "ok" table. If N is zero, no limit. If ridBackTo is not zero
165 ** then stop the search upon reaching the ancestor with rid==ridBackTo.
@@ -197,13 +214,11 @@
197 ** (3) Cherrypick merge parents.
198 ** (4) All ancestores of 1 and 2 but not of 3.
199 */
200 double rLimitMtime = 0.0;
201 if( ridBackTo ){
202 rLimitMtime = db_double(0.0,
203 "SELECT mtime FROM event WHERE objid=%d",
204 ridBackTo);
205 }
206 db_multi_exec(
207 "WITH RECURSIVE\n"
208 " parent(pid,cid,isCP) AS (\n"
209 " SELECT plink.pid, plink.cid, 0 AS xisCP FROM plink\n"
210
--- src/descendants.c
+++ src/descendants.c
@@ -156,10 +156,27 @@
156 " AND tagxref.tagtype>0)",
157 TAG_CLOSED
158 );
159 }
160 }
161
162 /*
163 ** If RID refers to a check-in, return the mtime of that check-in - the
164 ** julian day number of when the check-in occurred.
165 */
166 double mtime_of_rid(int rid, double mtime){
167 static Stmt q;
168 db_static_prepare(&q,"SELECT mtime FROM event WHERE objid=:rid");
169 db_bind_int(&q, ":rid", rid);
170 if( db_step(&q)==SQLITE_ROW ){
171 mtime = db_column_double(&q,0);
172 }
173 db_reset(&q);
174 return mtime;
175 }
176
177
178
179 /*
180 ** Load the record ID rid and up to |N|-1 closest ancestors into
181 ** the "ok" table. If N is zero, no limit. If ridBackTo is not zero
182 ** then stop the search upon reaching the ancestor with rid==ridBackTo.
@@ -197,13 +214,11 @@
214 ** (3) Cherrypick merge parents.
215 ** (4) All ancestores of 1 and 2 but not of 3.
216 */
217 double rLimitMtime = 0.0;
218 if( ridBackTo ){
219 rLimitMtime = mtime_of_rid(ridBackTo, 0.0);
 
 
220 }
221 db_multi_exec(
222 "WITH RECURSIVE\n"
223 " parent(pid,cid,isCP) AS (\n"
224 " SELECT plink.pid, plink.cid, 0 AS xisCP FROM plink\n"
225
+31 -1
--- src/dispatch.c
+++ src/dispatch.c
@@ -811,10 +811,32 @@
811811
fossil_print(" %s\n", az[j]);
812812
}
813813
}
814814
}
815815
816
+
817
+/*
818
+** Returns 1 if the command or page name zName is known to be a
819
+** command/page which is only available in certain builds/platforms,
820
+** else returns 0.
821
+*/
822
+static int help_is_platform_command(const char *zName){
823
+ const char *aList[] = {
824
+ /* List of commands/pages which are known to only be available in
825
+ ** certain builds/platforms. */
826
+ "winsrv",
827
+ "json", "/json",
828
+ NULL /* end-of-list sentinel */
829
+ };
830
+ int i = 0;
831
+ const char *z;
832
+ for( z = aList[0]; z ; z = aList[++i] ){
833
+ if( 0==fossil_strcmp(zName, z) ) return 1;
834
+ }
835
+ return 0;
836
+}
837
+
816838
/*
817839
** WEBPAGE: help
818840
** URL: /help?name=CMD
819841
**
820842
** Show the built-in help text for CMD. CMD can be a command-line interface
@@ -860,11 +882,15 @@
860882
@ <h1>The "%h(pCmd->zName)" setting:</h1>
861883
}else{
862884
@ <h1>The "%h(pCmd->zName)" command:</h1>
863885
}
864886
if( rc==1 || (rc==2 && zCmd[0]=='/') ){
865
- @ Unknown topic: "%h(zCmd)"
887
+ if( zCmd && help_is_platform_command(zCmd) ){
888
+ @ Not available in this build: "%h(zCmd)"
889
+ }else{
890
+ @ Unknown topic: "%h(zCmd)"
891
+ }
866892
}else if( rc==2 ){
867893
@ Ambiguous prefix: "%h(zCmd)"
868894
}else{
869895
if( pCmd->zHelp[0]==0 ){
870896
@ No help available for "%h(pCmd->zName)"
@@ -1518,10 +1544,14 @@
15181544
rc = dispatch_name_search(g.argv[2], mask|CMDFLAG_PREFIX, &pCmd);
15191545
if( rc ){
15201546
int i, n;
15211547
const char *az[5];
15221548
if( rc==1 ){
1549
+ if( help_is_platform_command(g.argv[2]) ){
1550
+ fossil_print("Not available in this build: %s\n", g.argv[2]);
1551
+ return;
1552
+ }
15231553
fossil_print("unknown %s: %s\n", zCmdOrPage, g.argv[2]);
15241554
}else{
15251555
fossil_print("ambiguous %s prefix: %s\n",
15261556
zCmdOrPage, g.argv[2]);
15271557
}
15281558
--- src/dispatch.c
+++ src/dispatch.c
@@ -811,10 +811,32 @@
811 fossil_print(" %s\n", az[j]);
812 }
813 }
814 }
815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816 /*
817 ** WEBPAGE: help
818 ** URL: /help?name=CMD
819 **
820 ** Show the built-in help text for CMD. CMD can be a command-line interface
@@ -860,11 +882,15 @@
860 @ <h1>The "%h(pCmd->zName)" setting:</h1>
861 }else{
862 @ <h1>The "%h(pCmd->zName)" command:</h1>
863 }
864 if( rc==1 || (rc==2 && zCmd[0]=='/') ){
865 @ Unknown topic: "%h(zCmd)"
 
 
 
 
866 }else if( rc==2 ){
867 @ Ambiguous prefix: "%h(zCmd)"
868 }else{
869 if( pCmd->zHelp[0]==0 ){
870 @ No help available for "%h(pCmd->zName)"
@@ -1518,10 +1544,14 @@
1518 rc = dispatch_name_search(g.argv[2], mask|CMDFLAG_PREFIX, &pCmd);
1519 if( rc ){
1520 int i, n;
1521 const char *az[5];
1522 if( rc==1 ){
 
 
 
 
1523 fossil_print("unknown %s: %s\n", zCmdOrPage, g.argv[2]);
1524 }else{
1525 fossil_print("ambiguous %s prefix: %s\n",
1526 zCmdOrPage, g.argv[2]);
1527 }
1528
--- src/dispatch.c
+++ src/dispatch.c
@@ -811,10 +811,32 @@
811 fossil_print(" %s\n", az[j]);
812 }
813 }
814 }
815
816
817 /*
818 ** Returns 1 if the command or page name zName is known to be a
819 ** command/page which is only available in certain builds/platforms,
820 ** else returns 0.
821 */
822 static int help_is_platform_command(const char *zName){
823 const char *aList[] = {
824 /* List of commands/pages which are known to only be available in
825 ** certain builds/platforms. */
826 "winsrv",
827 "json", "/json",
828 NULL /* end-of-list sentinel */
829 };
830 int i = 0;
831 const char *z;
832 for( z = aList[0]; z ; z = aList[++i] ){
833 if( 0==fossil_strcmp(zName, z) ) return 1;
834 }
835 return 0;
836 }
837
838 /*
839 ** WEBPAGE: help
840 ** URL: /help?name=CMD
841 **
842 ** Show the built-in help text for CMD. CMD can be a command-line interface
@@ -860,11 +882,15 @@
882 @ <h1>The "%h(pCmd->zName)" setting:</h1>
883 }else{
884 @ <h1>The "%h(pCmd->zName)" command:</h1>
885 }
886 if( rc==1 || (rc==2 && zCmd[0]=='/') ){
887 if( zCmd && help_is_platform_command(zCmd) ){
888 @ Not available in this build: "%h(zCmd)"
889 }else{
890 @ Unknown topic: "%h(zCmd)"
891 }
892 }else if( rc==2 ){
893 @ Ambiguous prefix: "%h(zCmd)"
894 }else{
895 if( pCmd->zHelp[0]==0 ){
896 @ No help available for "%h(pCmd->zName)"
@@ -1518,10 +1544,14 @@
1544 rc = dispatch_name_search(g.argv[2], mask|CMDFLAG_PREFIX, &pCmd);
1545 if( rc ){
1546 int i, n;
1547 const char *az[5];
1548 if( rc==1 ){
1549 if( help_is_platform_command(g.argv[2]) ){
1550 fossil_print("Not available in this build: %s\n", g.argv[2]);
1551 return;
1552 }
1553 fossil_print("unknown %s: %s\n", zCmdOrPage, g.argv[2]);
1554 }else{
1555 fossil_print("ambiguous %s prefix: %s\n",
1556 zCmdOrPage, g.argv[2]);
1557 }
1558
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -14,11 +14,11 @@
1414
cache), in the form of an "winfo" object:
1515
1616
{
1717
name: string,
1818
mimetype: mimetype string,
19
- type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
19
+ type: "normal" | "tag" | "checkin" | "branch" | "ticket" | "sandbox",
2020
version: UUID string or null for a sandbox page or new page,
2121
parent: parent UUID string or null if no parent,
2222
isEmpty: true if page has no content (is "deleted").
2323
content: string, optional in most contexts
2424
}
@@ -192,11 +192,11 @@
192192
name: winfo.name
193193
});
194194
record.mimetype = winfo.mimetype;
195195
record.type = winfo.type;
196196
record.parent = winfo.parent;
197
- record.version = winfo.version;
197
+ record.version = winfo.version;
198198
record.stashTime = new Date().getTime();
199199
record.isEmpty = !!winfo.isEmpty;
200200
record.attachments = winfo.attachments;
201201
this.storeIndex();
202202
if(arguments.length>1){
@@ -207,11 +207,11 @@
207207
return this;
208208
},
209209
/**
210210
Returns the stashed content, if any, for the given winfo
211211
object.
212
- */
212
+ */
213213
stashedContent: function(winfo){
214214
return F.storage.get(this.contentKey(this.indexKey(winfo)));
215215
},
216216
/** Returns true if we have stashed content for the given winfo
217217
record or page name. */
@@ -270,11 +270,11 @@
270270
if(n) this._fireStashEvent();
271271
}
272272
};
273273
$stash.prune.defaultMaxCount = P.config.defaultMaxStashSize || 10;
274274
P.$stash = $stash /* we have to expose this for the new-page case :/ */;
275
-
275
+
276276
/**
277277
Internal workaround to select the current preview mode
278278
and fire a change event if the value actually changes
279279
or if forceEvent is truthy.
280280
*/
@@ -536,10 +536,11 @@
536536
name = name.trim();
537537
if(!this.validatePageName(name)) return false;
538538
var wtype = 'normal';
539539
if(0===name.indexOf('checkin/')) wtype = 'checkin';
540540
else if(0===name.indexOf('branch/')) wtype = 'branch';
541
+ else if(0===name.indexOf('ticket/')) wtype = 'ticket';
541542
else if(0===name.indexOf('tag/')) wtype = 'tag';
542543
/* ^^^ note that we're not validating that, e.g., checkin/XYZ
543544
has a full artifact ID after "checkin/". */
544545
const winfo = {
545546
name: name, type: wtype, mimetype: 'text/x-markdown',
@@ -573,11 +574,11 @@
573574
574575
/** Set up filter checkboxes for the various types
575576
of wiki pages... */
576577
const fsFilter = D.addClass(D.fieldset("Page types"),"page-types-list"),
577578
fsFilterBody = D.div(),
578
- filters = ['normal', 'branch/...', 'tag/...', 'checkin/...']
579
+ filters = ['normal', 'branch/...', 'tag/...', 'checkin/...', 'ticket/...']
579580
;
580581
D.append(fsFilter, fsFilterBody);
581582
D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
582583
583584
// Add filters by page type...
@@ -1045,11 +1046,11 @@
10451046
P.e.btnSave.addEventListener('click', ()=>doSave(), false);
10461047
P.e.btnSaveClose.addEventListener('click', ()=>doSave(true), false);
10471048
}
10481049
10491050
P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
1050
-
1051
+
10511052
P.selectMimetype(false, true);
10521053
P.e.selectMimetype.addEventListener(
10531054
'change',
10541055
function(e){
10551056
if(P.winfo && P.winfo.mimetype !== e.target.value){
@@ -1058,11 +1059,11 @@
10581059
P.stashContentChange(true);
10591060
}
10601061
},
10611062
false
10621063
);
1063
-
1064
+
10641065
const selectFontSize = E('select[name=editor_font_size]');
10651066
if(selectFontSize){
10661067
selectFontSize.addEventListener(
10671068
"change",function(e){
10681069
const ed = P.e.taEditor;
@@ -1590,11 +1591,11 @@
15901591
responseType: 'json',
15911592
onload: callee.onload
15921593
});
15931594
return this;
15941595
};
1595
-
1596
+
15961597
/**
15971598
Updates P.winfo for certain state and stashes P.winfo, with the
15981599
current content fetched via P.wikiContent().
15991600
16001601
If passed truthy AND the stash already has stashed content for
16011602
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -14,11 +14,11 @@
14 cache), in the form of an "winfo" object:
15
16 {
17 name: string,
18 mimetype: mimetype string,
19 type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
20 version: UUID string or null for a sandbox page or new page,
21 parent: parent UUID string or null if no parent,
22 isEmpty: true if page has no content (is "deleted").
23 content: string, optional in most contexts
24 }
@@ -192,11 +192,11 @@
192 name: winfo.name
193 });
194 record.mimetype = winfo.mimetype;
195 record.type = winfo.type;
196 record.parent = winfo.parent;
197 record.version = winfo.version;
198 record.stashTime = new Date().getTime();
199 record.isEmpty = !!winfo.isEmpty;
200 record.attachments = winfo.attachments;
201 this.storeIndex();
202 if(arguments.length>1){
@@ -207,11 +207,11 @@
207 return this;
208 },
209 /**
210 Returns the stashed content, if any, for the given winfo
211 object.
212 */
213 stashedContent: function(winfo){
214 return F.storage.get(this.contentKey(this.indexKey(winfo)));
215 },
216 /** Returns true if we have stashed content for the given winfo
217 record or page name. */
@@ -270,11 +270,11 @@
270 if(n) this._fireStashEvent();
271 }
272 };
273 $stash.prune.defaultMaxCount = P.config.defaultMaxStashSize || 10;
274 P.$stash = $stash /* we have to expose this for the new-page case :/ */;
275
276 /**
277 Internal workaround to select the current preview mode
278 and fire a change event if the value actually changes
279 or if forceEvent is truthy.
280 */
@@ -536,10 +536,11 @@
536 name = name.trim();
537 if(!this.validatePageName(name)) return false;
538 var wtype = 'normal';
539 if(0===name.indexOf('checkin/')) wtype = 'checkin';
540 else if(0===name.indexOf('branch/')) wtype = 'branch';
 
541 else if(0===name.indexOf('tag/')) wtype = 'tag';
542 /* ^^^ note that we're not validating that, e.g., checkin/XYZ
543 has a full artifact ID after "checkin/". */
544 const winfo = {
545 name: name, type: wtype, mimetype: 'text/x-markdown',
@@ -573,11 +574,11 @@
573
574 /** Set up filter checkboxes for the various types
575 of wiki pages... */
576 const fsFilter = D.addClass(D.fieldset("Page types"),"page-types-list"),
577 fsFilterBody = D.div(),
578 filters = ['normal', 'branch/...', 'tag/...', 'checkin/...']
579 ;
580 D.append(fsFilter, fsFilterBody);
581 D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
582
583 // Add filters by page type...
@@ -1045,11 +1046,11 @@
1045 P.e.btnSave.addEventListener('click', ()=>doSave(), false);
1046 P.e.btnSaveClose.addEventListener('click', ()=>doSave(true), false);
1047 }
1048
1049 P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
1050
1051 P.selectMimetype(false, true);
1052 P.e.selectMimetype.addEventListener(
1053 'change',
1054 function(e){
1055 if(P.winfo && P.winfo.mimetype !== e.target.value){
@@ -1058,11 +1059,11 @@
1058 P.stashContentChange(true);
1059 }
1060 },
1061 false
1062 );
1063
1064 const selectFontSize = E('select[name=editor_font_size]');
1065 if(selectFontSize){
1066 selectFontSize.addEventListener(
1067 "change",function(e){
1068 const ed = P.e.taEditor;
@@ -1590,11 +1591,11 @@
1590 responseType: 'json',
1591 onload: callee.onload
1592 });
1593 return this;
1594 };
1595
1596 /**
1597 Updates P.winfo for certain state and stashes P.winfo, with the
1598 current content fetched via P.wikiContent().
1599
1600 If passed truthy AND the stash already has stashed content for
1601
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -14,11 +14,11 @@
14 cache), in the form of an "winfo" object:
15
16 {
17 name: string,
18 mimetype: mimetype string,
19 type: "normal" | "tag" | "checkin" | "branch" | "ticket" | "sandbox",
20 version: UUID string or null for a sandbox page or new page,
21 parent: parent UUID string or null if no parent,
22 isEmpty: true if page has no content (is "deleted").
23 content: string, optional in most contexts
24 }
@@ -192,11 +192,11 @@
192 name: winfo.name
193 });
194 record.mimetype = winfo.mimetype;
195 record.type = winfo.type;
196 record.parent = winfo.parent;
197 record.version = winfo.version;
198 record.stashTime = new Date().getTime();
199 record.isEmpty = !!winfo.isEmpty;
200 record.attachments = winfo.attachments;
201 this.storeIndex();
202 if(arguments.length>1){
@@ -207,11 +207,11 @@
207 return this;
208 },
209 /**
210 Returns the stashed content, if any, for the given winfo
211 object.
212 */
213 stashedContent: function(winfo){
214 return F.storage.get(this.contentKey(this.indexKey(winfo)));
215 },
216 /** Returns true if we have stashed content for the given winfo
217 record or page name. */
@@ -270,11 +270,11 @@
270 if(n) this._fireStashEvent();
271 }
272 };
273 $stash.prune.defaultMaxCount = P.config.defaultMaxStashSize || 10;
274 P.$stash = $stash /* we have to expose this for the new-page case :/ */;
275
276 /**
277 Internal workaround to select the current preview mode
278 and fire a change event if the value actually changes
279 or if forceEvent is truthy.
280 */
@@ -536,10 +536,11 @@
536 name = name.trim();
537 if(!this.validatePageName(name)) return false;
538 var wtype = 'normal';
539 if(0===name.indexOf('checkin/')) wtype = 'checkin';
540 else if(0===name.indexOf('branch/')) wtype = 'branch';
541 else if(0===name.indexOf('ticket/')) wtype = 'ticket';
542 else if(0===name.indexOf('tag/')) wtype = 'tag';
543 /* ^^^ note that we're not validating that, e.g., checkin/XYZ
544 has a full artifact ID after "checkin/". */
545 const winfo = {
546 name: name, type: wtype, mimetype: 'text/x-markdown',
@@ -573,11 +574,11 @@
574
575 /** Set up filter checkboxes for the various types
576 of wiki pages... */
577 const fsFilter = D.addClass(D.fieldset("Page types"),"page-types-list"),
578 fsFilterBody = D.div(),
579 filters = ['normal', 'branch/...', 'tag/...', 'checkin/...', 'ticket/...']
580 ;
581 D.append(fsFilter, fsFilterBody);
582 D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
583
584 // Add filters by page type...
@@ -1045,11 +1046,11 @@
1046 P.e.btnSave.addEventListener('click', ()=>doSave(), false);
1047 P.e.btnSaveClose.addEventListener('click', ()=>doSave(true), false);
1048 }
1049
1050 P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
1051
1052 P.selectMimetype(false, true);
1053 P.e.selectMimetype.addEventListener(
1054 'change',
1055 function(e){
1056 if(P.winfo && P.winfo.mimetype !== e.target.value){
@@ -1058,11 +1059,11 @@
1059 P.stashContentChange(true);
1060 }
1061 },
1062 false
1063 );
1064
1065 const selectFontSize = E('select[name=editor_font_size]');
1066 if(selectFontSize){
1067 selectFontSize.addEventListener(
1068 "change",function(e){
1069 const ed = P.e.taEditor;
@@ -1590,11 +1591,11 @@
1591 responseType: 'json',
1592 onload: callee.onload
1593 });
1594 return this;
1595 };
1596
1597 /**
1598 Updates P.winfo for certain state and stashes P.winfo, with the
1599 current content fetched via P.wikiContent().
1600
1601 If passed truthy AND the stash already has stashed content for
1602
+6 -6
--- src/main.c
+++ src/main.c
@@ -726,14 +726,14 @@
726726
fossil_limit_memory(1);
727727
728728
/* When updating the minimum SQLite version, change the number here,
729729
** and also MINIMUM_SQLITE_VERSION value set in ../auto.def. Take
730730
** care that both places agree! */
731
- if( sqlite3_libversion_number()<3046000
732
- || strncmp(sqlite3_sourceid(),"2024-08-16",10)<0
731
+ if( sqlite3_libversion_number()<3049000
732
+ || strncmp(sqlite3_sourceid(),"2025-02-06",10)<0
733733
){
734
- fossil_panic("Unsuitable SQLite version %s, must be at least 3.43.0",
734
+ fossil_panic("Unsuitable SQLite version %s, must be at least 3.49.0",
735735
sqlite3_libversion());
736736
}
737737
738738
sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
739739
sqlite3_config(SQLITE_CONFIG_LOG, fossil_sqlite_log, 0);
@@ -2399,11 +2399,11 @@
23992399
** The lines are processed in the order they are read, which is most
24002400
** significant for "errorlog:", which should be set before "repository:"
24012401
** so that any warnings from the database when opening the repository
24022402
** go to that log file.
24032403
**
2404
-** See also: [[http]], [[server]], [[winsrv]]
2404
+** See also: [[http]], [[server]], [[winsrv]] [Windows only]
24052405
*/
24062406
void cmd_cgi(void){
24072407
const char *zNotFound = 0;
24082408
char **azRedirect = 0; /* List of repositories to redirect to */
24092409
int nRedirect = 0; /* Number of entries in azRedirect */
@@ -2858,11 +2858,11 @@
28582858
** to force use of the current local skin config.
28592859
** --th-trace Trace TH1 execution (for debugging purposes)
28602860
** --usepidkey Use saved encryption key from parent process. This is
28612861
** only necessary when using SEE on Windows or Linux.
28622862
**
2863
-** See also: [[cgi]], [[server]], [[winsrv]]
2863
+** See also: [[cgi]], [[server]], [[winsrv]] [Windows only]
28642864
*/
28652865
void cmd_http(void){
28662866
const char *zIpAddr = 0;
28672867
const char *zNotFound;
28682868
const char *zHost;
@@ -3240,11 +3240,11 @@
32403240
** user and group.
32413241
** --th-trace Trace TH1 execution (for debugging purposes)
32423242
** --usepidkey Use saved encryption key from parent process. This is
32433243
** only necessary when using SEE on Windows or Linux.
32443244
**
3245
-** See also: [[cgi]], [[http]], [[winsrv]]
3245
+** See also: [[cgi]], [[http]], [[winsrv]] [Windows only]
32463246
*/
32473247
void cmd_webserver(void){
32483248
int iPort, mxPort; /* Range of TCP ports allowed */
32493249
const char *zPort; /* Value of the --port option */
32503250
const char *zBrowser; /* Name of web browser program */
32513251
--- src/main.c
+++ src/main.c
@@ -726,14 +726,14 @@
726 fossil_limit_memory(1);
727
728 /* When updating the minimum SQLite version, change the number here,
729 ** and also MINIMUM_SQLITE_VERSION value set in ../auto.def. Take
730 ** care that both places agree! */
731 if( sqlite3_libversion_number()<3046000
732 || strncmp(sqlite3_sourceid(),"2024-08-16",10)<0
733 ){
734 fossil_panic("Unsuitable SQLite version %s, must be at least 3.43.0",
735 sqlite3_libversion());
736 }
737
738 sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
739 sqlite3_config(SQLITE_CONFIG_LOG, fossil_sqlite_log, 0);
@@ -2399,11 +2399,11 @@
2399 ** The lines are processed in the order they are read, which is most
2400 ** significant for "errorlog:", which should be set before "repository:"
2401 ** so that any warnings from the database when opening the repository
2402 ** go to that log file.
2403 **
2404 ** See also: [[http]], [[server]], [[winsrv]]
2405 */
2406 void cmd_cgi(void){
2407 const char *zNotFound = 0;
2408 char **azRedirect = 0; /* List of repositories to redirect to */
2409 int nRedirect = 0; /* Number of entries in azRedirect */
@@ -2858,11 +2858,11 @@
2858 ** to force use of the current local skin config.
2859 ** --th-trace Trace TH1 execution (for debugging purposes)
2860 ** --usepidkey Use saved encryption key from parent process. This is
2861 ** only necessary when using SEE on Windows or Linux.
2862 **
2863 ** See also: [[cgi]], [[server]], [[winsrv]]
2864 */
2865 void cmd_http(void){
2866 const char *zIpAddr = 0;
2867 const char *zNotFound;
2868 const char *zHost;
@@ -3240,11 +3240,11 @@
3240 ** user and group.
3241 ** --th-trace Trace TH1 execution (for debugging purposes)
3242 ** --usepidkey Use saved encryption key from parent process. This is
3243 ** only necessary when using SEE on Windows or Linux.
3244 **
3245 ** See also: [[cgi]], [[http]], [[winsrv]]
3246 */
3247 void cmd_webserver(void){
3248 int iPort, mxPort; /* Range of TCP ports allowed */
3249 const char *zPort; /* Value of the --port option */
3250 const char *zBrowser; /* Name of web browser program */
3251
--- src/main.c
+++ src/main.c
@@ -726,14 +726,14 @@
726 fossil_limit_memory(1);
727
728 /* When updating the minimum SQLite version, change the number here,
729 ** and also MINIMUM_SQLITE_VERSION value set in ../auto.def. Take
730 ** care that both places agree! */
731 if( sqlite3_libversion_number()<3049000
732 || strncmp(sqlite3_sourceid(),"2025-02-06",10)<0
733 ){
734 fossil_panic("Unsuitable SQLite version %s, must be at least 3.49.0",
735 sqlite3_libversion());
736 }
737
738 sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
739 sqlite3_config(SQLITE_CONFIG_LOG, fossil_sqlite_log, 0);
@@ -2399,11 +2399,11 @@
2399 ** The lines are processed in the order they are read, which is most
2400 ** significant for "errorlog:", which should be set before "repository:"
2401 ** so that any warnings from the database when opening the repository
2402 ** go to that log file.
2403 **
2404 ** See also: [[http]], [[server]], [[winsrv]] [Windows only]
2405 */
2406 void cmd_cgi(void){
2407 const char *zNotFound = 0;
2408 char **azRedirect = 0; /* List of repositories to redirect to */
2409 int nRedirect = 0; /* Number of entries in azRedirect */
@@ -2858,11 +2858,11 @@
2858 ** to force use of the current local skin config.
2859 ** --th-trace Trace TH1 execution (for debugging purposes)
2860 ** --usepidkey Use saved encryption key from parent process. This is
2861 ** only necessary when using SEE on Windows or Linux.
2862 **
2863 ** See also: [[cgi]], [[server]], [[winsrv]] [Windows only]
2864 */
2865 void cmd_http(void){
2866 const char *zIpAddr = 0;
2867 const char *zNotFound;
2868 const char *zHost;
@@ -3240,11 +3240,11 @@
3240 ** user and group.
3241 ** --th-trace Trace TH1 execution (for debugging purposes)
3242 ** --usepidkey Use saved encryption key from parent process. This is
3243 ** only necessary when using SEE on Windows or Linux.
3244 **
3245 ** See also: [[cgi]], [[http]], [[winsrv]] [Windows only]
3246 */
3247 void cmd_webserver(void){
3248 int iPort, mxPort; /* Range of TCP ports allowed */
3249 const char *zPort; /* Value of the --port option */
3250 const char *zBrowser; /* Name of web browser program */
3251
+1 -1
--- src/name.c
+++ src/name.c
@@ -231,11 +231,11 @@
231231
** This is a tricky query to do efficiently.
232232
** If the tag is very common (ex: "trunk") then
233233
** we want to use the query identified below as Q1 - which searches
234234
** the most recent EVENT table entries for the most recent with the tag.
235235
** But if the tag is relatively scarce (anything other than "trunk", basically)
236
-** then we want to do the indexed search show below as Q2.
236
+** then we want to do the indexed search shown below as Q2.
237237
*/
238238
static int most_recent_event_with_tag(const char *zTag, const char *zType){
239239
return db_int(0,
240240
"SELECT objid FROM ("
241241
/* Q1: Begin by looking for the tag in the 30 most recent events */
242242
--- src/name.c
+++ src/name.c
@@ -231,11 +231,11 @@
231 ** This is a tricky query to do efficiently.
232 ** If the tag is very common (ex: "trunk") then
233 ** we want to use the query identified below as Q1 - which searches
234 ** the most recent EVENT table entries for the most recent with the tag.
235 ** But if the tag is relatively scarce (anything other than "trunk", basically)
236 ** then we want to do the indexed search show below as Q2.
237 */
238 static int most_recent_event_with_tag(const char *zTag, const char *zType){
239 return db_int(0,
240 "SELECT objid FROM ("
241 /* Q1: Begin by looking for the tag in the 30 most recent events */
242
--- src/name.c
+++ src/name.c
@@ -231,11 +231,11 @@
231 ** This is a tricky query to do efficiently.
232 ** If the tag is very common (ex: "trunk") then
233 ** we want to use the query identified below as Q1 - which searches
234 ** the most recent EVENT table entries for the most recent with the tag.
235 ** But if the tag is relatively scarce (anything other than "trunk", basically)
236 ** then we want to do the indexed search shown below as Q2.
237 */
238 static int most_recent_event_with_tag(const char *zTag, const char *zType){
239 return db_int(0,
240 "SELECT objid FROM ("
241 /* Q1: Begin by looking for the tag in the 30 most recent events */
242
+157 -97
--- src/path.c
+++ src/path.c
@@ -18,40 +18,45 @@
1818
** directed acyclic graph (DAG) of check-ins.
1919
*/
2020
#include "config.h"
2121
#include "path.h"
2222
#include <assert.h>
23
+#include <math.h>
2324
2425
#if INTERFACE
2526
/* Nodes for the paths through the DAG.
2627
*/
2728
struct PathNode {
2829
int rid; /* ID for this node */
2930
u8 fromIsParent; /* True if pFrom is the parent of rid */
3031
u8 isPrim; /* True if primary side of common ancestor */
3132
u8 isHidden; /* Abbreviate output in "fossil bisect ls" */
33
+ char *zBranch; /* Branch name for this node. Might be NULL */
34
+ double mtime; /* Date/time of this check-in */
3235
PathNode *pFrom; /* Node we came from */
3336
union {
34
- PathNode *pPeer; /* List of nodes of the same generation */
37
+ double rCost; /* Cost of getting to this node from pStart */
3538
PathNode *pTo; /* Next on path from beginning to end */
3639
} u;
37
- PathNode *pAll; /* List of all nodes */
40
+ PathNode *pAll; /* List of all nodes */
3841
};
3942
#endif
4043
4144
/*
4245
** Local variables for this module
4346
*/
4447
static struct {
45
- PathNode *pCurrent; /* Current generation of nodes */
48
+ PQueue pending; /* Nodes pending review for inclusion in the graph */
4649
PathNode *pAll; /* All nodes */
47
- Bag seen; /* Nodes seen before */
4850
int nStep; /* Number of steps from first to last */
4951
int nNotHidden; /* Number of steps not counting hidden nodes */
52
+ int brCost; /* Extra cost for moving to a different branch */
53
+ int revCost; /* Extra cost for changing directions */
5054
PathNode *pStart; /* Earliest node */
5155
PathNode *pEnd; /* Most recent */
5256
} path;
57
+static int path_debug = 0; /* Flag to enable debugging */
5358
5459
/*
5560
** Return the first (last) element of the computed path.
5661
*/
5762
PathNode *path_first(void){ return path.pStart; }
@@ -66,25 +71,71 @@
6671
** Return the number of non-hidden steps in the computed path.
6772
*/
6873
int path_length_not_hidden(void){ return path.nNotHidden; }
6974
7075
/*
71
-** Create a new node
76
+** Used for debugging only.
77
+**
78
+** Given a RID, return the ISO date/time string and branch for the
79
+** corresponding check-in. Memory is held locally and is overwritten
80
+** with each call.
81
+*/
82
+char *path_rid_desc(int rid){
83
+ static Stmt q;
84
+ static char *zDesc = 0;
85
+ db_static_prepare(&q,
86
+ "SELECT concat(strftime('%%Y%%m%%d%%H%%M',event.mtime),'/',value)"
87
+ " FROM event, tagxref"
88
+ " WHERE event.objid=:rid"
89
+ " AND tagxref.rid=:rid"
90
+ " AND tagxref.tagid=%d"
91
+ " AND tagxref.tagtype>0",
92
+ TAG_BRANCH
93
+ );
94
+ fossil_free(zDesc);
95
+ db_bind_int(&q, ":rid", rid);
96
+ if( db_step(&q)==SQLITE_ROW ){
97
+ zDesc = fossil_strdup(db_column_text(&q,0));
98
+ }
99
+ db_reset(&q);
100
+ return zDesc ? zDesc : "???";
101
+}
102
+
103
+/*
104
+** Create a new node and insert it into the path.pending queue.
72105
*/
73106
static PathNode *path_new_node(int rid, PathNode *pFrom, int isParent){
74107
PathNode *p;
75108
76109
p = fossil_malloc( sizeof(*p) );
77110
memset(p, 0, sizeof(*p));
111
+ p->pAll = path.pAll;
112
+ path.pAll = p;
78113
p->rid = rid;
79114
p->fromIsParent = isParent;
80115
p->pFrom = pFrom;
81
- p->u.pPeer = path.pCurrent;
82
- path.pCurrent = p;
83
- p->pAll = path.pAll;
84
- path.pAll = p;
85
- bag_insert(&path.seen, rid);
116
+ p->u.rCost = pFrom ? pFrom->u.rCost : 0.0;
117
+ if( path.brCost ){
118
+ p->zBranch = branch_of_rid(rid);
119
+ p->mtime = mtime_of_rid(rid, 0.0);
120
+ if( pFrom ){
121
+ p->u.rCost += fabs(pFrom->mtime - p->mtime);
122
+ if( fossil_strcmp(p->zBranch, pFrom->zBranch)!=0 ){
123
+ p->u.rCost += path.brCost;
124
+ }
125
+ }
126
+ }else{
127
+ /* When brCost==0, we try to minimize the number of nodes
128
+ ** along the path. The cost is just the number of nodes back
129
+ ** to the start. We do not need to know the branch name nor
130
+ ** the mtime */
131
+ p->u.rCost += 1.0;
132
+ }
133
+ if( path_debug ){
134
+ fossil_print("PUSH %-50s cost = %g\n", path_rid_desc(p->rid), p->u.rCost);
135
+ }
136
+ pqueuex_insert_ptr(&path.pending, (void*)p, p->u.rCost);
86137
return p;
87138
}
88139
89140
/*
90141
** Reset memory used by the shortest path algorithm.
@@ -92,13 +143,14 @@
92143
void path_reset(void){
93144
PathNode *p;
94145
while( path.pAll ){
95146
p = path.pAll;
96147
path.pAll = p->pAll;
148
+ fossil_free(p->zBranch);
97149
fossil_free(p);
98150
}
99
- bag_clear(&path.seen);
151
+ pqueuex_clear(&path.pending);
100152
memset(&path, 0, sizeof(path));
101153
}
102154
103155
/*
104156
** Construct the path from path.pStart to path.pEnd in the u.pTo fields.
@@ -128,17 +180,19 @@
128180
PathNode *path_shortest(
129181
int iFrom, /* Path starts here */
130182
int iTo, /* Path ends here */
131183
int directOnly, /* No merge links if true */
132184
int oneWayOnly, /* Parent->child only if true */
133
- Bag *pHidden /* Hidden nodes */
185
+ Bag *pHidden, /* Hidden nodes */
186
+ int branchCost /* Add extra cost to changing branches */
134187
){
135188
Stmt s;
136
- PathNode *pPrev;
189
+ Bag seen;
137190
PathNode *p;
138191
139192
path_reset();
193
+ path.brCost = branchCost;
140194
path.pStart = path_new_node(iFrom, 0, 0);
141195
if( iTo==iFrom ){
142196
path.pEnd = path.pStart;
143197
return path.pStart;
144198
}
@@ -152,44 +206,46 @@
152206
);
153207
}else if( directOnly ){
154208
db_prepare(&s,
155209
"SELECT cid, 1 FROM plink WHERE pid=:pid AND isprim "
156210
"UNION ALL "
157
- "SELECT pid, 0 FROM plink WHERE cid=:pid AND isprim"
211
+ "SELECT pid, 0 FROM plink WHERE :back AND cid=:pid AND isprim"
158212
);
159213
}else{
160214
db_prepare(&s,
161215
"SELECT cid, 1 FROM plink WHERE pid=:pid "
162216
"UNION ALL "
163
- "SELECT pid, 0 FROM plink WHERE cid=:pid"
217
+ "SELECT pid, 0 FROM plink WHERE :back AND cid=:pid"
164218
);
165219
}
166
- while( path.pCurrent ){
167
- path.nStep++;
168
- pPrev = path.pCurrent;
169
- path.pCurrent = 0;
170
- while( pPrev ){
171
- db_bind_int(&s, ":pid", pPrev->rid);
172
- while( db_step(&s)==SQLITE_ROW ){
173
- int cid = db_column_int(&s, 0);
174
- int isParent = db_column_int(&s, 1);
175
- if( bag_find(&path.seen, cid) ) continue;
176
- p = path_new_node(cid, pPrev, isParent);
177
- if( pHidden && bag_find(pHidden,cid) ) p->isHidden = 1;
178
- if( cid==iTo ){
179
- db_finalize(&s);
180
- path.pEnd = p;
181
- path_reverse_path();
182
- for(p=path.pStart->u.pTo; p; p=p->u.pTo ){
183
- if( !p->isHidden ) path.nNotHidden++;
184
- }
185
- return path.pStart;
186
- }
187
- }
188
- db_reset(&s);
189
- pPrev = pPrev->u.pPeer;
190
- }
220
+ bag_init(&seen);
221
+ while( (p = pqueuex_extract_ptr(&path.pending))!=0 ){
222
+ if( path_debug ){
223
+ printf("PULL %s %g\n", path_rid_desc(p->rid), p->u.rCost);
224
+ }
225
+ if( p->rid==iTo ){
226
+ db_finalize(&s);
227
+ path.pEnd = p;
228
+ path_reverse_path();
229
+ for(p=path.pStart->u.pTo; p; p=p->u.pTo ){
230
+ if( !p->isHidden ) path.nNotHidden++;
231
+ }
232
+ return path.pStart;
233
+ }
234
+ if( bag_find(&seen, p->rid) ) continue;
235
+ bag_insert(&seen, p->rid);
236
+ db_bind_int(&s, ":pid", p->rid);
237
+ if( !oneWayOnly ) db_bind_int(&s, ":back", !p->fromIsParent);
238
+ while( db_step(&s)==SQLITE_ROW ){
239
+ int cid = db_column_int(&s, 0);
240
+ int isParent = db_column_int(&s, 1);
241
+ PathNode *pNew;
242
+ if( bag_find(&seen, cid) ) continue;
243
+ pNew = path_new_node(cid, p, isParent);
244
+ if( pHidden && bag_find(pHidden,cid) ) pNew->isHidden = 1;
245
+ }
246
+ db_reset(&s);
191247
}
192248
db_finalize(&s);
193249
path_reset();
194250
return 0;
195251
}
@@ -215,10 +271,22 @@
215271
PathNode *p;
216272
p = path.pStart;
217273
if( p ) p = p->u.pTo;
218274
return p;
219275
}
276
+
277
+/*
278
+** Return the branch for a path node.
279
+**
280
+** Storage space is managed by the path subsystem. The returned value
281
+** is valid until the path is reset.
282
+*/
283
+const char *path_branch(PathNode *p){
284
+ if( p==0 ) return 0;
285
+ if( p->zBranch==0 ) p->zBranch = branch_of_rid(p->rid);
286
+ return p->zBranch;
287
+}
220288
221289
/*
222290
** Return an estimate of the number of comparisons remaining in order
223291
** to bisect path. This is based on the log2() of path.nStep.
224292
*/
@@ -238,11 +306,11 @@
238306
int cid /* RID for check-in at the end of the path */
239307
){
240308
PathNode *pPath;
241309
int gen = 0;
242310
Stmt ins;
243
- pPath = path_shortest(cid, origid, 1, 0, 0);
311
+ pPath = path_shortest(cid, origid, 1, 0, 0, 0);
244312
db_multi_exec(
245313
"CREATE TEMP TABLE IF NOT EXISTS ancestor("
246314
" rid INT UNIQUE,"
247315
" generation INTEGER PRIMARY KEY"
248316
");"
@@ -261,58 +329,55 @@
261329
}
262330
263331
/*
264332
** COMMAND: test-shortest-path
265333
**
266
-** Usage: %fossil test-shortest-path ?--no-merge? VERSION1 VERSION2
334
+** Usage: %fossil test-shortest-path [OPTIONS] VERSION1 VERSION2
335
+**
336
+** Report the shortest path between two check-ins. Options:
267337
**
268
-** Report the shortest path between two check-ins. If the --no-merge flag
269
-** is used, follow only direct parent-child paths and omit merge links.
338
+** --branch-cost N Additional cost N for changing branches
339
+** --debug Show debugging output
340
+** --one-way One-way forwards in time, parent->child only
341
+** --no-merge Follow only direct parent-child paths and omit
342
+** merge links.
270343
*/
271344
void shortest_path_test_cmd(void){
272345
int iFrom;
273346
int iTo;
274347
PathNode *p;
275348
int n;
276349
int directOnly;
277350
int oneWay;
351
+ const char *zBrCost;
278352
279353
db_find_and_open_repository(0,0);
280354
directOnly = find_option("no-merge",0,0)!=0;
281355
oneWay = find_option("one-way",0,0)!=0;
356
+ zBrCost = find_option("branch-cost",0,1);
357
+ if( find_option("debug",0,0)!=0 ) path_debug = 1;
282358
if( g.argc!=4 ) usage("VERSION1 VERSION2");
283359
iFrom = name_to_rid(g.argv[2]);
284360
iTo = name_to_rid(g.argv[3]);
285
- p = path_shortest(iFrom, iTo, directOnly, oneWay, 0);
361
+ p = path_shortest(iFrom, iTo, directOnly, oneWay, 0,
362
+ zBrCost ? atoi(zBrCost) : 0);
286363
if( p==0 ){
287364
fossil_fatal("no path from %s to %s", g.argv[1], g.argv[2]);
288365
}
289366
for(n=1, p=path.pStart; p; p=p->u.pTo, n++){
290
- char *z;
291
- z = db_text(0,
292
- "SELECT substr(uuid,1,12) || ' ' || datetime(mtime)"
293
- " FROM blob, event"
294
- " WHERE blob.rid=%d AND event.objid=%d AND event.type='ci'",
295
- p->rid, p->rid);
296
- fossil_print("%4d: %5d %s", n, p->rid, z);
297
- fossil_free(z);
298
- if( p->u.pTo ){
299
- fossil_print(" is a %s of\n",
300
- p->u.pTo->fromIsParent ? "parent" : "child");
301
- }else{
302
- fossil_print("\n");
303
- }
304
- }
367
+ fossil_print("%4d: %s\n", n, path_rid_desc(p->rid));
368
+ }
369
+ path_debug = 0;
305370
}
306371
307372
/*
308373
** Find the closest common ancestor of two nodes. "Closest" means the
309374
** fewest number of arcs.
310375
*/
311376
int path_common_ancestor(int iMe, int iYou){
312377
Stmt s;
313
- PathNode *pPrev;
378
+ PathNode *pThis;
314379
PathNode *p;
315380
Bag me, you;
316381
317382
if( iMe==iYou ) return iMe;
318383
if( iMe==0 || iYou==0 ) return 0;
@@ -323,45 +388,40 @@
323388
db_prepare(&s, "SELECT pid FROM plink WHERE cid=:cid");
324389
bag_init(&me);
325390
bag_insert(&me, iMe);
326391
bag_init(&you);
327392
bag_insert(&you, iYou);
328
- while( path.pCurrent ){
329
- pPrev = path.pCurrent;
330
- path.pCurrent = 0;
331
- while( pPrev ){
332
- db_bind_int(&s, ":cid", pPrev->rid);
333
- while( db_step(&s)==SQLITE_ROW ){
334
- int pid = db_column_int(&s, 0);
335
- if( bag_find(pPrev->isPrim ? &you : &me, pid) ){
336
- /* pid is the common ancestor */
337
- PathNode *pNext;
338
- for(p=path.pAll; p && p->rid!=pid; p=p->pAll){}
339
- assert( p!=0 );
340
- pNext = p;
341
- while( pNext ){
342
- pNext = p->pFrom;
343
- p->pFrom = pPrev;
344
- pPrev = p;
345
- p = pNext;
346
- }
347
- if( pPrev==path.pStart ) path.pStart = path.pEnd;
348
- path.pEnd = pPrev;
349
- path_reverse_path();
350
- db_finalize(&s);
351
- return pid;
352
- }else if( bag_find(&path.seen, pid) ){
353
- /* pid is just an alternative path on one of the legs */
354
- continue;
355
- }
356
- p = path_new_node(pid, pPrev, 0);
357
- p->isPrim = pPrev->isPrim;
358
- bag_insert(pPrev->isPrim ? &me : &you, pid);
359
- }
360
- db_reset(&s);
361
- pPrev = pPrev->u.pPeer;
362
- }
393
+ while( (pThis = pqueuex_extract_ptr(&path.pending))!=0 ){
394
+ db_bind_int(&s, ":cid", pThis->rid);
395
+ while( db_step(&s)==SQLITE_ROW ){
396
+ int pid = db_column_int(&s, 0);
397
+ if( bag_find(pThis->isPrim ? &you : &me, pid) ){
398
+ /* pid is the common ancestor */
399
+ PathNode *pNext;
400
+ for(p=path.pAll; p && p->rid!=pid; p=p->pAll){}
401
+ assert( p!=0 );
402
+ pNext = p;
403
+ while( pNext ){
404
+ pNext = p->pFrom;
405
+ p->pFrom = pThis;
406
+ pThis = p;
407
+ p = pNext;
408
+ }
409
+ if( pThis==path.pStart ) path.pStart = path.pEnd;
410
+ path.pEnd = pThis;
411
+ path_reverse_path();
412
+ db_finalize(&s);
413
+ return pid;
414
+ }else if( bag_find(pThis->isPrim ? &me : &you, pid) ){
415
+ /* pid is just an alternative path to a node we've already visited */
416
+ continue;
417
+ }
418
+ p = path_new_node(pid, pThis, 0);
419
+ p->isPrim = pThis->isPrim;
420
+ bag_insert(pThis->isPrim ? &me : &you, pid);
421
+ }
422
+ db_reset(&s);
363423
}
364424
db_finalize(&s);
365425
path_reset();
366426
return 0;
367427
}
@@ -453,11 +513,11 @@
453513
}else if(0==iTo){
454514
fossil_fatal("Invalid 'to' RID: 0");
455515
}
456516
if( iFrom==iTo ) return;
457517
path_reset();
458
- p = path_shortest(iFrom, iTo, 1, revOK==0, 0);
518
+ p = path_shortest(iFrom, iTo, 1, revOK==0, 0, 0);
459519
if( p==0 ) return;
460520
path_reverse_path();
461521
db_prepare(&q1,
462522
"SELECT pfnid, fnid FROM mlink"
463523
" WHERE mid=:mid AND (pfnid>0 OR fid==0)"
464524
--- src/path.c
+++ src/path.c
@@ -18,40 +18,45 @@
18 ** directed acyclic graph (DAG) of check-ins.
19 */
20 #include "config.h"
21 #include "path.h"
22 #include <assert.h>
 
23
24 #if INTERFACE
25 /* Nodes for the paths through the DAG.
26 */
27 struct PathNode {
28 int rid; /* ID for this node */
29 u8 fromIsParent; /* True if pFrom is the parent of rid */
30 u8 isPrim; /* True if primary side of common ancestor */
31 u8 isHidden; /* Abbreviate output in "fossil bisect ls" */
 
 
32 PathNode *pFrom; /* Node we came from */
33 union {
34 PathNode *pPeer; /* List of nodes of the same generation */
35 PathNode *pTo; /* Next on path from beginning to end */
36 } u;
37 PathNode *pAll; /* List of all nodes */
38 };
39 #endif
40
41 /*
42 ** Local variables for this module
43 */
44 static struct {
45 PathNode *pCurrent; /* Current generation of nodes */
46 PathNode *pAll; /* All nodes */
47 Bag seen; /* Nodes seen before */
48 int nStep; /* Number of steps from first to last */
49 int nNotHidden; /* Number of steps not counting hidden nodes */
 
 
50 PathNode *pStart; /* Earliest node */
51 PathNode *pEnd; /* Most recent */
52 } path;
 
53
54 /*
55 ** Return the first (last) element of the computed path.
56 */
57 PathNode *path_first(void){ return path.pStart; }
@@ -66,25 +71,71 @@
66 ** Return the number of non-hidden steps in the computed path.
67 */
68 int path_length_not_hidden(void){ return path.nNotHidden; }
69
70 /*
71 ** Create a new node
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72 */
73 static PathNode *path_new_node(int rid, PathNode *pFrom, int isParent){
74 PathNode *p;
75
76 p = fossil_malloc( sizeof(*p) );
77 memset(p, 0, sizeof(*p));
 
 
78 p->rid = rid;
79 p->fromIsParent = isParent;
80 p->pFrom = pFrom;
81 p->u.pPeer = path.pCurrent;
82 path.pCurrent = p;
83 p->pAll = path.pAll;
84 path.pAll = p;
85 bag_insert(&path.seen, rid);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86 return p;
87 }
88
89 /*
90 ** Reset memory used by the shortest path algorithm.
@@ -92,13 +143,14 @@
92 void path_reset(void){
93 PathNode *p;
94 while( path.pAll ){
95 p = path.pAll;
96 path.pAll = p->pAll;
 
97 fossil_free(p);
98 }
99 bag_clear(&path.seen);
100 memset(&path, 0, sizeof(path));
101 }
102
103 /*
104 ** Construct the path from path.pStart to path.pEnd in the u.pTo fields.
@@ -128,17 +180,19 @@
128 PathNode *path_shortest(
129 int iFrom, /* Path starts here */
130 int iTo, /* Path ends here */
131 int directOnly, /* No merge links if true */
132 int oneWayOnly, /* Parent->child only if true */
133 Bag *pHidden /* Hidden nodes */
 
134 ){
135 Stmt s;
136 PathNode *pPrev;
137 PathNode *p;
138
139 path_reset();
 
140 path.pStart = path_new_node(iFrom, 0, 0);
141 if( iTo==iFrom ){
142 path.pEnd = path.pStart;
143 return path.pStart;
144 }
@@ -152,44 +206,46 @@
152 );
153 }else if( directOnly ){
154 db_prepare(&s,
155 "SELECT cid, 1 FROM plink WHERE pid=:pid AND isprim "
156 "UNION ALL "
157 "SELECT pid, 0 FROM plink WHERE cid=:pid AND isprim"
158 );
159 }else{
160 db_prepare(&s,
161 "SELECT cid, 1 FROM plink WHERE pid=:pid "
162 "UNION ALL "
163 "SELECT pid, 0 FROM plink WHERE cid=:pid"
164 );
165 }
166 while( path.pCurrent ){
167 path.nStep++;
168 pPrev = path.pCurrent;
169 path.pCurrent = 0;
170 while( pPrev ){
171 db_bind_int(&s, ":pid", pPrev->rid);
172 while( db_step(&s)==SQLITE_ROW ){
173 int cid = db_column_int(&s, 0);
174 int isParent = db_column_int(&s, 1);
175 if( bag_find(&path.seen, cid) ) continue;
176 p = path_new_node(cid, pPrev, isParent);
177 if( pHidden && bag_find(pHidden,cid) ) p->isHidden = 1;
178 if( cid==iTo ){
179 db_finalize(&s);
180 path.pEnd = p;
181 path_reverse_path();
182 for(p=path.pStart->u.pTo; p; p=p->u.pTo ){
183 if( !p->isHidden ) path.nNotHidden++;
184 }
185 return path.pStart;
186 }
187 }
188 db_reset(&s);
189 pPrev = pPrev->u.pPeer;
190 }
 
 
191 }
192 db_finalize(&s);
193 path_reset();
194 return 0;
195 }
@@ -215,10 +271,22 @@
215 PathNode *p;
216 p = path.pStart;
217 if( p ) p = p->u.pTo;
218 return p;
219 }
 
 
 
 
 
 
 
 
 
 
 
 
220
221 /*
222 ** Return an estimate of the number of comparisons remaining in order
223 ** to bisect path. This is based on the log2() of path.nStep.
224 */
@@ -238,11 +306,11 @@
238 int cid /* RID for check-in at the end of the path */
239 ){
240 PathNode *pPath;
241 int gen = 0;
242 Stmt ins;
243 pPath = path_shortest(cid, origid, 1, 0, 0);
244 db_multi_exec(
245 "CREATE TEMP TABLE IF NOT EXISTS ancestor("
246 " rid INT UNIQUE,"
247 " generation INTEGER PRIMARY KEY"
248 ");"
@@ -261,58 +329,55 @@
261 }
262
263 /*
264 ** COMMAND: test-shortest-path
265 **
266 ** Usage: %fossil test-shortest-path ?--no-merge? VERSION1 VERSION2
 
 
267 **
268 ** Report the shortest path between two check-ins. If the --no-merge flag
269 ** is used, follow only direct parent-child paths and omit merge links.
 
 
 
270 */
271 void shortest_path_test_cmd(void){
272 int iFrom;
273 int iTo;
274 PathNode *p;
275 int n;
276 int directOnly;
277 int oneWay;
 
278
279 db_find_and_open_repository(0,0);
280 directOnly = find_option("no-merge",0,0)!=0;
281 oneWay = find_option("one-way",0,0)!=0;
 
 
282 if( g.argc!=4 ) usage("VERSION1 VERSION2");
283 iFrom = name_to_rid(g.argv[2]);
284 iTo = name_to_rid(g.argv[3]);
285 p = path_shortest(iFrom, iTo, directOnly, oneWay, 0);
 
286 if( p==0 ){
287 fossil_fatal("no path from %s to %s", g.argv[1], g.argv[2]);
288 }
289 for(n=1, p=path.pStart; p; p=p->u.pTo, n++){
290 char *z;
291 z = db_text(0,
292 "SELECT substr(uuid,1,12) || ' ' || datetime(mtime)"
293 " FROM blob, event"
294 " WHERE blob.rid=%d AND event.objid=%d AND event.type='ci'",
295 p->rid, p->rid);
296 fossil_print("%4d: %5d %s", n, p->rid, z);
297 fossil_free(z);
298 if( p->u.pTo ){
299 fossil_print(" is a %s of\n",
300 p->u.pTo->fromIsParent ? "parent" : "child");
301 }else{
302 fossil_print("\n");
303 }
304 }
305 }
306
307 /*
308 ** Find the closest common ancestor of two nodes. "Closest" means the
309 ** fewest number of arcs.
310 */
311 int path_common_ancestor(int iMe, int iYou){
312 Stmt s;
313 PathNode *pPrev;
314 PathNode *p;
315 Bag me, you;
316
317 if( iMe==iYou ) return iMe;
318 if( iMe==0 || iYou==0 ) return 0;
@@ -323,45 +388,40 @@
323 db_prepare(&s, "SELECT pid FROM plink WHERE cid=:cid");
324 bag_init(&me);
325 bag_insert(&me, iMe);
326 bag_init(&you);
327 bag_insert(&you, iYou);
328 while( path.pCurrent ){
329 pPrev = path.pCurrent;
330 path.pCurrent = 0;
331 while( pPrev ){
332 db_bind_int(&s, ":cid", pPrev->rid);
333 while( db_step(&s)==SQLITE_ROW ){
334 int pid = db_column_int(&s, 0);
335 if( bag_find(pPrev->isPrim ? &you : &me, pid) ){
336 /* pid is the common ancestor */
337 PathNode *pNext;
338 for(p=path.pAll; p && p->rid!=pid; p=p->pAll){}
339 assert( p!=0 );
340 pNext = p;
341 while( pNext ){
342 pNext = p->pFrom;
343 p->pFrom = pPrev;
344 pPrev = p;
345 p = pNext;
346 }
347 if( pPrev==path.pStart ) path.pStart = path.pEnd;
348 path.pEnd = pPrev;
349 path_reverse_path();
350 db_finalize(&s);
351 return pid;
352 }else if( bag_find(&path.seen, pid) ){
353 /* pid is just an alternative path on one of the legs */
354 continue;
355 }
356 p = path_new_node(pid, pPrev, 0);
357 p->isPrim = pPrev->isPrim;
358 bag_insert(pPrev->isPrim ? &me : &you, pid);
359 }
360 db_reset(&s);
361 pPrev = pPrev->u.pPeer;
362 }
363 }
364 db_finalize(&s);
365 path_reset();
366 return 0;
367 }
@@ -453,11 +513,11 @@
453 }else if(0==iTo){
454 fossil_fatal("Invalid 'to' RID: 0");
455 }
456 if( iFrom==iTo ) return;
457 path_reset();
458 p = path_shortest(iFrom, iTo, 1, revOK==0, 0);
459 if( p==0 ) return;
460 path_reverse_path();
461 db_prepare(&q1,
462 "SELECT pfnid, fnid FROM mlink"
463 " WHERE mid=:mid AND (pfnid>0 OR fid==0)"
464
--- src/path.c
+++ src/path.c
@@ -18,40 +18,45 @@
18 ** directed acyclic graph (DAG) of check-ins.
19 */
20 #include "config.h"
21 #include "path.h"
22 #include <assert.h>
23 #include <math.h>
24
25 #if INTERFACE
26 /* Nodes for the paths through the DAG.
27 */
28 struct PathNode {
29 int rid; /* ID for this node */
30 u8 fromIsParent; /* True if pFrom is the parent of rid */
31 u8 isPrim; /* True if primary side of common ancestor */
32 u8 isHidden; /* Abbreviate output in "fossil bisect ls" */
33 char *zBranch; /* Branch name for this node. Might be NULL */
34 double mtime; /* Date/time of this check-in */
35 PathNode *pFrom; /* Node we came from */
36 union {
37 double rCost; /* Cost of getting to this node from pStart */
38 PathNode *pTo; /* Next on path from beginning to end */
39 } u;
40 PathNode *pAll; /* List of all nodes */
41 };
42 #endif
43
44 /*
45 ** Local variables for this module
46 */
47 static struct {
48 PQueue pending; /* Nodes pending review for inclusion in the graph */
49 PathNode *pAll; /* All nodes */
 
50 int nStep; /* Number of steps from first to last */
51 int nNotHidden; /* Number of steps not counting hidden nodes */
52 int brCost; /* Extra cost for moving to a different branch */
53 int revCost; /* Extra cost for changing directions */
54 PathNode *pStart; /* Earliest node */
55 PathNode *pEnd; /* Most recent */
56 } path;
57 static int path_debug = 0; /* Flag to enable debugging */
58
59 /*
60 ** Return the first (last) element of the computed path.
61 */
62 PathNode *path_first(void){ return path.pStart; }
@@ -66,25 +71,71 @@
71 ** Return the number of non-hidden steps in the computed path.
72 */
73 int path_length_not_hidden(void){ return path.nNotHidden; }
74
75 /*
76 ** Used for debugging only.
77 **
78 ** Given a RID, return the ISO date/time string and branch for the
79 ** corresponding check-in. Memory is held locally and is overwritten
80 ** with each call.
81 */
82 char *path_rid_desc(int rid){
83 static Stmt q;
84 static char *zDesc = 0;
85 db_static_prepare(&q,
86 "SELECT concat(strftime('%%Y%%m%%d%%H%%M',event.mtime),'/',value)"
87 " FROM event, tagxref"
88 " WHERE event.objid=:rid"
89 " AND tagxref.rid=:rid"
90 " AND tagxref.tagid=%d"
91 " AND tagxref.tagtype>0",
92 TAG_BRANCH
93 );
94 fossil_free(zDesc);
95 db_bind_int(&q, ":rid", rid);
96 if( db_step(&q)==SQLITE_ROW ){
97 zDesc = fossil_strdup(db_column_text(&q,0));
98 }
99 db_reset(&q);
100 return zDesc ? zDesc : "???";
101 }
102
103 /*
104 ** Create a new node and insert it into the path.pending queue.
105 */
106 static PathNode *path_new_node(int rid, PathNode *pFrom, int isParent){
107 PathNode *p;
108
109 p = fossil_malloc( sizeof(*p) );
110 memset(p, 0, sizeof(*p));
111 p->pAll = path.pAll;
112 path.pAll = p;
113 p->rid = rid;
114 p->fromIsParent = isParent;
115 p->pFrom = pFrom;
116 p->u.rCost = pFrom ? pFrom->u.rCost : 0.0;
117 if( path.brCost ){
118 p->zBranch = branch_of_rid(rid);
119 p->mtime = mtime_of_rid(rid, 0.0);
120 if( pFrom ){
121 p->u.rCost += fabs(pFrom->mtime - p->mtime);
122 if( fossil_strcmp(p->zBranch, pFrom->zBranch)!=0 ){
123 p->u.rCost += path.brCost;
124 }
125 }
126 }else{
127 /* When brCost==0, we try to minimize the number of nodes
128 ** along the path. The cost is just the number of nodes back
129 ** to the start. We do not need to know the branch name nor
130 ** the mtime */
131 p->u.rCost += 1.0;
132 }
133 if( path_debug ){
134 fossil_print("PUSH %-50s cost = %g\n", path_rid_desc(p->rid), p->u.rCost);
135 }
136 pqueuex_insert_ptr(&path.pending, (void*)p, p->u.rCost);
137 return p;
138 }
139
140 /*
141 ** Reset memory used by the shortest path algorithm.
@@ -92,13 +143,14 @@
143 void path_reset(void){
144 PathNode *p;
145 while( path.pAll ){
146 p = path.pAll;
147 path.pAll = p->pAll;
148 fossil_free(p->zBranch);
149 fossil_free(p);
150 }
151 pqueuex_clear(&path.pending);
152 memset(&path, 0, sizeof(path));
153 }
154
155 /*
156 ** Construct the path from path.pStart to path.pEnd in the u.pTo fields.
@@ -128,17 +180,19 @@
180 PathNode *path_shortest(
181 int iFrom, /* Path starts here */
182 int iTo, /* Path ends here */
183 int directOnly, /* No merge links if true */
184 int oneWayOnly, /* Parent->child only if true */
185 Bag *pHidden, /* Hidden nodes */
186 int branchCost /* Add extra cost to changing branches */
187 ){
188 Stmt s;
189 Bag seen;
190 PathNode *p;
191
192 path_reset();
193 path.brCost = branchCost;
194 path.pStart = path_new_node(iFrom, 0, 0);
195 if( iTo==iFrom ){
196 path.pEnd = path.pStart;
197 return path.pStart;
198 }
@@ -152,44 +206,46 @@
206 );
207 }else if( directOnly ){
208 db_prepare(&s,
209 "SELECT cid, 1 FROM plink WHERE pid=:pid AND isprim "
210 "UNION ALL "
211 "SELECT pid, 0 FROM plink WHERE :back AND cid=:pid AND isprim"
212 );
213 }else{
214 db_prepare(&s,
215 "SELECT cid, 1 FROM plink WHERE pid=:pid "
216 "UNION ALL "
217 "SELECT pid, 0 FROM plink WHERE :back AND cid=:pid"
218 );
219 }
220 bag_init(&seen);
221 while( (p = pqueuex_extract_ptr(&path.pending))!=0 ){
222 if( path_debug ){
223 printf("PULL %s %g\n", path_rid_desc(p->rid), p->u.rCost);
224 }
225 if( p->rid==iTo ){
226 db_finalize(&s);
227 path.pEnd = p;
228 path_reverse_path();
229 for(p=path.pStart->u.pTo; p; p=p->u.pTo ){
230 if( !p->isHidden ) path.nNotHidden++;
231 }
232 return path.pStart;
233 }
234 if( bag_find(&seen, p->rid) ) continue;
235 bag_insert(&seen, p->rid);
236 db_bind_int(&s, ":pid", p->rid);
237 if( !oneWayOnly ) db_bind_int(&s, ":back", !p->fromIsParent);
238 while( db_step(&s)==SQLITE_ROW ){
239 int cid = db_column_int(&s, 0);
240 int isParent = db_column_int(&s, 1);
241 PathNode *pNew;
242 if( bag_find(&seen, cid) ) continue;
243 pNew = path_new_node(cid, p, isParent);
244 if( pHidden && bag_find(pHidden,cid) ) pNew->isHidden = 1;
245 }
246 db_reset(&s);
247 }
248 db_finalize(&s);
249 path_reset();
250 return 0;
251 }
@@ -215,10 +271,22 @@
271 PathNode *p;
272 p = path.pStart;
273 if( p ) p = p->u.pTo;
274 return p;
275 }
276
277 /*
278 ** Return the branch for a path node.
279 **
280 ** Storage space is managed by the path subsystem. The returned value
281 ** is valid until the path is reset.
282 */
283 const char *path_branch(PathNode *p){
284 if( p==0 ) return 0;
285 if( p->zBranch==0 ) p->zBranch = branch_of_rid(p->rid);
286 return p->zBranch;
287 }
288
289 /*
290 ** Return an estimate of the number of comparisons remaining in order
291 ** to bisect path. This is based on the log2() of path.nStep.
292 */
@@ -238,11 +306,11 @@
306 int cid /* RID for check-in at the end of the path */
307 ){
308 PathNode *pPath;
309 int gen = 0;
310 Stmt ins;
311 pPath = path_shortest(cid, origid, 1, 0, 0, 0);
312 db_multi_exec(
313 "CREATE TEMP TABLE IF NOT EXISTS ancestor("
314 " rid INT UNIQUE,"
315 " generation INTEGER PRIMARY KEY"
316 ");"
@@ -261,58 +329,55 @@
329 }
330
331 /*
332 ** COMMAND: test-shortest-path
333 **
334 ** Usage: %fossil test-shortest-path [OPTIONS] VERSION1 VERSION2
335 **
336 ** Report the shortest path between two check-ins. Options:
337 **
338 ** --branch-cost N Additional cost N for changing branches
339 ** --debug Show debugging output
340 ** --one-way One-way forwards in time, parent->child only
341 ** --no-merge Follow only direct parent-child paths and omit
342 ** merge links.
343 */
344 void shortest_path_test_cmd(void){
345 int iFrom;
346 int iTo;
347 PathNode *p;
348 int n;
349 int directOnly;
350 int oneWay;
351 const char *zBrCost;
352
353 db_find_and_open_repository(0,0);
354 directOnly = find_option("no-merge",0,0)!=0;
355 oneWay = find_option("one-way",0,0)!=0;
356 zBrCost = find_option("branch-cost",0,1);
357 if( find_option("debug",0,0)!=0 ) path_debug = 1;
358 if( g.argc!=4 ) usage("VERSION1 VERSION2");
359 iFrom = name_to_rid(g.argv[2]);
360 iTo = name_to_rid(g.argv[3]);
361 p = path_shortest(iFrom, iTo, directOnly, oneWay, 0,
362 zBrCost ? atoi(zBrCost) : 0);
363 if( p==0 ){
364 fossil_fatal("no path from %s to %s", g.argv[1], g.argv[2]);
365 }
366 for(n=1, p=path.pStart; p; p=p->u.pTo, n++){
367 fossil_print("%4d: %s\n", n, path_rid_desc(p->rid));
368 }
369 path_debug = 0;
 
 
 
 
 
 
 
 
 
 
 
 
370 }
371
372 /*
373 ** Find the closest common ancestor of two nodes. "Closest" means the
374 ** fewest number of arcs.
375 */
376 int path_common_ancestor(int iMe, int iYou){
377 Stmt s;
378 PathNode *pThis;
379 PathNode *p;
380 Bag me, you;
381
382 if( iMe==iYou ) return iMe;
383 if( iMe==0 || iYou==0 ) return 0;
@@ -323,45 +388,40 @@
388 db_prepare(&s, "SELECT pid FROM plink WHERE cid=:cid");
389 bag_init(&me);
390 bag_insert(&me, iMe);
391 bag_init(&you);
392 bag_insert(&you, iYou);
393 while( (pThis = pqueuex_extract_ptr(&path.pending))!=0 ){
394 db_bind_int(&s, ":cid", pThis->rid);
395 while( db_step(&s)==SQLITE_ROW ){
396 int pid = db_column_int(&s, 0);
397 if( bag_find(pThis->isPrim ? &you : &me, pid) ){
398 /* pid is the common ancestor */
399 PathNode *pNext;
400 for(p=path.pAll; p && p->rid!=pid; p=p->pAll){}
401 assert( p!=0 );
402 pNext = p;
403 while( pNext ){
404 pNext = p->pFrom;
405 p->pFrom = pThis;
406 pThis = p;
407 p = pNext;
408 }
409 if( pThis==path.pStart ) path.pStart = path.pEnd;
410 path.pEnd = pThis;
411 path_reverse_path();
412 db_finalize(&s);
413 return pid;
414 }else if( bag_find(pThis->isPrim ? &me : &you, pid) ){
415 /* pid is just an alternative path to a node we've already visited */
416 continue;
417 }
418 p = path_new_node(pid, pThis, 0);
419 p->isPrim = pThis->isPrim;
420 bag_insert(pThis->isPrim ? &me : &you, pid);
421 }
422 db_reset(&s);
 
 
 
 
 
423 }
424 db_finalize(&s);
425 path_reset();
426 return 0;
427 }
@@ -453,11 +513,11 @@
513 }else if(0==iTo){
514 fossil_fatal("Invalid 'to' RID: 0");
515 }
516 if( iFrom==iTo ) return;
517 path_reset();
518 p = path_shortest(iFrom, iTo, 1, revOK==0, 0, 0);
519 if( p==0 ) return;
520 path_reverse_path();
521 db_prepare(&q1,
522 "SELECT pfnid, fnid FROM mlink"
523 " WHERE mid=:mid AND (pfnid>0 OR fid==0)"
524
+147 -27
--- src/pqueue.c
+++ src/pqueue.c
@@ -15,17 +15,24 @@
1515
**
1616
*******************************************************************************
1717
**
1818
** This file contains code used to implement a priority queue.
1919
** A priority queue is a list of items order by a floating point
20
-** value. We can insert integers with each integer tied to its
21
-** value then extract the integer with the smallest value.
20
+** value. Each value can be associated with either a pointer or
21
+** an integer. Items are inserted into the queue in an arbitrary
22
+** order, but are returned in order of the floating point value.
23
+**
24
+** This implementation uses a heap of QueueElement objects. The
25
+** root of the heap is PQueue.a[0]. Each node a[x] has two daughter
26
+** nodes a[x*2+1] and a[x*2+2]. The mother node of a[y] is a[(y-1)/2]
27
+** (assuming integer division rounded down). The following is always true:
28
+**
29
+** The value of any node is less than or equal two the values
30
+** of both daughter nodes. (The Heap Property).
2231
**
23
-** The way this queue is used, we never expect it to contain more
24
-** than 2 or 3 elements, so a simple array is sufficient as the
25
-** implementation. This could give worst case O(N) insert times,
26
-** but because of the nature of the problem we expect O(1) performance.
32
+** A consequence of the heap property is that a[0] always contains
33
+** the node with the smallest value.
2734
**
2835
** Compatibility note: Some versions of OpenSSL export a symbols
2936
** like "pqueue_insert". This is, technically, a bug in OpenSSL.
3037
** We work around it here by using "pqueuex_" instead of "pqueue_".
3138
*/
@@ -41,11 +48,14 @@
4148
*/
4249
struct PQueue {
4350
int cnt; /* Number of entries in the queue */
4451
int sz; /* Number of slots in a[] */
4552
struct QueueElement {
46
- int id; /* ID of the element */
53
+ union {
54
+ int id; /* ID of the element */
55
+ void *p; /* Pointer to an object */
56
+ } u;
4757
double value; /* Value of element. Kept in ascending order */
4858
} *a;
4959
};
5060
#endif
5161
@@ -69,44 +79,154 @@
6979
*/
7080
static void pqueuex_resize(PQueue *p, int N){
7181
p->a = fossil_realloc(p->a, sizeof(p->a[0])*N);
7282
p->sz = N;
7383
}
84
+
85
+/*
86
+** Allocate a new queue entry and return a pointer to it.
87
+*/
88
+static struct QueueElement *pqueuex_new_entry(PQueue *p){
89
+ if( p->cnt+1>p->sz ){
90
+ pqueuex_resize(p, p->cnt+7);
91
+ }
92
+ return &p->a[p->cnt++];
93
+}
94
+
95
+/*
96
+** Element p->a[p->cnt-1] has just been inserted. Shift entries
97
+** around so as to preserve the heap property.
98
+*/
99
+static void pqueuex_rebalance(PQueue *p){
100
+ int i, j;
101
+ struct QueueElement *a = p->a;
102
+ i = p->cnt-1;
103
+ while( (j = (i-1)/2)>=0 && a[j].value>a[i].value ){
104
+ struct QueueElement t = a[j];
105
+ a[j] = a[i];
106
+ a[i] = t;
107
+ i = j;
108
+ }
109
+}
74110
75111
/*
76112
** Insert element e into the queue.
77113
*/
78114
void pqueuex_insert(PQueue *p, int e, double v){
115
+ struct QueueElement *pE = pqueuex_new_entry(p);
116
+ pE->value = v;
117
+ pE->u.id = e;
118
+ pqueuex_rebalance(p);
119
+}
120
+void pqueuex_insert_ptr(PQueue *p, void *pPtr, double v){
121
+ struct QueueElement *pE = pqueuex_new_entry(p);
122
+ pE->value = v;
123
+ pE->u.p = pPtr;
124
+ pqueuex_rebalance(p);
125
+}
126
+
127
+/*
128
+** Remove and discard p->a[0] element from the queue. Rearrange
129
+** nodes to preserve the heap property.
130
+*/
131
+static void pqueuex_pop(PQueue *p){
79132
int i, j;
80
- if( p->cnt+1>p->sz ){
81
- pqueuex_resize(p, p->cnt+5);
82
- }
83
- for(i=0; i<p->cnt; i++){
84
- if( p->a[i].value>v ){
85
- for(j=p->cnt; j>i; j--){
86
- p->a[j] = p->a[j-1];
87
- }
88
- break;
89
- }
90
- }
91
- p->a[i].id = e;
92
- p->a[i].value = v;
93
- p->cnt++;
133
+ struct QueueElement *a = p->a;
134
+ struct QueueElement tmp;
135
+ i = 0;
136
+ a[0] = a[p->cnt-1];
137
+ p->cnt--;
138
+ while( (j = i*2+1)<p->cnt ){
139
+ if( j+1<p->cnt && a[j].value > a[j+1].value ) j++;
140
+ if( a[i].value < a[j].value ) break;
141
+ tmp = a[i];
142
+ a[i] = a[j];
143
+ a[j] = tmp;
144
+ i = j;
145
+ }
94146
}
95147
96148
/*
97149
** Extract the first element from the queue (the element with
98150
** the smallest value) and return its ID. Return 0 if the queue
99151
** is empty.
100152
*/
101153
int pqueuex_extract(PQueue *p){
102
- int e, i;
154
+ int e;
155
+ if( p->cnt==0 ){
156
+ return 0;
157
+ }
158
+ e = p->a[0].u.id;
159
+ pqueuex_pop(p);
160
+ return e;
161
+}
162
+void *pqueuex_extract_ptr(PQueue *p){
163
+ void *pPtr;
103164
if( p->cnt==0 ){
104165
return 0;
105166
}
106
- e = p->a[0].id;
107
- for(i=0; i<p->cnt-1; i++){
108
- p->a[i] = p->a[i+1];
167
+ pPtr = p->a[0].u.p;
168
+ pqueuex_pop(p);
169
+ return pPtr;
170
+}
171
+
172
+/*
173
+** Print the entire heap associated with the test-pqueue command.
174
+*/
175
+static void pqueuex_test_print(PQueue *p){
176
+ int j;
177
+ for(j=0; j<p->cnt; j++){
178
+ fossil_print("(%d) %g/%s ",j,p->a[j].value,p->a[j].u.p);
179
+ }
180
+ fossil_print("\n");
181
+}
182
+
183
+/*
184
+** COMMAND: test-pqueue
185
+**
186
+** This command is used for testing the PQueue object. There are one
187
+** or more arguments, each of the form:
188
+**
189
+** (1) NUMBER/TEXT
190
+** (2) ^
191
+** (3) -v
192
+**
193
+** Form (1) arguments add an entry to the queue with value NUMBER and
194
+** content TEXT. Form (2) pops off the queue entry with the smallest
195
+** value. Form (3) (the -v option) causes the heap to be displayed after
196
+** each subsequent operation.
197
+*/
198
+void pqueuex_test_cmd(void){
199
+ int i;
200
+ PQueue x;
201
+ const char *zId;
202
+ int bDebug = 0;
203
+
204
+ pqueuex_init(&x);
205
+ for(i=2; i<g.argc; i++){
206
+ const char *zArg = g.argv[i];
207
+ if( strcmp(zArg,"-v")==0 ){
208
+ bDebug = 1;
209
+ }else if( strcmp(zArg, "^")==0 ){
210
+ zId = pqueuex_extract_ptr(&x);
211
+ if( zId==0 ){
212
+ fossil_print("%2d: POP NULL\n", i);
213
+ }else{
214
+ fossil_print("%2d: POP \"%s\"\n", i, zId);
215
+ }
216
+ if( bDebug) pqueuex_test_print(&x);
217
+ }else{
218
+ double r = atof(zArg);
219
+ zId = strchr(zArg,'/');
220
+ if( zId==0 ) zId = zArg;
221
+ if( zId[0]=='/' ) zId++;
222
+ pqueuex_insert_ptr(&x, (void*)zId, r);
223
+ fossil_print("%2d: INSERT \"%s\"\n", i, zId);
224
+ if( bDebug) pqueuex_test_print(&x);
225
+ }
226
+ }
227
+ while( (zId = pqueuex_extract_ptr(&x))!=0 ){
228
+ fossil_print("... POP \"%s\"\n", zId);
229
+ if( bDebug) pqueuex_test_print(&x);
109230
}
110
- p->cnt--;
111
- return e;
231
+ pqueuex_clear(&x);
112232
}
113233
--- src/pqueue.c
+++ src/pqueue.c
@@ -15,17 +15,24 @@
15 **
16 *******************************************************************************
17 **
18 ** This file contains code used to implement a priority queue.
19 ** A priority queue is a list of items order by a floating point
20 ** value. We can insert integers with each integer tied to its
21 ** value then extract the integer with the smallest value.
 
 
 
 
 
 
 
 
 
22 **
23 ** The way this queue is used, we never expect it to contain more
24 ** than 2 or 3 elements, so a simple array is sufficient as the
25 ** implementation. This could give worst case O(N) insert times,
26 ** but because of the nature of the problem we expect O(1) performance.
27 **
28 ** Compatibility note: Some versions of OpenSSL export a symbols
29 ** like "pqueue_insert". This is, technically, a bug in OpenSSL.
30 ** We work around it here by using "pqueuex_" instead of "pqueue_".
31 */
@@ -41,11 +48,14 @@
41 */
42 struct PQueue {
43 int cnt; /* Number of entries in the queue */
44 int sz; /* Number of slots in a[] */
45 struct QueueElement {
46 int id; /* ID of the element */
 
 
 
47 double value; /* Value of element. Kept in ascending order */
48 } *a;
49 };
50 #endif
51
@@ -69,44 +79,154 @@
69 */
70 static void pqueuex_resize(PQueue *p, int N){
71 p->a = fossil_realloc(p->a, sizeof(p->a[0])*N);
72 p->sz = N;
73 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
75 /*
76 ** Insert element e into the queue.
77 */
78 void pqueuex_insert(PQueue *p, int e, double v){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79 int i, j;
80 if( p->cnt+1>p->sz ){
81 pqueuex_resize(p, p->cnt+5);
82 }
83 for(i=0; i<p->cnt; i++){
84 if( p->a[i].value>v ){
85 for(j=p->cnt; j>i; j--){
86 p->a[j] = p->a[j-1];
87 }
88 break;
89 }
90 }
91 p->a[i].id = e;
92 p->a[i].value = v;
93 p->cnt++;
94 }
95
96 /*
97 ** Extract the first element from the queue (the element with
98 ** the smallest value) and return its ID. Return 0 if the queue
99 ** is empty.
100 */
101 int pqueuex_extract(PQueue *p){
102 int e, i;
 
 
 
 
 
 
 
 
 
103 if( p->cnt==0 ){
104 return 0;
105 }
106 e = p->a[0].id;
107 for(i=0; i<p->cnt-1; i++){
108 p->a[i] = p->a[i+1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109 }
110 p->cnt--;
111 return e;
112 }
113
--- src/pqueue.c
+++ src/pqueue.c
@@ -15,17 +15,24 @@
15 **
16 *******************************************************************************
17 **
18 ** This file contains code used to implement a priority queue.
19 ** A priority queue is a list of items order by a floating point
20 ** value. Each value can be associated with either a pointer or
21 ** an integer. Items are inserted into the queue in an arbitrary
22 ** order, but are returned in order of the floating point value.
23 **
24 ** This implementation uses a heap of QueueElement objects. The
25 ** root of the heap is PQueue.a[0]. Each node a[x] has two daughter
26 ** nodes a[x*2+1] and a[x*2+2]. The mother node of a[y] is a[(y-1)/2]
27 ** (assuming integer division rounded down). The following is always true:
28 **
29 ** The value of any node is less than or equal two the values
30 ** of both daughter nodes. (The Heap Property).
31 **
32 ** A consequence of the heap property is that a[0] always contains
33 ** the node with the smallest value.
 
 
34 **
35 ** Compatibility note: Some versions of OpenSSL export a symbols
36 ** like "pqueue_insert". This is, technically, a bug in OpenSSL.
37 ** We work around it here by using "pqueuex_" instead of "pqueue_".
38 */
@@ -41,11 +48,14 @@
48 */
49 struct PQueue {
50 int cnt; /* Number of entries in the queue */
51 int sz; /* Number of slots in a[] */
52 struct QueueElement {
53 union {
54 int id; /* ID of the element */
55 void *p; /* Pointer to an object */
56 } u;
57 double value; /* Value of element. Kept in ascending order */
58 } *a;
59 };
60 #endif
61
@@ -69,44 +79,154 @@
79 */
80 static void pqueuex_resize(PQueue *p, int N){
81 p->a = fossil_realloc(p->a, sizeof(p->a[0])*N);
82 p->sz = N;
83 }
84
85 /*
86 ** Allocate a new queue entry and return a pointer to it.
87 */
88 static struct QueueElement *pqueuex_new_entry(PQueue *p){
89 if( p->cnt+1>p->sz ){
90 pqueuex_resize(p, p->cnt+7);
91 }
92 return &p->a[p->cnt++];
93 }
94
95 /*
96 ** Element p->a[p->cnt-1] has just been inserted. Shift entries
97 ** around so as to preserve the heap property.
98 */
99 static void pqueuex_rebalance(PQueue *p){
100 int i, j;
101 struct QueueElement *a = p->a;
102 i = p->cnt-1;
103 while( (j = (i-1)/2)>=0 && a[j].value>a[i].value ){
104 struct QueueElement t = a[j];
105 a[j] = a[i];
106 a[i] = t;
107 i = j;
108 }
109 }
110
111 /*
112 ** Insert element e into the queue.
113 */
114 void pqueuex_insert(PQueue *p, int e, double v){
115 struct QueueElement *pE = pqueuex_new_entry(p);
116 pE->value = v;
117 pE->u.id = e;
118 pqueuex_rebalance(p);
119 }
120 void pqueuex_insert_ptr(PQueue *p, void *pPtr, double v){
121 struct QueueElement *pE = pqueuex_new_entry(p);
122 pE->value = v;
123 pE->u.p = pPtr;
124 pqueuex_rebalance(p);
125 }
126
127 /*
128 ** Remove and discard p->a[0] element from the queue. Rearrange
129 ** nodes to preserve the heap property.
130 */
131 static void pqueuex_pop(PQueue *p){
132 int i, j;
133 struct QueueElement *a = p->a;
134 struct QueueElement tmp;
135 i = 0;
136 a[0] = a[p->cnt-1];
137 p->cnt--;
138 while( (j = i*2+1)<p->cnt ){
139 if( j+1<p->cnt && a[j].value > a[j+1].value ) j++;
140 if( a[i].value < a[j].value ) break;
141 tmp = a[i];
142 a[i] = a[j];
143 a[j] = tmp;
144 i = j;
145 }
 
146 }
147
148 /*
149 ** Extract the first element from the queue (the element with
150 ** the smallest value) and return its ID. Return 0 if the queue
151 ** is empty.
152 */
153 int pqueuex_extract(PQueue *p){
154 int e;
155 if( p->cnt==0 ){
156 return 0;
157 }
158 e = p->a[0].u.id;
159 pqueuex_pop(p);
160 return e;
161 }
162 void *pqueuex_extract_ptr(PQueue *p){
163 void *pPtr;
164 if( p->cnt==0 ){
165 return 0;
166 }
167 pPtr = p->a[0].u.p;
168 pqueuex_pop(p);
169 return pPtr;
170 }
171
172 /*
173 ** Print the entire heap associated with the test-pqueue command.
174 */
175 static void pqueuex_test_print(PQueue *p){
176 int j;
177 for(j=0; j<p->cnt; j++){
178 fossil_print("(%d) %g/%s ",j,p->a[j].value,p->a[j].u.p);
179 }
180 fossil_print("\n");
181 }
182
183 /*
184 ** COMMAND: test-pqueue
185 **
186 ** This command is used for testing the PQueue object. There are one
187 ** or more arguments, each of the form:
188 **
189 ** (1) NUMBER/TEXT
190 ** (2) ^
191 ** (3) -v
192 **
193 ** Form (1) arguments add an entry to the queue with value NUMBER and
194 ** content TEXT. Form (2) pops off the queue entry with the smallest
195 ** value. Form (3) (the -v option) causes the heap to be displayed after
196 ** each subsequent operation.
197 */
198 void pqueuex_test_cmd(void){
199 int i;
200 PQueue x;
201 const char *zId;
202 int bDebug = 0;
203
204 pqueuex_init(&x);
205 for(i=2; i<g.argc; i++){
206 const char *zArg = g.argv[i];
207 if( strcmp(zArg,"-v")==0 ){
208 bDebug = 1;
209 }else if( strcmp(zArg, "^")==0 ){
210 zId = pqueuex_extract_ptr(&x);
211 if( zId==0 ){
212 fossil_print("%2d: POP NULL\n", i);
213 }else{
214 fossil_print("%2d: POP \"%s\"\n", i, zId);
215 }
216 if( bDebug) pqueuex_test_print(&x);
217 }else{
218 double r = atof(zArg);
219 zId = strchr(zArg,'/');
220 if( zId==0 ) zId = zArg;
221 if( zId[0]=='/' ) zId++;
222 pqueuex_insert_ptr(&x, (void*)zId, r);
223 fossil_print("%2d: INSERT \"%s\"\n", i, zId);
224 if( bDebug) pqueuex_test_print(&x);
225 }
226 }
227 while( (zId = pqueuex_extract_ptr(&x))!=0 ){
228 fossil_print("... POP \"%s\"\n", zId);
229 if( bDebug) pqueuex_test_print(&x);
230 }
231 pqueuex_clear(&x);
 
232 }
233
--- src/security_audit.c
+++ src/security_audit.c
@@ -1019,5 +1019,92 @@
10191019
}
10201020
fclose(in);
10211021
@ </pre>
10221022
style_finish_page();
10231023
}
1024
+
1025
+/*
1026
+** WEBPAGE: logsummary
1027
+**
1028
+** Scan the error log and count the various kinds of entries.
1029
+*/
1030
+void logsummary_page(void){
1031
+ i64 szFile;
1032
+ char *zLog;
1033
+ FILE *in;
1034
+ int prevWasTime = 0;
1035
+ int nHack = 0;
1036
+ int nPanic = 0;
1037
+ int nOther = 0;
1038
+ int nTotal = 0;
1039
+ char z[10000];
1040
+
1041
+ login_check_credentials();
1042
+ if( !g.perm.Admin ){
1043
+ login_needed(0);
1044
+ return;
1045
+ }
1046
+ style_header("Server Hack Log");
1047
+ style_submenu_element("Log-Menu", "%R/setup-logmenu");
1048
+
1049
+ if( g.zErrlog==0 || fossil_strcmp(g.zErrlog,"-")==0 ){
1050
+ no_error_log_available();
1051
+ style_finish_page();
1052
+ return;
1053
+ }
1054
+ in = fossil_fopen(g.zErrlog, "rb");
1055
+ if( in==0 ){
1056
+ @ <p class='generalError'>Unable to open that file for reading!</p>
1057
+ style_finish_page();
1058
+ return;
1059
+ }
1060
+ szFile = file_size(g.zErrlog, ExtFILE);
1061
+ zLog = file_canonical_name_dup(g.zErrlog);
1062
+ @ Summary of messages contained within the %lld(szFile)-byte
1063
+ @ <a href="%R/errorlog?all">error log</a> found at
1064
+ @ "%h(zLog)".
1065
+ fossil_free(zLog);
1066
+ @ <hr>
1067
+ while( fgets(z, sizeof(z), in) ){
1068
+ if( prevWasTime
1069
+ && (strncmp(z,"possible hack attempt - 418 ", 27)==0)
1070
+ ){
1071
+ nHack++;
1072
+ prevWasTime = 0;
1073
+ continue;
1074
+ }
1075
+ if( prevWasTime
1076
+ && (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0)
1077
+ ){
1078
+ nPanic++;
1079
+ prevWasTime = 0;
1080
+ continue;
1081
+ }
1082
+ if( prevWasTime ) nOther++;
1083
+ if( strncmp(z, "--------", 8)==0 ){
1084
+ nTotal++;
1085
+ prevWasTime = 1;
1086
+ continue;
1087
+ }
1088
+ prevWasTime = 0;
1089
+ }
1090
+ fclose(in);
1091
+ @ <p><table border="a" cellspacing="0" cellpadding="5">
1092
+ @ <tr><td align="right">%d(nPanic)</td>
1093
+ if( nPanic>0 ){
1094
+ @ <td><a href="./paniclog">Panics</a></td>
1095
+ } else {
1096
+ @ <td>Panics</td>
1097
+ }
1098
+ @ <tr><td align="right">%d(nHack)</td>
1099
+ if( nHack>0 ){
1100
+ @ <td><a href="./hacklog">Hack Attempts</a></td>
1101
+ }else{
1102
+ @ <td>Hack Attempts</td>
1103
+ }
1104
+ @ <tr><td align="right">%d(nOther)</td>
1105
+ @ <td>Other</td>
1106
+ @ <tr><td align="right">%d(nTotal)</td>
1107
+ @ <td>Total Messages</td>
1108
+ @ </table>
1109
+ style_finish_page();
1110
+}
10241111
--- src/security_audit.c
+++ src/security_audit.c
@@ -1019,5 +1019,92 @@
1019 }
1020 fclose(in);
1021 @ </pre>
1022 style_finish_page();
1023 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1024
--- src/security_audit.c
+++ src/security_audit.c
@@ -1019,5 +1019,92 @@
1019 }
1020 fclose(in);
1021 @ </pre>
1022 style_finish_page();
1023 }
1024
1025 /*
1026 ** WEBPAGE: logsummary
1027 **
1028 ** Scan the error log and count the various kinds of entries.
1029 */
1030 void logsummary_page(void){
1031 i64 szFile;
1032 char *zLog;
1033 FILE *in;
1034 int prevWasTime = 0;
1035 int nHack = 0;
1036 int nPanic = 0;
1037 int nOther = 0;
1038 int nTotal = 0;
1039 char z[10000];
1040
1041 login_check_credentials();
1042 if( !g.perm.Admin ){
1043 login_needed(0);
1044 return;
1045 }
1046 style_header("Server Hack Log");
1047 style_submenu_element("Log-Menu", "%R/setup-logmenu");
1048
1049 if( g.zErrlog==0 || fossil_strcmp(g.zErrlog,"-")==0 ){
1050 no_error_log_available();
1051 style_finish_page();
1052 return;
1053 }
1054 in = fossil_fopen(g.zErrlog, "rb");
1055 if( in==0 ){
1056 @ <p class='generalError'>Unable to open that file for reading!</p>
1057 style_finish_page();
1058 return;
1059 }
1060 szFile = file_size(g.zErrlog, ExtFILE);
1061 zLog = file_canonical_name_dup(g.zErrlog);
1062 @ Summary of messages contained within the %lld(szFile)-byte
1063 @ <a href="%R/errorlog?all">error log</a> found at
1064 @ "%h(zLog)".
1065 fossil_free(zLog);
1066 @ <hr>
1067 while( fgets(z, sizeof(z), in) ){
1068 if( prevWasTime
1069 && (strncmp(z,"possible hack attempt - 418 ", 27)==0)
1070 ){
1071 nHack++;
1072 prevWasTime = 0;
1073 continue;
1074 }
1075 if( prevWasTime
1076 && (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0)
1077 ){
1078 nPanic++;
1079 prevWasTime = 0;
1080 continue;
1081 }
1082 if( prevWasTime ) nOther++;
1083 if( strncmp(z, "--------", 8)==0 ){
1084 nTotal++;
1085 prevWasTime = 1;
1086 continue;
1087 }
1088 prevWasTime = 0;
1089 }
1090 fclose(in);
1091 @ <p><table border="a" cellspacing="0" cellpadding="5">
1092 @ <tr><td align="right">%d(nPanic)</td>
1093 if( nPanic>0 ){
1094 @ <td><a href="./paniclog">Panics</a></td>
1095 } else {
1096 @ <td>Panics</td>
1097 }
1098 @ <tr><td align="right">%d(nHack)</td>
1099 if( nHack>0 ){
1100 @ <td><a href="./hacklog">Hack Attempts</a></td>
1101 }else{
1102 @ <td>Hack Attempts</td>
1103 }
1104 @ <tr><td align="right">%d(nOther)</td>
1105 @ <td>Other</td>
1106 @ <tr><td align="right">%d(nTotal)</td>
1107 @ <td>Total Messages</td>
1108 @ </table>
1109 style_finish_page();
1110 }
1111
--- src/setup.c
+++ src/setup.c
@@ -270,10 +270,13 @@
270270
@ &mdash;&mdash;
271271
@ <i>The remaining links are subsets of the Error Log</i>
272272
@ &mdash;&mdash;
273273
@ </td>
274274
275
+ setup_menu_entry("Error Summary", bErrLog ? "logsummary" : 0,
276
+ "Counts of the various message types seen in the Error Log.\n"
277
+ );
275278
setup_menu_entry("Panic Log", bErrLog ? "paniclog" : 0,
276279
"Only the most important messages in the Error Log:\n"
277280
"assertion faults, segmentation faults, and similar malfunctions.\n"
278281
);
279282
setup_menu_entry("Hack Log", bErrLog ? "hacklog" : 0,
280283
--- src/setup.c
+++ src/setup.c
@@ -270,10 +270,13 @@
270 @ &mdash;&mdash;
271 @ <i>The remaining links are subsets of the Error Log</i>
272 @ &mdash;&mdash;
273 @ </td>
274
 
 
 
275 setup_menu_entry("Panic Log", bErrLog ? "paniclog" : 0,
276 "Only the most important messages in the Error Log:\n"
277 "assertion faults, segmentation faults, and similar malfunctions.\n"
278 );
279 setup_menu_entry("Hack Log", bErrLog ? "hacklog" : 0,
280
--- src/setup.c
+++ src/setup.c
@@ -270,10 +270,13 @@
270 @ &mdash;&mdash;
271 @ <i>The remaining links are subsets of the Error Log</i>
272 @ &mdash;&mdash;
273 @ </td>
274
275 setup_menu_entry("Error Summary", bErrLog ? "logsummary" : 0,
276 "Counts of the various message types seen in the Error Log.\n"
277 );
278 setup_menu_entry("Panic Log", bErrLog ? "paniclog" : 0,
279 "Only the most important messages in the Error Log:\n"
280 "assertion faults, segmentation faults, and similar malfunctions.\n"
281 );
282 setup_menu_entry("Hack Log", bErrLog ? "hacklog" : 0,
283
--- src/setup.c
+++ src/setup.c
@@ -270,10 +270,13 @@
270270
@ &mdash;&mdash;
271271
@ <i>The remaining links are subsets of the Error Log</i>
272272
@ &mdash;&mdash;
273273
@ </td>
274274
275
+ setup_menu_entry("Error Summary", bErrLog ? "logsummary" : 0,
276
+ "Counts of the various message types seen in the Error Log.\n"
277
+ );
275278
setup_menu_entry("Panic Log", bErrLog ? "paniclog" : 0,
276279
"Only the most important messages in the Error Log:\n"
277280
"assertion faults, segmentation faults, and similar malfunctions.\n"
278281
);
279282
setup_menu_entry("Hack Log", bErrLog ? "hacklog" : 0,
280283
--- src/setup.c
+++ src/setup.c
@@ -270,10 +270,13 @@
270 @ &mdash;&mdash;
271 @ <i>The remaining links are subsets of the Error Log</i>
272 @ &mdash;&mdash;
273 @ </td>
274
 
 
 
275 setup_menu_entry("Panic Log", bErrLog ? "paniclog" : 0,
276 "Only the most important messages in the Error Log:\n"
277 "assertion faults, segmentation faults, and similar malfunctions.\n"
278 );
279 setup_menu_entry("Hack Log", bErrLog ? "hacklog" : 0,
280
--- src/setup.c
+++ src/setup.c
@@ -270,10 +270,13 @@
270 @ &mdash;&mdash;
271 @ <i>The remaining links are subsets of the Error Log</i>
272 @ &mdash;&mdash;
273 @ </td>
274
275 setup_menu_entry("Error Summary", bErrLog ? "logsummary" : 0,
276 "Counts of the various message types seen in the Error Log.\n"
277 );
278 setup_menu_entry("Panic Log", bErrLog ? "paniclog" : 0,
279 "Only the most important messages in the Error Log:\n"
280 "assertion faults, segmentation faults, and similar malfunctions.\n"
281 );
282 setup_menu_entry("Hack Log", bErrLog ? "hacklog" : 0,
283
+9 -9
--- src/setupuser.c
+++ src/setupuser.c
@@ -393,30 +393,30 @@
393393
}else if( !cgi_csrf_safe(2) ){
394394
/* This might be a cross-site request forgery, so ignore it */
395395
}else{
396396
/* We have all the information we need to make the change to the user */
397397
char c;
398
- char zCap[70], zNm[4];
398
+ char aCap[70], zNm[4];
399399
zNm[0] = 'a';
400400
zNm[2] = 0;
401401
for(i=0, c='a'; c<='z'; c++){
402402
zNm[1] = c;
403403
a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0;
404
- if( a[c&0x7f] ) zCap[i++] = c;
404
+ if( a[c&0x7f] ) aCap[i++] = c;
405405
}
406406
for(c='0'; c<='9'; c++){
407407
zNm[1] = c;
408408
a[c&0x7f] = P(zNm)!=0;
409
- if( a[c&0x7f] ) zCap[i++] = c;
409
+ if( a[c&0x7f] ) aCap[i++] = c;
410410
}
411411
for(c='A'; c<='Z'; c++){
412412
zNm[1] = c;
413413
a[c&0x7f] = P(zNm)!=0;
414
- if( a[c&0x7f] ) zCap[i++] = c;
414
+ if( a[c&0x7f] ) aCap[i++] = c;
415415
}
416416
417
- zCap[i] = 0;
417
+ aCap[i] = 0;
418418
zPw = P("pw");
419419
zLogin = P("login");
420420
if( strlen(zLogin)==0 ){
421421
const char *zRef = cgi_referer("setup_ulist");
422422
style_header("User Creation Error");
@@ -447,11 +447,11 @@
447447
cgi_csrf_verify();
448448
db_unprotect(PROTECT_USER);
449449
db_multi_exec(
450450
"REPLACE INTO user(uid,login,info,pw,cap,mtime) "
451451
"VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now())",
452
- uid, zLogin, P("info"), zPw, zCap
452
+ uid, zLogin, P("info"), zPw, &aCap[0]
453453
);
454454
if( zOldLogin && fossil_strcmp(zLogin, zOldLogin)!=0 ){
455455
if( alert_tables_exist() ){
456456
/* Rename matching subscriber entry, else the user cannot
457457
re-subscribe with their same email address. */
@@ -461,11 +461,11 @@
461461
admin_log( "Renamed user [%q] to [%q].", zOldLogin, zLogin );
462462
}
463463
db_protect_pop();
464464
setup_incr_cfgcnt();
465465
admin_log( "Updated user [%q] with capabilities [%q].",
466
- zLogin, zCap );
466
+ zLogin, &aCap[0] );
467467
if( atoi(PD("all","0"))>0 ){
468468
Blob sql;
469469
char *zErr = 0;
470470
blob_zero(&sql);
471471
if( zOldLogin==0 ){
@@ -496,20 +496,20 @@
496496
"(SELECT value FROM config WHERE name='project-code')),pw),"
497497
" info=%Q,"
498498
" cap=%Q,"
499499
" mtime=now()"
500500
" WHERE login=%Q;",
501
- zLogin, P("pw"), zLogin, P("info"), zCap,
501
+ zLogin, P("pw"), zLogin, P("info"), &aCap[0],
502502
zOldLogin
503503
);
504504
db_unprotect(PROTECT_USER);
505505
login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
506506
db_protect_pop();
507507
blob_reset(&sql);
508508
admin_log( "Updated user [%q] in all login groups "
509509
"with capabilities [%q].",
510
- zLogin, zCap );
510
+ zLogin, &aCap[0] );
511511
if( zErr ){
512512
const char *zRef = cgi_referer("setup_ulist");
513513
style_header("User Change Error");
514514
admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
515515
@ <span class="loginError">%h(zErr)</span>
516516
--- src/setupuser.c
+++ src/setupuser.c
@@ -393,30 +393,30 @@
393 }else if( !cgi_csrf_safe(2) ){
394 /* This might be a cross-site request forgery, so ignore it */
395 }else{
396 /* We have all the information we need to make the change to the user */
397 char c;
398 char zCap[70], zNm[4];
399 zNm[0] = 'a';
400 zNm[2] = 0;
401 for(i=0, c='a'; c<='z'; c++){
402 zNm[1] = c;
403 a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0;
404 if( a[c&0x7f] ) zCap[i++] = c;
405 }
406 for(c='0'; c<='9'; c++){
407 zNm[1] = c;
408 a[c&0x7f] = P(zNm)!=0;
409 if( a[c&0x7f] ) zCap[i++] = c;
410 }
411 for(c='A'; c<='Z'; c++){
412 zNm[1] = c;
413 a[c&0x7f] = P(zNm)!=0;
414 if( a[c&0x7f] ) zCap[i++] = c;
415 }
416
417 zCap[i] = 0;
418 zPw = P("pw");
419 zLogin = P("login");
420 if( strlen(zLogin)==0 ){
421 const char *zRef = cgi_referer("setup_ulist");
422 style_header("User Creation Error");
@@ -447,11 +447,11 @@
447 cgi_csrf_verify();
448 db_unprotect(PROTECT_USER);
449 db_multi_exec(
450 "REPLACE INTO user(uid,login,info,pw,cap,mtime) "
451 "VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now())",
452 uid, zLogin, P("info"), zPw, zCap
453 );
454 if( zOldLogin && fossil_strcmp(zLogin, zOldLogin)!=0 ){
455 if( alert_tables_exist() ){
456 /* Rename matching subscriber entry, else the user cannot
457 re-subscribe with their same email address. */
@@ -461,11 +461,11 @@
461 admin_log( "Renamed user [%q] to [%q].", zOldLogin, zLogin );
462 }
463 db_protect_pop();
464 setup_incr_cfgcnt();
465 admin_log( "Updated user [%q] with capabilities [%q].",
466 zLogin, zCap );
467 if( atoi(PD("all","0"))>0 ){
468 Blob sql;
469 char *zErr = 0;
470 blob_zero(&sql);
471 if( zOldLogin==0 ){
@@ -496,20 +496,20 @@
496 "(SELECT value FROM config WHERE name='project-code')),pw),"
497 " info=%Q,"
498 " cap=%Q,"
499 " mtime=now()"
500 " WHERE login=%Q;",
501 zLogin, P("pw"), zLogin, P("info"), zCap,
502 zOldLogin
503 );
504 db_unprotect(PROTECT_USER);
505 login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
506 db_protect_pop();
507 blob_reset(&sql);
508 admin_log( "Updated user [%q] in all login groups "
509 "with capabilities [%q].",
510 zLogin, zCap );
511 if( zErr ){
512 const char *zRef = cgi_referer("setup_ulist");
513 style_header("User Change Error");
514 admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
515 @ <span class="loginError">%h(zErr)</span>
516
--- src/setupuser.c
+++ src/setupuser.c
@@ -393,30 +393,30 @@
393 }else if( !cgi_csrf_safe(2) ){
394 /* This might be a cross-site request forgery, so ignore it */
395 }else{
396 /* We have all the information we need to make the change to the user */
397 char c;
398 char aCap[70], zNm[4];
399 zNm[0] = 'a';
400 zNm[2] = 0;
401 for(i=0, c='a'; c<='z'; c++){
402 zNm[1] = c;
403 a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0;
404 if( a[c&0x7f] ) aCap[i++] = c;
405 }
406 for(c='0'; c<='9'; c++){
407 zNm[1] = c;
408 a[c&0x7f] = P(zNm)!=0;
409 if( a[c&0x7f] ) aCap[i++] = c;
410 }
411 for(c='A'; c<='Z'; c++){
412 zNm[1] = c;
413 a[c&0x7f] = P(zNm)!=0;
414 if( a[c&0x7f] ) aCap[i++] = c;
415 }
416
417 aCap[i] = 0;
418 zPw = P("pw");
419 zLogin = P("login");
420 if( strlen(zLogin)==0 ){
421 const char *zRef = cgi_referer("setup_ulist");
422 style_header("User Creation Error");
@@ -447,11 +447,11 @@
447 cgi_csrf_verify();
448 db_unprotect(PROTECT_USER);
449 db_multi_exec(
450 "REPLACE INTO user(uid,login,info,pw,cap,mtime) "
451 "VALUES(nullif(%d,0),%Q,%Q,%Q,%Q,now())",
452 uid, zLogin, P("info"), zPw, &aCap[0]
453 );
454 if( zOldLogin && fossil_strcmp(zLogin, zOldLogin)!=0 ){
455 if( alert_tables_exist() ){
456 /* Rename matching subscriber entry, else the user cannot
457 re-subscribe with their same email address. */
@@ -461,11 +461,11 @@
461 admin_log( "Renamed user [%q] to [%q].", zOldLogin, zLogin );
462 }
463 db_protect_pop();
464 setup_incr_cfgcnt();
465 admin_log( "Updated user [%q] with capabilities [%q].",
466 zLogin, &aCap[0] );
467 if( atoi(PD("all","0"))>0 ){
468 Blob sql;
469 char *zErr = 0;
470 blob_zero(&sql);
471 if( zOldLogin==0 ){
@@ -496,20 +496,20 @@
496 "(SELECT value FROM config WHERE name='project-code')),pw),"
497 " info=%Q,"
498 " cap=%Q,"
499 " mtime=now()"
500 " WHERE login=%Q;",
501 zLogin, P("pw"), zLogin, P("info"), &aCap[0],
502 zOldLogin
503 );
504 db_unprotect(PROTECT_USER);
505 login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
506 db_protect_pop();
507 blob_reset(&sql);
508 admin_log( "Updated user [%q] in all login groups "
509 "with capabilities [%q].",
510 zLogin, &aCap[0] );
511 if( zErr ){
512 const char *zRef = cgi_referer("setup_ulist");
513 style_header("User Change Error");
514 admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
515 @ <span class="loginError">%h(zErr)</span>
516
+140 -77
--- src/timeline.c
+++ src/timeline.c
@@ -1113,11 +1113,11 @@
11131113
return mtime;
11141114
}
11151115
}
11161116
rid = symbolic_name_to_rid(z, "*");
11171117
if( rid ){
1118
- mtime = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
1118
+ mtime = mtime_of_rid(rid, 0.0);
11191119
}else{
11201120
mtime = db_double(-1.0,
11211121
"SELECT max(event.mtime) FROM event, tag, tagxref"
11221122
" WHERE tag.tagname GLOB 'event-%q*'"
11231123
" AND tagxref.tagid=tag.tagid AND tagxref.tagtype"
@@ -1408,11 +1408,12 @@
14081408
" AND plink.mtime<=(SELECT max(event.mtime) FROM tagxref, event"
14091409
" WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
14101410
" AND event.objid=tagxref.rid)"
14111411
" ORDER BY plink.mtime)"
14121412
"SELECT id FROM dx, tagxref"
1413
- " WHERE tagid=%d AND tagtype>0 AND rid=id LIMIT 1",
1413
+ " WHERE tagid=%d AND tagtype>0 AND rid=id"
1414
+ " ORDER BY dx.mtime LIMIT 1",
14141415
iFrom, iFrom, tagId, tagId
14151416
);
14161417
}else{
14171418
db_prepare(&q,
14181419
"WITH RECURSIVE dx(id,mtime) AS ("
@@ -1439,11 +1440,12 @@
14391440
" AND event.mtime>=(SELECT min(event.mtime) FROM tagxref, event"
14401441
" WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
14411442
" AND event.objid=tagxref.rid)"
14421443
" ORDER BY event.mtime DESC)"
14431444
"SELECT id FROM dx, tagxref"
1444
- " WHERE tagid=%d AND tagtype>0 AND rid=id LIMIT 1",
1445
+ " WHERE tagid=%d AND tagtype>0 AND rid=id"
1446
+ " ORDER BY dx.mtime DESC LIMIT 1",
14451447
iFrom, iFrom, tagId, tagId
14461448
);
14471449
}else{
14481450
db_prepare(&q,
14491451
"WITH RECURSIVE dx(id,mtime) AS ("
@@ -1572,14 +1574,14 @@
15721574
** dp=CHECKIN Same as 'd=CHECKIN&p=CHECKIN'
15731575
** dp2=CKIN2 Same as 'd2=CKIN2&p2=CKIN2'
15741576
** df=CHECKIN Same as 'd=CHECKIN&n1=all&nd'. Mnemonic: "Derived From"
15751577
** bt=CHECKIN "Back To". Show ancenstors going back to CHECKIN
15761578
** p=CX ... from CX back to time of CHECKIN
1577
-** from=CX ... shortest path from CX back to CHECKIN
1579
+** from=CX ... path from CX back to CHECKIN
15781580
** ft=CHECKIN "Forward To": Show decendents forward to CHECKIN
15791581
** d=CX ... from CX up to the time of CHECKIN
1580
-** from=CX ... shortest path from CX up to CHECKIN
1582
+** from=CX ... path from CX up to CHECKIN
15811583
** t=TAG Show only check-ins with the given TAG
15821584
** r=TAG Same as 't=TAG&rel'. Mnemonic: "Related"
15831585
** tl=TAGLIST Same as 't=TAGLIST&ms=brlist'. Mnemonic: "Tag List"
15841586
** rl=TAGLIST Same as 'r=TAGLIST&ms=brlist'. Mnemonic: "Related List"
15851587
** ml=TAGLIST Same as 'tl=TAGLIST&mionly'. Mnemonic: "Merge-in List"
@@ -1600,20 +1602,21 @@
16001602
** nsm Omit the submenu
16011603
** nc Omit all graph colors other than highlights
16021604
** v Show details of files changed
16031605
** vfx Show complete text of forum messages
16041606
** f=CHECKIN Family (immediate parents and children) of CHECKIN
1605
-** from=CHECKIN Path through common ancestor from...
1606
-** to=CHECKIN ... to this
1607
-** to2=CHECKIN ... backup name if to= doesn't resolve
1608
-** shortest ... show only the shortest path
1609
-** rel ... also show related checkins
1610
-** bt=PRIOR ... path from CHECKIN back to PRIOR
1611
-** ft=LATER ... path from CHECKIN forward to LATER
1612
-** me=CHECKIN Most direct path from...
1613
-** you=CHECKIN ... to this
1614
-** rel ... also show related checkins
1607
+** from=CHECKIN Path through common ancestor from CHECKIN...
1608
+** to=CHECKIN ... to this
1609
+** to2=CHECKIN ... backup name if to= doesn't resolve
1610
+** shortest ... pick path with least number of nodes
1611
+** rel ... also show related checkins
1612
+** min ... hide long sequences along same branch
1613
+** bt=PRIOR ... path from CHECKIN back to PRIOR
1614
+** ft=LATER ... path from CHECKIN forward to LATER
1615
+** me=CHECKIN Most direct path from CHECKIN...
1616
+** you=CHECKIN ... to this
1617
+** rel ... also show related checkins
16151618
** uf=FILE_HASH Show only check-ins that contain the given file version
16161619
** All qualifying check-ins are shown unless there is
16171620
** also an n= or n1= query parameter.
16181621
** chng=GLOBLIST Show only check-ins that involve changes to a file whose
16191622
** name matches one of the comma-separate GLOBLIST
@@ -1696,15 +1699,15 @@
16961699
const char *zThisUser = 0; /* Suppress links to this user */
16971700
HQuery url; /* URL for various branch links */
16981701
int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
16991702
const char *zTo2 = 0;
17001703
int to_rid = name_choice("to","to2",&zTo2); /* to= for path timelines */
1701
- int noMerge = P("shortest")==0; /* Follow merge links if shorter */
1704
+ int bShort = P("shortest")!=0; /* shortest possible path */
17021705
int me_rid = name_to_typed_rid(P("me"),"ci"); /* me= for common ancestory */
17031706
int you_rid = name_to_typed_rid(P("you"),"ci");/* you= for common ancst */
17041707
int pd_rid;
1705
- const char *zDPName; /* Value of p=, d=, or dp= params */
1708
+ const char *zDPNameP, *zDPNameD; /* Value of p=, d=, or dp= params */
17061709
double rBefore, rAfter, rCirca; /* Boundary times */
17071710
const char *z;
17081711
char *zOlderButton = 0; /* URL for Older button at the bottom */
17091712
char *zOlderButtonLabel = 0; /* Label for the Older Button */
17101713
char *zNewerButton = 0; /* URL for Newer button at the top */
@@ -1717,10 +1720,11 @@
17171720
int showCherrypicks = 1; /* True to show cherrypick merges */
17181721
int haveParameterN; /* True if n= query parameter present */
17191722
int from_to_mode = 0; /* 0: from,to. 1: from,ft 2: from,bt */
17201723
int showSql = PB("showsql"); /* True to show the SQL */
17211724
Blob allSql; /* Copy of all SQL text */
1725
+ int bMin = P("min")!=0; /* True if "min" query parameter used */
17221726
17231727
login_check_credentials();
17241728
url_initialize(&url, "timeline");
17251729
cgi_query_parameters_to_url(&url);
17261730
blob_init(&allSql, 0, 0);
@@ -1766,20 +1770,20 @@
17661770
}else{
17671771
nEntry = 50;
17681772
}
17691773
17701774
/* Query parameters d=, p=, and f= and variants */
1771
- p_rid = name_choice("p","p2", &zDPName);
1772
- d_rid = name_choice("d","d2", &zDPName);
1775
+ p_rid = name_choice("p","p2", &zDPNameP);
1776
+ d_rid = name_choice("d","d2", &zDPNameD);
17731777
z = P("f");
17741778
f_rid = z ? name_to_typed_rid(z,"ci") : 0;
17751779
z = P("df");
17761780
if( z && (d_rid = name_to_typed_rid(z,"ci"))!=0 ){
17771781
nEntry = 0;
17781782
useDividers = 0;
17791783
cgi_replace_query_parameter("d",fossil_strdup(z));
1780
- zDPName = z;
1784
+ zDPNameD = zDPNameP = z;
17811785
}
17821786
17831787
/* Undocumented query parameter to set JS mode */
17841788
builtin_set_js_delivery_mode(P("jsmode"),1);
17851789
@@ -1799,13 +1803,14 @@
17991803
showCherrypicks = 0;
18001804
}
18011805
18021806
/* To view the timeline, must have permission to read project data.
18031807
*/
1804
- pd_rid = name_choice("dp","dp2",&zDPName);
1808
+ pd_rid = name_choice("dp","dp2",&zDPNameP);
18051809
if( pd_rid ){
18061810
p_rid = d_rid = pd_rid;
1811
+ zDPNameD = zDPNameP;
18071812
}
18081813
if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum)
18091814
|| (bisectLocal && !g.perm.Setup)
18101815
){
18111816
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
@@ -2072,20 +2077,22 @@
20722077
const char *zTo = 0;
20732078
Blob ins;
20742079
int nNodeOnPath = 0;
20752080
int commonAncs = 0; /* Common ancestors of me_rid and you_rid. */
20762081
int earlierRid = 0, laterRid = 0;
2082
+ int cost = bShort ? 0 : 1;
2083
+ int nSkip = 0;
20772084
20782085
if( from_rid && to_rid ){
20792086
if( from_to_mode==0 ){
2080
- p = path_shortest(from_rid, to_rid, noMerge, 0, 0);
2087
+ p = path_shortest(from_rid, to_rid, 0, 0, 0, cost);
20812088
}else if( from_to_mode==1 ){
2082
- p = path_shortest(from_rid, to_rid, 0, 1, 0);
2089
+ p = path_shortest(from_rid, to_rid, 0, 1, 0, cost);
20832090
earlierRid = commonAncs = from_rid;
20842091
laterRid = to_rid;
20852092
}else{
2086
- p = path_shortest(to_rid, from_rid, 0, 1, 0);
2093
+ p = path_shortest(to_rid, from_rid, 0, 1, 0, cost);
20872094
earlierRid = commonAncs = to_rid;
20882095
laterRid = from_rid;
20892096
}
20902097
zFrom = P("from");
20912098
zTo = zTo2 ? zTo2 : P("to");
@@ -2112,20 +2119,25 @@
21122119
);
21132120
if( p ){
21142121
int cnt = 4;
21152122
blob_init(&ins, 0, 0);
21162123
blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
2117
- p = p->u.pTo;
2118
- while( p ){
2119
- if( cnt==8 ){
2124
+ if( p->u.pTo==0 ) bMin = 0;
2125
+ for(p=p->u.pTo; p; p=p->u.pTo){
2126
+ if( bMin
2127
+ && p->u.pTo!=0
2128
+ && fossil_strcmp(path_branch(p->pFrom),path_branch(p))==0
2129
+ && fossil_strcmp(path_branch(p),path_branch(p->u.pTo))==0
2130
+ ){
2131
+ nSkip++;
2132
+ }else if( cnt==8 ){
21202133
blob_append_sql(&ins, ",\n (%d)", p->rid);
21212134
cnt = 0;
21222135
}else{
21232136
cnt++;
21242137
blob_append_sql(&ins, ",(%d)", p->rid);
21252138
}
2126
- p = p->u.pTo;
21272139
}
21282140
}
21292141
path_reset();
21302142
db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
21312143
blob_reset(&ins);
@@ -2185,12 +2197,13 @@
21852197
style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
21862198
}
21872199
nNodeOnPath = db_int(0, "SELECT count(*) FROM temp.pathnode");
21882200
if( nNodeOnPath==1 && from_to_mode>0 ){
21892201
blob_appendf(&desc,"Check-in ");
2190
- }else if( from_to_mode>0 ){
2191
- blob_appendf(&desc, "%d check-ins on the shorted path from ",nNodeOnPath);
2202
+ }else if( bMin ){
2203
+ blob_appendf(&desc, "%d of %d check-ins along the path from ",
2204
+ nNodeOnPath, nNodeOnPath+nSkip);
21922205
}else{
21932206
blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
21942207
}
21952208
if( from_rid==selectedRid ){
21962209
blob_appendf(&desc, "<span class='timelineSelected'>");
@@ -2214,49 +2227,60 @@
22142227
}
22152228
}
22162229
}
22172230
addFileGlobDescription(zChng, &desc);
22182231
}else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
2219
- /* If p= or d= is present, ignore all other parameters other than n= */
2220
- char *zUuid;
2221
- const char *zCiName;
2232
+ /* If either p= or d= or both are present, ignore all other parameters
2233
+ ** other than n=, ft=, and bt= */
2234
+ const char *zBaseName;
22222235
int np = 0, nd;
22232236
const char *zBackTo = 0;
22242237
const char *zFwdTo = 0;
22252238
int ridBackTo = 0;
22262239
int ridFwdTo = 0;
2240
+ int bBackAdded = 0; /* True if the zBackTo node was added */
2241
+ int bFwdAdded = 0; /* True if the zBackTo node was added */
2242
+ int bSeparateDandP = 0; /* p_rid & d_rid both exist and are distinct */
22272243
22282244
tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
2229
- if( p_rid && d_rid ){
2230
- if( p_rid!=d_rid ) p_rid = d_rid;
2231
- if( !haveParameterN ) nEntry = 10;
2245
+ if( p_rid && d_rid && p_rid!=d_rid ){
2246
+ bSeparateDandP = 1;
2247
+ db_multi_exec(
2248
+ "CREATE TEMP TABLE IF NOT EXISTS ok_d(rid INTEGER PRIMARY KEY)"
2249
+ );
2250
+ }else{
2251
+ zBaseName = p_rid ? zDPNameP : zDPNameD;
22322252
}
22332253
db_multi_exec(
2234
- "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)"
2254
+ "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)"
22352255
);
22362256
add_extra_rids("ok", P("x"));
2237
- zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
2238
- p_rid ? p_rid : d_rid);
2239
- zCiName = zDPName;
2240
- if( zCiName==0 ) zCiName = zUuid;
22412257
blob_append_sql(&sql, " AND event.objid IN ok");
22422258
nd = 0;
22432259
if( d_rid ){
22442260
double rStopTime = 9e99;
22452261
zFwdTo = P("ft");
2262
+ if( zFwdTo && bSeparateDandP ){
2263
+ if( zError==0 ){
2264
+ zError = "Cannot use the ft= query parameter when both p= and d= "
2265
+ "are used and have distinct values.";
2266
+ }
2267
+ zFwdTo = 0;
2268
+ }
22462269
if( zFwdTo ){
2247
- double rStartDate = db_double(0.0,
2248
- "SELECT mtime FROM event WHERE objid=%d", d_rid);
2270
+ double rStartDate = mtime_of_rid(d_rid, 0.0);
22492271
ridFwdTo = first_checkin_with_tag_after_date(zFwdTo, rStartDate);
22502272
if( ridFwdTo==0 ){
22512273
ridFwdTo = name_to_typed_rid(zBackTo,"ci");
22522274
}
22532275
if( ridFwdTo ){
22542276
if( !haveParameterN ) nEntry = 0;
2255
- rStopTime = db_double(9e99,
2256
- "SELECT mtime FROM event WHERE objid=%d", ridFwdTo);
2277
+ rStopTime = mtime_of_rid(ridFwdTo, 9e99);
22572278
}
2279
+ }else if( bSeparateDandP ){
2280
+ rStopTime = mtime_of_rid(p_rid, 9e99);
2281
+ nEntry = 0;
22582282
}
22592283
if( rStopTime<9e99 ){
22602284
rStopTime += 5.8e-6; /* Round up by 1/2 second */
22612285
}
22622286
db_multi_exec(
@@ -2269,72 +2293,111 @@
22692293
" ORDER BY 2\n"
22702294
")\n"
22712295
"INSERT OR IGNORE INTO ok SELECT rid FROM dx LIMIT %d",
22722296
d_rid, rStopTime<8e99 ? 17 : 2, rStopTime, nEntry<=0 ? -1 : nEntry+1
22732297
);
2274
- nd = db_int(0, "SELECT count(*)-1 FROM ok");
2275
- if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
2276
- if( nd>0 || p_rid==0 ){
2277
- blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
2298
+ if( ridFwdTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridFwdTo) ){
2299
+ db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridFwdTo);
2300
+ bFwdAdded = 1;
22782301
}
2279
- if( useDividers && !selectedRid ) selectedRid = d_rid;
2280
- db_multi_exec("DELETE FROM ok");
2302
+ if( bSeparateDandP ){
2303
+ db_multi_exec(
2304
+ "INSERT INTO ok_d SELECT rid FROM ok;"
2305
+ "DELETE FROM ok;"
2306
+ );
2307
+ }else{
2308
+ nd = db_int(0, "SELECT count(*)-1 FROM ok");
2309
+ if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
2310
+ if( nd>0 || p_rid==0 ){
2311
+ blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
2312
+ }
2313
+ if( useDividers && !selectedRid ) selectedRid = d_rid;
2314
+ db_multi_exec("DELETE FROM ok");
2315
+ }
22812316
}
22822317
if( p_rid ){
22832318
zBackTo = P("bt");
2319
+ if( zBackTo && bSeparateDandP ){
2320
+ if( zError==0 ){
2321
+ zError = "Cannot use the bt= query parameter when both p= and d= "
2322
+ "are used and have distinct values.";
2323
+ }
2324
+ zBackTo = 0;
2325
+ }
22842326
if( zBackTo ){
2285
- double rDateLimit = db_double(0.0,
2286
- "SELECT mtime FROM event WHERE objid=%d", p_rid);
2327
+ double rDateLimit = mtime_of_rid(p_rid, 0.0);
22872328
ridBackTo = last_checkin_with_tag_before_date(zBackTo, rDateLimit);
22882329
if( ridBackTo==0 ){
22892330
ridBackTo = name_to_typed_rid(zBackTo,"ci");
22902331
}
22912332
if( ridBackTo && !haveParameterN ) nEntry = 0;
2333
+ }else if( bSeparateDandP ){
2334
+ ridBackTo = d_rid;
2335
+ nEntry = 0;
22922336
}
22932337
compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0, ridBackTo);
2294
- np = db_int(0, "SELECT count(*)-1 FROM ok");
2295
- if( np>0 || nd==0 ){
2296
- if( nd>0 ) blob_appendf(&desc, " and ");
2297
- blob_appendf(&desc, "%d ancestor%s", np, (1==np)?"":"s");
2338
+ if( ridBackTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridBackTo) ){
2339
+ db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridBackTo);
2340
+ bBackAdded = 1;
2341
+ }
2342
+ if( bSeparateDandP ){
2343
+ db_multi_exec("DELETE FROM ok WHERE rid NOT IN ok_d;");
22982344
db_multi_exec("%s", blob_sql_text(&sql));
2345
+ }else{
2346
+ np = db_int(0, "SELECT count(*)-1 FROM ok");
2347
+ if( np>0 || nd==0 ){
2348
+ if( nd>0 ) blob_appendf(&desc, " and ");
2349
+ blob_appendf(&desc, "%d ancestor%s", np, (1==np)?"":"s");
2350
+ db_multi_exec("%s", blob_sql_text(&sql));
2351
+ }
2352
+ if( useDividers && !selectedRid ) selectedRid = p_rid;
22992353
}
2300
- if( useDividers && !selectedRid ) selectedRid = p_rid;
23012354
}
2302
-
2303
- blob_appendf(&desc, " of %z%h</a>",
2304
- href("%R/info?name=%h", zCiName), zCiName);
2355
+ if( bSeparateDandP ){
2356
+ int n = db_int(0, "SELECT count(*) FROM ok");
2357
+ blob_reset(&desc);
2358
+ blob_appendf(&desc,
2359
+ "%d check-ins that are both ancestors of %z%h</a>"
2360
+ " and descendents of %z%h</a>",
2361
+ n,
2362
+ href("%R/info?name=%h",zDPNameP),zDPNameP,
2363
+ href("%R/info/name=%h",zDPNameD),zDPNameD
2364
+ );
2365
+ ridBackTo = 0;
2366
+ ridFwdTo = 0;
2367
+ }else{
2368
+ blob_appendf(&desc, " of %z%h</a>",
2369
+ href("%R/info?name=%h", zBaseName), zBaseName);
2370
+ }
23052371
if( ridBackTo ){
23062372
if( np==0 ){
23072373
blob_reset(&desc);
23082374
blob_appendf(&desc,
2309
- "Check-in %z%h</a> only (%z%h</a> is not an ancestor)",
2310
- href("%R/info?name=%h",zCiName), zCiName,
2375
+ "Check-in %z%h</a> only (%z%h</a> does not precede it)",
2376
+ href("%R/info?name=%h",zBaseName), zBaseName,
23112377
href("%R/info?name=%h",zBackTo), zBackTo);
23122378
}else{
2313
- blob_appendf(&desc, " back to %z%h</a>",
2314
- href("%R/info?name=%h",zBackTo), zBackTo);
2379
+ blob_appendf(&desc, " back to %z%h</a>%s",
2380
+ href("%R/info?name=%h",zBackTo), zBackTo,
2381
+ bBackAdded ? " (not a direct anscestor)" : "");
23152382
if( ridFwdTo && zFwdTo ){
2316
- blob_appendf(&desc, " and up to %z%h</a>",
2317
- href("%R/info?name=%h",zFwdTo), zFwdTo);
2383
+ blob_appendf(&desc, " and up to %z%h</a>%s",
2384
+ href("%R/info?name=%h",zFwdTo), zFwdTo,
2385
+ bFwdAdded ? " (not a direct descendent)" : "");
23182386
}
23192387
}
23202388
}else if( ridFwdTo ){
23212389
if( nd==0 ){
23222390
blob_reset(&desc);
23232391
blob_appendf(&desc,
2324
- "Check-in %z%h</a> only (%z%h</a> is not an descendant)",
2325
- href("%R/info?name=%h",zCiName), zCiName,
2392
+ "Check-in %z%h</a> only (%z%h</a> does not follow it)",
2393
+ href("%R/info?name=%h",zBaseName), zBaseName,
23262394
href("%R/info?name=%h",zFwdTo), zFwdTo);
23272395
}else{
2328
- blob_appendf(&desc, " up to %z%h</a>",
2329
- href("%R/info?name=%h",zFwdTo), zFwdTo);
2330
- }
2331
- }
2332
- if( d_rid ){
2333
- if( p_rid ){
2334
- /* If both p= and d= are set, we don't have the uuid of d yet. */
2335
- zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", d_rid);
2396
+ blob_appendf(&desc, " up to %z%h</a>%s",
2397
+ href("%R/info?name=%h",zFwdTo), zFwdTo,
2398
+ bFwdAdded ? " (not a direct descendent)":"");
23362399
}
23372400
}
23382401
if( advancedMenu ){
23392402
style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
23402403
}
23412404
--- src/timeline.c
+++ src/timeline.c
@@ -1113,11 +1113,11 @@
1113 return mtime;
1114 }
1115 }
1116 rid = symbolic_name_to_rid(z, "*");
1117 if( rid ){
1118 mtime = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
1119 }else{
1120 mtime = db_double(-1.0,
1121 "SELECT max(event.mtime) FROM event, tag, tagxref"
1122 " WHERE tag.tagname GLOB 'event-%q*'"
1123 " AND tagxref.tagid=tag.tagid AND tagxref.tagtype"
@@ -1408,11 +1408,12 @@
1408 " AND plink.mtime<=(SELECT max(event.mtime) FROM tagxref, event"
1409 " WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
1410 " AND event.objid=tagxref.rid)"
1411 " ORDER BY plink.mtime)"
1412 "SELECT id FROM dx, tagxref"
1413 " WHERE tagid=%d AND tagtype>0 AND rid=id LIMIT 1",
 
1414 iFrom, iFrom, tagId, tagId
1415 );
1416 }else{
1417 db_prepare(&q,
1418 "WITH RECURSIVE dx(id,mtime) AS ("
@@ -1439,11 +1440,12 @@
1439 " AND event.mtime>=(SELECT min(event.mtime) FROM tagxref, event"
1440 " WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
1441 " AND event.objid=tagxref.rid)"
1442 " ORDER BY event.mtime DESC)"
1443 "SELECT id FROM dx, tagxref"
1444 " WHERE tagid=%d AND tagtype>0 AND rid=id LIMIT 1",
 
1445 iFrom, iFrom, tagId, tagId
1446 );
1447 }else{
1448 db_prepare(&q,
1449 "WITH RECURSIVE dx(id,mtime) AS ("
@@ -1572,14 +1574,14 @@
1572 ** dp=CHECKIN Same as 'd=CHECKIN&p=CHECKIN'
1573 ** dp2=CKIN2 Same as 'd2=CKIN2&p2=CKIN2'
1574 ** df=CHECKIN Same as 'd=CHECKIN&n1=all&nd'. Mnemonic: "Derived From"
1575 ** bt=CHECKIN "Back To". Show ancenstors going back to CHECKIN
1576 ** p=CX ... from CX back to time of CHECKIN
1577 ** from=CX ... shortest path from CX back to CHECKIN
1578 ** ft=CHECKIN "Forward To": Show decendents forward to CHECKIN
1579 ** d=CX ... from CX up to the time of CHECKIN
1580 ** from=CX ... shortest path from CX up to CHECKIN
1581 ** t=TAG Show only check-ins with the given TAG
1582 ** r=TAG Same as 't=TAG&rel'. Mnemonic: "Related"
1583 ** tl=TAGLIST Same as 't=TAGLIST&ms=brlist'. Mnemonic: "Tag List"
1584 ** rl=TAGLIST Same as 'r=TAGLIST&ms=brlist'. Mnemonic: "Related List"
1585 ** ml=TAGLIST Same as 'tl=TAGLIST&mionly'. Mnemonic: "Merge-in List"
@@ -1600,20 +1602,21 @@
1600 ** nsm Omit the submenu
1601 ** nc Omit all graph colors other than highlights
1602 ** v Show details of files changed
1603 ** vfx Show complete text of forum messages
1604 ** f=CHECKIN Family (immediate parents and children) of CHECKIN
1605 ** from=CHECKIN Path through common ancestor from...
1606 ** to=CHECKIN ... to this
1607 ** to2=CHECKIN ... backup name if to= doesn't resolve
1608 ** shortest ... show only the shortest path
1609 ** rel ... also show related checkins
1610 ** bt=PRIOR ... path from CHECKIN back to PRIOR
1611 ** ft=LATER ... path from CHECKIN forward to LATER
1612 ** me=CHECKIN Most direct path from...
1613 ** you=CHECKIN ... to this
1614 ** rel ... also show related checkins
 
1615 ** uf=FILE_HASH Show only check-ins that contain the given file version
1616 ** All qualifying check-ins are shown unless there is
1617 ** also an n= or n1= query parameter.
1618 ** chng=GLOBLIST Show only check-ins that involve changes to a file whose
1619 ** name matches one of the comma-separate GLOBLIST
@@ -1696,15 +1699,15 @@
1696 const char *zThisUser = 0; /* Suppress links to this user */
1697 HQuery url; /* URL for various branch links */
1698 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
1699 const char *zTo2 = 0;
1700 int to_rid = name_choice("to","to2",&zTo2); /* to= for path timelines */
1701 int noMerge = P("shortest")==0; /* Follow merge links if shorter */
1702 int me_rid = name_to_typed_rid(P("me"),"ci"); /* me= for common ancestory */
1703 int you_rid = name_to_typed_rid(P("you"),"ci");/* you= for common ancst */
1704 int pd_rid;
1705 const char *zDPName; /* Value of p=, d=, or dp= params */
1706 double rBefore, rAfter, rCirca; /* Boundary times */
1707 const char *z;
1708 char *zOlderButton = 0; /* URL for Older button at the bottom */
1709 char *zOlderButtonLabel = 0; /* Label for the Older Button */
1710 char *zNewerButton = 0; /* URL for Newer button at the top */
@@ -1717,10 +1720,11 @@
1717 int showCherrypicks = 1; /* True to show cherrypick merges */
1718 int haveParameterN; /* True if n= query parameter present */
1719 int from_to_mode = 0; /* 0: from,to. 1: from,ft 2: from,bt */
1720 int showSql = PB("showsql"); /* True to show the SQL */
1721 Blob allSql; /* Copy of all SQL text */
 
1722
1723 login_check_credentials();
1724 url_initialize(&url, "timeline");
1725 cgi_query_parameters_to_url(&url);
1726 blob_init(&allSql, 0, 0);
@@ -1766,20 +1770,20 @@
1766 }else{
1767 nEntry = 50;
1768 }
1769
1770 /* Query parameters d=, p=, and f= and variants */
1771 p_rid = name_choice("p","p2", &zDPName);
1772 d_rid = name_choice("d","d2", &zDPName);
1773 z = P("f");
1774 f_rid = z ? name_to_typed_rid(z,"ci") : 0;
1775 z = P("df");
1776 if( z && (d_rid = name_to_typed_rid(z,"ci"))!=0 ){
1777 nEntry = 0;
1778 useDividers = 0;
1779 cgi_replace_query_parameter("d",fossil_strdup(z));
1780 zDPName = z;
1781 }
1782
1783 /* Undocumented query parameter to set JS mode */
1784 builtin_set_js_delivery_mode(P("jsmode"),1);
1785
@@ -1799,13 +1803,14 @@
1799 showCherrypicks = 0;
1800 }
1801
1802 /* To view the timeline, must have permission to read project data.
1803 */
1804 pd_rid = name_choice("dp","dp2",&zDPName);
1805 if( pd_rid ){
1806 p_rid = d_rid = pd_rid;
 
1807 }
1808 if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum)
1809 || (bisectLocal && !g.perm.Setup)
1810 ){
1811 login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
@@ -2072,20 +2077,22 @@
2072 const char *zTo = 0;
2073 Blob ins;
2074 int nNodeOnPath = 0;
2075 int commonAncs = 0; /* Common ancestors of me_rid and you_rid. */
2076 int earlierRid = 0, laterRid = 0;
 
 
2077
2078 if( from_rid && to_rid ){
2079 if( from_to_mode==0 ){
2080 p = path_shortest(from_rid, to_rid, noMerge, 0, 0);
2081 }else if( from_to_mode==1 ){
2082 p = path_shortest(from_rid, to_rid, 0, 1, 0);
2083 earlierRid = commonAncs = from_rid;
2084 laterRid = to_rid;
2085 }else{
2086 p = path_shortest(to_rid, from_rid, 0, 1, 0);
2087 earlierRid = commonAncs = to_rid;
2088 laterRid = from_rid;
2089 }
2090 zFrom = P("from");
2091 zTo = zTo2 ? zTo2 : P("to");
@@ -2112,20 +2119,25 @@
2112 );
2113 if( p ){
2114 int cnt = 4;
2115 blob_init(&ins, 0, 0);
2116 blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
2117 p = p->u.pTo;
2118 while( p ){
2119 if( cnt==8 ){
 
 
 
 
 
 
2120 blob_append_sql(&ins, ",\n (%d)", p->rid);
2121 cnt = 0;
2122 }else{
2123 cnt++;
2124 blob_append_sql(&ins, ",(%d)", p->rid);
2125 }
2126 p = p->u.pTo;
2127 }
2128 }
2129 path_reset();
2130 db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
2131 blob_reset(&ins);
@@ -2185,12 +2197,13 @@
2185 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
2186 }
2187 nNodeOnPath = db_int(0, "SELECT count(*) FROM temp.pathnode");
2188 if( nNodeOnPath==1 && from_to_mode>0 ){
2189 blob_appendf(&desc,"Check-in ");
2190 }else if( from_to_mode>0 ){
2191 blob_appendf(&desc, "%d check-ins on the shorted path from ",nNodeOnPath);
 
2192 }else{
2193 blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
2194 }
2195 if( from_rid==selectedRid ){
2196 blob_appendf(&desc, "<span class='timelineSelected'>");
@@ -2214,49 +2227,60 @@
2214 }
2215 }
2216 }
2217 addFileGlobDescription(zChng, &desc);
2218 }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
2219 /* If p= or d= is present, ignore all other parameters other than n= */
2220 char *zUuid;
2221 const char *zCiName;
2222 int np = 0, nd;
2223 const char *zBackTo = 0;
2224 const char *zFwdTo = 0;
2225 int ridBackTo = 0;
2226 int ridFwdTo = 0;
 
 
 
2227
2228 tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
2229 if( p_rid && d_rid ){
2230 if( p_rid!=d_rid ) p_rid = d_rid;
2231 if( !haveParameterN ) nEntry = 10;
 
 
 
 
2232 }
2233 db_multi_exec(
2234 "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)"
2235 );
2236 add_extra_rids("ok", P("x"));
2237 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d",
2238 p_rid ? p_rid : d_rid);
2239 zCiName = zDPName;
2240 if( zCiName==0 ) zCiName = zUuid;
2241 blob_append_sql(&sql, " AND event.objid IN ok");
2242 nd = 0;
2243 if( d_rid ){
2244 double rStopTime = 9e99;
2245 zFwdTo = P("ft");
 
 
 
 
 
 
 
2246 if( zFwdTo ){
2247 double rStartDate = db_double(0.0,
2248 "SELECT mtime FROM event WHERE objid=%d", d_rid);
2249 ridFwdTo = first_checkin_with_tag_after_date(zFwdTo, rStartDate);
2250 if( ridFwdTo==0 ){
2251 ridFwdTo = name_to_typed_rid(zBackTo,"ci");
2252 }
2253 if( ridFwdTo ){
2254 if( !haveParameterN ) nEntry = 0;
2255 rStopTime = db_double(9e99,
2256 "SELECT mtime FROM event WHERE objid=%d", ridFwdTo);
2257 }
 
 
 
2258 }
2259 if( rStopTime<9e99 ){
2260 rStopTime += 5.8e-6; /* Round up by 1/2 second */
2261 }
2262 db_multi_exec(
@@ -2269,72 +2293,111 @@
2269 " ORDER BY 2\n"
2270 ")\n"
2271 "INSERT OR IGNORE INTO ok SELECT rid FROM dx LIMIT %d",
2272 d_rid, rStopTime<8e99 ? 17 : 2, rStopTime, nEntry<=0 ? -1 : nEntry+1
2273 );
2274 nd = db_int(0, "SELECT count(*)-1 FROM ok");
2275 if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
2276 if( nd>0 || p_rid==0 ){
2277 blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
2278 }
2279 if( useDividers && !selectedRid ) selectedRid = d_rid;
2280 db_multi_exec("DELETE FROM ok");
 
 
 
 
 
 
 
 
 
 
 
 
2281 }
2282 if( p_rid ){
2283 zBackTo = P("bt");
 
 
 
 
 
 
 
2284 if( zBackTo ){
2285 double rDateLimit = db_double(0.0,
2286 "SELECT mtime FROM event WHERE objid=%d", p_rid);
2287 ridBackTo = last_checkin_with_tag_before_date(zBackTo, rDateLimit);
2288 if( ridBackTo==0 ){
2289 ridBackTo = name_to_typed_rid(zBackTo,"ci");
2290 }
2291 if( ridBackTo && !haveParameterN ) nEntry = 0;
 
 
 
2292 }
2293 compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0, ridBackTo);
2294 np = db_int(0, "SELECT count(*)-1 FROM ok");
2295 if( np>0 || nd==0 ){
2296 if( nd>0 ) blob_appendf(&desc, " and ");
2297 blob_appendf(&desc, "%d ancestor%s", np, (1==np)?"":"s");
 
 
2298 db_multi_exec("%s", blob_sql_text(&sql));
 
 
 
 
 
 
 
 
2299 }
2300 if( useDividers && !selectedRid ) selectedRid = p_rid;
2301 }
2302
2303 blob_appendf(&desc, " of %z%h</a>",
2304 href("%R/info?name=%h", zCiName), zCiName);
 
 
 
 
 
 
 
 
 
 
 
 
 
2305 if( ridBackTo ){
2306 if( np==0 ){
2307 blob_reset(&desc);
2308 blob_appendf(&desc,
2309 "Check-in %z%h</a> only (%z%h</a> is not an ancestor)",
2310 href("%R/info?name=%h",zCiName), zCiName,
2311 href("%R/info?name=%h",zBackTo), zBackTo);
2312 }else{
2313 blob_appendf(&desc, " back to %z%h</a>",
2314 href("%R/info?name=%h",zBackTo), zBackTo);
 
2315 if( ridFwdTo && zFwdTo ){
2316 blob_appendf(&desc, " and up to %z%h</a>",
2317 href("%R/info?name=%h",zFwdTo), zFwdTo);
 
2318 }
2319 }
2320 }else if( ridFwdTo ){
2321 if( nd==0 ){
2322 blob_reset(&desc);
2323 blob_appendf(&desc,
2324 "Check-in %z%h</a> only (%z%h</a> is not an descendant)",
2325 href("%R/info?name=%h",zCiName), zCiName,
2326 href("%R/info?name=%h",zFwdTo), zFwdTo);
2327 }else{
2328 blob_appendf(&desc, " up to %z%h</a>",
2329 href("%R/info?name=%h",zFwdTo), zFwdTo);
2330 }
2331 }
2332 if( d_rid ){
2333 if( p_rid ){
2334 /* If both p= and d= are set, we don't have the uuid of d yet. */
2335 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", d_rid);
2336 }
2337 }
2338 if( advancedMenu ){
2339 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
2340 }
2341
--- src/timeline.c
+++ src/timeline.c
@@ -1113,11 +1113,11 @@
1113 return mtime;
1114 }
1115 }
1116 rid = symbolic_name_to_rid(z, "*");
1117 if( rid ){
1118 mtime = mtime_of_rid(rid, 0.0);
1119 }else{
1120 mtime = db_double(-1.0,
1121 "SELECT max(event.mtime) FROM event, tag, tagxref"
1122 " WHERE tag.tagname GLOB 'event-%q*'"
1123 " AND tagxref.tagid=tag.tagid AND tagxref.tagtype"
@@ -1408,11 +1408,12 @@
1408 " AND plink.mtime<=(SELECT max(event.mtime) FROM tagxref, event"
1409 " WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
1410 " AND event.objid=tagxref.rid)"
1411 " ORDER BY plink.mtime)"
1412 "SELECT id FROM dx, tagxref"
1413 " WHERE tagid=%d AND tagtype>0 AND rid=id"
1414 " ORDER BY dx.mtime LIMIT 1",
1415 iFrom, iFrom, tagId, tagId
1416 );
1417 }else{
1418 db_prepare(&q,
1419 "WITH RECURSIVE dx(id,mtime) AS ("
@@ -1439,11 +1440,12 @@
1440 " AND event.mtime>=(SELECT min(event.mtime) FROM tagxref, event"
1441 " WHERE tagxref.tagid=%d AND tagxref.tagtype>0"
1442 " AND event.objid=tagxref.rid)"
1443 " ORDER BY event.mtime DESC)"
1444 "SELECT id FROM dx, tagxref"
1445 " WHERE tagid=%d AND tagtype>0 AND rid=id"
1446 " ORDER BY dx.mtime DESC LIMIT 1",
1447 iFrom, iFrom, tagId, tagId
1448 );
1449 }else{
1450 db_prepare(&q,
1451 "WITH RECURSIVE dx(id,mtime) AS ("
@@ -1572,14 +1574,14 @@
1574 ** dp=CHECKIN Same as 'd=CHECKIN&p=CHECKIN'
1575 ** dp2=CKIN2 Same as 'd2=CKIN2&p2=CKIN2'
1576 ** df=CHECKIN Same as 'd=CHECKIN&n1=all&nd'. Mnemonic: "Derived From"
1577 ** bt=CHECKIN "Back To". Show ancenstors going back to CHECKIN
1578 ** p=CX ... from CX back to time of CHECKIN
1579 ** from=CX ... path from CX back to CHECKIN
1580 ** ft=CHECKIN "Forward To": Show decendents forward to CHECKIN
1581 ** d=CX ... from CX up to the time of CHECKIN
1582 ** from=CX ... path from CX up to CHECKIN
1583 ** t=TAG Show only check-ins with the given TAG
1584 ** r=TAG Same as 't=TAG&rel'. Mnemonic: "Related"
1585 ** tl=TAGLIST Same as 't=TAGLIST&ms=brlist'. Mnemonic: "Tag List"
1586 ** rl=TAGLIST Same as 'r=TAGLIST&ms=brlist'. Mnemonic: "Related List"
1587 ** ml=TAGLIST Same as 'tl=TAGLIST&mionly'. Mnemonic: "Merge-in List"
@@ -1600,20 +1602,21 @@
1602 ** nsm Omit the submenu
1603 ** nc Omit all graph colors other than highlights
1604 ** v Show details of files changed
1605 ** vfx Show complete text of forum messages
1606 ** f=CHECKIN Family (immediate parents and children) of CHECKIN
1607 ** from=CHECKIN Path through common ancestor from CHECKIN...
1608 ** to=CHECKIN ... to this
1609 ** to2=CHECKIN ... backup name if to= doesn't resolve
1610 ** shortest ... pick path with least number of nodes
1611 ** rel ... also show related checkins
1612 ** min ... hide long sequences along same branch
1613 ** bt=PRIOR ... path from CHECKIN back to PRIOR
1614 ** ft=LATER ... path from CHECKIN forward to LATER
1615 ** me=CHECKIN Most direct path from CHECKIN...
1616 ** you=CHECKIN ... to this
1617 ** rel ... also show related checkins
1618 ** uf=FILE_HASH Show only check-ins that contain the given file version
1619 ** All qualifying check-ins are shown unless there is
1620 ** also an n= or n1= query parameter.
1621 ** chng=GLOBLIST Show only check-ins that involve changes to a file whose
1622 ** name matches one of the comma-separate GLOBLIST
@@ -1696,15 +1699,15 @@
1699 const char *zThisUser = 0; /* Suppress links to this user */
1700 HQuery url; /* URL for various branch links */
1701 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
1702 const char *zTo2 = 0;
1703 int to_rid = name_choice("to","to2",&zTo2); /* to= for path timelines */
1704 int bShort = P("shortest")!=0; /* shortest possible path */
1705 int me_rid = name_to_typed_rid(P("me"),"ci"); /* me= for common ancestory */
1706 int you_rid = name_to_typed_rid(P("you"),"ci");/* you= for common ancst */
1707 int pd_rid;
1708 const char *zDPNameP, *zDPNameD; /* Value of p=, d=, or dp= params */
1709 double rBefore, rAfter, rCirca; /* Boundary times */
1710 const char *z;
1711 char *zOlderButton = 0; /* URL for Older button at the bottom */
1712 char *zOlderButtonLabel = 0; /* Label for the Older Button */
1713 char *zNewerButton = 0; /* URL for Newer button at the top */
@@ -1717,10 +1720,11 @@
1720 int showCherrypicks = 1; /* True to show cherrypick merges */
1721 int haveParameterN; /* True if n= query parameter present */
1722 int from_to_mode = 0; /* 0: from,to. 1: from,ft 2: from,bt */
1723 int showSql = PB("showsql"); /* True to show the SQL */
1724 Blob allSql; /* Copy of all SQL text */
1725 int bMin = P("min")!=0; /* True if "min" query parameter used */
1726
1727 login_check_credentials();
1728 url_initialize(&url, "timeline");
1729 cgi_query_parameters_to_url(&url);
1730 blob_init(&allSql, 0, 0);
@@ -1766,20 +1770,20 @@
1770 }else{
1771 nEntry = 50;
1772 }
1773
1774 /* Query parameters d=, p=, and f= and variants */
1775 p_rid = name_choice("p","p2", &zDPNameP);
1776 d_rid = name_choice("d","d2", &zDPNameD);
1777 z = P("f");
1778 f_rid = z ? name_to_typed_rid(z,"ci") : 0;
1779 z = P("df");
1780 if( z && (d_rid = name_to_typed_rid(z,"ci"))!=0 ){
1781 nEntry = 0;
1782 useDividers = 0;
1783 cgi_replace_query_parameter("d",fossil_strdup(z));
1784 zDPNameD = zDPNameP = z;
1785 }
1786
1787 /* Undocumented query parameter to set JS mode */
1788 builtin_set_js_delivery_mode(P("jsmode"),1);
1789
@@ -1799,13 +1803,14 @@
1803 showCherrypicks = 0;
1804 }
1805
1806 /* To view the timeline, must have permission to read project data.
1807 */
1808 pd_rid = name_choice("dp","dp2",&zDPNameP);
1809 if( pd_rid ){
1810 p_rid = d_rid = pd_rid;
1811 zDPNameD = zDPNameP;
1812 }
1813 if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum)
1814 || (bisectLocal && !g.perm.Setup)
1815 ){
1816 login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
@@ -2072,20 +2077,22 @@
2077 const char *zTo = 0;
2078 Blob ins;
2079 int nNodeOnPath = 0;
2080 int commonAncs = 0; /* Common ancestors of me_rid and you_rid. */
2081 int earlierRid = 0, laterRid = 0;
2082 int cost = bShort ? 0 : 1;
2083 int nSkip = 0;
2084
2085 if( from_rid && to_rid ){
2086 if( from_to_mode==0 ){
2087 p = path_shortest(from_rid, to_rid, 0, 0, 0, cost);
2088 }else if( from_to_mode==1 ){
2089 p = path_shortest(from_rid, to_rid, 0, 1, 0, cost);
2090 earlierRid = commonAncs = from_rid;
2091 laterRid = to_rid;
2092 }else{
2093 p = path_shortest(to_rid, from_rid, 0, 1, 0, cost);
2094 earlierRid = commonAncs = to_rid;
2095 laterRid = from_rid;
2096 }
2097 zFrom = P("from");
2098 zTo = zTo2 ? zTo2 : P("to");
@@ -2112,20 +2119,25 @@
2119 );
2120 if( p ){
2121 int cnt = 4;
2122 blob_init(&ins, 0, 0);
2123 blob_append_sql(&ins, "INSERT INTO pathnode(x) VALUES(%d)", p->rid);
2124 if( p->u.pTo==0 ) bMin = 0;
2125 for(p=p->u.pTo; p; p=p->u.pTo){
2126 if( bMin
2127 && p->u.pTo!=0
2128 && fossil_strcmp(path_branch(p->pFrom),path_branch(p))==0
2129 && fossil_strcmp(path_branch(p),path_branch(p->u.pTo))==0
2130 ){
2131 nSkip++;
2132 }else if( cnt==8 ){
2133 blob_append_sql(&ins, ",\n (%d)", p->rid);
2134 cnt = 0;
2135 }else{
2136 cnt++;
2137 blob_append_sql(&ins, ",(%d)", p->rid);
2138 }
 
2139 }
2140 }
2141 path_reset();
2142 db_multi_exec("%s", blob_str(&ins)/*safe-for-%s*/);
2143 blob_reset(&ins);
@@ -2185,12 +2197,13 @@
2197 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
2198 }
2199 nNodeOnPath = db_int(0, "SELECT count(*) FROM temp.pathnode");
2200 if( nNodeOnPath==1 && from_to_mode>0 ){
2201 blob_appendf(&desc,"Check-in ");
2202 }else if( bMin ){
2203 blob_appendf(&desc, "%d of %d check-ins along the path from ",
2204 nNodeOnPath, nNodeOnPath+nSkip);
2205 }else{
2206 blob_appendf(&desc, "%d check-ins going from ", nNodeOnPath);
2207 }
2208 if( from_rid==selectedRid ){
2209 blob_appendf(&desc, "<span class='timelineSelected'>");
@@ -2214,49 +2227,60 @@
2227 }
2228 }
2229 }
2230 addFileGlobDescription(zChng, &desc);
2231 }else if( (p_rid || d_rid) && g.perm.Read && zTagSql==0 ){
2232 /* If either p= or d= or both are present, ignore all other parameters
2233 ** other than n=, ft=, and bt= */
2234 const char *zBaseName;
2235 int np = 0, nd;
2236 const char *zBackTo = 0;
2237 const char *zFwdTo = 0;
2238 int ridBackTo = 0;
2239 int ridFwdTo = 0;
2240 int bBackAdded = 0; /* True if the zBackTo node was added */
2241 int bFwdAdded = 0; /* True if the zBackTo node was added */
2242 int bSeparateDandP = 0; /* p_rid & d_rid both exist and are distinct */
2243
2244 tmFlags |= TIMELINE_XMERGE | TIMELINE_FILLGAPS;
2245 if( p_rid && d_rid && p_rid!=d_rid ){
2246 bSeparateDandP = 1;
2247 db_multi_exec(
2248 "CREATE TEMP TABLE IF NOT EXISTS ok_d(rid INTEGER PRIMARY KEY)"
2249 );
2250 }else{
2251 zBaseName = p_rid ? zDPNameP : zDPNameD;
2252 }
2253 db_multi_exec(
2254 "CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY)"
2255 );
2256 add_extra_rids("ok", P("x"));
 
 
 
 
2257 blob_append_sql(&sql, " AND event.objid IN ok");
2258 nd = 0;
2259 if( d_rid ){
2260 double rStopTime = 9e99;
2261 zFwdTo = P("ft");
2262 if( zFwdTo && bSeparateDandP ){
2263 if( zError==0 ){
2264 zError = "Cannot use the ft= query parameter when both p= and d= "
2265 "are used and have distinct values.";
2266 }
2267 zFwdTo = 0;
2268 }
2269 if( zFwdTo ){
2270 double rStartDate = mtime_of_rid(d_rid, 0.0);
 
2271 ridFwdTo = first_checkin_with_tag_after_date(zFwdTo, rStartDate);
2272 if( ridFwdTo==0 ){
2273 ridFwdTo = name_to_typed_rid(zBackTo,"ci");
2274 }
2275 if( ridFwdTo ){
2276 if( !haveParameterN ) nEntry = 0;
2277 rStopTime = mtime_of_rid(ridFwdTo, 9e99);
 
2278 }
2279 }else if( bSeparateDandP ){
2280 rStopTime = mtime_of_rid(p_rid, 9e99);
2281 nEntry = 0;
2282 }
2283 if( rStopTime<9e99 ){
2284 rStopTime += 5.8e-6; /* Round up by 1/2 second */
2285 }
2286 db_multi_exec(
@@ -2269,72 +2293,111 @@
2293 " ORDER BY 2\n"
2294 ")\n"
2295 "INSERT OR IGNORE INTO ok SELECT rid FROM dx LIMIT %d",
2296 d_rid, rStopTime<8e99 ? 17 : 2, rStopTime, nEntry<=0 ? -1 : nEntry+1
2297 );
2298 if( ridFwdTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridFwdTo) ){
2299 db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridFwdTo);
2300 bFwdAdded = 1;
 
2301 }
2302 if( bSeparateDandP ){
2303 db_multi_exec(
2304 "INSERT INTO ok_d SELECT rid FROM ok;"
2305 "DELETE FROM ok;"
2306 );
2307 }else{
2308 nd = db_int(0, "SELECT count(*)-1 FROM ok");
2309 if( nd>=0 ) db_multi_exec("%s", blob_sql_text(&sql));
2310 if( nd>0 || p_rid==0 ){
2311 blob_appendf(&desc, "%d descendant%s", nd,(1==nd)?"":"s");
2312 }
2313 if( useDividers && !selectedRid ) selectedRid = d_rid;
2314 db_multi_exec("DELETE FROM ok");
2315 }
2316 }
2317 if( p_rid ){
2318 zBackTo = P("bt");
2319 if( zBackTo && bSeparateDandP ){
2320 if( zError==0 ){
2321 zError = "Cannot use the bt= query parameter when both p= and d= "
2322 "are used and have distinct values.";
2323 }
2324 zBackTo = 0;
2325 }
2326 if( zBackTo ){
2327 double rDateLimit = mtime_of_rid(p_rid, 0.0);
 
2328 ridBackTo = last_checkin_with_tag_before_date(zBackTo, rDateLimit);
2329 if( ridBackTo==0 ){
2330 ridBackTo = name_to_typed_rid(zBackTo,"ci");
2331 }
2332 if( ridBackTo && !haveParameterN ) nEntry = 0;
2333 }else if( bSeparateDandP ){
2334 ridBackTo = d_rid;
2335 nEntry = 0;
2336 }
2337 compute_ancestors(p_rid, nEntry==0 ? 0 : nEntry+1, 0, ridBackTo);
2338 if( ridBackTo && !db_exists("SELECT 1 FROM ok WHERE rid=%d",ridBackTo) ){
2339 db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridBackTo);
2340 bBackAdded = 1;
2341 }
2342 if( bSeparateDandP ){
2343 db_multi_exec("DELETE FROM ok WHERE rid NOT IN ok_d;");
2344 db_multi_exec("%s", blob_sql_text(&sql));
2345 }else{
2346 np = db_int(0, "SELECT count(*)-1 FROM ok");
2347 if( np>0 || nd==0 ){
2348 if( nd>0 ) blob_appendf(&desc, " and ");
2349 blob_appendf(&desc, "%d ancestor%s", np, (1==np)?"":"s");
2350 db_multi_exec("%s", blob_sql_text(&sql));
2351 }
2352 if( useDividers && !selectedRid ) selectedRid = p_rid;
2353 }
 
2354 }
2355 if( bSeparateDandP ){
2356 int n = db_int(0, "SELECT count(*) FROM ok");
2357 blob_reset(&desc);
2358 blob_appendf(&desc,
2359 "%d check-ins that are both ancestors of %z%h</a>"
2360 " and descendents of %z%h</a>",
2361 n,
2362 href("%R/info?name=%h",zDPNameP),zDPNameP,
2363 href("%R/info/name=%h",zDPNameD),zDPNameD
2364 );
2365 ridBackTo = 0;
2366 ridFwdTo = 0;
2367 }else{
2368 blob_appendf(&desc, " of %z%h</a>",
2369 href("%R/info?name=%h", zBaseName), zBaseName);
2370 }
2371 if( ridBackTo ){
2372 if( np==0 ){
2373 blob_reset(&desc);
2374 blob_appendf(&desc,
2375 "Check-in %z%h</a> only (%z%h</a> does not precede it)",
2376 href("%R/info?name=%h",zBaseName), zBaseName,
2377 href("%R/info?name=%h",zBackTo), zBackTo);
2378 }else{
2379 blob_appendf(&desc, " back to %z%h</a>%s",
2380 href("%R/info?name=%h",zBackTo), zBackTo,
2381 bBackAdded ? " (not a direct anscestor)" : "");
2382 if( ridFwdTo && zFwdTo ){
2383 blob_appendf(&desc, " and up to %z%h</a>%s",
2384 href("%R/info?name=%h",zFwdTo), zFwdTo,
2385 bFwdAdded ? " (not a direct descendent)" : "");
2386 }
2387 }
2388 }else if( ridFwdTo ){
2389 if( nd==0 ){
2390 blob_reset(&desc);
2391 blob_appendf(&desc,
2392 "Check-in %z%h</a> only (%z%h</a> does not follow it)",
2393 href("%R/info?name=%h",zBaseName), zBaseName,
2394 href("%R/info?name=%h",zFwdTo), zFwdTo);
2395 }else{
2396 blob_appendf(&desc, " up to %z%h</a>%s",
2397 href("%R/info?name=%h",zFwdTo), zFwdTo,
2398 bFwdAdded ? " (not a direct descendent)":"");
 
 
 
 
 
2399 }
2400 }
2401 if( advancedMenu ){
2402 style_submenu_checkbox("v", "Files", (zType[0]!='a' && zType[0]!='c'),0);
2403 }
2404
+2 -2
--- src/winhttp.c
+++ src/winhttp.c
@@ -996,11 +996,11 @@
996996
**
997997
** -D|--display DISPLAY-NAME
998998
**
999999
** Sets the display name of the service. This name is shown
10001000
** by graphical interface programs. By default, the display name
1001
-** equals to the service name.
1001
+** is equal to the service name.
10021002
**
10031003
** -S|--start TYPE
10041004
**
10051005
** Sets the start type of the service. TYPE can be "manual",
10061006
** which means you need to start the service yourself with the
@@ -1019,11 +1019,11 @@
10191019
** -W|--password PASSWORD
10201020
**
10211021
** Password for the user account.
10221022
**
10231023
** The following options are more or less the same as for the "server"
1024
-** command and influence the behaviour of the http server:
1024
+** command and influence the behavior of the http server:
10251025
**
10261026
** --baseurl URL
10271027
**
10281028
** Use URL as the base (useful for reverse proxies)
10291029
**
10301030
--- src/winhttp.c
+++ src/winhttp.c
@@ -996,11 +996,11 @@
996 **
997 ** -D|--display DISPLAY-NAME
998 **
999 ** Sets the display name of the service. This name is shown
1000 ** by graphical interface programs. By default, the display name
1001 ** equals to the service name.
1002 **
1003 ** -S|--start TYPE
1004 **
1005 ** Sets the start type of the service. TYPE can be "manual",
1006 ** which means you need to start the service yourself with the
@@ -1019,11 +1019,11 @@
1019 ** -W|--password PASSWORD
1020 **
1021 ** Password for the user account.
1022 **
1023 ** The following options are more or less the same as for the "server"
1024 ** command and influence the behaviour of the http server:
1025 **
1026 ** --baseurl URL
1027 **
1028 ** Use URL as the base (useful for reverse proxies)
1029 **
1030
--- src/winhttp.c
+++ src/winhttp.c
@@ -996,11 +996,11 @@
996 **
997 ** -D|--display DISPLAY-NAME
998 **
999 ** Sets the display name of the service. This name is shown
1000 ** by graphical interface programs. By default, the display name
1001 ** is equal to the service name.
1002 **
1003 ** -S|--start TYPE
1004 **
1005 ** Sets the start type of the service. TYPE can be "manual",
1006 ** which means you need to start the service yourself with the
@@ -1019,11 +1019,11 @@
1019 ** -W|--password PASSWORD
1020 **
1021 ** Password for the user account.
1022 **
1023 ** The following options are more or less the same as for the "server"
1024 ** command and influence the behavior of the http server:
1025 **
1026 ** --baseurl URL
1027 **
1028 ** Use URL as the base (useful for reverse proxies)
1029 **
1030

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button