diff --git a/cli/testing/bench-revsets-git.txt b/cli/testing/bench-revsets-git.txt index f0d7b2c13..412011fed 100644 --- a/cli/testing/bench-revsets-git.txt +++ b/cli/testing/bench-revsets-git.txt @@ -15,6 +15,7 @@ v2.39.0..v2.40.0 v2.39.0::v2.40.0 # Mostly recent history v2.40.0-.. +~(::v2.40.0) # Tags and branches tags() branches() diff --git a/lib/src/revset.rs b/lib/src/revset.rs index e9dede630..ea2315cbd 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -1865,6 +1865,23 @@ fn fold_difference(expression: &Rc) -> TransformedExpression { }) } +/// Transforms remaining negated ancestors `~(::h)` to range `h..`. +/// +/// Since this rule inserts redundant `visible_heads()`, negative intersections +/// should have been transformed. +fn fold_not_in_ancestors(expression: &Rc) -> TransformedExpression { + transform_expression_bottom_up(expression, |expression| match expression.as_ref() { + RevsetExpression::NotIn(complement) + if matches!(complement.as_ref(), RevsetExpression::Ancestors { .. }) => + { + // ~(::heads) -> heads.. + // ~(::heads-) -> ~ancestors(heads, 1..) -> heads-.. + to_difference_range(&RevsetExpression::visible_heads().ancestors(), complement) + } + _ => None, + }) +} + /// Transforms binary difference to more primitive negative intersection. /// /// For example, `all() ~ e` will become `all() & ~e`, which can be simplified @@ -1953,7 +1970,8 @@ pub fn optimize(expression: Rc) -> Rc { let expression = fold_redundant_expression(&expression).unwrap_or(expression); let expression = fold_generation(&expression).unwrap_or(expression); let expression = internalize_filter(&expression).unwrap_or(expression); - fold_difference(&expression).unwrap_or(expression) + let expression = fold_difference(&expression).unwrap_or(expression); + fold_not_in_ancestors(&expression).unwrap_or(expression) } // TODO: find better place to host this function (or add compile-time revset @@ -3777,6 +3795,84 @@ mod tests { "###); } + #[test] + fn test_optimize_not_in_ancestors() { + // '~(::foo)' is equivalent to 'foo..'. + insta::assert_debug_snapshot!(optimize(parse("~(::foo)").unwrap()), @r###" + Range { + roots: CommitRef( + Symbol( + "foo", + ), + ), + heads: CommitRef( + VisibleHeads, + ), + generation: 0..18446744073709551615, + } + "###); + + // '~(::foo-)' is equivalent to 'foo-..'. + insta::assert_debug_snapshot!(optimize(parse("~(::foo-)").unwrap()), @r###" + Range { + roots: Ancestors { + heads: CommitRef( + Symbol( + "foo", + ), + ), + generation: 1..2, + }, + heads: CommitRef( + VisibleHeads, + ), + generation: 0..18446744073709551615, + } + "###); + insta::assert_debug_snapshot!(optimize(parse("~(::foo--)").unwrap()), @r###" + Range { + roots: Ancestors { + heads: CommitRef( + Symbol( + "foo", + ), + ), + generation: 2..3, + }, + heads: CommitRef( + VisibleHeads, + ), + generation: 0..18446744073709551615, + } + "###); + + // Bounded ancestors shouldn't be substituted. + insta::assert_debug_snapshot!(optimize(parse("~ancestors(foo, 1)").unwrap()), @r###" + NotIn( + Ancestors { + heads: CommitRef( + Symbol( + "foo", + ), + ), + generation: 0..1, + }, + ) + "###); + insta::assert_debug_snapshot!(optimize(parse("~ancestors(foo-, 1)").unwrap()), @r###" + NotIn( + Ancestors { + heads: CommitRef( + Symbol( + "foo", + ), + ), + generation: 1..2, + }, + ) + "###); + } + #[test] fn test_optimize_filter_difference() { // '~empty()' -> '~~file(*)' -> 'file(*)'