jj_cli/
commit_templater.rs

1// Copyright 2020-2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::any::Any;
16use std::cmp::max;
17use std::cmp::Ordering;
18use std::collections::HashMap;
19use std::fmt;
20use std::fmt::Display;
21use std::io;
22use std::rc::Rc;
23
24use bstr::BString;
25use futures::stream::BoxStream;
26use futures::StreamExt as _;
27use futures::TryStreamExt as _;
28use itertools::Itertools as _;
29use jj_lib::backend::BackendResult;
30use jj_lib::backend::ChangeId;
31use jj_lib::backend::CommitId;
32use jj_lib::backend::TreeValue;
33use jj_lib::commit::Commit;
34use jj_lib::conflicts;
35use jj_lib::conflicts::ConflictMarkerStyle;
36use jj_lib::copies::CopiesTreeDiffEntry;
37use jj_lib::copies::CopiesTreeDiffEntryPath;
38use jj_lib::copies::CopyRecords;
39use jj_lib::extensions_map::ExtensionsMap;
40use jj_lib::fileset;
41use jj_lib::fileset::FilesetDiagnostics;
42use jj_lib::fileset::FilesetExpression;
43use jj_lib::id_prefix::IdPrefixContext;
44use jj_lib::id_prefix::IdPrefixIndex;
45use jj_lib::matchers::Matcher;
46use jj_lib::merge::MergedTreeValue;
47use jj_lib::merged_tree::MergedTree;
48use jj_lib::object_id::ObjectId as _;
49use jj_lib::op_store::RefTarget;
50use jj_lib::op_store::RemoteRef;
51use jj_lib::ref_name::WorkspaceName;
52use jj_lib::ref_name::WorkspaceNameBuf;
53use jj_lib::repo::Repo;
54use jj_lib::repo_path::RepoPathBuf;
55use jj_lib::repo_path::RepoPathUiConverter;
56use jj_lib::revset;
57use jj_lib::revset::Revset;
58use jj_lib::revset::RevsetContainingFn;
59use jj_lib::revset::RevsetDiagnostics;
60use jj_lib::revset::RevsetModifier;
61use jj_lib::revset::RevsetParseContext;
62use jj_lib::revset::UserRevsetExpression;
63use jj_lib::settings::UserSettings;
64use jj_lib::signing::SigStatus;
65use jj_lib::signing::SignError;
66use jj_lib::signing::SignResult;
67use jj_lib::signing::Verification;
68use jj_lib::store::Store;
69use jj_lib::trailer;
70use jj_lib::trailer::Trailer;
71use once_cell::unsync::OnceCell;
72use pollster::FutureExt as _;
73
74use crate::diff_util;
75use crate::diff_util::DiffStats;
76use crate::formatter::Formatter;
77use crate::revset_util;
78use crate::template_builder;
79use crate::template_builder::expect_plain_text_expression;
80use crate::template_builder::merge_fn_map;
81use crate::template_builder::BuildContext;
82use crate::template_builder::CoreTemplateBuildFnTable;
83use crate::template_builder::CoreTemplatePropertyKind;
84use crate::template_builder::CoreTemplatePropertyVar;
85use crate::template_builder::TemplateBuildMethodFnMap;
86use crate::template_builder::TemplateLanguage;
87use crate::template_parser;
88use crate::template_parser::ExpressionNode;
89use crate::template_parser::FunctionCallNode;
90use crate::template_parser::TemplateDiagnostics;
91use crate::template_parser::TemplateParseError;
92use crate::template_parser::TemplateParseResult;
93use crate::templater;
94use crate::templater::BoxedTemplateProperty;
95use crate::templater::ListTemplate;
96use crate::templater::PlainTextFormattedProperty;
97use crate::templater::SizeHint;
98use crate::templater::Template;
99use crate::templater::TemplateFormatter;
100use crate::templater::TemplatePropertyError;
101use crate::templater::TemplatePropertyExt as _;
102use crate::text_util;
103
104pub trait CommitTemplateLanguageExtension {
105    fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>;
106
107    fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
108}
109
110pub struct CommitTemplateLanguage<'repo> {
111    repo: &'repo dyn Repo,
112    path_converter: &'repo RepoPathUiConverter,
113    workspace_name: WorkspaceNameBuf,
114    // RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime
115    // anyway to capture it to evaluate dynamically-constructed user expression
116    // such as `revset("ancestors(" ++ commit_id ++ ")")`.
117    // TODO: Maybe refactor context structs? RepoPathUiConverter and
118    // WorkspaceName are contained in RevsetParseContext for example.
119    revset_parse_context: RevsetParseContext<'repo>,
120    id_prefix_context: &'repo IdPrefixContext,
121    immutable_expression: Rc<UserRevsetExpression>,
122    conflict_marker_style: ConflictMarkerStyle,
123    build_fn_table: CommitTemplateBuildFnTable<'repo>,
124    keyword_cache: CommitKeywordCache<'repo>,
125    cache_extensions: ExtensionsMap,
126}
127
128impl<'repo> CommitTemplateLanguage<'repo> {
129    /// Sets up environment where commit template will be transformed to
130    /// evaluation tree.
131    #[expect(clippy::too_many_arguments)]
132    pub fn new(
133        repo: &'repo dyn Repo,
134        path_converter: &'repo RepoPathUiConverter,
135        workspace_name: &WorkspaceName,
136        revset_parse_context: RevsetParseContext<'repo>,
137        id_prefix_context: &'repo IdPrefixContext,
138        immutable_expression: Rc<UserRevsetExpression>,
139        conflict_marker_style: ConflictMarkerStyle,
140        extensions: &[impl AsRef<dyn CommitTemplateLanguageExtension>],
141    ) -> Self {
142        let mut build_fn_table = CommitTemplateBuildFnTable::builtin();
143        let mut cache_extensions = ExtensionsMap::empty();
144
145        for extension in extensions {
146            build_fn_table.merge(extension.as_ref().build_fn_table());
147            extension
148                .as_ref()
149                .build_cache_extensions(&mut cache_extensions);
150        }
151
152        CommitTemplateLanguage {
153            repo,
154            path_converter,
155            workspace_name: workspace_name.to_owned(),
156            revset_parse_context,
157            id_prefix_context,
158            immutable_expression,
159            conflict_marker_style,
160            build_fn_table,
161            keyword_cache: CommitKeywordCache::default(),
162            cache_extensions,
163        }
164    }
165}
166
167impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
168    type Property = CommitTemplatePropertyKind<'repo>;
169
170    fn settings(&self) -> &UserSettings {
171        self.repo.base_repo().settings()
172    }
173
174    fn build_function(
175        &self,
176        diagnostics: &mut TemplateDiagnostics,
177        build_ctx: &BuildContext<Self::Property>,
178        function: &FunctionCallNode,
179    ) -> TemplateParseResult<Self::Property> {
180        let table = &self.build_fn_table.core;
181        table.build_function(self, diagnostics, build_ctx, function)
182    }
183
184    fn build_method(
185        &self,
186        diagnostics: &mut TemplateDiagnostics,
187        build_ctx: &BuildContext<Self::Property>,
188        property: Self::Property,
189        function: &FunctionCallNode,
190    ) -> TemplateParseResult<Self::Property> {
191        let type_name = property.type_name();
192        match property {
193            CommitTemplatePropertyKind::Core(property) => {
194                let table = &self.build_fn_table.core;
195                table.build_method(self, diagnostics, build_ctx, property, function)
196            }
197            CommitTemplatePropertyKind::Commit(property) => {
198                let table = &self.build_fn_table.commit_methods;
199                let build = template_parser::lookup_method(type_name, table, function)?;
200                build(self, diagnostics, build_ctx, property, function)
201            }
202            CommitTemplatePropertyKind::CommitOpt(property) => {
203                let type_name = "Commit";
204                let table = &self.build_fn_table.commit_methods;
205                let build = template_parser::lookup_method(type_name, table, function)?;
206                let inner_property = property.try_unwrap(type_name).into_dyn();
207                build(self, diagnostics, build_ctx, inner_property, function)
208            }
209            CommitTemplatePropertyKind::CommitList(property) => {
210                let table = &self.build_fn_table.commit_list_methods;
211                let build = template_parser::lookup_method(type_name, table, function)?;
212                build(self, diagnostics, build_ctx, property, function)
213            }
214            CommitTemplatePropertyKind::CommitRef(property) => {
215                let table = &self.build_fn_table.commit_ref_methods;
216                let build = template_parser::lookup_method(type_name, table, function)?;
217                build(self, diagnostics, build_ctx, property, function)
218            }
219            CommitTemplatePropertyKind::CommitRefOpt(property) => {
220                let type_name = "CommitRef";
221                let table = &self.build_fn_table.commit_ref_methods;
222                let build = template_parser::lookup_method(type_name, table, function)?;
223                let inner_property = property.try_unwrap(type_name).into_dyn();
224                build(self, diagnostics, build_ctx, inner_property, function)
225            }
226            CommitTemplatePropertyKind::CommitRefList(property) => {
227                let table = &self.build_fn_table.commit_ref_list_methods;
228                let build = template_parser::lookup_method(type_name, table, function)?;
229                build(self, diagnostics, build_ctx, property, function)
230            }
231            CommitTemplatePropertyKind::RefSymbol(property) => {
232                let table = &self.build_fn_table.core.string_methods;
233                let build = template_parser::lookup_method(type_name, table, function)?;
234                let inner_property = property.map(|RefSymbolBuf(s)| s).into_dyn();
235                build(self, diagnostics, build_ctx, inner_property, function)
236            }
237            CommitTemplatePropertyKind::RefSymbolOpt(property) => {
238                let type_name = "RefSymbol";
239                let table = &self.build_fn_table.core.string_methods;
240                let build = template_parser::lookup_method(type_name, table, function)?;
241                let inner_property = property
242                    .try_unwrap(type_name)
243                    .map(|RefSymbolBuf(s)| s)
244                    .into_dyn();
245                build(self, diagnostics, build_ctx, inner_property, function)
246            }
247            CommitTemplatePropertyKind::RepoPath(property) => {
248                let table = &self.build_fn_table.repo_path_methods;
249                let build = template_parser::lookup_method(type_name, table, function)?;
250                build(self, diagnostics, build_ctx, property, function)
251            }
252            CommitTemplatePropertyKind::RepoPathOpt(property) => {
253                let type_name = "RepoPath";
254                let table = &self.build_fn_table.repo_path_methods;
255                let build = template_parser::lookup_method(type_name, table, function)?;
256                let inner_property = property.try_unwrap(type_name).into_dyn();
257                build(self, diagnostics, build_ctx, inner_property, function)
258            }
259            CommitTemplatePropertyKind::ChangeId(property) => {
260                let table = &self.build_fn_table.change_id_methods;
261                let build = template_parser::lookup_method(type_name, table, function)?;
262                build(self, diagnostics, build_ctx, property, function)
263            }
264            CommitTemplatePropertyKind::CommitId(property) => {
265                let table = &self.build_fn_table.commit_id_methods;
266                let build = template_parser::lookup_method(type_name, table, function)?;
267                build(self, diagnostics, build_ctx, property, function)
268            }
269            CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
270                let table = &self.build_fn_table.shortest_id_prefix_methods;
271                let build = template_parser::lookup_method(type_name, table, function)?;
272                build(self, diagnostics, build_ctx, property, function)
273            }
274            CommitTemplatePropertyKind::TreeDiff(property) => {
275                let table = &self.build_fn_table.tree_diff_methods;
276                let build = template_parser::lookup_method(type_name, table, function)?;
277                build(self, diagnostics, build_ctx, property, function)
278            }
279            CommitTemplatePropertyKind::TreeDiffEntry(property) => {
280                let table = &self.build_fn_table.tree_diff_entry_methods;
281                let build = template_parser::lookup_method(type_name, table, function)?;
282                build(self, diagnostics, build_ctx, property, function)
283            }
284            CommitTemplatePropertyKind::TreeDiffEntryList(property) => {
285                let table = &self.build_fn_table.tree_diff_entry_list_methods;
286                let build = template_parser::lookup_method(type_name, table, function)?;
287                build(self, diagnostics, build_ctx, property, function)
288            }
289            CommitTemplatePropertyKind::TreeEntry(property) => {
290                let table = &self.build_fn_table.tree_entry_methods;
291                let build = template_parser::lookup_method(type_name, table, function)?;
292                build(self, diagnostics, build_ctx, property, function)
293            }
294            CommitTemplatePropertyKind::DiffStats(property) => {
295                let table = &self.build_fn_table.diff_stats_methods;
296                let build = template_parser::lookup_method(type_name, table, function)?;
297                // Strip off formatting parameters which are needed only for the
298                // default template output.
299                let property = property.map(|formatted| formatted.stats).into_dyn();
300                build(self, diagnostics, build_ctx, property, function)
301            }
302            CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => {
303                let type_name = "CryptographicSignature";
304                let table = &self.build_fn_table.cryptographic_signature_methods;
305                let build = template_parser::lookup_method(type_name, table, function)?;
306                let inner_property = property.try_unwrap(type_name).into_dyn();
307                build(self, diagnostics, build_ctx, inner_property, function)
308            }
309            CommitTemplatePropertyKind::AnnotationLine(property) => {
310                let type_name = "AnnotationLine";
311                let table = &self.build_fn_table.annotation_line_methods;
312                let build = template_parser::lookup_method(type_name, table, function)?;
313                build(self, diagnostics, build_ctx, property, function)
314            }
315            CommitTemplatePropertyKind::Trailer(property) => {
316                let table = &self.build_fn_table.trailer_methods;
317                let build = template_parser::lookup_method(type_name, table, function)?;
318                build(self, diagnostics, build_ctx, property, function)
319            }
320            CommitTemplatePropertyKind::TrailerList(property) => {
321                let table = &self.build_fn_table.trailer_list_methods;
322                let build = template_parser::lookup_method(type_name, table, function)?;
323                build(self, diagnostics, build_ctx, property, function)
324            }
325        }
326    }
327}
328
329// If we need to add multiple languages that support Commit types, this can be
330// turned into a trait which extends TemplateLanguage.
331impl<'repo> CommitTemplateLanguage<'repo> {
332    pub fn repo(&self) -> &'repo dyn Repo {
333        self.repo
334    }
335
336    pub fn workspace_name(&self) -> &WorkspaceName {
337        &self.workspace_name
338    }
339
340    pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> {
341        &self.keyword_cache
342    }
343
344    pub fn cache_extension<T: Any>(&self) -> Option<&T> {
345        self.cache_extensions.get::<T>()
346    }
347}
348
349pub enum CommitTemplatePropertyKind<'repo> {
350    Core(CoreTemplatePropertyKind<'repo>),
351    Commit(BoxedTemplateProperty<'repo, Commit>),
352    CommitOpt(BoxedTemplateProperty<'repo, Option<Commit>>),
353    CommitList(BoxedTemplateProperty<'repo, Vec<Commit>>),
354    CommitRef(BoxedTemplateProperty<'repo, Rc<CommitRef>>),
355    CommitRefOpt(BoxedTemplateProperty<'repo, Option<Rc<CommitRef>>>),
356    CommitRefList(BoxedTemplateProperty<'repo, Vec<Rc<CommitRef>>>),
357    RefSymbol(BoxedTemplateProperty<'repo, RefSymbolBuf>),
358    RefSymbolOpt(BoxedTemplateProperty<'repo, Option<RefSymbolBuf>>),
359    RepoPath(BoxedTemplateProperty<'repo, RepoPathBuf>),
360    RepoPathOpt(BoxedTemplateProperty<'repo, Option<RepoPathBuf>>),
361    ChangeId(BoxedTemplateProperty<'repo, ChangeId>),
362    CommitId(BoxedTemplateProperty<'repo, CommitId>),
363    ShortestIdPrefix(BoxedTemplateProperty<'repo, ShortestIdPrefix>),
364    TreeDiff(BoxedTemplateProperty<'repo, TreeDiff>),
365    TreeDiffEntry(BoxedTemplateProperty<'repo, TreeDiffEntry>),
366    TreeDiffEntryList(BoxedTemplateProperty<'repo, Vec<TreeDiffEntry>>),
367    TreeEntry(BoxedTemplateProperty<'repo, TreeEntry>),
368    DiffStats(BoxedTemplateProperty<'repo, DiffStatsFormatted<'repo>>),
369    CryptographicSignatureOpt(BoxedTemplateProperty<'repo, Option<CryptographicSignature>>),
370    AnnotationLine(BoxedTemplateProperty<'repo, AnnotationLine>),
371    Trailer(BoxedTemplateProperty<'repo, Trailer>),
372    TrailerList(BoxedTemplateProperty<'repo, Vec<Trailer>>),
373}
374
375template_builder::impl_core_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> => Core);
376template_builder::impl_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> {
377    Commit(Commit),
378    CommitOpt(Option<Commit>),
379    CommitList(Vec<Commit>),
380    CommitRef(Rc<CommitRef>),
381    CommitRefOpt(Option<Rc<CommitRef>>),
382    CommitRefList(Vec<Rc<CommitRef>>),
383    RefSymbol(RefSymbolBuf),
384    RefSymbolOpt(Option<RefSymbolBuf>),
385    RepoPath(RepoPathBuf),
386    RepoPathOpt(Option<RepoPathBuf>),
387    ChangeId(ChangeId),
388    CommitId(CommitId),
389    ShortestIdPrefix(ShortestIdPrefix),
390    TreeDiff(TreeDiff),
391    TreeDiffEntry(TreeDiffEntry),
392    TreeDiffEntryList(Vec<TreeDiffEntry>),
393    TreeEntry(TreeEntry),
394    DiffStats(DiffStatsFormatted<'repo>),
395    CryptographicSignatureOpt(Option<CryptographicSignature>),
396    AnnotationLine(AnnotationLine),
397    Trailer(Trailer),
398    TrailerList(Vec<Trailer>),
399});
400
401impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> {
402    fn wrap_template(template: Box<dyn Template + 'repo>) -> Self {
403        Self::Core(CoreTemplatePropertyKind::wrap_template(template))
404    }
405
406    fn wrap_list_template(template: Box<dyn ListTemplate + 'repo>) -> Self {
407        Self::Core(CoreTemplatePropertyKind::wrap_list_template(template))
408    }
409
410    fn type_name(&self) -> &'static str {
411        match self {
412            Self::Core(property) => property.type_name(),
413            Self::Commit(_) => "Commit",
414            Self::CommitOpt(_) => "Option<Commit>",
415            Self::CommitList(_) => "List<Commit>",
416            Self::CommitRef(_) => "CommitRef",
417            Self::CommitRefOpt(_) => "Option<CommitRef>",
418            Self::CommitRefList(_) => "List<CommitRef>",
419            Self::RefSymbol(_) => "RefSymbol",
420            Self::RefSymbolOpt(_) => "Option<RefSymbol>",
421            Self::RepoPath(_) => "RepoPath",
422            Self::RepoPathOpt(_) => "Option<RepoPath>",
423            Self::ChangeId(_) => "ChangeId",
424            Self::CommitId(_) => "CommitId",
425            Self::ShortestIdPrefix(_) => "ShortestIdPrefix",
426            Self::TreeDiff(_) => "TreeDiff",
427            Self::TreeDiffEntry(_) => "TreeDiffEntry",
428            Self::TreeDiffEntryList(_) => "List<TreeDiffEntry>",
429            Self::TreeEntry(_) => "TreeEntry",
430            Self::DiffStats(_) => "DiffStats",
431            Self::CryptographicSignatureOpt(_) => "Option<CryptographicSignature>",
432            Self::AnnotationLine(_) => "AnnotationLine",
433            Self::Trailer(_) => "Trailer",
434            Self::TrailerList(_) => "List<Trailer>",
435        }
436    }
437
438    fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'repo, bool>> {
439        match self {
440            Self::Core(property) => property.try_into_boolean(),
441            Self::Commit(_) => None,
442            Self::CommitOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
443            Self::CommitList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
444            Self::CommitRef(_) => None,
445            Self::CommitRefOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
446            Self::CommitRefList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
447            Self::RefSymbol(_) => None,
448            Self::RefSymbolOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
449            Self::RepoPath(_) => None,
450            Self::RepoPathOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
451            Self::ChangeId(_) => None,
452            Self::CommitId(_) => None,
453            Self::ShortestIdPrefix(_) => None,
454            // TODO: boolean cast could be implemented, but explicit
455            // diff.empty() method might be better.
456            Self::TreeDiff(_) => None,
457            Self::TreeDiffEntry(_) => None,
458            Self::TreeDiffEntryList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
459            Self::TreeEntry(_) => None,
460            Self::DiffStats(_) => None,
461            Self::CryptographicSignatureOpt(property) => {
462                Some(property.map(|sig| sig.is_some()).into_dyn())
463            }
464            Self::AnnotationLine(_) => None,
465            Self::Trailer(_) => None,
466            Self::TrailerList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
467        }
468    }
469
470    fn try_into_integer(self) -> Option<BoxedTemplateProperty<'repo, i64>> {
471        match self {
472            Self::Core(property) => property.try_into_integer(),
473            _ => None,
474        }
475    }
476
477    fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'repo, String>> {
478        match self {
479            Self::Core(property) => property.try_into_plain_text(),
480            Self::RefSymbol(property) => Some(property.map(|RefSymbolBuf(s)| s).into_dyn()),
481            Self::RefSymbolOpt(property) => Some(
482                property
483                    .try_unwrap("RefSymbol")
484                    .map(|RefSymbolBuf(s)| s)
485                    .into_dyn(),
486            ),
487            _ => {
488                let template = self.try_into_template()?;
489                Some(PlainTextFormattedProperty::new(template).into_dyn())
490            }
491        }
492    }
493
494    fn try_into_template(self) -> Option<Box<dyn Template + 'repo>> {
495        match self {
496            Self::Core(property) => property.try_into_template(),
497            Self::Commit(_) => None,
498            Self::CommitOpt(_) => None,
499            Self::CommitList(_) => None,
500            Self::CommitRef(property) => Some(property.into_template()),
501            Self::CommitRefOpt(property) => Some(property.into_template()),
502            Self::CommitRefList(property) => Some(property.into_template()),
503            Self::RefSymbol(property) => Some(property.into_template()),
504            Self::RefSymbolOpt(property) => Some(property.into_template()),
505            Self::RepoPath(property) => Some(property.into_template()),
506            Self::RepoPathOpt(property) => Some(property.into_template()),
507            Self::ChangeId(property) => Some(property.into_template()),
508            Self::CommitId(property) => Some(property.into_template()),
509            Self::ShortestIdPrefix(property) => Some(property.into_template()),
510            Self::TreeDiff(_) => None,
511            Self::TreeDiffEntry(_) => None,
512            Self::TreeDiffEntryList(_) => None,
513            Self::TreeEntry(_) => None,
514            Self::DiffStats(property) => Some(property.into_template()),
515            Self::CryptographicSignatureOpt(_) => None,
516            Self::AnnotationLine(_) => None,
517            Self::Trailer(property) => Some(property.into_template()),
518            Self::TrailerList(property) => Some(property.into_template()),
519        }
520    }
521
522    fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'repo, bool>> {
523        type Core<'repo> = CoreTemplatePropertyKind<'repo>;
524        match (self, other) {
525            (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_eq(rhs),
526            (Self::Core(Core::String(lhs)), Self::RefSymbol(rhs)) => {
527                Some((lhs, rhs).map(|(l, r)| RefSymbolBuf(l) == r).into_dyn())
528            }
529            (Self::Core(Core::String(lhs)), Self::RefSymbolOpt(rhs)) => Some(
530                (lhs, rhs)
531                    .map(|(l, r)| Some(RefSymbolBuf(l)) == r)
532                    .into_dyn(),
533            ),
534            (Self::RefSymbol(lhs), Self::Core(Core::String(rhs))) => {
535                Some((lhs, rhs).map(|(l, r)| l == RefSymbolBuf(r)).into_dyn())
536            }
537            (Self::RefSymbol(lhs), Self::RefSymbol(rhs)) => {
538                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
539            }
540            (Self::RefSymbol(lhs), Self::RefSymbolOpt(rhs)) => {
541                Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn())
542            }
543            (Self::RefSymbolOpt(lhs), Self::Core(Core::String(rhs))) => Some(
544                (lhs, rhs)
545                    .map(|(l, r)| l == Some(RefSymbolBuf(r)))
546                    .into_dyn(),
547            ),
548            (Self::RefSymbolOpt(lhs), Self::RefSymbol(rhs)) => {
549                Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn())
550            }
551            (Self::RefSymbolOpt(lhs), Self::RefSymbolOpt(rhs)) => {
552                Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
553            }
554            (Self::Core(_), _) => None,
555            (Self::Commit(_), _) => None,
556            (Self::CommitOpt(_), _) => None,
557            (Self::CommitList(_), _) => None,
558            (Self::CommitRef(_), _) => None,
559            (Self::CommitRefOpt(_), _) => None,
560            (Self::CommitRefList(_), _) => None,
561            (Self::RefSymbol(_), _) => None,
562            (Self::RefSymbolOpt(_), _) => None,
563            (Self::RepoPath(_), _) => None,
564            (Self::RepoPathOpt(_), _) => None,
565            (Self::ChangeId(_), _) => None,
566            (Self::CommitId(_), _) => None,
567            (Self::ShortestIdPrefix(_), _) => None,
568            (Self::TreeDiff(_), _) => None,
569            (Self::TreeDiffEntry(_), _) => None,
570            (Self::TreeDiffEntryList(_), _) => None,
571            (Self::TreeEntry(_), _) => None,
572            (Self::DiffStats(_), _) => None,
573            (Self::CryptographicSignatureOpt(_), _) => None,
574            (Self::AnnotationLine(_), _) => None,
575            (Self::Trailer(_), _) => None,
576            (Self::TrailerList(_), _) => None,
577        }
578    }
579
580    fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'repo, Ordering>> {
581        match (self, other) {
582            (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_cmp(rhs),
583            (Self::Core(_), _) => None,
584            (Self::Commit(_), _) => None,
585            (Self::CommitOpt(_), _) => None,
586            (Self::CommitList(_), _) => None,
587            (Self::CommitRef(_), _) => None,
588            (Self::CommitRefOpt(_), _) => None,
589            (Self::CommitRefList(_), _) => None,
590            (Self::RefSymbol(_), _) => None,
591            (Self::RefSymbolOpt(_), _) => None,
592            (Self::RepoPath(_), _) => None,
593            (Self::RepoPathOpt(_), _) => None,
594            (Self::ChangeId(_), _) => None,
595            (Self::CommitId(_), _) => None,
596            (Self::ShortestIdPrefix(_), _) => None,
597            (Self::TreeDiff(_), _) => None,
598            (Self::TreeDiffEntry(_), _) => None,
599            (Self::TreeDiffEntryList(_), _) => None,
600            (Self::TreeEntry(_), _) => None,
601            (Self::DiffStats(_), _) => None,
602            (Self::CryptographicSignatureOpt(_), _) => None,
603            (Self::AnnotationLine(_), _) => None,
604            (Self::Trailer(_), _) => None,
605            (Self::TrailerList(_), _) => None,
606        }
607    }
608}
609
610/// Table of functions that translate method call node of self type `T`.
611pub type CommitTemplateBuildMethodFnMap<'repo, T> =
612    TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>;
613
614/// Symbol table of methods available in the commit template.
615pub struct CommitTemplateBuildFnTable<'repo> {
616    pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>,
617    pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>,
618    pub commit_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Commit>>,
619    pub commit_ref_methods: CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>>,
620    pub commit_ref_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Rc<CommitRef>>>,
621    pub repo_path_methods: CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf>,
622    pub change_id_methods: CommitTemplateBuildMethodFnMap<'repo, ChangeId>,
623    pub commit_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitId>,
624    pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>,
625    pub tree_diff_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiff>,
626    pub tree_diff_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>,
627    pub tree_diff_entry_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<TreeDiffEntry>>,
628    pub tree_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeEntry>,
629    pub diff_stats_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStats>,
630    pub cryptographic_signature_methods:
631        CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature>,
632    pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>,
633    pub trailer_methods: CommitTemplateBuildMethodFnMap<'repo, Trailer>,
634    pub trailer_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Trailer>>,
635}
636
637impl<'repo> CommitTemplateBuildFnTable<'repo> {
638    /// Creates new symbol table containing the builtin methods.
639    fn builtin() -> Self {
640        CommitTemplateBuildFnTable {
641            core: CoreTemplateBuildFnTable::builtin(),
642            commit_methods: builtin_commit_methods(),
643            commit_list_methods: template_builder::builtin_unformattable_list_methods(),
644            commit_ref_methods: builtin_commit_ref_methods(),
645            commit_ref_list_methods: template_builder::builtin_formattable_list_methods(),
646            repo_path_methods: builtin_repo_path_methods(),
647            change_id_methods: builtin_change_id_methods(),
648            commit_id_methods: builtin_commit_id_methods(),
649            shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(),
650            tree_diff_methods: builtin_tree_diff_methods(),
651            tree_diff_entry_methods: builtin_tree_diff_entry_methods(),
652            tree_diff_entry_list_methods: template_builder::builtin_unformattable_list_methods(),
653            tree_entry_methods: builtin_tree_entry_methods(),
654            diff_stats_methods: builtin_diff_stats_methods(),
655            cryptographic_signature_methods: builtin_cryptographic_signature_methods(),
656            annotation_line_methods: builtin_annotation_line_methods(),
657            trailer_methods: builtin_trailer_methods(),
658            trailer_list_methods: builtin_trailer_list_methods(),
659        }
660    }
661
662    pub fn empty() -> Self {
663        CommitTemplateBuildFnTable {
664            core: CoreTemplateBuildFnTable::empty(),
665            commit_methods: HashMap::new(),
666            commit_list_methods: HashMap::new(),
667            commit_ref_methods: HashMap::new(),
668            commit_ref_list_methods: HashMap::new(),
669            repo_path_methods: HashMap::new(),
670            change_id_methods: HashMap::new(),
671            commit_id_methods: HashMap::new(),
672            shortest_id_prefix_methods: HashMap::new(),
673            tree_diff_methods: HashMap::new(),
674            tree_diff_entry_methods: HashMap::new(),
675            tree_diff_entry_list_methods: HashMap::new(),
676            tree_entry_methods: HashMap::new(),
677            diff_stats_methods: HashMap::new(),
678            cryptographic_signature_methods: HashMap::new(),
679            annotation_line_methods: HashMap::new(),
680            trailer_methods: HashMap::new(),
681            trailer_list_methods: HashMap::new(),
682        }
683    }
684
685    fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) {
686        let CommitTemplateBuildFnTable {
687            core,
688            commit_methods,
689            commit_list_methods,
690            commit_ref_methods,
691            commit_ref_list_methods,
692            repo_path_methods,
693            change_id_methods,
694            commit_id_methods,
695            shortest_id_prefix_methods,
696            tree_diff_methods,
697            tree_diff_entry_methods,
698            tree_diff_entry_list_methods,
699            tree_entry_methods,
700            diff_stats_methods,
701            cryptographic_signature_methods,
702            annotation_line_methods,
703            trailer_methods,
704            trailer_list_methods,
705        } = extension;
706
707        self.core.merge(core);
708        merge_fn_map(&mut self.commit_methods, commit_methods);
709        merge_fn_map(&mut self.commit_list_methods, commit_list_methods);
710        merge_fn_map(&mut self.commit_ref_methods, commit_ref_methods);
711        merge_fn_map(&mut self.commit_ref_list_methods, commit_ref_list_methods);
712        merge_fn_map(&mut self.repo_path_methods, repo_path_methods);
713        merge_fn_map(&mut self.change_id_methods, change_id_methods);
714        merge_fn_map(&mut self.commit_id_methods, commit_id_methods);
715        merge_fn_map(
716            &mut self.shortest_id_prefix_methods,
717            shortest_id_prefix_methods,
718        );
719        merge_fn_map(&mut self.tree_diff_methods, tree_diff_methods);
720        merge_fn_map(&mut self.tree_diff_entry_methods, tree_diff_entry_methods);
721        merge_fn_map(
722            &mut self.tree_diff_entry_list_methods,
723            tree_diff_entry_list_methods,
724        );
725        merge_fn_map(&mut self.tree_entry_methods, tree_entry_methods);
726        merge_fn_map(&mut self.diff_stats_methods, diff_stats_methods);
727        merge_fn_map(
728            &mut self.cryptographic_signature_methods,
729            cryptographic_signature_methods,
730        );
731        merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods);
732        merge_fn_map(&mut self.trailer_methods, trailer_methods);
733        merge_fn_map(&mut self.trailer_list_methods, trailer_list_methods);
734    }
735}
736
737#[derive(Default)]
738pub struct CommitKeywordCache<'repo> {
739    // Build index lazily, and Rc to get away from &self lifetime.
740    bookmarks_index: OnceCell<Rc<CommitRefsIndex>>,
741    tags_index: OnceCell<Rc<CommitRefsIndex>>,
742    git_refs_index: OnceCell<Rc<CommitRefsIndex>>,
743    is_immutable_fn: OnceCell<Rc<RevsetContainingFn<'repo>>>,
744}
745
746impl<'repo> CommitKeywordCache<'repo> {
747    pub fn bookmarks_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
748        self.bookmarks_index
749            .get_or_init(|| Rc::new(build_bookmarks_index(repo)))
750    }
751
752    pub fn tags_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
753        self.tags_index
754            .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().tags())))
755    }
756
757    pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
758        self.git_refs_index
759            .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().git_refs())))
760    }
761
762    pub fn is_immutable_fn(
763        &self,
764        language: &CommitTemplateLanguage<'repo>,
765        span: pest::Span<'_>,
766    ) -> TemplateParseResult<&Rc<RevsetContainingFn<'repo>>> {
767        // Alternatively, a negated (i.e. visible mutable) set could be computed.
768        // It's usually smaller than the immutable set. The revset engine can also
769        // optimize "::<recent_heads>" query to use bitset-based implementation.
770        self.is_immutable_fn.get_or_try_init(|| {
771            let expression = &language.immutable_expression;
772            let revset = evaluate_revset_expression(language, span, expression)?;
773            Ok(revset.containing_fn().into())
774        })
775    }
776}
777
778fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> {
779    // Not using maplit::hashmap!{} or custom declarative macro here because
780    // code completion inside macro is quite restricted.
781    let mut map = CommitTemplateBuildMethodFnMap::<Commit>::new();
782    map.insert(
783        "description",
784        |_language, _diagnostics, _build_ctx, self_property, function| {
785            function.expect_no_arguments()?;
786            let out_property =
787                self_property.map(|commit| text_util::complete_newline(commit.description()));
788            Ok(out_property.into_dyn_wrapped())
789        },
790    );
791    map.insert(
792        "trailers",
793        |_language, _diagnostics, _build_ctx, self_property, function| {
794            function.expect_no_arguments()?;
795            let out_property = self_property
796                .map(|commit| trailer::parse_description_trailers(commit.description()));
797            Ok(out_property.into_dyn_wrapped())
798        },
799    );
800    map.insert(
801        "change_id",
802        |_language, _diagnostics, _build_ctx, self_property, function| {
803            function.expect_no_arguments()?;
804            let out_property = self_property.map(|commit| commit.change_id().to_owned());
805            Ok(out_property.into_dyn_wrapped())
806        },
807    );
808    map.insert(
809        "commit_id",
810        |_language, _diagnostics, _build_ctx, self_property, function| {
811            function.expect_no_arguments()?;
812            let out_property = self_property.map(|commit| commit.id().to_owned());
813            Ok(out_property.into_dyn_wrapped())
814        },
815    );
816    map.insert(
817        "parents",
818        |_language, _diagnostics, _build_ctx, self_property, function| {
819            function.expect_no_arguments()?;
820            let out_property = self_property.and_then(|commit| {
821                let commits: Vec<_> = commit.parents().try_collect()?;
822                Ok(commits)
823            });
824            Ok(out_property.into_dyn_wrapped())
825        },
826    );
827    map.insert(
828        "author",
829        |_language, _diagnostics, _build_ctx, self_property, function| {
830            function.expect_no_arguments()?;
831            let out_property = self_property.map(|commit| commit.author().clone());
832            Ok(out_property.into_dyn_wrapped())
833        },
834    );
835    map.insert(
836        "committer",
837        |_language, _diagnostics, _build_ctx, self_property, function| {
838            function.expect_no_arguments()?;
839            let out_property = self_property.map(|commit| commit.committer().clone());
840            Ok(out_property.into_dyn_wrapped())
841        },
842    );
843    map.insert(
844        "mine",
845        |language, _diagnostics, _build_ctx, self_property, function| {
846            function.expect_no_arguments()?;
847            let user_email = language.revset_parse_context.user_email.to_owned();
848            let out_property = self_property.map(move |commit| commit.author().email == user_email);
849            Ok(out_property.into_dyn_wrapped())
850        },
851    );
852    map.insert(
853        "signature",
854        |_language, _diagnostics, _build_ctx, self_property, function| {
855            function.expect_no_arguments()?;
856            let out_property = self_property.map(CryptographicSignature::new);
857            Ok(out_property.into_dyn_wrapped())
858        },
859    );
860    map.insert(
861        "working_copies",
862        |language, _diagnostics, _build_ctx, self_property, function| {
863            function.expect_no_arguments()?;
864            let repo = language.repo;
865            let out_property = self_property.map(|commit| extract_working_copies(repo, &commit));
866            Ok(out_property.into_dyn_wrapped())
867        },
868    );
869    map.insert(
870        "current_working_copy",
871        |language, _diagnostics, _build_ctx, self_property, function| {
872            function.expect_no_arguments()?;
873            let repo = language.repo;
874            let name = language.workspace_name.clone();
875            let out_property = self_property
876                .map(move |commit| Some(commit.id()) == repo.view().get_wc_commit_id(&name));
877            Ok(out_property.into_dyn_wrapped())
878        },
879    );
880    map.insert(
881        "bookmarks",
882        |language, _diagnostics, _build_ctx, self_property, function| {
883            function.expect_no_arguments()?;
884            let index = language
885                .keyword_cache
886                .bookmarks_index(language.repo)
887                .clone();
888            let out_property = self_property.map(move |commit| {
889                index
890                    .get(commit.id())
891                    .iter()
892                    .filter(|commit_ref| commit_ref.is_local() || !commit_ref.synced)
893                    .cloned()
894                    .collect_vec()
895            });
896            Ok(out_property.into_dyn_wrapped())
897        },
898    );
899    map.insert(
900        "local_bookmarks",
901        |language, _diagnostics, _build_ctx, self_property, function| {
902            function.expect_no_arguments()?;
903            let index = language
904                .keyword_cache
905                .bookmarks_index(language.repo)
906                .clone();
907            let out_property = self_property.map(move |commit| {
908                index
909                    .get(commit.id())
910                    .iter()
911                    .filter(|commit_ref| commit_ref.is_local())
912                    .cloned()
913                    .collect_vec()
914            });
915            Ok(out_property.into_dyn_wrapped())
916        },
917    );
918    map.insert(
919        "remote_bookmarks",
920        |language, _diagnostics, _build_ctx, self_property, function| {
921            function.expect_no_arguments()?;
922            let index = language
923                .keyword_cache
924                .bookmarks_index(language.repo)
925                .clone();
926            let out_property = self_property.map(move |commit| {
927                index
928                    .get(commit.id())
929                    .iter()
930                    .filter(|commit_ref| commit_ref.is_remote())
931                    .cloned()
932                    .collect_vec()
933            });
934            Ok(out_property.into_dyn_wrapped())
935        },
936    );
937    map.insert(
938        "tags",
939        |language, _diagnostics, _build_ctx, self_property, function| {
940            function.expect_no_arguments()?;
941            let index = language.keyword_cache.tags_index(language.repo).clone();
942            let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec());
943            Ok(out_property.into_dyn_wrapped())
944        },
945    );
946    map.insert(
947        "git_refs",
948        |language, _diagnostics, _build_ctx, self_property, function| {
949            function.expect_no_arguments()?;
950            let index = language.keyword_cache.git_refs_index(language.repo).clone();
951            let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec());
952            Ok(out_property.into_dyn_wrapped())
953        },
954    );
955    map.insert(
956        "git_head",
957        |language, _diagnostics, _build_ctx, self_property, function| {
958            function.expect_no_arguments()?;
959            let repo = language.repo;
960            let out_property = self_property.map(|commit| {
961                let target = repo.view().git_head();
962                target.added_ids().contains(commit.id())
963            });
964            Ok(out_property.into_dyn_wrapped())
965        },
966    );
967    map.insert(
968        "divergent",
969        |language, _diagnostics, _build_ctx, self_property, function| {
970            function.expect_no_arguments()?;
971            let repo = language.repo;
972            let out_property = self_property.map(|commit| {
973                // The given commit could be hidden in e.g. `jj evolog`.
974                let maybe_entries = repo.resolve_change_id(commit.change_id());
975                maybe_entries.map_or(0, |entries| entries.len()) > 1
976            });
977            Ok(out_property.into_dyn_wrapped())
978        },
979    );
980    map.insert(
981        "hidden",
982        |language, _diagnostics, _build_ctx, self_property, function| {
983            function.expect_no_arguments()?;
984            let repo = language.repo;
985            let out_property = self_property.map(|commit| commit.is_hidden(repo));
986            Ok(out_property.into_dyn_wrapped())
987        },
988    );
989    map.insert(
990        "immutable",
991        |language, _diagnostics, _build_ctx, self_property, function| {
992            function.expect_no_arguments()?;
993            let is_immutable = language
994                .keyword_cache
995                .is_immutable_fn(language, function.name_span)?
996                .clone();
997            let out_property = self_property.and_then(move |commit| Ok(is_immutable(commit.id())?));
998            Ok(out_property.into_dyn_wrapped())
999        },
1000    );
1001    map.insert(
1002        "contained_in",
1003        |language, diagnostics, _build_ctx, self_property, function| {
1004            let [revset_node] = function.expect_exact_arguments()?;
1005
1006            let is_contained =
1007                template_parser::expect_string_literal_with(revset_node, |revset, span| {
1008                    Ok(evaluate_user_revset(language, diagnostics, span, revset)?.containing_fn())
1009                })?;
1010
1011            let out_property = self_property.and_then(move |commit| Ok(is_contained(commit.id())?));
1012            Ok(out_property.into_dyn_wrapped())
1013        },
1014    );
1015    map.insert(
1016        "conflict",
1017        |_language, _diagnostics, _build_ctx, self_property, function| {
1018            function.expect_no_arguments()?;
1019            let out_property = self_property.and_then(|commit| Ok(commit.has_conflict()?));
1020            Ok(out_property.into_dyn_wrapped())
1021        },
1022    );
1023    map.insert(
1024        "empty",
1025        |language, _diagnostics, _build_ctx, self_property, function| {
1026            function.expect_no_arguments()?;
1027            let repo = language.repo;
1028            let out_property = self_property.and_then(|commit| Ok(commit.is_empty(repo)?));
1029            Ok(out_property.into_dyn_wrapped())
1030        },
1031    );
1032    map.insert(
1033        "diff",
1034        |language, diagnostics, _build_ctx, self_property, function| {
1035            let ([], [files_node]) = function.expect_arguments()?;
1036            let files = if let Some(node) = files_node {
1037                expect_fileset_literal(diagnostics, node, language.path_converter)?
1038            } else {
1039                // TODO: defaults to CLI path arguments?
1040                // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731
1041                FilesetExpression::all()
1042            };
1043            let repo = language.repo;
1044            let matcher: Rc<dyn Matcher> = files.to_matcher().into();
1045            let out_property = self_property
1046                .and_then(move |commit| Ok(TreeDiff::from_commit(repo, &commit, matcher.clone())?));
1047            Ok(out_property.into_dyn_wrapped())
1048        },
1049    );
1050    map.insert(
1051        "root",
1052        |language, _diagnostics, _build_ctx, self_property, function| {
1053            function.expect_no_arguments()?;
1054            let repo = language.repo;
1055            let out_property =
1056                self_property.map(|commit| commit.id() == repo.store().root_commit_id());
1057            Ok(out_property.into_dyn_wrapped())
1058        },
1059    );
1060    map
1061}
1062
1063// TODO: return Vec<String>
1064fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String {
1065    let wc_commit_ids = repo.view().wc_commit_ids();
1066    if wc_commit_ids.len() <= 1 {
1067        return "".to_string();
1068    }
1069    let mut names = vec![];
1070    for (name, wc_commit_id) in wc_commit_ids {
1071        if wc_commit_id == commit.id() {
1072            names.push(format!("{}@", name.as_symbol()));
1073        }
1074    }
1075    names.join(" ")
1076}
1077
1078fn expect_fileset_literal(
1079    diagnostics: &mut TemplateDiagnostics,
1080    node: &ExpressionNode,
1081    path_converter: &RepoPathUiConverter,
1082) -> Result<FilesetExpression, TemplateParseError> {
1083    template_parser::expect_string_literal_with(node, |text, span| {
1084        let mut inner_diagnostics = FilesetDiagnostics::new();
1085        let expression =
1086            fileset::parse(&mut inner_diagnostics, text, path_converter).map_err(|err| {
1087                TemplateParseError::expression("In fileset expression", span).with_source(err)
1088            })?;
1089        diagnostics.extend_with(inner_diagnostics, |diag| {
1090            TemplateParseError::expression("In fileset expression", span).with_source(diag)
1091        });
1092        Ok(expression)
1093    })
1094}
1095
1096fn evaluate_revset_expression<'repo>(
1097    language: &CommitTemplateLanguage<'repo>,
1098    span: pest::Span<'_>,
1099    expression: &UserRevsetExpression,
1100) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
1101    let make_error = || TemplateParseError::expression("Failed to evaluate revset", span);
1102    let repo = language.repo;
1103    let symbol_resolver = revset_util::default_symbol_resolver(
1104        repo,
1105        language.revset_parse_context.extensions.symbol_resolvers(),
1106        language.id_prefix_context,
1107    );
1108    let revset = expression
1109        .resolve_user_expression(repo, &symbol_resolver)
1110        .map_err(|err| make_error().with_source(err))?
1111        .evaluate(repo)
1112        .map_err(|err| make_error().with_source(err))?;
1113    Ok(revset)
1114}
1115
1116fn evaluate_user_revset<'repo>(
1117    language: &CommitTemplateLanguage<'repo>,
1118    diagnostics: &mut TemplateDiagnostics,
1119    span: pest::Span<'_>,
1120    revset: &str,
1121) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
1122    let mut inner_diagnostics = RevsetDiagnostics::new();
1123    let (expression, modifier) = revset::parse_with_modifier(
1124        &mut inner_diagnostics,
1125        revset,
1126        &language.revset_parse_context,
1127    )
1128    .map_err(|err| TemplateParseError::expression("In revset expression", span).with_source(err))?;
1129    diagnostics.extend_with(inner_diagnostics, |diag| {
1130        TemplateParseError::expression("In revset expression", span).with_source(diag)
1131    });
1132    let (None | Some(RevsetModifier::All)) = modifier;
1133
1134    evaluate_revset_expression(language, span, &expression)
1135}
1136
1137/// Bookmark or tag name with metadata.
1138#[derive(Debug)]
1139pub struct CommitRef {
1140    // Not using Ref/GitRef/RemoteName types here because it would be overly
1141    // complex to generalize the name type as T: RefName|GitRefName.
1142    /// Local name.
1143    name: RefSymbolBuf,
1144    /// Remote name if this is a remote or Git-tracking ref.
1145    remote: Option<RefSymbolBuf>,
1146    /// Target commit ids.
1147    target: RefTarget,
1148    /// Local ref metadata which tracks this remote ref.
1149    tracking_ref: Option<TrackingRef>,
1150    /// Local ref is synchronized with all tracking remotes, or tracking remote
1151    /// ref is synchronized with the local.
1152    synced: bool,
1153}
1154
1155#[derive(Debug)]
1156struct TrackingRef {
1157    /// Local ref target which tracks the other remote ref.
1158    target: RefTarget,
1159    /// Number of commits ahead of the tracking `target`.
1160    ahead_count: OnceCell<SizeHint>,
1161    /// Number of commits behind of the tracking `target`.
1162    behind_count: OnceCell<SizeHint>,
1163}
1164
1165impl CommitRef {
1166    // CommitRef is wrapped by Rc<T> to make it cheaply cloned and share
1167    // lazy-evaluation results across clones.
1168
1169    /// Creates local ref representation which might track some of the
1170    /// `remote_refs`.
1171    pub fn local<'a>(
1172        name: impl Into<String>,
1173        target: RefTarget,
1174        remote_refs: impl IntoIterator<Item = &'a RemoteRef>,
1175    ) -> Rc<Self> {
1176        let synced = remote_refs
1177            .into_iter()
1178            .all(|remote_ref| !remote_ref.is_tracked() || remote_ref.target == target);
1179        Rc::new(CommitRef {
1180            name: RefSymbolBuf(name.into()),
1181            remote: None,
1182            target,
1183            tracking_ref: None,
1184            synced,
1185        })
1186    }
1187
1188    /// Creates local ref representation which doesn't track any remote refs.
1189    pub fn local_only(name: impl Into<String>, target: RefTarget) -> Rc<Self> {
1190        Self::local(name, target, [])
1191    }
1192
1193    /// Creates remote ref representation which might be tracked by a local ref
1194    /// pointing to the `local_target`.
1195    pub fn remote(
1196        name: impl Into<String>,
1197        remote_name: impl Into<String>,
1198        remote_ref: RemoteRef,
1199        local_target: &RefTarget,
1200    ) -> Rc<Self> {
1201        let synced = remote_ref.is_tracked() && remote_ref.target == *local_target;
1202        let tracking_ref = remote_ref.is_tracked().then(|| {
1203            let count = if synced {
1204                OnceCell::from((0, Some(0))) // fast path for synced remotes
1205            } else {
1206                OnceCell::new()
1207            };
1208            TrackingRef {
1209                target: local_target.clone(),
1210                ahead_count: count.clone(),
1211                behind_count: count,
1212            }
1213        });
1214        Rc::new(CommitRef {
1215            name: RefSymbolBuf(name.into()),
1216            remote: Some(RefSymbolBuf(remote_name.into())),
1217            target: remote_ref.target,
1218            tracking_ref,
1219            synced,
1220        })
1221    }
1222
1223    /// Creates remote ref representation which isn't tracked by a local ref.
1224    pub fn remote_only(
1225        name: impl Into<String>,
1226        remote_name: impl Into<String>,
1227        target: RefTarget,
1228    ) -> Rc<Self> {
1229        Rc::new(CommitRef {
1230            name: RefSymbolBuf(name.into()),
1231            remote: Some(RefSymbolBuf(remote_name.into())),
1232            target,
1233            tracking_ref: None,
1234            synced: false, // has no local counterpart
1235        })
1236    }
1237
1238    /// Local name.
1239    pub fn name(&self) -> &str {
1240        self.name.as_ref()
1241    }
1242
1243    /// Remote name if this is a remote or Git-tracking ref.
1244    pub fn remote_name(&self) -> Option<&str> {
1245        self.remote.as_ref().map(AsRef::as_ref)
1246    }
1247
1248    /// Target commit ids.
1249    pub fn target(&self) -> &RefTarget {
1250        &self.target
1251    }
1252
1253    /// Returns true if this is a local ref.
1254    pub fn is_local(&self) -> bool {
1255        self.remote.is_none()
1256    }
1257
1258    /// Returns true if this is a remote ref.
1259    pub fn is_remote(&self) -> bool {
1260        self.remote.is_some()
1261    }
1262
1263    /// Returns true if this ref points to no commit.
1264    pub fn is_absent(&self) -> bool {
1265        self.target.is_absent()
1266    }
1267
1268    /// Returns true if this ref points to any commit.
1269    pub fn is_present(&self) -> bool {
1270        self.target.is_present()
1271    }
1272
1273    /// Whether the ref target has conflicts.
1274    pub fn has_conflict(&self) -> bool {
1275        self.target.has_conflict()
1276    }
1277
1278    /// Returns true if this ref is tracked by a local ref. The local ref might
1279    /// have been deleted (but not pushed yet.)
1280    pub fn is_tracked(&self) -> bool {
1281        self.tracking_ref.is_some()
1282    }
1283
1284    /// Returns true if this ref is tracked by a local ref, and if the local ref
1285    /// is present.
1286    pub fn is_tracking_present(&self) -> bool {
1287        self.tracking_ref
1288            .as_ref()
1289            .is_some_and(|tracking| tracking.target.is_present())
1290    }
1291
1292    /// Number of commits ahead of the tracking local ref.
1293    fn tracking_ahead_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1294        let Some(tracking) = &self.tracking_ref else {
1295            return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1296        };
1297        tracking
1298            .ahead_count
1299            .get_or_try_init(|| {
1300                let self_ids = self.target.added_ids().cloned().collect_vec();
1301                let other_ids = tracking.target.added_ids().cloned().collect_vec();
1302                Ok(revset::walk_revs(repo, &self_ids, &other_ids)?.count_estimate()?)
1303            })
1304            .copied()
1305    }
1306
1307    /// Number of commits behind of the tracking local ref.
1308    fn tracking_behind_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1309        let Some(tracking) = &self.tracking_ref else {
1310            return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1311        };
1312        tracking
1313            .behind_count
1314            .get_or_try_init(|| {
1315                let self_ids = self.target.added_ids().cloned().collect_vec();
1316                let other_ids = tracking.target.added_ids().cloned().collect_vec();
1317                Ok(revset::walk_revs(repo, &other_ids, &self_ids)?.count_estimate()?)
1318            })
1319            .copied()
1320    }
1321}
1322
1323// If wrapping with Rc<T> becomes common, add generic impl for Rc<T>.
1324impl Template for Rc<CommitRef> {
1325    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1326        write!(formatter.labeled("name"), "{}", self.name)?;
1327        if let Some(remote) = &self.remote {
1328            write!(formatter, "@")?;
1329            write!(formatter.labeled("remote"), "{remote}")?;
1330        }
1331        // Don't show both conflict and unsynced sigils as conflicted ref wouldn't
1332        // be pushed.
1333        if self.has_conflict() {
1334            write!(formatter, "??")?;
1335        } else if self.is_local() && !self.synced {
1336            write!(formatter, "*")?;
1337        }
1338        Ok(())
1339    }
1340}
1341
1342impl Template for Vec<Rc<CommitRef>> {
1343    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1344        templater::format_joined(formatter, self, " ")
1345    }
1346}
1347
1348fn builtin_commit_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>> {
1349    // Not using maplit::hashmap!{} or custom declarative macro here because
1350    // code completion inside macro is quite restricted.
1351    let mut map = CommitTemplateBuildMethodFnMap::<Rc<CommitRef>>::new();
1352    map.insert(
1353        "name",
1354        |_language, _diagnostics, _build_ctx, self_property, function| {
1355            function.expect_no_arguments()?;
1356            let out_property = self_property.map(|commit_ref| commit_ref.name.clone());
1357            Ok(out_property.into_dyn_wrapped())
1358        },
1359    );
1360    map.insert(
1361        "remote",
1362        |_language, _diagnostics, _build_ctx, self_property, function| {
1363            function.expect_no_arguments()?;
1364            let out_property = self_property.map(|commit_ref| commit_ref.remote.clone());
1365            Ok(out_property.into_dyn_wrapped())
1366        },
1367    );
1368    map.insert(
1369        "present",
1370        |_language, _diagnostics, _build_ctx, self_property, function| {
1371            function.expect_no_arguments()?;
1372            let out_property = self_property.map(|commit_ref| commit_ref.is_present());
1373            Ok(out_property.into_dyn_wrapped())
1374        },
1375    );
1376    map.insert(
1377        "conflict",
1378        |_language, _diagnostics, _build_ctx, self_property, function| {
1379            function.expect_no_arguments()?;
1380            let out_property = self_property.map(|commit_ref| commit_ref.has_conflict());
1381            Ok(out_property.into_dyn_wrapped())
1382        },
1383    );
1384    map.insert(
1385        "normal_target",
1386        |language, _diagnostics, _build_ctx, self_property, function| {
1387            function.expect_no_arguments()?;
1388            let repo = language.repo;
1389            let out_property = self_property.and_then(|commit_ref| {
1390                let maybe_id = commit_ref.target.as_normal();
1391                Ok(maybe_id.map(|id| repo.store().get_commit(id)).transpose()?)
1392            });
1393            Ok(out_property.into_dyn_wrapped())
1394        },
1395    );
1396    map.insert(
1397        "removed_targets",
1398        |language, _diagnostics, _build_ctx, self_property, function| {
1399            function.expect_no_arguments()?;
1400            let repo = language.repo;
1401            let out_property = self_property.and_then(|commit_ref| {
1402                let ids = commit_ref.target.removed_ids();
1403                let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?;
1404                Ok(commits)
1405            });
1406            Ok(out_property.into_dyn_wrapped())
1407        },
1408    );
1409    map.insert(
1410        "added_targets",
1411        |language, _diagnostics, _build_ctx, self_property, function| {
1412            function.expect_no_arguments()?;
1413            let repo = language.repo;
1414            let out_property = self_property.and_then(|commit_ref| {
1415                let ids = commit_ref.target.added_ids();
1416                let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?;
1417                Ok(commits)
1418            });
1419            Ok(out_property.into_dyn_wrapped())
1420        },
1421    );
1422    map.insert(
1423        "tracked",
1424        |_language, _diagnostics, _build_ctx, self_property, function| {
1425            function.expect_no_arguments()?;
1426            let out_property = self_property.map(|commit_ref| commit_ref.is_tracked());
1427            Ok(out_property.into_dyn_wrapped())
1428        },
1429    );
1430    map.insert(
1431        "tracking_present",
1432        |_language, _diagnostics, _build_ctx, self_property, function| {
1433            function.expect_no_arguments()?;
1434            let out_property = self_property.map(|commit_ref| commit_ref.is_tracking_present());
1435            Ok(out_property.into_dyn_wrapped())
1436        },
1437    );
1438    map.insert(
1439        "tracking_ahead_count",
1440        |language, _diagnostics, _build_ctx, self_property, function| {
1441            function.expect_no_arguments()?;
1442            let repo = language.repo;
1443            let out_property =
1444                self_property.and_then(|commit_ref| commit_ref.tracking_ahead_count(repo));
1445            Ok(out_property.into_dyn_wrapped())
1446        },
1447    );
1448    map.insert(
1449        "tracking_behind_count",
1450        |language, _diagnostics, _build_ctx, self_property, function| {
1451            function.expect_no_arguments()?;
1452            let repo = language.repo;
1453            let out_property =
1454                self_property.and_then(|commit_ref| commit_ref.tracking_behind_count(repo));
1455            Ok(out_property.into_dyn_wrapped())
1456        },
1457    );
1458    map
1459}
1460
1461/// Cache for reverse lookup refs.
1462#[derive(Clone, Debug, Default)]
1463pub struct CommitRefsIndex {
1464    index: HashMap<CommitId, Vec<Rc<CommitRef>>>,
1465}
1466
1467impl CommitRefsIndex {
1468    fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: Rc<CommitRef>) {
1469        for id in ids {
1470            let commit_refs = self.index.entry(id.clone()).or_default();
1471            commit_refs.push(name.clone());
1472        }
1473    }
1474
1475    pub fn get(&self, id: &CommitId) -> &[Rc<CommitRef>] {
1476        self.index.get(id).map_or(&[], |refs: &Vec<_>| refs)
1477    }
1478}
1479
1480fn build_bookmarks_index(repo: &dyn Repo) -> CommitRefsIndex {
1481    let mut index = CommitRefsIndex::default();
1482    for (bookmark_name, bookmark_target) in repo.view().bookmarks() {
1483        let local_target = bookmark_target.local_target;
1484        let remote_refs = bookmark_target.remote_refs;
1485        if local_target.is_present() {
1486            let commit_ref = CommitRef::local(
1487                bookmark_name,
1488                local_target.clone(),
1489                remote_refs.iter().map(|&(_, remote_ref)| remote_ref),
1490            );
1491            index.insert(local_target.added_ids(), commit_ref);
1492        }
1493        for &(remote_name, remote_ref) in &remote_refs {
1494            let commit_ref =
1495                CommitRef::remote(bookmark_name, remote_name, remote_ref.clone(), local_target);
1496            index.insert(remote_ref.target.added_ids(), commit_ref);
1497        }
1498    }
1499    index
1500}
1501
1502fn build_commit_refs_index<'a, K: Into<String>>(
1503    ref_pairs: impl IntoIterator<Item = (K, &'a RefTarget)>,
1504) -> CommitRefsIndex {
1505    let mut index = CommitRefsIndex::default();
1506    for (name, target) in ref_pairs {
1507        let commit_ref = CommitRef::local_only(name, target.clone());
1508        index.insert(target.added_ids(), commit_ref);
1509    }
1510    index
1511}
1512
1513/// Wrapper to render ref/remote name in revset syntax.
1514#[derive(Clone, Debug, Eq, PartialEq)]
1515pub struct RefSymbolBuf(String);
1516
1517impl AsRef<str> for RefSymbolBuf {
1518    fn as_ref(&self) -> &str {
1519        &self.0
1520    }
1521}
1522
1523impl Display for RefSymbolBuf {
1524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1525        f.pad(&revset::format_symbol(&self.0))
1526    }
1527}
1528
1529impl Template for RefSymbolBuf {
1530    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1531        write!(formatter, "{self}")
1532    }
1533}
1534
1535impl Template for RepoPathBuf {
1536    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1537        write!(formatter, "{}", self.as_internal_file_string())
1538    }
1539}
1540
1541fn builtin_repo_path_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf> {
1542    // Not using maplit::hashmap!{} or custom declarative macro here because
1543    // code completion inside macro is quite restricted.
1544    let mut map = CommitTemplateBuildMethodFnMap::<RepoPathBuf>::new();
1545    map.insert(
1546        "display",
1547        |language, _diagnostics, _build_ctx, self_property, function| {
1548            function.expect_no_arguments()?;
1549            let path_converter = language.path_converter;
1550            let out_property = self_property.map(|path| path_converter.format_file_path(&path));
1551            Ok(out_property.into_dyn_wrapped())
1552        },
1553    );
1554    map.insert(
1555        "parent",
1556        |_language, _diagnostics, _build_ctx, self_property, function| {
1557            function.expect_no_arguments()?;
1558            let out_property = self_property.map(|path| Some(path.parent()?.to_owned()));
1559            Ok(out_property.into_dyn_wrapped())
1560        },
1561    );
1562    map
1563}
1564
1565trait ShortestIdPrefixLen {
1566    fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize;
1567}
1568
1569impl ShortestIdPrefixLen for ChangeId {
1570    fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize {
1571        index.shortest_change_prefix_len(repo, self)
1572    }
1573}
1574
1575impl Template for ChangeId {
1576    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1577        write!(formatter, "{self}")
1578    }
1579}
1580
1581fn builtin_change_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, ChangeId> {
1582    let mut map = builtin_commit_or_change_id_methods::<ChangeId>();
1583    map.insert(
1584        "normal_hex",
1585        |_language, _diagnostics, _build_ctx, self_property, function| {
1586            function.expect_no_arguments()?;
1587            // Note: this is _not_ the same as id.to_string(), which returns the
1588            // "reverse" hex (z-k), instead of the "forward" / normal hex
1589            // (0-9a-f) we want here.
1590            let out_property = self_property.map(|id| id.hex());
1591            Ok(out_property.into_dyn_wrapped())
1592        },
1593    );
1594    map
1595}
1596
1597impl ShortestIdPrefixLen for CommitId {
1598    fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize {
1599        index.shortest_commit_prefix_len(repo, self)
1600    }
1601}
1602
1603impl Template for CommitId {
1604    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1605        write!(formatter, "{self}")
1606    }
1607}
1608
1609fn builtin_commit_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, CommitId> {
1610    let mut map = builtin_commit_or_change_id_methods::<CommitId>();
1611    // TODO: Remove in jj 0.36+
1612    map.insert(
1613        "normal_hex",
1614        |_language, diagnostics, _build_ctx, self_property, function| {
1615            diagnostics.add_warning(TemplateParseError::expression(
1616                "commit_id.normal_hex() is deprecated; use stringify(commit_id) instead",
1617                function.name_span,
1618            ));
1619            function.expect_no_arguments()?;
1620            let out_property = self_property.map(|id| id.hex());
1621            Ok(out_property.into_dyn_wrapped())
1622        },
1623    );
1624    map
1625}
1626
1627fn builtin_commit_or_change_id_methods<'repo, O>() -> CommitTemplateBuildMethodFnMap<'repo, O>
1628where
1629    O: Display + ShortestIdPrefixLen + 'repo,
1630{
1631    // Not using maplit::hashmap!{} or custom declarative macro here because
1632    // code completion inside macro is quite restricted.
1633    let mut map = CommitTemplateBuildMethodFnMap::<O>::new();
1634    map.insert(
1635        "short",
1636        |language, diagnostics, build_ctx, self_property, function| {
1637            let ([], [len_node]) = function.expect_arguments()?;
1638            let len_property = len_node
1639                .map(|node| {
1640                    template_builder::expect_usize_expression(
1641                        language,
1642                        diagnostics,
1643                        build_ctx,
1644                        node,
1645                    )
1646                })
1647                .transpose()?;
1648            let out_property = (self_property, len_property)
1649                .map(|(id, len)| format!("{id:.len$}", len = len.unwrap_or(12)));
1650            Ok(out_property.into_dyn_wrapped())
1651        },
1652    );
1653    map.insert(
1654        "shortest",
1655        |language, diagnostics, build_ctx, self_property, function| {
1656            let ([], [len_node]) = function.expect_arguments()?;
1657            let len_property = len_node
1658                .map(|node| {
1659                    template_builder::expect_usize_expression(
1660                        language,
1661                        diagnostics,
1662                        build_ctx,
1663                        node,
1664                    )
1665                })
1666                .transpose()?;
1667            let repo = language.repo;
1668            let index = match language.id_prefix_context.populate(repo) {
1669                Ok(index) => index,
1670                Err(err) => {
1671                    // Not an error because we can still produce somewhat
1672                    // reasonable output.
1673                    diagnostics.add_warning(
1674                        TemplateParseError::expression(
1675                            "Failed to load short-prefixes index",
1676                            function.name_span,
1677                        )
1678                        .with_source(err),
1679                    );
1680                    IdPrefixIndex::empty()
1681                }
1682            };
1683            // The length of the id printed will be the maximum of the minimum
1684            // `len` and the length of the shortest unique prefix.
1685            let out_property = (self_property, len_property).map(move |(id, len)| {
1686                let prefix_len = id.shortest_prefix_len(repo, &index);
1687                let mut hex = format!("{id:.len$}", len = max(prefix_len, len.unwrap_or(0)));
1688                let rest = hex.split_off(prefix_len);
1689                ShortestIdPrefix { prefix: hex, rest }
1690            });
1691            Ok(out_property.into_dyn_wrapped())
1692        },
1693    );
1694    map
1695}
1696
1697pub struct ShortestIdPrefix {
1698    pub prefix: String,
1699    pub rest: String,
1700}
1701
1702impl Template for ShortestIdPrefix {
1703    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1704        write!(formatter.labeled("prefix"), "{}", self.prefix)?;
1705        write!(formatter.labeled("rest"), "{}", self.rest)?;
1706        Ok(())
1707    }
1708}
1709
1710impl ShortestIdPrefix {
1711    fn to_upper(&self) -> Self {
1712        Self {
1713            prefix: self.prefix.to_ascii_uppercase(),
1714            rest: self.rest.to_ascii_uppercase(),
1715        }
1716    }
1717    fn to_lower(&self) -> Self {
1718        Self {
1719            prefix: self.prefix.to_ascii_lowercase(),
1720            rest: self.rest.to_ascii_lowercase(),
1721        }
1722    }
1723}
1724
1725fn builtin_shortest_id_prefix_methods<'repo>(
1726) -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> {
1727    // Not using maplit::hashmap!{} or custom declarative macro here because
1728    // code completion inside macro is quite restricted.
1729    let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new();
1730    map.insert(
1731        "prefix",
1732        |_language, _diagnostics, _build_ctx, self_property, function| {
1733            function.expect_no_arguments()?;
1734            let out_property = self_property.map(|id| id.prefix);
1735            Ok(out_property.into_dyn_wrapped())
1736        },
1737    );
1738    map.insert(
1739        "rest",
1740        |_language, _diagnostics, _build_ctx, self_property, function| {
1741            function.expect_no_arguments()?;
1742            let out_property = self_property.map(|id| id.rest);
1743            Ok(out_property.into_dyn_wrapped())
1744        },
1745    );
1746    map.insert(
1747        "upper",
1748        |_language, _diagnostics, _build_ctx, self_property, function| {
1749            function.expect_no_arguments()?;
1750            let out_property = self_property.map(|id| id.to_upper());
1751            Ok(out_property.into_dyn_wrapped())
1752        },
1753    );
1754    map.insert(
1755        "lower",
1756        |_language, _diagnostics, _build_ctx, self_property, function| {
1757            function.expect_no_arguments()?;
1758            let out_property = self_property.map(|id| id.to_lower());
1759            Ok(out_property.into_dyn_wrapped())
1760        },
1761    );
1762    map
1763}
1764
1765/// Pair of trees to be diffed.
1766#[derive(Debug)]
1767pub struct TreeDiff {
1768    from_tree: MergedTree,
1769    to_tree: MergedTree,
1770    matcher: Rc<dyn Matcher>,
1771    copy_records: CopyRecords,
1772}
1773
1774impl TreeDiff {
1775    fn from_commit(
1776        repo: &dyn Repo,
1777        commit: &Commit,
1778        matcher: Rc<dyn Matcher>,
1779    ) -> BackendResult<Self> {
1780        let mut copy_records = CopyRecords::default();
1781        for parent in commit.parent_ids() {
1782            let records =
1783                diff_util::get_copy_records(repo.store(), parent, commit.id(), &*matcher)?;
1784            copy_records.add_records(records)?;
1785        }
1786        Ok(TreeDiff {
1787            from_tree: commit.parent_tree(repo)?,
1788            to_tree: commit.tree()?,
1789            matcher,
1790            copy_records,
1791        })
1792    }
1793
1794    fn diff_stream(&self) -> BoxStream<'_, CopiesTreeDiffEntry> {
1795        self.from_tree
1796            .diff_stream_with_copies(&self.to_tree, &*self.matcher, &self.copy_records)
1797    }
1798
1799    async fn collect_entries(&self) -> BackendResult<Vec<TreeDiffEntry>> {
1800        self.diff_stream()
1801            .map(TreeDiffEntry::from_backend_entry_with_copies)
1802            .try_collect()
1803            .await
1804    }
1805
1806    fn into_formatted<F, E>(self, show: F) -> TreeDiffFormatted<F>
1807    where
1808        F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>,
1809        E: Into<TemplatePropertyError>,
1810    {
1811        TreeDiffFormatted { diff: self, show }
1812    }
1813}
1814
1815/// Tree diff to be rendered by predefined function `F`.
1816struct TreeDiffFormatted<F> {
1817    diff: TreeDiff,
1818    show: F,
1819}
1820
1821impl<F, E> Template for TreeDiffFormatted<F>
1822where
1823    F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>,
1824    E: Into<TemplatePropertyError>,
1825{
1826    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1827        let show = &self.show;
1828        let store = self.diff.from_tree.store();
1829        let tree_diff = self.diff.diff_stream();
1830        show(formatter.as_mut(), store, tree_diff).or_else(|err| formatter.handle_error(err.into()))
1831    }
1832}
1833
1834fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiff> {
1835    type P<'repo> = CommitTemplatePropertyKind<'repo>;
1836    // Not using maplit::hashmap!{} or custom declarative macro here because
1837    // code completion inside macro is quite restricted.
1838    let mut map = CommitTemplateBuildMethodFnMap::<TreeDiff>::new();
1839    map.insert(
1840        "files",
1841        |_language, _diagnostics, _build_ctx, self_property, function| {
1842            function.expect_no_arguments()?;
1843            // TODO: cache and reuse diff entries within the current evaluation?
1844            let out_property =
1845                self_property.and_then(|diff| Ok(diff.collect_entries().block_on()?));
1846            Ok(out_property.into_dyn_wrapped())
1847        },
1848    );
1849    map.insert(
1850        "color_words",
1851        |language, diagnostics, build_ctx, self_property, function| {
1852            let ([], [context_node]) = function.expect_arguments()?;
1853            let context_property = context_node
1854                .map(|node| {
1855                    template_builder::expect_usize_expression(
1856                        language,
1857                        diagnostics,
1858                        build_ctx,
1859                        node,
1860                    )
1861                })
1862                .transpose()?;
1863            let path_converter = language.path_converter;
1864            let options = diff_util::ColorWordsDiffOptions::from_settings(language.settings())
1865                .map_err(|err| {
1866                    let message = "Failed to load diff settings";
1867                    TemplateParseError::expression(message, function.name_span).with_source(err)
1868                })?;
1869            let conflict_marker_style = language.conflict_marker_style;
1870            let template = (self_property, context_property)
1871                .map(move |(diff, context)| {
1872                    let mut options = options.clone();
1873                    if let Some(context) = context {
1874                        options.context = context;
1875                    }
1876                    diff.into_formatted(move |formatter, store, tree_diff| {
1877                        diff_util::show_color_words_diff(
1878                            formatter,
1879                            store,
1880                            tree_diff,
1881                            path_converter,
1882                            &options,
1883                            conflict_marker_style,
1884                        )
1885                    })
1886                })
1887                .into_template();
1888            Ok(P::wrap_template(template))
1889        },
1890    );
1891    map.insert(
1892        "git",
1893        |language, diagnostics, build_ctx, self_property, function| {
1894            let ([], [context_node]) = function.expect_arguments()?;
1895            let context_property = context_node
1896                .map(|node| {
1897                    template_builder::expect_usize_expression(
1898                        language,
1899                        diagnostics,
1900                        build_ctx,
1901                        node,
1902                    )
1903                })
1904                .transpose()?;
1905            let options = diff_util::UnifiedDiffOptions::from_settings(language.settings())
1906                .map_err(|err| {
1907                    let message = "Failed to load diff settings";
1908                    TemplateParseError::expression(message, function.name_span).with_source(err)
1909                })?;
1910            let conflict_marker_style = language.conflict_marker_style;
1911            let template = (self_property, context_property)
1912                .map(move |(diff, context)| {
1913                    let mut options = options.clone();
1914                    if let Some(context) = context {
1915                        options.context = context;
1916                    }
1917                    diff.into_formatted(move |formatter, store, tree_diff| {
1918                        diff_util::show_git_diff(
1919                            formatter,
1920                            store,
1921                            tree_diff,
1922                            &options,
1923                            conflict_marker_style,
1924                        )
1925                    })
1926                })
1927                .into_template();
1928            Ok(P::wrap_template(template))
1929        },
1930    );
1931    map.insert(
1932        "stat",
1933        |language, diagnostics, build_ctx, self_property, function| {
1934            let ([], [width_node]) = function.expect_arguments()?;
1935            let width_property = width_node
1936                .map(|node| {
1937                    template_builder::expect_usize_expression(
1938                        language,
1939                        diagnostics,
1940                        build_ctx,
1941                        node,
1942                    )
1943                })
1944                .transpose()?;
1945            let path_converter = language.path_converter;
1946            // No user configuration exists for diff stat.
1947            let options = diff_util::DiffStatOptions::default();
1948            let conflict_marker_style = language.conflict_marker_style;
1949            // TODO: cache and reuse stats within the current evaluation?
1950            let out_property = (self_property, width_property).and_then(move |(diff, width)| {
1951                let store = diff.from_tree.store();
1952                let tree_diff = diff.diff_stream();
1953                let stats = DiffStats::calculate(store, tree_diff, &options, conflict_marker_style)
1954                    .block_on()?;
1955                Ok(DiffStatsFormatted {
1956                    stats,
1957                    path_converter,
1958                    // TODO: fall back to current available width
1959                    width: width.unwrap_or(80),
1960                })
1961            });
1962            Ok(out_property.into_dyn_wrapped())
1963        },
1964    );
1965    map.insert(
1966        "summary",
1967        |language, _diagnostics, _build_ctx, self_property, function| {
1968            function.expect_no_arguments()?;
1969            let path_converter = language.path_converter;
1970            let template = self_property
1971                .map(move |diff| {
1972                    diff.into_formatted(move |formatter, _store, tree_diff| {
1973                        diff_util::show_diff_summary(formatter, tree_diff, path_converter)
1974                    })
1975                })
1976                .into_template();
1977            Ok(P::wrap_template(template))
1978        },
1979    );
1980    // TODO: add support for external tools
1981    map
1982}
1983
1984/// [`MergedTree`] diff entry.
1985#[derive(Clone, Debug)]
1986pub struct TreeDiffEntry {
1987    pub path: CopiesTreeDiffEntryPath,
1988    pub source_value: MergedTreeValue,
1989    pub target_value: MergedTreeValue,
1990}
1991
1992impl TreeDiffEntry {
1993    fn from_backend_entry_with_copies(entry: CopiesTreeDiffEntry) -> BackendResult<Self> {
1994        let (source_value, target_value) = entry.values?;
1995        Ok(TreeDiffEntry {
1996            path: entry.path,
1997            source_value,
1998            target_value,
1999        })
2000    }
2001
2002    fn status_label(&self) -> &'static str {
2003        let (label, _sigil) = diff_util::diff_status_label_and_char(
2004            &self.path,
2005            &self.source_value,
2006            &self.target_value,
2007        );
2008        label
2009    }
2010
2011    fn into_source_entry(self) -> TreeEntry {
2012        TreeEntry {
2013            path: self.path.source.map_or(self.path.target, |(path, _)| path),
2014            value: self.source_value,
2015        }
2016    }
2017
2018    fn into_target_entry(self) -> TreeEntry {
2019        TreeEntry {
2020            path: self.path.target,
2021            value: self.target_value,
2022        }
2023    }
2024}
2025
2026fn builtin_tree_diff_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>
2027{
2028    // Not using maplit::hashmap!{} or custom declarative macro here because
2029    // code completion inside macro is quite restricted.
2030    let mut map = CommitTemplateBuildMethodFnMap::<TreeDiffEntry>::new();
2031    map.insert(
2032        "path",
2033        |_language, _diagnostics, _build_ctx, self_property, function| {
2034            function.expect_no_arguments()?;
2035            let out_property = self_property.map(|entry| entry.path.target);
2036            Ok(out_property.into_dyn_wrapped())
2037        },
2038    );
2039    map.insert(
2040        "status",
2041        |_language, _diagnostics, _build_ctx, self_property, function| {
2042            function.expect_no_arguments()?;
2043            let out_property = self_property.map(|entry| entry.status_label().to_owned());
2044            Ok(out_property.into_dyn_wrapped())
2045        },
2046    );
2047    // TODO: add status_code() or status_char()?
2048    map.insert(
2049        "source",
2050        |_language, _diagnostics, _build_ctx, self_property, function| {
2051            function.expect_no_arguments()?;
2052            let out_property = self_property.map(TreeDiffEntry::into_source_entry);
2053            Ok(out_property.into_dyn_wrapped())
2054        },
2055    );
2056    map.insert(
2057        "target",
2058        |_language, _diagnostics, _build_ctx, self_property, function| {
2059            function.expect_no_arguments()?;
2060            let out_property = self_property.map(TreeDiffEntry::into_target_entry);
2061            Ok(out_property.into_dyn_wrapped())
2062        },
2063    );
2064    map
2065}
2066
2067/// [`MergedTree`] entry.
2068#[derive(Clone, Debug)]
2069pub struct TreeEntry {
2070    pub path: RepoPathBuf,
2071    pub value: MergedTreeValue,
2072}
2073
2074fn builtin_tree_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeEntry> {
2075    // Not using maplit::hashmap!{} or custom declarative macro here because
2076    // code completion inside macro is quite restricted.
2077    let mut map = CommitTemplateBuildMethodFnMap::<TreeEntry>::new();
2078    map.insert(
2079        "path",
2080        |_language, _diagnostics, _build_ctx, self_property, function| {
2081            function.expect_no_arguments()?;
2082            let out_property = self_property.map(|entry| entry.path);
2083            Ok(out_property.into_dyn_wrapped())
2084        },
2085    );
2086    map.insert(
2087        "conflict",
2088        |_language, _diagnostics, _build_ctx, self_property, function| {
2089            function.expect_no_arguments()?;
2090            let out_property = self_property.map(|entry| !entry.value.is_resolved());
2091            Ok(out_property.into_dyn_wrapped())
2092        },
2093    );
2094    map.insert(
2095        "file_type",
2096        |_language, _diagnostics, _build_ctx, self_property, function| {
2097            function.expect_no_arguments()?;
2098            let out_property =
2099                self_property.map(|entry| describe_file_type(&entry.value).to_owned());
2100            Ok(out_property.into_dyn_wrapped())
2101        },
2102    );
2103    map.insert(
2104        "executable",
2105        |_language, _diagnostics, _build_ctx, self_property, function| {
2106            function.expect_no_arguments()?;
2107            let out_property =
2108                self_property.map(|entry| is_executable_file(&entry.value).unwrap_or_default());
2109            Ok(out_property.into_dyn_wrapped())
2110        },
2111    );
2112    map
2113}
2114
2115fn describe_file_type(value: &MergedTreeValue) -> &'static str {
2116    match value.as_resolved() {
2117        Some(Some(TreeValue::File { .. })) => "file",
2118        Some(Some(TreeValue::Symlink(_))) => "symlink",
2119        Some(Some(TreeValue::Tree(_))) => "tree",
2120        Some(Some(TreeValue::GitSubmodule(_))) => "git-submodule",
2121        Some(None) => "", // absent
2122        None | Some(Some(TreeValue::Conflict(_))) => "conflict",
2123    }
2124}
2125
2126fn is_executable_file(value: &MergedTreeValue) -> Option<bool> {
2127    let executable = value.to_executable_merge()?;
2128    conflicts::resolve_file_executable(&executable)
2129}
2130
2131/// [`DiffStats`] with rendering parameters.
2132#[derive(Clone, Debug)]
2133pub struct DiffStatsFormatted<'a> {
2134    stats: DiffStats,
2135    path_converter: &'a RepoPathUiConverter,
2136    width: usize,
2137}
2138
2139impl Template for DiffStatsFormatted<'_> {
2140    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2141        diff_util::show_diff_stats(
2142            formatter.as_mut(),
2143            &self.stats,
2144            self.path_converter,
2145            self.width,
2146        )
2147    }
2148}
2149
2150fn builtin_diff_stats_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStats> {
2151    // Not using maplit::hashmap!{} or custom declarative macro here because
2152    // code completion inside macro is quite restricted.
2153    let mut map = CommitTemplateBuildMethodFnMap::<DiffStats>::new();
2154    // TODO: add files() -> List<DiffStatEntry> ?
2155    map.insert(
2156        "total_added",
2157        |_language, _diagnostics, _build_ctx, self_property, function| {
2158            function.expect_no_arguments()?;
2159            let out_property =
2160                self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_added())?));
2161            Ok(out_property.into_dyn_wrapped())
2162        },
2163    );
2164    map.insert(
2165        "total_removed",
2166        |_language, _diagnostics, _build_ctx, self_property, function| {
2167            function.expect_no_arguments()?;
2168            let out_property =
2169                self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_removed())?));
2170            Ok(out_property.into_dyn_wrapped())
2171        },
2172    );
2173    map
2174}
2175
2176#[derive(Debug)]
2177pub struct CryptographicSignature {
2178    commit: Commit,
2179}
2180
2181impl CryptographicSignature {
2182    fn new(commit: Commit) -> Option<Self> {
2183        commit.is_signed().then_some(Self { commit })
2184    }
2185
2186    fn verify(&self) -> SignResult<Verification> {
2187        self.commit
2188            .verification()
2189            .transpose()
2190            .expect("must have signature")
2191    }
2192
2193    fn status(&self) -> SignResult<SigStatus> {
2194        self.verify().map(|verification| verification.status)
2195    }
2196
2197    /// Defaults to empty string if key is not present.
2198    fn key(&self) -> SignResult<String> {
2199        self.verify()
2200            .map(|verification| verification.key.unwrap_or_default())
2201    }
2202
2203    /// Defaults to empty string if display is not present.
2204    fn display(&self) -> SignResult<String> {
2205        self.verify()
2206            .map(|verification| verification.display.unwrap_or_default())
2207    }
2208}
2209
2210fn builtin_cryptographic_signature_methods<'repo>(
2211) -> CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature> {
2212    // Not using maplit::hashmap!{} or custom declarative macro here because
2213    // code completion inside macro is quite restricted.
2214    let mut map = CommitTemplateBuildMethodFnMap::<CryptographicSignature>::new();
2215    map.insert(
2216        "status",
2217        |_language, _diagnostics, _build_ctx, self_property, function| {
2218            function.expect_no_arguments()?;
2219            let out_property = self_property.and_then(|sig| match sig.status() {
2220                Ok(status) => Ok(status.to_string()),
2221                Err(SignError::InvalidSignatureFormat) => Ok("invalid".to_string()),
2222                Err(err) => Err(err.into()),
2223            });
2224            Ok(out_property.into_dyn_wrapped())
2225        },
2226    );
2227    map.insert(
2228        "key",
2229        |_language, _diagnostics, _build_ctx, self_property, function| {
2230            function.expect_no_arguments()?;
2231            let out_property = self_property.and_then(|sig| Ok(sig.key()?));
2232            Ok(out_property.into_dyn_wrapped())
2233        },
2234    );
2235    map.insert(
2236        "display",
2237        |_language, _diagnostics, _build_ctx, self_property, function| {
2238            function.expect_no_arguments()?;
2239            let out_property = self_property.and_then(|sig| Ok(sig.display()?));
2240            Ok(out_property.into_dyn_wrapped())
2241        },
2242    );
2243    map
2244}
2245
2246#[derive(Debug, Clone)]
2247pub struct AnnotationLine {
2248    pub commit: Commit,
2249    pub content: BString,
2250    pub line_number: usize,
2251    pub first_line_in_hunk: bool,
2252}
2253
2254fn builtin_annotation_line_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>
2255{
2256    type P<'repo> = CommitTemplatePropertyKind<'repo>;
2257    let mut map = CommitTemplateBuildMethodFnMap::<AnnotationLine>::new();
2258    map.insert(
2259        "commit",
2260        |_language, _diagnostics, _build_ctx, self_property, function| {
2261            function.expect_no_arguments()?;
2262            let out_property = self_property.map(|line| line.commit);
2263            Ok(out_property.into_dyn_wrapped())
2264        },
2265    );
2266    map.insert(
2267        "content",
2268        |_language, _diagnostics, _build_ctx, self_property, function| {
2269            function.expect_no_arguments()?;
2270            let out_property = self_property.map(|line| line.content);
2271            // TODO: Add Bytes or BString template type?
2272            Ok(P::wrap_template(out_property.into_template()))
2273        },
2274    );
2275    map.insert(
2276        "line_number",
2277        |_language, _diagnostics, _build_ctx, self_property, function| {
2278            function.expect_no_arguments()?;
2279            let out_property = self_property.and_then(|line| Ok(i64::try_from(line.line_number)?));
2280            Ok(out_property.into_dyn_wrapped())
2281        },
2282    );
2283    map.insert(
2284        "first_line_in_hunk",
2285        |_language, _diagnostics, _build_ctx, self_property, function| {
2286            function.expect_no_arguments()?;
2287            let out_property = self_property.map(|line| line.first_line_in_hunk);
2288            Ok(out_property.into_dyn_wrapped())
2289        },
2290    );
2291    map
2292}
2293
2294impl Template for Trailer {
2295    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2296        write!(formatter, "{}: {}", self.key, self.value)
2297    }
2298}
2299
2300impl Template for Vec<Trailer> {
2301    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2302        templater::format_joined(formatter, self, "\n")
2303    }
2304}
2305
2306fn builtin_trailer_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Trailer> {
2307    let mut map = CommitTemplateBuildMethodFnMap::<Trailer>::new();
2308    map.insert(
2309        "key",
2310        |_language, _diagnostics, _build_ctx, self_property, function| {
2311            function.expect_no_arguments()?;
2312            let out_property = self_property.map(|trailer| trailer.key);
2313            Ok(out_property.into_dyn_wrapped())
2314        },
2315    );
2316    map.insert(
2317        "value",
2318        |_language, _diagnostics, _build_ctx, self_property, function| {
2319            function.expect_no_arguments()?;
2320            let out_property = self_property.map(|trailer| trailer.value);
2321            Ok(out_property.into_dyn_wrapped())
2322        },
2323    );
2324    map
2325}
2326
2327fn builtin_trailer_list_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Vec<Trailer>> {
2328    let mut map: CommitTemplateBuildMethodFnMap<Vec<Trailer>> =
2329        template_builder::builtin_formattable_list_methods();
2330    map.insert(
2331        "contains_key",
2332        |language, diagnostics, build_ctx, self_property, function| {
2333            let [key_node] = function.expect_exact_arguments()?;
2334            let key_property =
2335                expect_plain_text_expression(language, diagnostics, build_ctx, key_node)?;
2336            let out_property = (self_property, key_property)
2337                .map(|(trailers, key)| trailers.iter().any(|t| t.key == key));
2338            Ok(out_property.into_dyn_wrapped())
2339        },
2340    );
2341    map
2342}
2343
2344#[cfg(test)]
2345mod tests {
2346    use std::path::Path;
2347    use std::sync::Arc;
2348
2349    use jj_lib::config::ConfigLayer;
2350    use jj_lib::config::ConfigSource;
2351    use jj_lib::revset::RevsetAliasesMap;
2352    use jj_lib::revset::RevsetExpression;
2353    use jj_lib::revset::RevsetExtensions;
2354    use jj_lib::revset::RevsetWorkspaceContext;
2355    use testutils::repo_path_buf;
2356    use testutils::TestRepoBackend;
2357    use testutils::TestWorkspace;
2358
2359    use super::*;
2360    use crate::formatter::PlainTextFormatter;
2361    use crate::template_parser::TemplateAliasesMap;
2362    use crate::templater::TemplateRenderer;
2363    use crate::templater::WrapTemplateProperty;
2364
2365    // TemplateBuildFunctionFn defined for<'a>
2366    type BuildFunctionFn = for<'a> fn(
2367        &CommitTemplateLanguage<'a>,
2368        &mut TemplateDiagnostics,
2369        &BuildContext<CommitTemplatePropertyKind<'a>>,
2370        &FunctionCallNode,
2371    ) -> TemplateParseResult<CommitTemplatePropertyKind<'a>>;
2372
2373    struct CommitTemplateTestEnv {
2374        test_workspace: TestWorkspace,
2375        path_converter: RepoPathUiConverter,
2376        revset_extensions: Arc<RevsetExtensions>,
2377        id_prefix_context: IdPrefixContext,
2378        revset_aliases_map: RevsetAliasesMap,
2379        template_aliases_map: TemplateAliasesMap,
2380        immutable_expression: Rc<UserRevsetExpression>,
2381        extra_functions: HashMap<&'static str, BuildFunctionFn>,
2382    }
2383
2384    impl CommitTemplateTestEnv {
2385        fn init() -> Self {
2386            // Stabilize commit id of the initialized working copy
2387            let settings = stable_settings();
2388            let test_workspace =
2389                TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &settings);
2390            let path_converter = RepoPathUiConverter::Fs {
2391                cwd: test_workspace.workspace.workspace_root().to_owned(),
2392                base: test_workspace.workspace.workspace_root().to_owned(),
2393            };
2394            // IdPrefixContext::new() expects Arc<RevsetExtensions>
2395            #[expect(clippy::arc_with_non_send_sync)]
2396            let revset_extensions = Arc::new(RevsetExtensions::new());
2397            let id_prefix_context = IdPrefixContext::new(revset_extensions.clone());
2398            CommitTemplateTestEnv {
2399                test_workspace,
2400                path_converter,
2401                revset_extensions,
2402                id_prefix_context,
2403                revset_aliases_map: RevsetAliasesMap::new(),
2404                template_aliases_map: TemplateAliasesMap::new(),
2405                immutable_expression: RevsetExpression::none(),
2406                extra_functions: HashMap::new(),
2407            }
2408        }
2409
2410        fn set_cwd(&mut self, path: impl AsRef<Path>) {
2411            self.path_converter = RepoPathUiConverter::Fs {
2412                cwd: self.test_workspace.workspace.workspace_root().join(path),
2413                base: self.test_workspace.workspace.workspace_root().to_owned(),
2414            };
2415        }
2416
2417        fn add_function(&mut self, name: &'static str, f: BuildFunctionFn) {
2418            self.extra_functions.insert(name, f);
2419        }
2420
2421        fn new_language(&self) -> CommitTemplateLanguage<'_> {
2422            let revset_parse_context = RevsetParseContext {
2423                aliases_map: &self.revset_aliases_map,
2424                local_variables: HashMap::new(),
2425                user_email: "test.user@example.com",
2426                date_pattern_context: chrono::DateTime::UNIX_EPOCH.fixed_offset().into(),
2427                extensions: &self.revset_extensions,
2428                workspace: Some(RevsetWorkspaceContext {
2429                    path_converter: &self.path_converter,
2430                    workspace_name: self.test_workspace.workspace.workspace_name(),
2431                }),
2432            };
2433            let mut language = CommitTemplateLanguage::new(
2434                self.test_workspace.repo.as_ref(),
2435                &self.path_converter,
2436                self.test_workspace.workspace.workspace_name(),
2437                revset_parse_context,
2438                &self.id_prefix_context,
2439                self.immutable_expression.clone(),
2440                ConflictMarkerStyle::default(),
2441                &[] as &[Box<dyn CommitTemplateLanguageExtension>],
2442            );
2443            // Not using .extend() to infer lifetime of f
2444            for (&name, &f) in &self.extra_functions {
2445                language.build_fn_table.core.functions.insert(name, f);
2446            }
2447            language
2448        }
2449
2450        fn parse<'a, C>(&'a self, text: &str) -> TemplateParseResult<TemplateRenderer<'a, C>>
2451        where
2452            C: Clone + 'a,
2453            CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>,
2454        {
2455            let language = self.new_language();
2456            let mut diagnostics = TemplateDiagnostics::new();
2457            template_builder::parse(
2458                &language,
2459                &mut diagnostics,
2460                text,
2461                &self.template_aliases_map,
2462            )
2463        }
2464
2465        fn render_ok<'a, C>(&'a self, text: &str, context: &C) -> String
2466        where
2467            C: Clone + 'a,
2468            CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>,
2469        {
2470            let template = self.parse(text).unwrap();
2471            let mut output = Vec::new();
2472            let mut formatter = PlainTextFormatter::new(&mut output);
2473            template.format(context, &mut formatter).unwrap();
2474            String::from_utf8(output).unwrap()
2475        }
2476    }
2477
2478    fn stable_settings() -> UserSettings {
2479        let mut config = testutils::base_user_config();
2480        let mut layer = ConfigLayer::empty(ConfigSource::User);
2481        layer
2482            .set_value("debug.commit-timestamp", "2001-02-03T04:05:06+07:00")
2483            .unwrap();
2484        config.add_layer(layer);
2485        UserSettings::from_config(config).unwrap()
2486    }
2487
2488    #[test]
2489    fn test_ref_symbol_type() {
2490        let mut env = CommitTemplateTestEnv::init();
2491        env.add_function("sym", |language, diagnostics, build_ctx, function| {
2492            let [value_node] = function.expect_exact_arguments()?;
2493            let value = expect_plain_text_expression(language, diagnostics, build_ctx, value_node)?;
2494            let out_property = value.map(RefSymbolBuf);
2495            Ok(out_property.into_dyn_wrapped())
2496        });
2497        let sym = |s: &str| RefSymbolBuf(s.to_owned());
2498
2499        // default formatting
2500        insta::assert_snapshot!(env.render_ok("self", &sym("")), @r#""""#);
2501        insta::assert_snapshot!(env.render_ok("self", &sym("foo")), @"foo");
2502        insta::assert_snapshot!(env.render_ok("self", &sym("foo bar")), @r#""foo bar""#);
2503
2504        // comparison
2505        insta::assert_snapshot!(env.render_ok("self == 'foo'", &sym("foo")), @"true");
2506        insta::assert_snapshot!(env.render_ok("'bar' == self", &sym("foo")), @"false");
2507        insta::assert_snapshot!(env.render_ok("self == self", &sym("foo")), @"true");
2508        insta::assert_snapshot!(env.render_ok("self == sym('bar')", &sym("foo")), @"false");
2509
2510        insta::assert_snapshot!(env.render_ok("self == 'bar'", &Some(sym("foo"))), @"false");
2511        insta::assert_snapshot!(env.render_ok("self == sym('foo')", &Some(sym("foo"))), @"true");
2512        insta::assert_snapshot!(env.render_ok("'foo' == self", &Some(sym("foo"))), @"true");
2513        insta::assert_snapshot!(env.render_ok("sym('bar') == self", &Some(sym("foo"))), @"false");
2514        insta::assert_snapshot!(env.render_ok("self == self", &Some(sym("foo"))), @"true");
2515        insta::assert_snapshot!(env.render_ok("self == ''", &None::<RefSymbolBuf>), @"false");
2516        insta::assert_snapshot!(env.render_ok("sym('') == self", &None::<RefSymbolBuf>), @"false");
2517        insta::assert_snapshot!(env.render_ok("self == self", &None::<RefSymbolBuf>), @"true");
2518
2519        // string cast != formatting: it would be weird if function argument of
2520        // string type were quoted/escaped. (e.g. `"foo".contains(bookmark)`)
2521        insta::assert_snapshot!(env.render_ok("stringify(self)", &sym("a b")), @"a b");
2522        insta::assert_snapshot!(env.render_ok("stringify(self)", &Some(sym("a b"))), @"a b");
2523        insta::assert_snapshot!(
2524            env.render_ok("stringify(self)", &None::<RefSymbolBuf>),
2525            @"<Error: No RefSymbol available>");
2526
2527        // string methods
2528        insta::assert_snapshot!(env.render_ok("self.len()", &sym("a b")), @"3");
2529    }
2530
2531    #[test]
2532    fn test_repo_path_type() {
2533        let mut env = CommitTemplateTestEnv::init();
2534        env.set_cwd("dir");
2535
2536        // slash-separated by default
2537        insta::assert_snapshot!(
2538            env.render_ok("self", &repo_path_buf("dir/file")), @"dir/file");
2539
2540        // .display() to convert to filesystem path
2541        insta::assert_snapshot!(
2542            env.render_ok("self.display()", &repo_path_buf("dir/file")), @"file");
2543        if cfg!(windows) {
2544            insta::assert_snapshot!(
2545                env.render_ok("self.display()", &repo_path_buf("file")), @"..\\file");
2546        } else {
2547            insta::assert_snapshot!(
2548                env.render_ok("self.display()", &repo_path_buf("file")), @"../file");
2549        }
2550
2551        let template = "if(self.parent(), self.parent(), '<none>')";
2552        insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("")), @"<none>");
2553        insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("file")), @"");
2554        insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("dir/file")), @"dir");
2555    }
2556
2557    #[test]
2558    fn test_commit_id_type() {
2559        let env = CommitTemplateTestEnv::init();
2560
2561        let id = CommitId::from_hex("08a70ab33d7143b7130ed8594d8216ef688623c0");
2562        insta::assert_snapshot!(
2563            env.render_ok("self", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2564        insta::assert_snapshot!(
2565            env.render_ok("self.normal_hex()", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2566
2567        insta::assert_snapshot!(env.render_ok("self.short()", &id), @"08a70ab33d71");
2568        insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @"");
2569        insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @"");
2570        insta::assert_snapshot!(
2571            env.render_ok("self.short(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2572        insta::assert_snapshot!(
2573            env.render_ok("self.short(-100)", &id),
2574            @"<Error: out of range integral type conversion attempted>");
2575
2576        insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"08");
2577        insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"08");
2578        insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"08");
2579        insta::assert_snapshot!(
2580            env.render_ok("self.shortest(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2581        insta::assert_snapshot!(
2582            env.render_ok("self.shortest(-100)", &id),
2583            @"<Error: out of range integral type conversion attempted>");
2584    }
2585
2586    #[test]
2587    fn test_change_id_type() {
2588        let env = CommitTemplateTestEnv::init();
2589
2590        let id = ChangeId::from_hex("ffdaa62087a280bddc5e3d3ff933b8ae");
2591        insta::assert_snapshot!(
2592            env.render_ok("self", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
2593        insta::assert_snapshot!(
2594            env.render_ok("self.normal_hex()", &id), @"ffdaa62087a280bddc5e3d3ff933b8ae");
2595
2596        insta::assert_snapshot!(env.render_ok("self.short()", &id), @"kkmpptxzrspx");
2597        insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @"");
2598        insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @"");
2599        insta::assert_snapshot!(
2600            env.render_ok("self.short(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
2601        insta::assert_snapshot!(
2602            env.render_ok("self.short(-100)", &id),
2603            @"<Error: out of range integral type conversion attempted>");
2604
2605        insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"k");
2606        insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"k");
2607        insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"k");
2608        insta::assert_snapshot!(
2609            env.render_ok("self.shortest(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
2610        insta::assert_snapshot!(
2611            env.render_ok("self.shortest(-100)", &id),
2612            @"<Error: out of range integral type conversion attempted>");
2613    }
2614}