Fossil SCM

Integrate andygoth-timeline-ms for broader testing and comment

andygoth 2016-11-07 00:35 trunk merge
Commit 5e1736971142349745b909abb741d919a08951b6
+32
--- src/cgi.c
+++ src/cgi.c
@@ -483,10 +483,13 @@
483483
** Copies are made of both the zName and zValue parameters.
484484
*/
485485
void cgi_set_parameter(const char *zName, const char *zValue){
486486
cgi_set_parameter_nocopy(mprintf("%s",zName), mprintf("%s",zValue), 0);
487487
}
488
+void cgi_set_query_parameter(const char *zName, const char *zValue){
489
+ cgi_set_parameter_nocopy(mprintf("%s",zName), mprintf("%s",zValue), 1);
490
+}
488491
489492
/*
490493
** Replace a parameter with a new value.
491494
*/
492495
void cgi_replace_parameter(const char *zName, const char *zValue){
@@ -508,10 +511,39 @@
508511
return;
509512
}
510513
}
511514
cgi_set_parameter_nocopy(zName, zValue, 1);
512515
}
516
+
517
+/*
518
+** Delete a parameter.
519
+*/
520
+void cgi_delete_parameter(const char *zName){
521
+ int i;
522
+ for(i=0; i<nUsedQP; i++){
523
+ if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){
524
+ --nUsedQP;
525
+ if( i<nUsedQP ){
526
+ memmove(aParamQP+i, aParamQP+i+1, sizeof(*aParamQP)*(nUsedQP-i));
527
+ }
528
+ return;
529
+ }
530
+ }
531
+}
532
+void cgi_delete_query_parameter(const char *zName){
533
+ int i;
534
+ for(i=0; i<nUsedQP; i++){
535
+ if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){
536
+ assert( aParamQP[i].isQP );
537
+ --nUsedQP;
538
+ if( i<nUsedQP ){
539
+ memmove(aParamQP+i, aParamQP+i+1, sizeof(*aParamQP)*(nUsedQP-i));
540
+ }
541
+ return;
542
+ }
543
+ }
544
+}
513545
514546
/*
515547
** Add a query parameter. The zName portion is fixed but a copy
516548
** must be made of zValue.
517549
*/
518550
--- src/cgi.c
+++ src/cgi.c
@@ -483,10 +483,13 @@
483 ** Copies are made of both the zName and zValue parameters.
484 */
485 void cgi_set_parameter(const char *zName, const char *zValue){
486 cgi_set_parameter_nocopy(mprintf("%s",zName), mprintf("%s",zValue), 0);
487 }
 
 
 
488
489 /*
490 ** Replace a parameter with a new value.
491 */
492 void cgi_replace_parameter(const char *zName, const char *zValue){
@@ -508,10 +511,39 @@
508 return;
509 }
510 }
511 cgi_set_parameter_nocopy(zName, zValue, 1);
512 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
514 /*
515 ** Add a query parameter. The zName portion is fixed but a copy
516 ** must be made of zValue.
517 */
518
--- src/cgi.c
+++ src/cgi.c
@@ -483,10 +483,13 @@
483 ** Copies are made of both the zName and zValue parameters.
484 */
485 void cgi_set_parameter(const char *zName, const char *zValue){
486 cgi_set_parameter_nocopy(mprintf("%s",zName), mprintf("%s",zValue), 0);
487 }
488 void cgi_set_query_parameter(const char *zName, const char *zValue){
489 cgi_set_parameter_nocopy(mprintf("%s",zName), mprintf("%s",zValue), 1);
490 }
491
492 /*
493 ** Replace a parameter with a new value.
494 */
495 void cgi_replace_parameter(const char *zName, const char *zValue){
@@ -508,10 +511,39 @@
511 return;
512 }
513 }
514 cgi_set_parameter_nocopy(zName, zValue, 1);
515 }
516
517 /*
518 ** Delete a parameter.
519 */
520 void cgi_delete_parameter(const char *zName){
521 int i;
522 for(i=0; i<nUsedQP; i++){
523 if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){
524 --nUsedQP;
525 if( i<nUsedQP ){
526 memmove(aParamQP+i, aParamQP+i+1, sizeof(*aParamQP)*(nUsedQP-i));
527 }
528 return;
529 }
530 }
531 }
532 void cgi_delete_query_parameter(const char *zName){
533 int i;
534 for(i=0; i<nUsedQP; i++){
535 if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){
536 assert( aParamQP[i].isQP );
537 --nUsedQP;
538 if( i<nUsedQP ){
539 memmove(aParamQP+i, aParamQP+i+1, sizeof(*aParamQP)*(nUsedQP-i));
540 }
541 return;
542 }
543 }
544 }
545
546 /*
547 ** Add a query parameter. The zName portion is fixed but a copy
548 ** must be made of zValue.
549 */
550
+14 -1
--- src/encode.c
+++ src/encode.c
@@ -23,11 +23,11 @@
2323
/*
2424
** Make the given string safe for HTML by converting every "<" into "&lt;",
2525
** every ">" into "&gt;" and every "&" into "&amp;". Return a pointer
2626
** to a new string obtained from malloc().
2727
**
28
-** We also encode " as &quot; so that it can appear as an argument
28
+** We also encode " as &quot; and ' as &#39; so they can appear as an argument
2929
** to markup.
3030
*/
3131
char *htmlize(const char *zIn, int n){
3232
int c;
3333
int i = 0;
@@ -39,10 +39,11 @@
3939
switch( c ){
4040
case '<': count += 4; break;
4141
case '>': count += 4; break;
4242
case '&': count += 5; break;
4343
case '"': count += 6; break;
44
+ case '\'': count += 5; break;
4445
default: count++; break;
4546
}
4647
i++;
4748
}
4849
i = 0;
@@ -74,10 +75,17 @@
7475
zOut[i++] = 'u';
7576
zOut[i++] = 'o';
7677
zOut[i++] = 't';
7778
zOut[i++] = ';';
7879
break;
80
+ case '\'':
81
+ zOut[i++] = '&';
82
+ zOut[i++] = '#';
83
+ zOut[i++] = '3';
84
+ zOut[i++] = '9';
85
+ zOut[i++] = ';';
86
+ break;
7987
default:
8088
zOut[i++] = c;
8189
break;
8290
}
8391
zIn++;
@@ -112,10 +120,15 @@
112120
break;
113121
case '"':
114122
if( j<i ) blob_append(p, zIn+j, i-j);
115123
blob_append(p, "&quot;", 6);
116124
j = i+1;
125
+ break;
126
+ case '\'':
127
+ if( j<i ) blob_append(p, zIn+j, i-j);
128
+ blob_append(p, "&#39;", 5);
129
+ j = i+1;
117130
break;
118131
}
119132
}
120133
if( j<i ) blob_append(p, zIn+j, i-j);
121134
}
122135
--- src/encode.c
+++ src/encode.c
@@ -23,11 +23,11 @@
23 /*
24 ** Make the given string safe for HTML by converting every "<" into "&lt;",
25 ** every ">" into "&gt;" and every "&" into "&amp;". Return a pointer
26 ** to a new string obtained from malloc().
27 **
28 ** We also encode " as &quot; so that it can appear as an argument
29 ** to markup.
30 */
31 char *htmlize(const char *zIn, int n){
32 int c;
33 int i = 0;
@@ -39,10 +39,11 @@
39 switch( c ){
40 case '<': count += 4; break;
41 case '>': count += 4; break;
42 case '&': count += 5; break;
43 case '"': count += 6; break;
 
44 default: count++; break;
45 }
46 i++;
47 }
48 i = 0;
@@ -74,10 +75,17 @@
74 zOut[i++] = 'u';
75 zOut[i++] = 'o';
76 zOut[i++] = 't';
77 zOut[i++] = ';';
78 break;
 
 
 
 
 
 
 
79 default:
80 zOut[i++] = c;
81 break;
82 }
83 zIn++;
@@ -112,10 +120,15 @@
112 break;
113 case '"':
114 if( j<i ) blob_append(p, zIn+j, i-j);
115 blob_append(p, "&quot;", 6);
116 j = i+1;
 
 
 
 
 
117 break;
118 }
119 }
120 if( j<i ) blob_append(p, zIn+j, i-j);
121 }
122
--- src/encode.c
+++ src/encode.c
@@ -23,11 +23,11 @@
23 /*
24 ** Make the given string safe for HTML by converting every "<" into "&lt;",
25 ** every ">" into "&gt;" and every "&" into "&amp;". Return a pointer
26 ** to a new string obtained from malloc().
27 **
28 ** We also encode " as &quot; and ' as &#39; so they can appear as an argument
29 ** to markup.
30 */
31 char *htmlize(const char *zIn, int n){
32 int c;
33 int i = 0;
@@ -39,10 +39,11 @@
39 switch( c ){
40 case '<': count += 4; break;
41 case '>': count += 4; break;
42 case '&': count += 5; break;
43 case '"': count += 6; break;
44 case '\'': count += 5; break;
45 default: count++; break;
46 }
47 i++;
48 }
49 i = 0;
@@ -74,10 +75,17 @@
75 zOut[i++] = 'u';
76 zOut[i++] = 'o';
77 zOut[i++] = 't';
78 zOut[i++] = ';';
79 break;
80 case '\'':
81 zOut[i++] = '&';
82 zOut[i++] = '#';
83 zOut[i++] = '3';
84 zOut[i++] = '9';
85 zOut[i++] = ';';
86 break;
87 default:
88 zOut[i++] = c;
89 break;
90 }
91 zIn++;
@@ -112,10 +120,15 @@
120 break;
121 case '"':
122 if( j<i ) blob_append(p, zIn+j, i-j);
123 blob_append(p, "&quot;", 6);
124 j = i+1;
125 break;
126 case '\'':
127 if( j<i ) blob_append(p, zIn+j, i-j);
128 blob_append(p, "&#39;", 5);
129 j = i+1;
130 break;
131 }
132 }
133 if( j<i ) blob_append(p, zIn+j, i-j);
134 }
135
+10 -2
--- src/import.c
+++ src/import.c
@@ -76,18 +76,26 @@
7676
} gg;
7777
7878
/*
7979
** Duplicate a string.
8080
*/
81
-char *fossil_strdup(const char *zOrig){
81
+char *fossil_strndup(const char *zOrig, int len){
8282
char *z = 0;
8383
if( zOrig ){
84
- int n = strlen(zOrig);
84
+ int n;
85
+ if( len<0 ){
86
+ n = strlen(zOrig);
87
+ }else{
88
+ for( n=0; zOrig[n] && n<len; ++n );
89
+ }
8590
z = fossil_malloc( n+1 );
8691
memcpy(z, zOrig, n+1);
8792
}
8893
return z;
94
+}
95
+char *fossil_strdup(const char *zOrig){
96
+ return fossil_strndup(zOrig, -1);
8997
}
9098
9199
/*
92100
** A no-op "xFinish" method
93101
*/
94102
--- src/import.c
+++ src/import.c
@@ -76,18 +76,26 @@
76 } gg;
77
78 /*
79 ** Duplicate a string.
80 */
81 char *fossil_strdup(const char *zOrig){
82 char *z = 0;
83 if( zOrig ){
84 int n = strlen(zOrig);
 
 
 
 
 
85 z = fossil_malloc( n+1 );
86 memcpy(z, zOrig, n+1);
87 }
88 return z;
 
 
 
89 }
90
91 /*
92 ** A no-op "xFinish" method
93 */
94
--- src/import.c
+++ src/import.c
@@ -76,18 +76,26 @@
76 } gg;
77
78 /*
79 ** Duplicate a string.
80 */
81 char *fossil_strndup(const char *zOrig, int len){
82 char *z = 0;
83 if( zOrig ){
84 int n;
85 if( len<0 ){
86 n = strlen(zOrig);
87 }else{
88 for( n=0; zOrig[n] && n<len; ++n );
89 }
90 z = fossil_malloc( n+1 );
91 memcpy(z, zOrig, n+1);
92 }
93 return z;
94 }
95 char *fossil_strdup(const char *zOrig){
96 return fossil_strndup(zOrig, -1);
97 }
98
99 /*
100 ** A no-op "xFinish" method
101 */
102
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -72,10 +72,12 @@
7272
BLOB_APPEND_LITERAL(ob, "&gt;");
7373
}else if( data[i]=='&' ){
7474
BLOB_APPEND_LITERAL(ob, "&amp;");
7575
}else if( data[i]=='"' ){
7676
BLOB_APPEND_LITERAL(ob, "&quot;");
77
+ }else if( data[i]=='\'' ){
78
+ BLOB_APPEND_LITERAL(ob, "&#39;");
7779
}else{
7880
break;
7981
}
8082
i++;
8183
}
8284
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -72,10 +72,12 @@
72 BLOB_APPEND_LITERAL(ob, "&gt;");
73 }else if( data[i]=='&' ){
74 BLOB_APPEND_LITERAL(ob, "&amp;");
75 }else if( data[i]=='"' ){
76 BLOB_APPEND_LITERAL(ob, "&quot;");
 
 
77 }else{
78 break;
79 }
80 i++;
81 }
82
--- src/markdown_html.c
+++ src/markdown_html.c
@@ -72,10 +72,12 @@
72 BLOB_APPEND_LITERAL(ob, "&gt;");
73 }else if( data[i]=='&' ){
74 BLOB_APPEND_LITERAL(ob, "&amp;");
75 }else if( data[i]=='"' ){
76 BLOB_APPEND_LITERAL(ob, "&quot;");
77 }else if( data[i]=='\'' ){
78 BLOB_APPEND_LITERAL(ob, "&#39;");
79 }else{
80 break;
81 }
82 i++;
83 }
84
+299 -32
--- src/timeline.c
+++ src/timeline.c
@@ -281,11 +281,11 @@
281281
char zTime[20];
282282
283283
if( zDate==0 ){
284284
zDate = "YYYY-MM-DD HH:MM:SS"; /* Something wrong with the repo */
285285
}
286
- modPending = moderation_pending(rid);
286
+ modPending = moderation_pending(rid);
287287
if( tagid ){
288288
if( modPending ) tagid = -tagid;
289289
if( tagid==prevTagid ){
290290
if( tmFlags & TIMELINE_BRIEF ){
291291
suppressCnt++;
@@ -573,11 +573,11 @@
573573
}
574574
continue;
575575
}
576576
zA = href("%R/artifact/%!S",fid?zNew:zOld);
577577
if( content_is_private(fid) ){
578
- zUnpub = UNPUB_TAG;
578
+ zUnpub = UNPUB_TAG;
579579
}
580580
if( isNew ){
581581
@ <li> %s(zA)%h(zFilename)</a>%s(zId) %s(zUnpub)
582582
if( isMergeNew ){
583583
@ (added by merge)
@@ -1218,10 +1218,225 @@
12181218
if( zChng==0 || zChng[0]==0 ) return;
12191219
blob_appendf(pDescription, " that include changes to files matching %Q",
12201220
zChng);
12211221
}
12221222
1223
+/*
1224
+** Tag match expression type code.
1225
+*/
1226
+typedef enum {
1227
+ MS_EXACT, /* Matches a single tag by exact string comparison. */
1228
+ MS_GLOB, /* Matches tags against a list of GLOB patterns. */
1229
+ MS_LIKE, /* Matches tags against a list of LIKE patterns. */
1230
+ MS_REGEXP /* Matches tags against a list of regular expressions. */
1231
+} MatchStyle;
1232
+
1233
+/*
1234
+** Quote a tag string by surrounding it with double quotes and preceding
1235
+** internal double quotes and backslashes with backslashes.
1236
+*/
1237
+static const char *tagQuote(
1238
+ int len, /* Maximum length of zTag, or negative for unlimited */
1239
+ const char *zTag /* Tag string */
1240
+){
1241
+ Blob blob = BLOB_INITIALIZER;
1242
+ int i, j;
1243
+ blob_zero(&blob);
1244
+ blob_append(&blob, "\"", 1);
1245
+ for( i=j=0; zTag[j] && (len<0 || j<len); ++j ){
1246
+ if( zTag[j]=='\"' || zTag[j]=='\\' ){
1247
+ if( j>i ){
1248
+ blob_append(&blob, zTag+i, j-i);
1249
+ }
1250
+ blob_append(&blob, "\\", 1);
1251
+ i = j;
1252
+ }
1253
+ }
1254
+ if( j>i ){
1255
+ blob_append(&blob, zTag+i, j-i);
1256
+ }
1257
+ blob_append(&blob, "\"", 1);
1258
+ return blob_str(&blob);
1259
+}
1260
+
1261
+/*
1262
+** Construct the tag match SQL expression.
1263
+**
1264
+** This function is adapted from glob_expr() to support the MS_EXACT, MS_GLOB,
1265
+** MS_LIKE, and MS_REGEXP match styles. For MS_EXACT, the returned expression
1266
+** checks for integer match against the tag ID which is looked up directly by
1267
+** this function. For the other modes, the returned SQL expression performs
1268
+** string comparisons against the tag names, so it is necessary to join against
1269
+** the tag table to access the "tagname" column.
1270
+**
1271
+** Each pattern is adjusted to to start with "sym-" and be anchored at end.
1272
+**
1273
+** In MS_REGEXP mode, backslash can be used to protect delimiter characters.
1274
+** The backslashes are not removed from the regular expression.
1275
+**
1276
+** In addition to assembling and returning an SQL expression, this function
1277
+** makes an English-language description of the patterns being matched, suitable
1278
+** for display in the web interface.
1279
+**
1280
+** If any errors arise during processing, *zError is set to an error message.
1281
+** Otherwise it is set to NULL.
1282
+*/
1283
+static const char *tagMatchExpression(
1284
+ MatchStyle matchStyle, /* Match style code */
1285
+ const char *zTag, /* Tag name, match pattern, or pattern list */
1286
+ const char **zDesc, /* Output expression description string */
1287
+ const char **zError /* Output error string */
1288
+){
1289
+ Blob expr = BLOB_INITIALIZER; /* SQL expression string assembly buffer */
1290
+ Blob desc = BLOB_INITIALIZER; /* English description of match patterns */
1291
+ Blob err = BLOB_INITIALIZER; /* Error text assembly buffer */
1292
+ const char *zStart; /* Text at start of expression */
1293
+ const char *zDelimiter; /* Text between expression terms */
1294
+ const char *zEnd; /* Text at end of expression */
1295
+ const char *zPrefix; /* Text before each match pattern */
1296
+ const char *zSuffix; /* Text after each match pattern */
1297
+ const char *zIntro; /* Text introducing pattern description */
1298
+ const char *zPattern = 0; /* Previous quoted pattern */
1299
+ const char *zFail = 0; /* Current failure message or NULL if okay */
1300
+ const char *zOr = " or "; /* Text before final quoted pattern */
1301
+ char cDel; /* Input delimiter character */
1302
+ int i; /* Input match pattern length counter */
1303
+
1304
+ /* Optimize exact matches by looking up the ID in advance to create a simple
1305
+ * numeric comparison. Bypass the remainder of this function. */
1306
+ if( matchStyle==MS_EXACT ){
1307
+ *zDesc = tagQuote(-1, zTag);
1308
+ return mprintf("(tagid=%d)", db_int(-1,
1309
+ "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag));
1310
+ }
1311
+
1312
+ /* Decide pattern prefix and suffix strings according to match style. */
1313
+ if( matchStyle==MS_GLOB ){
1314
+ zStart = "(";
1315
+ zDelimiter = " OR ";
1316
+ zEnd = ")";
1317
+ zPrefix = "tagname GLOB 'sym-";
1318
+ zSuffix = "'";
1319
+ zIntro = "glob pattern ";
1320
+ }else if( matchStyle==MS_LIKE ){
1321
+ zStart = "(";
1322
+ zDelimiter = " OR ";
1323
+ zEnd = ")";
1324
+ zPrefix = "tagname LIKE 'sym-";
1325
+ zSuffix = "'";
1326
+ zIntro = "SQL LIKE pattern ";
1327
+ }else/* if( matchStyle==MS_REGEXP )*/{
1328
+ zStart = "(tagname REGEXP '^sym-(";
1329
+ zDelimiter = "|";
1330
+ zEnd = ")$')";
1331
+ zPrefix = "";
1332
+ zSuffix = "";
1333
+ zIntro = "regular expression ";
1334
+ }
1335
+
1336
+ /* Convert the list of matches into an SQL expression and text description. */
1337
+ blob_zero(&expr);
1338
+ blob_zero(&desc);
1339
+ blob_zero(&err);
1340
+ while( 1 ){
1341
+ /* Skip leading delimiters. */
1342
+ for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag );
1343
+
1344
+ /* Next non-delimiter character determines quoting. */
1345
+ if( !*zTag ){
1346
+ /* Terminate loop at end of string. */
1347
+ break;
1348
+ }else if( *zTag=='\'' || *zTag=='"' ){
1349
+ /* If word is quoted, prepare to stop at end quote. */
1350
+ cDel = *zTag;
1351
+ ++zTag;
1352
+ }else{
1353
+ /* If word is not quoted, prepare to stop at delimiter. */
1354
+ cDel = ',';
1355
+ }
1356
+
1357
+ /* Find the next delimiter character or end of string. */
1358
+ for( i=0; zTag[i] && zTag[i]!=cDel; ++i ){
1359
+ /* If delimiter is comma, also recognize spaces as delimiters. */
1360
+ if( cDel==',' && fossil_isspace(zTag[i]) ){
1361
+ break;
1362
+ }
1363
+
1364
+ /* In regexp mode, ignore delimiters following backslashes. */
1365
+ if( matchStyle==MS_REGEXP && zTag[i]=='\\' && zTag[i+1] ){
1366
+ ++i;
1367
+ }
1368
+ }
1369
+
1370
+ /* Check for regular expression syntax errors. */
1371
+ if( matchStyle==MS_REGEXP ){
1372
+ ReCompiled *regexp;
1373
+ char *zTagDup = fossil_strndup(zTag, i);
1374
+ zFail = re_compile(&regexp, zTagDup, 0);
1375
+ re_free(regexp);
1376
+ fossil_free(zTagDup);
1377
+ }
1378
+
1379
+ /* Process success and error results. */
1380
+ if( !zFail ){
1381
+ /* Incorporate the match word into the output expression. %q is used to
1382
+ * protect against SQL injection attacks by replacing ' with ''. */
1383
+ blob_appendf(&expr, "%s%s%#q%s", blob_size(&expr) ? zDelimiter : zStart,
1384
+ zPrefix, i, zTag, zSuffix);
1385
+
1386
+ /* Build up the description string. */
1387
+ if( !blob_size(&desc) ){
1388
+ /* First tag: start with intro followed by first quoted tag. */
1389
+ blob_append(&desc, zIntro, -1);
1390
+ blob_append(&desc, tagQuote(i, zTag), -1);
1391
+ }else{
1392
+ if( zPattern ){
1393
+ /* Third and subsequent tags: append comma then previous tag. */
1394
+ blob_append(&desc, ", ", 2);
1395
+ blob_append(&desc, zPattern, -1);
1396
+ zOr = ", or ";
1397
+ }
1398
+
1399
+ /* Second and subsequent tags: store quoted tag for next iteration. */
1400
+ zPattern = tagQuote(i, zTag);
1401
+ }
1402
+ }else{
1403
+ /* On error, skip the match word and build up the error message buffer. */
1404
+ if( !blob_size(&err) ){
1405
+ blob_append(&err, "Error: ", 7);
1406
+ }else{
1407
+ blob_append(&err, ", ", 2);
1408
+ }
1409
+ blob_appendf(&err, "(%s%s: %s)", zIntro, tagQuote(i, zTag), zFail);
1410
+ }
1411
+
1412
+ /* Advance past all consumed input characters. */
1413
+ zTag += i;
1414
+ if( cDel!=',' && *zTag==cDel ){
1415
+ ++zTag;
1416
+ }
1417
+ }
1418
+
1419
+ /* Finalize and extract the pattern description. */
1420
+ if( zPattern ){
1421
+ blob_append(&desc, zOr, -1);
1422
+ blob_append(&desc, zPattern, -1);
1423
+ }
1424
+ *zDesc = blob_str(&desc);
1425
+
1426
+ /* Finalize and extract the error text. */
1427
+ *zError = blob_size(&err) ? blob_str(&err) : 0;
1428
+
1429
+ /* Finalize and extract the SQL expression. */
1430
+ if( blob_size(&expr) ){
1431
+ blob_append(&expr, zEnd, -1);
1432
+ return blob_str(&expr);
1433
+ }
1434
+
1435
+ /* If execution reaches this point, the pattern was empty. Return NULL. */
1436
+ return 0;
1437
+}
12231438
12241439
/*
12251440
** WEBPAGE: timeline
12261441
**
12271442
** Query parameters:
@@ -1233,12 +1448,14 @@
12331448
** n=COUNT Suggested number of events in output
12341449
** p=CHECKIN Parents and ancestors of CHECKIN
12351450
** d=CHECKIN Descendants of CHECIN
12361451
** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
12371452
** t=TAG Show only check-ins with the given TAG
1238
-** r=TAG Show check-ins related to TAG
1239
-** mionly Limit r=TAG to show ancestors but not descendants
1453
+** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1454
+** rel Show related check-ins as well as those matching t=TAG
1455
+** mionly Limit rel to show ancestors but not descendants
1456
+** ms=STYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
12401457
** u=USER Only show items associated with USER
12411458
** y=TYPE 'ci', 'w', 't', 'e', or (default) 'all'
12421459
** ng No Graph.
12431460
** nd Do not highlight the focus check-in
12441461
** v Show details of files changed
@@ -1279,11 +1496,17 @@
12791496
const char *zAfter = P("a"); /* Events after this time */
12801497
const char *zBefore = P("b"); /* Events before this time */
12811498
const char *zCirca = P("c"); /* Events near this time */
12821499
const char *zMark = P("m"); /* Mark this event or an event this time */
12831500
const char *zTagName = P("t"); /* Show events with this tag */
1284
- const char *zBrName = P("r"); /* Show events related to this tag */
1501
+ const char *zBrName = P("r"); /* Equivalent to t=TAG&rel */
1502
+ int related = PB("rel"); /* Show events related to zTagName */
1503
+ const char *zMatchStyle = P("ms"); /* Tag/branch match style string */
1504
+ MatchStyle matchStyle = MS_EXACT; /* Match style code */
1505
+ const char *zMatchDesc = 0; /* Tag match expression description text */
1506
+ const char *zError = 0; /* Tag match error string */
1507
+ const char *zTagSql = 0; /* Tag/branch match SQL expression */
12851508
const char *zSearch = P("s"); /* Search string */
12861509
const char *zUses = P("uf"); /* Only show check-ins hold this file */
12871510
const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */
12881511
const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */
12891512
const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */
@@ -1290,11 +1513,10 @@
12901513
const char *zChng = P("chng"); /* List of GLOBs for files that changed */
12911514
int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
12921515
int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
12931516
int forkOnly = PB("forks"); /* Show only forks and their children */
12941517
int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
1295
- int tagid; /* Tag ID */
12961518
int tmFlags = 0; /* Timeline flags */
12971519
const char *zThisTag = 0; /* Suppress links to this tag */
12981520
const char *zThisUser = 0; /* Suppress links to this user */
12991521
HQuery url; /* URL for various branch links */
13001522
int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1342,28 +1564,53 @@
13421564
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
13431565
return;
13441566
}
13451567
url_initialize(&url, "timeline");
13461568
cgi_query_parameters_to_url(&url);
1347
- if( zTagName && g.perm.Read ){
1348
- tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zTagName);
1349
- zThisTag = zTagName;
1350
- timeline_submenu(&url, "Related", "r", zTagName, "t");
1351
- }else if( zBrName && g.perm.Read ){
1352
- tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zBrName);
1353
- zThisTag = zBrName;
1354
- timeline_submenu(&url, "Branch Only", "t", zBrName, "r");
1355
- }else{
1356
- tagid = 0;
1357
- }
1569
+
1570
+ /* Convert r=TAG to t=TAG&rel. */
1571
+ if( zBrName && !related ){
1572
+ cgi_delete_query_parameter("r");
1573
+ cgi_set_query_parameter("t", zBrName);
1574
+ cgi_set_query_parameter("rel", "1");
1575
+ zTagName = zBrName;
1576
+ related = 1;
1577
+ }
1578
+
1579
+ /* Ignore empty tag query strings. */
1580
+ if( zTagName && !*zTagName ){
1581
+ zTagName = 0;
1582
+ }
1583
+
1584
+ /* Finish preliminary processing of tag match queries. */
1585
+ if( zTagName ){
1586
+ /* Interpet the tag style string. */
1587
+ if( fossil_stricmp(zMatchStyle, "glob")==0 ){
1588
+ matchStyle = MS_GLOB;
1589
+ }else if( fossil_stricmp(zMatchStyle, "like")==0 ){
1590
+ matchStyle = MS_LIKE;
1591
+ }else if( fossil_stricmp(zMatchStyle, "regexp")==0 ){
1592
+ matchStyle = MS_REGEXP;
1593
+ }else{
1594
+ /* For exact maching, inhibit links to the selected tag. */
1595
+ zThisTag = zTagName;
1596
+ }
1597
+
1598
+ /* Display a checkbox to enable/disable display of related check-ins. */
1599
+ style_submenu_checkbox("rel", "Related", 0);
1600
+
1601
+ /* Construct the tag match expression. */
1602
+ zTagSql = tagMatchExpression(matchStyle, zTagName, &zMatchDesc, &zError);
1603
+ }
1604
+
13581605
if( zMark && zMark[0]==0 ){
13591606
if( zAfter ) zMark = zAfter;
13601607
if( zBefore ) zMark = zBefore;
13611608
if( zCirca ) zMark = zCirca;
13621609
}
1363
- if( tagid
1364
- && db_int(0,"SELECT count(*) FROM tagxref WHERE tagid=%d",tagid)<=nEntry
1610
+ if( (zTagSql && db_int(0,"SELECT count(*) "
1611
+ "FROM tagxref NATURAL JOIN tag WHERE %s",zTagSql/*safe-for-%s*/)<=nEntry)
13651612
){
13661613
nEntry = -1;
13671614
zCirca = 0;
13681615
}
13691616
if( zType[0]=='a' ){
@@ -1583,26 +1830,26 @@
15831830
}
15841831
else if( zDay ){
15851832
blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m-%%d',event.mtime) ",
15861833
zDay);
15871834
}
1588
- if( tagid ){
1835
+ if( zTagSql ){
15891836
blob_append_sql(&cond,
1590
- " AND (EXISTS(SELECT 1 FROM tagxref"
1591
- " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", tagid);
1837
+ " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag"
1838
+ " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/);
15921839
1593
- if( zBrName ){
1840
+ if( related ){
15941841
/* The next two blob_appendf() calls add SQL that causes check-ins that
15951842
** are not part of the branch which are parents or children of the
15961843
** branch to be included in the report. This related check-ins are
15971844
** useful in helping to visualize what has happened on a quiescent
15981845
** branch that is infrequently merged with a much more activate branch.
15991846
*/
16001847
blob_append_sql(&cond,
16011848
" OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid"
1602
- " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
1603
- tagid
1849
+ " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n",
1850
+ zTagSql/*safe-for-%s*/
16041851
);
16051852
if( (tmFlags & TIMELINE_UNHIDE)==0 ){
16061853
blob_append_sql(&cond,
16071854
" AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
16081855
" WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
@@ -1610,12 +1857,12 @@
16101857
);
16111858
}
16121859
if( P("mionly")==0 ){
16131860
blob_append_sql(&cond,
16141861
" OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid"
1615
- " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
1616
- tagid
1862
+ " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n",
1863
+ zTagSql/*safe-for-%s*/
16171864
);
16181865
if( (tmFlags & TIMELINE_UNHIDE)==0 ){
16191866
blob_append_sql(&cond,
16201867
" AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
16211868
" WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
@@ -1762,15 +2009,24 @@
17622009
}
17632010
if( zUser ){
17642011
blob_appendf(&desc, " by user %h", zUser);
17652012
tmFlags |= TIMELINE_DISJOINT;
17662013
}
1767
- if( zTagName ){
1768
- blob_appendf(&desc, " tagged with \"%h\"", zTagName);
1769
- tmFlags |= TIMELINE_DISJOINT;
1770
- }else if( zBrName ){
1771
- blob_appendf(&desc, " related to \"%h\"", zBrName);
2014
+ if( zTagSql ){
2015
+ if( matchStyle==MS_EXACT ){
2016
+ if( related ){
2017
+ blob_appendf(&desc, " related to %h", zMatchDesc);
2018
+ }else{
2019
+ blob_appendf(&desc, " tagged with %h", zMatchDesc);
2020
+ }
2021
+ }else{
2022
+ if( related ){
2023
+ blob_appendf(&desc, " related to tags matching %h", zMatchDesc);
2024
+ }else{
2025
+ blob_appendf(&desc, " with tags matching %h", zMatchDesc);
2026
+ }
2027
+ }
17722028
tmFlags |= TIMELINE_DISJOINT;
17732029
}
17742030
addFileGlobDescription(zChng, &desc);
17752031
if( rAfter>0.0 ){
17762032
if( rBefore>0.0 ){
@@ -1786,10 +2042,13 @@
17862042
}
17872043
if( zSearch ){
17882044
blob_appendf(&desc, " matching \"%h\"", zSearch);
17892045
}
17902046
if( g.perm.Hyperlink ){
2047
+ static const char *const azMatchStyles[] = {
2048
+ "exact", "Exact", "glob", "Glob", "like", "Like", "regexp", "Regexp"
2049
+ };
17912050
double rDate;
17922051
zDate = db_text(0, "SELECT min(timestamp) FROM timeline /*scan*/");
17932052
if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){
17942053
zDate = mprintf("%s", (zAfter ? zAfter : zBefore));
17952054
}
@@ -1824,10 +2083,12 @@
18242083
style_submenu_checkbox("unhide", "Unhide", 0);
18252084
}
18262085
style_submenu_checkbox("v", "Files", zType[0]!='a' && zType[0]!='c');
18272086
style_submenu_entry("n","Max:",4,0);
18282087
timeline_y_submenu(disableY);
2088
+ style_submenu_entry("t", "Tag Filter:", -8, 0);
2089
+ style_submenu_multichoice("ms", count(azMatchStyles)/2, azMatchStyles, 0);
18292090
}
18302091
blob_zero(&cond);
18312092
}
18322093
if( PB("showsql") ){
18332094
@ <pre>%h(blob_sql_text(&sql))</pre>
@@ -1842,10 +2103,16 @@
18422103
}
18432104
blob_zero(&sql);
18442105
db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
18452106
@ <h2>%b(&desc)</h2>
18462107
blob_reset(&desc);
2108
+
2109
+ /* Report any errors. */
2110
+ if( zError ){
2111
+ @ <p class="generalError">%h(zError)</p>
2112
+ }
2113
+
18472114
www_print_timeline(&q, tmFlags, zThisUser, zThisTag, selectedRid, 0);
18482115
db_finalize(&q);
18492116
if( zOlderButton ){
18502117
@ %z(xhref("class='button'","%z",zOlderButton))Older</a>
18512118
}
18522119
--- src/timeline.c
+++ src/timeline.c
@@ -281,11 +281,11 @@
281 char zTime[20];
282
283 if( zDate==0 ){
284 zDate = "YYYY-MM-DD HH:MM:SS"; /* Something wrong with the repo */
285 }
286 modPending = moderation_pending(rid);
287 if( tagid ){
288 if( modPending ) tagid = -tagid;
289 if( tagid==prevTagid ){
290 if( tmFlags & TIMELINE_BRIEF ){
291 suppressCnt++;
@@ -573,11 +573,11 @@
573 }
574 continue;
575 }
576 zA = href("%R/artifact/%!S",fid?zNew:zOld);
577 if( content_is_private(fid) ){
578 zUnpub = UNPUB_TAG;
579 }
580 if( isNew ){
581 @ <li> %s(zA)%h(zFilename)</a>%s(zId) %s(zUnpub)
582 if( isMergeNew ){
583 @ (added by merge)
@@ -1218,10 +1218,225 @@
1218 if( zChng==0 || zChng[0]==0 ) return;
1219 blob_appendf(pDescription, " that include changes to files matching %Q",
1220 zChng);
1221 }
1222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1223
1224 /*
1225 ** WEBPAGE: timeline
1226 **
1227 ** Query parameters:
@@ -1233,12 +1448,14 @@
1233 ** n=COUNT Suggested number of events in output
1234 ** p=CHECKIN Parents and ancestors of CHECKIN
1235 ** d=CHECKIN Descendants of CHECIN
1236 ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
1237 ** t=TAG Show only check-ins with the given TAG
1238 ** r=TAG Show check-ins related to TAG
1239 ** mionly Limit r=TAG to show ancestors but not descendants
 
 
1240 ** u=USER Only show items associated with USER
1241 ** y=TYPE 'ci', 'w', 't', 'e', or (default) 'all'
1242 ** ng No Graph.
1243 ** nd Do not highlight the focus check-in
1244 ** v Show details of files changed
@@ -1279,11 +1496,17 @@
1279 const char *zAfter = P("a"); /* Events after this time */
1280 const char *zBefore = P("b"); /* Events before this time */
1281 const char *zCirca = P("c"); /* Events near this time */
1282 const char *zMark = P("m"); /* Mark this event or an event this time */
1283 const char *zTagName = P("t"); /* Show events with this tag */
1284 const char *zBrName = P("r"); /* Show events related to this tag */
 
 
 
 
 
 
1285 const char *zSearch = P("s"); /* Search string */
1286 const char *zUses = P("uf"); /* Only show check-ins hold this file */
1287 const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */
1288 const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */
1289 const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */
@@ -1290,11 +1513,10 @@
1290 const char *zChng = P("chng"); /* List of GLOBs for files that changed */
1291 int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
1292 int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
1293 int forkOnly = PB("forks"); /* Show only forks and their children */
1294 int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
1295 int tagid; /* Tag ID */
1296 int tmFlags = 0; /* Timeline flags */
1297 const char *zThisTag = 0; /* Suppress links to this tag */
1298 const char *zThisUser = 0; /* Suppress links to this user */
1299 HQuery url; /* URL for various branch links */
1300 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1342,28 +1564,53 @@
1342 login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
1343 return;
1344 }
1345 url_initialize(&url, "timeline");
1346 cgi_query_parameters_to_url(&url);
1347 if( zTagName && g.perm.Read ){
1348 tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zTagName);
1349 zThisTag = zTagName;
1350 timeline_submenu(&url, "Related", "r", zTagName, "t");
1351 }else if( zBrName && g.perm.Read ){
1352 tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zBrName);
1353 zThisTag = zBrName;
1354 timeline_submenu(&url, "Branch Only", "t", zBrName, "r");
1355 }else{
1356 tagid = 0;
1357 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1358 if( zMark && zMark[0]==0 ){
1359 if( zAfter ) zMark = zAfter;
1360 if( zBefore ) zMark = zBefore;
1361 if( zCirca ) zMark = zCirca;
1362 }
1363 if( tagid
1364 && db_int(0,"SELECT count(*) FROM tagxref WHERE tagid=%d",tagid)<=nEntry
1365 ){
1366 nEntry = -1;
1367 zCirca = 0;
1368 }
1369 if( zType[0]=='a' ){
@@ -1583,26 +1830,26 @@
1583 }
1584 else if( zDay ){
1585 blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m-%%d',event.mtime) ",
1586 zDay);
1587 }
1588 if( tagid ){
1589 blob_append_sql(&cond,
1590 " AND (EXISTS(SELECT 1 FROM tagxref"
1591 " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", tagid);
1592
1593 if( zBrName ){
1594 /* The next two blob_appendf() calls add SQL that causes check-ins that
1595 ** are not part of the branch which are parents or children of the
1596 ** branch to be included in the report. This related check-ins are
1597 ** useful in helping to visualize what has happened on a quiescent
1598 ** branch that is infrequently merged with a much more activate branch.
1599 */
1600 blob_append_sql(&cond,
1601 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid"
1602 " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
1603 tagid
1604 );
1605 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1606 blob_append_sql(&cond,
1607 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
1608 " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
@@ -1610,12 +1857,12 @@
1610 );
1611 }
1612 if( P("mionly")==0 ){
1613 blob_append_sql(&cond,
1614 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid"
1615 " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
1616 tagid
1617 );
1618 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1619 blob_append_sql(&cond,
1620 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
1621 " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
@@ -1762,15 +2009,24 @@
1762 }
1763 if( zUser ){
1764 blob_appendf(&desc, " by user %h", zUser);
1765 tmFlags |= TIMELINE_DISJOINT;
1766 }
1767 if( zTagName ){
1768 blob_appendf(&desc, " tagged with \"%h\"", zTagName);
1769 tmFlags |= TIMELINE_DISJOINT;
1770 }else if( zBrName ){
1771 blob_appendf(&desc, " related to \"%h\"", zBrName);
 
 
 
 
 
 
 
 
 
1772 tmFlags |= TIMELINE_DISJOINT;
1773 }
1774 addFileGlobDescription(zChng, &desc);
1775 if( rAfter>0.0 ){
1776 if( rBefore>0.0 ){
@@ -1786,10 +2042,13 @@
1786 }
1787 if( zSearch ){
1788 blob_appendf(&desc, " matching \"%h\"", zSearch);
1789 }
1790 if( g.perm.Hyperlink ){
 
 
 
1791 double rDate;
1792 zDate = db_text(0, "SELECT min(timestamp) FROM timeline /*scan*/");
1793 if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){
1794 zDate = mprintf("%s", (zAfter ? zAfter : zBefore));
1795 }
@@ -1824,10 +2083,12 @@
1824 style_submenu_checkbox("unhide", "Unhide", 0);
1825 }
1826 style_submenu_checkbox("v", "Files", zType[0]!='a' && zType[0]!='c');
1827 style_submenu_entry("n","Max:",4,0);
1828 timeline_y_submenu(disableY);
 
 
1829 }
1830 blob_zero(&cond);
1831 }
1832 if( PB("showsql") ){
1833 @ <pre>%h(blob_sql_text(&sql))</pre>
@@ -1842,10 +2103,16 @@
1842 }
1843 blob_zero(&sql);
1844 db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
1845 @ <h2>%b(&desc)</h2>
1846 blob_reset(&desc);
 
 
 
 
 
 
1847 www_print_timeline(&q, tmFlags, zThisUser, zThisTag, selectedRid, 0);
1848 db_finalize(&q);
1849 if( zOlderButton ){
1850 @ %z(xhref("class='button'","%z",zOlderButton))Older</a>
1851 }
1852
--- src/timeline.c
+++ src/timeline.c
@@ -281,11 +281,11 @@
281 char zTime[20];
282
283 if( zDate==0 ){
284 zDate = "YYYY-MM-DD HH:MM:SS"; /* Something wrong with the repo */
285 }
286 modPending = moderation_pending(rid);
287 if( tagid ){
288 if( modPending ) tagid = -tagid;
289 if( tagid==prevTagid ){
290 if( tmFlags & TIMELINE_BRIEF ){
291 suppressCnt++;
@@ -573,11 +573,11 @@
573 }
574 continue;
575 }
576 zA = href("%R/artifact/%!S",fid?zNew:zOld);
577 if( content_is_private(fid) ){
578 zUnpub = UNPUB_TAG;
579 }
580 if( isNew ){
581 @ <li> %s(zA)%h(zFilename)</a>%s(zId) %s(zUnpub)
582 if( isMergeNew ){
583 @ (added by merge)
@@ -1218,10 +1218,225 @@
1218 if( zChng==0 || zChng[0]==0 ) return;
1219 blob_appendf(pDescription, " that include changes to files matching %Q",
1220 zChng);
1221 }
1222
1223 /*
1224 ** Tag match expression type code.
1225 */
1226 typedef enum {
1227 MS_EXACT, /* Matches a single tag by exact string comparison. */
1228 MS_GLOB, /* Matches tags against a list of GLOB patterns. */
1229 MS_LIKE, /* Matches tags against a list of LIKE patterns. */
1230 MS_REGEXP /* Matches tags against a list of regular expressions. */
1231 } MatchStyle;
1232
1233 /*
1234 ** Quote a tag string by surrounding it with double quotes and preceding
1235 ** internal double quotes and backslashes with backslashes.
1236 */
1237 static const char *tagQuote(
1238 int len, /* Maximum length of zTag, or negative for unlimited */
1239 const char *zTag /* Tag string */
1240 ){
1241 Blob blob = BLOB_INITIALIZER;
1242 int i, j;
1243 blob_zero(&blob);
1244 blob_append(&blob, "\"", 1);
1245 for( i=j=0; zTag[j] && (len<0 || j<len); ++j ){
1246 if( zTag[j]=='\"' || zTag[j]=='\\' ){
1247 if( j>i ){
1248 blob_append(&blob, zTag+i, j-i);
1249 }
1250 blob_append(&blob, "\\", 1);
1251 i = j;
1252 }
1253 }
1254 if( j>i ){
1255 blob_append(&blob, zTag+i, j-i);
1256 }
1257 blob_append(&blob, "\"", 1);
1258 return blob_str(&blob);
1259 }
1260
1261 /*
1262 ** Construct the tag match SQL expression.
1263 **
1264 ** This function is adapted from glob_expr() to support the MS_EXACT, MS_GLOB,
1265 ** MS_LIKE, and MS_REGEXP match styles. For MS_EXACT, the returned expression
1266 ** checks for integer match against the tag ID which is looked up directly by
1267 ** this function. For the other modes, the returned SQL expression performs
1268 ** string comparisons against the tag names, so it is necessary to join against
1269 ** the tag table to access the "tagname" column.
1270 **
1271 ** Each pattern is adjusted to to start with "sym-" and be anchored at end.
1272 **
1273 ** In MS_REGEXP mode, backslash can be used to protect delimiter characters.
1274 ** The backslashes are not removed from the regular expression.
1275 **
1276 ** In addition to assembling and returning an SQL expression, this function
1277 ** makes an English-language description of the patterns being matched, suitable
1278 ** for display in the web interface.
1279 **
1280 ** If any errors arise during processing, *zError is set to an error message.
1281 ** Otherwise it is set to NULL.
1282 */
1283 static const char *tagMatchExpression(
1284 MatchStyle matchStyle, /* Match style code */
1285 const char *zTag, /* Tag name, match pattern, or pattern list */
1286 const char **zDesc, /* Output expression description string */
1287 const char **zError /* Output error string */
1288 ){
1289 Blob expr = BLOB_INITIALIZER; /* SQL expression string assembly buffer */
1290 Blob desc = BLOB_INITIALIZER; /* English description of match patterns */
1291 Blob err = BLOB_INITIALIZER; /* Error text assembly buffer */
1292 const char *zStart; /* Text at start of expression */
1293 const char *zDelimiter; /* Text between expression terms */
1294 const char *zEnd; /* Text at end of expression */
1295 const char *zPrefix; /* Text before each match pattern */
1296 const char *zSuffix; /* Text after each match pattern */
1297 const char *zIntro; /* Text introducing pattern description */
1298 const char *zPattern = 0; /* Previous quoted pattern */
1299 const char *zFail = 0; /* Current failure message or NULL if okay */
1300 const char *zOr = " or "; /* Text before final quoted pattern */
1301 char cDel; /* Input delimiter character */
1302 int i; /* Input match pattern length counter */
1303
1304 /* Optimize exact matches by looking up the ID in advance to create a simple
1305 * numeric comparison. Bypass the remainder of this function. */
1306 if( matchStyle==MS_EXACT ){
1307 *zDesc = tagQuote(-1, zTag);
1308 return mprintf("(tagid=%d)", db_int(-1,
1309 "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag));
1310 }
1311
1312 /* Decide pattern prefix and suffix strings according to match style. */
1313 if( matchStyle==MS_GLOB ){
1314 zStart = "(";
1315 zDelimiter = " OR ";
1316 zEnd = ")";
1317 zPrefix = "tagname GLOB 'sym-";
1318 zSuffix = "'";
1319 zIntro = "glob pattern ";
1320 }else if( matchStyle==MS_LIKE ){
1321 zStart = "(";
1322 zDelimiter = " OR ";
1323 zEnd = ")";
1324 zPrefix = "tagname LIKE 'sym-";
1325 zSuffix = "'";
1326 zIntro = "SQL LIKE pattern ";
1327 }else/* if( matchStyle==MS_REGEXP )*/{
1328 zStart = "(tagname REGEXP '^sym-(";
1329 zDelimiter = "|";
1330 zEnd = ")$')";
1331 zPrefix = "";
1332 zSuffix = "";
1333 zIntro = "regular expression ";
1334 }
1335
1336 /* Convert the list of matches into an SQL expression and text description. */
1337 blob_zero(&expr);
1338 blob_zero(&desc);
1339 blob_zero(&err);
1340 while( 1 ){
1341 /* Skip leading delimiters. */
1342 for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag );
1343
1344 /* Next non-delimiter character determines quoting. */
1345 if( !*zTag ){
1346 /* Terminate loop at end of string. */
1347 break;
1348 }else if( *zTag=='\'' || *zTag=='"' ){
1349 /* If word is quoted, prepare to stop at end quote. */
1350 cDel = *zTag;
1351 ++zTag;
1352 }else{
1353 /* If word is not quoted, prepare to stop at delimiter. */
1354 cDel = ',';
1355 }
1356
1357 /* Find the next delimiter character or end of string. */
1358 for( i=0; zTag[i] && zTag[i]!=cDel; ++i ){
1359 /* If delimiter is comma, also recognize spaces as delimiters. */
1360 if( cDel==',' && fossil_isspace(zTag[i]) ){
1361 break;
1362 }
1363
1364 /* In regexp mode, ignore delimiters following backslashes. */
1365 if( matchStyle==MS_REGEXP && zTag[i]=='\\' && zTag[i+1] ){
1366 ++i;
1367 }
1368 }
1369
1370 /* Check for regular expression syntax errors. */
1371 if( matchStyle==MS_REGEXP ){
1372 ReCompiled *regexp;
1373 char *zTagDup = fossil_strndup(zTag, i);
1374 zFail = re_compile(&regexp, zTagDup, 0);
1375 re_free(regexp);
1376 fossil_free(zTagDup);
1377 }
1378
1379 /* Process success and error results. */
1380 if( !zFail ){
1381 /* Incorporate the match word into the output expression. %q is used to
1382 * protect against SQL injection attacks by replacing ' with ''. */
1383 blob_appendf(&expr, "%s%s%#q%s", blob_size(&expr) ? zDelimiter : zStart,
1384 zPrefix, i, zTag, zSuffix);
1385
1386 /* Build up the description string. */
1387 if( !blob_size(&desc) ){
1388 /* First tag: start with intro followed by first quoted tag. */
1389 blob_append(&desc, zIntro, -1);
1390 blob_append(&desc, tagQuote(i, zTag), -1);
1391 }else{
1392 if( zPattern ){
1393 /* Third and subsequent tags: append comma then previous tag. */
1394 blob_append(&desc, ", ", 2);
1395 blob_append(&desc, zPattern, -1);
1396 zOr = ", or ";
1397 }
1398
1399 /* Second and subsequent tags: store quoted tag for next iteration. */
1400 zPattern = tagQuote(i, zTag);
1401 }
1402 }else{
1403 /* On error, skip the match word and build up the error message buffer. */
1404 if( !blob_size(&err) ){
1405 blob_append(&err, "Error: ", 7);
1406 }else{
1407 blob_append(&err, ", ", 2);
1408 }
1409 blob_appendf(&err, "(%s%s: %s)", zIntro, tagQuote(i, zTag), zFail);
1410 }
1411
1412 /* Advance past all consumed input characters. */
1413 zTag += i;
1414 if( cDel!=',' && *zTag==cDel ){
1415 ++zTag;
1416 }
1417 }
1418
1419 /* Finalize and extract the pattern description. */
1420 if( zPattern ){
1421 blob_append(&desc, zOr, -1);
1422 blob_append(&desc, zPattern, -1);
1423 }
1424 *zDesc = blob_str(&desc);
1425
1426 /* Finalize and extract the error text. */
1427 *zError = blob_size(&err) ? blob_str(&err) : 0;
1428
1429 /* Finalize and extract the SQL expression. */
1430 if( blob_size(&expr) ){
1431 blob_append(&expr, zEnd, -1);
1432 return blob_str(&expr);
1433 }
1434
1435 /* If execution reaches this point, the pattern was empty. Return NULL. */
1436 return 0;
1437 }
1438
1439 /*
1440 ** WEBPAGE: timeline
1441 **
1442 ** Query parameters:
@@ -1233,12 +1448,14 @@
1448 ** n=COUNT Suggested number of events in output
1449 ** p=CHECKIN Parents and ancestors of CHECKIN
1450 ** d=CHECKIN Descendants of CHECIN
1451 ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN
1452 ** t=TAG Show only check-ins with the given TAG
1453 ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
1454 ** rel Show related check-ins as well as those matching t=TAG
1455 ** mionly Limit rel to show ancestors but not descendants
1456 ** ms=STYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
1457 ** u=USER Only show items associated with USER
1458 ** y=TYPE 'ci', 'w', 't', 'e', or (default) 'all'
1459 ** ng No Graph.
1460 ** nd Do not highlight the focus check-in
1461 ** v Show details of files changed
@@ -1279,11 +1496,17 @@
1496 const char *zAfter = P("a"); /* Events after this time */
1497 const char *zBefore = P("b"); /* Events before this time */
1498 const char *zCirca = P("c"); /* Events near this time */
1499 const char *zMark = P("m"); /* Mark this event or an event this time */
1500 const char *zTagName = P("t"); /* Show events with this tag */
1501 const char *zBrName = P("r"); /* Equivalent to t=TAG&rel */
1502 int related = PB("rel"); /* Show events related to zTagName */
1503 const char *zMatchStyle = P("ms"); /* Tag/branch match style string */
1504 MatchStyle matchStyle = MS_EXACT; /* Match style code */
1505 const char *zMatchDesc = 0; /* Tag match expression description text */
1506 const char *zError = 0; /* Tag match error string */
1507 const char *zTagSql = 0; /* Tag/branch match SQL expression */
1508 const char *zSearch = P("s"); /* Search string */
1509 const char *zUses = P("uf"); /* Only show check-ins hold this file */
1510 const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */
1511 const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */
1512 const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */
@@ -1290,11 +1513,10 @@
1513 const char *zChng = P("chng"); /* List of GLOBs for files that changed */
1514 int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */
1515 int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */
1516 int forkOnly = PB("forks"); /* Show only forks and their children */
1517 int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */
 
1518 int tmFlags = 0; /* Timeline flags */
1519 const char *zThisTag = 0; /* Suppress links to this tag */
1520 const char *zThisUser = 0; /* Suppress links to this user */
1521 HQuery url; /* URL for various branch links */
1522 int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */
@@ -1342,28 +1564,53 @@
1564 login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
1565 return;
1566 }
1567 url_initialize(&url, "timeline");
1568 cgi_query_parameters_to_url(&url);
1569
1570 /* Convert r=TAG to t=TAG&rel. */
1571 if( zBrName && !related ){
1572 cgi_delete_query_parameter("r");
1573 cgi_set_query_parameter("t", zBrName);
1574 cgi_set_query_parameter("rel", "1");
1575 zTagName = zBrName;
1576 related = 1;
1577 }
1578
1579 /* Ignore empty tag query strings. */
1580 if( zTagName && !*zTagName ){
1581 zTagName = 0;
1582 }
1583
1584 /* Finish preliminary processing of tag match queries. */
1585 if( zTagName ){
1586 /* Interpet the tag style string. */
1587 if( fossil_stricmp(zMatchStyle, "glob")==0 ){
1588 matchStyle = MS_GLOB;
1589 }else if( fossil_stricmp(zMatchStyle, "like")==0 ){
1590 matchStyle = MS_LIKE;
1591 }else if( fossil_stricmp(zMatchStyle, "regexp")==0 ){
1592 matchStyle = MS_REGEXP;
1593 }else{
1594 /* For exact maching, inhibit links to the selected tag. */
1595 zThisTag = zTagName;
1596 }
1597
1598 /* Display a checkbox to enable/disable display of related check-ins. */
1599 style_submenu_checkbox("rel", "Related", 0);
1600
1601 /* Construct the tag match expression. */
1602 zTagSql = tagMatchExpression(matchStyle, zTagName, &zMatchDesc, &zError);
1603 }
1604
1605 if( zMark && zMark[0]==0 ){
1606 if( zAfter ) zMark = zAfter;
1607 if( zBefore ) zMark = zBefore;
1608 if( zCirca ) zMark = zCirca;
1609 }
1610 if( (zTagSql && db_int(0,"SELECT count(*) "
1611 "FROM tagxref NATURAL JOIN tag WHERE %s",zTagSql/*safe-for-%s*/)<=nEntry)
1612 ){
1613 nEntry = -1;
1614 zCirca = 0;
1615 }
1616 if( zType[0]=='a' ){
@@ -1583,26 +1830,26 @@
1830 }
1831 else if( zDay ){
1832 blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m-%%d',event.mtime) ",
1833 zDay);
1834 }
1835 if( zTagSql ){
1836 blob_append_sql(&cond,
1837 " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag"
1838 " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/);
1839
1840 if( related ){
1841 /* The next two blob_appendf() calls add SQL that causes check-ins that
1842 ** are not part of the branch which are parents or children of the
1843 ** branch to be included in the report. This related check-ins are
1844 ** useful in helping to visualize what has happened on a quiescent
1845 ** branch that is infrequently merged with a much more activate branch.
1846 */
1847 blob_append_sql(&cond,
1848 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid"
1849 " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n",
1850 zTagSql/*safe-for-%s*/
1851 );
1852 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1853 blob_append_sql(&cond,
1854 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid"
1855 " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n",
@@ -1610,12 +1857,12 @@
1857 );
1858 }
1859 if( P("mionly")==0 ){
1860 blob_append_sql(&cond,
1861 " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid"
1862 " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n",
1863 zTagSql/*safe-for-%s*/
1864 );
1865 if( (tmFlags & TIMELINE_UNHIDE)==0 ){
1866 blob_append_sql(&cond,
1867 " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid"
1868 " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n",
@@ -1762,15 +2009,24 @@
2009 }
2010 if( zUser ){
2011 blob_appendf(&desc, " by user %h", zUser);
2012 tmFlags |= TIMELINE_DISJOINT;
2013 }
2014 if( zTagSql ){
2015 if( matchStyle==MS_EXACT ){
2016 if( related ){
2017 blob_appendf(&desc, " related to %h", zMatchDesc);
2018 }else{
2019 blob_appendf(&desc, " tagged with %h", zMatchDesc);
2020 }
2021 }else{
2022 if( related ){
2023 blob_appendf(&desc, " related to tags matching %h", zMatchDesc);
2024 }else{
2025 blob_appendf(&desc, " with tags matching %h", zMatchDesc);
2026 }
2027 }
2028 tmFlags |= TIMELINE_DISJOINT;
2029 }
2030 addFileGlobDescription(zChng, &desc);
2031 if( rAfter>0.0 ){
2032 if( rBefore>0.0 ){
@@ -1786,10 +2042,13 @@
2042 }
2043 if( zSearch ){
2044 blob_appendf(&desc, " matching \"%h\"", zSearch);
2045 }
2046 if( g.perm.Hyperlink ){
2047 static const char *const azMatchStyles[] = {
2048 "exact", "Exact", "glob", "Glob", "like", "Like", "regexp", "Regexp"
2049 };
2050 double rDate;
2051 zDate = db_text(0, "SELECT min(timestamp) FROM timeline /*scan*/");
2052 if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){
2053 zDate = mprintf("%s", (zAfter ? zAfter : zBefore));
2054 }
@@ -1824,10 +2083,12 @@
2083 style_submenu_checkbox("unhide", "Unhide", 0);
2084 }
2085 style_submenu_checkbox("v", "Files", zType[0]!='a' && zType[0]!='c');
2086 style_submenu_entry("n","Max:",4,0);
2087 timeline_y_submenu(disableY);
2088 style_submenu_entry("t", "Tag Filter:", -8, 0);
2089 style_submenu_multichoice("ms", count(azMatchStyles)/2, azMatchStyles, 0);
2090 }
2091 blob_zero(&cond);
2092 }
2093 if( PB("showsql") ){
2094 @ <pre>%h(blob_sql_text(&sql))</pre>
@@ -1842,10 +2103,16 @@
2103 }
2104 blob_zero(&sql);
2105 db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/");
2106 @ <h2>%b(&desc)</h2>
2107 blob_reset(&desc);
2108
2109 /* Report any errors. */
2110 if( zError ){
2111 @ <p class="generalError">%h(zError)</p>
2112 }
2113
2114 www_print_timeline(&q, tmFlags, zThisUser, zThisTag, selectedRid, 0);
2115 db_finalize(&q);
2116 if( zOlderButton ){
2117 @ %z(xhref("class='button'","%z",zOlderButton))Older</a>
2118 }
2119
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,10 +1,12 @@
11
<title>Change Log</title>
22
33
<a name='v1_37'></a>
44
<h2>Changes for Version 1.37 (2017-XX-YY)</h2>
55
6
+ * Added support for the ms=EXACT|LIKE|GLOB|REGEXP query parameter on the
7
+ [/help?cmd=/timeline|/timeline] webpage.
68
* Fix a C99-ism that prevents the 1.36 release from building with MSVC.
79
* Fix [/help?cmd=ticket|ticket set] when using the "+" prefix with fields
810
from the "ticketchng" table.
911
* Enhance the "brlist" page to make use of branch colors.
1012
* Remove the "fusefs" command from builds that do not have the underlying
1113
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,10 +1,12 @@
1 <title>Change Log</title>
2
3 <a name='v1_37'></a>
4 <h2>Changes for Version 1.37 (2017-XX-YY)</h2>
5
 
 
6 * Fix a C99-ism that prevents the 1.36 release from building with MSVC.
7 * Fix [/help?cmd=ticket|ticket set] when using the "+" prefix with fields
8 from the "ticketchng" table.
9 * Enhance the "brlist" page to make use of branch colors.
10 * Remove the "fusefs" command from builds that do not have the underlying
11
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,10 +1,12 @@
1 <title>Change Log</title>
2
3 <a name='v1_37'></a>
4 <h2>Changes for Version 1.37 (2017-XX-YY)</h2>
5
6 * Added support for the ms=EXACT|LIKE|GLOB|REGEXP query parameter on the
7 [/help?cmd=/timeline|/timeline] webpage.
8 * Fix a C99-ism that prevents the 1.36 release from building with MSVC.
9 * Fix [/help?cmd=ticket|ticket set] when using the "+" prefix with fields
10 from the "ticketchng" table.
11 * Enhance the "brlist" page to make use of branch colors.
12 * Remove the "fusefs" command from builds that do not have the underlying
13

Keyboard Shortcuts

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