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