From 1a8f649dcfa9c574d3b703aaf65f07f88ccbc625 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 13 Oct 2024 22:40:34 +0800 Subject: [PATCH] revset: add `coalesce(revsets...)` --- CHANGELOG.md | 3 + docs/revsets.md | 4 ++ lib/src/default_index/revset_engine.rs | 8 +++ lib/src/revset.rs | 26 +++++++++ lib/tests/test_revset.rs | 78 ++++++++++++++++++++++++++ 5 files changed, 119 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1acab142..5b4c45a3b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * New template function `raw_escape_sequence(...)` preserves escape sequences. +* 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 bbd6c29142..4a5b53aac2 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -310,6 +310,10 @@ given [string pattern](#string-patterns). example, `at_operation(@-, visible_heads())` will return all heads which were visible at the previous operation. +* `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()`. + [operation]: glossary.md#operation ??? examples diff --git a/lib/src/default_index/revset_engine.rs b/lib/src/default_index/revset_engine.rs index d84c48a333..ca3678dcc0 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 939c734922..c15bf1df92 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), @@ -528,6 +529,7 @@ pub enum ResolvedExpression { candidates: Box, count: usize, }, + Coalesce(Box, Box), Union(Box, Box), /// Intersects `candidates` with `predicate` by filtering. FilterWithin { @@ -853,6 +855,17 @@ static BUILTIN_FUNCTION_MAP: Lazy> = Lazy: visible_heads: None, })) }); + map.insert("coalesce", |diagnostics, function, context| { + let ([], args) = function.expect_some_arguments()?; + let mut revset = RevsetExpression::none(); + for arg in args.iter().rev() { + revset = Rc::new(RevsetExpression::Coalesce( + lower_expression(diagnostics, arg, context)?, + revset, + )); + } + Ok(revset) + }); map }); @@ -1171,6 +1184,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 +2069,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 +2153,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") } diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 9de50e0e30..7dd8acecec 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -3029,6 +3029,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();