Skip to main content

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