#![expect(missing_docs)]
use std::any::Any;
use std::collections::HashMap;
use std::collections::hash_map;
use std::convert::Infallible;
use std::fmt;
use std::ops::ControlFlow;
use std::ops::Range;
use std::sync::Arc;
use std::sync::LazyLock;
use futures::Stream;
use futures::StreamExt as _;
use futures::stream::LocalBoxStream;
use itertools::Itertools as _;
use pollster::FutureExt as _;
use thiserror::Error;
use crate::backend::BackendError;
use crate::backend::ChangeId;
use crate::backend::CommitId;
use crate::commit::Commit;
use crate::dsl_util;
use crate::dsl_util::collect_similar;
use crate::fileset;
use crate::fileset::FilesetAliasesMap;
use crate::fileset::FilesetDiagnostics;
use crate::fileset::FilesetExpression;
use crate::fileset::FilesetParseContext;
use crate::graph::GraphNode;
use crate::id_prefix::IdPrefixContext;
use crate::id_prefix::IdPrefixIndex;
use crate::index::ResolvedChangeTargets;
use crate::object_id::HexPrefix;
use crate::object_id::PrefixResolution;
use crate::op_store::LocalRemoteRefTarget;
use crate::op_store::RefTarget;
use crate::op_store::RemoteRefState;
use crate::op_walk;
use crate::ref_name::RefName;
use crate::ref_name::RemoteName;
use crate::ref_name::RemoteRefSymbol;
use crate::ref_name::RemoteRefSymbolBuf;
use crate::ref_name::WorkspaceName;
use crate::ref_name::WorkspaceNameBuf;
use crate::repo::ReadonlyRepo;
use crate::repo::Repo;
use crate::repo::RepoLoaderError;
use crate::repo_path::RepoPathUiConverter;
use crate::revset_parser;
pub use crate::revset_parser::BinaryOp;
pub use crate::revset_parser::ExpressionKind;
pub use crate::revset_parser::ExpressionNode;
pub use crate::revset_parser::FunctionCallNode;
pub use crate::revset_parser::RevsetAliasesMap;
pub use crate::revset_parser::RevsetDiagnostics;
pub use crate::revset_parser::RevsetParseError;
pub use crate::revset_parser::RevsetParseErrorKind;
pub use crate::revset_parser::UnaryOp;
pub use crate::revset_parser::expect_literal;
pub use crate::revset_parser::parse_program;
pub use crate::revset_parser::parse_symbol;
use crate::store::Store;
use crate::str_util::StringExpression;
use crate::str_util::StringPattern;
use crate::time_util::DatePattern;
use crate::time_util::DatePatternContext;
#[derive(Debug, Error)]
pub enum RevsetResolutionError {
#[error("Revision `{name}` doesn't exist")]
NoSuchRevision {
name: String,
candidates: Vec<String>,
},
#[error("Workspace `{}` doesn't have a working-copy commit", name.as_symbol())]
WorkspaceMissingWorkingCopy { name: WorkspaceNameBuf },
#[error("An empty string is not a valid revision")]
EmptyString,
#[error("Commit ID prefix `{0}` is ambiguous")]
AmbiguousCommitIdPrefix(String),
#[error("Change ID prefix `{0}` is ambiguous")]
AmbiguousChangeIdPrefix(String),
#[error("Change ID `{symbol}` is divergent")]
DivergentChangeId {
symbol: String,
visible_targets: Vec<(usize, CommitId)>,
},
#[error("Name `{symbol}` is conflicted")]
ConflictedRef {
kind: &'static str,
symbol: String,
targets: Vec<CommitId>,
},
#[error("Unexpected error from commit backend")]
Backend(#[source] BackendError),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
#[derive(Debug, Error)]
pub enum RevsetEvaluationError {
#[error("Unexpected error from commit backend")]
Backend(#[from] BackendError),
#[error(transparent)]
Other(Box<dyn std::error::Error + Send + Sync>),
}
impl RevsetEvaluationError {
pub fn into_backend_error(self) -> BackendError {
match self {
Self::Backend(err) => err,
Self::Other(err) => BackendError::Other(err),
}
}
}
pub const GENERATION_RANGE_FULL: Range<u64> = 0..u64::MAX;
pub const GENERATION_RANGE_EMPTY: Range<u64> = 0..0;
pub const PARENTS_RANGE_FULL: Range<u32> = 0..u32::MAX;
#[derive(Clone, Debug)]
pub enum RevsetCommitRef {
WorkingCopy(WorkspaceNameBuf),
WorkingCopies,
Symbol(String),
RemoteSymbol(RemoteRefSymbolBuf),
ChangeId(HexPrefix),
CommitId(HexPrefix),
Bookmarks(StringExpression),
RemoteBookmarks {
symbol: RemoteRefSymbolExpression,
remote_ref_state: Option<RemoteRefState>,
},
Tags(StringExpression),
RemoteTags {
symbol: RemoteRefSymbolExpression,
remote_ref_state: Option<RemoteRefState>,
},
GitRefs,
GitHead,
}
#[derive(Clone, Debug)]
pub struct RemoteRefSymbolExpression {
pub name: StringExpression,
pub remote: StringExpression,
}
pub trait RevsetFilterExtension: std::fmt::Debug + Any + Send + Sync {
fn matches_commit(&self, commit: &Commit) -> bool;
}
impl dyn RevsetFilterExtension {
pub fn downcast_ref<T: RevsetFilterExtension>(&self) -> Option<&T> {
(self as &dyn Any).downcast_ref()
}
}
#[derive(Eq, Copy, Clone, Debug, PartialEq)]
pub enum DiffMatchSide {
Either,
Left,
Right,
}
#[derive(Clone, Debug)]
pub enum RevsetFilterPredicate {
ParentCount(Range<u32>),
Description(StringExpression),
Subject(StringExpression),
AuthorName(StringExpression),
AuthorEmail(StringExpression),
AuthorDate(DatePattern),
CommitterName(StringExpression),
CommitterEmail(StringExpression),
CommitterDate(DatePattern),
File(FilesetExpression),
DiffLines {
text: StringExpression,
files: FilesetExpression,
side: DiffMatchSide,
},
HasConflict,
Signed,
Extension(Arc<dyn RevsetFilterExtension>),
}
mod private {
pub trait ExpressionState {
type CommitRef: Clone;
type Operation: Clone;
}
#[derive(Debug)]
pub enum UserExpressionState {}
#[derive(Debug)]
pub enum ResolvedExpressionState {}
}
use private::ExpressionState;
use private::ResolvedExpressionState;
use private::UserExpressionState;
impl ExpressionState for UserExpressionState {
type CommitRef = RevsetCommitRef;
type Operation = String;
}
impl ExpressionState for ResolvedExpressionState {
type CommitRef = Infallible;
type Operation = Infallible;
}
pub type UserRevsetExpression = RevsetExpression<UserExpressionState>;
pub type ResolvedRevsetExpression = RevsetExpression<ResolvedExpressionState>;
#[derive(Clone, Debug)]
pub enum RevsetExpression<St: ExpressionState> {
None,
All,
VisibleHeads,
VisibleHeadsOrReferenced,
Root,
Commits(Vec<CommitId>),
CommitRef(St::CommitRef),
Ancestors {
heads: Arc<Self>,
generation: Range<u64>,
parents_range: Range<u32>,
},
Descendants {
roots: Arc<Self>,
generation: Range<u64>,
},
Range {
roots: Arc<Self>,
heads: Arc<Self>,
generation: Range<u64>,
parents_range: Range<u32>,
},
DagRange {
roots: Arc<Self>,
heads: Arc<Self>,
},
Reachable {
sources: Arc<Self>,
domain: Arc<Self>,
},
Heads(Arc<Self>),
HeadsRange {
roots: Arc<Self>,
heads: Arc<Self>,
parents_range: Range<u32>,
filter: Arc<Self>,
},
Roots(Arc<Self>),
ForkPoint(Arc<Self>),
Bisect(Arc<Self>),
HasSize {
candidates: Arc<Self>,
count: usize,
},
Latest {
candidates: Arc<Self>,
count: usize,
},
Filter(RevsetFilterPredicate),
AsFilter(Arc<Self>),
Divergent,
AtOperation {
operation: St::Operation,
candidates: Arc<Self>,
},
WithinReference {
candidates: Arc<Self>,
commits: Vec<CommitId>,
},
WithinVisibility {
candidates: Arc<Self>,
visible_heads: Vec<CommitId>,
},
Coalesce(Arc<Self>, Arc<Self>),
Present(Arc<Self>),
NotIn(Arc<Self>),
Union(Arc<Self>, Arc<Self>),
Intersection(Arc<Self>, Arc<Self>),
Difference(Arc<Self>, Arc<Self>),
}
impl<St: ExpressionState> RevsetExpression<St> {
pub fn none() -> Arc<Self> {
Arc::new(Self::None)
}
pub fn all() -> Arc<Self> {
Arc::new(Self::All)
}
pub fn visible_heads() -> Arc<Self> {
Arc::new(Self::VisibleHeads)
}
fn visible_heads_or_referenced() -> Arc<Self> {
Arc::new(Self::VisibleHeadsOrReferenced)
}
pub fn root() -> Arc<Self> {
Arc::new(Self::Root)
}
pub fn commit(commit_id: CommitId) -> Arc<Self> {
Self::commits(vec![commit_id])
}
pub fn commits(commit_ids: Vec<CommitId>) -> Arc<Self> {
Arc::new(Self::Commits(commit_ids))
}
pub fn filter(predicate: RevsetFilterPredicate) -> Arc<Self> {
Arc::new(Self::Filter(predicate))
}
pub fn divergent() -> Arc<Self> {
Arc::new(Self::AsFilter(Arc::new(Self::Divergent)))
}
pub fn is_empty() -> Arc<Self> {
Self::filter(RevsetFilterPredicate::File(FilesetExpression::all())).negated()
}
}
impl<St: ExpressionState<CommitRef = RevsetCommitRef>> RevsetExpression<St> {
pub fn working_copy(name: WorkspaceNameBuf) -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::WorkingCopy(name)))
}
pub fn working_copies() -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::WorkingCopies))
}
pub fn symbol(value: String) -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::Symbol(value)))
}
pub fn remote_symbol(value: RemoteRefSymbolBuf) -> Arc<Self> {
let commit_ref = RevsetCommitRef::RemoteSymbol(value);
Arc::new(Self::CommitRef(commit_ref))
}
pub fn change_id_prefix(prefix: HexPrefix) -> Arc<Self> {
let commit_ref = RevsetCommitRef::ChangeId(prefix);
Arc::new(Self::CommitRef(commit_ref))
}
pub fn commit_id_prefix(prefix: HexPrefix) -> Arc<Self> {
let commit_ref = RevsetCommitRef::CommitId(prefix);
Arc::new(Self::CommitRef(commit_ref))
}
pub fn bookmarks(expression: StringExpression) -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::Bookmarks(expression)))
}
pub fn remote_bookmarks(
symbol: RemoteRefSymbolExpression,
remote_ref_state: Option<RemoteRefState>,
) -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::RemoteBookmarks {
symbol,
remote_ref_state,
}))
}
pub fn tags(expression: StringExpression) -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::Tags(expression)))
}
pub fn remote_tags(
symbol: RemoteRefSymbolExpression,
remote_ref_state: Option<RemoteRefState>,
) -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::RemoteTags {
symbol,
remote_ref_state,
}))
}
pub fn git_refs() -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::GitRefs))
}
pub fn git_head() -> Arc<Self> {
Arc::new(Self::CommitRef(RevsetCommitRef::GitHead))
}
}
impl<St: ExpressionState> RevsetExpression<St> {
pub fn latest(self: &Arc<Self>, count: usize) -> Arc<Self> {
Arc::new(Self::Latest {
candidates: self.clone(),
count,
})
}
pub fn heads(self: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Heads(self.clone()))
}
pub fn roots(self: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Roots(self.clone()))
}
pub fn parents(self: &Arc<Self>) -> Arc<Self> {
self.ancestors_at(1)
}
pub fn ancestors(self: &Arc<Self>) -> Arc<Self> {
self.ancestors_range(GENERATION_RANGE_FULL)
}
pub fn ancestors_at(self: &Arc<Self>, generation: u64) -> Arc<Self> {
self.ancestors_range(generation..generation.saturating_add(1))
}
pub fn ancestors_range(self: &Arc<Self>, generation_range: Range<u64>) -> Arc<Self> {
Arc::new(Self::Ancestors {
heads: self.clone(),
generation: generation_range,
parents_range: PARENTS_RANGE_FULL,
})
}
pub fn first_ancestors(self: &Arc<Self>) -> Arc<Self> {
self.first_ancestors_range(GENERATION_RANGE_FULL)
}
pub fn first_ancestors_at(self: &Arc<Self>, generation: u64) -> Arc<Self> {
self.first_ancestors_range(generation..generation.saturating_add(1))
}
pub fn first_ancestors_range(self: &Arc<Self>, generation_range: Range<u64>) -> Arc<Self> {
Arc::new(Self::Ancestors {
heads: self.clone(),
generation: generation_range,
parents_range: 0..1,
})
}
pub fn children(self: &Arc<Self>) -> Arc<Self> {
self.descendants_at(1)
}
pub fn descendants(self: &Arc<Self>) -> Arc<Self> {
self.descendants_range(GENERATION_RANGE_FULL)
}
pub fn descendants_at(self: &Arc<Self>, generation: u64) -> Arc<Self> {
self.descendants_range(generation..generation.saturating_add(1))
}
pub fn descendants_range(self: &Arc<Self>, generation_range: Range<u64>) -> Arc<Self> {
Arc::new(Self::Descendants {
roots: self.clone(),
generation: generation_range,
})
}
pub fn fork_point(self: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::ForkPoint(self.clone()))
}
pub fn bisect(self: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Bisect(self.clone()))
}
pub fn has_size(self: &Arc<Self>, count: usize) -> Arc<Self> {
Arc::new(Self::HasSize {
candidates: self.clone(),
count,
})
}
pub fn filtered(self: &Arc<Self>, predicate: RevsetFilterPredicate) -> Arc<Self> {
self.intersection(&Self::filter(predicate))
}
pub fn dag_range_to(self: &Arc<Self>, heads: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::DagRange {
roots: self.clone(),
heads: heads.clone(),
})
}
pub fn connected(self: &Arc<Self>) -> Arc<Self> {
self.dag_range_to(self)
}
pub fn reachable(self: &Arc<Self>, domain: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Reachable {
sources: self.clone(),
domain: domain.clone(),
})
}
pub fn range(self: &Arc<Self>, heads: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Range {
roots: self.clone(),
heads: heads.clone(),
generation: GENERATION_RANGE_FULL,
parents_range: PARENTS_RANGE_FULL,
})
}
pub fn present(self: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Present(self.clone()))
}
pub fn negated(self: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::NotIn(self.clone()))
}
pub fn union(self: &Arc<Self>, other: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Union(self.clone(), other.clone()))
}
pub fn union_all(expressions: &[Arc<Self>]) -> Arc<Self> {
to_binary_expression(expressions, &Self::none, &Self::union)
}
pub fn intersection(self: &Arc<Self>, other: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Intersection(self.clone(), other.clone()))
}
pub fn minus(self: &Arc<Self>, other: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Difference(self.clone(), other.clone()))
}
pub fn coalesce(expressions: &[Arc<Self>]) -> Arc<Self> {
to_binary_expression(expressions, &Self::none, &Self::coalesce2)
}
fn coalesce2(self: &Arc<Self>, other: &Arc<Self>) -> Arc<Self> {
Arc::new(Self::Coalesce(self.clone(), other.clone()))
}
}
impl<St: ExpressionState<CommitRef = RevsetCommitRef>> RevsetExpression<St> {
pub fn as_symbol(&self) -> Option<&str> {
match self {
Self::CommitRef(RevsetCommitRef::Symbol(name)) => Some(name),
_ => None,
}
}
}
impl UserRevsetExpression {
pub fn resolve_user_expression(
&self,
repo: &dyn Repo,
symbol_resolver: &SymbolResolver,
) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
resolve_symbols(repo, self, symbol_resolver)
}
}
impl ResolvedRevsetExpression {
pub fn evaluate<'index>(
self: Arc<Self>,
repo: &'index dyn Repo,
) -> Result<Box<dyn Revset + 'index>, RevsetEvaluationError> {
let expr = optimize(self).to_backend_expression(repo);
repo.index().evaluate_revset(&expr, repo.store())
}
pub fn evaluate_unoptimized<'index>(
self: &Arc<Self>,
repo: &'index dyn Repo,
) -> Result<Box<dyn Revset + 'index>, RevsetEvaluationError> {
let expr = resolve_referenced_commits(self)
.as_ref()
.unwrap_or(self)
.to_backend_expression(repo);
repo.index().evaluate_revset(&expr, repo.store())
}
pub fn to_backend_expression(&self, repo: &dyn Repo) -> ResolvedExpression {
resolve_visibility(repo, self)
}
}
#[derive(Clone, Debug)]
pub enum ResolvedPredicateExpression {
Filter(RevsetFilterPredicate),
Divergent {
visible_heads: Vec<CommitId>,
},
Set(Box<ResolvedExpression>),
NotIn(Box<Self>),
Union(Box<Self>, Box<Self>),
Intersection(Box<Self>, Box<Self>),
}
#[derive(Clone, Debug)]
pub enum ResolvedExpression {
Commits(Vec<CommitId>),
Ancestors {
heads: Box<Self>,
generation: Range<u64>,
parents_range: Range<u32>,
},
Range {
roots: Box<Self>,
heads: Box<Self>,
generation: Range<u64>,
parents_range: Range<u32>,
},
DagRange {
roots: Box<Self>,
heads: Box<Self>,
generation_from_roots: Range<u64>,
},
Reachable {
sources: Box<Self>,
domain: Box<Self>,
},
Heads(Box<Self>),
HeadsRange {
roots: Box<Self>,
heads: Box<Self>,
parents_range: Range<u32>,
filter: Option<ResolvedPredicateExpression>,
},
Roots(Box<Self>),
ForkPoint(Box<Self>),
Bisect(Box<Self>),
HasSize {
candidates: Box<Self>,
count: usize,
},
Latest {
candidates: Box<Self>,
count: usize,
},
Coalesce(Box<Self>, Box<Self>),
Union(Box<Self>, Box<Self>),
FilterWithin {
candidates: Box<Self>,
predicate: ResolvedPredicateExpression,
},
Intersection(Box<Self>, Box<Self>),
Difference(Box<Self>, Box<Self>),
}
pub type RevsetFunction = fn(
&mut RevsetDiagnostics,
&FunctionCallNode,
&LoweringContext,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError>;
static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock::new(|| {
let mut map: HashMap<&str, RevsetFunction> = HashMap::new();
map.insert("parents", |diagnostics, function, context| {
let ([arg], [depth_opt_arg]) = function.expect_arguments()?;
let expression = lower_expression(diagnostics, arg, context)?;
if let Some(depth_arg) = depth_opt_arg {
let depth = expect_literal("integer", depth_arg)?;
Ok(expression.ancestors_at(depth))
} else {
Ok(expression.parents())
}
});
map.insert("children", |diagnostics, function, context| {
let ([arg], [depth_opt_arg]) = function.expect_arguments()?;
let expression = lower_expression(diagnostics, arg, context)?;
if let Some(depth_arg) = depth_opt_arg {
let depth = expect_literal("integer", depth_arg)?;
Ok(expression.descendants_at(depth))
} else {
Ok(expression.children())
}
});
map.insert("ancestors", |diagnostics, function, context| {
let ([heads_arg], [depth_opt_arg]) = function.expect_arguments()?;
let heads = lower_expression(diagnostics, heads_arg, context)?;
let generation = if let Some(depth_arg) = depth_opt_arg {
let depth = expect_literal("integer", depth_arg)?;
0..depth
} else {
GENERATION_RANGE_FULL
};
Ok(heads.ancestors_range(generation))
});
map.insert("descendants", |diagnostics, function, context| {
let ([roots_arg], [depth_opt_arg]) = function.expect_arguments()?;
let roots = lower_expression(diagnostics, roots_arg, context)?;
let generation = if let Some(depth_arg) = depth_opt_arg {
let depth = expect_literal("integer", depth_arg)?;
0..depth
} else {
GENERATION_RANGE_FULL
};
Ok(roots.descendants_range(generation))
});
map.insert("first_parent", |diagnostics, function, context| {
let ([arg], [depth_opt_arg]) = function.expect_arguments()?;
let expression = lower_expression(diagnostics, arg, context)?;
let depth = if let Some(depth_arg) = depth_opt_arg {
expect_literal("integer", depth_arg)?
} else {
1
};
Ok(expression.first_ancestors_at(depth))
});
map.insert("first_ancestors", |diagnostics, function, context| {
let ([heads_arg], [depth_opt_arg]) = function.expect_arguments()?;
let heads = lower_expression(diagnostics, heads_arg, context)?;
let generation = if let Some(depth_arg) = depth_opt_arg {
let depth = expect_literal("integer", depth_arg)?;
0..depth
} else {
GENERATION_RANGE_FULL
};
Ok(heads.first_ancestors_range(generation))
});
map.insert("connected", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let candidates = lower_expression(diagnostics, arg, context)?;
Ok(candidates.connected())
});
map.insert("reachable", |diagnostics, function, context| {
let [source_arg, domain_arg] = function.expect_exact_arguments()?;
let sources = lower_expression(diagnostics, source_arg, context)?;
let domain = lower_expression(diagnostics, domain_arg, context)?;
Ok(sources.reachable(&domain))
});
map.insert("none", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::none())
});
map.insert("all", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::all())
});
map.insert("working_copies", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::working_copies())
});
map.insert("heads", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let candidates = lower_expression(diagnostics, arg, context)?;
Ok(candidates.heads())
});
map.insert("roots", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let candidates = lower_expression(diagnostics, arg, context)?;
Ok(candidates.roots())
});
map.insert("visible_heads", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::visible_heads())
});
map.insert("root", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::root())
});
map.insert("change_id", |diagnostics, function, _context| {
let [arg] = function.expect_exact_arguments()?;
let prefix = revset_parser::catch_aliases(diagnostics, arg, |_diagnostics, arg| {
let value = revset_parser::expect_string_literal("change ID prefix", arg)?;
HexPrefix::try_from_reverse_hex(value)
.ok_or_else(|| RevsetParseError::expression("Invalid change ID prefix", arg.span))
})?;
Ok(RevsetExpression::change_id_prefix(prefix))
});
map.insert("commit_id", |diagnostics, function, _context| {
let [arg] = function.expect_exact_arguments()?;
let prefix = revset_parser::catch_aliases(diagnostics, arg, |_diagnostics, arg| {
let value = revset_parser::expect_string_literal("commit ID prefix", arg)?;
HexPrefix::try_from_hex(value)
.ok_or_else(|| RevsetParseError::expression("Invalid commit ID prefix", arg.span))
})?;
Ok(RevsetExpression::commit_id_prefix(prefix))
});
map.insert("bookmarks", |diagnostics, function, context| {
let ([], [opt_arg]) = function.expect_arguments()?;
let expr = if let Some(arg) = opt_arg {
expect_string_expression(diagnostics, arg, context)?
} else {
StringExpression::all()
};
Ok(RevsetExpression::bookmarks(expr))
});
map.insert("remote_bookmarks", |diagnostics, function, context| {
let symbol = parse_remote_refs_arguments(diagnostics, function, context)?;
let state = None;
Ok(RevsetExpression::remote_bookmarks(symbol, state))
});
map.insert(
"tracked_remote_bookmarks",
|diagnostics, function, context| {
let symbol = parse_remote_refs_arguments(diagnostics, function, context)?;
let state = Some(RemoteRefState::Tracked);
Ok(RevsetExpression::remote_bookmarks(symbol, state))
},
);
map.insert(
"untracked_remote_bookmarks",
|diagnostics, function, context| {
let symbol = parse_remote_refs_arguments(diagnostics, function, context)?;
let state = Some(RemoteRefState::New);
Ok(RevsetExpression::remote_bookmarks(symbol, state))
},
);
map.insert("tags", |diagnostics, function, context| {
let ([], [opt_arg]) = function.expect_arguments()?;
let expr = if let Some(arg) = opt_arg {
expect_string_expression(diagnostics, arg, context)?
} else {
StringExpression::all()
};
Ok(RevsetExpression::tags(expr))
});
map.insert("remote_tags", |diagnostics, function, context| {
let symbol = parse_remote_refs_arguments(diagnostics, function, context)?;
let state = None;
Ok(RevsetExpression::remote_tags(symbol, state))
});
map.insert("tracked_remote_tags", |diagnostics, function, context| {
let symbol = parse_remote_refs_arguments(diagnostics, function, context)?;
let state = Some(RemoteRefState::Tracked);
Ok(RevsetExpression::remote_tags(symbol, state))
});
map.insert("untracked_remote_tags", |diagnostics, function, context| {
let symbol = parse_remote_refs_arguments(diagnostics, function, context)?;
let state = Some(RemoteRefState::New);
Ok(RevsetExpression::remote_tags(symbol, state))
});
map.insert("git_refs", |diagnostics, function, _context| {
diagnostics.add_warning(RevsetParseError::expression(
"git_refs() is deprecated; use remote_bookmarks()/tags() instead",
function.name_span,
));
function.expect_no_arguments()?;
Ok(RevsetExpression::git_refs())
});
map.insert("git_head", |diagnostics, function, _context| {
diagnostics.add_warning(RevsetParseError::expression(
"git_head() is deprecated; use first_parent(@) instead",
function.name_span,
));
function.expect_no_arguments()?;
Ok(RevsetExpression::git_head())
});
map.insert("latest", |diagnostics, function, context| {
let ([candidates_arg], [count_opt_arg]) = function.expect_arguments()?;
let candidates = lower_expression(diagnostics, candidates_arg, context)?;
let count = if let Some(count_arg) = count_opt_arg {
expect_literal("integer", count_arg)?
} else {
1
};
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("bisect", |diagnostics, function, context| {
let [expression_arg] = function.expect_exact_arguments()?;
let expression = lower_expression(diagnostics, expression_arg, context)?;
Ok(RevsetExpression::bisect(&expression))
});
map.insert("exactly", |diagnostics, function, context| {
let ([candidates_arg, count_arg], []) = function.expect_arguments()?;
let candidates = lower_expression(diagnostics, candidates_arg, context)?;
let count = expect_literal("integer", count_arg)?;
Ok(candidates.has_size(count))
});
map.insert("merges", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::filter(
RevsetFilterPredicate::ParentCount(2..u32::MAX),
))
});
map.insert("description", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::Description(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("subject", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::Subject(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("author", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let name_predicate = RevsetFilterPredicate::AuthorName(expr.clone());
let email_predicate = RevsetFilterPredicate::AuthorEmail(expr);
Ok(RevsetExpression::filter(name_predicate)
.union(&RevsetExpression::filter(email_predicate)))
});
map.insert("author_name", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::AuthorName(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("author_email", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::AuthorEmail(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("author_date", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let pattern = expect_date_pattern(diagnostics, arg, context.date_pattern_context())?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::AuthorDate(
pattern,
)))
});
map.insert("signed", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
let predicate = RevsetFilterPredicate::Signed;
Ok(RevsetExpression::filter(predicate))
});
map.insert("mine", |_diagnostics, function, context| {
function.expect_no_arguments()?;
let pattern = StringPattern::exact_i(context.user_email);
let predicate = RevsetFilterPredicate::AuthorEmail(StringExpression::pattern(pattern));
Ok(RevsetExpression::filter(predicate))
});
map.insert("committer", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let name_predicate = RevsetFilterPredicate::CommitterName(expr.clone());
let email_predicate = RevsetFilterPredicate::CommitterEmail(expr);
Ok(RevsetExpression::filter(name_predicate)
.union(&RevsetExpression::filter(email_predicate)))
});
map.insert("committer_name", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::CommitterName(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("committer_email", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expr = expect_string_expression(diagnostics, arg, context)?;
let predicate = RevsetFilterPredicate::CommitterEmail(expr);
Ok(RevsetExpression::filter(predicate))
});
map.insert("committer_date", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let pattern = expect_date_pattern(diagnostics, arg, context.date_pattern_context())?;
Ok(RevsetExpression::filter(
RevsetFilterPredicate::CommitterDate(pattern),
))
});
map.insert("empty", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::is_empty())
});
map.insert("files", |diagnostics, function, context| {
let fileset_context = context.fileset_parse_context().ok_or_else(|| {
RevsetParseError::with_span(
RevsetParseErrorKind::FsPathWithoutWorkspace,
function.args_span, )
})?;
let [arg] = function.expect_exact_arguments()?;
let expr = expect_fileset_expression(diagnostics, arg, &fileset_context)?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::File(expr)))
});
map.insert("diff_lines", |diagnostics, function, context| {
if function.name != "diff_lines" {
diagnostics.add_warning(RevsetParseError::expression(
"diff_contains() is deprecated; use diff_lines() instead",
function.name_span,
));
}
let ([text_arg], [files_opt_arg]) = function.expect_arguments()?;
let text = expect_string_expression(diagnostics, text_arg, context)?;
let files = expand_optional_files_arg(files_opt_arg, diagnostics, context)?;
let predicate = RevsetFilterPredicate::DiffLines {
text,
files,
side: DiffMatchSide::Either,
};
Ok(RevsetExpression::filter(predicate))
});
map.insert("diff_lines_added", |diagnostics, function, context| {
let ([text_arg], [files_opt_arg]) = function.expect_arguments()?;
let text = expect_string_expression(diagnostics, text_arg, context)?;
let files = expand_optional_files_arg(files_opt_arg, diagnostics, context)?;
let predicate = RevsetFilterPredicate::DiffLines {
text,
files,
side: DiffMatchSide::Right,
};
Ok(RevsetExpression::filter(predicate))
});
map.insert("diff_lines_removed", |diagnostics, function, context| {
let ([text_arg], [files_opt_arg]) = function.expect_arguments()?;
let text = expect_string_expression(diagnostics, text_arg, context)?;
let files = expand_optional_files_arg(files_opt_arg, diagnostics, context)?;
let predicate = RevsetFilterPredicate::DiffLines {
text,
files,
side: DiffMatchSide::Left,
};
Ok(RevsetExpression::filter(predicate))
});
map.insert("diff_contains", map["diff_lines"]);
map.insert("conflicts", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::HasConflict))
});
map.insert("divergent", |_diagnostics, function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::divergent())
});
map.insert("present", |diagnostics, function, context| {
let [arg] = function.expect_exact_arguments()?;
let expression = lower_expression(diagnostics, arg, context)?;
Ok(expression.present())
});
map.insert("at_operation", |diagnostics, function, context| {
let [op_arg, cand_arg] = function.expect_exact_arguments()?;
let operation = revset_parser::catch_aliases(diagnostics, op_arg, |_diagnostics, node| {
Ok(node.span.as_str().to_owned())
})?;
let candidates = lower_expression(diagnostics, cand_arg, context)?;
Ok(Arc::new(RevsetExpression::AtOperation {
operation,
candidates,
}))
});
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
});
fn expand_optional_files_arg(
files_opt_arg: Option<&ExpressionNode>,
diagnostics: &mut RevsetDiagnostics,
context: &LoweringContext,
) -> Result<FilesetExpression, RevsetParseError> {
if let Some(files_arg) = files_opt_arg {
let fileset_context = context.fileset_parse_context().ok_or_else(|| {
RevsetParseError::with_span(
RevsetParseErrorKind::FsPathWithoutWorkspace,
files_arg.span,
)
})?;
expect_fileset_expression(diagnostics, files_arg, &fileset_context)
} else {
Ok(FilesetExpression::all())
}
}
pub fn expect_fileset_expression(
diagnostics: &mut RevsetDiagnostics,
node: &ExpressionNode,
context: &FilesetParseContext,
) -> Result<FilesetExpression, RevsetParseError> {
revset_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
let mut inner_diagnostics = FilesetDiagnostics::new();
let expression = fileset::parse(&mut inner_diagnostics, node.span.as_str(), context)
.map_err(|err| {
RevsetParseError::expression("In fileset expression", node.span).with_source(err)
})?;
diagnostics.extend_with(inner_diagnostics, |diag| {
RevsetParseError::expression("In fileset expression", node.span).with_source(diag)
});
Ok(expression)
})
}
pub fn expect_string_expression(
diagnostics: &mut RevsetDiagnostics,
node: &ExpressionNode,
context: &LoweringContext,
) -> Result<StringExpression, RevsetParseError> {
let default_kind = if context.use_glob_by_default {
"glob"
} else {
"substring"
};
expect_string_expression_inner(diagnostics, node, default_kind)
}
fn expect_string_expression_inner(
diagnostics: &mut RevsetDiagnostics,
node: &ExpressionNode,
default_kind: &str,
) -> Result<StringExpression, RevsetParseError> {
revset_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
let expr_error = || RevsetParseError::expression("Invalid string expression", node.span);
let pattern_error = || RevsetParseError::expression("Invalid string pattern", node.span);
let default_pattern = |diagnostics: &mut RevsetDiagnostics, value: &str| {
if default_kind == "substring" {
diagnostics.add_warning(RevsetParseError::expression(
"ui.revsets-use-glob-by-default=false will be removed in a future release",
node.span,
));
}
let pattern = StringPattern::from_str_kind(value, default_kind)
.map_err(|err| pattern_error().with_source(err))?;
Ok(StringExpression::pattern(pattern))
};
match &node.kind {
ExpressionKind::Identifier(value) => default_pattern(diagnostics, value),
ExpressionKind::String(value) => default_pattern(diagnostics, value),
ExpressionKind::Pattern(pattern) => {
let value = revset_parser::expect_string_literal("string", &pattern.value)?;
let pattern = StringPattern::from_str_kind(value, pattern.name)
.map_err(|err| pattern_error().with_source(err))?;
Ok(StringExpression::pattern(pattern))
}
ExpressionKind::RemoteSymbol(_)
| ExpressionKind::AtWorkspace(_)
| ExpressionKind::AtCurrentWorkspace
| ExpressionKind::DagRangeAll
| ExpressionKind::RangeAll => Err(expr_error()),
ExpressionKind::Unary(op, arg_node) => {
let arg = expect_string_expression_inner(diagnostics, arg_node, default_kind)?;
match op {
UnaryOp::Negate => Ok(arg.negated()),
UnaryOp::DagRangePre
| UnaryOp::DagRangePost
| UnaryOp::RangePre
| UnaryOp::RangePost
| UnaryOp::Parents
| UnaryOp::Children => Err(expr_error()),
}
}
ExpressionKind::Binary(op, lhs_node, rhs_node) => {
let lhs = expect_string_expression_inner(diagnostics, lhs_node, default_kind)?;
let rhs = expect_string_expression_inner(diagnostics, rhs_node, default_kind)?;
match op {
BinaryOp::Intersection => Ok(lhs.intersection(rhs)),
BinaryOp::Difference => Ok(lhs.intersection(rhs.negated())),
BinaryOp::DagRange | BinaryOp::Range => Err(expr_error()),
}
}
ExpressionKind::UnionAll(nodes) => {
let expressions = nodes
.iter()
.map(|node| expect_string_expression_inner(diagnostics, node, default_kind))
.try_collect()?;
Ok(StringExpression::union_all(expressions))
}
ExpressionKind::FunctionCall(_) => Err(expr_error()),
ExpressionKind::AliasExpanded(..) => unreachable!(),
}
})
}
pub fn expect_date_pattern(
diagnostics: &mut RevsetDiagnostics,
node: &ExpressionNode,
context: &DatePatternContext,
) -> Result<DatePattern, RevsetParseError> {
revset_parser::catch_aliases(diagnostics, node, |_diagnostics, node| {
let (value, kind) = revset_parser::expect_string_pattern("date pattern", node)?;
let kind = kind.ok_or_else(|| {
RevsetParseError::expression("Date pattern must specify 'after' or 'before'", node.span)
})?;
context.parse_relative(value, kind).map_err(|err| {
RevsetParseError::expression("Invalid date pattern", node.span).with_source(err)
})
})
}
fn parse_remote_refs_arguments(
diagnostics: &mut RevsetDiagnostics,
function: &FunctionCallNode,
context: &LoweringContext,
) -> Result<RemoteRefSymbolExpression, RevsetParseError> {
let ([], [name_opt_arg, remote_opt_arg]) = function.expect_named_arguments(&["", "remote"])?;
let name = if let Some(name_arg) = name_opt_arg {
expect_string_expression(diagnostics, name_arg, context)?
} else {
StringExpression::all()
};
let remote = if let Some(remote_arg) = remote_opt_arg {
expect_string_expression(diagnostics, remote_arg, context)?
} else if let Some(remote) = context.default_ignored_remote {
StringExpression::exact(remote).negated()
} else {
StringExpression::all()
};
Ok(RemoteRefSymbolExpression { name, remote })
}
fn lower_function_call(
diagnostics: &mut RevsetDiagnostics,
function: &FunctionCallNode,
context: &LoweringContext,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
let function_map = &context.extensions.function_map;
if let Some(func) = function_map.get(function.name) {
func(diagnostics, function, context)
} else {
Err(RevsetParseError::with_span(
RevsetParseErrorKind::NoSuchFunction {
name: function.name.to_owned(),
candidates: collect_similar(function.name, function_map.keys()),
},
function.name_span,
))
}
}
pub fn lower_expression(
diagnostics: &mut RevsetDiagnostics,
node: &ExpressionNode,
context: &LoweringContext,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
revset_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
ExpressionKind::Identifier(name) => Ok(RevsetExpression::symbol((*name).to_owned())),
ExpressionKind::String(name) => Ok(RevsetExpression::symbol(name.to_owned())),
ExpressionKind::Pattern(_) => Err(RevsetParseError::with_span(
RevsetParseErrorKind::NotInfixOperator {
op: ":".to_owned(),
similar_op: "::".to_owned(),
description: "DAG range".to_owned(),
},
node.span,
)),
ExpressionKind::RemoteSymbol(symbol) => Ok(RevsetExpression::remote_symbol(symbol.clone())),
ExpressionKind::AtWorkspace(name) => Ok(RevsetExpression::working_copy(name.into())),
ExpressionKind::AtCurrentWorkspace => {
let ctx = context.workspace.as_ref().ok_or_else(|| {
RevsetParseError::with_span(
RevsetParseErrorKind::WorkingCopyWithoutWorkspace,
node.span,
)
})?;
Ok(RevsetExpression::working_copy(
ctx.workspace_name.to_owned(),
))
}
ExpressionKind::DagRangeAll => Ok(RevsetExpression::all()),
ExpressionKind::RangeAll => Ok(RevsetExpression::root().negated()),
ExpressionKind::Unary(op, arg_node) => {
let arg = lower_expression(diagnostics, arg_node, context)?;
match op {
UnaryOp::Negate => Ok(arg.negated()),
UnaryOp::DagRangePre => Ok(arg.ancestors()),
UnaryOp::DagRangePost => Ok(arg.descendants()),
UnaryOp::RangePre => Ok(RevsetExpression::root().range(&arg)),
UnaryOp::RangePost => Ok(arg.ancestors().negated()),
UnaryOp::Parents => Ok(arg.parents()),
UnaryOp::Children => Ok(arg.children()),
}
}
ExpressionKind::Binary(op, lhs_node, rhs_node) => {
let lhs = lower_expression(diagnostics, lhs_node, context)?;
let rhs = lower_expression(diagnostics, rhs_node, context)?;
match op {
BinaryOp::Intersection => Ok(lhs.intersection(&rhs)),
BinaryOp::Difference => Ok(lhs.minus(&rhs)),
BinaryOp::DagRange => Ok(lhs.dag_range_to(&rhs)),
BinaryOp::Range => Ok(lhs.range(&rhs)),
}
}
ExpressionKind::UnionAll(nodes) => {
let expressions: Vec<_> = nodes
.iter()
.map(|node| lower_expression(diagnostics, node, context))
.try_collect()?;
Ok(RevsetExpression::union_all(&expressions))
}
ExpressionKind::FunctionCall(function) => {
lower_function_call(diagnostics, function, context)
}
ExpressionKind::AliasExpanded(..) => unreachable!(),
})
}
pub fn parse(
diagnostics: &mut RevsetDiagnostics,
revset_str: &str,
context: &RevsetParseContext,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
let node = parse_program(revset_str)?;
let node =
dsl_util::expand_aliases_with_locals(node, context.aliases_map, &context.local_variables)?;
lower_expression(diagnostics, &node, &context.to_lowering_context())
.map_err(|err| err.extend_function_candidates(context.aliases_map.function_names()))
}
pub fn parse_string_expression(
diagnostics: &mut RevsetDiagnostics,
text: &str,
) -> Result<StringExpression, RevsetParseError> {
let node = parse_program(text)?;
let default_kind = "glob";
expect_string_expression_inner(diagnostics, &node, default_kind)
}
fn to_binary_expression<T: Clone>(
expressions: &[T],
unit: &impl Fn() -> T,
binary: &impl Fn(&T, &T) -> T,
) -> T {
match expressions {
[] => unit(),
[expression] => expression.clone(),
_ => {
let (left, right) = expressions.split_at(expressions.len() / 2);
binary(
&to_binary_expression(left, unit, binary),
&to_binary_expression(right, unit, binary),
)
}
}
}
type TransformedExpression<St> = Option<Arc<RevsetExpression<St>>>;
type PreTransformedExpression<St> = ControlFlow<TransformedExpression<St>, ()>;
fn transform_expression<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
mut pre: impl FnMut(&Arc<RevsetExpression<St>>) -> PreTransformedExpression<St>,
mut post: impl FnMut(&Arc<RevsetExpression<St>>) -> TransformedExpression<St>,
) -> TransformedExpression<St> {
let Ok(transformed) =
try_transform_expression::<St, Infallible>(expression, |x| Ok(pre(x)), |x| Ok(post(x)));
transformed
}
fn transform_expression_bottom_up<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
post: impl FnMut(&Arc<RevsetExpression<St>>) -> TransformedExpression<St>,
) -> TransformedExpression<St> {
transform_expression(expression, |_| ControlFlow::Continue(()), post)
}
fn try_transform_expression<St: ExpressionState, E>(
expression: &Arc<RevsetExpression<St>>,
mut pre: impl FnMut(&Arc<RevsetExpression<St>>) -> Result<PreTransformedExpression<St>, E>,
mut post: impl FnMut(&Arc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
) -> Result<TransformedExpression<St>, E> {
fn transform_child_rec<St: ExpressionState, E>(
expression: &Arc<RevsetExpression<St>>,
pre: &mut impl FnMut(&Arc<RevsetExpression<St>>) -> Result<PreTransformedExpression<St>, E>,
post: &mut impl FnMut(&Arc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
) -> Result<TransformedExpression<St>, E> {
Ok(match expression.as_ref() {
RevsetExpression::None => None,
RevsetExpression::All => None,
RevsetExpression::VisibleHeads => None,
RevsetExpression::VisibleHeadsOrReferenced => None,
RevsetExpression::Root => None,
RevsetExpression::Commits(_) => None,
RevsetExpression::CommitRef(_) => None,
RevsetExpression::Ancestors {
heads,
generation,
parents_range,
} => transform_rec(heads, pre, post)?.map(|heads| RevsetExpression::Ancestors {
heads,
generation: generation.clone(),
parents_range: parents_range.clone(),
}),
RevsetExpression::Descendants { roots, generation } => transform_rec(roots, pre, post)?
.map(|roots| RevsetExpression::Descendants {
roots,
generation: generation.clone(),
}),
RevsetExpression::Range {
roots,
heads,
generation,
parents_range,
} => transform_rec_pair((roots, heads), pre, post)?.map(|(roots, heads)| {
RevsetExpression::Range {
roots,
heads,
generation: generation.clone(),
parents_range: parents_range.clone(),
}
}),
RevsetExpression::DagRange { roots, heads } => {
transform_rec_pair((roots, heads), pre, post)?
.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) => {
transform_rec(candidates, pre, post)?.map(RevsetExpression::Heads)
}
RevsetExpression::HeadsRange {
roots,
heads,
parents_range,
filter,
} => {
let transformed_roots = transform_rec(roots, pre, post)?;
let transformed_heads = transform_rec(heads, pre, post)?;
let transformed_filter = transform_rec(filter, pre, post)?;
(transformed_roots.is_some()
|| transformed_heads.is_some()
|| transformed_filter.is_some())
.then(|| RevsetExpression::HeadsRange {
roots: transformed_roots.unwrap_or_else(|| roots.clone()),
heads: transformed_heads.unwrap_or_else(|| heads.clone()),
parents_range: parents_range.clone(),
filter: transformed_filter.unwrap_or_else(|| filter.clone()),
})
}
RevsetExpression::Roots(candidates) => {
transform_rec(candidates, pre, post)?.map(RevsetExpression::Roots)
}
RevsetExpression::ForkPoint(expression) => {
transform_rec(expression, pre, post)?.map(RevsetExpression::ForkPoint)
}
RevsetExpression::Bisect(expression) => {
transform_rec(expression, pre, post)?.map(RevsetExpression::Bisect)
}
RevsetExpression::HasSize { candidates, count } => {
transform_rec(candidates, pre, post)?.map(|candidates| RevsetExpression::HasSize {
candidates,
count: *count,
})
}
RevsetExpression::Latest { candidates, count } => transform_rec(candidates, pre, post)?
.map(|candidates| RevsetExpression::Latest {
candidates,
count: *count,
}),
RevsetExpression::Filter(_) => None,
RevsetExpression::AsFilter(candidates) => {
transform_rec(candidates, pre, post)?.map(RevsetExpression::AsFilter)
}
RevsetExpression::Divergent => None,
RevsetExpression::AtOperation {
operation,
candidates,
} => transform_rec(candidates, pre, post)?.map(|candidates| {
RevsetExpression::AtOperation {
operation: operation.clone(),
candidates,
}
}),
RevsetExpression::WithinReference {
candidates,
commits,
} => transform_rec(candidates, pre, post)?.map(|candidates| {
RevsetExpression::WithinReference {
candidates,
commits: commits.clone(),
}
}),
RevsetExpression::WithinVisibility {
candidates,
visible_heads,
} => transform_rec(candidates, pre, post)?.map(|candidates| {
RevsetExpression::WithinVisibility {
candidates,
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)
}
RevsetExpression::NotIn(complement) => {
transform_rec(complement, pre, post)?.map(RevsetExpression::NotIn)
}
RevsetExpression::Union(expression1, expression2) => {
transform_rec_pair((expression1, expression2), pre, post)?.map(
|(expression1, expression2)| RevsetExpression::Union(expression1, expression2),
)
}
RevsetExpression::Intersection(expression1, expression2) => {
transform_rec_pair((expression1, expression2), pre, post)?.map(
|(expression1, expression2)| {
RevsetExpression::Intersection(expression1, expression2)
},
)
}
RevsetExpression::Difference(expression1, expression2) => {
transform_rec_pair((expression1, expression2), pre, post)?.map(
|(expression1, expression2)| {
RevsetExpression::Difference(expression1, expression2)
},
)
}
}
.map(Arc::new))
}
#[expect(clippy::type_complexity)]
fn transform_rec_pair<St: ExpressionState, E>(
(expression1, expression2): (&Arc<RevsetExpression<St>>, &Arc<RevsetExpression<St>>),
pre: &mut impl FnMut(&Arc<RevsetExpression<St>>) -> Result<PreTransformedExpression<St>, E>,
post: &mut impl FnMut(&Arc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
) -> Result<Option<(Arc<RevsetExpression<St>>, Arc<RevsetExpression<St>>)>, E> {
match (
transform_rec(expression1, pre, post)?,
transform_rec(expression2, pre, post)?,
) {
(Some(new_expression1), Some(new_expression2)) => {
Ok(Some((new_expression1, new_expression2)))
}
(Some(new_expression1), None) => Ok(Some((new_expression1, expression2.clone()))),
(None, Some(new_expression2)) => Ok(Some((expression1.clone(), new_expression2))),
(None, None) => Ok(None),
}
}
fn transform_rec<St: ExpressionState, E>(
expression: &Arc<RevsetExpression<St>>,
pre: &mut impl FnMut(&Arc<RevsetExpression<St>>) -> Result<PreTransformedExpression<St>, E>,
post: &mut impl FnMut(&Arc<RevsetExpression<St>>) -> Result<TransformedExpression<St>, E>,
) -> Result<TransformedExpression<St>, E> {
if let ControlFlow::Break(transformed) = pre(expression)? {
return Ok(transformed);
}
if let Some(new_expression) = transform_child_rec(expression, pre, post)? {
Ok(Some(post(&new_expression)?.unwrap_or(new_expression)))
} else {
post(expression)
}
}
transform_rec(expression, &mut pre, &mut post)
}
trait ExpressionStateFolder<InSt: ExpressionState, OutSt: ExpressionState> {
type Error;
fn fold_expression(
&mut self,
expression: &RevsetExpression<InSt>,
) -> Result<Arc<RevsetExpression<OutSt>>, Self::Error> {
fold_child_expression_state(self, expression)
}
fn fold_commit_ref(
&mut self,
commit_ref: &InSt::CommitRef,
) -> Result<Arc<RevsetExpression<OutSt>>, Self::Error>;
fn fold_at_operation(
&mut self,
operation: &InSt::Operation,
candidates: &RevsetExpression<InSt>,
) -> Result<Arc<RevsetExpression<OutSt>>, Self::Error>;
}
fn fold_child_expression_state<InSt, OutSt, F>(
folder: &mut F,
expression: &RevsetExpression<InSt>,
) -> Result<Arc<RevsetExpression<OutSt>>, F::Error>
where
InSt: ExpressionState,
OutSt: ExpressionState,
F: ExpressionStateFolder<InSt, OutSt> + ?Sized,
{
let expression: Arc<_> = match expression {
RevsetExpression::None => RevsetExpression::None.into(),
RevsetExpression::All => RevsetExpression::All.into(),
RevsetExpression::VisibleHeads => RevsetExpression::VisibleHeads.into(),
RevsetExpression::VisibleHeadsOrReferenced => {
RevsetExpression::VisibleHeadsOrReferenced.into()
}
RevsetExpression::Root => RevsetExpression::Root.into(),
RevsetExpression::Commits(ids) => RevsetExpression::Commits(ids.clone()).into(),
RevsetExpression::CommitRef(commit_ref) => folder.fold_commit_ref(commit_ref)?,
RevsetExpression::Ancestors {
heads,
generation,
parents_range,
} => {
let heads = folder.fold_expression(heads)?;
let generation = generation.clone();
let parents_range = parents_range.clone();
RevsetExpression::Ancestors {
heads,
generation,
parents_range,
}
.into()
}
RevsetExpression::Descendants { roots, generation } => {
let roots = folder.fold_expression(roots)?;
let generation = generation.clone();
RevsetExpression::Descendants { roots, generation }.into()
}
RevsetExpression::Range {
roots,
heads,
generation,
parents_range,
} => {
let roots = folder.fold_expression(roots)?;
let heads = folder.fold_expression(heads)?;
let generation = generation.clone();
let parents_range = parents_range.clone();
RevsetExpression::Range {
roots,
heads,
generation,
parents_range,
}
.into()
}
RevsetExpression::DagRange { roots, heads } => {
let roots = folder.fold_expression(roots)?;
let heads = folder.fold_expression(heads)?;
RevsetExpression::DagRange { roots, heads }.into()
}
RevsetExpression::Reachable { sources, domain } => {
let sources = folder.fold_expression(sources)?;
let domain = folder.fold_expression(domain)?;
RevsetExpression::Reachable { sources, domain }.into()
}
RevsetExpression::Heads(heads) => {
let heads = folder.fold_expression(heads)?;
RevsetExpression::Heads(heads).into()
}
RevsetExpression::HeadsRange {
roots,
heads,
parents_range,
filter,
} => {
let roots = folder.fold_expression(roots)?;
let heads = folder.fold_expression(heads)?;
let parents_range = parents_range.clone();
let filter = folder.fold_expression(filter)?;
RevsetExpression::HeadsRange {
roots,
heads,
parents_range,
filter,
}
.into()
}
RevsetExpression::Roots(roots) => {
let roots = folder.fold_expression(roots)?;
RevsetExpression::Roots(roots).into()
}
RevsetExpression::ForkPoint(expression) => {
let expression = folder.fold_expression(expression)?;
RevsetExpression::ForkPoint(expression).into()
}
RevsetExpression::Bisect(expression) => {
let expression = folder.fold_expression(expression)?;
RevsetExpression::Bisect(expression).into()
}
RevsetExpression::HasSize { candidates, count } => {
let candidates = folder.fold_expression(candidates)?;
RevsetExpression::HasSize {
candidates,
count: *count,
}
.into()
}
RevsetExpression::Latest { candidates, count } => {
let candidates = folder.fold_expression(candidates)?;
let count = *count;
RevsetExpression::Latest { candidates, count }.into()
}
RevsetExpression::Filter(predicate) => RevsetExpression::Filter(predicate.clone()).into(),
RevsetExpression::AsFilter(candidates) => {
let candidates = folder.fold_expression(candidates)?;
RevsetExpression::AsFilter(candidates).into()
}
RevsetExpression::Divergent => RevsetExpression::Divergent.into(),
RevsetExpression::AtOperation {
operation,
candidates,
} => folder.fold_at_operation(operation, candidates)?,
RevsetExpression::WithinReference {
candidates,
commits,
} => {
let candidates = folder.fold_expression(candidates)?;
let commits = commits.clone();
RevsetExpression::WithinReference {
candidates,
commits,
}
.into()
}
RevsetExpression::WithinVisibility {
candidates,
visible_heads,
} => {
let candidates = folder.fold_expression(candidates)?;
let visible_heads = visible_heads.clone();
RevsetExpression::WithinVisibility {
candidates,
visible_heads,
}
.into()
}
RevsetExpression::Coalesce(expression1, expression2) => {
let expression1 = folder.fold_expression(expression1)?;
let expression2 = folder.fold_expression(expression2)?;
RevsetExpression::Coalesce(expression1, expression2).into()
}
RevsetExpression::Present(candidates) => {
let candidates = folder.fold_expression(candidates)?;
RevsetExpression::Present(candidates).into()
}
RevsetExpression::NotIn(complement) => {
let complement = folder.fold_expression(complement)?;
RevsetExpression::NotIn(complement).into()
}
RevsetExpression::Union(expression1, expression2) => {
let expression1 = folder.fold_expression(expression1)?;
let expression2 = folder.fold_expression(expression2)?;
RevsetExpression::Union(expression1, expression2).into()
}
RevsetExpression::Intersection(expression1, expression2) => {
let expression1 = folder.fold_expression(expression1)?;
let expression2 = folder.fold_expression(expression2)?;
RevsetExpression::Intersection(expression1, expression2).into()
}
RevsetExpression::Difference(expression1, expression2) => {
let expression1 = folder.fold_expression(expression1)?;
let expression2 = folder.fold_expression(expression2)?;
RevsetExpression::Difference(expression1, expression2).into()
}
};
Ok(expression)
}
fn resolve_referenced_commits<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
if matches!(
expression.as_ref(),
RevsetExpression::WithinReference { .. }
) {
return None;
}
let mut inner_commits = Vec::new();
let mut outer_commits = Vec::new();
let transformed = transform_expression(
expression,
|expression| match expression.as_ref() {
RevsetExpression::WithinReference { commits, .. } => {
inner_commits.extend_from_slice(commits);
ControlFlow::Break(None)
}
RevsetExpression::WithinVisibility {
candidates,
visible_heads,
} => {
inner_commits.extend_from_slice(visible_heads);
let transformed = resolve_referenced_commits(candidates);
if let RevsetExpression::WithinReference { commits, .. } =
transformed.as_deref().unwrap_or(candidates)
{
inner_commits.extend_from_slice(commits);
}
ControlFlow::Break(transformed.map(|candidates| {
Arc::new(RevsetExpression::WithinVisibility {
candidates,
visible_heads: visible_heads.clone(),
})
}))
}
_ => ControlFlow::Continue(()),
},
|expression| {
if let RevsetExpression::Commits(commits) = expression.as_ref() {
outer_commits.extend_from_slice(commits);
}
None
},
);
outer_commits.extend(inner_commits);
if outer_commits.is_empty() {
return transformed;
}
Some(Arc::new(RevsetExpression::WithinReference {
candidates: transformed.unwrap_or_else(|| expression.clone()),
commits: outer_commits,
}))
}
fn flatten_intersections<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
fn flatten<St: ExpressionState>(
expression1: &Arc<RevsetExpression<St>>,
expression2: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
let recurse = |a, b| flatten(a, b).unwrap_or_else(|| a.intersection(b));
match expression2.as_ref() {
RevsetExpression::Intersection(inner1, inner2) => {
Some(recurse(expression1, inner1).intersection(inner2))
}
_ => None,
}
}
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Intersection(expression1, expression2) => {
flatten(expression1, expression2)
}
_ => None,
})
}
fn sort_intersection_by_key<St: ExpressionState, T: Ord>(
base: &Arc<RevsetExpression<St>>,
expression: &Arc<RevsetExpression<St>>,
mut get_key: impl FnMut(&RevsetExpression<St>) -> T,
) -> TransformedExpression<St> {
fn sort_intersection_helper<St: ExpressionState, T: Ord>(
base: &Arc<RevsetExpression<St>>,
expression: &Arc<RevsetExpression<St>>,
expression_key: T,
mut get_key: impl FnMut(&RevsetExpression<St>) -> T,
) -> TransformedExpression<St> {
if let RevsetExpression::Intersection(inner1, inner2) = base.as_ref() {
(expression_key < get_key(inner2)).then(|| {
sort_intersection_helper(inner1, expression, expression_key, get_key)
.unwrap_or_else(|| inner1.intersection(expression))
.intersection(inner2)
})
} else {
(expression_key < get_key(base)).then(|| expression.intersection(base))
}
}
sort_intersection_helper(base, expression, get_key(expression), get_key)
}
fn sort_negations_and_ancestors<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum AncestorsOrder {
NegatedAncestors,
Ancestors,
Other,
NegatedOther,
}
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Intersection(expression1, expression2) => {
sort_intersection_by_key(expression1, expression2, |expression| match expression {
RevsetExpression::Ancestors {
heads: _,
generation: Range { end: u64::MAX, .. },
parents_range: _,
} => AncestorsOrder::Ancestors,
RevsetExpression::NotIn(complement) => match complement.as_ref() {
RevsetExpression::Ancestors {
heads: _,
generation: Range { end: u64::MAX, .. },
parents_range: PARENTS_RANGE_FULL,
} => AncestorsOrder::NegatedAncestors,
_ => AncestorsOrder::NegatedOther,
},
_ => AncestorsOrder::Other,
})
}
_ => None,
})
}
fn internalize_filter<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
fn get_filter<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> Option<&Arc<RevsetExpression<St>>> {
match expression.as_ref() {
RevsetExpression::Filter(_) => Some(expression),
RevsetExpression::AsFilter(candidates) => Some(candidates),
_ => None,
}
}
fn mark_filter<St: ExpressionState>(
expression: Arc<RevsetExpression<St>>,
) -> Arc<RevsetExpression<St>> {
Arc::new(RevsetExpression::AsFilter(expression))
}
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Present(e) => get_filter(e).map(|f| mark_filter(f.present())),
RevsetExpression::NotIn(e) => get_filter(e).map(|f| mark_filter(f.negated())),
RevsetExpression::Union(e1, e2) => {
let f1 = get_filter(e1);
let f2 = get_filter(e2);
(f1.is_some() || f2.is_some())
.then(|| mark_filter(f1.unwrap_or(e1).union(f2.unwrap_or(e2))))
}
RevsetExpression::Intersection(e1, e2) => match (get_filter(e1), get_filter(e2)) {
(Some(f1), Some(f2)) => Some(mark_filter(f1.intersection(f2))),
(Some(_), None) => Some(e2.intersection(e1)),
(None, Some(f2)) => match e1.as_ref() {
RevsetExpression::Intersection(e1a, e1b) => {
get_filter(e1b).map(|f1b| e1a.intersection(&mark_filter(f1b.intersection(f2))))
}
_ => None,
},
(None, None) => match e1.as_ref() {
RevsetExpression::Intersection(e1a, e1b) => {
get_filter(e1b).map(|_| e1a.intersection(e2).intersection(e1b))
}
_ => None,
},
},
_ => None,
})
}
fn fold_redundant_expression<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Commits(commits) if commits.is_empty() => Some(RevsetExpression::none()),
RevsetExpression::NotIn(outer) => match outer.as_ref() {
RevsetExpression::NotIn(inner) => Some(inner.clone()),
RevsetExpression::None => Some(RevsetExpression::all()),
RevsetExpression::All => Some(RevsetExpression::none()),
_ => None,
},
RevsetExpression::Union(expression1, expression2) => {
match (expression1.as_ref(), expression2.as_ref()) {
(_, RevsetExpression::None) => Some(expression1.clone()),
(RevsetExpression::None, _) => Some(expression2.clone()),
(RevsetExpression::All, _) => Some(RevsetExpression::all()),
(_, RevsetExpression::All) => Some(RevsetExpression::all()),
_ => None,
}
}
RevsetExpression::Intersection(expression1, expression2) => {
match (expression1.as_ref(), expression2.as_ref()) {
(RevsetExpression::None, _) => Some(RevsetExpression::none()),
(_, RevsetExpression::None) => Some(RevsetExpression::none()),
(_, RevsetExpression::All) => Some(expression1.clone()),
(RevsetExpression::All, _) => Some(expression2.clone()),
_ => None,
}
}
_ => None,
})
}
fn ancestors_to_heads<St: ExpressionState>(
expression: &RevsetExpression<St>,
) -> Result<Arc<RevsetExpression<St>>, ()> {
match ancestors_to_heads_and_parents_range(expression) {
Ok((heads, PARENTS_RANGE_FULL)) => Ok(heads),
_ => Err(()),
}
}
fn ancestors_to_heads_and_parents_range<St: ExpressionState>(
expression: &RevsetExpression<St>,
) -> Result<(Arc<RevsetExpression<St>>, Range<u32>), ()> {
match expression {
RevsetExpression::Ancestors {
heads,
generation: GENERATION_RANGE_FULL,
parents_range,
} => Ok((heads.clone(), parents_range.clone())),
RevsetExpression::Ancestors {
heads,
generation: Range {
start,
end: u64::MAX,
},
parents_range,
} => Ok((
Arc::new(RevsetExpression::Ancestors {
heads: heads.clone(),
generation: (*start)..start.saturating_add(1),
parents_range: parents_range.clone(),
}),
parents_range.clone(),
)),
_ => Err(()),
}
}
fn fold_ancestors_union<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
fn union_ancestors<St: ExpressionState>(
expression1: &Arc<RevsetExpression<St>>,
expression2: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
let heads1 = ancestors_to_heads(expression1).ok()?;
let heads2 = ancestors_to_heads(expression2).ok()?;
Some(heads1.union(&heads2).ancestors())
}
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Union(expression1, expression2) => {
union_ancestors(expression1, expression2)
}
RevsetExpression::Intersection(expression1, expression2) => {
match (expression1.as_ref(), expression2.as_ref()) {
(RevsetExpression::NotIn(complement1), RevsetExpression::NotIn(complement2)) => {
union_ancestors(complement1, complement2).map(|expression| expression.negated())
}
_ => None,
}
}
_ => None,
})
}
fn fold_heads_range<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
struct FilteredRange<St: ExpressionState> {
roots: Arc<RevsetExpression<St>>,
heads_and_parents_range: Option<(Arc<RevsetExpression<St>>, Range<u32>)>,
filter: Arc<RevsetExpression<St>>,
}
impl<St: ExpressionState> FilteredRange<St> {
fn new(roots: Arc<RevsetExpression<St>>) -> Self {
Self {
roots,
heads_and_parents_range: None,
filter: RevsetExpression::all(),
}
}
fn add(mut self, expression: &Arc<RevsetExpression<St>>) -> Self {
if self.heads_and_parents_range.is_none() {
if let Ok(heads_and_parents_range) =
ancestors_to_heads_and_parents_range(expression)
{
self.heads_and_parents_range = Some(heads_and_parents_range);
return self;
}
}
self.add_filter(expression)
}
fn add_filter(mut self, expression: &Arc<RevsetExpression<St>>) -> Self {
self.filter = if let RevsetExpression::All = self.filter.as_ref() {
expression.clone()
} else {
self.filter.intersection(expression)
};
self
}
}
fn to_filtered_range<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> Option<FilteredRange<St>> {
if let Ok(heads_and_parents_range) = ancestors_to_heads_and_parents_range(expression) {
return Some(FilteredRange {
roots: RevsetExpression::none(),
heads_and_parents_range: Some(heads_and_parents_range),
filter: RevsetExpression::all(),
});
}
match expression.as_ref() {
RevsetExpression::NotIn(complement) => {
if let Ok(roots) = ancestors_to_heads(complement) {
Some(FilteredRange::new(roots))
} else {
Some(FilteredRange::new(RevsetExpression::none()).add_filter(expression))
}
}
RevsetExpression::All | RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_) => {
Some(FilteredRange::new(RevsetExpression::none()).add_filter(expression))
}
RevsetExpression::Intersection(expression1, expression2) => {
to_filtered_range(expression1).map(|filtered_range| filtered_range.add(expression2))
}
_ => None,
}
}
fn to_heads_range<St: ExpressionState>(
candidates: &Arc<RevsetExpression<St>>,
) -> Option<Arc<RevsetExpression<St>>> {
to_filtered_range(candidates).map(|filtered_range| {
let (heads, parents_range) =
filtered_range.heads_and_parents_range.unwrap_or_else(|| {
(
RevsetExpression::visible_heads_or_referenced(),
PARENTS_RANGE_FULL,
)
});
RevsetExpression::HeadsRange {
roots: filtered_range.roots,
heads,
parents_range,
filter: filtered_range.filter,
}
.into()
})
}
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Ancestors {
heads,
generation: GENERATION_RANGE_FULL,
parents_range: PARENTS_RANGE_FULL,
} => to_heads_range(heads).map(|heads| heads.ancestors()),
RevsetExpression::Heads(candidates) => to_heads_range(candidates),
_ => None,
})
}
fn to_difference_range<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
complement: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
let RevsetExpression::Ancestors {
heads,
generation,
parents_range,
} = expression.as_ref()
else {
return None;
};
let roots = ancestors_to_heads(complement).ok()?;
Some(Arc::new(RevsetExpression::Range {
roots,
heads: heads.clone(),
generation: generation.clone(),
parents_range: parents_range.clone(),
}))
}
fn fold_difference<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
fn to_difference<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
complement: &Arc<RevsetExpression<St>>,
) -> Arc<RevsetExpression<St>> {
to_difference_range(expression, complement).unwrap_or_else(|| expression.minus(complement))
}
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Intersection(expression1, expression2) => {
match (expression1.as_ref(), expression2.as_ref()) {
(_, RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_)) => None,
(_, RevsetExpression::NotIn(complement)) => {
Some(to_difference(expression1, complement))
}
(RevsetExpression::NotIn(complement), _) => {
Some(to_difference(expression2, complement))
}
_ => None,
}
}
_ => None,
})
}
fn fold_not_in_ancestors<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::NotIn(complement)
if matches!(complement.as_ref(), RevsetExpression::Ancestors { .. }) =>
{
to_difference_range(
&RevsetExpression::visible_heads_or_referenced().ancestors(),
complement,
)
}
_ => None,
})
}
fn unfold_difference<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Range {
roots,
heads,
parents_range,
generation,
} => {
let heads_ancestors = Arc::new(RevsetExpression::Ancestors {
heads: heads.clone(),
generation: generation.clone(),
parents_range: parents_range.clone(),
});
Some(heads_ancestors.intersection(&roots.ancestors().negated()))
}
RevsetExpression::Difference(expression1, expression2) => {
Some(expression1.intersection(&expression2.negated()))
}
_ => None,
})
}
fn fold_generation<St: ExpressionState>(
expression: &Arc<RevsetExpression<St>>,
) -> TransformedExpression<St> {
fn add_generation(generation1: &Range<u64>, generation2: &Range<u64>) -> Range<u64> {
if generation1.is_empty() || generation2.is_empty() {
GENERATION_RANGE_EMPTY
} else {
let start = u64::saturating_add(generation1.start, generation2.start);
let end = u64::saturating_add(generation1.end, generation2.end - 1);
start..end
}
}
transform_expression_bottom_up(expression, |expression| match expression.as_ref() {
RevsetExpression::Ancestors {
heads,
generation: generation1,
parents_range: parents1,
} => {
match heads.as_ref() {
RevsetExpression::Ancestors {
heads,
generation: generation2,
parents_range: parents2,
} if parents2 == parents1 => Some(Arc::new(RevsetExpression::Ancestors {
heads: heads.clone(),
generation: add_generation(generation1, generation2),
parents_range: parents1.clone(),
})),
_ => None,
}
}
RevsetExpression::Descendants {
roots,
generation: generation1,
} => {
match roots.as_ref() {
RevsetExpression::Descendants {
roots,
generation: generation2,
} => Some(Arc::new(RevsetExpression::Descendants {
roots: roots.clone(),
generation: add_generation(generation1, generation2),
})),
_ => None,
}
}
_ => None,
})
}
pub fn optimize<St: ExpressionState>(
expression: Arc<RevsetExpression<St>>,
) -> Arc<RevsetExpression<St>> {
let expression = resolve_referenced_commits(&expression).unwrap_or(expression);
let expression = unfold_difference(&expression).unwrap_or(expression);
let expression = fold_redundant_expression(&expression).unwrap_or(expression);
let expression = fold_generation(&expression).unwrap_or(expression);
let expression = flatten_intersections(&expression).unwrap_or(expression);
let expression = sort_negations_and_ancestors(&expression).unwrap_or(expression);
let expression = fold_ancestors_union(&expression).unwrap_or(expression);
let expression = internalize_filter(&expression).unwrap_or(expression);
let expression = fold_heads_range(&expression).unwrap_or(expression);
let expression = fold_difference(&expression).unwrap_or(expression);
fold_not_in_ancestors(&expression).unwrap_or(expression)
}
pub fn walk_revs<'index>(
repo: &'index dyn Repo,
wanted: &[CommitId],
unwanted: &[CommitId],
) -> Result<Box<dyn Revset + 'index>, RevsetEvaluationError> {
RevsetExpression::commits(unwanted.to_vec())
.range(&RevsetExpression::commits(wanted.to_vec()))
.evaluate(repo)
}
fn reload_repo_at_operation(
repo: &dyn Repo,
op_str: &str,
) -> Result<Arc<ReadonlyRepo>, RevsetResolutionError> {
let base_repo = repo.base_repo();
let operation = op_walk::resolve_op_with_repo(base_repo, op_str)
.block_on()
.map_err(|err| RevsetResolutionError::Other(err.into()))?;
base_repo
.reload_at(&operation)
.block_on()
.map_err(|err| match err {
RepoLoaderError::Backend(err) => RevsetResolutionError::Backend(err),
RepoLoaderError::Index(_)
| RepoLoaderError::IndexStore(_)
| RepoLoaderError::OpHeadResolution(_)
| RepoLoaderError::OpHeadsStoreError(_)
| RepoLoaderError::OpStore(_)
| RepoLoaderError::TransactionCommit(_) => RevsetResolutionError::Other(err.into()),
})
}
fn resolve_remote_symbol(
repo: &dyn Repo,
symbol: RemoteRefSymbol<'_>,
) -> Result<CommitId, RevsetResolutionError> {
let remote_ref = repo.view().get_remote_tag(symbol);
if let Some(id) = to_resolved_ref("remote_tag", symbol, &remote_ref.target)? {
return Ok(id);
}
let remote_ref = repo.view().get_remote_bookmark(symbol);
if let Some(id) = to_resolved_ref("remote_bookmark", symbol, &remote_ref.target)? {
return Ok(id);
}
Err(make_no_such_symbol_error(repo, symbol.to_string()))
}
fn to_resolved_ref(
kind: &'static str,
symbol: impl ToString,
target: &RefTarget,
) -> Result<Option<CommitId>, RevsetResolutionError> {
match target.as_resolved() {
Some(Some(id)) => Ok(Some(id.clone())),
Some(None) => Ok(None),
None => Err(RevsetResolutionError::ConflictedRef {
kind,
symbol: symbol.to_string(),
targets: target.added_ids().cloned().collect(),
}),
}
}
fn all_formatted_ref_symbols<'a>(
all_refs: impl Iterator<Item = (&'a RefName, LocalRemoteRefTarget<'a>)>,
include_synced_remotes: bool,
) -> impl Iterator<Item = String> {
all_refs.flat_map(move |(name, targets)| {
let local_target = targets.local_target;
let local_symbol = local_target
.is_present()
.then(|| format_symbol(name.as_str()));
let remote_symbols = targets
.remote_refs
.into_iter()
.filter(move |&(_, remote_ref)| {
include_synced_remotes
|| !remote_ref.is_tracked()
|| remote_ref.target != *local_target
})
.map(move |(remote, _)| format_remote_symbol(name.as_str(), remote.as_str()));
local_symbol.into_iter().chain(remote_symbols)
})
}
fn make_no_such_symbol_error(repo: &dyn Repo, name: String) -> RevsetResolutionError {
let include_synced_remotes = name.contains('@');
let tag_names = all_formatted_ref_symbols(repo.view().tags(), include_synced_remotes);
let bookmark_names = all_formatted_ref_symbols(repo.view().bookmarks(), include_synced_remotes);
let mut candidates = collect_similar(&name, itertools::chain(tag_names, bookmark_names));
candidates.dedup(); RevsetResolutionError::NoSuchRevision { name, candidates }
}
pub trait PartialSymbolResolver {
fn resolve_symbol(
&self,
repo: &dyn Repo,
symbol: &str,
) -> Result<Option<CommitId>, RevsetResolutionError>;
}
struct TagResolver;
impl PartialSymbolResolver for TagResolver {
fn resolve_symbol(
&self,
repo: &dyn Repo,
symbol: &str,
) -> Result<Option<CommitId>, RevsetResolutionError> {
let target = repo.view().get_local_tag(symbol.as_ref());
to_resolved_ref("tag", symbol, target)
}
}
struct BookmarkResolver;
impl PartialSymbolResolver for BookmarkResolver {
fn resolve_symbol(
&self,
repo: &dyn Repo,
symbol: &str,
) -> Result<Option<CommitId>, RevsetResolutionError> {
let target = repo.view().get_local_bookmark(symbol.as_ref());
to_resolved_ref("bookmark", symbol, target)
}
}
struct GitRefResolver;
impl PartialSymbolResolver for GitRefResolver {
fn resolve_symbol(
&self,
repo: &dyn Repo,
symbol: &str,
) -> Result<Option<CommitId>, RevsetResolutionError> {
let view = repo.view();
for git_ref_prefix in &["", "refs/"] {
let target = view.get_git_ref([git_ref_prefix, symbol].concat().as_ref());
if let Some(id) = to_resolved_ref("git_ref", symbol, target)? {
return Ok(Some(id));
}
}
Ok(None)
}
}
const DEFAULT_RESOLVERS: &[&dyn PartialSymbolResolver] =
&[&TagResolver, &BookmarkResolver, &GitRefResolver];
struct CommitPrefixResolver<'a> {
context_repo: &'a dyn Repo,
context: Option<&'a IdPrefixContext>,
}
impl CommitPrefixResolver<'_> {
fn try_resolve(
&self,
repo: &dyn Repo,
prefix: &HexPrefix,
) -> Result<Option<CommitId>, RevsetResolutionError> {
let index = self
.context
.map(|ctx| ctx.populate(self.context_repo))
.transpose()
.map_err(|err| RevsetResolutionError::Other(err.into()))?
.unwrap_or(IdPrefixIndex::empty());
match index
.resolve_commit_prefix(repo, prefix)
.map_err(|err| RevsetResolutionError::Other(err.into()))?
{
PrefixResolution::AmbiguousMatch => {
Err(RevsetResolutionError::AmbiguousCommitIdPrefix(prefix.hex()))
}
PrefixResolution::SingleMatch(id) => Ok(Some(id)),
PrefixResolution::NoMatch => Ok(None),
}
}
}
impl PartialSymbolResolver for CommitPrefixResolver<'_> {
fn resolve_symbol(
&self,
repo: &dyn Repo,
symbol: &str,
) -> Result<Option<CommitId>, RevsetResolutionError> {
if let Some(prefix) = HexPrefix::try_from_hex(symbol) {
self.try_resolve(repo, &prefix)
} else {
Ok(None)
}
}
}
struct ChangePrefixResolver<'a> {
context_repo: &'a dyn Repo,
context: Option<&'a IdPrefixContext>,
}
impl ChangePrefixResolver<'_> {
fn try_resolve(
&self,
repo: &dyn Repo,
prefix: &HexPrefix,
) -> Result<Option<ResolvedChangeTargets>, RevsetResolutionError> {
let index = self
.context
.map(|ctx| ctx.populate(self.context_repo))
.transpose()
.map_err(|err| RevsetResolutionError::Other(err.into()))?
.unwrap_or(IdPrefixIndex::empty());
match index
.resolve_change_prefix(repo, prefix)
.map_err(|err| RevsetResolutionError::Other(err.into()))?
{
PrefixResolution::AmbiguousMatch => Err(
RevsetResolutionError::AmbiguousChangeIdPrefix(prefix.reverse_hex()),
),
PrefixResolution::SingleMatch(ids) => Ok(Some(ids)),
PrefixResolution::NoMatch => Ok(None),
}
}
}
impl PartialSymbolResolver for ChangePrefixResolver<'_> {
fn resolve_symbol(
&self,
repo: &dyn Repo,
symbol: &str,
) -> Result<Option<CommitId>, RevsetResolutionError> {
let (change_id, offset) = if let Some((prefix, suffix)) = symbol.split_once('/') {
if prefix.is_empty() || suffix.is_empty() {
return Ok(None);
}
let Ok(offset) = suffix.parse() else {
return Ok(None);
};
(prefix, Some(offset))
} else {
(symbol, None)
};
let Some(prefix) = HexPrefix::try_from_reverse_hex(change_id) else {
return Ok(None);
};
let Some(targets) = self.try_resolve(repo, &prefix)? else {
return Ok(None);
};
if let Some(offset) = offset {
return Ok(targets.at_offset(offset).cloned());
}
match targets.visible_with_offsets().at_most_one() {
Ok(maybe_resolved) => Ok(maybe_resolved.map(|(_, target)| target.clone())),
Err(visible_targets) => Err(RevsetResolutionError::DivergentChangeId {
symbol: change_id.to_owned(),
visible_targets: visible_targets
.map(|(i, target)| (i, target.clone()))
.collect_vec(),
}),
}
}
}
pub trait SymbolResolverExtension: Send + Sync {
fn new_resolvers<'a>(
&self,
context_repo: &'a dyn Repo,
) -> Vec<Box<dyn PartialSymbolResolver + 'a>>;
}
pub struct SymbolResolver<'a> {
commit_id_resolver: CommitPrefixResolver<'a>,
change_id_resolver: ChangePrefixResolver<'a>,
extensions: Vec<Box<dyn PartialSymbolResolver + 'a>>,
}
impl<'a> SymbolResolver<'a> {
pub fn new(
context_repo: &'a dyn Repo,
extensions: &[impl AsRef<dyn SymbolResolverExtension>],
) -> Self {
SymbolResolver {
commit_id_resolver: CommitPrefixResolver {
context_repo,
context: None,
},
change_id_resolver: ChangePrefixResolver {
context_repo,
context: None,
},
extensions: extensions
.iter()
.flat_map(|ext| ext.as_ref().new_resolvers(context_repo))
.collect(),
}
}
pub fn with_id_prefix_context(mut self, id_prefix_context: &'a IdPrefixContext) -> Self {
self.commit_id_resolver.context = Some(id_prefix_context);
self.change_id_resolver.context = Some(id_prefix_context);
self
}
fn partial_resolvers(&self) -> impl Iterator<Item = &(dyn PartialSymbolResolver + 'a)> {
let prefix_resolvers: [&dyn PartialSymbolResolver; 2] =
[&self.commit_id_resolver, &self.change_id_resolver];
itertools::chain!(
DEFAULT_RESOLVERS.iter().copied(),
prefix_resolvers,
self.extensions.iter().map(|e| e.as_ref())
)
}
pub fn resolve_symbol(
&self,
repo: &dyn Repo,
symbol: &str,
) -> Result<CommitId, RevsetResolutionError> {
if symbol.is_empty() {
return Err(RevsetResolutionError::EmptyString);
}
for partial_resolver in self.partial_resolvers() {
if let Some(id) = partial_resolver.resolve_symbol(repo, symbol)? {
return Ok(id);
}
}
Err(make_no_such_symbol_error(repo, format_symbol(symbol)))
}
}
fn resolve_commit_ref(
repo: &dyn Repo,
commit_ref: &RevsetCommitRef,
symbol_resolver: &SymbolResolver,
) -> Result<Vec<CommitId>, RevsetResolutionError> {
match commit_ref {
RevsetCommitRef::Symbol(symbol) => {
let commit_id = symbol_resolver.resolve_symbol(repo, symbol)?;
Ok(vec![commit_id])
}
RevsetCommitRef::RemoteSymbol(symbol) => {
let commit_id = resolve_remote_symbol(repo, symbol.as_ref())?;
Ok(vec![commit_id])
}
RevsetCommitRef::WorkingCopy(name) => {
if let Some(commit_id) = repo.view().get_wc_commit_id(name) {
Ok(vec![commit_id.clone()])
} else {
Err(RevsetResolutionError::WorkspaceMissingWorkingCopy { name: name.clone() })
}
}
RevsetCommitRef::WorkingCopies => {
let wc_commits = repo.view().wc_commit_ids().values().cloned().collect_vec();
Ok(wc_commits)
}
RevsetCommitRef::ChangeId(prefix) => {
let resolver = &symbol_resolver.change_id_resolver;
Ok(resolver
.try_resolve(repo, prefix)?
.and_then(ResolvedChangeTargets::into_visible)
.unwrap_or_else(Vec::new))
}
RevsetCommitRef::CommitId(prefix) => {
let resolver = &symbol_resolver.commit_id_resolver;
Ok(resolver.try_resolve(repo, prefix)?.into_iter().collect())
}
RevsetCommitRef::Bookmarks(expression) => {
let commit_ids = repo
.view()
.local_bookmarks_matching(&expression.to_matcher())
.flat_map(|(_, target)| target.added_ids())
.cloned()
.collect();
Ok(commit_ids)
}
RevsetCommitRef::RemoteBookmarks {
symbol,
remote_ref_state,
} => {
let name_matcher = symbol.name.to_matcher();
let remote_matcher = symbol.remote.to_matcher();
let commit_ids = repo
.view()
.remote_bookmarks_matching(&name_matcher, &remote_matcher)
.filter(|(_, remote_ref)| {
remote_ref_state.is_none_or(|state| remote_ref.state == state)
})
.flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
.cloned()
.collect();
Ok(commit_ids)
}
RevsetCommitRef::Tags(expression) => {
let commit_ids = repo
.view()
.local_tags_matching(&expression.to_matcher())
.flat_map(|(_, target)| target.added_ids())
.cloned()
.collect();
Ok(commit_ids)
}
RevsetCommitRef::RemoteTags {
symbol,
remote_ref_state,
} => {
let name_matcher = symbol.name.to_matcher();
let remote_matcher = symbol.remote.to_matcher();
let commit_ids = repo
.view()
.remote_tags_matching(&name_matcher, &remote_matcher)
.filter(|(_, remote_ref)| {
remote_ref_state.is_none_or(|state| remote_ref.state == state)
})
.flat_map(|(_, remote_ref)| remote_ref.target.added_ids())
.cloned()
.collect();
Ok(commit_ids)
}
RevsetCommitRef::GitRefs => {
let mut commit_ids = vec![];
for ref_target in repo.view().git_refs().values() {
commit_ids.extend(ref_target.added_ids().cloned());
}
Ok(commit_ids)
}
RevsetCommitRef::GitHead => Ok(repo.view().git_head().added_ids().cloned().collect()),
}
}
struct ExpressionSymbolResolver<'a, 'b> {
base_repo: &'a dyn Repo,
repo_stack: Vec<Arc<ReadonlyRepo>>,
symbol_resolver: &'a SymbolResolver<'b>,
}
impl<'a, 'b> ExpressionSymbolResolver<'a, 'b> {
fn new(base_repo: &'a dyn Repo, symbol_resolver: &'a SymbolResolver<'b>) -> Self {
Self {
base_repo,
repo_stack: vec![],
symbol_resolver,
}
}
fn repo(&self) -> &dyn Repo {
self.repo_stack
.last()
.map_or(self.base_repo, |repo| repo.as_ref())
}
}
impl ExpressionStateFolder<UserExpressionState, ResolvedExpressionState>
for ExpressionSymbolResolver<'_, '_>
{
type Error = RevsetResolutionError;
fn fold_expression(
&mut self,
expression: &UserRevsetExpression,
) -> Result<Arc<ResolvedRevsetExpression>, Self::Error> {
match expression {
RevsetExpression::Present(candidates) => {
self.fold_expression(candidates).or_else(|err| match err {
RevsetResolutionError::NoSuchRevision { .. }
| RevsetResolutionError::WorkspaceMissingWorkingCopy { .. } => {
Ok(RevsetExpression::none())
}
RevsetResolutionError::EmptyString
| RevsetResolutionError::AmbiguousCommitIdPrefix(_)
| RevsetResolutionError::AmbiguousChangeIdPrefix(_)
| RevsetResolutionError::DivergentChangeId { .. }
| RevsetResolutionError::ConflictedRef { .. }
| RevsetResolutionError::Backend(_)
| RevsetResolutionError::Other(_) => Err(err),
})
}
_ => fold_child_expression_state(self, expression),
}
}
fn fold_commit_ref(
&mut self,
commit_ref: &RevsetCommitRef,
) -> Result<Arc<ResolvedRevsetExpression>, Self::Error> {
let commit_ids = resolve_commit_ref(self.repo(), commit_ref, self.symbol_resolver)?;
Ok(RevsetExpression::commits(commit_ids))
}
fn fold_at_operation(
&mut self,
operation: &String,
candidates: &UserRevsetExpression,
) -> Result<Arc<ResolvedRevsetExpression>, Self::Error> {
let repo = reload_repo_at_operation(self.repo(), operation)?;
self.repo_stack.push(repo);
let candidates = self.fold_expression(candidates)?;
let visible_heads = self.repo().view().heads().iter().cloned().collect();
self.repo_stack.pop();
Ok(Arc::new(RevsetExpression::WithinVisibility {
candidates,
visible_heads,
}))
}
}
fn resolve_symbols(
repo: &dyn Repo,
expression: &UserRevsetExpression,
symbol_resolver: &SymbolResolver,
) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
let mut resolver = ExpressionSymbolResolver::new(repo, symbol_resolver);
resolver.fold_expression(expression)
}
fn resolve_visibility(
repo: &dyn Repo,
expression: &ResolvedRevsetExpression,
) -> ResolvedExpression {
let context = VisibilityResolutionContext {
referenced_commits: &[],
visible_heads: &repo.view().heads().iter().cloned().collect_vec(),
root: repo.store().root_commit_id(),
};
context.resolve(expression)
}
#[derive(Clone, Debug)]
struct VisibilityResolutionContext<'a> {
referenced_commits: &'a [CommitId],
visible_heads: &'a [CommitId],
root: &'a CommitId,
}
impl VisibilityResolutionContext<'_> {
fn resolve(&self, expression: &ResolvedRevsetExpression) -> ResolvedExpression {
match expression {
RevsetExpression::None => ResolvedExpression::Commits(vec![]),
RevsetExpression::All => self.resolve_all(),
RevsetExpression::VisibleHeads => self.resolve_visible_heads(),
RevsetExpression::VisibleHeadsOrReferenced => {
self.resolve_visible_heads_or_referenced()
}
RevsetExpression::Root => self.resolve_root(),
RevsetExpression::Commits(commit_ids) => {
ResolvedExpression::Commits(commit_ids.clone())
}
RevsetExpression::CommitRef(commit_ref) => match *commit_ref {},
RevsetExpression::Ancestors {
heads,
generation,
parents_range,
} => ResolvedExpression::Ancestors {
heads: self.resolve(heads).into(),
generation: generation.clone(),
parents_range: parents_range.clone(),
},
RevsetExpression::Descendants { roots, generation } => ResolvedExpression::DagRange {
roots: self.resolve(roots).into(),
heads: self.resolve_visible_heads_or_referenced().into(),
generation_from_roots: generation.clone(),
},
RevsetExpression::Range {
roots,
heads,
generation,
parents_range,
} => ResolvedExpression::Range {
roots: self.resolve(roots).into(),
heads: self.resolve(heads).into(),
generation: generation.clone(),
parents_range: parents_range.clone(),
},
RevsetExpression::DagRange { roots, heads } => ResolvedExpression::DagRange {
roots: self.resolve(roots).into(),
heads: self.resolve(heads).into(),
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) => {
ResolvedExpression::Heads(self.resolve(candidates).into())
}
RevsetExpression::HeadsRange {
roots,
heads,
parents_range,
filter,
} => ResolvedExpression::HeadsRange {
roots: self.resolve(roots).into(),
heads: self.resolve(heads).into(),
parents_range: parents_range.clone(),
filter: (!matches!(filter.as_ref(), RevsetExpression::All))
.then(|| self.resolve_predicate(filter)),
},
RevsetExpression::Roots(candidates) => {
ResolvedExpression::Roots(self.resolve(candidates).into())
}
RevsetExpression::ForkPoint(expression) => {
ResolvedExpression::ForkPoint(self.resolve(expression).into())
}
RevsetExpression::Bisect(expression) => {
ResolvedExpression::Bisect(self.resolve(expression).into())
}
RevsetExpression::Latest { candidates, count } => ResolvedExpression::Latest {
candidates: self.resolve(candidates).into(),
count: *count,
},
RevsetExpression::HasSize { candidates, count } => ResolvedExpression::HasSize {
candidates: self.resolve(candidates).into(),
count: *count,
},
RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_) => {
ResolvedExpression::FilterWithin {
candidates: self.resolve_all().into(),
predicate: self.resolve_predicate(expression),
}
}
RevsetExpression::Divergent => ResolvedExpression::FilterWithin {
candidates: self.resolve_all().into(),
predicate: ResolvedPredicateExpression::Divergent {
visible_heads: self.visible_heads.to_owned(),
},
},
RevsetExpression::AtOperation { operation, .. } => match *operation {},
RevsetExpression::WithinReference {
candidates,
commits,
} => {
let context = VisibilityResolutionContext {
referenced_commits: commits,
visible_heads: self.visible_heads,
root: self.root,
};
context.resolve(candidates)
}
RevsetExpression::WithinVisibility {
candidates,
visible_heads,
} => {
let context = VisibilityResolutionContext {
referenced_commits: self.referenced_commits,
visible_heads,
root: self.root,
};
context.resolve(candidates)
}
RevsetExpression::Coalesce(expression1, expression2) => ResolvedExpression::Coalesce(
self.resolve(expression1).into(),
self.resolve(expression2).into(),
),
RevsetExpression::Present(candidates) => self.resolve(candidates),
RevsetExpression::NotIn(complement) => ResolvedExpression::Difference(
self.resolve_all().into(),
self.resolve(complement).into(),
),
RevsetExpression::Union(expression1, expression2) => ResolvedExpression::Union(
self.resolve(expression1).into(),
self.resolve(expression2).into(),
),
RevsetExpression::Intersection(expression1, expression2) => {
match expression2.as_ref() {
RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_) => {
ResolvedExpression::FilterWithin {
candidates: self.resolve(expression1).into(),
predicate: self.resolve_predicate(expression2),
}
}
_ => ResolvedExpression::Intersection(
self.resolve(expression1).into(),
self.resolve(expression2).into(),
),
}
}
RevsetExpression::Difference(expression1, expression2) => {
ResolvedExpression::Difference(
self.resolve(expression1).into(),
self.resolve(expression2).into(),
)
}
}
}
fn resolve_all(&self) -> ResolvedExpression {
ResolvedExpression::Ancestors {
heads: self.resolve_visible_heads_or_referenced().into(),
generation: GENERATION_RANGE_FULL,
parents_range: PARENTS_RANGE_FULL,
}
}
fn resolve_visible_heads(&self) -> ResolvedExpression {
ResolvedExpression::Commits(self.visible_heads.to_owned())
}
fn resolve_visible_heads_or_referenced(&self) -> ResolvedExpression {
let commits = itertools::chain(self.referenced_commits, self.visible_heads)
.cloned()
.collect();
ResolvedExpression::Commits(commits)
}
fn resolve_root(&self) -> ResolvedExpression {
ResolvedExpression::Commits(vec![self.root.to_owned()])
}
fn resolve_predicate(
&self,
expression: &ResolvedRevsetExpression,
) -> ResolvedPredicateExpression {
match expression {
RevsetExpression::None
| RevsetExpression::All
| RevsetExpression::VisibleHeads
| RevsetExpression::VisibleHeadsOrReferenced
| RevsetExpression::Root
| RevsetExpression::Commits(_)
| RevsetExpression::CommitRef(_)
| RevsetExpression::Ancestors { .. }
| RevsetExpression::Descendants { .. }
| RevsetExpression::Range { .. }
| RevsetExpression::DagRange { .. }
| RevsetExpression::Reachable { .. }
| RevsetExpression::Heads(_)
| RevsetExpression::HeadsRange { .. }
| RevsetExpression::Roots(_)
| RevsetExpression::ForkPoint(_)
| RevsetExpression::Bisect(_)
| RevsetExpression::HasSize { .. }
| RevsetExpression::Latest { .. } => {
ResolvedPredicateExpression::Set(self.resolve(expression).into())
}
RevsetExpression::Filter(predicate) => {
ResolvedPredicateExpression::Filter(predicate.clone())
}
RevsetExpression::AsFilter(candidates) => self.resolve_predicate(candidates),
RevsetExpression::Divergent => ResolvedPredicateExpression::Divergent {
visible_heads: self.visible_heads.to_owned(),
},
RevsetExpression::AtOperation { operation, .. } => match *operation {},
RevsetExpression::WithinReference { .. }
| RevsetExpression::WithinVisibility { .. } => {
ResolvedPredicateExpression::Set(self.resolve(expression).into())
}
RevsetExpression::Coalesce(_, _) => {
ResolvedPredicateExpression::Set(self.resolve(expression).into())
}
RevsetExpression::Present(candidates) => self.resolve_predicate(candidates),
RevsetExpression::NotIn(complement) => {
ResolvedPredicateExpression::NotIn(self.resolve_predicate(complement).into())
}
RevsetExpression::Union(expression1, expression2) => {
let predicate1 = self.resolve_predicate(expression1);
let predicate2 = self.resolve_predicate(expression2);
ResolvedPredicateExpression::Union(predicate1.into(), predicate2.into())
}
RevsetExpression::Intersection(expression1, expression2) => {
let predicate1 = self.resolve_predicate(expression1);
let predicate2 = self.resolve_predicate(expression2);
ResolvedPredicateExpression::Intersection(predicate1.into(), predicate2.into())
}
RevsetExpression::Difference(expression1, expression2) => {
let predicate1 = self.resolve_predicate(expression1);
let predicate2 = self.resolve_predicate(expression2);
let predicate2 = ResolvedPredicateExpression::NotIn(predicate2.into());
ResolvedPredicateExpression::Intersection(predicate1.into(), predicate2.into())
}
}
}
}
pub trait Revset: fmt::Debug {
fn iter<'a>(&self) -> Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'a>
where
Self: 'a;
fn stream<'a>(&self) -> LocalBoxStream<'a, Result<CommitId, RevsetEvaluationError>>
where
Self: 'a;
fn commit_change_ids<'a>(
&self,
) -> Box<dyn Iterator<Item = Result<(CommitId, ChangeId), RevsetEvaluationError>> + 'a>
where
Self: 'a;
fn iter_graph<'a>(
&self,
) -> Box<dyn Iterator<Item = Result<GraphNode<CommitId>, RevsetEvaluationError>> + 'a>
where
Self: 'a;
fn stream_graph<'a>(
&self,
) -> LocalBoxStream<'a, Result<GraphNode<CommitId>, RevsetEvaluationError>>
where
Self: 'a;
fn is_empty(&self) -> bool;
fn count_estimate(&self) -> Result<(usize, Option<usize>), RevsetEvaluationError>;
fn containing_fn<'a>(&self) -> Box<RevsetContainingFn<'a>>
where
Self: 'a;
}
pub type RevsetContainingFn<'a> = dyn Fn(&CommitId) -> Result<bool, RevsetEvaluationError> + 'a;
pub trait RevsetIteratorExt {
fn commits(
self,
store: &Arc<Store>,
) -> impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<Self>;
}
impl<I: Iterator<Item = Result<CommitId, RevsetEvaluationError>>> RevsetIteratorExt for I {
fn commits(
self,
store: &Arc<Store>,
) -> impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<I> {
let store = store.clone();
self.map(move |result| {
let commit_id = result?;
let commit = store
.clone()
.get_commit(&commit_id)
.map_err(RevsetEvaluationError::Backend)?;
Ok(commit)
})
}
}
pub trait RevsetStreamExt {
fn commits(
self,
store: &Arc<Store>,
) -> impl Stream<Item = Result<Commit, RevsetEvaluationError>> + use<'_, Self>;
}
impl<S: Stream<Item = Result<CommitId, RevsetEvaluationError>>> RevsetStreamExt for S {
fn commits(
self,
store: &Arc<Store>,
) -> impl Stream<Item = Result<Commit, RevsetEvaluationError>> + use<'_, S> {
self.map(async move |result| {
let commit_id = result?;
let commit = store
.get_commit_async(&commit_id)
.await
.map_err(RevsetEvaluationError::Backend)?;
Ok(commit)
})
.buffered(store.concurrency())
}
}
pub struct RevsetExtensions {
symbol_resolvers: Vec<Box<dyn SymbolResolverExtension>>,
function_map: HashMap<&'static str, RevsetFunction>,
}
impl Default for RevsetExtensions {
fn default() -> Self {
Self::new()
}
}
impl RevsetExtensions {
pub fn new() -> Self {
Self {
symbol_resolvers: vec![],
function_map: BUILTIN_FUNCTION_MAP.clone(),
}
}
pub fn symbol_resolvers(&self) -> &[Box<dyn SymbolResolverExtension>] {
&self.symbol_resolvers
}
pub fn add_symbol_resolver(&mut self, symbol_resolver: Box<dyn SymbolResolverExtension>) {
self.symbol_resolvers.push(symbol_resolver);
}
pub fn add_custom_function(&mut self, name: &'static str, func: RevsetFunction) {
match self.function_map.entry(name) {
hash_map::Entry::Occupied(_) => {
panic!("Conflict registering revset function '{name}'")
}
hash_map::Entry::Vacant(v) => v.insert(func),
};
}
}
#[derive(Clone)]
pub struct RevsetParseContext<'a> {
pub aliases_map: &'a RevsetAliasesMap,
pub local_variables: HashMap<&'a str, ExpressionNode<'a>>,
pub user_email: &'a str,
pub date_pattern_context: DatePatternContext,
pub default_ignored_remote: Option<&'a RemoteName>,
pub fileset_aliases_map: &'a FilesetAliasesMap,
pub use_glob_by_default: bool,
pub extensions: &'a RevsetExtensions,
pub workspace: Option<RevsetWorkspaceContext<'a>>,
}
impl<'a> RevsetParseContext<'a> {
fn to_lowering_context(&self) -> LoweringContext<'a> {
let RevsetParseContext {
aliases_map: _,
local_variables: _,
user_email,
date_pattern_context,
default_ignored_remote,
fileset_aliases_map,
use_glob_by_default,
extensions,
workspace,
} = *self;
LoweringContext {
user_email,
date_pattern_context,
default_ignored_remote,
fileset_aliases_map,
use_glob_by_default,
extensions,
workspace,
}
}
}
#[derive(Clone)]
pub struct LoweringContext<'a> {
user_email: &'a str,
date_pattern_context: DatePatternContext,
default_ignored_remote: Option<&'a RemoteName>,
fileset_aliases_map: &'a FilesetAliasesMap,
use_glob_by_default: bool,
extensions: &'a RevsetExtensions,
workspace: Option<RevsetWorkspaceContext<'a>>,
}
impl<'a> LoweringContext<'a> {
pub fn user_email(&self) -> &'a str {
self.user_email
}
pub fn date_pattern_context(&self) -> &DatePatternContext {
&self.date_pattern_context
}
pub fn fileset_parse_context(&self) -> Option<FilesetParseContext<'_>> {
Some(FilesetParseContext {
aliases_map: self.fileset_aliases_map,
path_converter: self.workspace?.path_converter,
})
}
pub fn symbol_resolvers(&self) -> &'a [impl AsRef<dyn SymbolResolverExtension> + use<>] {
self.extensions.symbol_resolvers()
}
}
#[derive(Clone, Copy, Debug)]
pub struct RevsetWorkspaceContext<'a> {
pub path_converter: &'a RepoPathUiConverter,
pub workspace_name: &'a WorkspaceName,
}
pub fn format_symbol(literal: &str) -> String {
if revset_parser::is_identifier(literal) {
literal.to_string()
} else {
format_string(literal)
}
}
pub fn format_string(literal: &str) -> String {
format!(r#""{}""#, dsl_util::escape_string(literal))
}
pub fn format_remote_symbol(name: &str, remote: &str) -> String {
let name = format_symbol(name);
let remote = format_symbol(remote);
format!("{name}@{remote}")
}
#[cfg(test)]
#[rustversion::attr(
since(1.89),
expect(clippy::cloned_ref_to_slice_refs, reason = "makes tests more readable")
)]
mod tests {
use std::path::PathBuf;
use assert_matches::assert_matches;
use super::*;
use crate::tests::TestResult;
fn parse(revset_str: &str) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
parse_with_aliases(revset_str, [] as [(&str, &str); 0])
}
fn parse_with_workspace(
revset_str: &str,
workspace_name: &WorkspaceName,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
parse_with_aliases_and_workspace(revset_str, [] as [(&str, &str); 0], workspace_name)
}
fn parse_with_aliases(
revset_str: &str,
aliases: impl IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
let mut aliases_map = RevsetAliasesMap::new();
for (decl, defn) in aliases {
aliases_map.insert(decl, defn)?;
}
let context = RevsetParseContext {
aliases_map: &aliases_map,
local_variables: HashMap::new(),
user_email: "test.user@example.com",
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some("ignored".as_ref()),
fileset_aliases_map: &FilesetAliasesMap::new(),
use_glob_by_default: true,
extensions: &RevsetExtensions::default(),
workspace: None,
};
super::parse(&mut RevsetDiagnostics::new(), revset_str, &context)
}
fn parse_with_aliases_and_workspace(
revset_str: &str,
aliases: impl IntoIterator<Item = (impl AsRef<str>, impl Into<String>)>,
workspace_name: &WorkspaceName,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
let path_converter = RepoPathUiConverter::Fs {
cwd: PathBuf::from("/"),
base: PathBuf::from("/"),
};
let workspace_ctx = RevsetWorkspaceContext {
path_converter: &path_converter,
workspace_name,
};
let mut aliases_map = RevsetAliasesMap::new();
for (decl, defn) in aliases {
aliases_map.insert(decl, defn)?;
}
let context = RevsetParseContext {
aliases_map: &aliases_map,
local_variables: HashMap::new(),
user_email: "test.user@example.com",
date_pattern_context: chrono::Utc::now().fixed_offset().into(),
default_ignored_remote: Some("ignored".as_ref()),
fileset_aliases_map: &FilesetAliasesMap::new(),
use_glob_by_default: true,
extensions: &RevsetExtensions::default(),
workspace: Some(workspace_ctx),
};
super::parse(&mut RevsetDiagnostics::new(), revset_str, &context)
}
fn insta_settings() -> insta::Settings {
let mut settings = insta::Settings::clone_current();
for _ in 0..4 {
settings.add_filter(
r"(?x)
\b([A-Z]\w*)\(\n
\s*(.{1,60}),\n
\s*\)",
"$1($2)",
);
}
settings
}
#[test]
#[expect(clippy::redundant_clone)] fn test_revset_expression_building() {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
let current_wc = UserRevsetExpression::working_copy(WorkspaceName::DEFAULT.to_owned());
let foo_symbol = UserRevsetExpression::symbol("foo".to_string());
let bar_symbol = UserRevsetExpression::symbol("bar".to_string());
let baz_symbol = UserRevsetExpression::symbol("baz".to_string());
insta::assert_debug_snapshot!(
current_wc,
@r#"CommitRef(WorkingCopy(WorkspaceNameBuf("default")))"#);
insta::assert_debug_snapshot!(
current_wc.heads(),
@r#"Heads(CommitRef(WorkingCopy(WorkspaceNameBuf("default"))))"#);
insta::assert_debug_snapshot!(
current_wc.roots(),
@r#"Roots(CommitRef(WorkingCopy(WorkspaceNameBuf("default"))))"#);
insta::assert_debug_snapshot!(
current_wc.parents(), @r#"
Ancestors {
heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
generation: 1..2,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
current_wc.ancestors(), @r#"
Ancestors {
heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
foo_symbol.children(), @r#"
Descendants {
roots: CommitRef(Symbol("foo")),
generation: 1..2,
}
"#);
insta::assert_debug_snapshot!(
foo_symbol.descendants(), @r#"
Descendants {
roots: CommitRef(Symbol("foo")),
generation: 0..18446744073709551615,
}
"#);
insta::assert_debug_snapshot!(
foo_symbol.dag_range_to(¤t_wc), @r#"
DagRange {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
}
"#);
insta::assert_debug_snapshot!(
foo_symbol.connected(), @r#"
DagRange {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("foo")),
}
"#);
insta::assert_debug_snapshot!(
foo_symbol.range(¤t_wc), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
foo_symbol.negated(),
@r#"NotIn(CommitRef(Symbol("foo")))"#);
insta::assert_debug_snapshot!(
foo_symbol.union(¤t_wc), @r#"
Union(
CommitRef(Symbol("foo")),
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
)
"#);
insta::assert_debug_snapshot!(
UserRevsetExpression::union_all(&[]),
@"None");
insta::assert_debug_snapshot!(
RevsetExpression::union_all(&[current_wc.clone()]),
@r#"CommitRef(WorkingCopy(WorkspaceNameBuf("default")))"#);
insta::assert_debug_snapshot!(
RevsetExpression::union_all(&[current_wc.clone(), foo_symbol.clone()]),
@r#"
Union(
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
CommitRef(Symbol("foo")),
)
"#);
insta::assert_debug_snapshot!(
RevsetExpression::union_all(&[
current_wc.clone(),
foo_symbol.clone(),
bar_symbol.clone(),
]),
@r#"
Union(
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
Union(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
),
)
"#);
insta::assert_debug_snapshot!(
RevsetExpression::union_all(&[
current_wc.clone(),
foo_symbol.clone(),
bar_symbol.clone(),
baz_symbol.clone(),
]),
@r#"
Union(
Union(
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
CommitRef(Symbol("foo")),
),
Union(
CommitRef(Symbol("bar")),
CommitRef(Symbol("baz")),
),
)
"#);
insta::assert_debug_snapshot!(
foo_symbol.intersection(¤t_wc), @r#"
Intersection(
CommitRef(Symbol("foo")),
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
)
"#);
insta::assert_debug_snapshot!(
foo_symbol.minus(¤t_wc), @r#"
Difference(
CommitRef(Symbol("foo")),
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
)
"#);
insta::assert_debug_snapshot!(
UserRevsetExpression::coalesce(&[]),
@"None");
insta::assert_debug_snapshot!(
RevsetExpression::coalesce(&[current_wc.clone()]),
@r#"CommitRef(WorkingCopy(WorkspaceNameBuf("default")))"#);
insta::assert_debug_snapshot!(
RevsetExpression::coalesce(&[current_wc.clone(), foo_symbol.clone()]),
@r#"
Coalesce(
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
CommitRef(Symbol("foo")),
)
"#);
insta::assert_debug_snapshot!(
RevsetExpression::coalesce(&[
current_wc.clone(),
foo_symbol.clone(),
bar_symbol.clone(),
]),
@r#"
Coalesce(
CommitRef(WorkingCopy(WorkspaceNameBuf("default"))),
Coalesce(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
),
)
"#);
}
#[test]
fn test_parse_revset() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
let main_workspace_name = WorkspaceNameBuf::from("main");
let other_workspace_name = WorkspaceNameBuf::from("other");
insta::assert_debug_snapshot!(
parse("@").unwrap_err().kind(),
@"WorkingCopyWithoutWorkspace");
insta::assert_debug_snapshot!(
parse("main@")?,
@r#"CommitRef(WorkingCopy(WorkspaceNameBuf("main")))"#);
insta::assert_debug_snapshot!(
parse_with_workspace("@", &main_workspace_name)?,
@r#"CommitRef(WorkingCopy(WorkspaceNameBuf("main")))"#);
insta::assert_debug_snapshot!(
parse_with_workspace("main@", &other_workspace_name)?,
@r#"CommitRef(WorkingCopy(WorkspaceNameBuf("main")))"#);
insta::assert_debug_snapshot!(
parse("author_name(foo@)").unwrap_err().kind(),
@r#"Expression("Invalid string expression")"#);
insta::assert_debug_snapshot!(
parse(r#"author_name("foo@")"#)?,
@r#"Filter(AuthorName(Pattern(Exact("foo@"))))"#);
insta::assert_debug_snapshot!(
parse("foo")?,
@r#"CommitRef(Symbol("foo"))"#);
insta::assert_debug_snapshot!(
parse("bookmarks()")?,
@r#"CommitRef(Bookmarks(Pattern(Substring(""))))"#);
insta::assert_debug_snapshot!(
parse("tags()")?,
@r#"CommitRef(Tags(Pattern(Substring(""))))"#);
insta::assert_debug_snapshot!(parse("remote_bookmarks()")?, @r#"
CommitRef(
RemoteBookmarks {
symbol: RemoteRefSymbolExpression {
name: Pattern(Substring("")),
remote: NotIn(Pattern(Exact("ignored"))),
},
remote_ref_state: None,
},
)
"#);
insta::assert_debug_snapshot!(parse("tracked_remote_bookmarks()")?, @r#"
CommitRef(
RemoteBookmarks {
symbol: RemoteRefSymbolExpression {
name: Pattern(Substring("")),
remote: NotIn(Pattern(Exact("ignored"))),
},
remote_ref_state: Some(Tracked),
},
)
"#);
insta::assert_debug_snapshot!(parse("untracked_remote_bookmarks()")?, @r#"
CommitRef(
RemoteBookmarks {
symbol: RemoteRefSymbolExpression {
name: Pattern(Substring("")),
remote: NotIn(Pattern(Exact("ignored"))),
},
remote_ref_state: Some(New),
},
)
"#);
insta::assert_debug_snapshot!(parse("remote_tags()")?, @r#"
CommitRef(
RemoteTags {
symbol: RemoteRefSymbolExpression {
name: Pattern(Substring("")),
remote: NotIn(Pattern(Exact("ignored"))),
},
remote_ref_state: None,
},
)
"#);
insta::assert_debug_snapshot!(parse("tracked_remote_tags()")?, @r#"
CommitRef(
RemoteTags {
symbol: RemoteRefSymbolExpression {
name: Pattern(Substring("")),
remote: NotIn(Pattern(Exact("ignored"))),
},
remote_ref_state: Some(Tracked),
},
)
"#);
insta::assert_debug_snapshot!(parse("untracked_remote_tags()")?, @r#"
CommitRef(
RemoteTags {
symbol: RemoteRefSymbolExpression {
name: Pattern(Substring("")),
remote: NotIn(Pattern(Exact("ignored"))),
},
remote_ref_state: Some(New),
},
)
"#);
insta::assert_debug_snapshot!(
parse("'foo'")?,
@r#"CommitRef(Symbol("foo"))"#);
insta::assert_debug_snapshot!(parse("foo-")?, @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 1..2,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(parse("foo+")?, @r#"
Descendants {
roots: CommitRef(Symbol("foo")),
generation: 1..2,
}
"#);
insta::assert_debug_snapshot!(parse("::foo")?, @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(parse("foo::")?, @r#"
Descendants {
roots: CommitRef(Symbol("foo")),
generation: 0..18446744073709551615,
}
"#);
insta::assert_debug_snapshot!(parse("foo::bar")?, @r#"
DagRange {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
}
"#);
insta::assert_debug_snapshot!(parse("::")?, @"All");
insta::assert_debug_snapshot!(parse("..foo")?, @r#"
Range {
roots: Root,
heads: CommitRef(Symbol("foo")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(parse("foo..")?, @r#"
NotIn(
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
)
"#);
insta::assert_debug_snapshot!(parse("foo..bar")?, @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(parse("..")?, @"NotIn(Root)");
insta::assert_debug_snapshot!(
parse("~ foo")?,
@r#"NotIn(CommitRef(Symbol("foo")))"#);
insta::assert_debug_snapshot!(parse("foo & bar")?, @r#"
Intersection(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(parse("foo | bar")?, @r#"
Union(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(parse("foo ~ bar")?, @r#"
Difference(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
Ok(())
}
#[test]
fn test_parse_string_pattern() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse(r#"bookmarks("foo")"#)?,
@r#"CommitRef(Bookmarks(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse(r#"bookmarks(exact:"foo")"#)?,
@r#"CommitRef(Bookmarks(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse(r#"bookmarks(substring:"foo")"#)?,
@r#"CommitRef(Bookmarks(Pattern(Substring("foo"))))"#);
insta::assert_debug_snapshot!(
parse(r#"bookmarks(bad:"foo")"#).unwrap_err().kind(),
@r#"Expression("Invalid string pattern")"#);
insta::assert_debug_snapshot!(
parse(r#"bookmarks(exact::"foo")"#).unwrap_err().kind(),
@r#"Expression("Invalid string expression")"#);
insta::assert_debug_snapshot!(
parse(r#"bookmarks(exact:"foo"+)"#).unwrap_err().kind(),
@r#"Expression("Expected string")"#);
insta::assert_debug_snapshot!(
parse(r#"tags("foo")"#)?,
@r#"CommitRef(Tags(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse(r#"tags(exact:"foo")"#)?,
@r#"CommitRef(Tags(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse(r#"tags(substring:"foo")"#)?,
@r#"CommitRef(Tags(Pattern(Substring("foo"))))"#);
insta::assert_debug_snapshot!(
parse(r#"tags(bad:"foo")"#).unwrap_err().kind(),
@r#"Expression("Invalid string pattern")"#);
insta::assert_debug_snapshot!(
parse(r#"tags(exact::"foo")"#).unwrap_err().kind(),
@r#"Expression("Invalid string expression")"#);
insta::assert_debug_snapshot!(
parse(r#"tags(exact:"foo"+)"#).unwrap_err().kind(),
@r#"Expression("Expected string")"#);
assert_matches!(
parse(r#"(exact:"foo")"#).unwrap_err().kind(),
RevsetParseErrorKind::NotInfixOperator { .. }
);
Ok(())
}
#[test]
fn test_parse_compound_string_expression() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse(r#"tags(~a)"#)?,
@r#"
CommitRef(
Tags(NotIn(Pattern(Exact("a")))),
)
"#);
insta::assert_debug_snapshot!(
parse(r#"tags(a|b&c)"#)?,
@r#"
CommitRef(
Tags(
Union(
Pattern(Exact("a")),
Intersection(
Pattern(Exact("b")),
Pattern(Exact("c")),
),
),
),
)
"#);
insta::assert_debug_snapshot!(
parse(r#"tags(a|b|c)"#)?,
@r#"
CommitRef(
Tags(
Union(
Pattern(Exact("a")),
Union(
Pattern(Exact("b")),
Pattern(Exact("c")),
),
),
),
)
"#);
insta::assert_debug_snapshot!(
parse(r#"tags(a~(b|c))"#)?,
@r#"
CommitRef(
Tags(
Intersection(
Pattern(Exact("a")),
NotIn(
Union(
Pattern(Exact("b")),
Pattern(Exact("c")),
),
),
),
),
)
"#);
Ok(())
}
#[test]
fn test_parse_revset_function() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse("parents(foo)")?, @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 1..2,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
parse("parents(\"foo\")")?, @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 1..2,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
parse("ancestors(parents(foo))")?, @r#"
Ancestors {
heads: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 1..2,
parents_range: 0..4294967295,
},
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
parse("parents(foo, bar, baz)").unwrap_err().kind(), @r#"
InvalidFunctionArguments {
name: "parents",
message: "Expected 1 to 2 arguments",
}
"#);
insta::assert_debug_snapshot!(
parse("parents(foo, 2)")?, @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 2..3,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
parse("root()")?,
@"Root");
assert!(parse("root(a)").is_err());
insta::assert_debug_snapshot!(
parse(r#"description("")"#)?,
@r#"Filter(Description(Pattern(Exact(""))))"#);
insta::assert_debug_snapshot!(
parse("description(foo)")?,
@r#"Filter(Description(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse("description(visible_heads())").unwrap_err().kind(),
@r#"Expression("Invalid string expression")"#);
insta::assert_debug_snapshot!(
parse("description(\"(foo)\")")?,
@r#"Filter(Description(Pattern(Exact("(foo)"))))"#);
assert!(parse("mine(foo)").is_err());
insta::assert_debug_snapshot!(
parse_with_workspace("empty()", WorkspaceName::DEFAULT)?,
@"NotIn(Filter(File(All)))");
assert!(parse_with_workspace("empty(foo)", WorkspaceName::DEFAULT).is_err());
assert!(parse_with_workspace("file()", WorkspaceName::DEFAULT).is_err());
insta::assert_debug_snapshot!(
parse_with_workspace("files(foo)", WorkspaceName::DEFAULT)?,
@r#"Filter(File(Pattern(PrefixPath("foo"))))"#);
insta::assert_debug_snapshot!(
parse_with_workspace("files(all())", WorkspaceName::DEFAULT)?,
@"Filter(File(All))");
insta::assert_debug_snapshot!(
parse_with_workspace(r#"files(file:"foo")"#, WorkspaceName::DEFAULT)?,
@r#"Filter(File(Pattern(FilePath("foo"))))"#);
insta::assert_debug_snapshot!(
parse_with_workspace("files(foo|bar&baz)", WorkspaceName::DEFAULT)?, @r#"
Filter(
File(
UnionAll(
[
Pattern(PrefixPath("foo")),
Intersection(
Pattern(PrefixPath("bar")),
Pattern(PrefixPath("baz")),
),
],
),
),
)
"#);
insta::assert_debug_snapshot!(
parse_with_workspace(r#"files(~(foo))"#, WorkspaceName::DEFAULT)?,
@r#"
Filter(
File(
Difference(
All,
Pattern(PrefixPath("foo")),
),
),
)
"#);
insta::assert_debug_snapshot!(parse("signed()")?, @"Filter(Signed)");
Ok(())
}
#[test]
fn test_parse_revset_change_commit_id_functions() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse("change_id(z)")?,
@r#"CommitRef(ChangeId(HexPrefix("0")))"#);
insta::assert_debug_snapshot!(
parse("change_id('zk')")?,
@r#"CommitRef(ChangeId(HexPrefix("0f")))"#);
insta::assert_debug_snapshot!(
parse("change_id(01234)").unwrap_err().kind(),
@r#"Expression("Invalid change ID prefix")"#);
insta::assert_debug_snapshot!(
parse("commit_id(0)")?,
@r#"CommitRef(CommitId(HexPrefix("0")))"#);
insta::assert_debug_snapshot!(
parse("commit_id('0f')")?,
@r#"CommitRef(CommitId(HexPrefix("0f")))"#);
insta::assert_debug_snapshot!(
parse("commit_id(xyzzy)").unwrap_err().kind(),
@r#"Expression("Invalid commit ID prefix")"#);
Ok(())
}
#[test]
fn test_parse_revset_author_committer_functions() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse("author(foo)")?, @r#"
Union(
Filter(AuthorName(Pattern(Exact("foo")))),
Filter(AuthorEmail(Pattern(Exact("foo")))),
)
"#);
insta::assert_debug_snapshot!(
parse("author_name(foo)")?,
@r#"Filter(AuthorName(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse("author_email(foo)")?,
@r#"Filter(AuthorEmail(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse("committer(foo)")?, @r#"
Union(
Filter(CommitterName(Pattern(Exact("foo")))),
Filter(CommitterEmail(Pattern(Exact("foo")))),
)
"#);
insta::assert_debug_snapshot!(
parse("committer_name(foo)")?,
@r#"Filter(CommitterName(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse("committer_email(foo)")?,
@r#"Filter(CommitterEmail(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(
parse("mine()")?,
@r#"Filter(AuthorEmail(Pattern(ExactI("test.user@example.com"))))"#);
Ok(())
}
#[test]
fn test_parse_revset_keyword_arguments() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse("remote_bookmarks(remote=foo)")?, @r#"
CommitRef(
RemoteBookmarks {
symbol: RemoteRefSymbolExpression {
name: Pattern(Substring("")),
remote: Pattern(Exact("foo")),
},
remote_ref_state: None,
},
)
"#);
insta::assert_debug_snapshot!(
parse("remote_bookmarks(foo, remote=bar)")?, @r#"
CommitRef(
RemoteBookmarks {
symbol: RemoteRefSymbolExpression {
name: Pattern(Exact("foo")),
remote: Pattern(Exact("bar")),
},
remote_ref_state: None,
},
)
"#);
insta::assert_debug_snapshot!(
parse("tracked_remote_bookmarks(foo, remote=bar)")?, @r#"
CommitRef(
RemoteBookmarks {
symbol: RemoteRefSymbolExpression {
name: Pattern(Exact("foo")),
remote: Pattern(Exact("bar")),
},
remote_ref_state: Some(Tracked),
},
)
"#);
insta::assert_debug_snapshot!(
parse("untracked_remote_bookmarks(foo, remote=bar)")?, @r#"
CommitRef(
RemoteBookmarks {
symbol: RemoteRefSymbolExpression {
name: Pattern(Exact("foo")),
remote: Pattern(Exact("bar")),
},
remote_ref_state: Some(New),
},
)
"#);
insta::assert_debug_snapshot!(
parse(r#"remote_bookmarks(remote=foo, bar)"#).unwrap_err().kind(),
@r#"
InvalidFunctionArguments {
name: "remote_bookmarks",
message: "Positional argument follows keyword argument",
}
"#);
insta::assert_debug_snapshot!(
parse(r#"remote_bookmarks("", foo, remote=bar)"#).unwrap_err().kind(),
@r#"
InvalidFunctionArguments {
name: "remote_bookmarks",
message: "Got multiple values for keyword \"remote\"",
}
"#);
insta::assert_debug_snapshot!(
parse(r#"remote_bookmarks(remote=bar, remote=bar)"#).unwrap_err().kind(),
@r#"
InvalidFunctionArguments {
name: "remote_bookmarks",
message: "Got multiple values for keyword \"remote\"",
}
"#);
insta::assert_debug_snapshot!(
parse(r#"remote_bookmarks(unknown=bar)"#).unwrap_err().kind(),
@r#"
InvalidFunctionArguments {
name: "remote_bookmarks",
message: "Unexpected keyword argument \"unknown\"",
}
"#);
Ok(())
}
#[test]
fn test_expand_symbol_alias() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse_with_aliases("AB|c", [("AB", "a|b")])?, @r#"
Union(
Union(
CommitRef(Symbol("a")),
CommitRef(Symbol("b")),
),
CommitRef(Symbol("c")),
)
"#);
insta::assert_debug_snapshot!(
parse_with_aliases_and_workspace("files(A)", [("A", "a")], WorkspaceName::DEFAULT)
?,
@r#"Filter(File(Pattern(PrefixPath("a"))))"#);
insta::assert_debug_snapshot!(
parse_with_aliases("author_name(A)", [("A", "a")])?,
@r#"Filter(AuthorName(Pattern(Exact("a"))))"#);
insta::assert_debug_snapshot!(
parse_with_aliases("author_name(A)", [("A", "exact:a")])?,
@r#"Filter(AuthorName(Pattern(Exact("a"))))"#);
Ok(())
}
#[test]
fn test_expand_function_alias() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
parse_with_aliases("F(a)", [("F(x)", "author_name(x)|committer_name(x)")])?,
@r#"
Union(
Filter(AuthorName(Pattern(Exact("a")))),
Filter(CommitterName(Pattern(Exact("a")))),
)
"#);
Ok(())
}
#[test]
fn test_transform_expression() {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
transform_expression(
&ResolvedRevsetExpression::root(),
|_| ControlFlow::Break(None),
|_| Some(RevsetExpression::none()),
), @"None");
insta::assert_debug_snapshot!(
transform_expression(
&ResolvedRevsetExpression::root(),
|_| ControlFlow::Break(Some(RevsetExpression::all())),
|_| Some(RevsetExpression::none()),
), @"Some(All)");
insta::assert_debug_snapshot!(
transform_expression(
&ResolvedRevsetExpression::root().heads(),
|_| ControlFlow::Continue(()),
|x| match x.as_ref() {
RevsetExpression::Root => Some(RevsetExpression::none()),
_ => None,
},
), @"Some(Heads(None))");
insta::assert_debug_snapshot!(
transform_expression(
&ResolvedRevsetExpression::root().heads(),
|_| ControlFlow::Continue(()),
|x| match x.as_ref() {
RevsetExpression::Heads(y) => Some(y.clone()),
_ => None,
},
), @"Some(Root)");
}
#[test]
fn test_resolve_referenced_commits() {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
let visibility1 = Arc::new(ResolvedRevsetExpression::WithinVisibility {
candidates: RevsetExpression::commit(CommitId::from_hex("100001")),
visible_heads: vec![CommitId::from_hex("100000")],
});
let visibility2 = Arc::new(ResolvedRevsetExpression::WithinVisibility {
candidates: RevsetExpression::filter(RevsetFilterPredicate::HasConflict),
visible_heads: vec![CommitId::from_hex("200000")],
});
let commit3 = RevsetExpression::commit(CommitId::from_hex("000003"));
insta::assert_debug_snapshot!(
resolve_referenced_commits(&visibility1.intersection(&RevsetExpression::all())), @r#"
Some(
WithinReference {
candidates: Intersection(
WithinVisibility {
candidates: WithinReference {
candidates: Commits(
[
CommitId("100001"),
],
),
commits: [
CommitId("100001"),
],
},
visible_heads: [
CommitId("100000"),
],
},
All,
),
commits: [
CommitId("100000"),
CommitId("100001"),
],
},
)
"#);
insta::assert_debug_snapshot!(
resolve_referenced_commits(
&visibility2
.intersection(&RevsetExpression::all())
.union(&commit3),
), @r#"
Some(
WithinReference {
candidates: Union(
Intersection(
WithinVisibility {
candidates: Filter(HasConflict),
visible_heads: [
CommitId("200000"),
],
},
All,
),
Commits(
[
CommitId("000003"),
],
),
),
commits: [
CommitId("000003"),
CommitId("200000"),
],
},
)
"#);
insta::assert_debug_snapshot!(
resolve_referenced_commits(
&visibility1
.union(&visibility2)
.union(&commit3)
.intersection(&RevsetExpression::all())
), @r#"
Some(
WithinReference {
candidates: Intersection(
Union(
Union(
WithinVisibility {
candidates: WithinReference {
candidates: Commits(
[
CommitId("100001"),
],
),
commits: [
CommitId("100001"),
],
},
visible_heads: [
CommitId("100000"),
],
},
WithinVisibility {
candidates: Filter(HasConflict),
visible_heads: [
CommitId("200000"),
],
},
),
Commits(
[
CommitId("000003"),
],
),
),
All,
),
commits: [
CommitId("000003"),
CommitId("100000"),
CommitId("100001"),
CommitId("200000"),
],
},
)
"#);
insta::assert_debug_snapshot!(
resolve_referenced_commits(&Arc::new(ResolvedRevsetExpression::WithinVisibility {
candidates: visibility1.clone(),
visible_heads: vec![CommitId::from_hex("400000")],
})), @r#"
Some(
WithinReference {
candidates: WithinVisibility {
candidates: WithinReference {
candidates: WithinVisibility {
candidates: WithinReference {
candidates: Commits(
[
CommitId("100001"),
],
),
commits: [
CommitId("100001"),
],
},
visible_heads: [
CommitId("100000"),
],
},
commits: [
CommitId("100000"),
CommitId("100001"),
],
},
visible_heads: [
CommitId("400000"),
],
},
commits: [
CommitId("400000"),
CommitId("100000"),
CommitId("100001"),
],
},
)
"#);
let resolved = Arc::new(ResolvedRevsetExpression::WithinReference {
candidates: RevsetExpression::none(),
commits: vec![CommitId::from_hex("100000")],
});
insta::assert_debug_snapshot!(
resolve_referenced_commits(&resolved), @"None");
insta::assert_debug_snapshot!(
resolve_referenced_commits(&resolved.intersection(&RevsetExpression::all())), @r#"
Some(
WithinReference {
candidates: Intersection(
WithinReference {
candidates: None,
commits: [
CommitId("100000"),
],
},
All,
),
commits: [
CommitId("100000"),
],
},
)
"#);
insta::assert_debug_snapshot!(
resolve_referenced_commits(&Arc::new(ResolvedRevsetExpression::WithinVisibility {
candidates: resolved.clone(),
visible_heads: vec![CommitId::from_hex("400000")],
})), @r#"
Some(
WithinReference {
candidates: WithinVisibility {
candidates: WithinReference {
candidates: None,
commits: [
CommitId("100000"),
],
},
visible_heads: [
CommitId("400000"),
],
},
commits: [
CommitId("400000"),
CommitId("100000"),
],
},
)
"#);
}
#[test]
fn test_optimize_subtree() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
optimize(parse("parents(bookmarks() & all())")?), @r#"
Ancestors {
heads: CommitRef(Bookmarks(Pattern(Substring("")))),
generation: 1..2,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("children(bookmarks() & all())")?), @r#"
Descendants {
roots: CommitRef(Bookmarks(Pattern(Substring("")))),
generation: 1..2,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("ancestors(bookmarks() & all())")?), @r#"
Ancestors {
heads: CommitRef(Bookmarks(Pattern(Substring("")))),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("descendants(bookmarks() & all())")?), @r#"
Descendants {
roots: CommitRef(Bookmarks(Pattern(Substring("")))),
generation: 0..18446744073709551615,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("(bookmarks() & all())..(all() & tags())")?), @r#"
Range {
roots: CommitRef(Bookmarks(Pattern(Substring("")))),
heads: CommitRef(Tags(Pattern(Substring("")))),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("(bookmarks() & all())::(all() & tags())")?), @r#"
DagRange {
roots: CommitRef(Bookmarks(Pattern(Substring("")))),
heads: CommitRef(Tags(Pattern(Substring("")))),
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("heads(bookmarks() & all())")?),
@r#"
Heads(
CommitRef(Bookmarks(Pattern(Substring("")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("roots(bookmarks() & all())")?),
@r#"
Roots(
CommitRef(Bookmarks(Pattern(Substring("")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("latest(bookmarks() & all(), 2)")?), @r#"
Latest {
candidates: CommitRef(Bookmarks(Pattern(Substring("")))),
count: 2,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("present(foo ~ bar)")?), @r#"
Present(
Difference(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("present(bookmarks() & all())")?),
@r#"
Present(
CommitRef(Bookmarks(Pattern(Substring("")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("at_operation(@-, bookmarks() & all())")?), @r#"
AtOperation {
operation: "@-",
candidates: CommitRef(Bookmarks(Pattern(Substring("")))),
}
"#);
insta::assert_debug_snapshot!(
optimize(Arc::new(RevsetExpression::WithinReference {
candidates: parse("bookmarks() & all()")?,
commits: vec![CommitId::from_hex("012345")],
})), @r#"
WithinReference {
candidates: CommitRef(Bookmarks(Pattern(Substring("")))),
commits: [
CommitId("012345"),
],
}
"#);
insta::assert_debug_snapshot!(
optimize(Arc::new(RevsetExpression::WithinVisibility {
candidates: parse("bookmarks() & all()")?,
visible_heads: vec![CommitId::from_hex("012345")],
})), @r#"
WithinReference {
candidates: WithinVisibility {
candidates: CommitRef(Bookmarks(Pattern(Substring("")))),
visible_heads: [
CommitId("012345"),
],
},
commits: [
CommitId("012345"),
],
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("~bookmarks() & all()")?),
@r#"
NotIn(
CommitRef(Bookmarks(Pattern(Substring("")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("(bookmarks() & all()) | (all() & tags())")?), @r#"
Union(
CommitRef(Bookmarks(Pattern(Substring("")))),
CommitRef(Tags(Pattern(Substring("")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("(bookmarks() & all()) & (all() & tags())")?), @r#"
Intersection(
CommitRef(Bookmarks(Pattern(Substring("")))),
CommitRef(Tags(Pattern(Substring("")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("(bookmarks() & all()) ~ (all() & tags())")?), @r#"
Difference(
CommitRef(Bookmarks(Pattern(Substring("")))),
CommitRef(Tags(Pattern(Substring("")))),
)
"#);
Ok(())
}
#[test]
fn test_optimize_unchanged_subtree() -> TestResult {
fn unwrap_union(
expression: &UserRevsetExpression,
) -> (&Arc<UserRevsetExpression>, &Arc<UserRevsetExpression>) {
match expression {
RevsetExpression::Union(left, right) => (left, right),
_ => panic!("unexpected expression: {expression:?}"),
}
}
let parsed = parse("foo-")?;
let optimized = optimize(parsed.clone());
assert!(Arc::ptr_eq(&parsed, &optimized));
let parsed = parse("bookmarks() | tags()")?;
let optimized = optimize(parsed.clone());
assert!(Arc::ptr_eq(&parsed, &optimized));
let parsed = parse("bookmarks() & tags()")?;
let optimized = optimize(parsed.clone());
assert!(Arc::ptr_eq(&parsed, &optimized));
let parsed = parse("(bookmarks() & all()) | tags()")?;
let optimized = optimize(parsed.clone());
assert_matches!(
unwrap_union(&optimized).0.as_ref(),
RevsetExpression::CommitRef(RevsetCommitRef::Bookmarks(_))
);
assert!(Arc::ptr_eq(
unwrap_union(&parsed).1,
unwrap_union(&optimized).1
));
let parsed = parse("bookmarks() | (all() & tags())")?;
let optimized = optimize(parsed.clone());
assert!(Arc::ptr_eq(
unwrap_union(&parsed).0,
unwrap_union(&optimized).0
));
assert_matches!(
unwrap_union(&optimized).1.as_ref(),
RevsetExpression::CommitRef(RevsetCommitRef::Tags(_))
);
Ok(())
}
#[test]
fn test_optimize_basic() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("all() | none()")?), @"All");
insta::assert_debug_snapshot!(optimize(parse("all() & none()")?), @"None");
insta::assert_debug_snapshot!(optimize(parse("root() | all()")?), @"All");
insta::assert_debug_snapshot!(optimize(parse("root() & all()")?), @"Root");
insta::assert_debug_snapshot!(optimize(parse("none() | root()")?), @"Root");
insta::assert_debug_snapshot!(optimize(parse("none() & root()")?), @"None");
insta::assert_debug_snapshot!(optimize(parse("~none()")?), @"All");
insta::assert_debug_snapshot!(optimize(parse("~~none()")?), @"None");
insta::assert_debug_snapshot!(optimize(parse("~all()")?), @"None");
insta::assert_debug_snapshot!(optimize(parse("~~all()")?), @"All");
insta::assert_debug_snapshot!(optimize(parse("~~foo")?), @r#"CommitRef(Symbol("foo"))"#);
insta::assert_debug_snapshot!(
optimize(parse("(root() | none()) & (visible_heads() | ~~all())")?), @"Root");
insta::assert_debug_snapshot!(
optimize(UserRevsetExpression::commits(vec![])), @"None");
insta::assert_debug_snapshot!(
optimize(UserRevsetExpression::commits(vec![]).negated()), @"All");
Ok(())
}
#[test]
fn test_optimize_difference() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("foo & ~bar")?), @r#"
Difference(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("~foo & bar")?), @r#"
Difference(
CommitRef(Symbol("bar")),
CommitRef(Symbol("foo")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("~foo & bar & ~baz")?), @r#"
Difference(
Difference(
CommitRef(Symbol("bar")),
CommitRef(Symbol("foo")),
),
CommitRef(Symbol("baz")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("(all() & ~foo) & bar")?), @r#"
Difference(
CommitRef(Symbol("bar")),
CommitRef(Symbol("foo")),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("all() ~ foo")?),
@r#"NotIn(CommitRef(Symbol("foo")))"#);
insta::assert_debug_snapshot!(optimize(parse("foo ~ bar")?), @r#"
Difference(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("(all() ~ foo) & bar")?), @r#"
Difference(
CommitRef(Symbol("bar")),
CommitRef(Symbol("foo")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("::foo & ~::bar")?), @r#"
Range {
roots: CommitRef(Symbol("bar")),
heads: CommitRef(Symbol("foo")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("~::foo & ::bar")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo..")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: VisibleHeadsOrReferenced,
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo..bar")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo.. & ::bar")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo.. & first_ancestors(bar)")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..1,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo & ~~bar")?), @r#"
Intersection(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("foo & ~~~bar")?), @r#"
Difference(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("~(all() & ~foo) & bar")?), @r#"
Intersection(
CommitRef(Symbol("foo")),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("~foo & ~bar")?), @r#"
Difference(
NotIn(CommitRef(Symbol("foo"))),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("a..b & c..d")?), @r#"
Intersection(
Range {
roots: Union(
CommitRef(Symbol("a")),
CommitRef(Symbol("c")),
),
heads: CommitRef(Symbol("b")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
Ancestors {
heads: CommitRef(Symbol("d")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
)
"#);
insta::assert_debug_snapshot!(optimize(parse("foo..bar ~ first_ancestors(baz)")?), @r#"
Difference(
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
Ancestors {
heads: CommitRef(Symbol("baz")),
generation: 0..18446744073709551615,
parents_range: 0..1,
},
)
"#);
insta::assert_debug_snapshot!(optimize(parse("foo ~ ::a & (::b & bar & ::c) & (baz ~ ::d)")?), @r#"
Intersection(
Intersection(
Intersection(
Intersection(
Range {
roots: Union(
CommitRef(Symbol("a")),
CommitRef(Symbol("d")),
),
heads: CommitRef(Symbol("b")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
Ancestors {
heads: CommitRef(Symbol("c")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
),
CommitRef(Symbol("foo")),
),
CommitRef(Symbol("bar")),
),
CommitRef(Symbol("baz")),
)
"#);
Ok(())
}
#[test]
fn test_optimize_not_in_ancestors() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("~(::foo)")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: VisibleHeadsOrReferenced,
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("~(::foo-)")?), @r#"
Range {
roots: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 1..2,
parents_range: 0..4294967295,
},
heads: VisibleHeadsOrReferenced,
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("~(::foo--)")?), @r#"
Range {
roots: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 2..3,
parents_range: 0..4294967295,
},
heads: VisibleHeadsOrReferenced,
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("~ancestors(foo, 1)")?), @r#"
NotIn(
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..1,
parents_range: 0..4294967295,
},
)
"#);
insta::assert_debug_snapshot!(optimize(parse("~ancestors(foo-, 1)")?), @r#"
NotIn(
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 1..2,
parents_range: 0..4294967295,
},
)
"#);
Ok(())
}
#[test]
fn test_optimize_filter_difference() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("~empty()")?), @"Filter(File(All))");
insta::assert_debug_snapshot!(
optimize(parse("(author_name(foo) & ~bar) & baz")?), @r#"
Intersection(
Difference(
CommitRef(Symbol("baz")),
CommitRef(Symbol("bar")),
),
Filter(AuthorName(Pattern(Exact("foo")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("~foo & author_name(bar)")?), @r#"
Intersection(
NotIn(CommitRef(Symbol("foo"))),
Filter(AuthorName(Pattern(Exact("bar")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("~foo & (author_name(bar) | baz)")?), @r#"
Intersection(
NotIn(CommitRef(Symbol("foo"))),
AsFilter(
Union(
Filter(AuthorName(Pattern(Exact("bar")))),
CommitRef(Symbol("baz")),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("author_name(foo) ~ bar")?), @r#"
Intersection(
NotIn(CommitRef(Symbol("bar"))),
Filter(AuthorName(Pattern(Exact("foo")))),
)
"#);
Ok(())
}
#[test]
fn test_optimize_filter_intersection() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
optimize(parse("author_name(foo)")?),
@r#"Filter(AuthorName(Pattern(Exact("foo"))))"#);
insta::assert_debug_snapshot!(optimize(parse("foo & description(bar)")?), @r#"
Intersection(
CommitRef(Symbol("foo")),
Filter(Description(Pattern(Exact("bar")))),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("author_name(foo) & bar")?), @r#"
Intersection(
CommitRef(Symbol("bar")),
Filter(AuthorName(Pattern(Exact("foo")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("author_name(foo) & committer_name(bar)")?), @r#"
AsFilter(
Intersection(
Filter(AuthorName(Pattern(Exact("foo")))),
Filter(CommitterName(Pattern(Exact("bar")))),
),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("divergent() & foo")?), @r#"
Intersection(
CommitRef(Symbol("foo")),
AsFilter(Divergent),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("foo & description(bar) & author_name(baz)")?), @r#"
Intersection(
CommitRef(Symbol("foo")),
AsFilter(
Intersection(
Filter(Description(Pattern(Exact("bar")))),
Filter(AuthorName(Pattern(Exact("baz")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("committer_name(foo) & bar & author_name(baz)")?), @r#"
Intersection(
CommitRef(Symbol("bar")),
AsFilter(
Intersection(
Filter(CommitterName(Pattern(Exact("foo")))),
Filter(AuthorName(Pattern(Exact("baz")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse_with_workspace(
"committer_name(foo) & files(bar) & baz",
WorkspaceName::DEFAULT)?,
), @r#"
Intersection(
CommitRef(Symbol("baz")),
AsFilter(
Intersection(
Filter(CommitterName(Pattern(Exact("foo")))),
Filter(File(Pattern(PrefixPath("bar")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse_with_workspace(
"committer_name(foo) & files(bar) & author_name(baz)",
WorkspaceName::DEFAULT)?,
), @r#"
AsFilter(
Intersection(
Intersection(
Filter(CommitterName(Pattern(Exact("foo")))),
Filter(File(Pattern(PrefixPath("bar")))),
),
Filter(AuthorName(Pattern(Exact("baz")))),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse_with_workspace(
"foo & files(bar) & baz",
WorkspaceName::DEFAULT)?,
), @r#"
Intersection(
Intersection(
CommitRef(Symbol("foo")),
CommitRef(Symbol("baz")),
),
Filter(File(Pattern(PrefixPath("bar")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("foo & description(bar) & author_name(baz) & qux")?), @r#"
Intersection(
Intersection(
CommitRef(Symbol("foo")),
CommitRef(Symbol("qux")),
),
AsFilter(
Intersection(
Filter(Description(Pattern(Exact("bar")))),
Filter(AuthorName(Pattern(Exact("baz")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("foo & description(bar) & parents(author_name(baz)) & qux")?),
@r#"
Intersection(
Intersection(
Intersection(
CommitRef(Symbol("foo")),
Ancestors {
heads: Filter(AuthorName(Pattern(Exact("baz")))),
generation: 1..2,
parents_range: 0..4294967295,
},
),
CommitRef(Symbol("qux")),
),
Filter(Description(Pattern(Exact("bar")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("foo & description(bar) & parents(author_name(baz) & qux)")?),
@r#"
Intersection(
Intersection(
CommitRef(Symbol("foo")),
Ancestors {
heads: Intersection(
CommitRef(Symbol("qux")),
Filter(AuthorName(Pattern(Exact("baz")))),
),
generation: 1..2,
parents_range: 0..4294967295,
},
),
Filter(Description(Pattern(Exact("bar")))),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("(a & author_name(A)) & (b & author_name(B)) & (c & author_name(C))")?),
@r#"
Intersection(
Intersection(
Intersection(
CommitRef(Symbol("a")),
CommitRef(Symbol("b")),
),
CommitRef(Symbol("c")),
),
AsFilter(
Intersection(
Intersection(
Filter(AuthorName(Pattern(Exact("A")))),
Filter(AuthorName(Pattern(Exact("B")))),
),
Filter(AuthorName(Pattern(Exact("C")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("(a & author_name(A)) & ((b & author_name(B)) & (c & author_name(C))) & d")?),
@r#"
Intersection(
Intersection(
Intersection(
Intersection(
CommitRef(Symbol("a")),
CommitRef(Symbol("b")),
),
CommitRef(Symbol("c")),
),
CommitRef(Symbol("d")),
),
AsFilter(
Intersection(
Intersection(
Filter(AuthorName(Pattern(Exact("A")))),
Filter(AuthorName(Pattern(Exact("B")))),
),
Filter(AuthorName(Pattern(Exact("C")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("foo & (all() & description(bar)) & (author_name(baz) & all())")?),
@r#"
Intersection(
CommitRef(Symbol("foo")),
AsFilter(
Intersection(
Filter(Description(Pattern(Exact("bar")))),
Filter(AuthorName(Pattern(Exact("baz")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("author_name(foo) & bar & at_operation(@-, committer_name(baz))")?),
@r#"
Intersection(
Intersection(
CommitRef(Symbol("bar")),
AtOperation {
operation: "@-",
candidates: Filter(CommitterName(Pattern(Exact("baz")))),
},
),
Filter(AuthorName(Pattern(Exact("foo")))),
)
"#);
Ok(())
}
#[test]
fn test_optimize_filter_subtree() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
optimize(parse("(author_name(foo) | bar) & baz")?), @r#"
Intersection(
CommitRef(Symbol("baz")),
AsFilter(
Union(
Filter(AuthorName(Pattern(Exact("foo")))),
CommitRef(Symbol("bar")),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("merges() & foo | bar")?), @r#"
Union(
Intersection(
CommitRef(Symbol("foo")),
Filter(ParentCount(2..4294967295)),
),
CommitRef(Symbol("bar")),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("merges() & foo | conflicts()")?), @r#"
AsFilter(
Union(
Intersection(
CommitRef(Symbol("foo")),
Filter(ParentCount(2..4294967295)),
),
Filter(HasConflict),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("foo | conflicts() & merges() & signed()")?), @r#"
AsFilter(
Union(
CommitRef(Symbol("foo")),
Intersection(
Intersection(
Filter(HasConflict),
Filter(ParentCount(2..4294967295)),
),
Filter(Signed),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse("(foo | committer_name(bar)) & description(baz) & qux")?), @r#"
Intersection(
CommitRef(Symbol("qux")),
AsFilter(
Intersection(
Union(
CommitRef(Symbol("foo")),
Filter(CommitterName(Pattern(Exact("bar")))),
),
Filter(Description(Pattern(Exact("baz")))),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse(
"(~present(author_name(foo) & description(bar)) | baz) & qux")?), @r#"
Intersection(
CommitRef(Symbol("qux")),
AsFilter(
Union(
NotIn(
Present(
Intersection(
Filter(AuthorName(Pattern(Exact("foo")))),
Filter(Description(Pattern(Exact("bar")))),
),
),
),
CommitRef(Symbol("baz")),
),
),
)
"#);
insta::assert_debug_snapshot!(
optimize(parse(
"(a & (author_name(A) | 0)) & (b & (author_name(B) | 1)) & (c & (author_name(C) | 2))")?),
@r#"
Intersection(
Intersection(
Intersection(
CommitRef(Symbol("a")),
CommitRef(Symbol("b")),
),
CommitRef(Symbol("c")),
),
AsFilter(
Intersection(
Intersection(
Union(
Filter(AuthorName(Pattern(Exact("A")))),
CommitRef(Symbol("0")),
),
Union(
Filter(AuthorName(Pattern(Exact("B")))),
CommitRef(Symbol("1")),
),
),
Union(
Filter(AuthorName(Pattern(Exact("C")))),
CommitRef(Symbol("2")),
),
),
),
)
"#);
insta::assert_debug_snapshot!(optimize(parse("::foo | ::author_name(bar)")?), @r#"
Ancestors {
heads: HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: AsFilter(
Union(
CommitRef(Symbol("foo")),
Filter(AuthorName(Pattern(Exact("bar")))),
),
),
},
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
Ok(())
}
#[test]
fn test_optimize_ancestors() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("foo--")?), @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 2..3,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("::(foo---)")?), @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 3..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("(::foo)---")?), @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 3..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo---+")?), @r#"
Descendants {
roots: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 3..4,
parents_range: 0..4294967295,
},
generation: 1..2,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo..(bar--)")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 2..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("(foo--)..(bar---)")?), @r#"
Range {
roots: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 2..3,
parents_range: 0..4294967295,
},
heads: CommitRef(Symbol("bar")),
generation: 3..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("~ancestors(foo, 2) & ::bar")?), @r#"
Difference(
Ancestors {
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..2,
parents_range: 0..4294967295,
},
)
"#);
insta::assert_debug_snapshot!(optimize(parse("(foo..bar)--")?), @r#"
Ancestors {
heads: Range {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
generation: 2..3,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo..(bar..baz)")?), @r#"
Range {
roots: CommitRef(Symbol("foo")),
heads: HeadsRange {
roots: CommitRef(Symbol("bar")),
heads: CommitRef(Symbol("baz")),
parents_range: 0..4294967295,
filter: All,
},
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(
optimize(parse("ancestors(ancestors(foo), 0)")?), @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..0,
parents_range: 0..4294967295,
}
"#
);
insta::assert_debug_snapshot!(
optimize(parse("ancestors(ancestors(foo, 0))")?), @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..0,
parents_range: 0..4294967295,
}
"#
);
insta::assert_debug_snapshot!(
optimize(parse("first_ancestors(first_ancestors(foo, 5), 5)")?), @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..9,
parents_range: 0..1,
}
"#
);
insta::assert_debug_snapshot!(
optimize(parse("first_ancestors(first_parent(foo), 5)")?), @r#"
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 1..6,
parents_range: 0..1,
}
"#
);
insta::assert_debug_snapshot!(
optimize(parse("first_ancestors(ancestors(foo, 5), 5)")?), @r#"
Ancestors {
heads: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..5,
parents_range: 0..4294967295,
},
generation: 0..5,
parents_range: 0..1,
}
"#
);
insta::assert_debug_snapshot!(
optimize(parse("ancestors(first_ancestors(foo, 5), 5)")?), @r#"
Ancestors {
heads: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..5,
parents_range: 0..1,
},
generation: 0..5,
parents_range: 0..4294967295,
}
"#
);
Ok(())
}
#[test]
fn test_optimize_descendants() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("foo++")?), @r#"
Descendants {
roots: CommitRef(Symbol("foo")),
generation: 2..3,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("(foo+++)::")?), @r#"
Descendants {
roots: CommitRef(Symbol("foo")),
generation: 3..18446744073709551615,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("(foo::)+++")?), @r#"
Descendants {
roots: CommitRef(Symbol("foo")),
generation: 3..18446744073709551615,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("foo+++-")?), @r#"
Ancestors {
heads: Descendants {
roots: CommitRef(Symbol("foo")),
generation: 3..4,
},
generation: 1..2,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("(foo++)::bar")?), @r#"
DagRange {
roots: Descendants {
roots: CommitRef(Symbol("foo")),
generation: 2..3,
},
heads: CommitRef(Symbol("bar")),
}
"#);
Ok(())
}
#[test]
fn test_optimize_flatten_intersection() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("a & ((b & c) & (d & e))")?), @r#"
Intersection(
Intersection(
Intersection(
Intersection(
CommitRef(Symbol("a")),
CommitRef(Symbol("b")),
),
CommitRef(Symbol("c")),
),
CommitRef(Symbol("d")),
),
CommitRef(Symbol("e")),
)
"#);
Ok(())
}
#[test]
fn test_optimize_ancestors_union() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("::a | ::b | ::c | ::d")?), @r#"
Ancestors {
heads: Union(
Union(
CommitRef(Symbol("a")),
CommitRef(Symbol("b")),
),
Union(
CommitRef(Symbol("c")),
CommitRef(Symbol("d")),
),
),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("ancestors(a-) | ancestors(b)")?), @r#"
Ancestors {
heads: Union(
Ancestors {
heads: CommitRef(Symbol("a")),
generation: 1..2,
parents_range: 0..4294967295,
},
CommitRef(Symbol("b")),
),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("~::a- & ~::b & ~::c & ::d")?), @r#"
Range {
roots: Union(
Union(
Ancestors {
heads: CommitRef(Symbol("a")),
generation: 1..2,
parents_range: 0..4294967295,
},
CommitRef(Symbol("b")),
),
CommitRef(Symbol("c")),
),
heads: CommitRef(Symbol("d")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("a..b ~ ::c- ~ ::d")?), @r#"
Range {
roots: Union(
Union(
CommitRef(Symbol("a")),
Ancestors {
heads: CommitRef(Symbol("c")),
generation: 1..2,
parents_range: 0..4294967295,
},
),
CommitRef(Symbol("d")),
),
heads: CommitRef(Symbol("b")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("ancestors(a, 2) | ancestors(b)")?), @r#"
Union(
Ancestors {
heads: CommitRef(Symbol("a")),
generation: 0..2,
parents_range: 0..4294967295,
},
Ancestors {
heads: CommitRef(Symbol("b")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
)
"#);
Ok(())
}
#[test]
fn test_optimize_sort_negations_and_ancestors() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("~a & ::b & ~::c & d ~ e & f & ::g & ~::h")?), @r#"
Difference(
Difference(
Intersection(
Intersection(
Intersection(
Range {
roots: Union(
CommitRef(Symbol("c")),
CommitRef(Symbol("h")),
),
heads: CommitRef(Symbol("b")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
Ancestors {
heads: CommitRef(Symbol("g")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
),
CommitRef(Symbol("d")),
),
CommitRef(Symbol("f")),
),
CommitRef(Symbol("a")),
),
CommitRef(Symbol("e")),
)
"#);
Ok(())
}
#[test]
fn test_optimize_heads_range() -> TestResult {
let settings = insta_settings();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(optimize(parse("heads(::)")?), @"
HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: All,
}
");
insta::assert_debug_snapshot!(optimize(parse("heads(::foo)")?), @r#"
HeadsRange {
roots: None,
heads: CommitRef(Symbol("foo")),
parents_range: 0..4294967295,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(..)")?), @"
HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: NotIn(Root),
}
");
insta::assert_debug_snapshot!(optimize(parse("heads(foo..)")?), @r#"
HeadsRange {
roots: CommitRef(Symbol("foo")),
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(..bar)")?), @r#"
HeadsRange {
roots: Root,
heads: CommitRef(Symbol("bar")),
parents_range: 0..4294967295,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(foo..bar)")?), @r#"
HeadsRange {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
parents_range: 0..4294967295,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(~::foo & ::bar)")?), @r#"
HeadsRange {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
parents_range: 0..4294967295,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(~::foo)")?), @r#"
HeadsRange {
roots: CommitRef(Symbol("foo")),
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(a..b & c..d)")?), @r#"
HeadsRange {
roots: Union(
CommitRef(Symbol("a")),
CommitRef(Symbol("c")),
),
heads: CommitRef(Symbol("b")),
parents_range: 0..4294967295,
filter: Ancestors {
heads: CommitRef(Symbol("d")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(first_ancestors(foo))")?), @r#"
HeadsRange {
roots: None,
heads: CommitRef(Symbol("foo")),
parents_range: 0..1,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(first_ancestors(foo) & bar..)")?), @r#"
HeadsRange {
roots: CommitRef(Symbol("bar")),
heads: CommitRef(Symbol("foo")),
parents_range: 0..1,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(foo.. & first_ancestors(bar) & ::baz)")?), @r#"
HeadsRange {
roots: CommitRef(Symbol("foo")),
heads: CommitRef(Symbol("bar")),
parents_range: 0..1,
filter: Ancestors {
heads: CommitRef(Symbol("baz")),
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
},
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(ancestors(foo, 2))")?), @r#"
Heads(
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..2,
parents_range: 0..4294967295,
},
)
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(first_ancestors(foo, 2))")?), @r#"
Heads(
Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 0..2,
parents_range: 0..1,
},
)
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(ancestors(foo--))")?), @r#"
HeadsRange {
roots: None,
heads: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 2..3,
parents_range: 0..4294967295,
},
parents_range: 0..4294967295,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(first_ancestors(first_parent(foo, 2)))")?), @r#"
HeadsRange {
roots: None,
heads: Ancestors {
heads: CommitRef(Symbol("foo")),
generation: 2..3,
parents_range: 0..1,
},
parents_range: 0..1,
filter: All,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(author_name(A) | author_name(B))")?), @r#"
HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: AsFilter(
Union(
Filter(AuthorName(Pattern(Exact("A")))),
Filter(AuthorName(Pattern(Exact("B")))),
),
),
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(~author_name(A))")?), @r#"
HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: AsFilter(
NotIn(
Filter(AuthorName(Pattern(Exact("A")))),
),
),
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(~foo)")?), @r#"
HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: NotIn(CommitRef(Symbol("foo"))),
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(author_name(A) & ::foo ~ author_name(B))")?), @r#"
HeadsRange {
roots: None,
heads: CommitRef(Symbol("foo")),
parents_range: 0..4294967295,
filter: AsFilter(
Difference(
Filter(AuthorName(Pattern(Exact("A")))),
Filter(AuthorName(Pattern(Exact("B")))),
),
),
}
"#);
insta::assert_debug_snapshot!(optimize(parse("heads(~foo & ~roots(bar) & ::baz)")?), @r#"
HeadsRange {
roots: None,
heads: CommitRef(Symbol("baz")),
parents_range: 0..4294967295,
filter: Difference(
NotIn(CommitRef(Symbol("foo"))),
Roots(CommitRef(Symbol("bar"))),
),
}
"#);
Ok(())
}
#[test]
fn test_optimize_ancestors_heads_range() -> TestResult {
insta::assert_debug_snapshot!(optimize(parse("::description(bar)")?), @r#"
Ancestors {
heads: HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: Filter(
Description(
Pattern(
Exact(
"bar",
),
),
),
),
},
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("author_name(foo)..")?), @r#"
Range {
roots: HeadsRange {
roots: None,
heads: VisibleHeadsOrReferenced,
parents_range: 0..4294967295,
filter: Filter(
AuthorName(
Pattern(
Exact(
"foo",
),
),
),
),
},
heads: VisibleHeadsOrReferenced,
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("::(foo..bar)")?), @r#"
Ancestors {
heads: HeadsRange {
roots: CommitRef(
Symbol(
"foo",
),
),
heads: CommitRef(
Symbol(
"bar",
),
),
parents_range: 0..4294967295,
filter: All,
},
generation: 0..18446744073709551615,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("ancestors(author_name(foo), 5)")?), @r#"
Ancestors {
heads: Filter(
AuthorName(
Pattern(
Exact(
"foo",
),
),
),
),
generation: 0..5,
parents_range: 0..4294967295,
}
"#);
insta::assert_debug_snapshot!(optimize(parse("first_ancestors(author_name(foo))")?), @r#"
Ancestors {
heads: Filter(
AuthorName(
Pattern(
Exact(
"foo",
),
),
),
),
generation: 0..18446744073709551615,
parents_range: 0..1,
}
"#);
Ok(())
}
#[test]
fn test_escape_string_literal() {
assert_eq!(format_symbol("foo"), "foo");
assert_eq!(format_symbol("foo.bar"), "foo.bar");
assert_eq!(format_symbol("foo@bar"), r#""foo@bar""#);
assert_eq!(format_symbol("foo bar"), r#""foo bar""#);
assert_eq!(format_symbol(" foo "), r#"" foo ""#);
assert_eq!(format_symbol("(foo)"), r#""(foo)""#);
assert_eq!(format_symbol("all:foo"), r#""all:foo""#);
assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#);
assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#);
assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#);
assert_eq!(format_symbol("foo\nbar"), r#""foo\nbar""#);
assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#);
assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#);
assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#);
assert_eq!(format_symbol("foo \x01 bar"), r#""foo \x01 bar""#);
}
#[test]
fn test_escape_remote_symbol() {
assert_eq!(format_remote_symbol("foo", "bar"), "foo@bar");
assert_eq!(
format_remote_symbol(" foo ", "bar:baz"),
r#"" foo "@"bar:baz""#
);
}
}