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