From 7e79f25508480ab48b1bf95d0a190f654ba7585c Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Wed, 13 Apr 2022 13:55:47 -0700 Subject: [PATCH] revset: add a `roots()` function --- CHANGELOG.md | 3 ++ docs/revsets.md | 1 + lib/src/revset.rs | 40 +++++++++++++++++++++++++- lib/tests/test_revset.rs | 62 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f38fbf8ee..a40609a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The new revset function `connected(x)` is the same as `x:x`. +* The new revset function `roots(x)` finds commits in the set that are not + descendants of other commits in the set. + ### Fixed bugs * When rebasing a conflict where one side modified a file and the other side diff --git a/docs/revsets.md b/docs/revsets.md index f1c32c428..a17f84972 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -101,6 +101,7 @@ revsets (expressions) as arguments. * `heads([x])`: Commits in `x` that are not ancestors of other commits in `x`. If `x` was not specified, it selects all visible heads (as if you had said `heads(all())`). +* `roots(x)`: Commits in `x` that are not descendants of other commits in `x`. * `merges([x])`: Merge commits within `x`. If `x` was not specified, it selects all visible merge commits (as if you had said `merges(all())`). * `description(needle[, x])`: Commits with the given string in their diff --git a/lib/src/revset.rs b/lib/src/revset.rs index eb9b3c5b7..b424b6301 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -213,8 +213,9 @@ pub enum RevsetExpression { roots: Rc, heads: Rc, }, - VisibleHeads, Heads(Rc), + Roots(Rc), + VisibleHeads, PublicHeads, Branches, RemoteBranches, @@ -298,6 +299,11 @@ impl RevsetExpression { Rc::new(RevsetExpression::Heads(self.clone())) } + /// Commits in `self` that don't have ancestors in `self`. + pub fn roots(self: &Rc) -> Rc { + Rc::new(RevsetExpression::Roots(self.clone())) + } + /// Parents of `self`. pub fn parents(self: &Rc) -> Rc { Rc::new(RevsetExpression::Parents(self.clone())) @@ -652,6 +658,18 @@ fn parse_function_expression( }) } } + "roots" => { + if arg_count == 1 { + let candidates = + parse_expression_rule(argument_pairs.next().unwrap().into_inner())?; + Ok(candidates.roots()) + } else { + Err(RevsetParseError::InvalidFunctionArguments { + name, + message: "Expected 1 argument".to_string(), + }) + } + } "public_heads" => { if arg_count == 0 { Ok(RevsetExpression::public_heads()) @@ -1185,6 +1203,22 @@ pub fn evaluate_expression<'repo>( &repo.index().heads(&candidate_ids), )) } + RevsetExpression::Roots(candidates) => { + let connected_set = candidates.connected().evaluate(repo, workspace_id)?; + let filled: HashSet<_> = connected_set.iter().map(|entry| entry.position()).collect(); + let mut index_entries = vec![]; + let candidate_set = candidates.evaluate(repo, workspace_id)?; + for candidate in candidate_set.iter() { + if !candidate + .parent_positions() + .iter() + .any(|parent| filled.contains(parent)) + { + index_entries.push(candidate); + } + } + Ok(Box::new(EagerRevset { index_entries })) + } RevsetExpression::ParentCount { candidates, parent_count_range, @@ -1343,6 +1377,10 @@ mod tests { checkout_symbol.heads(), Rc::new(RevsetExpression::Heads(checkout_symbol.clone())) ); + assert_eq!( + checkout_symbol.roots(), + Rc::new(RevsetExpression::Roots(checkout_symbol.clone())) + ); assert_eq!( checkout_symbol.parents(), Rc::new(RevsetExpression::Parents(checkout_symbol.clone())) diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index b37522952..96cb176c2 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -522,6 +522,68 @@ fn test_evaluate_expression_heads(use_git: bool) { ); } +#[test_case(false ; "local backend")] +#[test_case(true ; "git backend")] +fn test_evaluate_expression_roots(use_git: bool) { + let settings = testutils::user_settings(); + let test_repo = testutils::init_repo(&settings, use_git); + let repo = &test_repo.repo; + + let root_commit = repo.store().root_commit(); + let mut tx = repo.start_transaction("test"); + let mut_repo = tx.mut_repo(); + let mut graph_builder = CommitGraphBuilder::new(&settings, mut_repo); + let commit1 = graph_builder.initial_commit(); + let commit2 = graph_builder.commit_with_parents(&[&commit1]); + let commit3 = graph_builder.commit_with_parents(&[&commit2]); + + // Roots of an empty set is an empty set + assert_eq!( + resolve_commit_ids(mut_repo.as_repo_ref(), "roots(none())"), + vec![] + ); + + // Roots of the root is the root + assert_eq!( + resolve_commit_ids(mut_repo.as_repo_ref(), "roots(root)"), + vec![root_commit.id().clone()] + ); + + // Roots of a single commit is that commit + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("roots({})", commit2.id().hex()) + ), + vec![commit2.id().clone()] + ); + + // Roots of a parent and a child is the parent + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("roots({} | {})", commit2.id().hex(), commit3.id().hex()) + ), + vec![commit2.id().clone()] + ); + + // Roots of a grandparent and a grandchild is the grandparent (unlike + // Mercurial's roots() revset, which would include both) + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("roots({} | {})", commit1.id().hex(), commit3.id().hex()) + ), + vec![commit1.id().clone()] + ); + + // Roots of all commits is the root commit + assert_eq!( + resolve_commit_ids(mut_repo.as_repo_ref(), "roots(all())"), + vec![root_commit.id().clone()] + ); +} + #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_evaluate_expression_parents(use_git: bool) {