diff --git a/lib/src/revset.pest b/lib/src/revset.pest index 05ee10bec..4f507edfa 100644 --- a/lib/src/revset.pest +++ b/lib/src/revset.pest @@ -20,8 +20,9 @@ parents = { ":" } ancestors = { "*:" } prefix_operator = _{ parents | ancestors } +union = { "|" } difference = { "-" } -infix_operator = _{ difference } +infix_operator = _{ union| difference } function_name = @{ (ASCII_ALPHANUMERIC | "_")+ } // The grammar accepts a string literal or an expression for function diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 64fd14ff8..a79116b6d 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -107,6 +107,7 @@ pub enum RevsetExpression { needle: String, base_expression: Box, }, + Union(Box, Box), Difference(Box, Box), } @@ -131,6 +132,9 @@ fn parse_infix_expression_rule( while let Some(operator) = pairs.next() { let expression2 = parse_prefix_expression_rule(pairs.next().unwrap().into_inner())?; match operator.as_rule() { + Rule::union => { + expression1 = RevsetExpression::Union(Box::new(expression1), Box::new(expression2)) + } Rule::difference => { expression1 = RevsetExpression::Difference(Box::new(expression1), Box::new(expression2)) @@ -398,6 +402,44 @@ impl<'repo> Iterator for RevWalkRevsetIterator<'repo> { } } +struct UnionRevset<'revset, 'repo: 'revset> { + set1: Box + 'revset>, + set2: Box + 'revset>, +} + +impl<'repo> Revset<'repo> for UnionRevset<'_, 'repo> { + fn iter<'revset>(&'revset self) -> Box> + 'revset> { + Box::new(UnionRevsetIterator { + iter1: self.set1.iter().peekable(), + iter2: self.set2.iter().peekable(), + }) + } +} + +struct UnionRevsetIterator<'revset, 'repo> { + iter1: Peekable> + 'revset>>, + iter2: Peekable> + 'revset>>, +} + +impl<'revset, 'repo> Iterator for UnionRevsetIterator<'revset, 'repo> { + type Item = IndexEntry<'repo>; + + fn next(&mut self) -> Option { + match (self.iter1.peek(), self.iter2.peek()) { + (None, _) => self.iter2.next(), + (_, None) => self.iter1.next(), + (Some(entry1), Some(entry2)) => match entry1.position().cmp(&entry2.position()) { + Ordering::Less => self.iter2.next(), + Ordering::Equal => { + self.iter1.next(); + self.iter2.next() + } + Ordering::Greater => self.iter1.next(), + }, + } + } +} + struct DifferenceRevset<'revset, 'repo: 'revset> { // The minuend (what to subtract from) set1: Box + 'revset>, @@ -512,6 +554,11 @@ pub fn evaluate_expression<'revset, 'repo: 'revset>( index_entries.sort_by_key(|b| Reverse(b.position())); Ok(Box::new(EagerRevset { index_entries })) } + RevsetExpression::Union(expression1, expression2) => { + let set1 = evaluate_expression(repo, expression1.as_ref())?; + let set2 = evaluate_expression(repo, expression2.as_ref())?; + Ok(Box::new(UnionRevset { set1, set2 })) + } RevsetExpression::Difference(expression1, expression2) => { let set1 = evaluate_expression(repo, expression1.as_ref())?; let set2 = evaluate_expression(repo, expression2.as_ref())?; diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 96b70776e..6c7b3cc4c 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -394,6 +394,7 @@ fn test_evaluate_expression_parents(use_git: bool) { let mut tx = repo.start_transaction("test"); let mut_repo = tx.mut_repo(); + let root_commit = repo.store().root_commit(); 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()]) @@ -402,6 +403,9 @@ fn test_evaluate_expression_parents(use_git: bool) { let commit4 = testutils::create_random_commit(&settings, &repo) .set_parents(vec![commit2.id().clone(), commit3.id().clone()]) .write_to_repo(mut_repo); + let commit5 = testutils::create_random_commit(&settings, &repo) + .set_parents(vec![commit2.id().clone()]) + .write_to_repo(mut_repo); // The root commit has no parents assert_eq!(resolve_commit_ids(mut_repo.as_repo_ref(), ":root"), vec![]); @@ -416,7 +420,34 @@ fn test_evaluate_expression_parents(use_git: bool) { // Can find parents of a merge commit assert_eq!( resolve_commit_ids(mut_repo.as_repo_ref(), &format!(":{}", commit4.id().hex())), - vec![commit3.id().clone(), commit2.id().clone(),] + vec![commit3.id().clone(), commit2.id().clone()] + ); + + // Parents of all commits in input are returned + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!(":({} | {})", commit2.id().hex(), commit3.id().hex()) + ), + vec![commit1.id().clone(), root_commit.id().clone()] + ); + + // Parents already in input set are returned + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!(":*:{}", commit2.id().hex()) + ), + vec![commit1.id().clone(), root_commit.id().clone()] + ); + + // Parents shared among commits in input are not repeated + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!(":({} | {})", commit4.id().hex(), commit5.id().hex()) + ), + vec![commit3.id().clone(), commit2.id().clone()] ); tx.discard(); @@ -582,6 +613,88 @@ fn test_evaluate_expression_description(use_git: bool) { tx.discard(); } +#[test_case(false ; "local store")] +#[test_case(true ; "git store")] +fn test_evaluate_expression_union(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 root_commit = repo.store().root_commit(); + 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![commit3.id().clone()]) + .write_to_repo(mut_repo); + let commit5 = testutils::create_random_commit(&settings, &repo) + .set_parents(vec![commit2.id().clone()]) + .write_to_repo(mut_repo); + + // Union between ancestors + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("*:{} | *:{}", commit4.id().hex(), commit5.id().hex()) + ), + vec![ + commit5.id().clone(), + commit4.id().clone(), + commit3.id().clone(), + commit2.id().clone(), + commit1.id().clone(), + root_commit.id().clone() + ] + ); + + // Unioning can add back commits removed by difference + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!( + "(*:{} - *:{}) | *:{}", + commit4.id().hex(), + commit2.id().hex(), + commit5.id().hex() + ) + ), + vec![ + commit5.id().clone(), + commit4.id().clone(), + commit3.id().clone(), + commit2.id().clone(), + commit1.id().clone(), + root_commit.id().clone(), + ] + ); + + // Unioning of disjoint sets + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!( + "(*:{} - *:{}) | {}", + commit4.id().hex(), + commit2.id().hex(), + commit5.id().hex(), + ) + ), + vec![ + commit5.id().clone(), + commit4.id().clone(), + commit3.id().clone() + ] + ); + + tx.discard(); +} + #[test_case(false ; "local store")] #[test_case(true ; "git store")] fn test_evaluate_expression_difference(use_git: bool) {