mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-05 19:14:43 +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`
|
||||
|
||||
* 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`.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue