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