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 fork_point function #4795

Merged
merged 1 commit into from
Nov 15, 2024
Merged
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 @@ -38,6 +38,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`--insert-before` options to customize the location of the duplicated
revisions.

* New `fork_point()` revset function can be used to obtain the fork point
of multiple commits.

### Fixed bugs

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

* `fork_point(x)`: The fork point of all commits in `x`. The fork point is the
common ancestor(s) of all commits in `x` which do not have any descendants
that are also common ancestors of all commits in `x`. It is equivalent to
the revset `heads(::x_1 & ::x_2 & ... & ::x_N)`, where `x_{1..N}` are commits
in `x`. If `x` resolves to a single commit, `fork_point(x)` resolves to `x`.

* `merges()`: Merge commits.

* `description(pattern)`: Commits that have a description matching the given
Expand Down Expand Up @@ -362,6 +368,18 @@ given [string pattern](#string-patterns).
* `roots(E|A)` ⇒ `{A}`
* `roots(A)` ⇒ `{A}`

**function** `fork_point()`

* `fork_point(E|D)` ⇒ `{A}`
* `fork_point(E|C)` ⇒ `{A}`
* `fork_point(E|B)` ⇒ `{B}`
* `fork_point(E|A)` ⇒ `{A}`
* `fork_point(D|C)` ⇒ `{C}`
* `fork_point(D|B)` ⇒ `{A}`
* `fork_point(B|C)` ⇒ `{A}`
* `fork_point(A)` ⇒ `{A}`
* `fork_point(none())` ⇒ `{}`

## String patterns

Functions that perform string matching support the following pattern syntax:
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::ForkPoint(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>),
ForkPoint(Rc<Self>),
Latest {
candidates: Rc<Self>,
count: usize,
Expand Down Expand Up @@ -427,6 +428,11 @@ impl<St: ExpressionState> RevsetExpression<St> {
})
}

/// Fork point (best common ancestors) of `self`.
pub fn fork_point(self: &Rc<Self>) -> Rc<Self> {
Rc::new(Self::ForkPoint(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>),
ForkPoint(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("fork_point", |diagnostics, function, context| {
let [expression_arg] = function.expect_exact_arguments()?;
let expression = lower_expression(diagnostics, expression_arg, context)?;
Ok(RevsetExpression::fork_point(&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::ForkPoint(expression) => {
transform_rec(expression, pre, post)?.map(RevsetExpression::ForkPoint)
}
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::ForkPoint(expression) => {
let expression = folder.fold_expression(expression)?;
RevsetExpression::ForkPoint(expression).into()
}
RevsetExpression::Latest { candidates, count } => {
let candidates = folder.fold_expression(candidates)?;
let count = *count;
Expand Down Expand Up @@ -2327,6 +2346,9 @@ impl VisibilityResolutionContext<'_> {
RevsetExpression::Roots(candidates) => {
ResolvedExpression::Roots(self.resolve(candidates).into())
}
RevsetExpression::ForkPoint(expression) => {
ResolvedExpression::ForkPoint(self.resolve(expression).into())
}
RevsetExpression::Latest { candidates, count } => ResolvedExpression::Latest {
candidates: self.resolve(candidates).into(),
count: *count,
Expand Down Expand Up @@ -2431,6 +2453,7 @@ impl VisibilityResolutionContext<'_> {
| RevsetExpression::Reachable { .. }
| RevsetExpression::Heads(_)
| RevsetExpression::Roots(_)
| RevsetExpression::ForkPoint(_)
| RevsetExpression::Latest { .. } => {
ResolvedPredicateExpression::Set(self.resolve(expression).into())
}
Expand Down
181 changes: 181 additions & 0 deletions lib/tests/test_revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,187 @@ fn test_evaluate_expression_latest() {
);
}

#[test]
fn test_evaluate_expression_fork_point() {
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, "fork_point(none())"), vec![]);
assert_eq!(
resolve_commit_ids(mut_repo, "fork_point(root())"),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("fork_point({})", commit1.id())),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("fork_point({})", commit2.id())),
vec![commit2.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("fork_point({})", commit3.id())),
vec![commit3.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("fork_point({})", commit4.id())),
vec![commit4.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("fork_point({})", commit5.id())),
vec![commit5.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, &format!("fork_point({})", commit6.id())),
vec![commit6.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit1.id(), commit2.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit2.id(), commit3.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!(
"fork_point({} | {} | {})",
commit1.id(),
commit2.id(),
commit3.id()
)
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit1.id(), commit4.id())
),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit2.id(), commit5.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit3.id(), commit6.id())
),
vec![root_commit.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit1.id(), commit5.id())
),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit4.id(), commit5.id())
),
vec![commit4.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo,
&format!("fork_point({} | {})", commit5.id(), commit6.id())
),
vec![commit4.id().clone()]
);
}

#[test]
fn test_evaluate_expression_fork_point_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!("fork_point({} | {})", commit3.id(), commit4.id())
),
vec![commit2.id().clone(), commit1.id().clone()]
);
}

#[test]
fn test_evaluate_expression_fork_point_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!("fork_point({} | {})", commit4.id(), commit5.id())
),
vec![commit2.id().clone()]
);
}

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