From d905698a9575ebac2f646ead943c50d6bd5f9c69 Mon Sep 17 00:00:00 2001 From: dploch Date: Thu, 29 Feb 2024 15:44:49 -0500 Subject: [PATCH] commit_templater: support extensions of the template language --- cli/examples/custom-commit-templater/main.rs | 102 +++++++++++++++++++ cli/src/cli_util.rs | 23 +++++ cli/src/commit_templater.rs | 32 +++++- 3 files changed, 153 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..53386a133d --- /dev/null +++ b/cli/examples/custom-commit-templater/main.rs @@ -0,0 +1,102 @@ +// 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 std::sync::Arc; + +use jj_cli::cli_util::CliRunner; +use jj_cli::commit_templater::{ + CommitTemplateBuildFnTable, CommitTemplateLanguageExtension, CommitTemplatePropertyKind, +}; +use jj_cli::template_builder::CoreTemplatePropertyKind; +use jj_cli::template_parser::{self, ExpressionKind, ExpressionNode, 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::new(); + table.commit_methods.insert( + "num_digits_in_id", + |_language, _build_context, property, call| { + template_parser::expect_no_arguments(call)?; + Ok(CommitTemplatePropertyKind::Core( + CoreTemplatePropertyKind::Integer(Box::new(TemplateFunction::new( + property, + num_digits_in_id, + ))), + )) + }, + ); + table.commit_methods.insert( + "num_char_in_id", + |_language, _build_context, property, call| match &call.args[..] { + [ExpressionNode { + kind: ExpressionKind::String(string), + span: _, + }] => { + let chars: Vec<_> = string.chars().collect(); + if chars.len() != 1 { + return Err(TemplateParseError::invalid_arguments( + call, + "Expected single character argument", + )); + } + + let char = chars[0]; + Ok(CommitTemplatePropertyKind::Core( + CoreTemplatePropertyKind::Integer(Box::new(TemplateFunction::new( + property, + move |commit| num_char_in_id(commit, char), + ))), + )) + } + _ => Err(TemplateParseError::invalid_arguments( + call, + "Expected single character argument", + )), + }, + ); + + table + } +} + +fn main() -> std::process::ExitCode { + CliRunner::init() + .set_commit_template_extension(Arc::new(HexCounter)) + .run() +} diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 3b3dac4725..10e03f8b54 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: Arc, + ) -> Self { + self.commit_template_extension = Some(commit_template_extension); + 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 ea33195220..6f7800b249 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,20 @@ impl<'repo> CommitTemplateBuildFnTable<'repo> { shortest_id_prefix_methods: HashMap::new(), } } + + fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) { + self.core.merge(extension.core); + merge_fn_map(&mut self.commit_methods, extension.commit_methods); + merge_fn_map(&mut self.ref_name_methods, extension.ref_name_methods); + merge_fn_map( + &mut self.commit_or_change_id_methods, + extension.commit_or_change_id_methods, + ); + merge_fn_map( + &mut self.shortest_id_prefix_methods, + extension.shortest_id_prefix_methods, + ); + } } impl<'a> Default for CommitTemplateBuildFnTable<'a> { @@ -811,14 +829,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)?;