From d1f668551a149294b06d4752ca845334cb0f104d Mon Sep 17 00:00:00 2001 From: dploch Date: Thu, 29 Feb 2024 15:53:59 -0500 Subject: [PATCH 1/3] template_builder: support extending the core template build fn table --- cli/src/template_builder.rs | 52 ++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 6940c54ed2..eff3ee4c4a 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -246,15 +246,26 @@ pub type TemplateBuildMethodFnMap<'a, L, T> = /// Symbol table of methods available in the core template. pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a>> { - string_methods: TemplateBuildMethodFnMap<'a, L, String>, - boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>, - integer_methods: TemplateBuildMethodFnMap<'a, L, i64>, - signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>, - timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>, - timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>, + pub string_methods: TemplateBuildMethodFnMap<'a, L, String>, + pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>, + pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>, + pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>, + pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>, + pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>, // TODO: add global functions table? } +pub fn merge_fn_map<'a, L: TemplateLanguage<'a>, T>( + base: &mut TemplateBuildMethodFnMap<'a, L, T>, + extension: TemplateBuildMethodFnMap<'a, L, T>, +) { + for (name, function) in extension { + if base.insert(name, function).is_some() { + panic!("Conflicting template definitions for '{name}' function"); + } + } +} + impl<'a, L: TemplateLanguage<'a>> CoreTemplateBuildFnTable<'a, L> { /// Creates new symbol table containing the builtin methods. pub fn builtin() -> Self { @@ -268,6 +279,35 @@ impl<'a, L: TemplateLanguage<'a>> CoreTemplateBuildFnTable<'a, L> { } } + pub fn empty() -> Self { + CoreTemplateBuildFnTable { + string_methods: HashMap::new(), + boolean_methods: HashMap::new(), + integer_methods: HashMap::new(), + signature_methods: HashMap::new(), + timestamp_methods: HashMap::new(), + timestamp_range_methods: HashMap::new(), + } + } + + pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) { + let CoreTemplateBuildFnTable { + string_methods, + boolean_methods, + integer_methods, + signature_methods, + timestamp_methods, + timestamp_range_methods, + } = extension; + + merge_fn_map(&mut self.string_methods, string_methods); + merge_fn_map(&mut self.boolean_methods, boolean_methods); + merge_fn_map(&mut self.integer_methods, integer_methods); + merge_fn_map(&mut self.signature_methods, signature_methods); + merge_fn_map(&mut self.timestamp_methods, timestamp_methods); + merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods); + } + /// Applies the method call node `function` to the given `property` by using /// this symbol table. pub fn build_method( From 6c4f44ab81eff32c219e10c4518c708cec6b0958 Mon Sep 17 00:00:00 2001 From: dploch Date: Thu, 29 Feb 2024 16:15:17 -0500 Subject: [PATCH 2/3] commit_templater: make a bunch of types public for extensions --- cli/src/commit_templater.rs | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index b25f414b4e..ed49762c66 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -40,7 +40,7 @@ use crate::templater::{ }; use crate::text_util; -struct CommitTemplateLanguage<'repo> { +pub struct CommitTemplateLanguage<'repo> { repo: &'repo dyn Repo, workspace_id: WorkspaceId, id_prefix_context: &'repo IdPrefixContext, @@ -160,7 +160,7 @@ impl<'repo> CommitTemplateLanguage<'repo> { } } -enum CommitTemplatePropertyKind<'repo> { +pub enum CommitTemplatePropertyKind<'repo> { Core(CoreTemplatePropertyKind<'repo, Commit>), Commit(Box + 'repo>), CommitList(Box> + 'repo>), @@ -232,15 +232,15 @@ type CommitTemplateBuildMethodFnMap<'repo, T> = TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>; /// Symbol table of methods available in the commit template. -struct CommitTemplateBuildFnTable<'repo> { - core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>, - commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>, - ref_name_methods: CommitTemplateBuildMethodFnMap<'repo, RefName>, - commit_or_change_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId>, - shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>, +pub struct CommitTemplateBuildFnTable<'repo> { + pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>, + pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>, + pub ref_name_methods: CommitTemplateBuildMethodFnMap<'repo, RefName>, + pub commit_or_change_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId>, + pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>, } -impl CommitTemplateBuildFnTable<'_> { +impl<'repo> CommitTemplateBuildFnTable<'repo> { /// Creates new symbol table containing the builtin methods. fn builtin() -> Self { CommitTemplateBuildFnTable { @@ -251,6 +251,16 @@ impl CommitTemplateBuildFnTable<'_> { shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(), } } + + pub fn empty() -> Self { + CommitTemplateBuildFnTable { + core: CoreTemplateBuildFnTable::empty(), + commit_methods: HashMap::new(), + ref_name_methods: HashMap::new(), + commit_or_change_id_methods: HashMap::new(), + shortest_id_prefix_methods: HashMap::new(), + } + } } #[derive(Debug, Default)] @@ -506,7 +516,7 @@ fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String { /// Branch or tag name with metadata. #[derive(Clone, Debug, Eq, PartialEq)] -struct RefName { +pub struct RefName { /// Local name. name: String, /// Remote name if this is a remote or Git-tracking ref. @@ -656,7 +666,7 @@ fn extract_git_head(repo: &dyn Repo, commit: &Commit) -> Vec { } #[derive(Clone, Debug, Eq, PartialEq)] -enum CommitOrChangeId { +pub enum CommitOrChangeId { Commit(CommitId), Change(ChangeId), } @@ -736,7 +746,7 @@ fn builtin_commit_or_change_id_methods<'repo>( map } -struct ShortestIdPrefix { +pub struct ShortestIdPrefix { pub prefix: String, pub rest: String, } From baced8b1f6dea2c9811c6496402643bfaced29fa Mon Sep 17 00:00:00 2001 From: dploch Date: Thu, 29 Feb 2024 15:44:49 -0500 Subject: [PATCH 3/3] commit_templater: support extensions of the template language --- cli/examples/custom-commit-templater/main.rs | 87 ++++++++++++++++++++ cli/src/cli_util.rs | 23 ++++++ cli/src/commit_templater.rs | 40 ++++++++- 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 cli/examples/custom-commit-templater/main.rs diff --git a/cli/examples/custom-commit-templater/main.rs b/cli/examples/custom-commit-templater/main.rs new file mode 100644 index 0000000000..edb78fd843 --- /dev/null +++ b/cli/examples/custom-commit-templater/main.rs @@ -0,0 +1,87 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_cli::cli_util::CliRunner; +use jj_cli::commit_templater::{CommitTemplateBuildFnTable, CommitTemplateLanguageExtension}; +use jj_cli::template_builder::TemplateLanguage; +use jj_cli::template_parser::{self, TemplateParseError}; +use jj_cli::templater::{TemplateFunction, TemplatePropertyError}; +use jj_lib::commit::Commit; +use jj_lib::object_id::ObjectId; + +struct HexCounter; + +fn num_digits_in_id(commit: Commit) -> Result { + let mut count = 0; + for ch in commit.id().hex().chars() { + if ch.is_ascii_digit() { + count += 1; + } + } + Ok(count) +} + +fn num_char_in_id(commit: Commit, ch_match: char) -> Result { + let mut count = 0; + for ch in commit.id().hex().chars() { + if ch == ch_match { + count += 1; + } + } + Ok(count) +} + +impl CommitTemplateLanguageExtension for HexCounter { + fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo> { + let mut table = CommitTemplateBuildFnTable::empty(); + table.commit_methods.insert( + "num_digits_in_id", + |language, _build_context, property, call| { + template_parser::expect_no_arguments(call)?; + Ok(language.wrap_integer(TemplateFunction::new(property, num_digits_in_id))) + }, + ); + table.commit_methods.insert( + "num_char_in_id", + |language, _build_context, property, call| { + let [string_arg] = template_parser::expect_exact_arguments(call)?; + let char_arg = + template_parser::expect_string_literal_with(string_arg, |string, span| { + let chars: Vec<_> = string.chars().collect(); + match chars[..] { + [ch] => Ok(ch), + _ => Err(TemplateParseError::unexpected_expression( + "Expected singular character argument", + span, + )), + } + })?; + + Ok( + language.wrap_integer(TemplateFunction::new(property, move |commit| { + num_char_in_id(commit, char_arg) + })), + ) + }, + ); + + table + } +} + +fn main() -> std::process::ExitCode { + CliRunner::init() + .set_commit_template_extension(Box::new(HexCounter)) + .run() +} diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 3b3dac4725..1acc9295ec 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -78,6 +78,7 @@ use tracing::instrument; use tracing_chrome::ChromeLayerBuilder; use tracing_subscriber::prelude::*; +use crate::commit_templater::CommitTemplateLanguageExtension; use crate::config::{ new_config_path, AnnotatedValue, CommandNameAndArgs, ConfigSource, LayeredConfigs, }; @@ -590,6 +591,7 @@ pub struct CommandHelper { global_args: GlobalArgs, settings: UserSettings, layered_configs: LayeredConfigs, + commit_template_extension: Option>, maybe_workspace_loader: Result, store_factories: StoreFactories, working_copy_factories: HashMap>, @@ -605,6 +607,7 @@ impl CommandHelper { global_args: GlobalArgs, settings: UserSettings, layered_configs: LayeredConfigs, + commit_template_extension: Option>, maybe_workspace_loader: Result, store_factories: StoreFactories, working_copy_factories: HashMap>, @@ -621,6 +624,7 @@ impl CommandHelper { global_args, settings, layered_configs, + commit_template_extension, maybe_workspace_loader, store_factories, working_copy_factories, @@ -799,6 +803,7 @@ pub struct WorkspaceCommandHelper { settings: UserSettings, workspace: Workspace, user_repo: ReadonlyUserRepo, + commit_template_extension: Option>, revset_aliases_map: RevsetAliasesMap, template_aliases_map: TemplateAliasesMap, may_update_working_copy: bool, @@ -823,6 +828,7 @@ impl WorkspaceCommandHelper { repo.as_ref(), workspace.workspace_id(), &id_prefix_context, + command.commit_template_extension.as_deref(), &template_aliases_map, &command.settings, )?; @@ -836,6 +842,7 @@ impl WorkspaceCommandHelper { settings: command.settings.clone(), workspace, user_repo: ReadonlyUserRepo::new(repo), + commit_template_extension: command.commit_template_extension.clone(), revset_aliases_map, template_aliases_map, may_update_working_copy, @@ -1263,6 +1270,7 @@ Set which revision the branch points to with `jj branch set {branch_name} -r { self.tx.repo(), self.helper.workspace_id(), &id_prefix_context, + self.helper.commit_template_extension.as_deref(), &self.helper.template_aliases_map, &self.helper.settings, ) @@ -2152,6 +2162,7 @@ fn parse_commit_summary_template<'a>( repo: &'a dyn Repo, workspace_id: &WorkspaceId, id_prefix_context: &'a IdPrefixContext, + extension: Option<&dyn CommitTemplateLanguageExtension>, aliases_map: &TemplateAliasesMap, settings: &UserSettings, ) -> Result + 'a>, CommandError> { @@ -2160,6 +2171,7 @@ fn parse_commit_summary_template<'a>( repo, workspace_id, id_prefix_context, + extension, &template_text, aliases_map, )?) @@ -2841,6 +2853,7 @@ pub struct CliRunner { extra_configs: Option, store_factories: Option, working_copy_factories: Option>>, + commit_template_extension: Option>, dispatch_fn: CliDispatchFn, start_hook_fns: Vec, process_global_args_fns: Vec, @@ -2862,6 +2875,7 @@ impl CliRunner { extra_configs: None, store_factories: None, working_copy_factories: None, + commit_template_extension: None, dispatch_fn: Box::new(crate::commands::run_command), start_hook_fns: vec![], process_global_args_fns: vec![], @@ -2895,6 +2909,14 @@ impl CliRunner { self } + pub fn set_commit_template_extension( + mut self, + commit_template_extension: Box, + ) -> Self { + self.commit_template_extension = Some(commit_template_extension.into()); + self + } + pub fn add_start_hook(mut self, start_hook_fn: CliDispatchFn) -> Self { self.start_hook_fns.push(start_hook_fn); self @@ -3009,6 +3031,7 @@ impl CliRunner { args.global_args, settings, layered_configs, + self.commit_template_extension, maybe_workspace_loader, self.store_factories.unwrap_or_default(), working_copy_factories, diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index ed49762c66..5fa704fb52 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -30,8 +30,8 @@ use once_cell::unsync::OnceCell; use crate::formatter::Formatter; use crate::template_builder::{ - self, BuildContext, CoreTemplateBuildFnTable, CoreTemplatePropertyKind, IntoTemplateProperty, - TemplateBuildMethodFnMap, TemplateLanguage, + self, merge_fn_map, BuildContext, CoreTemplateBuildFnTable, CoreTemplatePropertyKind, + IntoTemplateProperty, TemplateBuildMethodFnMap, TemplateLanguage, }; use crate::template_parser::{self, FunctionCallNode, TemplateAliasesMap, TemplateParseResult}; use crate::templater::{ @@ -48,6 +48,10 @@ pub struct CommitTemplateLanguage<'repo> { keyword_cache: CommitKeywordCache, } +pub trait CommitTemplateLanguageExtension { + fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>; +} + impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { type Context = Commit; type Property = CommitTemplatePropertyKind<'repo>; @@ -228,7 +232,7 @@ impl<'repo> IntoTemplateProperty<'repo, Commit> for CommitTemplatePropertyKind<' } /// Table of functions that translate method call node of self type `T`. -type CommitTemplateBuildMethodFnMap<'repo, T> = +pub type CommitTemplateBuildMethodFnMap<'repo, T> = TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>; /// Symbol table of methods available in the commit template. @@ -261,6 +265,28 @@ impl<'repo> CommitTemplateBuildFnTable<'repo> { shortest_id_prefix_methods: HashMap::new(), } } + + fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) { + let CommitTemplateBuildFnTable { + core, + commit_methods, + ref_name_methods, + commit_or_change_id_methods, + shortest_id_prefix_methods, + } = extension; + + self.core.merge(core); + merge_fn_map(&mut self.commit_methods, commit_methods); + merge_fn_map(&mut self.ref_name_methods, ref_name_methods); + merge_fn_map( + &mut self.commit_or_change_id_methods, + commit_or_change_id_methods, + ); + merge_fn_map( + &mut self.shortest_id_prefix_methods, + shortest_id_prefix_methods, + ); + } } #[derive(Debug, Default)] @@ -805,14 +831,20 @@ pub fn parse<'repo>( repo: &'repo dyn Repo, workspace_id: &WorkspaceId, id_prefix_context: &'repo IdPrefixContext, + extension: Option<&dyn CommitTemplateLanguageExtension>, template_text: &str, aliases_map: &TemplateAliasesMap, ) -> TemplateParseResult + 'repo>> { + let mut build_fn_table = CommitTemplateBuildFnTable::builtin(); + if let Some(extension) = extension { + build_fn_table.merge(extension.build_fn_table()); + } + let language = CommitTemplateLanguage { repo, workspace_id: workspace_id.clone(), id_prefix_context, - build_fn_table: CommitTemplateBuildFnTable::builtin(), + build_fn_table, keyword_cache: CommitKeywordCache::default(), }; let node = template_parser::parse(template_text, aliases_map)?;