diff --git a/CHANGELOG.md b/CHANGELOG.md index 98757cdc7..f38fbf8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `ui.editor` config. There is also a new `$JJ_EDITOR` environment variable, which has even higher priority than the config. +* The new revset function `connected(x)` is the same as `x:x`. + ### 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 d3d9ef7c3..f1c32c428 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -85,6 +85,7 @@ revsets (expressions) as arguments. * `children(x)`: Same as `x+`. * `ancestors(x)`: Same as `:x`. * `descendants(x)`: Same as `x:`. +* `connected(x)`: Same as `x:x`. * `all()`: All visible commits in the repo. * `none()`: No commits. This function is rarely useful; it is provided for completeness. diff --git a/lib/src/revset.rs b/lib/src/revset.rs index cfc02f82f..eb9b3c5b7 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -330,6 +330,12 @@ impl RevsetExpression { }) } + /// Connects any ancestors and descendants in the set by adding the commits + /// between them. + pub fn connected(self: &Rc) -> Rc { + self.dag_range_to(self) + } + /// Commits reachable from `heads` but not from `self`. pub fn range( self: &Rc, @@ -600,6 +606,18 @@ fn parse_function_expression( }) } } + "connected" => { + if arg_count == 1 { + let candidates = + parse_expression_rule(argument_pairs.next().unwrap().into_inner())?; + Ok(candidates.connected()) + } else { + Err(RevsetParseError::InvalidFunctionArguments { + name, + message: "Expected 1 argument".to_string(), + }) + } + } "none" => { if arg_count == 0 { Ok(RevsetExpression::none()) @@ -1351,6 +1369,13 @@ mod tests { heads: checkout_symbol.clone(), }) ); + assert_eq!( + foo_symbol.connected(), + Rc::new(RevsetExpression::DagRange { + roots: foo_symbol.clone(), + heads: foo_symbol.clone(), + }) + ); assert_eq!( foo_symbol.range(&checkout_symbol), Rc::new(RevsetExpression::Range { diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index deb71eaca..b37522952 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -765,7 +765,7 @@ fn test_evaluate_expression_dag_range(use_git: bool) { mut_repo.as_repo_ref(), &format!("{}:{}", root_commit_id.hex(), commit2.id().hex()) ), - vec![commit2.id().clone(), commit1.id().clone(), root_commit_id,] + vec![commit2.id().clone(), commit1.id().clone(), root_commit_id] ); // Empty range @@ -792,7 +792,7 @@ fn test_evaluate_expression_dag_range(use_git: bool) { ] ); - // Including a merge, but only ancestors only from one side + // Including a merge, but ancestors only from one side assert_eq!( resolve_commit_ids( mut_repo.as_repo_ref(), @@ -806,6 +806,86 @@ fn test_evaluate_expression_dag_range(use_git: bool) { ); } +#[test_case(false ; "local backend")] +#[test_case(true ; "git backend")] +fn test_evaluate_expression_connected(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_id = repo.store().root_commit_id().clone(); + 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]); + let commit4 = graph_builder.commit_with_parents(&[&commit1]); + let commit5 = graph_builder.commit_with_parents(&[&commit3, &commit4]); + + // Connecting an empty set yields an empty set + assert_eq!( + resolve_commit_ids(mut_repo.as_repo_ref(), "connected(none())"), + vec![] + ); + + // Can connect just the root commit + assert_eq!( + resolve_commit_ids(mut_repo.as_repo_ref(), "connected(root)"), + vec![root_commit_id.clone()] + ); + + // Can connect linearly + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!( + "connected({} | {})", + root_commit_id.hex(), + commit2.id().hex() + ) + ), + vec![commit2.id().clone(), commit1.id().clone(), root_commit_id] + ); + + // Siblings don't get connected + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("connected({} | {})", commit2.id().hex(), commit4.id().hex()) + ), + vec![commit4.id().clone(), commit2.id().clone()] + ); + + // Including a merge + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("connected({} | {})", commit1.id().hex(), commit5.id().hex()) + ), + vec![ + commit5.id().clone(), + commit4.id().clone(), + commit3.id().clone(), + commit2.id().clone(), + commit1.id().clone(), + ] + ); + + // Including a merge, but ancestors only from one side + assert_eq!( + resolve_commit_ids( + mut_repo.as_repo_ref(), + &format!("connected({} | {})", commit2.id().hex(), commit5.id().hex()) + ), + vec![ + commit5.id().clone(), + commit3.id().clone(), + commit2.id().clone(), + ] + ); +} + #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_evaluate_expression_descendants(use_git: bool) {