Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1//! A deliberately small revset subset (spec §8).
Matt W2//!
Matt W3//! > Do not attempt to implement jj's full revset language against a Git-backed
Matt W4//! > index; it will not be faithful, and a subtly wrong revset is worse than an
Matt W5//! > honestly limited one. Show a clear "unsupported expression" message rather
Matt W6//! > than silently returning partial results.
Matt W7//!
Matt W8//! Supported: `author()`, `description()`, `conflicted()`, `bookmarks()`,
Matt W9//! `open()`, and the operators `&`, `|`, `~`.
Matt W10//!
Matt W11//! `docs/revset-semantics.md` records the trap this parser is built around: in
Matt W12//! jj, `description("x")` is an **exact** match, not a substring match. The
Matt W13//! obvious `ILIKE '%x%'` translation returns *more* rows than the CLI would, so
Matt W14//! the bare form is rejected and `description(substring:"x")` is required to
Matt W15//! ask for substring matching.
Matt W16
Matt W17#[derive(Debug, Clone, PartialEq, Eq)]
Matt W18pub enum Expr {
Matt W19 Author(String),
Matt W20 /// Exact match on the full description.
Matt W21 DescriptionExact(String),
Matt W22 /// Substring match, only via the explicit `substring:` prefix.
Matt W23 DescriptionSubstring(String),
Matt W24 Conflicted,
Matt W25 Open,
Matt W26 Bookmark(String),
Matt W27 And(Box<Expr>, Box<Expr>),
Matt W28 Or(Box<Expr>, Box<Expr>),
Matt W29 Not(Box<Expr>),
Matt W30}
Matt W31
Matt W32#[derive(Debug, Clone, PartialEq, Eq)]
Matt W33pub struct RevsetError(pub String);
Matt W34
Matt W35impl std::fmt::Display for RevsetError {
Matt W36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Matt W37 f.write_str(&self.0)
Matt W38 }
Matt W39}
Matt W40
Matt W41/// Functions jj has that this subset deliberately does not implement.
Matt W42///
Matt W43/// Named explicitly so the message says *which* function is unsupported rather
Matt W44/// than producing a generic syntax error.
Matt W45const KNOWN_UNSUPPORTED: &[&str] = &[
Matt W46 "heads", "roots", "ancestors", "descendants", "parents", "children",
Matt W47 "latest", "merges", "file", "diff_contains", "empty", "tags", "git_refs",
Matt W48 "visible_heads", "reachable", "connected", "mine", "trunk", "present",
Matt W49];
Matt W50
Matt W51pub fn parse(input: &str) -> Result<Option<Expr>, RevsetError> {
Matt W52 let trimmed = input.trim();
Matt W53 if trimmed.is_empty() {
Matt W54 return Ok(None);
Matt W55 }
Matt W56 if trimmed.len() > 500 {
Matt W57 return Err(RevsetError("Expression is too long.".into()));
Matt W58 }
Matt W59
Matt W60 let mut p = Parser {
Matt W61 s: trimmed.as_bytes(),
Matt W62 i: 0,
Matt W63 };
Matt W64 let e = p.parse_or()?;
Matt W65 p.skip_ws();
Matt W66 if p.i < p.s.len() {
Matt W67 return Err(RevsetError(format!(
Matt W68 "Unexpected input at position {}.",
Matt W69 p.i
Matt W70 )));
Matt W71 }
Matt W72 Ok(Some(e))
Matt W73}
Matt W74
Matt W75struct Parser<'a> {
Matt W76 s: &'a [u8],
Matt W77 i: usize,
Matt W78}
Matt W79
Matt W80impl<'a> Parser<'a> {
Matt W81 fn skip_ws(&mut self) {
Matt W82 while self.i < self.s.len() && self.s[self.i].is_ascii_whitespace() {
Matt W83 self.i += 1;
Matt W84 }
Matt W85 }
Matt W86
Matt W87 fn peek(&mut self) -> Option<u8> {
Matt W88 self.skip_ws();
Matt W89 self.s.get(self.i).copied()
Matt W90 }
Matt W91
Matt W92 fn parse_or(&mut self) -> Result<Expr, RevsetError> {
Matt W93 let mut left = self.parse_and()?;
Matt W94 while self.peek() == Some(b'|') {
Matt W95 self.i += 1;
Matt W96 let right = self.parse_and()?;
Matt W97 left = Expr::Or(Box::new(left), Box::new(right));
Matt W98 }
Matt W99 Ok(left)
Matt W100 }
Matt W101
Matt W102 fn parse_and(&mut self) -> Result<Expr, RevsetError> {
Matt W103 let mut left = self.parse_unary()?;
Matt W104 while self.peek() == Some(b'&') {
Matt W105 self.i += 1;
Matt W106 let right = self.parse_unary()?;
Matt W107 left = Expr::And(Box::new(left), Box::new(right));
Matt W108 }
Matt W109 Ok(left)
Matt W110 }
Matt W111
Matt W112 fn parse_unary(&mut self) -> Result<Expr, RevsetError> {
Matt W113 if self.peek() == Some(b'~') {
Matt W114 self.i += 1;
Matt W115 return Ok(Expr::Not(Box::new(self.parse_unary()?)));
Matt W116 }
Matt W117 self.parse_atom()
Matt W118 }
Matt W119
Matt W120 fn parse_atom(&mut self) -> Result<Expr, RevsetError> {
Matt W121 match self.peek() {
Matt W122 Some(b'(') => {
Matt W123 self.i += 1;
Matt W124 let e = self.parse_or()?;
Matt W125 if self.peek() != Some(b')') {
Matt W126 return Err(RevsetError("Missing closing parenthesis.".into()));
Matt W127 }
Matt W128 self.i += 1;
Matt W129 Ok(e)
Matt W130 }
Matt W131 Some(c) if c.is_ascii_alphabetic() || c == b'_' => self.parse_call(),
Matt W132 Some(c) => Err(RevsetError(format!(
Matt W133 "Unexpected character '{}'.",
Matt W134 c as char
Matt W135 ))),
Matt W136 None => Err(RevsetError("Unexpected end of expression.".into())),
Matt W137 }
Matt W138 }
Matt W139
Matt W140 fn parse_call(&mut self) -> Result<Expr, RevsetError> {
Matt W141 self.skip_ws();
Matt W142 let start = self.i;
Matt W143 while self.i < self.s.len()
Matt W144 && (self.s[self.i].is_ascii_alphanumeric() || self.s[self.i] == b'_')
Matt W145 {
Matt W146 self.i += 1;
Matt W147 }
Matt W148 let name = std::str::from_utf8(&self.s[start..self.i])
Matt W149 .map_err(|_| RevsetError("Invalid characters in function name.".into()))?
Matt W150 .to_string();
Matt W151
Matt W152 if self.peek() != Some(b'(') {
Matt W153 return Err(RevsetError(format!(
Matt W154 "`{name}` is not a value. This subset supports only function calls \
Matt W155 such as author(…), description(…), conflicted(), open(), bookmarks(…)."
Matt W156 )));
Matt W157 }
Matt W158 self.i += 1;
Matt W159
Matt W160 let arg = self.parse_arg()?;
Matt W161
Matt W162 if self.peek() != Some(b')') {
Matt W163 return Err(RevsetError(format!("Missing closing parenthesis after `{name}(`.")));
Matt W164 }
Matt W165 self.i += 1;
Matt W166
Matt W167 build(&name, arg)
Matt W168 }
Matt W169
Matt W170 /// Read a function argument: a quoted string, a `prefix:"value"` form, or a
Matt W171 /// bare word.
Matt W172 fn parse_arg(&mut self) -> Result<String, RevsetError> {
Matt W173 self.skip_ws();
Matt W174 if self.peek() == Some(b')') {
Matt W175 return Ok(String::new());
Matt W176 }
Matt W177
Matt W178 let mut out = String::new();
Matt W179 // Optional `substring:` / `exact:` prefix, kept verbatim for `build`.
Matt W180 let start = self.i;
Matt W181 while self.i < self.s.len()
Matt W182 && (self.s[self.i].is_ascii_alphanumeric() || self.s[self.i] == b'_')
Matt W183 {
Matt W184 self.i += 1;
Matt W185 }
Matt W186 if self.i < self.s.len() && self.s[self.i] == b':' {
Matt W187 out.push_str(
Matt W188 std::str::from_utf8(&self.s[start..self.i])
Matt W189 .map_err(|_| RevsetError("Invalid argument.".into()))?,
Matt W190 );
Matt W191 out.push(':');
Matt W192 self.i += 1;
Matt W193 } else {
Matt W194 self.i = start;
Matt W195 }
Matt W196
Matt W197 self.skip_ws();
Matt W198 match self.s.get(self.i) {
Matt W199 Some(b'"') | Some(b'\'') => {
Matt W200 let quote = self.s[self.i];
Matt W201 self.i += 1;
Matt W202 let vstart = self.i;
Matt W203 while self.i < self.s.len() && self.s[self.i] != quote {
Matt W204 self.i += 1;
Matt W205 }
Matt W206 if self.i >= self.s.len() {
Matt W207 return Err(RevsetError("Unterminated string.".into()));
Matt W208 }
Matt W209 out.push_str(
Matt W210 std::str::from_utf8(&self.s[vstart..self.i])
Matt W211 .map_err(|_| RevsetError("Invalid string.".into()))?,
Matt W212 );
Matt W213 self.i += 1;
Matt W214 }
Matt W215 _ => {
Matt W216 let vstart = self.i;
Matt W217 while self.i < self.s.len()
Matt W218 && self.s[self.i] != b')'
Matt W219 && self.s[self.i] != b'&'
Matt W220 && self.s[self.i] != b'|'
Matt W221 {
Matt W222 self.i += 1;
Matt W223 }
Matt W224 out.push_str(
Matt W225 std::str::from_utf8(&self.s[vstart..self.i])
Matt W226 .map_err(|_| RevsetError("Invalid argument.".into()))?
Matt W227 .trim(),
Matt W228 );
Matt W229 }
Matt W230 }
Matt W231 Ok(out)
Matt W232 }
Matt W233}
Matt W234
Matt W235fn build(name: &str, arg: String) -> Result<Expr, RevsetError> {
Matt W236 match name {
Matt W237 "author" => {
Matt W238 if arg.is_empty() {
Matt W239 return Err(RevsetError("author() needs an argument.".into()));
Matt W240 }
Matt W241 Ok(Expr::Author(strip_prefix(&arg)))
Matt W242 }
Matt W243 "description" | "subject" => {
Matt W244 if let Some(v) = arg.strip_prefix("substring:") {
Matt W245 Ok(Expr::DescriptionSubstring(v.to_string()))
Matt W246 } else if let Some(v) = arg.strip_prefix("exact:") {
Matt W247 Ok(Expr::DescriptionExact(v.to_string()))
Matt W248 } else {
Matt W249 // The trap. jj matches exactly here; a substring translation
Matt W250 // would return more than the CLI does.
Matt W251 Err(RevsetError(format!(
Matt W252 "`{name}(\"\")` matches the description EXACTLY in jj, which rarely \
Matt W253 does what you want here. Write {name}(substring:\"\") for a \
Matt W254 substring match, or {name}(exact:\"\") to be explicit."
Matt W255 )))
Matt W256 }
Matt W257 }
Matt W258 "conflicted" | "conflicts" => Ok(Expr::Conflicted),
Matt W259 "open" => Ok(Expr::Open),
Matt W260 "bookmarks" | "bookmark" | "branches" => Ok(Expr::Bookmark(strip_prefix(&arg))),
Matt W261 other if KNOWN_UNSUPPORTED.contains(&other) => Err(RevsetError(format!(
Matt W262 "`{other}()` needs graph traversal or content search, which this index \
Matt W263 cannot answer faithfully. Supported: author, description, conflicted, \
Matt W264 open, bookmarks."
Matt W265 ))),
Matt W266 other => Err(RevsetError(format!(
Matt W267 "Unknown function `{other}()`. Supported: author, description, \
Matt W268 conflicted, open, bookmarks."
Matt W269 ))),
Matt W270 }
Matt W271}
Matt W272
Matt W273fn strip_prefix(arg: &str) -> String {
Matt W274 arg.strip_prefix("substring:")
Matt W275 .or_else(|| arg.strip_prefix("exact:"))
Matt W276 .unwrap_or(arg)
Matt W277 .to_string()
Matt W278}
Matt W279
Matt W280/// Compile to a SQL fragment plus its bind values.
Matt W281///
Matt W282/// Placeholders start at `next_param`, which the caller advances so the
Matt W283/// fragment can be spliced into a larger query.
Matt W284/// Escape the wildcards in a value destined for `ILIKE`.
Matt W285///
Matt W286/// The value is bound as a parameter, so this is not about SQL injection — it
Matt W287/// is about meaning. `author(%)` would otherwise match every author, and
Matt W288/// `description-substring(a_b)` would match `axb`, neither of which is what the
Matt W289/// user wrote. `\` is the escape character Postgres uses by default.
Matt W290fn escape_like(v: &str) -> String {
Matt W291 let mut out = String::with_capacity(v.len());
Matt W292 for c in v.chars() {
Matt W293 if matches!(c, '%' | '_' | '\\') {
Matt W294 out.push('\\');
Matt W295 }
Matt W296 out.push(c);
Matt W297 }
Matt W298 out
Matt W299}
Matt W300
Matt W301pub fn to_sql(e: &Expr, next_param: &mut usize) -> (String, Vec<String>) {
Matt W302 match e {
Matt W303 Expr::Author(v) => {
Matt W304 // jj matches author name *and* email as a substring.
Matt W305 let a = *next_param;
Matt W306 *next_param += 1;
Matt W307 (
Matt W308 format!(
Matt W309 "EXISTS (SELECT 1 FROM revisions rv WHERE rv.change_id_fk = c.id \
Matt W310 AND (rv.author_name ILIKE '%' || ${a} || '%' \
Matt W311 OR rv.author_email ILIKE '%' || ${a} || '%'))"
Matt W312 ),
Matt W313 vec![escape_like(v)],
Matt W314 )
Matt W315 }
Matt W316 Expr::DescriptionExact(v) => {
Matt W317 let a = *next_param;
Matt W318 *next_param += 1;
Matt W319 (format!("(c.title = ${a} OR c.description = ${a})"), vec![v.clone()])
Matt W320 }
Matt W321 Expr::DescriptionSubstring(v) => {
Matt W322 let a = *next_param;
Matt W323 *next_param += 1;
Matt W324 (
Matt W325 format!("(c.title ILIKE '%' || ${a} || '%' OR c.description ILIKE '%' || ${a} || '%')"),
Matt W326 vec![escape_like(v)],
Matt W327 )
Matt W328 }
Matt W329 Expr::Conflicted => ("c.conflicted".to_string(), vec![]),
Matt W330 Expr::Open => ("c.state = 'open'".to_string(), vec![]),
Matt W331 Expr::Bookmark(v) => {
Matt W332 let a = *next_param;
Matt W333 *next_param += 1;
Matt W334 (format!("c.target_bookmark = ${a}"), vec![v.clone()])
Matt W335 }
Matt W336 Expr::And(l, r) => {
Matt W337 let (ls, mut lv) = to_sql(l, next_param);
Matt W338 let (rs, rv) = to_sql(r, next_param);
Matt W339 lv.extend(rv);
Matt W340 (format!("({ls} AND {rs})"), lv)
Matt W341 }
Matt W342 Expr::Or(l, r) => {
Matt W343 let (ls, mut lv) = to_sql(l, next_param);
Matt W344 let (rs, rv) = to_sql(r, next_param);
Matt W345 lv.extend(rv);
Matt W346 (format!("({ls} OR {rs})"), lv)
Matt W347 }
Matt W348 Expr::Not(i) => {
Matt W349 let (s, v) = to_sql(i, next_param);
Matt W350 (format!("(NOT {s})"), v)
Matt W351 }
Matt W352 }
Matt W353}
Matt W354
Matt W355#[cfg(test)]
Matt W356mod tests {
Matt W357 use super::*;
Matt W358
Matt W359 #[test]
Matt W360 fn empty_input_is_no_filter() {
Matt W361 assert_eq!(parse(""), Ok(None));
Matt W362 assert_eq!(parse(" "), Ok(None));
Matt W363 }
Matt W364
Matt W365 #[test]
Matt W366 fn parses_the_supported_functions() {
Matt W367 assert_eq!(parse("conflicted()").unwrap(), Some(Expr::Conflicted));
Matt W368 assert_eq!(parse("open()").unwrap(), Some(Expr::Open));
Matt W369 assert_eq!(
Matt W370 parse("author(mira)").unwrap(),
Matt W371 Some(Expr::Author("mira".into()))
Matt W372 );
Matt W373 assert_eq!(
Matt W374 parse("author(\"mira\")").unwrap(),
Matt W375 Some(Expr::Author("mira".into()))
Matt W376 );
Matt W377 assert_eq!(
Matt W378 parse("bookmarks(main)").unwrap(),
Matt W379 Some(Expr::Bookmark("main".into()))
Matt W380 );
Matt W381 }
Matt W382
Matt W383 // ─── the description trap (docs/revset-semantics.md) ─────────────────────
Matt W384
Matt W385 #[test]
Matt W386 fn bare_description_is_rejected_rather_than_silently_made_substring() {
Matt W387 // jj's description("x") is an EXACT match. Translating it to ILIKE
Matt W388 // would return more rows than the CLI, which §8 calls worse than an
Matt W389 // honest limitation.
Matt W390 let e = parse("description(\"fix\")").unwrap_err();
Matt W391 assert!(e.0.contains("EXACTLY"), "message must explain: {}", e.0);
Matt W392 assert!(e.0.contains("substring:"), "must suggest the fix: {}", e.0);
Matt W393 }
Matt W394
Matt W395 #[test]
Matt W396 fn explicit_substring_and_exact_forms_are_accepted() {
Matt W397 assert_eq!(
Matt W398 parse("description(substring:\"fix\")").unwrap(),
Matt W399 Some(Expr::DescriptionSubstring("fix".into()))
Matt W400 );
Matt W401 assert_eq!(
Matt W402 parse("description(exact:\"fix\")").unwrap(),
Matt W403 Some(Expr::DescriptionExact("fix".into()))
Matt W404 );
Matt W405 }
Matt W406
Matt W407 // ─── operators ───────────────────────────────────────────────────────────
Matt W408
Matt W409 #[test]
Matt W410 fn parses_and_or_not() {
Matt W411 let e = parse("author(a) & conflicted()").unwrap().unwrap();
Matt W412 assert!(matches!(e, Expr::And(_, _)));
Matt W413
Matt W414 let e = parse("open() | conflicted()").unwrap().unwrap();
Matt W415 assert!(matches!(e, Expr::Or(_, _)));
Matt W416
Matt W417 let e = parse("~conflicted()").unwrap().unwrap();
Matt W418 assert!(matches!(e, Expr::Not(_)));
Matt W419 }
Matt W420
Matt W421 #[test]
Matt W422 fn and_binds_tighter_than_or() {
Matt W423 // a | b & c == a | (b & c)
Matt W424 let e = parse("open() | conflicted() & author(x)").unwrap().unwrap();
Matt W425 match e {
Matt W426 Expr::Or(l, r) => {
Matt W427 assert_eq!(*l, Expr::Open);
Matt W428 assert!(matches!(*r, Expr::And(_, _)));
Matt W429 }
Matt W430 other => panic!("expected Or at the top, got {other:?}"),
Matt W431 }
Matt W432 }
Matt W433
Matt W434 #[test]
Matt W435 fn parentheses_override_precedence() {
Matt W436 let e = parse("(open() | conflicted()) & author(x)").unwrap().unwrap();
Matt W437 assert!(matches!(e, Expr::And(_, _)));
Matt W438 }
Matt W439
Matt W440 // ─── honest rejection (spec §8) ──────────────────────────────────────────
Matt W441
Matt W442 #[test]
Matt W443 fn unsupported_functions_are_named_in_the_error() {
Matt W444 for f in ["heads()", "ancestors(x)", "file(a.rs)", "merges()"] {
Matt W445 let e = parse(f).unwrap_err();
Matt W446 let name = f.split('(').next().unwrap();
Matt W447 assert!(
Matt W448 e.0.contains(name),
Matt W449 "error for {f} must name the function: {}",
Matt W450 e.0
Matt W451 );
Matt W452 }
Matt W453 }
Matt W454
Matt W455 #[test]
Matt W456 fn unknown_functions_are_rejected_not_ignored() {
Matt W457 let e = parse("authr(mira)").unwrap_err();
Matt W458 assert!(e.0.contains("authr"), "got {}", e.0);
Matt W459 }
Matt W460
Matt W461 #[test]
Matt W462 fn malformed_input_produces_an_error_not_a_partial_result() {
Matt W463 for bad in [
Matt W464 "authr(mira) & (",
Matt W465 "open(",
Matt W466 ")",
Matt W467 "&",
Matt W468 "author(",
Matt W469 "open() &",
Matt W470 "\"unterminated",
Matt W471 ] {
Matt W472 assert!(parse(bad).is_err(), "{bad:?} should not parse");
Matt W473 }
Matt W474 }
Matt W475
Matt W476 #[test]
Matt W477 fn a_bare_word_is_rejected_with_a_helpful_message() {
Matt W478 let e = parse("main").unwrap_err();
Matt W479 assert!(e.0.contains("not a value"), "got {}", e.0);
Matt W480 }
Matt W481
Matt W482 #[test]
Matt W483 fn absurdly_long_input_is_refused() {
Matt W484 assert!(parse(&"open() | ".repeat(200)).is_err());
Matt W485 }
Matt W486
Matt W487 // ─── SQL generation ──────────────────────────────────────────────────────
Matt W488
Matt W489 #[test]
Matt W490 fn generates_numbered_placeholders_without_interpolating_values() {
Matt W491 let e = parse("author(mira) & conflicted()").unwrap().unwrap();
Matt W492 let mut n = 3;
Matt W493 let (sql, vals) = to_sql(&e, &mut n);
Matt W494
Matt W495 assert!(sql.contains("$3"), "got {sql}");
Matt W496 assert_eq!(vals, vec!["mira".to_string()]);
Matt W497 assert_eq!(n, 4, "the parameter counter must advance");
Matt W498 // The value must never appear inline — that would be an injection.
Matt W499 assert!(!sql.contains("mira"), "value was interpolated into SQL: {sql}");
Matt W500 }
Matt W501
Matt W502 #[test]
Matt W503 fn a_hostile_value_stays_a_bind_parameter() {
Matt W504 let e = parse("author(\"'; DROP TABLE changes; --\")").unwrap().unwrap();
Matt W505 let mut n = 1;
Matt W506 let (sql, vals) = to_sql(&e, &mut n);
Matt W507 assert!(!sql.contains("DROP"), "SQL injection: {sql}");
Matt W508 assert_eq!(vals[0], "'; DROP TABLE changes; --");
Matt W509 }
Matt W510
Matt W511 #[test]
Matt W512 fn placeholders_stay_unique_across_a_compound_expression() {
Matt W513 let e = parse("author(a) & (bookmarks(b) | description(substring:\"c\"))")
Matt W514 .unwrap()
Matt W515 .unwrap();
Matt W516 let mut n = 1;
Matt W517 let (sql, vals) = to_sql(&e, &mut n);
Matt W518 assert_eq!(vals.len(), 3);
Matt W519 for i in 1..=3 {
Matt W520 assert!(sql.contains(&format!("${i}")), "missing ${i} in {sql}");
Matt W521 }
Matt W522 assert_eq!(n, 4);
Matt W523 }
Matt W524
Matt W525 #[test]
Matt W526 fn like_wildcards_in_a_value_are_escaped() {
Matt W527 // Bound as a parameter either way, so this is about meaning rather than
Matt W528 // injection: `author(%)` must not match every author.
Matt W529 assert_eq!(escape_like("%"), "\\%");
Matt W530 assert_eq!(escape_like("a_b"), "a\\_b");
Matt W531 assert_eq!(escape_like("100%_sure"), "100\\%\\_sure");
Matt W532 assert_eq!(escape_like("plain"), "plain");
Matt W533 assert_eq!(escape_like("back\\slash"), "back\\\\slash");
Matt W534 }
Matt W535
Matt W536 #[test]
Matt W537 fn escaped_values_reach_the_bind_list() {
Matt W538 let e = parse("author(%)").unwrap().unwrap();
Matt W539 let mut n = 3;
Matt W540 let (_, vals) = to_sql(&e, &mut n);
Matt W541 assert_eq!(vals, vec!["\\%".to_string()]);
Matt W542 }
Matt W543}

543 lines · Rust