mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-18 18:27:38 +00:00
templater: add timestamp.format() method
A format string is parsed statically due to error handling restriction. I think it covers almost all use cases.
This commit is contained in:
parent
b6a5d63b09
commit
86318bf530
4 changed files with 107 additions and 1 deletions
|
@ -137,6 +137,8 @@ Any types can be implicitly converted to `Template`. No methods are defined.
|
||||||
The following methods are defined.
|
The following methods are defined.
|
||||||
|
|
||||||
* `.ago() -> String`: Format as relative timestamp.
|
* `.ago() -> String`: Format as relative timestamp.
|
||||||
|
* `.format(format: String) -> String`: Format with [the specified strftime-like
|
||||||
|
format string](https://docs.rs/chrono/latest/chrono/format/strftime/).
|
||||||
|
|
||||||
### TimestampRange type
|
### TimestampRange type
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ use jujutsu_lib::backend::{Signature, Timestamp};
|
||||||
|
|
||||||
use crate::template_parser::{
|
use crate::template_parser::{
|
||||||
self, ExpressionKind, ExpressionNode, FunctionCallNode, MethodCallNode, TemplateParseError,
|
self, ExpressionKind, ExpressionNode, FunctionCallNode, MethodCallNode, TemplateParseError,
|
||||||
TemplateParseResult,
|
TemplateParseErrorKind, TemplateParseResult,
|
||||||
};
|
};
|
||||||
use crate::templater::{
|
use crate::templater::{
|
||||||
ConcatTemplate, ConditionalTemplate, FormattablePropertyListTemplate, IntoTemplate,
|
ConcatTemplate, ConditionalTemplate, FormattablePropertyListTemplate, IntoTemplate,
|
||||||
|
@ -382,6 +382,21 @@ fn build_timestamp_method<'a, L: TemplateLanguage<'a>>(
|
||||||
time_util::format_timestamp_relative_to_now(×tamp)
|
time_util::format_timestamp_relative_to_now(×tamp)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
"format" => {
|
||||||
|
// No dynamic string is allowed as the templater has no runtime error type.
|
||||||
|
let [format_node] = template_parser::expect_exact_arguments(function)?;
|
||||||
|
let format =
|
||||||
|
template_parser::expect_string_literal_with(format_node, |format, span| {
|
||||||
|
time_util::FormattingItems::parse(format).ok_or_else(|| {
|
||||||
|
let kind = TemplateParseErrorKind::InvalidTimeFormat;
|
||||||
|
TemplateParseError::with_span(kind, span)
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.into_owned();
|
||||||
|
language.wrap_string(TemplateFunction::new(self_property, move |timestamp| {
|
||||||
|
time_util::format_absolute_timestamp_with(×tamp, &format)
|
||||||
|
}))
|
||||||
|
}
|
||||||
_ => return Err(TemplateParseError::no_such_method("Timestamp", function)),
|
_ => return Err(TemplateParseError::no_such_method("Timestamp", function)),
|
||||||
};
|
};
|
||||||
Ok(property)
|
Ok(property)
|
||||||
|
|
|
@ -57,6 +57,8 @@ pub enum TemplateParseErrorKind {
|
||||||
InvalidArgumentCountRangeFrom(RangeFrom<usize>),
|
InvalidArgumentCountRangeFrom(RangeFrom<usize>),
|
||||||
#[error(r#"Expected argument of type "{0}""#)]
|
#[error(r#"Expected argument of type "{0}""#)]
|
||||||
InvalidArgumentType(String),
|
InvalidArgumentType(String),
|
||||||
|
#[error("Invalid time format")]
|
||||||
|
InvalidTimeFormat,
|
||||||
#[error("Redefinition of function parameter")]
|
#[error("Redefinition of function parameter")]
|
||||||
RedefinedFunctionParameter,
|
RedefinedFunctionParameter,
|
||||||
#[error(r#"Alias "{0}" cannot be expanded"#)]
|
#[error(r#"Alias "{0}" cannot be expanded"#)]
|
||||||
|
@ -648,6 +650,26 @@ pub fn expect_arguments<'a, 'i, const N: usize, const M: usize>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies the given function if the `node` is a string literal.
|
||||||
|
pub fn expect_string_literal_with<'a, 'i, T>(
|
||||||
|
node: &'a ExpressionNode<'i>,
|
||||||
|
f: impl FnOnce(&'a str, pest::Span<'i>) -> TemplateParseResult<T>,
|
||||||
|
) -> TemplateParseResult<T> {
|
||||||
|
match &node.kind {
|
||||||
|
ExpressionKind::String(s) => f(s, node.span),
|
||||||
|
ExpressionKind::Identifier(_)
|
||||||
|
| ExpressionKind::Integer(_)
|
||||||
|
| ExpressionKind::Concat(_)
|
||||||
|
| ExpressionKind::FunctionCall(_)
|
||||||
|
| ExpressionKind::MethodCall(_) => Err(TemplateParseError::invalid_argument_type(
|
||||||
|
"String literal",
|
||||||
|
node.span,
|
||||||
|
)),
|
||||||
|
ExpressionKind::AliasExpanded(id, subst) => expect_string_literal_with(subst, f)
|
||||||
|
.map_err(|e| e.within_alias_expansion(*id, node.span)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use assert_matches::assert_matches;
|
use assert_matches::assert_matches;
|
||||||
|
|
|
@ -332,6 +332,73 @@ fn test_templater_signature() {
|
||||||
insta::assert_snapshot!(render(r#"author.username()"#), @"x");
|
insta::assert_snapshot!(render(r#"author.username()"#), @"x");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_templater_timestamp_method() {
|
||||||
|
let test_env = TestEnvironment::default();
|
||||||
|
test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
|
||||||
|
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]);
|
||||||
|
|
||||||
|
test_env.add_config(
|
||||||
|
r###"
|
||||||
|
[template-aliases]
|
||||||
|
'time_format' = '"%Y-%m-%d"'
|
||||||
|
'bad_time_format' = '"%_"'
|
||||||
|
"###,
|
||||||
|
);
|
||||||
|
|
||||||
|
insta::assert_snapshot!(
|
||||||
|
render(r#"author.timestamp().format("%Y%m%d %H:%M:%S")"#), @"19700101 00:00:00");
|
||||||
|
|
||||||
|
// Invalid format string
|
||||||
|
insta::assert_snapshot!(render_err(r#"author.timestamp().format("%_")"#), @r###"
|
||||||
|
Error: Failed to parse template: --> 1:27
|
||||||
|
|
|
||||||
|
1 | author.timestamp().format("%_")
|
||||||
|
| ^--^
|
||||||
|
|
|
||||||
|
= Invalid time format
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Invalid type
|
||||||
|
insta::assert_snapshot!(render_err(r#"author.timestamp().format(0)"#), @r###"
|
||||||
|
Error: Failed to parse template: --> 1:27
|
||||||
|
|
|
||||||
|
1 | author.timestamp().format(0)
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
= Expected argument of type "String literal"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Dynamic string isn't supported yet
|
||||||
|
insta::assert_snapshot!(render_err(r#"author.timestamp().format("%Y" ++ "%m")"#), @r###"
|
||||||
|
Error: Failed to parse template: --> 1:27
|
||||||
|
|
|
||||||
|
1 | author.timestamp().format("%Y" ++ "%m")
|
||||||
|
| ^----------^
|
||||||
|
|
|
||||||
|
= Expected argument of type "String literal"
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Literal alias expansion
|
||||||
|
insta::assert_snapshot!(render(r#"author.timestamp().format(time_format)"#), @"1970-01-01");
|
||||||
|
insta::assert_snapshot!(render_err(r#"author.timestamp().format(bad_time_format)"#), @r###"
|
||||||
|
Error: Failed to parse template: --> 1:27
|
||||||
|
|
|
||||||
|
1 | author.timestamp().format(bad_time_format)
|
||||||
|
| ^-------------^
|
||||||
|
|
|
||||||
|
= Alias "bad_time_format" cannot be expanded
|
||||||
|
--> 1:1
|
||||||
|
|
|
||||||
|
1 | "%_"
|
||||||
|
| ^--^
|
||||||
|
|
|
||||||
|
= Invalid time format
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_templater_fill_function() {
|
fn test_templater_fill_function() {
|
||||||
let test_env = TestEnvironment::default();
|
let test_env = TestEnvironment::default();
|
||||||
|
|
Loading…
Reference in a new issue