diff --git a/cli/examples/custom-commit-templater/main.rs b/cli/examples/custom-commit-templater/main.rs index 181438dff1..6fa70db6b2 100644 --- a/cli/examples/custom-commit-templater/main.rs +++ b/cli/examples/custom-commit-templater/main.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use itertools::Itertools; use jj_cli::cli_util::CliRunner; use jj_cli::commit_templater::{ CommitTemplateBuildFnTable, CommitTemplateLanguage, CommitTemplateLanguageExtension, @@ -24,7 +25,9 @@ use jj_lib::commit::Commit; use jj_lib::extensions_map::ExtensionsMap; use jj_lib::object_id::ObjectId; use jj_lib::repo::Repo; -use jj_lib::revset::RevsetExpression; +use jj_lib::revset::{ + PartialSymbolResolver, RevsetExpression, RevsetResolutionError, SymbolResolverExtension, +}; use once_cell::sync::OnceCell; struct HexCounter; @@ -73,6 +76,48 @@ impl MostDigitsInId { } } +struct TheDigitestResolver<'a> { + repo: &'a dyn Repo, + cache: MostDigitsInId, +} + +impl<'a> TheDigitestResolver<'a> { + fn new(repo: &'a dyn Repo) -> Self { + Self { + repo, + cache: MostDigitsInId::new(), + } + } +} + +impl<'a> PartialSymbolResolver<'a> for TheDigitestResolver<'a> { + fn resolve_symbol(&self, symbol: &str) -> Result>, RevsetResolutionError> { + if symbol != "thedigitest" { + return Ok(None); + } + + Ok(Some( + RevsetExpression::all() + .evaluate_programmatic(self.repo) + .map_err(|err| RevsetResolutionError::Other(err.into()))? + .iter() + .filter(|id| num_digits_in_id(id) == self.cache.count(self.repo)) + .collect_vec(), + )) + } +} + +struct TheDigitest; + +impl SymbolResolverExtension for TheDigitest { + fn get_symbol_resolver<'a>( + &self, + repo: &'a dyn Repo, + ) -> Box + 'a> { + Box::new(TheDigitestResolver::new(repo)) + } +} + impl CommitTemplateLanguageExtension for HexCounter { fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo> { type L<'repo> = CommitTemplateLanguage<'repo>; @@ -132,5 +177,6 @@ impl CommitTemplateLanguageExtension for HexCounter { fn main() -> std::process::ExitCode { CliRunner::init() .add_commit_template_extension(Box::new(HexCounter)) + .add_symbol_resolver(Box::new(TheDigitest)) .run() } diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 9e7001f2a5..0b0949c8b9 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -55,7 +55,7 @@ use jj_lib::repo::{ use jj_lib::repo_path::{FsPathParseError, RepoPath, RepoPathBuf}; use jj_lib::revset::{ RevsetAliasesMap, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt, RevsetModifier, - RevsetParseContext, RevsetWorkspaceContext, + RevsetParseContext, RevsetWorkspaceContext, SymbolResolverExtension, }; use jj_lib::rewrite::restore_tree; use jj_lib::settings::{ConfigResultExt as _, UserSettings}; @@ -91,7 +91,7 @@ use crate::git_util::{ }; use crate::merge_tools::{DiffEditor, MergeEditor, MergeToolConfigError}; use crate::operation_templater::OperationTemplateLanguageExtension; -use crate::revset_util::RevsetExpressionEvaluator; +use crate::revset_util::{RevsetExpressionEvaluator, RevsetExtensions}; use crate::template_builder::TemplateLanguage; use crate::template_parser::TemplateAliasesMap; use crate::templater::{PropertyPlaceholder, TemplateRenderer}; @@ -193,6 +193,7 @@ pub struct CommandHelper { global_args: GlobalArgs, settings: UserSettings, layered_configs: LayeredConfigs, + revset_extensions: Arc, commit_template_extensions: Vec>, operation_template_extensions: Vec>, maybe_workspace_loader: Result, @@ -236,6 +237,10 @@ impl CommandHelper { self.layered_configs.resolved_config_values(prefix) } + pub fn revset_extensions(&self) -> &Arc { + &self.revset_extensions + } + /// Loads template aliases from the configs. /// /// For most commands that depend on a loaded repo, you should use @@ -402,6 +407,7 @@ pub struct WorkspaceCommandHelper { settings: UserSettings, workspace: Workspace, user_repo: ReadonlyUserRepo, + revset_extensions: Arc, // TODO: Parsed template can be cached if it doesn't capture 'repo lifetime commit_summary_template_text: String, commit_template_extensions: Vec>, @@ -434,6 +440,7 @@ impl WorkspaceCommandHelper { settings, workspace, user_repo: ReadonlyUserRepo::new(repo), + revset_extensions: command.revset_extensions.clone(), commit_summary_template_text, commit_template_extensions: command.commit_template_extensions.clone(), revset_aliases_map, @@ -877,6 +884,7 @@ impl WorkspaceCommandHelper { ) -> Result, CommandError> { Ok(RevsetExpressionEvaluator::new( self.repo().as_ref(), + self.revset_extensions.clone(), self.id_prefix_context()?, expression, )) @@ -908,7 +916,10 @@ impl WorkspaceCommandHelper { revset::parse(&revset_string, &self.revset_parse_context()).map_err(|err| { config_error_with_message("Invalid `revsets.short-prefixes`", err) })?; - context = context.disambiguate_within(revset::optimize(disambiguation_revset)); + context = context.disambiguate_within( + revset::optimize(disambiguation_revset), + self.revset_extensions.symbol_resolvers().to_vec(), + ); } Ok(context) }) @@ -957,6 +968,7 @@ impl WorkspaceCommandHelper { self.workspace_id(), self.revset_parse_context(), self.id_prefix_context()?, + self.revset_extensions.clone(), &self.commit_template_extensions, )) } @@ -1437,6 +1449,7 @@ impl WorkspaceCommandTransaction<'_> { self.helper.workspace_id(), self.helper.revset_parse_context(), &id_prefix_context, + self.helper.revset_extensions.clone(), &self.helper.commit_template_extensions, ); let template = self @@ -2506,6 +2519,7 @@ pub struct CliRunner { extra_configs: Vec, store_factories: StoreFactories, working_copy_factories: WorkingCopyFactories, + revset_extensions: RevsetExtensions, commit_template_extensions: Vec>, operation_template_extensions: Vec>, dispatch_fn: CliDispatchFn, @@ -2529,6 +2543,7 @@ impl CliRunner { extra_configs: vec![], store_factories: StoreFactories::default(), working_copy_factories: default_working_copy_factories(), + revset_extensions: RevsetExtensions::default(), commit_template_extensions: vec![], operation_template_extensions: vec![], dispatch_fn: Box::new(crate::commands::run_command), @@ -2564,6 +2579,14 @@ impl CliRunner { self } + pub fn add_symbol_resolver( + mut self, + symbol_resolver: Box, + ) -> Self { + self.revset_extensions.add_symbol_resolver(symbol_resolver); + self + } + pub fn add_commit_template_extension( mut self, commit_template_extension: Box, @@ -2698,6 +2721,7 @@ impl CliRunner { global_args: args.global_args, settings, layered_configs, + revset_extensions: self.revset_extensions.into(), commit_template_extensions: self.commit_template_extensions, operation_template_extensions: self.operation_template_extensions, maybe_workspace_loader, diff --git a/cli/src/commands/bench.rs b/cli/src/commands/bench.rs index 6401a92133..2bd85157dc 100644 --- a/cli/src/commands/bench.rs +++ b/cli/src/commands/bench.rs @@ -210,7 +210,10 @@ fn bench_revset( let routine = |workspace_command: &WorkspaceCommandHelper, expression: Rc| { // Evaluate the expression without parsing/evaluating short-prefixes. let repo = workspace_command.repo().as_ref(); - let symbol_resolver = DefaultSymbolResolver::new(repo); + let symbol_resolver = DefaultSymbolResolver::new( + repo, + workspace_command.revset_extensions().symbol_resolvers(), + ); let resolved = expression .resolve_user_expression(repo, &symbol_resolver) .unwrap(); diff --git a/cli/src/commands/debug.rs b/cli/src/commands/debug.rs index 624e5e772d..5c06ddc978 100644 --- a/cli/src/commands/debug.rs +++ b/cli/src/commands/debug.rs @@ -181,8 +181,11 @@ fn cmd_debug_revset( writeln!(ui.stdout(), "{expression:#?}")?; writeln!(ui.stdout())?; - let symbol_resolver = - revset_util::default_symbol_resolver(repo, workspace_command.id_prefix_context()?); + let symbol_resolver = revset_util::default_symbol_resolver( + repo, + command.revset_extensions().symbol_resolvers(), + workspace_command.id_prefix_context()?, + ); let expression = expression.resolve_user_expression(repo, &symbol_resolver)?; writeln!(ui.stdout(), "-- Resolved:")?; writeln!(ui.stdout(), "{expression:#?}")?; diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index 284de7be8d..438d35ba05 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -17,6 +17,7 @@ use std::cmp::max; use std::collections::HashMap; use std::io; use std::rc::Rc; +use std::sync::Arc; use itertools::Itertools as _; use jj_lib::backend::{ChangeId, CommitId}; @@ -31,6 +32,7 @@ use jj_lib::revset::{Revset, RevsetParseContext}; use jj_lib::{git, rewrite}; use once_cell::unsync::OnceCell; +use crate::revset_util::RevsetExtensions; use crate::template_builder::{ self, merge_fn_map, BuildContext, CoreTemplateBuildFnTable, CoreTemplatePropertyKind, IntoTemplateProperty, TemplateBuildMethodFnMap, TemplateLanguage, @@ -60,6 +62,7 @@ pub struct CommitTemplateLanguage<'repo> { id_prefix_context: &'repo IdPrefixContext, build_fn_table: CommitTemplateBuildFnTable<'repo>, keyword_cache: CommitKeywordCache, + revset_extensions: Arc, cache_extensions: ExtensionsMap, } @@ -71,12 +74,13 @@ impl<'repo> CommitTemplateLanguage<'repo> { workspace_id: &WorkspaceId, revset_parse_context: RevsetParseContext<'repo>, id_prefix_context: &'repo IdPrefixContext, - extensions: &[impl AsRef], + revset_extensions: Arc, + template_extensions: &[impl AsRef], ) -> Self { let mut build_fn_table = CommitTemplateBuildFnTable::builtin(); let mut cache_extensions = ExtensionsMap::empty(); - for extension in extensions { + for extension in template_extensions { build_fn_table.merge(extension.as_ref().build_fn_table()); extension .as_ref() @@ -91,6 +95,7 @@ impl<'repo> CommitTemplateLanguage<'repo> { build_fn_table, keyword_cache: CommitKeywordCache::default(), cache_extensions, + revset_extensions, } } } @@ -645,7 +650,11 @@ fn evaluate_immutable_revset<'repo>( .map_err(|err| { TemplateParseError::expression("Failed to parse revset", span).with_source(err) })?; - let symbol_resolver = revset_util::default_symbol_resolver(repo, language.id_prefix_context); + let symbol_resolver = revset_util::default_symbol_resolver( + repo, + language.revset_extensions.symbol_resolvers(), + language.id_prefix_context, + ); let revset = revset_util::evaluate(repo, &symbol_resolver, expression).map_err(|err| { TemplateParseError::expression("Failed to evaluate revset", span).with_source(err) })?; diff --git a/cli/src/revset_util.rs b/cli/src/revset_util.rs index bf1f13e518..cff1893378 100644 --- a/cli/src/revset_util.rs +++ b/cli/src/revset_util.rs @@ -15,6 +15,7 @@ //! Utility for parsing and evaluating user-provided revset expressions. use std::rc::Rc; +use std::sync::Arc; use itertools::Itertools as _; use jj_lib::backend::{BackendResult, CommitId}; @@ -24,7 +25,7 @@ use jj_lib::repo::Repo; use jj_lib::revset::{ self, DefaultSymbolResolver, Revset, RevsetAliasesMap, RevsetCommitRef, RevsetEvaluationError, RevsetExpression, RevsetIteratorExt as _, RevsetParseContext, RevsetParseError, - RevsetResolutionError, + RevsetResolutionError, SymbolResolverExtension, }; use jj_lib::settings::ConfigResultExt as _; use thiserror::Error; @@ -45,9 +46,26 @@ pub enum UserRevsetEvaluationError { Evaluation(RevsetEvaluationError), } +#[derive(Default)] +pub struct RevsetExtensions { + symbol_resolvers: Vec>, + // TODO: Add more fields for extending the revset language +} + +impl RevsetExtensions { + pub fn symbol_resolvers(&self) -> &[Arc] { + &self.symbol_resolvers + } + + pub fn add_symbol_resolver(&mut self, symbol_resolver: Box) { + self.symbol_resolvers.push(symbol_resolver.into()) + } +} + /// Wrapper around `RevsetExpression` to provide convenient methods. pub struct RevsetExpressionEvaluator<'repo> { repo: &'repo dyn Repo, + extensions: Arc, id_prefix_context: &'repo IdPrefixContext, expression: Rc, } @@ -55,11 +73,13 @@ pub struct RevsetExpressionEvaluator<'repo> { impl<'repo> RevsetExpressionEvaluator<'repo> { pub fn new( repo: &'repo dyn Repo, + extensions: Arc, id_prefix_context: &'repo IdPrefixContext, expression: Rc, ) -> Self { RevsetExpressionEvaluator { repo, + extensions, id_prefix_context, expression, } @@ -77,7 +97,11 @@ impl<'repo> RevsetExpressionEvaluator<'repo> { /// Evaluates the expression. pub fn evaluate(&self) -> Result, UserRevsetEvaluationError> { - let symbol_resolver = default_symbol_resolver(self.repo, self.id_prefix_context); + let symbol_resolver = default_symbol_resolver( + self.repo, + &self.extensions.symbol_resolvers, + self.id_prefix_context, + ); evaluate(self.repo, &symbol_resolver, self.expression.clone()) } @@ -157,13 +181,14 @@ pub fn evaluate<'a>( /// `evaluate()`. pub fn default_symbol_resolver<'a>( repo: &'a dyn Repo, + extensions: &[Arc], id_prefix_context: &'a IdPrefixContext, ) -> DefaultSymbolResolver<'a> { let commit_id_resolver: revset::PrefixResolver = Box::new(|repo, prefix| id_prefix_context.resolve_commit_prefix(repo, prefix)); let change_id_resolver: revset::PrefixResolver> = Box::new(|repo, prefix| id_prefix_context.resolve_change_prefix(repo, prefix)); - DefaultSymbolResolver::new(repo) + DefaultSymbolResolver::new(repo, extensions) .with_commit_id_resolver(commit_id_resolver) .with_change_id_resolver(change_id_resolver) } diff --git a/lib/src/id_prefix.rs b/lib/src/id_prefix.rs index e410436984..8de9de9936 100644 --- a/lib/src/id_prefix.rs +++ b/lib/src/id_prefix.rs @@ -17,6 +17,7 @@ use std::iter; use std::marker::PhantomData; use std::rc::Rc; +use std::sync::Arc; use itertools::Itertools as _; use once_cell::unsync::OnceCell; @@ -25,12 +26,13 @@ use crate::backend::{ChangeId, CommitId}; use crate::hex_util; use crate::object_id::{HexPrefix, ObjectId, PrefixResolution}; use crate::repo::Repo; -use crate::revset::{DefaultSymbolResolver, RevsetExpression}; +use crate::revset::{DefaultSymbolResolver, RevsetExpression, SymbolResolverExtension}; struct PrefixDisambiguationError; struct DisambiguationData { expression: Rc, + extensions: Vec>, indexes: OnceCell, } @@ -43,7 +45,7 @@ struct Indexes { impl DisambiguationData { fn indexes(&self, repo: &dyn Repo) -> Result<&Indexes, PrefixDisambiguationError> { self.indexes.get_or_try_init(|| { - let symbol_resolver = DefaultSymbolResolver::new(repo); + let symbol_resolver = DefaultSymbolResolver::new(repo, &self.extensions); let resolved_expression = self .expression .clone() @@ -98,9 +100,14 @@ pub struct IdPrefixContext { } impl IdPrefixContext { - pub fn disambiguate_within(mut self, expression: Rc) -> Self { + pub fn disambiguate_within( + mut self, + expression: Rc, + extensions: Vec>, + ) -> Self { self.disambiguation = Some(DisambiguationData { expression, + extensions, indexes: OnceCell::new(), }); self diff --git a/lib/src/revset.rs b/lib/src/revset.rs index e28ed659d4..313563c982 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -2102,22 +2102,38 @@ impl SymbolResolver for FailingSymbolResolver { pub type PrefixResolver<'a, T> = Box PrefixResolution + 'a>; +pub trait PartialSymbolResolver<'a> { + fn resolve_symbol(&self, symbol: &str) -> Result>, RevsetResolutionError>; +} + +pub trait SymbolResolverExtension { + fn get_symbol_resolver<'a>( + &self, + repo: &'a dyn Repo, + ) -> Box + 'a>; +} + /// Resolves branches, remote branches, tags, git refs, and full and abbreviated /// commit and change ids. pub struct DefaultSymbolResolver<'a> { repo: &'a dyn Repo, commit_id_resolver: PrefixResolver<'a, CommitId>, change_id_resolver: PrefixResolver<'a, Vec>, + extensions: Vec + 'a>>, } impl<'a> DefaultSymbolResolver<'a> { - pub fn new(repo: &'a dyn Repo) -> Self { + pub fn new(repo: &'a dyn Repo, extensions: &[Arc]) -> Self { DefaultSymbolResolver { repo, commit_id_resolver: Box::new(|repo, prefix| { repo.index().resolve_commit_id_prefix(prefix) }), change_id_resolver: Box::new(|repo, prefix| repo.resolve_change_id_prefix(prefix)), + extensions: extensions + .iter() + .map(|ext| ext.get_symbol_resolver(repo)) + .collect_vec(), } } @@ -2138,6 +2154,12 @@ impl<'a> DefaultSymbolResolver<'a> { } } +fn ids_equal(list1: &[CommitId], list2: &[CommitId]) -> bool { + let list1 = list1.iter().sorted().dedup().collect_vec(); + let list2 = list2.iter().sorted().dedup().collect_vec(); + list1 == list2 +} + impl SymbolResolver for DefaultSymbolResolver<'_> { fn resolve_symbol(&self, symbol: &str) -> Result, RevsetResolutionError> { if symbol.is_empty() { @@ -2194,7 +2216,24 @@ impl SymbolResolver for DefaultSymbolResolver<'_> { } } - Err(make_no_such_symbol_error(self.repo, symbol)) + // Let an extension resolve the symbol. + let ext_ids = self.extensions.iter().try_fold( + None, + |ids, r| -> Result>, RevsetResolutionError> { + if let Some(new_ids) = r.resolve_symbol(symbol)? { + match ids { + Some(prev_ids) if !ids_equal(&prev_ids, &new_ids) => { + Err(RevsetResolutionError::AmbiguousSymbol(symbol.to_owned())) + } + _ => Ok(Some(new_ids)), + } + } else { + Ok(ids) + } + }, + )?; + + ext_ids.ok_or_else(|| make_no_such_symbol_error(self.repo, symbol)) } } diff --git a/lib/tests/test_id_prefix.rs b/lib/tests/test_id_prefix.rs index a9ec064ddd..f86dbe5997 100644 --- a/lib/tests/test_id_prefix.rs +++ b/lib/tests/test_id_prefix.rs @@ -184,7 +184,7 @@ fn test_id_prefix() { // --------------------------------------------------------------------------------------------- let expression = RevsetExpression::commits(vec![commits[0].id().clone(), commits[2].id().clone()]); - let c = c.disambiguate_within(expression); + let c = c.disambiguate_within(expression, vec![]); // The prefix is now shorter assert_eq!( c.shortest_commit_prefix_len(repo.as_ref(), commits[2].id()), @@ -213,7 +213,7 @@ fn test_id_prefix() { // needed. // --------------------------------------------------------------------------------------------- let expression = RevsetExpression::commit(root_commit_id.clone()); - let c = c.disambiguate_within(expression); + let c = c.disambiguate_within(expression, vec![]); assert_eq!( c.shortest_commit_prefix_len(repo.as_ref(), root_commit_id), 1 @@ -243,7 +243,7 @@ fn test_id_prefix() { // --------------------------------------------------------------------------------------------- // TODO: Should be an error let expression = RevsetExpression::symbol("nonexistent".to_string()); - let context = c.disambiguate_within(expression); + let context = c.disambiguate_within(expression, vec![]); assert_eq!( context.shortest_commit_prefix_len(repo.as_ref(), commits[2].id()), 2 diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 278206c045..169394380a 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::path::Path; +use std::sync::Arc; use assert_matches::assert_matches; use itertools::Itertools; @@ -26,9 +27,9 @@ use jj_lib::op_store::{RefTarget, RemoteRef, RemoteRefState, WorkspaceId}; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; use jj_lib::revset::{ - optimize, parse, DefaultSymbolResolver, FailingSymbolResolver, ResolvedExpression, Revset, - RevsetAliasesMap, RevsetExpression, RevsetFilterPredicate, RevsetParseContext, - RevsetResolutionError, RevsetWorkspaceContext, + optimize, parse, DefaultSymbolResolver, FailingSymbolResolver, PartialSymbolResolver, + ResolvedExpression, Revset, RevsetAliasesMap, RevsetExpression, RevsetFilterPredicate, + RevsetParseContext, RevsetResolutionError, RevsetWorkspaceContext, SymbolResolverExtension, }; use jj_lib::revset_graph::{ReverseRevsetGraphIterator, RevsetGraphEdge}; use jj_lib::settings::GitSettings; @@ -39,7 +40,11 @@ use testutils::{ TestRepoBackend, TestWorkspace, }; -fn resolve_symbol(repo: &dyn Repo, symbol: &str) -> Result, RevsetResolutionError> { +fn resolve_symbol_with_extensions( + repo: &dyn Repo, + extensions: &[&Arc], + symbol: &str, +) -> Result, RevsetResolutionError> { let context = RevsetParseContext { aliases_map: &RevsetAliasesMap::new(), user_email: String::new(), @@ -47,18 +52,23 @@ fn resolve_symbol(repo: &dyn Repo, symbol: &str) -> Result, Revset }; let expression = parse(symbol, &context).unwrap(); assert_matches!(*expression, RevsetExpression::CommitRef(_)); - let symbol_resolver = DefaultSymbolResolver::new(repo); + let extensions = extensions.iter().map(|e| (*e).clone()).collect_vec(); + let symbol_resolver = DefaultSymbolResolver::new(repo, &extensions); match expression.resolve_user_expression(repo, &symbol_resolver)? { ResolvedExpression::Commits(commits) => Ok(commits), expression => panic!("symbol resolved to compound expression: {expression:?}"), } } +fn resolve_symbol(repo: &dyn Repo, symbol: &str) -> Result, RevsetResolutionError> { + resolve_symbol_with_extensions(repo, &[], symbol) +} + fn revset_for_commits<'index>( repo: &'index dyn Repo, commits: &[&Commit], ) -> Box { - let symbol_resolver = DefaultSymbolResolver::new(repo); + let symbol_resolver = DefaultSymbolResolver::new(repo, &[]); RevsetExpression::commits(commits.iter().map(|commit| commit.id().clone()).collect()) .resolve_user_expression(repo, &symbol_resolver) .unwrap() @@ -167,7 +177,7 @@ fn test_resolve_symbol_commit_id() { // Test present() suppresses only NoSuchRevision error assert_eq!(resolve_commit_ids(repo.as_ref(), "present(foo)"), []); - let symbol_resolver = DefaultSymbolResolver::new(repo.as_ref()); + let symbol_resolver = DefaultSymbolResolver::new(repo.as_ref(), &[]); let context = RevsetParseContext { aliases_map: &RevsetAliasesMap::new(), user_email: settings.user_email(), @@ -823,6 +833,151 @@ fn test_resolve_symbol_git_refs() { ); } +struct CustomSymbolResolver { + ext: CustomSymbolExtension, +} + +impl CustomSymbolResolver { + fn new(ext: CustomSymbolExtension) -> Self { + Self { ext } + } +} + +impl PartialSymbolResolver<'_> for CustomSymbolResolver { + fn resolve_symbol(&self, symbol: &str) -> Result>, RevsetResolutionError> { + let ids = if symbol == self.ext.symbol { + Some(self.ext.ids.clone()) + } else { + None + }; + + Ok(ids) + } +} + +#[derive(Clone)] +struct CustomSymbolExtension { + symbol: &'static str, + ids: Vec, +} + +impl CustomSymbolExtension { + fn new_arc(symbol: &'static str, commits: &[&Commit]) -> Arc { + Arc::new(Self { + symbol, + ids: commits.iter().map(|c| c.id().to_owned()).collect_vec(), + }) + } +} + +impl SymbolResolverExtension for CustomSymbolExtension { + fn get_symbol_resolver<'a>( + &self, + _repo: &'a dyn Repo, + ) -> Box + 'a> { + Box::new(CustomSymbolResolver::new(self.clone())) + } +} + +#[test] +fn test_resolve_symbol_custom_extensions() { + let settings = testutils::user_settings(); + let test_repo = TestRepo::init(); + let repo = &test_repo.repo; + + let mut tx = repo.start_transaction(&settings); + let mut_repo = tx.mut_repo(); + + // Create some commits and refs to work with and so the repo is not empty + let commit1 = write_random_commit(mut_repo, &settings); + let commit2 = write_random_commit(mut_repo, &settings); + let commit3 = write_random_commit(mut_repo, &settings); + let commit4 = write_random_commit(mut_repo, &settings); + let commit5 = write_random_commit(mut_repo, &settings); + + // Define some extensions which resolve custom labels. + let first_commit = CustomSymbolExtension::new_arc("first_commit", &[&commit1]); + let middle_commit = CustomSymbolExtension::new_arc("middle_commit", &[&commit3]); + let last_commit = CustomSymbolExtension::new_arc("last_commit", &[&commit5]); + + // Normal resolution works fine. + assert_eq!( + resolve_symbol_with_extensions(mut_repo, &[&first_commit], "first_commit").unwrap(), + vec![commit1.id().clone()] + ); + assert_eq!( + resolve_symbol_with_extensions(mut_repo, &[&middle_commit], "middle_commit").unwrap(), + vec![commit3.id().clone()] + ); + assert_eq!( + resolve_symbol_with_extensions(mut_repo, &[&last_commit], "last_commit").unwrap(), + vec![commit5.id().clone()] + ); + + // Multiple extensions are resolved in order. + let exts = &[&first_commit, &middle_commit, &last_commit]; + assert_eq!( + resolve_symbol_with_extensions(mut_repo, exts, "first_commit").unwrap(), + vec![commit1.id().clone()] + ); + assert_eq!( + resolve_symbol_with_extensions(mut_repo, exts, "middle_commit").unwrap(), + vec![commit3.id().clone()] + ); + assert_eq!( + resolve_symbol_with_extensions(mut_repo, exts, "last_commit").unwrap(), + vec![commit5.id().clone()] + ); + assert_matches!( + resolve_symbol_with_extensions(mut_repo, exts, "unknown_symbol"), + Err(RevsetResolutionError::NoSuchRevision { .. }) + ); + + // Conflicts result in an error. + let last_commit_conflict = CustomSymbolExtension::new_arc("last_commit", &[&commit4]); + assert_matches!( + resolve_symbol_with_extensions( + mut_repo, + &[&last_commit, &last_commit_conflict], + "last_commit" + ), + Err(RevsetResolutionError::AmbiguousSymbol { .. }) + ); + // But not if they don't actually conflict. + let last_commit2 = CustomSymbolExtension::new_arc("last_commit", &[&commit5]); + assert_eq!( + resolve_symbol_with_extensions(mut_repo, &[&last_commit, &last_commit2], "last_commit") + .unwrap(), + vec![commit5.id().clone()] + ); + + // Empty and multiple are handled correctly. + let missing = CustomSymbolExtension::new_arc("greatest_commit_of_all_time", &[]); + assert_eq!( + resolve_symbol_with_extensions(mut_repo, &[&missing], "greatest_commit_of_all_time") + .unwrap(), + vec![], + ); + let even_commits = CustomSymbolExtension::new_arc("even_commits", &[&commit2, &commit4]); + assert_eq!( + resolve_symbol_with_extensions(mut_repo, &[&even_commits], "even_commits").unwrap(), + vec![commit2.id().clone(), commit4.id().clone()] + ); + + // Order doesn't matter for conflicts. + let even_commits_conflict = + CustomSymbolExtension::new_arc("even_commits", &[&commit4, &commit2]); + assert_eq!( + resolve_symbol_with_extensions( + mut_repo, + &[&even_commits, &even_commits_conflict], + "even_commits" + ) + .unwrap(), + vec![commit4.id().clone(), commit2.id().clone()] + ); +} + fn resolve_commit_ids(repo: &dyn Repo, revset_str: &str) -> Vec { let settings = testutils::user_settings(); let context = RevsetParseContext { @@ -831,7 +986,7 @@ fn resolve_commit_ids(repo: &dyn Repo, revset_str: &str) -> Vec { workspace: None, }; let expression = optimize(parse(revset_str, &context).unwrap()); - let symbol_resolver = DefaultSymbolResolver::new(repo); + let symbol_resolver = DefaultSymbolResolver::new(repo, &[]); let expression = expression .resolve_user_expression(repo, &symbol_resolver) .unwrap(); @@ -856,7 +1011,7 @@ fn resolve_commit_ids_in_workspace( workspace: Some(workspace_ctx), }; let expression = optimize(parse(revset_str, &context).unwrap()); - let symbol_resolver = DefaultSymbolResolver::new(repo); + let symbol_resolver = DefaultSymbolResolver::new(repo, &[]); let expression = expression .resolve_user_expression(repo, &symbol_resolver) .unwrap();