diff --git a/CHANGELOG.md b/CHANGELOG.md index b3bbd79534..69f6e0d62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj rebase -r` now accepts `--insert-after` and `--insert-before` options to customize the location of the rebased revisions. +* Commit objects in templates now have a `containted_in([revset: String]) -> Boolean` method. + ### Fixed bugs * Revsets now support `\`-escapes in string literal. diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index 8abc371c85..e258d1373f 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -27,7 +27,7 @@ use jj_lib::id_prefix::IdPrefixContext; use jj_lib::object_id::ObjectId as _; use jj_lib::op_store::{RefTarget, WorkspaceId}; use jj_lib::repo::Repo; -use jj_lib::revset::{Revset, RevsetParseContext}; +use jj_lib::revset::{self, Revset, RevsetExpression, RevsetParseContext}; use jj_lib::{git, rewrite}; use once_cell::unsync::OnceCell; @@ -620,6 +620,20 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm Ok(L::wrap_boolean(out_property)) }, ); + map.insert( + "contained_in", + |language, _build_ctx, self_property, function| { + let [revset_node] = template_parser::expect_exact_arguments(function)?; + + let is_contained = + template_parser::expect_string_literal_with(revset_node, |revset, span| { + Ok(evaluate_user_revset(language, span, revset)?.containing_fn()) + })?; + + let out_property = self_property.map(move |commit| is_contained(commit.id())); + Ok(L::wrap_boolean(out_property)) + }, + ); map.insert( "conflict", |_language, _build_ctx, self_property, function| { @@ -666,11 +680,27 @@ fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String { type RevsetContainingFn<'repo> = dyn Fn(&CommitId) -> bool + 'repo; +fn evaluate_revset_expression<'repo>( + language: &CommitTemplateLanguage<'repo>, + span: pest::Span<'_>, + expression: Rc, +) -> Result, TemplateParseError> { + let symbol_resolver = revset_util::default_symbol_resolver( + language.repo, + language.revset_parse_context.extensions.symbol_resolvers(), + language.id_prefix_context, + ); + let revset = + revset_util::evaluate(language.repo, &symbol_resolver, expression).map_err(|err| { + TemplateParseError::expression("Failed to evaluate revset", span).with_source(err) + })?; + Ok(revset) +} + fn evaluate_immutable_revset<'repo>( language: &CommitTemplateLanguage<'repo>, span: pest::Span<'_>, ) -> Result, TemplateParseError> { - let repo = language.repo; // Alternatively, a negated (i.e. visible mutable) set could be computed. // It's usually smaller than the immutable set. The revset engine can also // optimize "::" query to use bitset-based implementation. @@ -678,15 +708,20 @@ fn evaluate_immutable_revset<'repo>( .map_err(|err| { TemplateParseError::expression("Failed to parse revset", span).with_source(err) })?; - let symbol_resolver = revset_util::default_symbol_resolver( - repo, - language.revset_parse_context.extensions.symbol_resolvers(), - language.id_prefix_context, - ); - let revset = revset_util::evaluate(repo, &symbol_resolver, expression).map_err(|err| { - TemplateParseError::expression("Failed to evaluate revset", span).with_source(err) + + evaluate_revset_expression(language, span, expression) +} + +fn evaluate_user_revset<'repo>( + language: &CommitTemplateLanguage<'repo>, + span: pest::Span<'_>, + revset: &str, +) -> Result, TemplateParseError> { + let expression = revset::parse(&revset, &language.revset_parse_context).map_err(|err| { + TemplateParseError::expression("Failed to parse revset", span).with_source(err) })?; - Ok(revset) + + evaluate_revset_expression(language, span, expression) } /// Branch or tag name with metadata. diff --git a/cli/tests/test_commit_template.rs b/cli/tests/test_commit_template.rs index 1be13f16b9..0e0363ee4a 100644 --- a/cli/tests/test_commit_template.rs +++ b/cli/tests/test_commit_template.rs @@ -658,3 +658,101 @@ fn test_log_immutable() { 2: Revision "unknown_symbol" doesn't exist "###); } + +#[test] +fn test_log_contained_in() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + test_env.jj_cmd_ok(&repo_path, &["new", "-mA", "root()"]); + test_env.jj_cmd_ok(&repo_path, &["new", "-mB"]); + test_env.jj_cmd_ok(&repo_path, &["branch", "create", "main"]); + test_env.jj_cmd_ok(&repo_path, &["new", "-mC"]); + test_env.jj_cmd_ok(&repo_path, &["new", "-mD", "root()"]); + + let template_for_revset = |revset: &str| { + format!( + r#" + separate(" ", + description.first_line(), + branches, + if(self.contained_in("{revset}"), "[contained_in]"), + ) ++ "\n" + "# + ) + }; + + let stdout = test_env.jj_cmd_success( + &repo_path, + &[ + "log", + "-r::", + "-T", + &template_for_revset(r#"description(A)::"#), + ], + ); + insta::assert_snapshot!(stdout, @r###" + @ D + │ ◉ C [contained_in] + │ ◉ B main [contained_in] + │ ◉ A [contained_in] + ├─╯ + ◉ + "###); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &[ + "log", + "-r::", + "-T", + &template_for_revset(r#"visible_heads()"#), + ], + ); + insta::assert_snapshot!(stdout, @r###" + @ D [contained_in] + │ ◉ C [contained_in] + │ ◉ B main + │ ◉ A + ├─╯ + ◉ + "###); + + // Suppress error that could be detected earlier + let stderr = test_env.jj_cmd_failure( + &repo_path, + &["log", "-r::", "-T", &template_for_revset("unknown_fn()")], + ); + insta::assert_snapshot!(stderr, @r###" + Error: Failed to parse template: Failed to parse revset + Caused by: + 1: --> 5:28 + | + 5 | if(self.contained_in("unknown_fn()"), "[contained_in]"), + | ^------------^ + | + = Failed to parse revset + 2: --> 1:1 + | + 1 | unknown_fn() + | ^--------^ + | + = Function "unknown_fn" doesn't exist + "###); + + let stderr = test_env.jj_cmd_failure( + &repo_path, + &["log", "-r::", "-T", &template_for_revset("unknown_symbol")], + ); + insta::assert_snapshot!(stderr, @r###" + Error: Failed to parse template: Failed to evaluate revset + Caused by: + 1: --> 5:28 + | + 5 | if(self.contained_in("unknown_symbol"), "[contained_in]"), + | ^--------------^ + | + = Failed to evaluate revset + 2: Revision "unknown_symbol" doesn't exist + "###); +} diff --git a/docs/templates.md b/docs/templates.md index f0f2a396c9..bd67771c3e 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -91,6 +91,7 @@ This type cannot be printed. The following methods are defined. * `hidden() -> Boolean`: True if the commit is not visible (a.k.a. abandoned). * `immutable() -> Boolean`: True if the commit is included in [the set of immutable commits](config.md#set-of-immutable-commits). +* `contained_in([revset: String]) -> Boolean`: True if the commit is included in [the provided revset](revsets.md). * `conflict() -> Boolean`: True if the commit contains merge conflicts. * `empty() -> Boolean`: True if the commit modifies no files. * `root() -> Boolean`: True if the commit is the root commit.