Skip to content

Commit

Permalink
revset: add fork_point function
Browse files Browse the repository at this point in the history
This can be used to find the fork point (best common ancestors) of a
revset with an arbitrary number of commits, which cannot be expressed
currently in the revset language.
  • Loading branch information
bnjmnt4n committed Nov 15, 2024
1 parent 1ed2ef0 commit 4db4f41
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 0 deletions.
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 }))
}
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

0 comments on commit 4db4f41

Please sign in to comment.