From 3b6a7ab238999a5a8c42a268ddf53d5f6c2e8a98 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Thu, 9 Jan 2025 11:31:37 +0900 Subject: [PATCH] templater: add config(name) function This could be used in order to switch template outputs conditionally, or to get the default push remote for example. --- CHANGELOG.md | 3 +++ cli/src/commands/config/list.rs | 9 +++++--- cli/src/commit_templater.rs | 5 ++++ cli/src/generic_templater.rs | 20 ++++++++++++---- cli/src/operation_templater.rs | 5 ++++ cli/src/template_builder.rs | 30 +++++++++++++++++++++++- cli/tests/test_templater.rs | 41 +++++++++++++++++++++++++++++++++ docs/templates.md | 1 + 8 files changed, 105 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2766d72b43..8b4847623e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Add a new template alias `builtin_op_log_oneline` along with `format_operation_oneline` and `format_snapshot_operation_oneline` +* New template function `config(name)` to access to configuration variable from + template. + ### Fixed bugs * Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`. diff --git a/cli/src/commands/config/list.rs b/cli/src/commands/config/list.rs index c0c6fc34a0..7285a96b4a 100644 --- a/cli/src/commands/config/list.rs +++ b/cli/src/commands/config/list.rs @@ -15,6 +15,7 @@ use clap_complete::ArgValueCandidates; use jj_lib::config::ConfigNamePathBuf; use jj_lib::config::ConfigSource; +use jj_lib::settings::UserSettings; use tracing::instrument; use super::ConfigLevelArgs; @@ -64,7 +65,7 @@ pub fn cmd_config_list( args: &ConfigListArgs, ) -> Result<(), CommandError> { let template = { - let language = config_template_language(); + let language = config_template_language(command.settings()); let text = match &args.template { Some(value) => value.to_owned(), None => command.settings().get_string("templates.config_list")?, @@ -107,9 +108,11 @@ pub fn cmd_config_list( // AnnotatedValue will be cloned internally in the templater. If the cloning // cost matters, wrap it with Rc. -fn config_template_language() -> GenericTemplateLanguage<'static, AnnotatedValue> { +fn config_template_language( + settings: &UserSettings, +) -> GenericTemplateLanguage<'static, AnnotatedValue> { type L = GenericTemplateLanguage<'static, AnnotatedValue>; - let mut language = L::new(); + let mut language = L::new(settings); language.add_keyword("name", |self_property| { let out_property = self_property.map(|annotated| annotated.name.to_string()); Ok(L::wrap_string(out_property)) diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index 1dbf579afa..95ea7f1d8f 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -49,6 +49,7 @@ use jj_lib::revset::RevsetDiagnostics; use jj_lib::revset::RevsetModifier; use jj_lib::revset::RevsetParseContext; use jj_lib::revset::UserRevsetExpression; +use jj_lib::settings::UserSettings; use jj_lib::signing::SigStatus; use jj_lib::signing::SignError; use jj_lib::signing::SignResult; @@ -151,6 +152,10 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core); + fn settings(&self) -> &UserSettings { + self.repo.base_repo().settings() + } + fn build_function( &self, diagnostics: &mut TemplateDiagnostics, diff --git a/cli/src/generic_templater.rs b/cli/src/generic_templater.rs index cf8fee15b2..89443e266c 100644 --- a/cli/src/generic_templater.rs +++ b/cli/src/generic_templater.rs @@ -15,6 +15,8 @@ use std::cmp::Ordering; use std::collections::HashMap; +use jj_lib::settings::UserSettings; + use crate::template_builder; use crate::template_builder::BuildContext; use crate::template_builder::CoreTemplateBuildFnTable; @@ -35,6 +37,7 @@ use crate::templater::TemplateProperty; /// types. It's cloned several times internally. Keyword functions need to be /// registered to extract properties from the self object. pub struct GenericTemplateLanguage<'a, C> { + settings: UserSettings, build_fn_table: GenericTemplateBuildFnTable<'a, C>, } @@ -42,15 +45,18 @@ impl<'a, C> GenericTemplateLanguage<'a, C> { /// Sets up environment with no keywords. /// /// New keyword functions can be registered by `add_keyword()`. - // It's not "Default" in a way that the core methods table is NOT empty. - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self::with_keywords(HashMap::new()) + pub fn new(settings: &UserSettings) -> Self { + Self::with_keywords(HashMap::new(), settings) } /// Sets up environment with the given `keywords` table. - pub fn with_keywords(keywords: GenericTemplateBuildKeywordFnMap<'a, C>) -> Self { + pub fn with_keywords( + keywords: GenericTemplateBuildKeywordFnMap<'a, C>, + settings: &UserSettings, + ) -> Self { GenericTemplateLanguage { + // Clone settings to keep lifetime simple. It's cheap. + settings: settings.clone(), build_fn_table: GenericTemplateBuildFnTable { core: CoreTemplateBuildFnTable::builtin(), keywords, @@ -86,6 +92,10 @@ impl<'a, C: 'a> TemplateLanguage<'a> for GenericTemplateLanguage<'a, C> { template_builder::impl_core_wrap_property_fns!('a, GenericTemplatePropertyKind::Core); + fn settings(&self) -> &UserSettings { + &self.settings + } + fn build_function( &self, diagnostics: &mut TemplateDiagnostics, diff --git a/cli/src/operation_templater.rs b/cli/src/operation_templater.rs index eda021dcac..5eea5164ef 100644 --- a/cli/src/operation_templater.rs +++ b/cli/src/operation_templater.rs @@ -23,6 +23,7 @@ use jj_lib::object_id::ObjectId; use jj_lib::op_store::OperationId; use jj_lib::operation::Operation; use jj_lib::repo::RepoLoader; +use jj_lib::settings::UserSettings; use crate::template_builder; use crate::template_builder::merge_fn_map; @@ -89,6 +90,10 @@ impl TemplateLanguage<'static> for OperationTemplateLanguage { template_builder::impl_core_wrap_property_fns!('static, OperationTemplatePropertyKind::Core); + fn settings(&self) -> &UserSettings { + self.repo_loader.settings() + } + fn build_function( &self, diagnostics: &mut TemplateDiagnostics, diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index d97a133cd0..4536cc5c6e 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -19,8 +19,10 @@ use std::io; use itertools::Itertools as _; use jj_lib::backend::Signature; use jj_lib::backend::Timestamp; +use jj_lib::config::ConfigNamePathBuf; use jj_lib::config::ConfigValue; use jj_lib::dsl_util::AliasExpandError as _; +use jj_lib::settings::UserSettings; use jj_lib::time_util::DatePattern; use serde::de::IntoDeserializer as _; use serde::Deserialize; @@ -88,6 +90,8 @@ pub trait TemplateLanguage<'a> { fn wrap_template(template: Box) -> Self::Property; fn wrap_list_template(template: Box) -> Self::Property; + fn settings(&self) -> &UserSettings; + /// Translates the given global `function` call to a property. /// /// This should be delegated to @@ -1490,6 +1494,24 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun }); Ok(L::wrap_template(Box::new(template))) }); + map.insert("config", |language, _diagnostics, _build_ctx, function| { + // Dynamic lookup can be implemented if needed. The name is literal + // string for now so the error can be reported early. + let [name_node] = function.expect_exact_arguments()?; + let name: ConfigNamePathBuf = + template_parser::expect_string_literal_with(name_node, |name, span| { + name.parse().map_err(|err| { + TemplateParseError::expression("Failed to parse config name", span) + .with_source(err) + }) + })?; + let value = language.settings().get_value(&name).map_err(|err| { + TemplateParseError::expression("Failed to get config value", function.name_span) + .with_source(err) + })?; + // .decorated("", "") to trim leading/trailing whitespace + Ok(L::wrap_config_value(Literal(value.decorated("", "")))) + }); map } @@ -1796,6 +1818,7 @@ mod tests { use std::iter; use jj_lib::backend::MillisSinceEpoch; + use jj_lib::config::StackedConfig; use super::*; use crate::formatter; @@ -1814,8 +1837,13 @@ mod tests { impl TestTemplateEnv { fn new() -> Self { + Self::with_config(StackedConfig::with_defaults()) + } + + fn with_config(config: StackedConfig) -> Self { + let settings = UserSettings::from_config(config).unwrap(); TestTemplateEnv { - language: L::new(), + language: L::new(&settings), aliases_map: TemplateAliasesMap::new(), color_rules: Vec::new(), } diff --git a/cli/tests/test_templater.rs b/cli/tests/test_templater.rs index a37647e59e..5cea472da4 100644 --- a/cli/tests/test_templater.rs +++ b/cli/tests/test_templater.rs @@ -481,6 +481,47 @@ fn test_templater_bad_alias_decl() { "###); } +#[test] +fn test_templater_config_function() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + let render = |template| get_template_output(&test_env, &repo_path, "@-", template); + let render_err = |template| test_env.jj_cmd_failure(&repo_path, &["log", "-T", template]); + + insta::assert_snapshot!( + render("config('user.name')"), + @r#""Test User""#); + insta::assert_snapshot!( + render("config('user')"), + @r#"{ email = "test.user@example.com", name = "Test User" }"#); + insta::assert_snapshot!(render_err("config('invalid name')"), @r" + Error: Failed to parse template: Failed to parse config name + Caused by: + 1: --> 1:8 + | + 1 | config('invalid name') + | ^------------^ + | + = Failed to parse config name + 2: TOML parse error at line 1, column 9 + | + 1 | invalid name + | ^ + "); + insta::assert_snapshot!(render_err("config('unknown')"), @r" + Error: Failed to parse template: Failed to get config value + Caused by: + 1: --> 1:1 + | + 1 | config('unknown') + | ^----^ + | + = Failed to get config value + 2: Value not found for unknown + "); +} + fn get_template_output( test_env: &TestEnvironment, repo_path: &Path, diff --git a/docs/templates.md b/docs/templates.md index 6294547f79..cc977762ca 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -76,6 +76,7 @@ The following functions are defined. Insert separator between **non-empty** contents. * `surround(prefix: Template, suffix: Template, content: Template) -> Template`: Surround **non-empty** content with texts such as parentheses. +* `config(name: String) -> ConfigValue`: Look up configuration value by `name`. ## Types