mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-10 22:39:32 +00:00
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.
This commit is contained in:
parent
4e30fc7215
commit
98724278c5
8 changed files with 105 additions and 9 deletions
|
@ -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`
|
* 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 bugs
|
||||||
|
|
||||||
* Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`.
|
* Fixed diff selection by external tools with `jj split`/`commit -i FILESETS`.
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
use clap_complete::ArgValueCandidates;
|
use clap_complete::ArgValueCandidates;
|
||||||
use jj_lib::config::ConfigNamePathBuf;
|
use jj_lib::config::ConfigNamePathBuf;
|
||||||
use jj_lib::config::ConfigSource;
|
use jj_lib::config::ConfigSource;
|
||||||
|
use jj_lib::settings::UserSettings;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::ConfigLevelArgs;
|
use super::ConfigLevelArgs;
|
||||||
|
@ -64,7 +65,7 @@ pub fn cmd_config_list(
|
||||||
args: &ConfigListArgs,
|
args: &ConfigListArgs,
|
||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError> {
|
||||||
let template = {
|
let template = {
|
||||||
let language = config_template_language();
|
let language = config_template_language(command.settings());
|
||||||
let text = match &args.template {
|
let text = match &args.template {
|
||||||
Some(value) => value.to_owned(),
|
Some(value) => value.to_owned(),
|
||||||
None => command.settings().get_string("templates.config_list")?,
|
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
|
// AnnotatedValue will be cloned internally in the templater. If the cloning
|
||||||
// cost matters, wrap it with Rc.
|
// 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>;
|
type L = GenericTemplateLanguage<'static, AnnotatedValue>;
|
||||||
let mut language = L::new();
|
let mut language = L::new(settings);
|
||||||
language.add_keyword("name", |self_property| {
|
language.add_keyword("name", |self_property| {
|
||||||
let out_property = self_property.map(|annotated| annotated.name.to_string());
|
let out_property = self_property.map(|annotated| annotated.name.to_string());
|
||||||
Ok(L::wrap_string(out_property))
|
Ok(L::wrap_string(out_property))
|
||||||
|
|
|
@ -49,6 +49,7 @@ use jj_lib::revset::RevsetDiagnostics;
|
||||||
use jj_lib::revset::RevsetModifier;
|
use jj_lib::revset::RevsetModifier;
|
||||||
use jj_lib::revset::RevsetParseContext;
|
use jj_lib::revset::RevsetParseContext;
|
||||||
use jj_lib::revset::UserRevsetExpression;
|
use jj_lib::revset::UserRevsetExpression;
|
||||||
|
use jj_lib::settings::UserSettings;
|
||||||
use jj_lib::signing::SigStatus;
|
use jj_lib::signing::SigStatus;
|
||||||
use jj_lib::signing::SignError;
|
use jj_lib::signing::SignError;
|
||||||
use jj_lib::signing::SignResult;
|
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);
|
template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core);
|
||||||
|
|
||||||
|
fn settings(&self) -> &UserSettings {
|
||||||
|
self.repo.base_repo().settings()
|
||||||
|
}
|
||||||
|
|
||||||
fn build_function(
|
fn build_function(
|
||||||
&self,
|
&self,
|
||||||
diagnostics: &mut TemplateDiagnostics,
|
diagnostics: &mut TemplateDiagnostics,
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use jj_lib::settings::UserSettings;
|
||||||
|
|
||||||
use crate::template_builder;
|
use crate::template_builder;
|
||||||
use crate::template_builder::BuildContext;
|
use crate::template_builder::BuildContext;
|
||||||
use crate::template_builder::CoreTemplateBuildFnTable;
|
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
|
/// types. It's cloned several times internally. Keyword functions need to be
|
||||||
/// registered to extract properties from the self object.
|
/// registered to extract properties from the self object.
|
||||||
pub struct GenericTemplateLanguage<'a, C> {
|
pub struct GenericTemplateLanguage<'a, C> {
|
||||||
|
settings: UserSettings,
|
||||||
build_fn_table: GenericTemplateBuildFnTable<'a, C>,
|
build_fn_table: GenericTemplateBuildFnTable<'a, C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,15 +45,18 @@ impl<'a, C> GenericTemplateLanguage<'a, C> {
|
||||||
/// Sets up environment with no keywords.
|
/// Sets up environment with no keywords.
|
||||||
///
|
///
|
||||||
/// New keyword functions can be registered by `add_keyword()`.
|
/// New keyword functions can be registered by `add_keyword()`.
|
||||||
// It's not "Default" in a way that the core methods table is NOT empty.
|
pub fn new(settings: &UserSettings) -> Self {
|
||||||
#[allow(clippy::new_without_default)]
|
Self::with_keywords(HashMap::new(), settings)
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::with_keywords(HashMap::new())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up environment with the given `keywords` table.
|
/// 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 {
|
GenericTemplateLanguage {
|
||||||
|
// Clone settings to keep lifetime simple. It's cheap.
|
||||||
|
settings: settings.clone(),
|
||||||
build_fn_table: GenericTemplateBuildFnTable {
|
build_fn_table: GenericTemplateBuildFnTable {
|
||||||
core: CoreTemplateBuildFnTable::builtin(),
|
core: CoreTemplateBuildFnTable::builtin(),
|
||||||
keywords,
|
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);
|
template_builder::impl_core_wrap_property_fns!('a, GenericTemplatePropertyKind::Core);
|
||||||
|
|
||||||
|
fn settings(&self) -> &UserSettings {
|
||||||
|
&self.settings
|
||||||
|
}
|
||||||
|
|
||||||
fn build_function(
|
fn build_function(
|
||||||
&self,
|
&self,
|
||||||
diagnostics: &mut TemplateDiagnostics,
|
diagnostics: &mut TemplateDiagnostics,
|
||||||
|
|
|
@ -23,6 +23,7 @@ use jj_lib::object_id::ObjectId;
|
||||||
use jj_lib::op_store::OperationId;
|
use jj_lib::op_store::OperationId;
|
||||||
use jj_lib::operation::Operation;
|
use jj_lib::operation::Operation;
|
||||||
use jj_lib::repo::RepoLoader;
|
use jj_lib::repo::RepoLoader;
|
||||||
|
use jj_lib::settings::UserSettings;
|
||||||
|
|
||||||
use crate::template_builder;
|
use crate::template_builder;
|
||||||
use crate::template_builder::merge_fn_map;
|
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);
|
template_builder::impl_core_wrap_property_fns!('static, OperationTemplatePropertyKind::Core);
|
||||||
|
|
||||||
|
fn settings(&self) -> &UserSettings {
|
||||||
|
self.repo_loader.settings()
|
||||||
|
}
|
||||||
|
|
||||||
fn build_function(
|
fn build_function(
|
||||||
&self,
|
&self,
|
||||||
diagnostics: &mut TemplateDiagnostics,
|
diagnostics: &mut TemplateDiagnostics,
|
||||||
|
|
|
@ -19,8 +19,10 @@ use std::io;
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use jj_lib::backend::Signature;
|
use jj_lib::backend::Signature;
|
||||||
use jj_lib::backend::Timestamp;
|
use jj_lib::backend::Timestamp;
|
||||||
|
use jj_lib::config::ConfigNamePathBuf;
|
||||||
use jj_lib::config::ConfigValue;
|
use jj_lib::config::ConfigValue;
|
||||||
use jj_lib::dsl_util::AliasExpandError as _;
|
use jj_lib::dsl_util::AliasExpandError as _;
|
||||||
|
use jj_lib::settings::UserSettings;
|
||||||
use jj_lib::time_util::DatePattern;
|
use jj_lib::time_util::DatePattern;
|
||||||
use serde::de::IntoDeserializer as _;
|
use serde::de::IntoDeserializer as _;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -88,6 +90,8 @@ pub trait TemplateLanguage<'a> {
|
||||||
fn wrap_template(template: Box<dyn Template + 'a>) -> Self::Property;
|
fn wrap_template(template: Box<dyn Template + 'a>) -> Self::Property;
|
||||||
fn wrap_list_template(template: Box<dyn ListTemplate + '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.
|
/// Translates the given global `function` call to a property.
|
||||||
///
|
///
|
||||||
/// This should be delegated to
|
/// 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)))
|
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
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1796,6 +1818,7 @@ mod tests {
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
|
||||||
use jj_lib::backend::MillisSinceEpoch;
|
use jj_lib::backend::MillisSinceEpoch;
|
||||||
|
use jj_lib::config::StackedConfig;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::formatter;
|
use crate::formatter;
|
||||||
|
@ -1814,8 +1837,13 @@ mod tests {
|
||||||
|
|
||||||
impl TestTemplateEnv {
|
impl TestTemplateEnv {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
Self::with_config(StackedConfig::with_defaults())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_config(config: StackedConfig) -> Self {
|
||||||
|
let settings = UserSettings::from_config(config).unwrap();
|
||||||
TestTemplateEnv {
|
TestTemplateEnv {
|
||||||
language: L::new(),
|
language: L::new(&settings),
|
||||||
aliases_map: TemplateAliasesMap::new(),
|
aliases_map: TemplateAliasesMap::new(),
|
||||||
color_rules: Vec::new(),
|
color_rules: Vec::new(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
fn get_template_output(
|
||||||
test_env: &TestEnvironment,
|
test_env: &TestEnvironment,
|
||||||
repo_path: &Path,
|
repo_path: &Path,
|
||||||
|
|
|
@ -76,6 +76,7 @@ The following functions are defined.
|
||||||
Insert separator between **non-empty** contents.
|
Insert separator between **non-empty** contents.
|
||||||
* `surround(prefix: Template, suffix: Template, content: Template) -> Template`:
|
* `surround(prefix: Template, suffix: Template, content: Template) -> Template`:
|
||||||
Surround **non-empty** content with texts such as parentheses.
|
Surround **non-empty** content with texts such as parentheses.
|
||||||
|
* `config(name: String) -> ConfigValue`: Look up configuration value by `name`.
|
||||||
|
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue