diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9763634..21fe76f28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Revsets gained a new function `mine()` that aliases `author(exact:"your_email")`. +* Added support for `::` and `..` revset operators with both left and right + operands omitted. These expressions are equivalent to `all()` and `~root()` + respectively. + * `jj log` timestamp format now accepts `.utc()` to convert a timestamp to UTC. * templates now support additional string methods `.starts_with(x)`, `.ends_with(x)` diff --git a/cli/tests/test_revset_output.rs b/cli/tests/test_revset_output.rs index 41ced192f..6b72ca5e9 100644 --- a/cli/tests/test_revset_output.rs +++ b/cli/tests/test_revset_output.rs @@ -29,7 +29,7 @@ fn test_syntax_error() { 1 | x & | ^--- | - = expected dag_range_pre_op, legacy_dag_range_pre_op, range_pre_op, negate_op, or primary + = expected dag_range_pre_op, dag_range_all_op, legacy_dag_range_pre_op, range_pre_op, range_all_op, negate_op, or primary "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "x - y"]); @@ -287,7 +287,7 @@ fn test_alias() { 1 | whatever & | ^--- | - = expected dag_range_pre_op, legacy_dag_range_pre_op, range_pre_op, negate_op, or primary + = expected dag_range_pre_op, dag_range_all_op, legacy_dag_range_pre_op, range_pre_op, range_all_op, negate_op, or primary "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "identity()"]); diff --git a/docs/revsets.md b/docs/revsets.md index 224c996bd..1d78f1b2f 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -60,11 +60,14 @@ only symbols. to `x: & :y`. This is what `git log` calls `--ancestry-path x..y`. * `::x`, `x::`, and `x::y`: New versions of for `:x`, `x:`, and `x:y` to be released in jj 0.9.0. We plan to delete the latter in jj 0.15+. +* `::`: All visible commits in the repo. Equivalent to `all()`. * `x..y`: Ancestors of `y` that are not also ancestors of `x`. Equivalent to `:y ~ :x`. This is what `git log` calls `x..y` (i.e. the same as we call it). * `..x`: Ancestors of `x`, including the commits in `x` itself, but excluding the root commit. Equivalent to `:x ~ root()`. * `x..`: Revisions that are not ancestors of `x`. +* `..`: All visible commits in the repo, but excluding the root commit. + Equivalent to `~root()`. You can use parentheses to control evaluation order, such as `(x & y) | z` or `x & (y | z)`. diff --git a/lib/src/revset.pest b/lib/src/revset.pest index ac1be23db..7703a6197 100644 --- a/lib/src/revset.pest +++ b/lib/src/revset.pest @@ -33,6 +33,7 @@ compat_parents_op = { "^" } dag_range_op = { "::" } dag_range_pre_op = { "::" } dag_range_post_op = { "::" } +dag_range_all_op = { "::" } // TODO: Drop support for these in 0.15+ legacy_dag_range_op = { ":" } legacy_dag_range_pre_op = { ":" } @@ -40,9 +41,11 @@ legacy_dag_range_post_op = { ":" } range_op = { ".." } range_pre_op = { ".." } range_post_op = { ".." } +range_all_op = { ".." } range_ops = _{ dag_range_op | legacy_dag_range_op | range_op } range_pre_ops = _{ dag_range_pre_op | legacy_dag_range_pre_op | range_pre_op } range_post_ops = _{ dag_range_post_op | legacy_dag_range_post_op | range_post_op } +range_all_ops = _{ dag_range_all_op | range_all_op } negate_op = { "~" } union_op = { "|" } @@ -81,6 +84,7 @@ range_expression = _{ | neighbors_expression ~ range_post_ops | range_pre_ops ~ neighbors_expression | neighbors_expression + | range_all_ops } expression = { diff --git a/lib/src/revset.rs b/lib/src/revset.rs index f48973be2..669d8f88c 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -850,7 +850,14 @@ fn parse_expression_rule( | Op::postfix(Rule::compat_parents_op)) }); PRATT - .map_primary(|primary| parse_primary_rule(primary, state)) + .map_primary(|primary| match primary.as_rule() { + Rule::primary => parse_primary_rule(primary, state), + Rule::dag_range_all_op => Ok(RevsetExpression::all()), + Rule::range_all_op => { + Ok(RevsetExpression::root().range(&RevsetExpression::visible_heads())) + } + r => panic!("unexpected primary rule {r:?}"), + }) .map_prefix(|op, rhs| match op.as_rule() { Rule::negate_op => Ok(rhs?.negated()), Rule::dag_range_pre_op => Ok(rhs?.ancestors()), @@ -2767,6 +2774,8 @@ mod tests { assert_eq!(parse("foo::"), Ok(foo_symbol.descendants())); // Parse the "dag range" operator assert_eq!(parse("foo::bar"), Ok(foo_symbol.dag_range_to(&bar_symbol))); + // Parse the nullary "dag range" operator + assert_eq!(parse("::"), Ok(RevsetExpression::all())); // Parse the "range" prefix operator assert_eq!( parse("..foo"), @@ -2777,6 +2786,11 @@ mod tests { Ok(foo_symbol.range(&RevsetExpression::visible_heads())) ); assert_eq!(parse("foo..bar"), Ok(foo_symbol.range(&bar_symbol))); + // Parse the nullary "range" operator + assert_eq!( + parse(".."), + Ok(RevsetExpression::root().range(&RevsetExpression::visible_heads())) + ); // Parse the "negate" operator assert_eq!(parse("~ foo"), Ok(foo_symbol.negated())); assert_eq!( @@ -2967,6 +2981,7 @@ mod tests { assert_eq!(parse("x&y|z").unwrap(), parse("(x&y)|z").unwrap()); assert_eq!(parse("x|y&z").unwrap(), parse("x|(y&z)").unwrap()); assert_eq!(parse("x|y~z").unwrap(), parse("x|(y~z)").unwrap()); + assert_eq!(parse("::&..").unwrap(), parse("(::)&(..)").unwrap()); // Parse repeated "ancestors"/"descendants"/"dag range"/"range" operators assert_eq!(parse("::foo::"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse(":::foo"), Err(RevsetParseErrorKind::SyntaxError)); @@ -2977,16 +2992,21 @@ mod tests { assert_eq!(parse("foo::::bar"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("::foo::bar"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("foo::bar::"), Err(RevsetParseErrorKind::SyntaxError)); + assert_eq!(parse("::::"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("....foo"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("foo...."), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("foo.....bar"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("..foo..bar"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("foo..bar.."), Err(RevsetParseErrorKind::SyntaxError)); + assert_eq!(parse("...."), Err(RevsetParseErrorKind::SyntaxError)); + assert_eq!(parse("::.."), Err(RevsetParseErrorKind::SyntaxError)); // Parse combinations of "parents"/"children" operators and the range operators. // The former bind more strongly. assert_eq!(parse("foo-+"), Ok(foo_symbol.parents().children())); assert_eq!(parse("foo-::"), Ok(foo_symbol.parents().descendants())); assert_eq!(parse("::foo+"), Ok(foo_symbol.children().ancestors())); + assert_eq!(parse("::-"), Err(RevsetParseErrorKind::SyntaxError)); + assert_eq!(parse("..+"), Err(RevsetParseErrorKind::SyntaxError)); } #[test] diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 6b647480f..f49fc23ce 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -1250,6 +1250,16 @@ fn test_evaluate_expression_range(use_git: bool) { resolve_commit_ids(mut_repo, &format!("{}..", commit2.id().hex())), vec![commit4.id().clone(), commit3.id().clone()] ); + + assert_eq!( + resolve_commit_ids(mut_repo, ".."), + vec![ + commit4.id().clone(), + commit3.id().clone(), + commit2.id().clone(), + commit1.id().clone(), + ] + ); } #[test_case(false ; "local backend")] @@ -1281,7 +1291,11 @@ fn test_evaluate_expression_dag_range(use_git: bool) { mut_repo, &format!("{}:{}", root_commit_id.hex(), commit2.id().hex()) ), - vec![commit2.id().clone(), commit1.id().clone(), root_commit_id] + vec![ + commit2.id().clone(), + commit1.id().clone(), + root_commit_id.clone(), + ] ); // Empty range @@ -1344,6 +1358,19 @@ fn test_evaluate_expression_dag_range(use_git: bool) { commit2.id().clone(), ] ); + + // Full range meaning all() + assert_eq!( + resolve_commit_ids(mut_repo, "::"), + vec![ + commit5.id().clone(), + commit4.id().clone(), + commit3.id().clone(), + commit2.id().clone(), + commit1.id().clone(), + root_commit_id.clone(), + ] + ); } #[test_case(false ; "local backend")]