templater: add config(name) function
Some checks are pending
binaries / Build binary artifacts (push) Waiting to run
website / prerelease-docs-build-deploy (ubuntu-24.04) (push) Waiting to run
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run

This could be used in order to switch template outputs conditionally, or to
get the default push remote for example.
This commit is contained in:
Yuya Nishihara 2025-01-09 11:31:37 +09:00
parent 4e30fc7215
commit 98724278c5
8 changed files with 105 additions and 9 deletions

View file

@ -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`.

View file

@ -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))

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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<dyn Template + 'a>) -> Self::Property;
fn wrap_list_template(template: Box<dyn ListTemplate + 'a>) -> 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(),
}

View file

@ -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,

View file

@ -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