Skip to content

Commit

Permalink
templater: add config(name) function
Browse files Browse the repository at this point in the history
This could be used in order to switch template outputs conditionally, or to
get the default push remote for example.
  • Loading branch information
yuja committed Jan 11, 2025
1 parent a9d2dd0 commit 3b6a7ab
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
9 changes: 6 additions & 3 deletions cli/src/commands/config/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")?,
Expand Down Expand Up @@ -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))
Expand Down
5 changes: 5 additions & 0 deletions cli/src/commit_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 15 additions & 5 deletions cli/src/generic_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,22 +37,26 @@ 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>,
}

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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions cli/src/operation_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 29 additions & 1 deletion cli/src/template_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -1796,6 +1818,7 @@ mod tests {
use std::iter;

use jj_lib::backend::MillisSinceEpoch;
use jj_lib::config::StackedConfig;

use super::*;
use crate::formatter;
Expand All @@ -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(),
}
Expand Down
41 changes: 41 additions & 0 deletions cli/tests/test_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]", 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,
Expand Down
1 change: 1 addition & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 3b6a7ab

Please sign in to comment.