mirror of
https://github.com/jj-vcs/jj.git
synced 2026-07-03 14:02:54 +08:00
revset: forks() function to get all visible commits with multiple children
`forks()` is especially useful in combination with `visible_heads()` and `merges()` to get a topological overview of the revset. The initial implementation of this took a revset argument, with `forks()` mapping to `forks(all())`. After a discussion[1] this was decided against, since it's not obvious whether it should trace through nodes in the global graph but not the subgraph `x`. [1]: https://github.com/jj-vcs/jj/pull/9678#discussion_r3467851229
This commit is contained in:
committed by
Yuya Nishihara
parent
6a4ae89551
commit
e0ef08ed38
@@ -49,6 +49,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
existed in a stack, those rewritten revisions and their descendants wouldn't
|
||||
always be rebased. Note that immutable descendants will not be rebased.
|
||||
|
||||
* Add a `forks()` revset function that yields all commits with more than 1 child.
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
* On Windows, querying a path's file identity no longer follows symbolic links,
|
||||
|
||||
@@ -361,6 +361,8 @@ revsets (expressions) as arguments.
|
||||
|
||||
* `merges()`: Merge commits.
|
||||
|
||||
* `forks()`: Fork commits, i.e. those with more than 1 child.
|
||||
|
||||
* `description(pattern)`: Commits that have a description matching the given
|
||||
[string pattern](#string-patterns).
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use std::cmp::Ordering;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::iter;
|
||||
@@ -970,6 +971,26 @@ impl EvaluationContext<'_> {
|
||||
});
|
||||
Ok(Box::new(EagerRevset { positions }))
|
||||
}
|
||||
ResolvedExpression::Forks { heads } => {
|
||||
let head_positions = self
|
||||
.evaluate(heads)?
|
||||
.positions()
|
||||
.attach(index)
|
||||
.try_collect()?;
|
||||
let mut child_counts: HashMap<GlobalCommitPosition, u32> = HashMap::new();
|
||||
let walk = RevWalkBuilder::new(index)
|
||||
.wanted_heads(head_positions)
|
||||
.ancestors()
|
||||
.detach()
|
||||
.filter_map(move |index: &CompositeIndex, pos| {
|
||||
let is_fork = child_counts.remove(&pos).unwrap_or(0) >= 2;
|
||||
for parent in index.commits().entry_by_pos(pos).parent_positions() {
|
||||
*child_counts.entry(parent).or_insert(0) += 1;
|
||||
}
|
||||
is_fork.then_some(pos)
|
||||
});
|
||||
Ok(Box::new(RevWalkRevset { walk }))
|
||||
}
|
||||
ResolvedExpression::ForkPoint(expression) => {
|
||||
let expression_set = self.evaluate(expression)?;
|
||||
let mut expression_positions_iter = expression_set.positions().attach(index);
|
||||
|
||||
@@ -313,6 +313,7 @@ pub enum RevsetExpression<St: ExpressionState> {
|
||||
},
|
||||
Roots(Arc<Self>),
|
||||
ForkPoint(Arc<Self>),
|
||||
Forks,
|
||||
Bisect(Arc<Self>),
|
||||
HasSize {
|
||||
candidates: Arc<Self>,
|
||||
@@ -377,6 +378,10 @@ impl<St: ExpressionState> RevsetExpression<St> {
|
||||
Arc::new(Self::Root)
|
||||
}
|
||||
|
||||
pub fn forks() -> Arc<Self> {
|
||||
Arc::new(Self::Forks)
|
||||
}
|
||||
|
||||
pub fn commit(commit_id: CommitId) -> Arc<Self> {
|
||||
Self::commits(vec![commit_id])
|
||||
}
|
||||
@@ -758,6 +763,9 @@ pub enum ResolvedExpression {
|
||||
filter: Option<ResolvedPredicateExpression>,
|
||||
},
|
||||
Roots(Box<Self>),
|
||||
Forks {
|
||||
heads: Box<Self>,
|
||||
},
|
||||
ForkPoint(Box<Self>),
|
||||
Bisect(Box<Self>),
|
||||
HasSize {
|
||||
@@ -1000,6 +1008,10 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
|
||||
RevsetFilterPredicate::ParentCount(2..u32::MAX),
|
||||
))
|
||||
});
|
||||
map.insert("forks", |_diagnostics, function, _context| {
|
||||
function.expect_no_arguments()?;
|
||||
Ok(RevsetExpression::forks())
|
||||
});
|
||||
map.insert("description", |diagnostics, function, _context| {
|
||||
let [arg] = function.expect_exact_arguments()?;
|
||||
let expr = expect_string_expression(diagnostics, arg)?;
|
||||
@@ -1559,6 +1571,7 @@ fn try_transform_expression<St: ExpressionState, E>(
|
||||
RevsetExpression::Roots(candidates) => {
|
||||
transform_rec(candidates, pre, post)?.map(RevsetExpression::Roots)
|
||||
}
|
||||
RevsetExpression::Forks => None,
|
||||
RevsetExpression::ForkPoint(expression) => {
|
||||
transform_rec(expression, pre, post)?.map(RevsetExpression::ForkPoint)
|
||||
}
|
||||
@@ -1805,6 +1818,7 @@ where
|
||||
let roots = folder.fold_expression(roots)?;
|
||||
RevsetExpression::Roots(roots).into()
|
||||
}
|
||||
RevsetExpression::Forks => RevsetExpression::Forks.into(),
|
||||
RevsetExpression::ForkPoint(expression) => {
|
||||
let expression = folder.fold_expression(expression)?;
|
||||
RevsetExpression::ForkPoint(expression).into()
|
||||
@@ -3154,6 +3168,9 @@ impl VisibilityResolutionContext<'_> {
|
||||
RevsetExpression::Roots(candidates) => {
|
||||
ResolvedExpression::Roots(self.resolve(candidates).into())
|
||||
}
|
||||
RevsetExpression::Forks => ResolvedExpression::Forks {
|
||||
heads: self.resolve_visible_heads_or_referenced().into(),
|
||||
},
|
||||
RevsetExpression::ForkPoint(expression) => {
|
||||
ResolvedExpression::ForkPoint(self.resolve(expression).into())
|
||||
}
|
||||
@@ -3293,6 +3310,7 @@ impl VisibilityResolutionContext<'_> {
|
||||
| RevsetExpression::Heads(_)
|
||||
| RevsetExpression::HeadsRange { .. }
|
||||
| RevsetExpression::Roots(_)
|
||||
| RevsetExpression::Forks
|
||||
| RevsetExpression::ForkPoint(_)
|
||||
| RevsetExpression::Bisect(_)
|
||||
| RevsetExpression::HasSize { .. }
|
||||
|
||||
@@ -1419,6 +1419,64 @@ fn test_evaluate_expression_roots() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_expression_forks() {
|
||||
let test_repo = TestRepo::init();
|
||||
let repo = &test_repo.repo;
|
||||
|
||||
let root_commit = repo.store().root_commit();
|
||||
let mut tx = repo.start_transaction();
|
||||
let mut_repo = tx.repo_mut();
|
||||
/*
|
||||
* 9 <- merge
|
||||
* /|\
|
||||
* 6 7 8
|
||||
* \|/
|
||||
* 5 <- 3-way fork
|
||||
* /|
|
||||
* 3 4 <- not a fork
|
||||
* |/
|
||||
* 1 2 <- 2-way fork
|
||||
* |/
|
||||
* 0 <- 2-way fork from root
|
||||
*/
|
||||
let commit1 = write_random_commit(mut_repo);
|
||||
let commit2 = write_random_commit(mut_repo);
|
||||
let commit3 = write_random_commit_with_parents(mut_repo, &[&commit1]);
|
||||
let commit4 = write_random_commit_with_parents(mut_repo, &[&commit1]);
|
||||
let commit5 = write_random_commit_with_parents(mut_repo, &[&commit3, &commit4]);
|
||||
let commit6 = write_random_commit_with_parents(mut_repo, &[&commit5]);
|
||||
let commit7 = write_random_commit_with_parents(mut_repo, &[&commit5]);
|
||||
let commit8 = write_random_commit_with_parents(mut_repo, &[&commit5]);
|
||||
let _commit9 = write_random_commit_with_parents(mut_repo, &[&commit6, &commit7, &commit8]);
|
||||
|
||||
// In the above graph, the forks are 0, 1, and 5
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "forks()"),
|
||||
vec![
|
||||
commit5.id().clone(),
|
||||
commit1.id().clone(),
|
||||
root_commit.id().clone(),
|
||||
]
|
||||
);
|
||||
|
||||
// 5 is a merge and a fork
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "merges() & forks()"),
|
||||
vec![commit5.id().clone()]
|
||||
);
|
||||
|
||||
mut_repo.record_abandoned_commit(&commit2);
|
||||
mut_repo.record_abandoned_commit(&commit7);
|
||||
mut_repo.rebase_descendants().block_on().unwrap();
|
||||
|
||||
// After abandoning 2 and 7, only the root is no longer a fork.
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "forks()"),
|
||||
vec![commit5.id().clone(), commit1.id().clone(),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_expression_parents() -> TestResult {
|
||||
let test_workspace = TestWorkspace::init();
|
||||
|
||||
@@ -88,6 +88,7 @@ fn arb_expression(
|
||||
Just(RevsetExpression::all()),
|
||||
Just(RevsetExpression::visible_heads()),
|
||||
Just(RevsetExpression::root()),
|
||||
Just(RevsetExpression::forks()),
|
||||
proptest::sample::subsequence(known_commits, 1..=5.min(max_commits))
|
||||
.prop_map(RevsetExpression::commits),
|
||||
// Use merges() as a filter that isn't constant. Since we don't have an
|
||||
|
||||
Reference in New Issue
Block a user