| | @@ -281,11 +281,11 @@ |
| 281 | 281 | char zTime[20]; |
| 282 | 282 | |
| 283 | 283 | if( zDate==0 ){ |
| 284 | 284 | zDate = "YYYY-MM-DD HH:MM:SS"; /* Something wrong with the repo */ |
| 285 | 285 | } |
| 286 | | - modPending = moderation_pending(rid); |
| 286 | + modPending = moderation_pending(rid); |
| 287 | 287 | if( tagid ){ |
| 288 | 288 | if( modPending ) tagid = -tagid; |
| 289 | 289 | if( tagid==prevTagid ){ |
| 290 | 290 | if( tmFlags & TIMELINE_BRIEF ){ |
| 291 | 291 | suppressCnt++; |
| | @@ -573,11 +573,11 @@ |
| 573 | 573 | } |
| 574 | 574 | continue; |
| 575 | 575 | } |
| 576 | 576 | zA = href("%R/artifact/%!S",fid?zNew:zOld); |
| 577 | 577 | if( content_is_private(fid) ){ |
| 578 | | - zUnpub = UNPUB_TAG; |
| 578 | + zUnpub = UNPUB_TAG; |
| 579 | 579 | } |
| 580 | 580 | if( isNew ){ |
| 581 | 581 | @ <li> %s(zA)%h(zFilename)</a>%s(zId) %s(zUnpub) |
| 582 | 582 | if( isMergeNew ){ |
| 583 | 583 | @ (added by merge) |
| | @@ -1218,10 +1218,225 @@ |
| 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_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(®exp, 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 | +} |
| 1223 | 1438 | |
| 1224 | 1439 | /* |
| 1225 | 1440 | ** WEBPAGE: timeline |
| 1226 | 1441 | ** |
| 1227 | 1442 | ** Query parameters: |
| | @@ -1233,12 +1448,14 @@ |
| 1233 | 1448 | ** n=COUNT Suggested number of events in output |
| 1234 | 1449 | ** p=CHECKIN Parents and ancestors of CHECKIN |
| 1235 | 1450 | ** d=CHECKIN Descendants of CHECIN |
| 1236 | 1451 | ** dp=CHECKIN The same as d=CHECKIN&p=CHECKIN |
| 1237 | 1452 | ** 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 |
| 1240 | 1457 | ** u=USER Only show items associated with USER |
| 1241 | 1458 | ** y=TYPE 'ci', 'w', 't', 'e', or (default) 'all' |
| 1242 | 1459 | ** ng No Graph. |
| 1243 | 1460 | ** nd Do not highlight the focus check-in |
| 1244 | 1461 | ** v Show details of files changed |
| | @@ -1279,11 +1496,17 @@ |
| 1279 | 1496 | const char *zAfter = P("a"); /* Events after this time */ |
| 1280 | 1497 | const char *zBefore = P("b"); /* Events before this time */ |
| 1281 | 1498 | const char *zCirca = P("c"); /* Events near this time */ |
| 1282 | 1499 | const char *zMark = P("m"); /* Mark this event or an event this time */ |
| 1283 | 1500 | 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 */ |
| 1285 | 1508 | const char *zSearch = P("s"); /* Search string */ |
| 1286 | 1509 | const char *zUses = P("uf"); /* Only show check-ins hold this file */ |
| 1287 | 1510 | const char *zYearMonth = P("ym"); /* Show check-ins for the given YYYY-MM */ |
| 1288 | 1511 | const char *zYearWeek = P("yw"); /* Check-ins for YYYY-WW (week-of-year) */ |
| 1289 | 1512 | const char *zDay = P("ymd"); /* Check-ins for the day YYYY-MM-DD */ |
| | @@ -1290,11 +1513,10 @@ |
| 1290 | 1513 | const char *zChng = P("chng"); /* List of GLOBs for files that changed */ |
| 1291 | 1514 | int useDividers = P("nd")==0; /* Show dividers if "nd" is missing */ |
| 1292 | 1515 | int renameOnly = P("namechng")!=0; /* Show only check-ins that rename files */ |
| 1293 | 1516 | int forkOnly = PB("forks"); /* Show only forks and their children */ |
| 1294 | 1517 | int bisectOnly = PB("bisect"); /* Show the check-ins of the bisect */ |
| 1295 | | - int tagid; /* Tag ID */ |
| 1296 | 1518 | int tmFlags = 0; /* Timeline flags */ |
| 1297 | 1519 | const char *zThisTag = 0; /* Suppress links to this tag */ |
| 1298 | 1520 | const char *zThisUser = 0; /* Suppress links to this user */ |
| 1299 | 1521 | HQuery url; /* URL for various branch links */ |
| 1300 | 1522 | int from_rid = name_to_typed_rid(P("from"),"ci"); /* from= for paths */ |
| | @@ -1342,28 +1564,53 @@ |
| 1342 | 1564 | login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
| 1343 | 1565 | return; |
| 1344 | 1566 | } |
| 1345 | 1567 | url_initialize(&url, "timeline"); |
| 1346 | 1568 | 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 | + |
| 1358 | 1605 | if( zMark && zMark[0]==0 ){ |
| 1359 | 1606 | if( zAfter ) zMark = zAfter; |
| 1360 | 1607 | if( zBefore ) zMark = zBefore; |
| 1361 | 1608 | if( zCirca ) zMark = zCirca; |
| 1362 | 1609 | } |
| 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) |
| 1365 | 1612 | ){ |
| 1366 | 1613 | nEntry = -1; |
| 1367 | 1614 | zCirca = 0; |
| 1368 | 1615 | } |
| 1369 | 1616 | if( zType[0]=='a' ){ |
| | @@ -1583,26 +1830,26 @@ |
| 1583 | 1830 | } |
| 1584 | 1831 | else if( zDay ){ |
| 1585 | 1832 | blob_append_sql(&cond, " AND %Q=strftime('%%Y-%%m-%%d',event.mtime) ", |
| 1586 | 1833 | zDay); |
| 1587 | 1834 | } |
| 1588 | | - if( tagid ){ |
| 1835 | + if( zTagSql ){ |
| 1589 | 1836 | 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*/); |
| 1592 | 1839 | |
| 1593 | | - if( zBrName ){ |
| 1840 | + if( related ){ |
| 1594 | 1841 | /* The next two blob_appendf() calls add SQL that causes check-ins that |
| 1595 | 1842 | ** are not part of the branch which are parents or children of the |
| 1596 | 1843 | ** branch to be included in the report. This related check-ins are |
| 1597 | 1844 | ** useful in helping to visualize what has happened on a quiescent |
| 1598 | 1845 | ** branch that is infrequently merged with a much more activate branch. |
| 1599 | 1846 | */ |
| 1600 | 1847 | blob_append_sql(&cond, |
| 1601 | 1848 | " 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*/ |
| 1604 | 1851 | ); |
| 1605 | 1852 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1606 | 1853 | blob_append_sql(&cond, |
| 1607 | 1854 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid" |
| 1608 | 1855 | " WHERE tagid=%d AND tagtype>0 AND pid=blob.rid)\n", |
| | @@ -1610,12 +1857,12 @@ |
| 1610 | 1857 | ); |
| 1611 | 1858 | } |
| 1612 | 1859 | if( P("mionly")==0 ){ |
| 1613 | 1860 | blob_append_sql(&cond, |
| 1614 | 1861 | " 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*/ |
| 1617 | 1864 | ); |
| 1618 | 1865 | if( (tmFlags & TIMELINE_UNHIDE)==0 ){ |
| 1619 | 1866 | blob_append_sql(&cond, |
| 1620 | 1867 | " AND NOT EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid" |
| 1621 | 1868 | " WHERE tagid=%d AND tagtype>0 AND cid=blob.rid)\n", |
| | @@ -1762,15 +2009,24 @@ |
| 1762 | 2009 | } |
| 1763 | 2010 | if( zUser ){ |
| 1764 | 2011 | blob_appendf(&desc, " by user %h", zUser); |
| 1765 | 2012 | tmFlags |= TIMELINE_DISJOINT; |
| 1766 | 2013 | } |
| 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 | + } |
| 1772 | 2028 | tmFlags |= TIMELINE_DISJOINT; |
| 1773 | 2029 | } |
| 1774 | 2030 | addFileGlobDescription(zChng, &desc); |
| 1775 | 2031 | if( rAfter>0.0 ){ |
| 1776 | 2032 | if( rBefore>0.0 ){ |
| | @@ -1786,10 +2042,13 @@ |
| 1786 | 2042 | } |
| 1787 | 2043 | if( zSearch ){ |
| 1788 | 2044 | blob_appendf(&desc, " matching \"%h\"", zSearch); |
| 1789 | 2045 | } |
| 1790 | 2046 | if( g.perm.Hyperlink ){ |
| 2047 | + static const char *const azMatchStyles[] = { |
| 2048 | + "exact", "Exact", "glob", "Glob", "like", "Like", "regexp", "Regexp" |
| 2049 | + }; |
| 1791 | 2050 | double rDate; |
| 1792 | 2051 | zDate = db_text(0, "SELECT min(timestamp) FROM timeline /*scan*/"); |
| 1793 | 2052 | if( (!zDate || !zDate[0]) && ( zAfter || zBefore ) ){ |
| 1794 | 2053 | zDate = mprintf("%s", (zAfter ? zAfter : zBefore)); |
| 1795 | 2054 | } |
| | @@ -1824,10 +2083,12 @@ |
| 1824 | 2083 | style_submenu_checkbox("unhide", "Unhide", 0); |
| 1825 | 2084 | } |
| 1826 | 2085 | style_submenu_checkbox("v", "Files", zType[0]!='a' && zType[0]!='c'); |
| 1827 | 2086 | style_submenu_entry("n","Max:",4,0); |
| 1828 | 2087 | timeline_y_submenu(disableY); |
| 2088 | + style_submenu_entry("t", "Tag Filter:", -8, 0); |
| 2089 | + style_submenu_multichoice("ms", count(azMatchStyles)/2, azMatchStyles, 0); |
| 1829 | 2090 | } |
| 1830 | 2091 | blob_zero(&cond); |
| 1831 | 2092 | } |
| 1832 | 2093 | if( PB("showsql") ){ |
| 1833 | 2094 | @ <pre>%h(blob_sql_text(&sql))</pre> |
| | @@ -1842,10 +2103,16 @@ |
| 1842 | 2103 | } |
| 1843 | 2104 | blob_zero(&sql); |
| 1844 | 2105 | db_prepare(&q, "SELECT * FROM timeline ORDER BY sortby DESC /*scan*/"); |
| 1845 | 2106 | @ <h2>%b(&desc)</h2> |
| 1846 | 2107 | blob_reset(&desc); |
| 2108 | + |
| 2109 | + /* Report any errors. */ |
| 2110 | + if( zError ){ |
| 2111 | + @ <p class="generalError">%h(zError)</p> |
| 2112 | + } |
| 2113 | + |
| 1847 | 2114 | www_print_timeline(&q, tmFlags, zThisUser, zThisTag, selectedRid, 0); |
| 1848 | 2115 | db_finalize(&q); |
| 1849 | 2116 | if( zOlderButton ){ |
| 1850 | 2117 | @ %z(xhref("class='button'","%z",zOlderButton))Older</a> |
| 1851 | 2118 | } |
| 1852 | 2119 | |