revset: add a roots() function

This commit is contained in:
Martin von Zweigbergk 2022-04-13 13:55:47 -07:00 committed by Martin von Zweigbergk
parent 9ff21d8924
commit 7e79f25508
4 changed files with 105 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -213,8 +213,9 @@ pub enum RevsetExpression {
roots: Rc<RevsetExpression>,
heads: Rc<RevsetExpression>,
},
VisibleHeads,
Heads(Rc<RevsetExpression>),
Roots(Rc<RevsetExpression>),
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<RevsetExpression>) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::Roots(self.clone()))
}
/// Parents of `self`.
pub fn parents(self: &Rc<RevsetExpression>) -> Rc<RevsetExpression> {
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()))

View file

@ -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) {