Skip to content

Commit

Permalink
revset: add coalesce(revsets...)
Browse files Browse the repository at this point in the history
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)`.
  • Loading branch information
bnjmnt4n committed Oct 15, 2024
1 parent f741102 commit 9046c5c
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* `jj git clone` now accepts a `--depth <DEPTH>` option, which
allows to clone the repository with a given depth.

* 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.
Expand Down
4 changes: 4 additions & 0 deletions docs/revsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/src/default_index/revset_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
65 changes: 65 additions & 0 deletions lib/src/revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>),
Expand Down Expand Up @@ -439,6 +440,19 @@ 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(),
[expression, remainder @ ..] => Rc::new(Self::Coalesce(
expression.clone(),
Self::coalesce(remainder),
)),
}
}

/// Resolve a programmatically created revset expression.
///
/// In particular, the expression must not contain any symbols (bookmarks,
Expand Down Expand Up @@ -528,6 +542,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 {
Expand Down Expand Up @@ -853,6 +868,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
});

Expand Down Expand Up @@ -1171,6 +1194,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)
}
Expand Down Expand Up @@ -2050,6 +2079,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");
}
Expand Down Expand Up @@ -2130,6 +2163,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")
}
Expand Down Expand Up @@ -2568,6 +2604,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]
Expand Down
78 changes: 78 additions & 0 deletions lib/tests/test_revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3029,6 +3029,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();
Expand Down

0 comments on commit 9046c5c

Please sign in to comment.