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:
parent
04d5f59cbb
commit
218b1c6c16
5 changed files with 107 additions and 2 deletions
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
"###);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue