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