mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-01 00:50:57 +00:00
revset: implement a 'reachable(src, domain)' expression
This revset correctly implements "reachability" from a set of source commits following both parent and child edges as far as they can go within a domain set. This type of 'bfs' query is currently impossible to express with existing revset functions.
This commit is contained in:
parent
5125eab505
commit
a49da4ad01
6 changed files with 235 additions and 3 deletions
|
@ -111,6 +111,9 @@ to avoid letting the user edit the immutable one.
|
||||||
* `jj rebase -r` now accepts `--insert-after` and `--insert-before` options to
|
* `jj rebase -r` now accepts `--insert-after` and `--insert-before` options to
|
||||||
customize the location of the rebased revisions.
|
customize the location of the rebased revisions.
|
||||||
|
|
||||||
|
* A new revset `reahable(srcs, domain)` will return all commits that are
|
||||||
|
reachable from `srcs` within `domain`.
|
||||||
|
|
||||||
### Fixed bugs
|
### Fixed bugs
|
||||||
|
|
||||||
* Revsets now support `\`-escapes in string literal.
|
* Revsets now support `\`-escapes in string literal.
|
||||||
|
|
|
@ -278,7 +278,7 @@ fn test_function_name_hint() {
|
||||||
| ^----^
|
| ^----^
|
||||||
|
|
|
|
||||||
= Function "branch" doesn't exist
|
= Function "branch" doesn't exist
|
||||||
Hint: Did you mean "branches"?
|
Hint: Did you mean "branches", "reachable"?
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// Both builtin function and function alias should be suggested
|
// Both builtin function and function alias should be suggested
|
||||||
|
@ -308,7 +308,7 @@ fn test_function_name_hint() {
|
||||||
| ^----^
|
| ^----^
|
||||||
|
|
|
|
||||||
= Function "branch" doesn't exist
|
= Function "branch" doesn't exist
|
||||||
Hint: Did you mean "branches"?
|
Hint: Did you mean "branches", "reachable"?
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,9 @@ revsets (expressions) as arguments.
|
||||||
|
|
||||||
* `descendants(x)`: Same as `x::`.
|
* `descendants(x)`: Same as `x::`.
|
||||||
|
|
||||||
|
* `reachable(srcs, domain)`: All commits reachable from `srcs` within
|
||||||
|
`domain`, traversing all parent and child edges.
|
||||||
|
|
||||||
* `connected(x)`: Same as `x::x`. Useful when `x` includes several commits.
|
* `connected(x)`: Same as `x::x`. Useful when `x` includes several commits.
|
||||||
|
|
||||||
* `all()`: All visible commits in the repo.
|
* `all()`: All visible commits in the repo.
|
||||||
|
|
|
@ -35,8 +35,8 @@ use crate::revset::{
|
||||||
RevsetFilterExtensionWrapper, RevsetFilterPredicate, GENERATION_RANGE_FULL,
|
RevsetFilterExtensionWrapper, RevsetFilterPredicate, GENERATION_RANGE_FULL,
|
||||||
};
|
};
|
||||||
use crate::revset_graph::RevsetGraphEdge;
|
use crate::revset_graph::RevsetGraphEdge;
|
||||||
use crate::rewrite;
|
|
||||||
use crate::store::Store;
|
use crate::store::Store;
|
||||||
|
use crate::{rewrite, union_find};
|
||||||
|
|
||||||
type BoxedPredicateFn<'a> = Box<dyn FnMut(&CompositeIndex, IndexPosition) -> bool + 'a>;
|
type BoxedPredicateFn<'a> = Box<dyn FnMut(&CompositeIndex, IndexPosition) -> bool + 'a>;
|
||||||
pub(super) type BoxedRevWalk<'a> = Box<dyn RevWalk<CompositeIndex, Item = IndexPosition> + 'a>;
|
pub(super) type BoxedRevWalk<'a> = Box<dyn RevWalk<CompositeIndex, Item = IndexPosition> + 'a>;
|
||||||
|
@ -829,6 +829,37 @@ impl<'index> EvaluationContext<'index> {
|
||||||
Ok(Box::new(EagerRevset { positions }))
|
Ok(Box::new(EagerRevset { positions }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResolvedExpression::Reachable { sources, domain } => {
|
||||||
|
let mut sets = union_find::UnionFind::<IndexPosition>::new();
|
||||||
|
|
||||||
|
// Compute all reachable subgraphs.
|
||||||
|
let domain_revset = self.evaluate(domain)?;
|
||||||
|
let domain_vec = domain_revset.positions().attach(index).collect_vec();
|
||||||
|
let domain_set: HashSet<_> = domain_vec.iter().copied().collect();
|
||||||
|
for pos in &domain_set {
|
||||||
|
for parent_pos in index.entry_by_pos(*pos).parent_positions() {
|
||||||
|
if domain_set.contains(&parent_pos) {
|
||||||
|
sets.union(*pos, parent_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify disjoint sets reachable from sources.
|
||||||
|
let set_reps: HashSet<_> = intersection_by(
|
||||||
|
self.evaluate(sources)?.positions(),
|
||||||
|
EagerRevWalk::new(domain_vec.iter().copied()),
|
||||||
|
|pos1, pos2| pos1.cmp(pos2).reverse(),
|
||||||
|
)
|
||||||
|
.attach(index)
|
||||||
|
.map(|pos| sets.find(pos))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let positions = domain_vec
|
||||||
|
.into_iter()
|
||||||
|
.filter(|pos| set_reps.contains(&sets.find(*pos)))
|
||||||
|
.collect_vec();
|
||||||
|
Ok(Box::new(EagerRevset { positions }))
|
||||||
|
}
|
||||||
ResolvedExpression::Heads(candidates) => {
|
ResolvedExpression::Heads(candidates) => {
|
||||||
let candidate_set = self.evaluate(candidates)?;
|
let candidate_set = self.evaluate(candidates)?;
|
||||||
let head_positions: BTreeSet<_> =
|
let head_positions: BTreeSet<_> =
|
||||||
|
|
|
@ -190,6 +190,11 @@ pub enum RevsetExpression {
|
||||||
heads: Rc<RevsetExpression>,
|
heads: Rc<RevsetExpression>,
|
||||||
// TODO: maybe add generation_from_roots/heads?
|
// TODO: maybe add generation_from_roots/heads?
|
||||||
},
|
},
|
||||||
|
// Commits reachable from "sources" within "domain"
|
||||||
|
Reachable {
|
||||||
|
sources: Rc<RevsetExpression>,
|
||||||
|
domain: Rc<RevsetExpression>,
|
||||||
|
},
|
||||||
Heads(Rc<RevsetExpression>),
|
Heads(Rc<RevsetExpression>),
|
||||||
Roots(Rc<RevsetExpression>),
|
Roots(Rc<RevsetExpression>),
|
||||||
Latest {
|
Latest {
|
||||||
|
@ -379,6 +384,18 @@ impl RevsetExpression {
|
||||||
self.dag_range_to(self)
|
self.dag_range_to(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All commits within `domain` reachable from this set of commits, by
|
||||||
|
/// traversing either parent or child edges.
|
||||||
|
pub fn reachable(
|
||||||
|
self: &Rc<RevsetExpression>,
|
||||||
|
domain: &Rc<RevsetExpression>,
|
||||||
|
) -> Rc<RevsetExpression> {
|
||||||
|
Rc::new(RevsetExpression::Reachable {
|
||||||
|
sources: self.clone(),
|
||||||
|
domain: domain.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Commits reachable from `heads` but not from `self`.
|
/// Commits reachable from `heads` but not from `self`.
|
||||||
pub fn range(
|
pub fn range(
|
||||||
self: &Rc<RevsetExpression>,
|
self: &Rc<RevsetExpression>,
|
||||||
|
@ -507,6 +524,11 @@ pub enum ResolvedExpression {
|
||||||
heads: Box<ResolvedExpression>,
|
heads: Box<ResolvedExpression>,
|
||||||
generation_from_roots: Range<u64>,
|
generation_from_roots: Range<u64>,
|
||||||
},
|
},
|
||||||
|
/// Commits reachable from `sources` within `domain`.
|
||||||
|
Reachable {
|
||||||
|
sources: Box<ResolvedExpression>,
|
||||||
|
domain: Box<ResolvedExpression>,
|
||||||
|
},
|
||||||
Heads(Box<ResolvedExpression>),
|
Heads(Box<ResolvedExpression>),
|
||||||
Roots(Box<ResolvedExpression>),
|
Roots(Box<ResolvedExpression>),
|
||||||
Latest {
|
Latest {
|
||||||
|
@ -635,6 +657,12 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
|
||||||
let candidates = parse_expression_rule(arg.into_inner(), state)?;
|
let candidates = parse_expression_rule(arg.into_inner(), state)?;
|
||||||
Ok(candidates.connected())
|
Ok(candidates.connected())
|
||||||
});
|
});
|
||||||
|
map.insert("reachable", |name, arguments_pair, state| {
|
||||||
|
let ([source_arg, domain_arg], []) = expect_arguments(name, arguments_pair)?;
|
||||||
|
let sources = parse_expression_rule(source_arg.into_inner(), state)?;
|
||||||
|
let domain = parse_expression_rule(domain_arg.into_inner(), state)?;
|
||||||
|
Ok(sources.reachable(&domain))
|
||||||
|
});
|
||||||
map.insert("none", |name, arguments_pair, _state| {
|
map.insert("none", |name, arguments_pair, _state| {
|
||||||
expect_no_arguments(name, arguments_pair)?;
|
expect_no_arguments(name, arguments_pair)?;
|
||||||
Ok(RevsetExpression::none())
|
Ok(RevsetExpression::none())
|
||||||
|
@ -960,6 +988,10 @@ fn try_transform_expression<E>(
|
||||||
transform_rec_pair((roots, heads), pre, post)?
|
transform_rec_pair((roots, heads), pre, post)?
|
||||||
.map(|(roots, heads)| RevsetExpression::DagRange { roots, heads })
|
.map(|(roots, heads)| RevsetExpression::DagRange { roots, heads })
|
||||||
}
|
}
|
||||||
|
RevsetExpression::Reachable { sources, domain } => {
|
||||||
|
transform_rec_pair((sources, domain), pre, post)?
|
||||||
|
.map(|(sources, domain)| RevsetExpression::Reachable { sources, domain })
|
||||||
|
}
|
||||||
RevsetExpression::Heads(candidates) => {
|
RevsetExpression::Heads(candidates) => {
|
||||||
transform_rec(candidates, pre, post)?.map(RevsetExpression::Heads)
|
transform_rec(candidates, pre, post)?.map(RevsetExpression::Heads)
|
||||||
}
|
}
|
||||||
|
@ -1748,6 +1780,10 @@ impl VisibilityResolutionContext<'_> {
|
||||||
heads: self.resolve(heads).into(),
|
heads: self.resolve(heads).into(),
|
||||||
generation_from_roots: GENERATION_RANGE_FULL,
|
generation_from_roots: GENERATION_RANGE_FULL,
|
||||||
},
|
},
|
||||||
|
RevsetExpression::Reachable { sources, domain } => ResolvedExpression::Reachable {
|
||||||
|
sources: self.resolve(sources).into(),
|
||||||
|
domain: self.resolve(domain).into(),
|
||||||
|
},
|
||||||
RevsetExpression::Heads(candidates) => {
|
RevsetExpression::Heads(candidates) => {
|
||||||
ResolvedExpression::Heads(self.resolve(candidates).into())
|
ResolvedExpression::Heads(self.resolve(candidates).into())
|
||||||
}
|
}
|
||||||
|
@ -1833,6 +1869,7 @@ impl VisibilityResolutionContext<'_> {
|
||||||
| RevsetExpression::Descendants { .. }
|
| RevsetExpression::Descendants { .. }
|
||||||
| RevsetExpression::Range { .. }
|
| RevsetExpression::Range { .. }
|
||||||
| RevsetExpression::DagRange { .. }
|
| RevsetExpression::DagRange { .. }
|
||||||
|
| RevsetExpression::Reachable { .. }
|
||||||
| RevsetExpression::Heads(_)
|
| RevsetExpression::Heads(_)
|
||||||
| RevsetExpression::Roots(_)
|
| RevsetExpression::Roots(_)
|
||||||
| RevsetExpression::Latest { .. } => {
|
| RevsetExpression::Latest { .. } => {
|
||||||
|
|
|
@ -1556,6 +1556,164 @@ fn test_evaluate_expression_connected() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_evaluate_expression_reachable() {
|
||||||
|
let settings = testutils::user_settings();
|
||||||
|
let test_repo = TestRepo::init();
|
||||||
|
let repo = &test_repo.repo;
|
||||||
|
|
||||||
|
let mut tx = repo.start_transaction(&settings);
|
||||||
|
let mut_repo = tx.mut_repo();
|
||||||
|
|
||||||
|
// Construct 3 separate subgraphs off the root commit.
|
||||||
|
// 1 is a chain, 2 is a merge, 3 is a pyramidal monstrosity
|
||||||
|
let graph1commit1 = write_random_commit(mut_repo, &settings);
|
||||||
|
let graph1commit2 = create_random_commit(mut_repo, &settings)
|
||||||
|
.set_parents(vec![graph1commit1.id().clone()])
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let graph1commit3 = create_random_commit(mut_repo, &settings)
|
||||||
|
.set_parents(vec![graph1commit2.id().clone()])
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let graph2commit1 = write_random_commit(mut_repo, &settings);
|
||||||
|
let graph2commit2 = write_random_commit(mut_repo, &settings);
|
||||||
|
let graph2commit3 = create_random_commit(mut_repo, &settings)
|
||||||
|
.set_parents(vec![graph2commit1.id().clone(), graph2commit2.id().clone()])
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let graph3commit1 = write_random_commit(mut_repo, &settings);
|
||||||
|
let graph3commit2 = write_random_commit(mut_repo, &settings);
|
||||||
|
let graph3commit3 = write_random_commit(mut_repo, &settings);
|
||||||
|
let graph3commit4 = create_random_commit(mut_repo, &settings)
|
||||||
|
.set_parents(vec![graph3commit1.id().clone(), graph3commit2.id().clone()])
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let graph3commit5 = create_random_commit(mut_repo, &settings)
|
||||||
|
.set_parents(vec![graph3commit2.id().clone(), graph3commit3.id().clone()])
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let graph3commit6 = create_random_commit(mut_repo, &settings)
|
||||||
|
.set_parents(vec![graph3commit3.id().clone()])
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
let graph3commit7 = create_random_commit(mut_repo, &settings)
|
||||||
|
.set_parents(vec![graph3commit4.id().clone(), graph3commit5.id().clone()])
|
||||||
|
.write()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Domain is respected.
|
||||||
|
assert_eq!(
|
||||||
|
resolve_commit_ids(
|
||||||
|
mut_repo,
|
||||||
|
&format!(
|
||||||
|
"reachable({}, all() ~ ::{})",
|
||||||
|
graph1commit2.id().hex(),
|
||||||
|
graph1commit1.id().hex()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vec![graph1commit3.id().clone(), graph1commit2.id().clone(),]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_commit_ids(
|
||||||
|
mut_repo,
|
||||||
|
&format!(
|
||||||
|
"reachable({}, all() ~ ::{})",
|
||||||
|
graph1commit2.id().hex(),
|
||||||
|
graph1commit3.id().hex()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vec![]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Each graph is identifiable from any node in it.
|
||||||
|
for (i, commit) in [&graph1commit1, &graph1commit2, &graph1commit3]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
resolve_commit_ids(
|
||||||
|
mut_repo,
|
||||||
|
&format!("reachable({}, all() ~ root())", commit.id().hex())
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
graph1commit3.id().clone(),
|
||||||
|
graph1commit2.id().clone(),
|
||||||
|
graph1commit1.id().clone(),
|
||||||
|
],
|
||||||
|
"commit {}",
|
||||||
|
i + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, commit) in [&graph2commit1, &graph2commit2, &graph2commit3]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
resolve_commit_ids(
|
||||||
|
mut_repo,
|
||||||
|
&format!("reachable({}, all() ~ root())", commit.id().hex())
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
graph2commit3.id().clone(),
|
||||||
|
graph2commit2.id().clone(),
|
||||||
|
graph2commit1.id().clone(),
|
||||||
|
],
|
||||||
|
"commit {}",
|
||||||
|
i + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, commit) in [
|
||||||
|
&graph3commit1,
|
||||||
|
&graph3commit2,
|
||||||
|
&graph3commit3,
|
||||||
|
&graph3commit4,
|
||||||
|
&graph3commit5,
|
||||||
|
&graph3commit6,
|
||||||
|
&graph3commit7,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
resolve_commit_ids(
|
||||||
|
mut_repo,
|
||||||
|
&format!("reachable({}, all() ~ root())", commit.id().hex())
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
graph3commit7.id().clone(),
|
||||||
|
graph3commit6.id().clone(),
|
||||||
|
graph3commit5.id().clone(),
|
||||||
|
graph3commit4.id().clone(),
|
||||||
|
graph3commit3.id().clone(),
|
||||||
|
graph3commit2.id().clone(),
|
||||||
|
graph3commit1.id().clone(),
|
||||||
|
],
|
||||||
|
"commit {}",
|
||||||
|
i + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test a split of the pyramidal monstrosity.
|
||||||
|
assert_eq!(
|
||||||
|
resolve_commit_ids(
|
||||||
|
mut_repo,
|
||||||
|
&format!(
|
||||||
|
"reachable({}, all() ~ ::{})",
|
||||||
|
graph3commit4.id().hex(),
|
||||||
|
graph3commit5.id().hex()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vec![
|
||||||
|
graph3commit7.id().clone(),
|
||||||
|
graph3commit4.id().clone(),
|
||||||
|
graph3commit1.id().clone(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_evaluate_expression_descendants() {
|
fn test_evaluate_expression_descendants() {
|
||||||
let settings = testutils::user_settings();
|
let settings = testutils::user_settings();
|
||||||
|
|
Loading…
Reference in a new issue