Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

revset: add common_ancestors function #4795

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
[documentation](https://martinvonz.github.io/jj/latest/install-and-setup/#command-line-completion)
to activate them.

* New `common_ancestors()` revset function can be used to obtain the best common
ancestor of multiple commits.

### Fixed bugs

## [0.23.0] - 2024-11-06
Expand Down
2 changes: 2 additions & 0 deletions docs/revsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ revsets (expressions) as arguments.
* `latest(x[, count])`: Latest `count` commits in `x`, based on committer
timestamp. The default `count` is 1.

* `common_ancestors(x)`: The best common ancestors of all commits in `x`.

* `merges()`: Merge commits.

* `description(pattern)`: Commits that have a description matching the given
Expand Down
16 changes: 16 additions & 0 deletions lib/src/default_index/revset_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,22 @@ impl EvaluationContext<'_> {
});
Ok(Box::new(EagerRevset { positions }))
}
ResolvedExpression::CommonAncestors(expression) => {
let expression_set = self.evaluate(expression)?;
let mut expression_positions_iter = expression_set.positions().attach(index);
let Some(position) = expression_positions_iter.next() else {
return Ok(Box::new(EagerRevset::empty()));
};
let mut positions = vec![position?];
for position in expression_positions_iter {
positions = index
.common_ancestors_pos(&positions, [position?].as_slice())
.into_iter()
.collect_vec();
}
positions.reverse();
Ok(Box::new(EagerRevset { positions }))
bnjmnt4n marked this conversation as resolved.
Show resolved Hide resolved
}
ResolvedExpression::Latest { candidates, count } => {
let candidate_set = self.evaluate(candidates)?;
Ok(Box::new(self.take_latest_revset(&*candidate_set, *count)?))
Expand Down
23 changes: 23 additions & 0 deletions lib/src/revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ pub enum RevsetExpression<St: ExpressionState> {
},
Heads(Rc<Self>),
Roots(Rc<Self>),
CommonAncestors(Rc<Self>),
Latest {
candidates: Rc<Self>,
count: usize,
Expand Down Expand Up @@ -427,6 +428,11 @@ impl<St: ExpressionState> RevsetExpression<St> {
})
}

/// Best common ancestors of `self`.
pub fn common_ancestors(self: &Rc<Self>) -> Rc<Self> {
Rc::new(Self::CommonAncestors(self.clone()))
}

/// Filter all commits by `predicate` in `self`.
pub fn filtered(self: &Rc<Self>, predicate: RevsetFilterPredicate) -> Rc<Self> {
self.intersection(&Self::filter(predicate))
Expand Down Expand Up @@ -615,6 +621,7 @@ pub enum ResolvedExpression {
},
Heads(Box<Self>),
Roots(Box<Self>),
CommonAncestors(Box<Self>),
Latest {
candidates: Box<Self>,
count: usize,
Expand Down Expand Up @@ -800,6 +807,11 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
};
Ok(candidates.latest(count))
});
map.insert("common_ancestors", |diagnostics, function, context| {
let [expression_arg] = function.expect_exact_arguments()?;
let expression = lower_expression(diagnostics, expression_arg, context)?;
Ok(RevsetExpression::common_ancestors(&expression))
});
map.insert("merges", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::filter(
Expand Down Expand Up @@ -1247,6 +1259,9 @@ fn try_transform_expression<St: ExpressionState, E>(
RevsetExpression::Roots(candidates) => {
transform_rec(candidates, pre, post)?.map(RevsetExpression::Roots)
}
RevsetExpression::CommonAncestors(expression) => {
transform_rec(expression, pre, post)?.map(RevsetExpression::CommonAncestors)
}
RevsetExpression::Latest { candidates, count } => transform_rec(candidates, pre, post)?
.map(|candidates| RevsetExpression::Latest {
candidates,
Expand Down Expand Up @@ -1437,6 +1452,10 @@ where
let roots = folder.fold_expression(roots)?;
RevsetExpression::Roots(roots).into()
}
RevsetExpression::CommonAncestors(expression) => {
let expression = folder.fold_expression(expression)?;
RevsetExpression::CommonAncestors(expression).into()
}
RevsetExpression::Latest { candidates, count } => {
let candidates = folder.fold_expression(candidates)?;
let count = *count;
Expand Down Expand Up @@ -2326,6 +2345,9 @@ impl VisibilityResolutionContext<'_> {
RevsetExpression::Roots(candidates) => {
ResolvedExpression::Roots(self.resolve(candidates).into())
}
RevsetExpression::CommonAncestors(expression) => {
ResolvedExpression::CommonAncestors(self.resolve(expression).into())
}
RevsetExpression::Latest { candidates, count } => ResolvedExpression::Latest {
candidates: self.resolve(candidates).into(),
count: *count,
Expand Down Expand Up @@ -2430,6 +2452,7 @@ impl VisibilityResolutionContext<'_> {
| RevsetExpression::Reachable { .. }
| RevsetExpression::Heads(_)
| RevsetExpression::Roots(_)
| RevsetExpression::CommonAncestors(_)
| RevsetExpression::Latest { .. } => {
ResolvedPredicateExpression::Set(self.resolve(expression).into())
}
Expand Down
184 changes: 184 additions & 0 deletions lib/tests/test_revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,190 @@ fn test_evaluate_expression_latest() {
);
}

#[test]
fn test_evaluate_expression_common_ancestors() {
let settings = testutils::user_settings();
let test_repo = TestRepo::init();
let repo = &test_repo.repo;

// 5 6
// |/|
// 4 |
// | |
// 1 2 3
// | |/
// |/
// 0
let mut tx = repo.start_transaction(&settings);
let mut_repo = tx.repo_mut();
let mut graph_builder = CommitGraphBuilder::new(&settings, mut_repo);
let root_commit = repo.store().root_commit();
let commit1 = graph_builder.initial_commit();
let commit2 = graph_builder.initial_commit();
let commit3 = graph_builder.initial_commit();
let commit4 = graph_builder.commit_with_parents(&[&commit1]);
let commit5 = graph_builder.commit_with_parents(&[&commit4]);
let commit6 = graph_builder.commit_with_parents(&[&commit4, &commit2]);

assert_eq!(
resolve_commit_ids(mut_repo, "common_ancestors(none())"),
vec![]
);
assert_eq!(
resolve_commit_ids(mut_repo, "common_ancestors(root())"),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("common_ancestors({})", commit1.id())),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("common_ancestors({})", commit2.id())),
vec![commit2.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("common_ancestors({})", commit3.id())),
vec![commit3.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("common_ancestors({})", commit4.id())),
vec![commit4.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("common_ancestors({})", commit5.id())),
vec![commit5.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("common_ancestors({})", commit6.id())),
vec![commit6.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit1.id(), commit2.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit2.id(), commit3.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!(
"common_ancestors({} | {} | {})",
commit1.id(),
commit2.id(),
commit3.id()
)
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit1.id(), commit4.id())
),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit2.id(), commit5.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit3.id(), commit6.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit1.id(), commit5.id())
),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit4.id(), commit5.id())
),
vec![commit4.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit5.id(), commit6.id())
),
vec![commit4.id().clone()]
);
}

#[test]
fn test_evaluate_expression_common_ancestors_criss_cross() {
let settings = testutils::user_settings();
let test_repo = TestRepo::init();
let repo = &test_repo.repo;

// 3 4
// |X|
// 1 2
// |/
// 0
let mut tx = repo.start_transaction(&settings);
let mut_repo = tx.repo_mut();
let mut graph_builder = CommitGraphBuilder::new(&settings, mut_repo);
let commit1 = graph_builder.initial_commit();
let commit2 = graph_builder.initial_commit();
let commit3 = graph_builder.commit_with_parents(&[&commit1, &commit2]);
let commit4 = graph_builder.commit_with_parents(&[&commit1, &commit2]);

assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit3.id(), commit4.id())
),
vec![commit2.id().clone(), commit1.id().clone()]
);
}

#[test]
fn test_evaluate_expression_common_ancestors_merge_with_ancestor() {
let settings = testutils::user_settings();
let test_repo = TestRepo::init();
let repo = &test_repo.repo;

// 4 5
// |\ /|
// 1 2 3
// \|/
// 0
let mut tx = repo.start_transaction(&settings);
let mut_repo = tx.repo_mut();
let mut graph_builder = CommitGraphBuilder::new(&settings, mut_repo);
let commit1 = graph_builder.initial_commit();
let commit2 = graph_builder.initial_commit();
let commit3 = graph_builder.initial_commit();
let commit4 = graph_builder.commit_with_parents(&[&commit1, &commit2]);
let commit5 = graph_builder.commit_with_parents(&[&commit2, &commit3]);

assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("common_ancestors({} | {})", commit4.id(), commit5.id())
),
vec![commit2.id().clone()]
);
}

#[test]
fn test_evaluate_expression_merges() {
let settings = testutils::user_settings();
Expand Down