diff --git a/CHANGELOG.md b/CHANGELOG.md index 733fa32f8..4aa38e147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The output can be customized via the `templates.annotate_commit_summary` config variable. +* New `coalesce(revsets...)` revset which returns commits in the first revset + in the `revsets` list that does not evaluate to `none()`. + ### Fixed bugs * Error on `trunk()` revset resolution is now handled gracefully. diff --git a/docs/revsets.md b/docs/revsets.md index bbd6c2914..9d42df006 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -304,6 +304,10 @@ given [string pattern](#string-patterns). * `present(x)`: Same as `x`, but evaluated to `none()` if any of the commits in `x` doesn't exist (e.g. is an unknown bookmark name.) +* `coalesce(revsets...)`: Commits in the first revset in the list of `revsets` + which does not evaluate to `none()`. If all revsets evaluate to `none()`, then + the result of `coalesce` will also be `none()`. + * `working_copies()`: The working copy commits across all the workspaces. * `at_operation(op, x)`: Evaluates `x` at the specified [operation][]. For diff --git a/lib/src/default_index/revset_engine.rs b/lib/src/default_index/revset_engine.rs index d84c48a33..ca3678dcc 100644 --- a/lib/src/default_index/revset_engine.rs +++ b/lib/src/default_index/revset_engine.rs @@ -919,6 +919,14 @@ impl<'index> EvaluationContext<'index> { self.take_latest_revset(candidate_set.as_ref(), *count), )) } + ResolvedExpression::Coalesce(expression1, expression2) => { + let set1 = self.evaluate(expression1)?; + if set1.positions().attach(index).next().is_some() { + Ok(set1) + } else { + self.evaluate(expression2) + } + } ResolvedExpression::Union(expression1, expression2) => { let set1 = self.evaluate(expression1)?; let set2 = self.evaluate(expression2)?; diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 939c73492..e32d8738e 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -218,6 +218,7 @@ pub enum RevsetExpression { /// Copy of `repo.view().heads()`, should be set by `resolve_symbols()`. visible_heads: Option>, }, + Coalesce(Rc, Rc), Present(Rc), NotIn(Rc), Union(Rc, Rc), @@ -439,6 +440,20 @@ impl RevsetExpression { Rc::new(Self::Difference(self.clone(), other.clone())) } + /// Commits that are in the first expression in `expressions` that is not + /// `none()`. + pub fn coalesce(expressions: &[Rc]) -> Rc { + match expressions { + [] => Self::none(), + [expression] => expression.clone(), + _ => { + // Build balanced tree to minimize the recursion depth. + let (left, right) = expressions.split_at(expressions.len() / 2); + Rc::new(Self::Coalesce(Self::coalesce(left), Self::coalesce(right))) + } + } + } + /// Resolve a programmatically created revset expression. /// /// In particular, the expression must not contain any symbols (bookmarks, @@ -528,6 +543,7 @@ pub enum ResolvedExpression { candidates: Box, count: usize, }, + Coalesce(Box, Box), Union(Box, Box), /// Intersects `candidates` with `predicate` by filtering. FilterWithin { @@ -853,6 +869,14 @@ static BUILTIN_FUNCTION_MAP: Lazy> = Lazy: visible_heads: None, })) }); + map.insert("coalesce", |diagnostics, function, context| { + let ([], args) = function.expect_some_arguments()?; + let expressions: Vec<_> = args + .iter() + .map(|arg| lower_expression(diagnostics, arg, context)) + .try_collect()?; + Ok(RevsetExpression::coalesce(&expressions)) + }); map }); @@ -1171,6 +1195,12 @@ fn try_transform_expression( visible_heads: visible_heads.clone(), } }), + RevsetExpression::Coalesce(expression1, expression2) => transform_rec_pair( + (expression1, expression2), + pre, + post, + )? + .map(|(expression1, expression2)| RevsetExpression::Coalesce(expression1, expression2)), RevsetExpression::Present(candidates) => { transform_rec(candidates, pre, post)?.map(RevsetExpression::Present) } @@ -2050,6 +2080,10 @@ impl VisibilityResolutionContext<'_> { let context = VisibilityResolutionContext { visible_heads }; context.resolve(candidates) } + RevsetExpression::Coalesce(expression1, expression2) => ResolvedExpression::Coalesce( + self.resolve(expression1).into(), + self.resolve(expression2).into(), + ), RevsetExpression::Present(_) => { panic!("Expression '{expression:?}' should have been resolved by caller"); } @@ -2130,6 +2164,9 @@ impl VisibilityResolutionContext<'_> { RevsetExpression::AtOperation { .. } => { ResolvedPredicateExpression::Set(self.resolve(expression).into()) } + RevsetExpression::Coalesce(_, _) => { + ResolvedPredicateExpression::Set(self.resolve(expression).into()) + } RevsetExpression::Present(_) => { panic!("Expression '{expression:?}' should have been resolved by caller") } @@ -2568,6 +2605,35 @@ mod tests { CommitRef(WorkingCopy(WorkspaceId("default"))), ) "###); + insta::assert_debug_snapshot!( + RevsetExpression::coalesce(&[]), + @"None"); + insta::assert_debug_snapshot!( + RevsetExpression::coalesce(&[current_wc.clone()]), + @r###"CommitRef(WorkingCopy(WorkspaceId("default")))"###); + insta::assert_debug_snapshot!( + RevsetExpression::coalesce(&[current_wc.clone(), foo_symbol.clone()]), + @r#" + Coalesce( + CommitRef(WorkingCopy(WorkspaceId("default"))), + CommitRef(Symbol("foo")), + ) + "#); + insta::assert_debug_snapshot!( + RevsetExpression::coalesce(&[ + current_wc.clone(), + foo_symbol.clone(), + bar_symbol.clone(), + ]), + @r#" + Coalesce( + CommitRef(WorkingCopy(WorkspaceId("default"))), + Coalesce( + CommitRef(Symbol("foo")), + CommitRef(Symbol("bar")), + ), + ) + "#); } #[test] diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index bdfa0f6ee..1fc61b093 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -2974,6 +2974,84 @@ fn test_evaluate_expression_at_operation() { ); } +#[test] +fn test_evaluate_expression_coalesce() { + let settings = testutils::user_settings(); + let test_repo = TestRepo::init(); + let repo = &test_repo.repo; + let root_commit_id = repo.store().root_commit_id().clone(); + + let mut tx = repo.start_transaction(&settings); + let mut_repo = tx.repo_mut(); + let mut graph_builder = CommitGraphBuilder::new(&settings, mut_repo); + let commit1 = graph_builder.initial_commit(); + let commit2 = graph_builder.commit_with_parents(&[&commit1]); + mut_repo.set_local_bookmark_target("commit1", RefTarget::normal(commit1.id().clone())); + mut_repo.set_local_bookmark_target("commit2", RefTarget::normal(commit2.id().clone())); + + assert_eq!(resolve_commit_ids(mut_repo, "coalesce()"), vec![]); + assert_eq!(resolve_commit_ids(mut_repo, "coalesce(none())"), vec![]); + assert_eq!( + resolve_commit_ids(mut_repo, "coalesce(all())"), + vec![ + commit2.id().clone(), + commit1.id().clone(), + root_commit_id.clone(), + ] + ); + assert_eq!( + resolve_commit_ids(mut_repo, "coalesce(all(), commit1)"), + vec![ + commit2.id().clone(), + commit1.id().clone(), + root_commit_id.clone(), + ] + ); + assert_eq!( + resolve_commit_ids(mut_repo, "coalesce(none(), commit1)"), + vec![commit1.id().clone()] + ); + assert_eq!( + resolve_commit_ids(mut_repo, "coalesce(commit1, commit2)"), + vec![commit1.id().clone()] + ); + assert_eq!( + resolve_commit_ids(mut_repo, "coalesce(none(), none(), commit2)"), + vec![commit2.id().clone()] + ); + assert_eq!( + resolve_commit_ids(mut_repo, "coalesce(none(), commit1, commit2)"), + vec![commit1.id().clone()] + ); + // Should resolve invalid symbols regardless of whether a specific revset is + // evaluated. + assert_matches!( + try_resolve_commit_ids(mut_repo, "coalesce(all(), commit1_invalid)"), + Err(RevsetResolutionError::NoSuchRevision { name, .. }) + if name == "commit1_invalid" + ); + assert_matches!( + try_resolve_commit_ids(mut_repo, "coalesce(none(), commit1_invalid)"), + Err(RevsetResolutionError::NoSuchRevision { name, .. }) + if name == "commit1_invalid" + ); + assert_matches!( + try_resolve_commit_ids(mut_repo, "coalesce(all(), commit1, commit2_invalid)"), + Err(RevsetResolutionError::NoSuchRevision { name, .. }) + if name == "commit2_invalid" + ); + assert_matches!( + try_resolve_commit_ids(mut_repo, "coalesce(none(), commit1, commit2_invalid)"), + Err(RevsetResolutionError::NoSuchRevision { name, .. }) + if name == "commit2_invalid" + ); + assert_matches!( + try_resolve_commit_ids(mut_repo, "coalesce(none(), commit1, commit2, commit2_invalid)"), + Err(RevsetResolutionError::NoSuchRevision { name, .. }) + if name == "commit2_invalid" + ); +} + #[test] fn test_evaluate_expression_union() { let settings = testutils::user_settings();