Fossil SCM
Add ms=EXACT|LIKE|GLOB|REGEXP timeline query parameter which changes the interpretation of the t= and r= query parameters to exact (default), SQL LIKE, glob, or regular expression matching. Other than exact matching, the t= or r= parameter value is interpreted as a whitespace- or comma-delimited list of match patterns, with the option to quote a pattern using single or double quotes in case it contains commas or whitespace. For regular expression matching only, backslash can also be used to inhibit delimiter interpretation of the next character. At this point, the only way to use this feature is to type ms= directly into the URL; the UI still needs to be updated. Be aware for LIKE interpretation that % needs to be written as %25.
Commit
be58684514f8c7dd1f1f076814d179fef2c94805
Parent
f59f4e1f6e9d161…
2 files changed
+167
-21
+2
+167
-21
| --- src/timeline.c | ||
| +++ src/timeline.c | ||
| @@ -1218,10 +1218,116 @@ | ||
| 1218 | 1218 | if( zChng==0 || zChng[0]==0 ) return; |
| 1219 | 1219 | blob_appendf(pDescription, " that include changes to files matching %Q", |
| 1220 | 1220 | zChng); |
| 1221 | 1221 | } |
| 1222 | 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_LIKE, /* Matches tags against a list of LIKE patterns. */ | |
| 1229 | + MS_GLOB, /* Matches tags against a list of GLOB patterns. */ | |
| 1230 | + MS_REGEXP /* Matches tags against a list of regular expressions. */ | |
| 1231 | +} MatchStyle; | |
| 1232 | + | |
| 1233 | +/* | |
| 1234 | +** Construct the tag match expression. | |
| 1235 | +*/ | |
| 1236 | +static const char *tagMatchExpression( | |
| 1237 | + MatchStyle matchStyle, /* Match style code */ | |
| 1238 | + const char *zTag, /* Tag name, match pattern, or list of patterns */ | |
| 1239 | + int *pCount /* Pointer to match pattern count variable */ | |
| 1240 | +){ | |
| 1241 | + Blob blob = BLOB_INITIALIZER; | |
| 1242 | + const char *zSep = "(", *zPre, *zSuf; | |
| 1243 | + char cDel; | |
| 1244 | + int i, dummy; | |
| 1245 | + | |
| 1246 | + /* Protect against NULL count pointer. */ | |
| 1247 | + if( !pCount ){ | |
| 1248 | + pCount = &dummy; | |
| 1249 | + } | |
| 1250 | + | |
| 1251 | + /* Decide pattern prefix and suffix strings according to match style. */ | |
| 1252 | + if( matchStyle==MS_EXACT ){ | |
| 1253 | + /* Optimize exact matches by looking up the numeric ID in advance. Bypass | |
| 1254 | + * the remainder of this function. */ | |
| 1255 | + *pCount = 1; | |
| 1256 | + return mprintf("(tagid=%d)", db_int(-1, | |
| 1257 | + "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag)); | |
| 1258 | + }else if( matchStyle==MS_LIKE ){ | |
| 1259 | + zPre = "LIKE 'sym-"; | |
| 1260 | + zSuf = "'"; | |
| 1261 | + }else if( matchStyle==MS_GLOB ){ | |
| 1262 | + zPre = "GLOB 'sym-"; | |
| 1263 | + zSuf = "'"; | |
| 1264 | + }else/* if( matchStyle==MS_REGEXP )*/{ | |
| 1265 | + zPre = "REGEXP '^sym-"; | |
| 1266 | + zSuf = "$'"; | |
| 1267 | + } | |
| 1268 | + | |
| 1269 | + /* The following code is glob_expr() modified to support LIKE and REGEXP, plus | |
| 1270 | + * adjust each pattern to start with "sym-" and be anchored at end. In REGEXP | |
| 1271 | + * mode, it allows backslash to protect delimiter characters. */ | |
| 1272 | + | |
| 1273 | + /* Convert the list of matches into an SQL expression. */ | |
| 1274 | + *pCount = 0; | |
| 1275 | + blob_zero(&blob); | |
| 1276 | + while( 1 ){ | |
| 1277 | + /* Skip leading delimiters. */ | |
| 1278 | + for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag ); | |
| 1279 | + | |
| 1280 | + /* Next non-delimiter character determines quoting. */ | |
| 1281 | + if( !*zTag ){ | |
| 1282 | + /* Terminate loop at end of string. */ | |
| 1283 | + break; | |
| 1284 | + }else if( *zTag=='\'' || *zTag=='"' ){ | |
| 1285 | + /* If word is quoted, prepare to stop at end quote. */ | |
| 1286 | + cDel = *zTag; | |
| 1287 | + ++zTag; | |
| 1288 | + }else{ | |
| 1289 | + /* If word is not quoted, prepare to stop at delimiter. */ | |
| 1290 | + cDel = ','; | |
| 1291 | + } | |
| 1292 | + | |
| 1293 | + /* Find the next delimiter character or end of string. */ | |
| 1294 | + for( i=0; zTag[i] && zTag[i]!=cDel; ++i ){ | |
| 1295 | + /* If delimiter is comma, also recognize spaces as delimiters. */ | |
| 1296 | + if( cDel==',' && fossil_isspace(zTag[i]) ){ | |
| 1297 | + break; | |
| 1298 | + } | |
| 1299 | + | |
| 1300 | + /* In regexp mode, ignore delimiters following backslashes. */ | |
| 1301 | + if( matchStyle==MS_REGEXP && zTag[i]=='\\' && zTag[i+1] ){ | |
| 1302 | + ++i; | |
| 1303 | + } | |
| 1304 | + } | |
| 1305 | + | |
| 1306 | + /* Incorporate the match word into the final expression. */ | |
| 1307 | + blob_appendf(&blob, "%stagname %s%#q%s", zSep, zPre, i, zTag, zSuf); | |
| 1308 | + | |
| 1309 | + /* Keep track of the number of match expressions. */ | |
| 1310 | + ++*pCount; | |
| 1311 | + | |
| 1312 | + /* Prepare for the next match word. */ | |
| 1313 | + zTag += i; | |
| 1314 | + if( cDel!=',' && *zTag==cDel ){ | |
| 1315 | + ++zTag; | |
| 1316 | + } | |
| 1317 | + zSep = " OR "; | |
| 1318 | + } | |
| 1319 | + | |
| 1320 | + /* Finalize and extract the SQL expression. */ | |
| 1321 | + if( *pCount ){ | |
| 1322 | + blob_append(&blob, ")", 1); | |
| 1323 | + return blob_str(&blob); | |
| 1324 | + } | |
| 1325 | + | |
| 1326 | + /* If execution reaches this point, the pattern was empty. Return NULL. */ | |
| 1327 | + return 0; | |
| 1328 | +} | |
| 1223 | 1329 | |
| 1224 | 1330 | /* |
| 1225 | 1331 | ** WEBPAGE: timeline |
| 1226 | 1332 | ** |
| 1227 | 1333 | ** Query parameters: |
| @@ -1234,10 +1340,11 @@ | ||
| 1234 | 1340 | ** p=CHECKIN parents and ancestors of CHECKIN |
| 1235 | 1341 | ** d=CHECKIN descendants of CHECIN |
| 1236 | 1342 | ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN |
| 1237 | 1343 | ** t=TAG show only check-ins with the given TAG |
| 1238 | 1344 | ** r=TAG show check-ins related to TAG |
| 1345 | +** ms=STYLE sets tag match style to EXACT, LIKE, GLOB, REGEXP | |
| 1239 | 1346 | ** u=USER only show items associated with USER |
| 1240 | 1347 | ** y=TYPE 'ci', 'w', 't', 'e', or (default) 'all' |
| 1241 | 1348 | ** ng No Graph. |
| 1242 | 1349 | ** nd Do not highlight the focus check-in |
| 1243 | 1350 | ** v Show details of files changed |
| @@ -1277,10 +1384,14 @@ | ||
| 1277 | 1384 | const char *zBefore = P("b"); /* Events before this time */ |
| 1278 | 1385 | const char *zCirca = P("c"); /* Events near this time */ |
| 1279 | 1386 | const char *zMark = P("m"); /* Mark this event or an event this time */ |
| 1280 | 1387 | const char *zTagName = P("t"); /* Show events with this tag */ |
| 1281 | 1388 | const char *zBrName = P("r"); /* Show events related to this tag */ |
| 1389 | + const char *zMatchStyle = P("ms"); /* Tag/branch match style string */ | |
| 1390 | + MatchStyle matchStyle = MS_EXACT; /* Match style code */ | |
| 1391 | + const char *zTagSql = 0; /* Tag/branch match SQL expression */ | |
| 1392 | + int tagMatchCount = 0; /* Number of tag match patterns */ | |
| 1282 | 1393 | const char *zSearch = P("s"); /* Search string */ |
| 1283 | 1394 | const char *zUses = P("uf"); /* Only show check-ins hold this file */ |
| 1284 | 1395 | const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ |
| 1285 | 1396 | const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ |
| 1286 | 1397 | const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ |
| @@ -1287,11 +1398,10 @@ | ||
| 1287 | 1398 | const char *zChng = P("chng"); /* List of GLOBs for files that changed */ |
| 1288 | 1399 | int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */ |
| 1289 | 1400 | int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */ |
| 1290 | 1401 | int forkOnly = PB("forks"); /* Show only forks and their children */ |
| 1291 | 1402 | int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */ |
| 1292 | - int tagid; /* Tag ID */ | |
| 1293 | 1403 | int tmFlags = 0; /* Timeline flags */ |
| 1294 | 1404 | const char *zThisTag = 0; /* Suppress links to this tag */ |
| 1295 | 1405 | const char *zThisUser = 0; /* Suppress links to this user */ |
| 1296 | 1406 | HQuery url; /* URL for various branch links */ |
| 1297 | 1407 | int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */ |
| @@ -1339,30 +1449,48 @@ | ||
| 1339 | 1449 | login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
| 1340 | 1450 | return; |
| 1341 | 1451 | } |
| 1342 | 1452 | url_initialize(&url, "timeline"); |
| 1343 | 1453 | cgi_query_parameters_to_url(&url); |
| 1344 | - if( zTagName && g.perm.Read ){ | |
| 1345 | - tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zTagName); | |
| 1454 | + | |
| 1455 | + /* Identify the tag or branch name or match pattern. */ | |
| 1456 | + if( zTagName ){ | |
| 1346 | 1457 | zThisTag = zTagName; |
| 1458 | + }else if( zBrName ){ | |
| 1459 | + zThisTag = zBrName; | |
| 1460 | + } | |
| 1461 | + | |
| 1462 | + /* Interpet the tag style string. */ | |
| 1463 | + if( zThisTag ){ | |
| 1464 | + if( fossil_stricmp(zMatchStyle, "LIKE")==0 ){ | |
| 1465 | + matchStyle = MS_LIKE; | |
| 1466 | + }else if( fossil_stricmp(zMatchStyle, "GLOB")==0 ){ | |
| 1467 | + matchStyle = MS_GLOB; | |
| 1468 | + }else if( fossil_stricmp(zMatchStyle, "REGEXP")==0 ){ | |
| 1469 | + matchStyle = MS_REGEXP; | |
| 1470 | + } | |
| 1471 | + } | |
| 1472 | + | |
| 1473 | + /* Construct the tag match expression. */ | |
| 1474 | + if( zThisTag ){ | |
| 1475 | + zTagSql = tagMatchExpression(matchStyle, zThisTag, &tagMatchCount); | |
| 1476 | + } | |
| 1477 | + | |
| 1478 | + if( zTagName && g.perm.Read ){ | |
| 1347 | 1479 | style_submenu_element("Related", "Related", "%s", |
| 1348 | 1480 | url_render(&url, "r", zTagName, "t", 0)); |
| 1349 | 1481 | }else if( zBrName && g.perm.Read ){ |
| 1350 | - tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zBrName); | |
| 1351 | - zThisTag = zBrName; | |
| 1352 | 1482 | style_submenu_element("Branch Only", "only", "%s", |
| 1353 | 1483 | url_render(&url, "t", zBrName, "r", 0)); |
| 1354 | - }else{ | |
| 1355 | - tagid = 0; | |
| 1356 | 1484 | } |
| 1357 | 1485 | if( zMark && zMark[0]==0 ){ |
| 1358 | 1486 | if( zAfter ) zMark = zAfter; |
| 1359 | 1487 | if( zBefore ) zMark = zBefore; |
| 1360 | 1488 | if( zCirca ) zMark = zCirca; |
| 1361 | 1489 | } |
| 1362 | - if( tagid | |
| 1363 | - && db_int(0,"SELECT count(*) FROM tagxref WHERE tagid=%d",tagid)<=nEntry | |
| 1490 | + if( (zTagSql && db_int(0,"SELECT count(*) " | |
| 1491 | + "FROM tagxref NATURAL JOIN tag WHERE %s",zTagSql/*safe-for-%s*/)<=nEntry) | |
| 1364 | 1492 | ){ |
| 1365 | 1493 | nEntry = -1; |
| 1366 | 1494 | zCirca = 0; |
| 1367 | 1495 | } |
| 1368 | 1496 | if( zType[0]=='a' ){ |
| @@ -1587,14 +1715,14 @@ | ||
| 1587 | 1715 | } |
| 1588 | 1716 | else if( zDay ){ |
| 1589 | 1717 | blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m-%%d',event.mtime) ", |
| 1590 | 1718 | zDay); |
| 1591 | 1719 | } |
| 1592 | - if( tagid ){ | |
| 1720 | + if( zTagSql ){ | |
| 1593 | 1721 | blob_append_sql(&cond, |
| 1594 | - " AND (EXISTS(SELECT 1 FROM tagxref" | |
| 1595 | - " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", tagid); | |
| 1722 | + " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag" | |
| 1723 | + " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/); | |
| 1596 | 1724 | |
| 1597 | 1725 | if( zBrName ){ |
| 1598 | 1726 | /* The next two blob_appendf() calls add SQL that causes check-ins that |
| 1599 | 1727 | ** are not part of the branch which are parents or children of the |
| 1600 | 1728 | ** branch to be included in the report. This related check-ins are |
| @@ -1601,12 +1729,12 @@ | ||
| 1601 | 1729 | ** useful in helping to visualize what has happened on a quiescent |
| 1602 | 1730 | ** branch that is infrequently merged with a much more activate branch. |
| 1603 | 1731 | */ |
| 1604 | 1732 | blob_append_sql(&cond, |
| 1605 | 1733 | " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid" |
| 1606 | - " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n", | |
| 1607 | - tagid | |
| 1734 | + " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n", | |
| 1735 | + zTagSql/*safe-for-%s*/ | |
| 1608 | 1736 | ); |
| 1609 | 1737 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1610 | 1738 | blob_append_sql(&cond, |
| 1611 | 1739 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid" |
| 1612 | 1740 | " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n", |
| @@ -1614,12 +1742,12 @@ | ||
| 1614 | 1742 | ); |
| 1615 | 1743 | } |
| 1616 | 1744 | if( P("mionly")==0 ){ |
| 1617 | 1745 | blob_append_sql(&cond, |
| 1618 | 1746 | " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid" |
| 1619 | - " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n", | |
| 1620 | - tagid | |
| 1747 | + " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n", | |
| 1748 | + zTagSql/*safe-for-%s*/ | |
| 1621 | 1749 | ); |
| 1622 | 1750 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1623 | 1751 | blob_append_sql(&cond, |
| 1624 | 1752 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid" |
| 1625 | 1753 | " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n", |
| @@ -1766,15 +1894,33 @@ | ||
| 1766 | 1894 | } |
| 1767 | 1895 | if( zUser ){ |
| 1768 | 1896 | blob_appendf(&desc, " by user %h", zUser); |
| 1769 | 1897 | tmFlags |= TIMELINE_DISJOINT; |
| 1770 | 1898 | } |
| 1771 | - if( zTagName ){ | |
| 1772 | - blob_appendf(&desc, " tagged with \"%h\"", zTagName); | |
| 1773 | - tmFlags |= TIMELINE_DISJOINT; | |
| 1774 | - }else if( zBrName ){ | |
| 1775 | - blob_appendf(&desc, " related to \"%h\"", zBrName); | |
| 1899 | + if( zThisTag ){ | |
| 1900 | + if( matchStyle!=MS_EXACT ){ | |
| 1901 | + if( zTagName ){ | |
| 1902 | + blob_append(&desc, " with tags matching ", -1); | |
| 1903 | + }else{ | |
| 1904 | + blob_append(&desc, " related to tags matching ", -1); | |
| 1905 | + } | |
| 1906 | + if( matchStyle==MS_LIKE ){ | |
| 1907 | + blob_append(&desc, " SQL LIKE pattern", -1); | |
| 1908 | + }else if( matchStyle==MS_GLOB ){ | |
| 1909 | + blob_append(&desc, " glob pattern", -1); | |
| 1910 | + }else/* if( matchStyle==MS_REGEXP )*/{ | |
| 1911 | + blob_append(&desc, " regular expression", -1); | |
| 1912 | + } | |
| 1913 | + if( tagMatchCount!=1 ){ | |
| 1914 | + blob_append(&desc, "s", 1); | |
| 1915 | + } | |
| 1916 | + blob_appendf(&desc, " (%h)", zThisTag); | |
| 1917 | + }else if( zTagName ){ | |
| 1918 | + blob_appendf(&desc, " tagged with \"%h\"", zTagName); | |
| 1919 | + }else{ | |
| 1920 | + blob_appendf(&desc, " related to \"%h\"", zBrName); | |
| 1921 | + } | |
| 1776 | 1922 | tmFlags |= TIMELINE_DISJOINT; |
| 1777 | 1923 | } |
| 1778 | 1924 | addFileGlobDescription(zChng, &desc); |
| 1779 | 1925 | if( rAfter>0.0 ){ |
| 1780 | 1926 | if( rBefore>0.0 ){ |
| 1781 | 1927 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -1218,10 +1218,116 @@ | |
| 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: |
| @@ -1234,10 +1340,11 @@ | |
| 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 | ** u=USER only show items associated with USER |
| 1240 | ** y=TYPE 'ci', 'w', 't', 'e', or (default) 'all' |
| 1241 | ** ng No Graph. |
| 1242 | ** nd Do not highlight the focus check-in |
| 1243 | ** v Show details of files changed |
| @@ -1277,10 +1384,14 @@ | |
| 1277 | const char *zBefore = P("b"); /* Events before this time */ |
| 1278 | const char *zCirca = P("c"); /* Events near this time */ |
| 1279 | const char *zMark = P("m"); /* Mark this event or an event this time */ |
| 1280 | const char *zTagName = P("t"); /* Show events with this tag */ |
| 1281 | const char *zBrName = P("r"); /* Show events related to this tag */ |
| 1282 | const char *zSearch = P("s"); /* Search string */ |
| 1283 | const char *zUses = P("uf"); /* Only show check-ins hold this file */ |
| 1284 | const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ |
| 1285 | const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ |
| 1286 | const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ |
| @@ -1287,11 +1398,10 @@ | |
| 1287 | const char *zChng = P("chng"); /* List of GLOBs for files that changed */ |
| 1288 | int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */ |
| 1289 | int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */ |
| 1290 | int forkOnly = PB("forks"); /* Show only forks and their children */ |
| 1291 | int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */ |
| 1292 | int tagid; /* Tag ID */ |
| 1293 | int tmFlags = 0; /* Timeline flags */ |
| 1294 | const char *zThisTag = 0; /* Suppress links to this tag */ |
| 1295 | const char *zThisUser = 0; /* Suppress links to this user */ |
| 1296 | HQuery url; /* URL for various branch links */ |
| 1297 | int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */ |
| @@ -1339,30 +1449,48 @@ | |
| 1339 | login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
| 1340 | return; |
| 1341 | } |
| 1342 | url_initialize(&url, "timeline"); |
| 1343 | cgi_query_parameters_to_url(&url); |
| 1344 | if( zTagName && g.perm.Read ){ |
| 1345 | tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zTagName); |
| 1346 | zThisTag = zTagName; |
| 1347 | style_submenu_element("Related", "Related", "%s", |
| 1348 | url_render(&url, "r", zTagName, "t", 0)); |
| 1349 | }else if( zBrName && g.perm.Read ){ |
| 1350 | tagid = db_int(-1,"SELECT tagid FROM tag WHERE tagname='sym-%q'",zBrName); |
| 1351 | zThisTag = zBrName; |
| 1352 | style_submenu_element("Branch Only", "only", "%s", |
| 1353 | url_render(&url, "t", zBrName, "r", 0)); |
| 1354 | }else{ |
| 1355 | tagid = 0; |
| 1356 | } |
| 1357 | if( zMark && zMark[0]==0 ){ |
| 1358 | if( zAfter ) zMark = zAfter; |
| 1359 | if( zBefore ) zMark = zBefore; |
| 1360 | if( zCirca ) zMark = zCirca; |
| 1361 | } |
| 1362 | if( tagid |
| 1363 | && db_int(0,"SELECT count(*) FROM tagxref WHERE tagid=%d",tagid)<=nEntry |
| 1364 | ){ |
| 1365 | nEntry = -1; |
| 1366 | zCirca = 0; |
| 1367 | } |
| 1368 | if( zType[0]=='a' ){ |
| @@ -1587,14 +1715,14 @@ | |
| 1587 | } |
| 1588 | else if( zDay ){ |
| 1589 | blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m-%%d',event.mtime) ", |
| 1590 | zDay); |
| 1591 | } |
| 1592 | if( tagid ){ |
| 1593 | blob_append_sql(&cond, |
| 1594 | " AND (EXISTS(SELECT 1 FROM tagxref" |
| 1595 | " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", tagid); |
| 1596 | |
| 1597 | if( zBrName ){ |
| 1598 | /* The next two blob_appendf() calls add SQL that causes check-ins that |
| 1599 | ** are not part of the branch which are parents or children of the |
| 1600 | ** branch to be included in the report. This related check-ins are |
| @@ -1601,12 +1729,12 @@ | |
| 1601 | ** useful in helping to visualize what has happened on a quiescent |
| 1602 | ** branch that is infrequently merged with a much more activate branch. |
| 1603 | */ |
| 1604 | blob_append_sql(&cond, |
| 1605 | " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid" |
| 1606 | " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n", |
| 1607 | tagid |
| 1608 | ); |
| 1609 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1610 | blob_append_sql(&cond, |
| 1611 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid" |
| 1612 | " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n", |
| @@ -1614,12 +1742,12 @@ | |
| 1614 | ); |
| 1615 | } |
| 1616 | if( P("mionly")==0 ){ |
| 1617 | blob_append_sql(&cond, |
| 1618 | " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid" |
| 1619 | " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n", |
| 1620 | tagid |
| 1621 | ); |
| 1622 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1623 | blob_append_sql(&cond, |
| 1624 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid" |
| 1625 | " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n", |
| @@ -1766,15 +1894,33 @@ | |
| 1766 | } |
| 1767 | if( zUser ){ |
| 1768 | blob_appendf(&desc, " by user %h", zUser); |
| 1769 | tmFlags |= TIMELINE_DISJOINT; |
| 1770 | } |
| 1771 | if( zTagName ){ |
| 1772 | blob_appendf(&desc, " tagged with \"%h\"", zTagName); |
| 1773 | tmFlags |= TIMELINE_DISJOINT; |
| 1774 | }else if( zBrName ){ |
| 1775 | blob_appendf(&desc, " related to \"%h\"", zBrName); |
| 1776 | tmFlags |= TIMELINE_DISJOINT; |
| 1777 | } |
| 1778 | addFileGlobDescription(zChng, &desc); |
| 1779 | if( rAfter>0.0 ){ |
| 1780 | if( rBefore>0.0 ){ |
| 1781 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -1218,10 +1218,116 @@ | |
| 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_LIKE, /* Matches tags against a list of LIKE patterns. */ |
| 1229 | MS_GLOB, /* Matches tags against a list of GLOB patterns. */ |
| 1230 | MS_REGEXP /* Matches tags against a list of regular expressions. */ |
| 1231 | } MatchStyle; |
| 1232 | |
| 1233 | /* |
| 1234 | ** Construct the tag match expression. |
| 1235 | */ |
| 1236 | static const char *tagMatchExpression( |
| 1237 | MatchStyle matchStyle, /* Match style code */ |
| 1238 | const char *zTag, /* Tag name, match pattern, or list of patterns */ |
| 1239 | int *pCount /* Pointer to match pattern count variable */ |
| 1240 | ){ |
| 1241 | Blob blob = BLOB_INITIALIZER; |
| 1242 | const char *zSep = "(", *zPre, *zSuf; |
| 1243 | char cDel; |
| 1244 | int i, dummy; |
| 1245 | |
| 1246 | /* Protect against NULL count pointer. */ |
| 1247 | if( !pCount ){ |
| 1248 | pCount = &dummy; |
| 1249 | } |
| 1250 | |
| 1251 | /* Decide pattern prefix and suffix strings according to match style. */ |
| 1252 | if( matchStyle==MS_EXACT ){ |
| 1253 | /* Optimize exact matches by looking up the numeric ID in advance. Bypass |
| 1254 | * the remainder of this function. */ |
| 1255 | *pCount = 1; |
| 1256 | return mprintf("(tagid=%d)", db_int(-1, |
| 1257 | "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTag)); |
| 1258 | }else if( matchStyle==MS_LIKE ){ |
| 1259 | zPre = "LIKE 'sym-"; |
| 1260 | zSuf = "'"; |
| 1261 | }else if( matchStyle==MS_GLOB ){ |
| 1262 | zPre = "GLOB 'sym-"; |
| 1263 | zSuf = "'"; |
| 1264 | }else/* if( matchStyle==MS_REGEXP )*/{ |
| 1265 | zPre = "REGEXP '^sym-"; |
| 1266 | zSuf = "$'"; |
| 1267 | } |
| 1268 | |
| 1269 | /* The following code is glob_expr() modified to support LIKE and REGEXP, plus |
| 1270 | * adjust each pattern to start with "sym-" and be anchored at end. In REGEXP |
| 1271 | * mode, it allows backslash to protect delimiter characters. */ |
| 1272 | |
| 1273 | /* Convert the list of matches into an SQL expression. */ |
| 1274 | *pCount = 0; |
| 1275 | blob_zero(&blob); |
| 1276 | while( 1 ){ |
| 1277 | /* Skip leading delimiters. */ |
| 1278 | for( ; fossil_isspace(*zTag) || *zTag==','; ++zTag ); |
| 1279 | |
| 1280 | /* Next non-delimiter character determines quoting. */ |
| 1281 | if( !*zTag ){ |
| 1282 | /* Terminate loop at end of string. */ |
| 1283 | break; |
| 1284 | }else if( *zTag=='\'' || *zTag=='"' ){ |
| 1285 | /* If word is quoted, prepare to stop at end quote. */ |
| 1286 | cDel = *zTag; |
| 1287 | ++zTag; |
| 1288 | }else{ |
| 1289 | /* If word is not quoted, prepare to stop at delimiter. */ |
| 1290 | cDel = ','; |
| 1291 | } |
| 1292 | |
| 1293 | /* Find the next delimiter character or end of string. */ |
| 1294 | for( i=0; zTag[i] && zTag[i]!=cDel; ++i ){ |
| 1295 | /* If delimiter is comma, also recognize spaces as delimiters. */ |
| 1296 | if( cDel==',' && fossil_isspace(zTag[i]) ){ |
| 1297 | break; |
| 1298 | } |
| 1299 | |
| 1300 | /* In regexp mode, ignore delimiters following backslashes. */ |
| 1301 | if( matchStyle==MS_REGEXP && zTag[i]=='\\' && zTag[i+1] ){ |
| 1302 | ++i; |
| 1303 | } |
| 1304 | } |
| 1305 | |
| 1306 | /* Incorporate the match word into the final expression. */ |
| 1307 | blob_appendf(&blob, "%stagname %s%#q%s", zSep, zPre, i, zTag, zSuf); |
| 1308 | |
| 1309 | /* Keep track of the number of match expressions. */ |
| 1310 | ++*pCount; |
| 1311 | |
| 1312 | /* Prepare for the next match word. */ |
| 1313 | zTag += i; |
| 1314 | if( cDel!=',' && *zTag==cDel ){ |
| 1315 | ++zTag; |
| 1316 | } |
| 1317 | zSep = " OR "; |
| 1318 | } |
| 1319 | |
| 1320 | /* Finalize and extract the SQL expression. */ |
| 1321 | if( *pCount ){ |
| 1322 | blob_append(&blob, ")", 1); |
| 1323 | return blob_str(&blob); |
| 1324 | } |
| 1325 | |
| 1326 | /* If execution reaches this point, the pattern was empty. Return NULL. */ |
| 1327 | return 0; |
| 1328 | } |
| 1329 | |
| 1330 | /* |
| 1331 | ** WEBPAGE: timeline |
| 1332 | ** |
| 1333 | ** Query parameters: |
| @@ -1234,10 +1340,11 @@ | |
| 1340 | ** p=CHECKIN parents and ancestors of CHECKIN |
| 1341 | ** d=CHECKIN descendants of CHECIN |
| 1342 | ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN |
| 1343 | ** t=TAG show only check-ins with the given TAG |
| 1344 | ** r=TAG show check-ins related to TAG |
| 1345 | ** ms=STYLE sets tag match style to EXACT, LIKE, GLOB, REGEXP |
| 1346 | ** u=USER only show items associated with USER |
| 1347 | ** y=TYPE 'ci', 'w', 't', 'e', or (default) 'all' |
| 1348 | ** ng No Graph. |
| 1349 | ** nd Do not highlight the focus check-in |
| 1350 | ** v Show details of files changed |
| @@ -1277,10 +1384,14 @@ | |
| 1384 | const char *zBefore = P("b"); /* Events before this time */ |
| 1385 | const char *zCirca = P("c"); /* Events near this time */ |
| 1386 | const char *zMark = P("m"); /* Mark this event or an event this time */ |
| 1387 | const char *zTagName = P("t"); /* Show events with this tag */ |
| 1388 | const char *zBrName = P("r"); /* Show events related to this tag */ |
| 1389 | const char *zMatchStyle = P("ms"); /* Tag/branch match style string */ |
| 1390 | MatchStyle matchStyle = MS_EXACT; /* Match style code */ |
| 1391 | const char *zTagSql = 0; /* Tag/branch match SQL expression */ |
| 1392 | int tagMatchCount = 0; /* Number of tag match patterns */ |
| 1393 | const char *zSearch = P("s"); /* Search string */ |
| 1394 | const char *zUses = P("uf"); /* Only show check-ins hold this file */ |
| 1395 | const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ |
| 1396 | const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ |
| 1397 | const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ |
| @@ -1287,11 +1398,10 @@ | |
| 1398 | const char *zChng = P("chng"); /* List of GLOBs for files that changed */ |
| 1399 | int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */ |
| 1400 | int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */ |
| 1401 | int forkOnly = PB("forks"); /* Show only forks and their children */ |
| 1402 | int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */ |
| 1403 | int tmFlags = 0; /* Timeline flags */ |
| 1404 | const char *zThisTag = 0; /* Suppress links to this tag */ |
| 1405 | const char *zThisUser = 0; /* Suppress links to this user */ |
| 1406 | HQuery url; /* URL for various branch links */ |
| 1407 | int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */ |
| @@ -1339,30 +1449,48 @@ | |
| 1449 | login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
| 1450 | return; |
| 1451 | } |
| 1452 | url_initialize(&url, "timeline"); |
| 1453 | cgi_query_parameters_to_url(&url); |
| 1454 | |
| 1455 | /* Identify the tag or branch name or match pattern. */ |
| 1456 | if( zTagName ){ |
| 1457 | zThisTag = zTagName; |
| 1458 | }else if( zBrName ){ |
| 1459 | zThisTag = zBrName; |
| 1460 | } |
| 1461 | |
| 1462 | /* Interpet the tag style string. */ |
| 1463 | if( zThisTag ){ |
| 1464 | if( fossil_stricmp(zMatchStyle, "LIKE")==0 ){ |
| 1465 | matchStyle = MS_LIKE; |
| 1466 | }else if( fossil_stricmp(zMatchStyle, "GLOB")==0 ){ |
| 1467 | matchStyle = MS_GLOB; |
| 1468 | }else if( fossil_stricmp(zMatchStyle, "REGEXP")==0 ){ |
| 1469 | matchStyle = MS_REGEXP; |
| 1470 | } |
| 1471 | } |
| 1472 | |
| 1473 | /* Construct the tag match expression. */ |
| 1474 | if( zThisTag ){ |
| 1475 | zTagSql = tagMatchExpression(matchStyle, zThisTag, &tagMatchCount); |
| 1476 | } |
| 1477 | |
| 1478 | if( zTagName && g.perm.Read ){ |
| 1479 | style_submenu_element("Related", "Related", "%s", |
| 1480 | url_render(&url, "r", zTagName, "t", 0)); |
| 1481 | }else if( zBrName && g.perm.Read ){ |
| 1482 | style_submenu_element("Branch Only", "only", "%s", |
| 1483 | url_render(&url, "t", zBrName, "r", 0)); |
| 1484 | } |
| 1485 | if( zMark && zMark[0]==0 ){ |
| 1486 | if( zAfter ) zMark = zAfter; |
| 1487 | if( zBefore ) zMark = zBefore; |
| 1488 | if( zCirca ) zMark = zCirca; |
| 1489 | } |
| 1490 | if( (zTagSql && db_int(0,"SELECT count(*) " |
| 1491 | "FROM tagxref NATURAL JOIN tag WHERE %s",zTagSql/*safe-for-%s*/)<=nEntry) |
| 1492 | ){ |
| 1493 | nEntry = -1; |
| 1494 | zCirca = 0; |
| 1495 | } |
| 1496 | if( zType[0]=='a' ){ |
| @@ -1587,14 +1715,14 @@ | |
| 1715 | } |
| 1716 | else if( zDay ){ |
| 1717 | blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m-%%d',event.mtime) ", |
| 1718 | zDay); |
| 1719 | } |
| 1720 | if( zTagSql ){ |
| 1721 | blob_append_sql(&cond, |
| 1722 | " AND (EXISTS(SELECT 1 FROM tagxref NATURAL JOIN tag" |
| 1723 | " WHERE %s AND tagtype>0 AND rid=blob.rid)\n", zTagSql/*safe-for-%s*/); |
| 1724 | |
| 1725 | if( zBrName ){ |
| 1726 | /* The next two blob_appendf() calls add SQL that causes check-ins that |
| 1727 | ** are not part of the branch which are parents or children of the |
| 1728 | ** branch to be included in the report. This related check-ins are |
| @@ -1601,12 +1729,12 @@ | |
| 1729 | ** useful in helping to visualize what has happened on a quiescent |
| 1730 | ** branch that is infrequently merged with a much more activate branch. |
| 1731 | */ |
| 1732 | blob_append_sql(&cond, |
| 1733 | " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=cid" |
| 1734 | " NATURAL JOIN tag WHERE %s AND tagtype>0 AND pid=blob.rid)\n", |
| 1735 | zTagSql/*safe-for-%s*/ |
| 1736 | ); |
| 1737 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1738 | blob_append_sql(&cond, |
| 1739 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid" |
| 1740 | " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n", |
| @@ -1614,12 +1742,12 @@ | |
| 1742 | ); |
| 1743 | } |
| 1744 | if( P("mionly")==0 ){ |
| 1745 | blob_append_sql(&cond, |
| 1746 | " OR EXISTS(SELECT 1 FROM plink CROSS JOIN tagxref ON rid=pid" |
| 1747 | " NATURAL JOIN tag WHERE %s AND tagtype>0 AND cid=blob.rid)\n", |
| 1748 | zTagSql/*safe-for-%s*/ |
| 1749 | ); |
| 1750 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1751 | blob_append_sql(&cond, |
| 1752 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid" |
| 1753 | " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n", |
| @@ -1766,15 +1894,33 @@ | |
| 1894 | } |
| 1895 | if( zUser ){ |
| 1896 | blob_appendf(&desc, " by user %h", zUser); |
| 1897 | tmFlags |= TIMELINE_DISJOINT; |
| 1898 | } |
| 1899 | if( zThisTag ){ |
| 1900 | if( matchStyle!=MS_EXACT ){ |
| 1901 | if( zTagName ){ |
| 1902 | blob_append(&desc, " with tags matching ", -1); |
| 1903 | }else{ |
| 1904 | blob_append(&desc, " related to tags matching ", -1); |
| 1905 | } |
| 1906 | if( matchStyle==MS_LIKE ){ |
| 1907 | blob_append(&desc, " SQL LIKE pattern", -1); |
| 1908 | }else if( matchStyle==MS_GLOB ){ |
| 1909 | blob_append(&desc, " glob pattern", -1); |
| 1910 | }else/* if( matchStyle==MS_REGEXP )*/{ |
| 1911 | blob_append(&desc, " regular expression", -1); |
| 1912 | } |
| 1913 | if( tagMatchCount!=1 ){ |
| 1914 | blob_append(&desc, "s", 1); |
| 1915 | } |
| 1916 | blob_appendf(&desc, " (%h)", zThisTag); |
| 1917 | }else if( zTagName ){ |
| 1918 | blob_appendf(&desc, " tagged with \"%h\"", zTagName); |
| 1919 | }else{ |
| 1920 | blob_appendf(&desc, " related to \"%h\"", zBrName); |
| 1921 | } |
| 1922 | tmFlags |= TIMELINE_DISJOINT; |
| 1923 | } |
| 1924 | addFileGlobDescription(zChng, &desc); |
| 1925 | if( rAfter>0.0 ){ |
| 1926 | if( rBefore>0.0 ){ |
| 1927 |
+2
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -26,10 +26,12 @@ | ||
| 26 | 26 | queries are filled in using HTTP query parameter values. |
| 27 | 27 | * Added support for [./childprojects.wiki|child projects] that are |
| 28 | 28 | able to pull from their parent but not push. |
| 29 | 29 | * Added the -nocomplain option to the TH1 "query" command. |
| 30 | 30 | * Added support for the chng=GLOBLIST query parameter on the |
| 31 | + [/help?cmd=/timeline|/timeline] webpage. | |
| 32 | + * Added support for the ms=EXACT|LIKE|GLOB|REGEXP query parameter on the | |
| 31 | 33 | [/help?cmd=/timeline|/timeline] webpage. |
| 32 | 34 | |
| 33 | 35 | <h2>Changes for Version 1.35 (2016-06-14)</h2> |
| 34 | 36 | |
| 35 | 37 | * Enable symlinks by default on all non-Windows platforms. |
| 36 | 38 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -26,10 +26,12 @@ | |
| 26 | queries are filled in using HTTP query parameter values. |
| 27 | * Added support for [./childprojects.wiki|child projects] that are |
| 28 | able to pull from their parent but not push. |
| 29 | * Added the -nocomplain option to the TH1 "query" command. |
| 30 | * Added support for the chng=GLOBLIST query parameter on the |
| 31 | [/help?cmd=/timeline|/timeline] webpage. |
| 32 | |
| 33 | <h2>Changes for Version 1.35 (2016-06-14)</h2> |
| 34 | |
| 35 | * Enable symlinks by default on all non-Windows platforms. |
| 36 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -26,10 +26,12 @@ | |
| 26 | queries are filled in using HTTP query parameter values. |
| 27 | * Added support for [./childprojects.wiki|child projects] that are |
| 28 | able to pull from their parent but not push. |
| 29 | * Added the -nocomplain option to the TH1 "query" command. |
| 30 | * Added support for the chng=GLOBLIST query parameter on the |
| 31 | [/help?cmd=/timeline|/timeline] webpage. |
| 32 | * Added support for the ms=EXACT|LIKE|GLOB|REGEXP query parameter on the |
| 33 | [/help?cmd=/timeline|/timeline] webpage. |
| 34 | |
| 35 | <h2>Changes for Version 1.35 (2016-06-14)</h2> |
| 36 | |
| 37 | * Enable symlinks by default on all non-Windows platforms. |
| 38 |