ok/jj
1
0
Fork 0
forked from mirrors/jj

commit_templater: add self.immutable() method

I don't know how immutable revisions should be labeled by default, but users
can customize templates whatever they like.
This commit is contained in:
Yuya Nishihara 2024-03-13 16:45:41 +09:00
parent 04d5f59cbb
commit 218b1c6c16
5 changed files with 107 additions and 2 deletions

View file

@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
It can thereby be for all use cases where `jj move` can be used. The `--from`
argument accepts a revset that resolves to move than one revision.
* Commit templates now support `immutable` keyword.
### Fixed bugs
## [0.15.1] - 2024-03-06

View file

@ -888,6 +888,7 @@ Set which revision the branch points to with `jj branch set {branch_name} -r <RE
let language = CommitTemplateLanguage::new(
self.repo().as_ref(),
self.workspace_id(),
self.revset_parse_context(),
self.id_prefix_context()?,
self.commit_template_extension.as_deref(),
);
@ -1361,6 +1362,7 @@ impl WorkspaceCommandTransaction<'_> {
let language = CommitTemplateLanguage::new(
self.tx.repo(),
self.helper.workspace_id(),
self.helper.revset_parse_context(),
&id_prefix_context,
self.helper.commit_template_extension.as_deref(),
);

View file

@ -27,6 +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::{git, rewrite};
use once_cell::unsync::OnceCell;
@ -35,12 +36,12 @@ use crate::template_builder::{
self, merge_fn_map, BuildContext, CoreTemplateBuildFnTable, CoreTemplatePropertyKind,
IntoTemplateProperty, TemplateBuildMethodFnMap, TemplateLanguage,
};
use crate::template_parser::{self, FunctionCallNode, TemplateParseResult};
use crate::template_parser::{self, FunctionCallNode, TemplateParseError, TemplateParseResult};
use crate::templater::{
self, IntoTemplate, PlainTextFormattedProperty, Template, TemplateFunction, TemplateProperty,
TemplatePropertyFn,
};
use crate::text_util;
use crate::{revset_util, text_util};
pub trait CommitTemplateLanguageExtension {
fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>;
@ -51,6 +52,12 @@ pub trait CommitTemplateLanguageExtension {
pub struct CommitTemplateLanguage<'repo> {
repo: &'repo dyn Repo,
workspace_id: WorkspaceId,
// RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime
// anyway to capture it to evaluate dynamically-constructed user expression
// such as `revset("ancestors(" ++ commit_id ++ ")")`.
// TODO: Maybe refactor context structs? WorkspaceId is contained in
// RevsetParseContext for example.
revset_parse_context: RevsetParseContext<'repo>,
id_prefix_context: &'repo IdPrefixContext,
build_fn_table: CommitTemplateBuildFnTable<'repo>,
keyword_cache: CommitKeywordCache,
@ -63,6 +70,7 @@ impl<'repo> CommitTemplateLanguage<'repo> {
pub fn new(
repo: &'repo dyn Repo,
workspace_id: &WorkspaceId,
revset_parse_context: RevsetParseContext<'repo>,
id_prefix_context: &'repo IdPrefixContext,
extension: Option<&dyn CommitTemplateLanguageExtension>,
) -> Self {
@ -79,6 +87,7 @@ impl<'repo> CommitTemplateLanguage<'repo> {
CommitTemplateLanguage {
repo,
workspace_id: workspace_id.clone(),
revset_parse_context,
id_prefix_context,
build_fn_table,
keyword_cache: CommitKeywordCache::default(),
@ -544,6 +553,17 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm
});
Ok(language.wrap_boolean(out_property))
});
map.insert(
"immutable",
|language, _build_ctx, self_property, function| {
template_parser::expect_no_arguments(function)?;
let revset = evaluate_immutable_revset(language, function.name_span)?;
let is_immutable = revset.containing_fn();
let out_property =
TemplateFunction::new(self_property, move |commit| Ok(is_immutable(commit.id())));
Ok(language.wrap_boolean(out_property))
},
);
map.insert(
"conflict",
|language, _build_ctx, self_property, function| {
@ -591,6 +611,24 @@ fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String {
names.join(" ")
}
fn evaluate_immutable_revset<'repo>(
language: &CommitTemplateLanguage<'repo>,
span: pest::Span<'_>,
) -> Result<Box<dyn Revset + 'repo>, 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 "::<recent_heads>" query to use bitset-based implementation.
let expression = revset_util::parse_immutable_expression(repo, &language.revset_parse_context)
.map_err(|err| {
TemplateParseError::unexpected_expression(revset_util::format_parse_error(&err), span)
})?;
let symbol_resolver = revset_util::default_symbol_resolver(repo, language.id_prefix_context);
let revset = revset_util::evaluate(repo, &symbol_resolver, expression)
.map_err(|err| TemplateParseError::unexpected_expression(err.to_string(), span))?;
Ok(revset)
}
/// Branch or tag name with metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RefName {

View file

@ -546,3 +546,64 @@ fn test_log_customize_short_id() {
ZZZZZZZZ root() 00000000
"###);
}
#[test]
fn test_log_immutable() {
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 = r#"
separate(" ",
description.first_line(),
branches,
if(immutable, "[immutable]"),
) ++ "\n"
"#;
test_env.add_config("revset-aliases.'immutable_heads()' = 'main'");
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r::", "-T", template]);
insta::assert_snapshot!(stdout, @r###"
@ D
C
B main [immutable]
A [immutable]
[immutable]
"###);
// Suppress error that could be detected earlier
test_env.add_config("revsets.short-prefixes = ''");
test_env.add_config("revset-aliases.'immutable_heads()' = 'unknown_fn()'");
let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r::", "-T", template]);
insta::assert_snapshot!(stderr, @r###"
Error: Failed to parse template: --> 5:10
|
5 | if(immutable, "[immutable]"),
| ^-------^
|
= Failed to parse revset: --> 1:1
|
1 | unknown_fn()
| ^--------^
|
= Revset function "unknown_fn" doesn't exist
"###);
test_env.add_config("revset-aliases.'immutable_heads()' = 'unknown_symbol'");
let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r::", "-T", template]);
insta::assert_snapshot!(stderr, @r###"
Error: Failed to parse template: --> 5:10
|
5 | if(immutable, "[immutable]"),
| ^-------^
|
= Revision "unknown_symbol" doesn't exist
"###);
}

View file

@ -85,6 +85,8 @@ This type cannot be printed. The following methods are defined.
* `divergent() -> Boolean`: True if the commit's change id corresponds to multiple
visible commits.
* `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).
* `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.