mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-01 00:50:57 +00:00
revset: add coalesce(revsets...)
The `coalesce` function takes a list of revsets and returns the commits in the first revset in the list which evalutes to a non-empty set of commits. It can be used to display fallbacks if a certain commit cannot be found, e.g. `coalesce(present(user_configured_trunk), builtin_trunk)`.
This commit is contained in:
parent
eaab8bbc36
commit
8e817bc24b
5 changed files with 159 additions and 0 deletions
|
@ -56,6 +56,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||
The output can be customized via the `templates.annotate_commit_summary`
|
||||
config variable.
|
||||
|
||||
* New `coalesce(revsets...)` revset which returns commits in the first revset
|
||||
in the `revsets` list that does not evaluate to `none()`.
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
* Error on `trunk()` revset resolution is now handled gracefully.
|
||||
|
|
|
@ -304,6 +304,10 @@ given [string pattern](#string-patterns).
|
|||
* `present(x)`: Same as `x`, but evaluated to `none()` if any of the commits
|
||||
in `x` doesn't exist (e.g. is an unknown bookmark name.)
|
||||
|
||||
* `coalesce(revsets...)`: Commits in the first revset in the list of `revsets`
|
||||
which does not evaluate to `none()`. If all revsets evaluate to `none()`, then
|
||||
the result of `coalesce` will also be `none()`.
|
||||
|
||||
* `working_copies()`: The working copy commits across all the workspaces.
|
||||
|
||||
* `at_operation(op, x)`: Evaluates `x` at the specified [operation][]. For
|
||||
|
|
|
@ -919,6 +919,14 @@ impl<'index> EvaluationContext<'index> {
|
|||
self.take_latest_revset(candidate_set.as_ref(), *count),
|
||||
))
|
||||
}
|
||||
ResolvedExpression::Coalesce(expression1, expression2) => {
|
||||
let set1 = self.evaluate(expression1)?;
|
||||
if set1.positions().attach(index).next().is_some() {
|
||||
Ok(set1)
|
||||
} else {
|
||||
self.evaluate(expression2)
|
||||
}
|
||||
}
|
||||
ResolvedExpression::Union(expression1, expression2) => {
|
||||
let set1 = self.evaluate(expression1)?;
|
||||
let set2 = self.evaluate(expression2)?;
|
||||
|
|
|
@ -218,6 +218,7 @@ pub enum RevsetExpression {
|
|||
/// Copy of `repo.view().heads()`, should be set by `resolve_symbols()`.
|
||||
visible_heads: Option<Vec<CommitId>>,
|
||||
},
|
||||
Coalesce(Rc<Self>, Rc<Self>),
|
||||
Present(Rc<Self>),
|
||||
NotIn(Rc<Self>),
|
||||
Union(Rc<Self>, Rc<Self>),
|
||||
|
@ -439,6 +440,20 @@ impl RevsetExpression {
|
|||
Rc::new(Self::Difference(self.clone(), other.clone()))
|
||||
}
|
||||
|
||||
/// Commits that are in the first expression in `expressions` that is not
|
||||
/// `none()`.
|
||||
pub fn coalesce(expressions: &[Rc<Self>]) -> Rc<Self> {
|
||||
match expressions {
|
||||
[] => Self::none(),
|
||||
[expression] => expression.clone(),
|
||||
_ => {
|
||||
// Build balanced tree to minimize the recursion depth.
|
||||
let (left, right) = expressions.split_at(expressions.len() / 2);
|
||||
Rc::new(Self::Coalesce(Self::coalesce(left), Self::coalesce(right)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a programmatically created revset expression.
|
||||
///
|
||||
/// In particular, the expression must not contain any symbols (bookmarks,
|
||||
|
@ -528,6 +543,7 @@ pub enum ResolvedExpression {
|
|||
candidates: Box<Self>,
|
||||
count: usize,
|
||||
},
|
||||
Coalesce(Box<Self>, Box<Self>),
|
||||
Union(Box<Self>, Box<Self>),
|
||||
/// Intersects `candidates` with `predicate` by filtering.
|
||||
FilterWithin {
|
||||
|
@ -853,6 +869,14 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
|
|||
visible_heads: None,
|
||||
}))
|
||||
});
|
||||
map.insert("coalesce", |diagnostics, function, context| {
|
||||
let ([], args) = function.expect_some_arguments()?;
|
||||
let expressions: Vec<_> = args
|
||||
.iter()
|
||||
.map(|arg| lower_expression(diagnostics, arg, context))
|
||||
.try_collect()?;
|
||||
Ok(RevsetExpression::coalesce(&expressions))
|
||||
});
|
||||
map
|
||||
});
|
||||
|
||||
|
@ -1171,6 +1195,12 @@ fn try_transform_expression<E>(
|
|||
visible_heads: visible_heads.clone(),
|
||||
}
|
||||
}),
|
||||
RevsetExpression::Coalesce(expression1, expression2) => transform_rec_pair(
|
||||
(expression1, expression2),
|
||||
pre,
|
||||
post,
|
||||
)?
|
||||
.map(|(expression1, expression2)| RevsetExpression::Coalesce(expression1, expression2)),
|
||||
RevsetExpression::Present(candidates) => {
|
||||
transform_rec(candidates, pre, post)?.map(RevsetExpression::Present)
|
||||
}
|
||||
|
@ -2050,6 +2080,10 @@ impl VisibilityResolutionContext<'_> {
|
|||
let context = VisibilityResolutionContext { visible_heads };
|
||||
context.resolve(candidates)
|
||||
}
|
||||
RevsetExpression::Coalesce(expression1, expression2) => ResolvedExpression::Coalesce(
|
||||
self.resolve(expression1).into(),
|
||||
self.resolve(expression2).into(),
|
||||
),
|
||||
RevsetExpression::Present(_) => {
|
||||
panic!("Expression '{expression:?}' should have been resolved by caller");
|
||||
}
|
||||
|
@ -2130,6 +2164,9 @@ impl VisibilityResolutionContext<'_> {
|
|||
RevsetExpression::AtOperation { .. } => {
|
||||
ResolvedPredicateExpression::Set(self.resolve(expression).into())
|
||||
}
|
||||
RevsetExpression::Coalesce(_, _) => {
|
||||
ResolvedPredicateExpression::Set(self.resolve(expression).into())
|
||||
}
|
||||
RevsetExpression::Present(_) => {
|
||||
panic!("Expression '{expression:?}' should have been resolved by caller")
|
||||
}
|
||||
|
@ -2568,6 +2605,35 @@ mod tests {
|
|||
CommitRef(WorkingCopy(WorkspaceId("default"))),
|
||||
)
|
||||
"###);
|
||||
insta::assert_debug_snapshot!(
|
||||
RevsetExpression::coalesce(&[]),
|
||||
@"None");
|
||||
insta::assert_debug_snapshot!(
|
||||
RevsetExpression::coalesce(&[current_wc.clone()]),
|
||||
@r###"CommitRef(WorkingCopy(WorkspaceId("default")))"###);
|
||||
insta::assert_debug_snapshot!(
|
||||
RevsetExpression::coalesce(&[current_wc.clone(), foo_symbol.clone()]),
|
||||
@r#"
|
||||
Coalesce(
|
||||
CommitRef(WorkingCopy(WorkspaceId("default"))),
|
||||
CommitRef(Symbol("foo")),
|
||||
)
|
||||
"#);
|
||||
insta::assert_debug_snapshot!(
|
||||
RevsetExpression::coalesce(&[
|
||||
current_wc.clone(),
|
||||
foo_symbol.clone(),
|
||||
bar_symbol.clone(),
|
||||
]),
|
||||
@r#"
|
||||
Coalesce(
|
||||
CommitRef(WorkingCopy(WorkspaceId("default"))),
|
||||
Coalesce(
|
||||
CommitRef(Symbol("foo")),
|
||||
CommitRef(Symbol("bar")),
|
||||
),
|
||||
)
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -2974,6 +2974,84 @@ fn test_evaluate_expression_at_operation() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_expression_coalesce() {
|
||||
let settings = testutils::user_settings();
|
||||
let test_repo = TestRepo::init();
|
||||
let repo = &test_repo.repo;
|
||||
let root_commit_id = repo.store().root_commit_id().clone();
|
||||
|
||||
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.commit_with_parents(&[&commit1]);
|
||||
mut_repo.set_local_bookmark_target("commit1", RefTarget::normal(commit1.id().clone()));
|
||||
mut_repo.set_local_bookmark_target("commit2", RefTarget::normal(commit2.id().clone()));
|
||||
|
||||
assert_eq!(resolve_commit_ids(mut_repo, "coalesce()"), vec![]);
|
||||
assert_eq!(resolve_commit_ids(mut_repo, "coalesce(none())"), vec![]);
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "coalesce(all())"),
|
||||
vec![
|
||||
commit2.id().clone(),
|
||||
commit1.id().clone(),
|
||||
root_commit_id.clone(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "coalesce(all(), commit1)"),
|
||||
vec![
|
||||
commit2.id().clone(),
|
||||
commit1.id().clone(),
|
||||
root_commit_id.clone(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "coalesce(none(), commit1)"),
|
||||
vec![commit1.id().clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "coalesce(commit1, commit2)"),
|
||||
vec![commit1.id().clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "coalesce(none(), none(), commit2)"),
|
||||
vec![commit2.id().clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_commit_ids(mut_repo, "coalesce(none(), commit1, commit2)"),
|
||||
vec![commit1.id().clone()]
|
||||
);
|
||||
// Should resolve invalid symbols regardless of whether a specific revset is
|
||||
// evaluated.
|
||||
assert_matches!(
|
||||
try_resolve_commit_ids(mut_repo, "coalesce(all(), commit1_invalid)"),
|
||||
Err(RevsetResolutionError::NoSuchRevision { name, .. })
|
||||
if name == "commit1_invalid"
|
||||
);
|
||||
assert_matches!(
|
||||
try_resolve_commit_ids(mut_repo, "coalesce(none(), commit1_invalid)"),
|
||||
Err(RevsetResolutionError::NoSuchRevision { name, .. })
|
||||
if name == "commit1_invalid"
|
||||
);
|
||||
assert_matches!(
|
||||
try_resolve_commit_ids(mut_repo, "coalesce(all(), commit1, commit2_invalid)"),
|
||||
Err(RevsetResolutionError::NoSuchRevision { name, .. })
|
||||
if name == "commit2_invalid"
|
||||
);
|
||||
assert_matches!(
|
||||
try_resolve_commit_ids(mut_repo, "coalesce(none(), commit1, commit2_invalid)"),
|
||||
Err(RevsetResolutionError::NoSuchRevision { name, .. })
|
||||
if name == "commit2_invalid"
|
||||
);
|
||||
assert_matches!(
|
||||
try_resolve_commit_ids(mut_repo, "coalesce(none(), commit1, commit2, commit2_invalid)"),
|
||||
Err(RevsetResolutionError::NoSuchRevision { name, .. })
|
||||
if name == "commit2_invalid"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_expression_union() {
|
||||
let settings = testutils::user_settings();
|
||||
|
|
Loading…
Reference in a new issue