Fossil SCM
Improve quoting of match tags and patterns. Ignore empty match tags and patterns.
Commit
98cc8782e11f94d1dbd19958a2d068c547e94f65
Parent
68bd2e7bedb8d05…
1 file changed
+94
-44
+94
-44
| --- src/timeline.c | ||
| +++ src/timeline.c | ||
| @@ -1227,10 +1227,38 @@ | ||
| 1227 | 1227 | MS_EXACT, /* Matches a single tag by exact string comparison. */ |
| 1228 | 1228 | MS_GLOB, /* Matches tags against a list of GLOB patterns. */ |
| 1229 | 1229 | MS_LIKE, /* Matches tags against a list of LIKE patterns. */ |
| 1230 | 1230 | MS_REGEXP /* Matches tags against a list of regular expressions. */ |
| 1231 | 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 | +} | |
| 1232 | 1260 | |
| 1233 | 1261 | /* |
| 1234 | 1262 | ** Construct the tag match SQL expression. |
| 1235 | 1263 | ** |
| 1236 | 1264 | ** This function is adapted from glob_expr() to support the MS_EXACT, MS_GLOB, |
| @@ -1241,29 +1269,38 @@ | ||
| 1241 | 1269 | ** the tag table to access the "tagname" column. |
| 1242 | 1270 | ** |
| 1243 | 1271 | ** Each pattern is adjusted to to start with "sym-" and be anchored at end. |
| 1244 | 1272 | ** |
| 1245 | 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. | |
| 1246 | 1279 | */ |
| 1247 | 1280 | static const char *tagMatchExpression( |
| 1248 | - MatchStyle matchStyle, /* Match style code */ | |
| 1249 | - const char *zTag, /* Tag name, match pattern, or list of patterns */ | |
| 1250 | - int *pCount /* Pointer to match pattern count variable */ | |
| 1281 | + MatchStyle matchStyle, /* Match style code */ | |
| 1282 | + const char *zTag, /* Tag name, match pattern, or pattern list */ | |
| 1283 | + const char **zDesc /* Output expression description string */ | |
| 1251 | 1284 | ){ |
| 1252 | - Blob blob = BLOB_INITIALIZER; /* SQL expression string assembly buffer */ | |
| 1253 | - const char *zStart; /* Text at start of expression */ | |
| 1254 | - const char *zDelimiter; /* Text between expression terms */ | |
| 1255 | - const char *zEnd; /* Text at end of expression */ | |
| 1256 | - const char *zPrefix; /* Text before each match pattern */ | |
| 1257 | - const char *zSuffix; /* Text after each match pattern */ | |
| 1258 | - char cDel; /* Input delimiter character */ | |
| 1259 | - int i; /* Input match pattern length counter */ | |
| 1285 | + Blob expr = BLOB_INITIALIZER; /* SQL expression string assembly buffer */ | |
| 1286 | + Blob desc = BLOB_INITIALIZER; /* English description of match patterns */ | |
| 1287 | + const char *zStart; /* Text at start of expression */ | |
| 1288 | + const char *zDelimiter; /* Text between expression terms */ | |
| 1289 | + const char *zEnd; /* Text at end of expression */ | |
| 1290 | + const char *zPrefix; /* Text before each match pattern */ | |
| 1291 | + const char *zSuffix; /* Text after each match pattern */ | |
| 1292 | + const char *zIntro; /* Text introducing pattern description */ | |
| 1293 | + const char *zPattern = 0; /* Previous quoted pattern */ | |
| 1294 | + const char *zOr = " or "; /* Text before final quoted pattern */ | |
| 1295 | + char cDel; /* Input delimiter character */ | |
| 1296 | + int i; /* Input match pattern length counter */ | |
| 1260 | 1297 | |
| 1261 | 1298 | /* Optimize exact matches by looking up the ID in advance to create a simple |
| 1262 | 1299 | * numeric comparison. Bypass the remainder of this function. */ |
| 1263 | 1300 | if( matchStyle==MS_EXACT ){ |
| 1264 | - *pCount = 1; | |
| 1301 | + *zDesc = tagQuote(-1, zTag); | |
| 1265 | 1302 | return mprintf("(tagid=%d)", db_int(-1, |
| 1266 | 1303 | "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag)); |
| 1267 | 1304 | } |
| 1268 | 1305 | |
| 1269 | 1306 | /* Decide pattern prefix and suffix strings according to match style. */ |
| @@ -1271,27 +1308,30 @@ | ||
| 1271 | 1308 | zStart = "("; |
| 1272 | 1309 | zDelimiter = " OR "; |
| 1273 | 1310 | zEnd = ")"; |
| 1274 | 1311 | zPrefix = "tagname GLOB 'sym-"; |
| 1275 | 1312 | zSuffix = "'"; |
| 1313 | + zIntro = "glob pattern "; | |
| 1276 | 1314 | }else if( matchStyle==MS_LIKE ){ |
| 1277 | 1315 | zStart = "("; |
| 1278 | 1316 | zDelimiter = " OR "; |
| 1279 | 1317 | zEnd = ")"; |
| 1280 | 1318 | zPrefix = "tagname LIKE 'sym-"; |
| 1281 | 1319 | zSuffix = "'"; |
| 1320 | + zIntro = "SQL LIKE pattern "; | |
| 1282 | 1321 | }else/* if( matchStyle==MS_REGEXP )*/{ |
| 1283 | 1322 | zStart = "(tagname REGEXP '^sym-("; |
| 1284 | 1323 | zDelimiter = "|"; |
| 1285 | 1324 | zEnd = ")$')"; |
| 1286 | 1325 | zPrefix = ""; |
| 1287 | 1326 | zSuffix = ""; |
| 1327 | + zIntro = "regular expression "; | |
| 1288 | 1328 | } |
| 1289 | 1329 | |
| 1290 | - /* Convert the list of matches into an SQL expression. */ | |
| 1291 | - *pCount = 0; | |
| 1292 | - blob_zero(&blob); | |
| 1330 | + /* Convert the list of matches into an SQL expression and text description. */ | |
| 1331 | + blob_zero(&expr); | |
| 1332 | + blob_zero(&desc); | |
| 1293 | 1333 | while( 1 ){ |
| 1294 | 1334 | /* Skip leading delimiters. */ |
| 1295 | 1335 | for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag ); |
| 1296 | 1336 | |
| 1297 | 1337 | /* Next non-delimiter character determines quoting. */ |
| @@ -1320,27 +1360,48 @@ | ||
| 1320 | 1360 | } |
| 1321 | 1361 | } |
| 1322 | 1362 | |
| 1323 | 1363 | /* Incorporate the match word into the output expression. The %q format is |
| 1324 | 1364 | * used to protect against SQL injection attacks by replacing ' with ''. */ |
| 1325 | - blob_appendf(&blob, "%s%s%#q%s", *pCount ? zDelimiter : zStart, | |
| 1365 | + blob_appendf(&expr, "%s%s%#q%s", blob_size(&expr) ? zDelimiter : zStart, | |
| 1326 | 1366 | zPrefix, i, zTag, zSuffix); |
| 1327 | 1367 | |
| 1328 | - /* Keep track of the number of match expressions. */ | |
| 1329 | - ++*pCount; | |
| 1368 | + /* Build up the description string. */ | |
| 1369 | + if( !blob_size(&desc) ){ | |
| 1370 | + /* First tag: start with intro followed by first quoted tag. */ | |
| 1371 | + blob_append(&desc, zIntro, -1); | |
| 1372 | + blob_append(&desc, tagQuote(i, zTag), -1); | |
| 1373 | + }else{ | |
| 1374 | + if( zPattern ){ | |
| 1375 | + /* Third and subsequent tags: append comma then previous tag. */ | |
| 1376 | + blob_append(&desc, ", ", 2); | |
| 1377 | + blob_append(&desc, zPattern, -1); | |
| 1378 | + zOr = ", or "; | |
| 1379 | + } | |
| 1380 | + | |
| 1381 | + /* Second and subsequent tags: store quoted tag for next iteration. */ | |
| 1382 | + zPattern = tagQuote(i, zTag); | |
| 1383 | + } | |
| 1330 | 1384 | |
| 1331 | 1385 | /* Advance past all consumed input characters. */ |
| 1332 | 1386 | zTag += i; |
| 1333 | 1387 | if( cDel!=',' && *zTag==cDel ){ |
| 1334 | 1388 | ++zTag; |
| 1335 | 1389 | } |
| 1336 | 1390 | } |
| 1391 | + | |
| 1392 | + /* Finalize and extract the pattern description. */ | |
| 1393 | + if( zPattern ){ | |
| 1394 | + blob_append(&desc, zOr, -1); | |
| 1395 | + blob_append(&desc, zPattern, -1); | |
| 1396 | + } | |
| 1397 | + *zDesc = blob_str(&desc); | |
| 1337 | 1398 | |
| 1338 | 1399 | /* Finalize and extract the SQL expression. */ |
| 1339 | - if( *pCount ){ | |
| 1340 | - blob_append(&blob, zEnd, -1); | |
| 1341 | - return blob_str(&blob); | |
| 1400 | + if( blob_size(&expr) ){ | |
| 1401 | + blob_append(&expr, zEnd, -1); | |
| 1402 | + return blob_str(&expr); | |
| 1342 | 1403 | } |
| 1343 | 1404 | |
| 1344 | 1405 | /* If execution reaches this point, the pattern was empty. Return NULL. */ |
| 1345 | 1406 | return 0; |
| 1346 | 1407 | } |
| @@ -1404,12 +1465,12 @@ | ||
| 1404 | 1465 | const char *zMark = P("m"); /* Mark this event or an event this time */ |
| 1405 | 1466 | const char *zTagName = P("t"); /* Show events with this tag */ |
| 1406 | 1467 | const char *zBrName = P("r"); /* Show events related to this tag */ |
| 1407 | 1468 | const char *zMatchStyle = P("ms"); /* Tag/branch match style string */ |
| 1408 | 1469 | MatchStyle matchStyle = MS_EXACT; /* Match style code */ |
| 1470 | + const char *zMatchDesc = 0; /* Tag match expression description text */ | |
| 1409 | 1471 | const char *zTagSql = 0; /* Tag/branch match SQL expression */ |
| 1410 | - int tagMatchCount = 0; /* Number of tag match patterns */ | |
| 1411 | 1472 | const char *zSearch = P("s"); /* Search string */ |
| 1412 | 1473 | const char *zUses = P("uf"); /* Only show check-ins hold this file */ |
| 1413 | 1474 | const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ |
| 1414 | 1475 | const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ |
| 1415 | 1476 | const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ |
| @@ -1469,13 +1530,13 @@ | ||
| 1469 | 1530 | } |
| 1470 | 1531 | url_initialize(&url, "timeline"); |
| 1471 | 1532 | cgi_query_parameters_to_url(&url); |
| 1472 | 1533 | |
| 1473 | 1534 | /* Identify the tag or branch name or match pattern. */ |
| 1474 | - if( zTagName ){ | |
| 1535 | + if( zTagName && *zTagName ){ | |
| 1475 | 1536 | zThisTag = zTagName; |
| 1476 | - }else if( zBrName ){ | |
| 1537 | + }else if( zBrName && *zBrName ){ | |
| 1477 | 1538 | zThisTag = zBrName; |
| 1478 | 1539 | } |
| 1479 | 1540 | |
| 1480 | 1541 | /* Interpet the tag style string. */ |
| 1481 | 1542 | if( zThisTag ){ |
| @@ -1488,11 +1549,11 @@ | ||
| 1488 | 1549 | } |
| 1489 | 1550 | } |
| 1490 | 1551 | |
| 1491 | 1552 | /* Construct the tag match expression. */ |
| 1492 | 1553 | if( zThisTag ){ |
| 1493 | - zTagSql = tagMatchExpression(matchStyle, zThisTag, &tagMatchCount); | |
| 1554 | + zTagSql = tagMatchExpression(matchStyle, zThisTag, &zMatchDesc); | |
| 1494 | 1555 | } |
| 1495 | 1556 | |
| 1496 | 1557 | if( zTagName && g.perm.Read ){ |
| 1497 | 1558 | style_submenu_element("Related", "Related", "%s", |
| 1498 | 1559 | url_render(&url, "r", zTagName, "t", 0)); |
| @@ -1915,29 +1976,18 @@ | ||
| 1915 | 1976 | tmFlags |= TIMELINE_DISJOINT; |
| 1916 | 1977 | } |
| 1917 | 1978 | if( zThisTag ){ |
| 1918 | 1979 | if( matchStyle!=MS_EXACT ){ |
| 1919 | 1980 | if( zTagName ){ |
| 1920 | - blob_append(&desc, " with tags matching ", -1); | |
| 1921 | - }else{ | |
| 1922 | - blob_append(&desc, " related to tags matching ", -1); | |
| 1923 | - } | |
| 1924 | - if( matchStyle==MS_GLOB ){ | |
| 1925 | - blob_append(&desc, " glob pattern", -1); | |
| 1926 | - }else if( matchStyle==MS_LIKE ){ | |
| 1927 | - blob_append(&desc, " SQL LIKE pattern", -1); | |
| 1928 | - }else/* if( matchStyle==MS_REGEXP )*/{ | |
| 1929 | - blob_append(&desc, " regular expression", -1); | |
| 1930 | - } | |
| 1931 | - if( tagMatchCount!=1 ){ | |
| 1932 | - blob_append(&desc, "s", 1); | |
| 1933 | - } | |
| 1934 | - blob_appendf(&desc, " (%h)", zThisTag); | |
| 1935 | - }else if( zTagName ){ | |
| 1936 | - blob_appendf(&desc, " tagged with \"%h\"", zTagName); | |
| 1937 | - }else{ | |
| 1938 | - blob_appendf(&desc, " related to \"%h\"", zBrName); | |
| 1981 | + blob_appendf(&desc, " with tags matching %h", zMatchDesc); | |
| 1982 | + }else{ | |
| 1983 | + blob_appendf(&desc, " related to tags matching %h", zMatchDesc); | |
| 1984 | + } | |
| 1985 | + }else if( zTagName ){ | |
| 1986 | + blob_appendf(&desc, " tagged with %h", zMatchDesc); | |
| 1987 | + }else{ | |
| 1988 | + blob_appendf(&desc, " related to %h", zMatchDesc); | |
| 1939 | 1989 | } |
| 1940 | 1990 | tmFlags |= TIMELINE_DISJOINT; |
| 1941 | 1991 | } |
| 1942 | 1992 | addFileGlobDescription(zChng, &desc); |
| 1943 | 1993 | if( rAfter>0.0 ){ |
| 1944 | 1994 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -1227,10 +1227,38 @@ | |
| 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 | ** Construct the tag match SQL expression. |
| 1235 | ** |
| 1236 | ** This function is adapted from glob_expr() to support the MS_EXACT, MS_GLOB, |
| @@ -1241,29 +1269,38 @@ | |
| 1241 | ** the tag table to access the "tagname" column. |
| 1242 | ** |
| 1243 | ** Each pattern is adjusted to to start with "sym-" and be anchored at end. |
| 1244 | ** |
| 1245 | ** In MS_REGEXP mode, backslash can be used to protect delimiter characters. |
| 1246 | */ |
| 1247 | static const char *tagMatchExpression( |
| 1248 | MatchStyle matchStyle, /* Match style code */ |
| 1249 | const char *zTag, /* Tag name, match pattern, or list of patterns */ |
| 1250 | int *pCount /* Pointer to match pattern count variable */ |
| 1251 | ){ |
| 1252 | Blob blob = BLOB_INITIALIZER; /* SQL expression string assembly buffer */ |
| 1253 | const char *zStart; /* Text at start of expression */ |
| 1254 | const char *zDelimiter; /* Text between expression terms */ |
| 1255 | const char *zEnd; /* Text at end of expression */ |
| 1256 | const char *zPrefix; /* Text before each match pattern */ |
| 1257 | const char *zSuffix; /* Text after each match pattern */ |
| 1258 | char cDel; /* Input delimiter character */ |
| 1259 | int i; /* Input match pattern length counter */ |
| 1260 | |
| 1261 | /* Optimize exact matches by looking up the ID in advance to create a simple |
| 1262 | * numeric comparison. Bypass the remainder of this function. */ |
| 1263 | if( matchStyle==MS_EXACT ){ |
| 1264 | *pCount = 1; |
| 1265 | return mprintf("(tagid=%d)", db_int(-1, |
| 1266 | "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag)); |
| 1267 | } |
| 1268 | |
| 1269 | /* Decide pattern prefix and suffix strings according to match style. */ |
| @@ -1271,27 +1308,30 @@ | |
| 1271 | zStart = "("; |
| 1272 | zDelimiter = " OR "; |
| 1273 | zEnd = ")"; |
| 1274 | zPrefix = "tagname GLOB 'sym-"; |
| 1275 | zSuffix = "'"; |
| 1276 | }else if( matchStyle==MS_LIKE ){ |
| 1277 | zStart = "("; |
| 1278 | zDelimiter = " OR "; |
| 1279 | zEnd = ")"; |
| 1280 | zPrefix = "tagname LIKE 'sym-"; |
| 1281 | zSuffix = "'"; |
| 1282 | }else/* if( matchStyle==MS_REGEXP )*/{ |
| 1283 | zStart = "(tagname REGEXP '^sym-("; |
| 1284 | zDelimiter = "|"; |
| 1285 | zEnd = ")$')"; |
| 1286 | zPrefix = ""; |
| 1287 | zSuffix = ""; |
| 1288 | } |
| 1289 | |
| 1290 | /* Convert the list of matches into an SQL expression. */ |
| 1291 | *pCount = 0; |
| 1292 | blob_zero(&blob); |
| 1293 | while( 1 ){ |
| 1294 | /* Skip leading delimiters. */ |
| 1295 | for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag ); |
| 1296 | |
| 1297 | /* Next non-delimiter character determines quoting. */ |
| @@ -1320,27 +1360,48 @@ | |
| 1320 | } |
| 1321 | } |
| 1322 | |
| 1323 | /* Incorporate the match word into the output expression. The %q format is |
| 1324 | * used to protect against SQL injection attacks by replacing ' with ''. */ |
| 1325 | blob_appendf(&blob, "%s%s%#q%s", *pCount ? zDelimiter : zStart, |
| 1326 | zPrefix, i, zTag, zSuffix); |
| 1327 | |
| 1328 | /* Keep track of the number of match expressions. */ |
| 1329 | ++*pCount; |
| 1330 | |
| 1331 | /* Advance past all consumed input characters. */ |
| 1332 | zTag += i; |
| 1333 | if( cDel!=',' && *zTag==cDel ){ |
| 1334 | ++zTag; |
| 1335 | } |
| 1336 | } |
| 1337 | |
| 1338 | /* Finalize and extract the SQL expression. */ |
| 1339 | if( *pCount ){ |
| 1340 | blob_append(&blob, zEnd, -1); |
| 1341 | return blob_str(&blob); |
| 1342 | } |
| 1343 | |
| 1344 | /* If execution reaches this point, the pattern was empty. Return NULL. */ |
| 1345 | return 0; |
| 1346 | } |
| @@ -1404,12 +1465,12 @@ | |
| 1404 | const char *zMark = P("m"); /* Mark this event or an event this time */ |
| 1405 | const char *zTagName = P("t"); /* Show events with this tag */ |
| 1406 | const char *zBrName = P("r"); /* Show events related to this tag */ |
| 1407 | const char *zMatchStyle = P("ms"); /* Tag/branch match style string */ |
| 1408 | MatchStyle matchStyle = MS_EXACT; /* Match style code */ |
| 1409 | const char *zTagSql = 0; /* Tag/branch match SQL expression */ |
| 1410 | int tagMatchCount = 0; /* Number of tag match patterns */ |
| 1411 | const char *zSearch = P("s"); /* Search string */ |
| 1412 | const char *zUses = P("uf"); /* Only show check-ins hold this file */ |
| 1413 | const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ |
| 1414 | const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ |
| 1415 | const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ |
| @@ -1469,13 +1530,13 @@ | |
| 1469 | } |
| 1470 | url_initialize(&url, "timeline"); |
| 1471 | cgi_query_parameters_to_url(&url); |
| 1472 | |
| 1473 | /* Identify the tag or branch name or match pattern. */ |
| 1474 | if( zTagName ){ |
| 1475 | zThisTag = zTagName; |
| 1476 | }else if( zBrName ){ |
| 1477 | zThisTag = zBrName; |
| 1478 | } |
| 1479 | |
| 1480 | /* Interpet the tag style string. */ |
| 1481 | if( zThisTag ){ |
| @@ -1488,11 +1549,11 @@ | |
| 1488 | } |
| 1489 | } |
| 1490 | |
| 1491 | /* Construct the tag match expression. */ |
| 1492 | if( zThisTag ){ |
| 1493 | zTagSql = tagMatchExpression(matchStyle, zThisTag, &tagMatchCount); |
| 1494 | } |
| 1495 | |
| 1496 | if( zTagName && g.perm.Read ){ |
| 1497 | style_submenu_element("Related", "Related", "%s", |
| 1498 | url_render(&url, "r", zTagName, "t", 0)); |
| @@ -1915,29 +1976,18 @@ | |
| 1915 | tmFlags |= TIMELINE_DISJOINT; |
| 1916 | } |
| 1917 | if( zThisTag ){ |
| 1918 | if( matchStyle!=MS_EXACT ){ |
| 1919 | if( zTagName ){ |
| 1920 | blob_append(&desc, " with tags matching ", -1); |
| 1921 | }else{ |
| 1922 | blob_append(&desc, " related to tags matching ", -1); |
| 1923 | } |
| 1924 | if( matchStyle==MS_GLOB ){ |
| 1925 | blob_append(&desc, " glob pattern", -1); |
| 1926 | }else if( matchStyle==MS_LIKE ){ |
| 1927 | blob_append(&desc, " SQL LIKE pattern", -1); |
| 1928 | }else/* if( matchStyle==MS_REGEXP )*/{ |
| 1929 | blob_append(&desc, " regular expression", -1); |
| 1930 | } |
| 1931 | if( tagMatchCount!=1 ){ |
| 1932 | blob_append(&desc, "s", 1); |
| 1933 | } |
| 1934 | blob_appendf(&desc, " (%h)", zThisTag); |
| 1935 | }else if( zTagName ){ |
| 1936 | blob_appendf(&desc, " tagged with \"%h\"", zTagName); |
| 1937 | }else{ |
| 1938 | blob_appendf(&desc, " related to \"%h\"", zBrName); |
| 1939 | } |
| 1940 | tmFlags |= TIMELINE_DISJOINT; |
| 1941 | } |
| 1942 | addFileGlobDescription(zChng, &desc); |
| 1943 | if( rAfter>0.0 ){ |
| 1944 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -1227,10 +1227,38 @@ | |
| 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, |
| @@ -1241,29 +1269,38 @@ | |
| 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 | static const char *tagMatchExpression( |
| 1281 | MatchStyle matchStyle, /* Match style code */ |
| 1282 | const char *zTag, /* Tag name, match pattern, or pattern list */ |
| 1283 | const char **zDesc /* Output expression description string */ |
| 1284 | ){ |
| 1285 | Blob expr = BLOB_INITIALIZER; /* SQL expression string assembly buffer */ |
| 1286 | Blob desc = BLOB_INITIALIZER; /* English description of match patterns */ |
| 1287 | const char *zStart; /* Text at start of expression */ |
| 1288 | const char *zDelimiter; /* Text between expression terms */ |
| 1289 | const char *zEnd; /* Text at end of expression */ |
| 1290 | const char *zPrefix; /* Text before each match pattern */ |
| 1291 | const char *zSuffix; /* Text after each match pattern */ |
| 1292 | const char *zIntro; /* Text introducing pattern description */ |
| 1293 | const char *zPattern = 0; /* Previous quoted pattern */ |
| 1294 | const char *zOr = " or "; /* Text before final quoted pattern */ |
| 1295 | char cDel; /* Input delimiter character */ |
| 1296 | int i; /* Input match pattern length counter */ |
| 1297 | |
| 1298 | /* Optimize exact matches by looking up the ID in advance to create a simple |
| 1299 | * numeric comparison. Bypass the remainder of this function. */ |
| 1300 | if( matchStyle==MS_EXACT ){ |
| 1301 | *zDesc = tagQuote(-1, zTag); |
| 1302 | return mprintf("(tagid=%d)", db_int(-1, |
| 1303 | "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag)); |
| 1304 | } |
| 1305 | |
| 1306 | /* Decide pattern prefix and suffix strings according to match style. */ |
| @@ -1271,27 +1308,30 @@ | |
| 1308 | zStart = "("; |
| 1309 | zDelimiter = " OR "; |
| 1310 | zEnd = ")"; |
| 1311 | zPrefix = "tagname GLOB 'sym-"; |
| 1312 | zSuffix = "'"; |
| 1313 | zIntro = "glob pattern "; |
| 1314 | }else if( matchStyle==MS_LIKE ){ |
| 1315 | zStart = "("; |
| 1316 | zDelimiter = " OR "; |
| 1317 | zEnd = ")"; |
| 1318 | zPrefix = "tagname LIKE 'sym-"; |
| 1319 | zSuffix = "'"; |
| 1320 | zIntro = "SQL LIKE pattern "; |
| 1321 | }else/* if( matchStyle==MS_REGEXP )*/{ |
| 1322 | zStart = "(tagname REGEXP '^sym-("; |
| 1323 | zDelimiter = "|"; |
| 1324 | zEnd = ")$')"; |
| 1325 | zPrefix = ""; |
| 1326 | zSuffix = ""; |
| 1327 | zIntro = "regular expression "; |
| 1328 | } |
| 1329 | |
| 1330 | /* Convert the list of matches into an SQL expression and text description. */ |
| 1331 | blob_zero(&expr); |
| 1332 | blob_zero(&desc); |
| 1333 | while( 1 ){ |
| 1334 | /* Skip leading delimiters. */ |
| 1335 | for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag ); |
| 1336 | |
| 1337 | /* Next non-delimiter character determines quoting. */ |
| @@ -1320,27 +1360,48 @@ | |
| 1360 | } |
| 1361 | } |
| 1362 | |
| 1363 | /* Incorporate the match word into the output expression. The %q format is |
| 1364 | * used to protect against SQL injection attacks by replacing ' with ''. */ |
| 1365 | blob_appendf(&expr, "%s%s%#q%s", blob_size(&expr) ? zDelimiter : zStart, |
| 1366 | zPrefix, i, zTag, zSuffix); |
| 1367 | |
| 1368 | /* Build up the description string. */ |
| 1369 | if( !blob_size(&desc) ){ |
| 1370 | /* First tag: start with intro followed by first quoted tag. */ |
| 1371 | blob_append(&desc, zIntro, -1); |
| 1372 | blob_append(&desc, tagQuote(i, zTag), -1); |
| 1373 | }else{ |
| 1374 | if( zPattern ){ |
| 1375 | /* Third and subsequent tags: append comma then previous tag. */ |
| 1376 | blob_append(&desc, ", ", 2); |
| 1377 | blob_append(&desc, zPattern, -1); |
| 1378 | zOr = ", or "; |
| 1379 | } |
| 1380 | |
| 1381 | /* Second and subsequent tags: store quoted tag for next iteration. */ |
| 1382 | zPattern = tagQuote(i, zTag); |
| 1383 | } |
| 1384 | |
| 1385 | /* Advance past all consumed input characters. */ |
| 1386 | zTag += i; |
| 1387 | if( cDel!=',' && *zTag==cDel ){ |
| 1388 | ++zTag; |
| 1389 | } |
| 1390 | } |
| 1391 | |
| 1392 | /* Finalize and extract the pattern description. */ |
| 1393 | if( zPattern ){ |
| 1394 | blob_append(&desc, zOr, -1); |
| 1395 | blob_append(&desc, zPattern, -1); |
| 1396 | } |
| 1397 | *zDesc = blob_str(&desc); |
| 1398 | |
| 1399 | /* Finalize and extract the SQL expression. */ |
| 1400 | if( blob_size(&expr) ){ |
| 1401 | blob_append(&expr, zEnd, -1); |
| 1402 | return blob_str(&expr); |
| 1403 | } |
| 1404 | |
| 1405 | /* If execution reaches this point, the pattern was empty. Return NULL. */ |
| 1406 | return 0; |
| 1407 | } |
| @@ -1404,12 +1465,12 @@ | |
| 1465 | const char *zMark = P("m"); /* Mark this event or an event this time */ |
| 1466 | const char *zTagName = P("t"); /* Show events with this tag */ |
| 1467 | const char *zBrName = P("r"); /* Show events related to this tag */ |
| 1468 | const char *zMatchStyle = P("ms"); /* Tag/branch match style string */ |
| 1469 | MatchStyle matchStyle = MS_EXACT; /* Match style code */ |
| 1470 | const char *zMatchDesc = 0; /* Tag match expression description text */ |
| 1471 | const char *zTagSql = 0; /* Tag/branch match SQL expression */ |
| 1472 | const char *zSearch = P("s"); /* Search string */ |
| 1473 | const char *zUses = P("uf"); /* Only show check-ins hold this file */ |
| 1474 | const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ |
| 1475 | const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ |
| 1476 | const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ |
| @@ -1469,13 +1530,13 @@ | |
| 1530 | } |
| 1531 | url_initialize(&url, "timeline"); |
| 1532 | cgi_query_parameters_to_url(&url); |
| 1533 | |
| 1534 | /* Identify the tag or branch name or match pattern. */ |
| 1535 | if( zTagName && *zTagName ){ |
| 1536 | zThisTag = zTagName; |
| 1537 | }else if( zBrName && *zBrName ){ |
| 1538 | zThisTag = zBrName; |
| 1539 | } |
| 1540 | |
| 1541 | /* Interpet the tag style string. */ |
| 1542 | if( zThisTag ){ |
| @@ -1488,11 +1549,11 @@ | |
| 1549 | } |
| 1550 | } |
| 1551 | |
| 1552 | /* Construct the tag match expression. */ |
| 1553 | if( zThisTag ){ |
| 1554 | zTagSql = tagMatchExpression(matchStyle, zThisTag, &zMatchDesc); |
| 1555 | } |
| 1556 | |
| 1557 | if( zTagName && g.perm.Read ){ |
| 1558 | style_submenu_element("Related", "Related", "%s", |
| 1559 | url_render(&url, "r", zTagName, "t", 0)); |
| @@ -1915,29 +1976,18 @@ | |
| 1976 | tmFlags |= TIMELINE_DISJOINT; |
| 1977 | } |
| 1978 | if( zThisTag ){ |
| 1979 | if( matchStyle!=MS_EXACT ){ |
| 1980 | if( zTagName ){ |
| 1981 | blob_appendf(&desc, " with tags matching %h", zMatchDesc); |
| 1982 | }else{ |
| 1983 | blob_appendf(&desc, " related to tags matching %h", zMatchDesc); |
| 1984 | } |
| 1985 | }else if( zTagName ){ |
| 1986 | blob_appendf(&desc, " tagged with %h", zMatchDesc); |
| 1987 | }else{ |
| 1988 | blob_appendf(&desc, " related to %h", zMatchDesc); |
| 1989 | } |
| 1990 | tmFlags |= TIMELINE_DISJOINT; |
| 1991 | } |
| 1992 | addFileGlobDescription(zChng, &desc); |
| 1993 | if( rAfter>0.0 ){ |
| 1994 |