jj_cli/
commit_templater.rs

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