diff --git a/lib/src/revset.pest b/lib/src/revset.pest index 40b9db970..2f23c17e6 100644 --- a/lib/src/revset.pest +++ b/lib/src/revset.pest @@ -28,6 +28,7 @@ children_op = { ":" } ancestors_op = { ",," } descendants_op = { ",," } dag_range_op = { ",," } +range_op = { ",,," } union_op = { "|" } intersection_op = { "&" } @@ -52,6 +53,7 @@ children_expression = { parents_expression ~ children_op* } range_expression = { children_expression ~ dag_range_op ~ children_expression + | children_expression ~ range_op ~ children_expression | ancestors_op ~ children_expression | children_expression ~ descendants_op | children_expression diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 688ddbc0b..9e1bc2d53 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -99,6 +99,7 @@ pub enum RevsetParseError { #[derive(Debug, PartialEq, Eq)] pub enum RevsetExpression { + None, Symbol(String), Parents(Rc), Children { @@ -106,6 +107,12 @@ pub enum RevsetExpression { heads: Rc, }, Ancestors(Rc), + // Commits that are ancestors of "heads" but not ancestors of "roots" + Range { + roots: Rc, + heads: Rc, + }, + // Commits that are descendants of "roots" and ancestors of "heads" DagRange { roots: Rc, heads: Rc, @@ -213,6 +220,14 @@ fn parse_range_expression_rule( heads: Rc::new(heads_expression), }; } + Rule::range_op => { + let expression2 = + parse_children_expression_rule(pairs.next().unwrap().into_inner())?; + expression = RevsetExpression::Range { + roots: Rc::new(expression), + heads: Rc::new(expression2), + }; + } _ => { panic!("unxpected revset range operator rule {:?}", next.as_rule()); } @@ -678,6 +693,9 @@ pub fn evaluate_expression<'revset, 'repo: 'revset>( expression: &RevsetExpression, ) -> Result + 'revset>, RevsetError> { match expression { + RevsetExpression::None => Ok(Box::new(EagerRevset { + index_entries: vec![], + })), RevsetExpression::Symbol(symbol) => { let commit_id = resolve_symbol(repo, &symbol)?.id().clone(); Ok(Box::new(EagerRevset { @@ -704,10 +722,19 @@ pub fn evaluate_expression<'revset, 'repo: 'revset>( candidate_set, })) } - RevsetExpression::Ancestors(base_expression) => { - let base_set = evaluate_expression(repo, base_expression.as_ref())?; - let base_ids: Vec<_> = base_set.iter().map(|entry| entry.commit_id()).collect(); - let walk = repo.index().walk_revs(&base_ids, &[]); + RevsetExpression::Ancestors(base_expression) => evaluate_expression( + repo, + &RevsetExpression::Range { + roots: Rc::new(RevsetExpression::None), + heads: base_expression.clone(), + }, + ), + RevsetExpression::Range { roots, heads } => { + let root_set = evaluate_expression(repo, roots.as_ref())?; + let root_ids: Vec<_> = root_set.iter().map(|entry| entry.commit_id()).collect(); + let head_set = evaluate_expression(repo, heads.as_ref())?; + let head_ids: Vec<_> = head_set.iter().map(|entry| entry.commit_id()).collect(); + let walk = repo.index().walk_revs(&head_ids, &root_ids); Ok(Box::new(RevWalkRevset { walk })) } RevsetExpression::DagRange { roots, heads } => { diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 772b244b0..d529a8d87 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -439,6 +439,76 @@ fn test_evaluate_expression_ancestors(use_git: bool) { tx.discard(); } +#[test_case(false ; "local store")] +#[test_case(true ; "git store")] +fn test_evaluate_expression_range(use_git: bool) { + let settings = testutils::user_settings(); + let (_temp_dir, repo) = testutils::init_repo(&settings, use_git); + + let mut tx = repo.start_transaction("test"); + let mut_repo = tx.mut_repo(); + + let commit1 = testutils::create_random_commit(&settings, &repo).write_to_repo(mut_repo); + let commit2 = testutils::create_random_commit(&settings, &repo) + .set_parents(vec![commit1.id().clone()]) + .write_to_repo(mut_repo); + let commit3 = testutils::create_random_commit(&settings, &repo) + .set_parents(vec![commit2.id().clone()]) + .write_to_repo(mut_repo); + let commit4 = testutils::create_random_commit(&settings, &repo) + .set_parents(vec![commit1.id().clone(), commit3.id().clone()]) + .write_to_repo(mut_repo); + + // The range from the root to the root is empty (because the left side of the + // range is exclusive) + assert_eq!( + resolve_commit_ids(mut_repo.as_repo_ref(), "root,,,root"), + vec![] + ); + + // Linear range + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("{},,,{}", commit1.id().hex(), commit3.id().hex()) + ), + vec![commit3.id().clone(), commit2.id().clone()] + ); + + // Empty range (descendant first) + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("{},,,{}", commit3.id().hex(), commit1.id().hex()) + ), + vec![] + ); + + // Range including a merge + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("{},,,{}", commit1.id().hex(), commit4.id().hex()) + ), + vec![ + commit4.id().clone(), + commit3.id().clone(), + commit2.id().clone() + ] + ); + + // Sibling commits + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("{},,,{}", commit2.id().hex(), commit3.id().hex()) + ), + vec![commit3.id().clone()] + ); + + tx.discard(); +} + #[test_case(false ; "local store")] #[test_case(true ; "git store")] fn test_evaluate_expression_dag_range(use_git: bool) {