diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index b66c5f6cb..57c7b3a6d 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -74,6 +74,7 @@ use crate::diff_util::DiffStats; use crate::formatter::Formatter; use crate::revset_util; use crate::template_builder; +use crate::template_builder::expect_plain_text_expression; use crate::template_builder::merge_fn_map; use crate::template_builder::BuildContext; use crate::template_builder::CoreTemplateBuildFnTable; @@ -341,15 +342,24 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { } CommitTemplatePropertyKind::TrailerList(property) => { // TODO: migrate to table? - template_builder::build_formattable_list_method( - self, - diagnostics, - build_ctx, - property, - function, - Self::wrap_trailer, - Self::wrap_trailer_list, - ) + if function.name == "contains_key" { + let [key_node] = function.expect_exact_arguments()?; + let key_property = + expect_plain_text_expression(self, diagnostics, build_ctx, key_node)?; + let out_property = (property, key_property) + .map(|(trailers, key)| trailers.iter().any(|t| t.key == key)); + Ok(Self::wrap_boolean(out_property)) + } else { + template_builder::build_formattable_list_method( + self, + diagnostics, + build_ctx, + property, + function, + Self::wrap_trailer, + Self::wrap_trailer_list, + ) + } } } } diff --git a/cli/tests/test_commit_template.rs b/cli/tests/test_commit_template.rs index eeb6bca56..229bda13d 100644 --- a/cli/tests/test_commit_template.rs +++ b/cli/tests/test_commit_template.rs @@ -1709,4 +1709,22 @@ fn test_log_format_trailers() { "-r@", ]); insta::assert_snapshot!(output, @"Test User I6a6a69649a45c67d3e96a7e5007c110ede34dec5[EOF]"); + + let output = work_dir.run_jj([ + "log", + "--no-graph", + "-T", + r#"self.trailers().contains_key("Signed-off-by")"#, + "-r@", + ]); + insta::assert_snapshot!(output, @"true[EOF]"); + + let output = work_dir.run_jj([ + "log", + "--no-graph", + "-T", + r#"self.trailers().contains_key("foo")"#, + "-r@", + ]); + insta::assert_snapshot!(output, @"false[EOF]"); } diff --git a/docs/config.md b/docs/config.md index 1837da446..9075a2d08 100644 --- a/docs/config.md +++ b/docs/config.md @@ -240,14 +240,15 @@ is ignored. ### Commit trailers -Trailers may be automatically added to the commit description with the -`commit_trailers` template. +You can configure automatic addition of trailers to commit descriptions using +the `commit_trailers` template. Trailers defined in this template will only be +added if they are not already present in the description. ```toml [templates] commit_trailers = ''' -"Signed-off-by: " ++ committer ++ "\n" -''' +format_signed_off_by_trailer(self) +++ if(!trailers.contains_key("Change-Id"), format_gerrit_change_id_trailer(self))''' ``` Some ready-to-use trailer templates are available for frequently used trailers: @@ -256,6 +257,11 @@ Some ready-to-use trailer templates are available for frequently used trailers: * `format_gerrit_change_id_trailer(commit)` creates a "Change-Id" trailer suitable to be used with Gerrit. It is based Jujutsu's change id. +The `trailers.contains_key(key)` method can be used within the template to +conditionally add a trailer if no other trailer with the same key exists. + +Existing trailers are also accessible via `commit.trailers()`. + ### Diff colors and styles In color-words and git diffs, word-level hunks are rendered with underline. You diff --git a/docs/templates.md b/docs/templates.md index 25da74e38..946b4b1f8 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -248,6 +248,13 @@ defined. * `.map(|item| expression) -> ListTemplate`: Apply template `expression` to each element. Example: `parents.map(|c| c.commit_id().short())` +### List type + +The following methods are defined. See also the `List` type. + +* `.contains_key(key: Template) -> Boolean`: True if the commit description + contains at least one trailer with the key `key`. + ### ListTemplate type The following methods are defined. See also the `List` type.