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    // TODO: Remove in jj 0.43+
1223    map.insert(
1224        "git_refs",
1225        |language, diagnostics, _build_ctx, self_property, function| {
1226            diagnostics.add_warning(TemplateParseError::expression(
1227                "commit.git_refs() is deprecated; use .remote_bookmarks()/tags() instead",
1228                function.name_span,
1229            ));
1230            function.expect_no_arguments()?;
1231            let index = language.keyword_cache.git_refs_index(language.repo).clone();
1232            let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec());
1233            Ok(out_property.into_dyn_wrapped())
1234        },
1235    );
1236    // TODO: Remove in jj 0.43+
1237    map.insert(
1238        "git_head",
1239        |language, diagnostics, _build_ctx, self_property, function| {
1240            diagnostics.add_warning(TemplateParseError::expression(
1241                "commit.git_head() is deprecated; use .contained_in('first_parent(@)') instead",
1242                function.name_span,
1243            ));
1244            function.expect_no_arguments()?;
1245            let repo = language.repo;
1246            let out_property = self_property.map(|commit| {
1247                let target = repo.view().git_head();
1248                target.added_ids().contains(commit.id())
1249            });
1250            Ok(out_property.into_dyn_wrapped())
1251        },
1252    );
1253    map.insert(
1254        "divergent",
1255        |language, _diagnostics, _build_ctx, self_property, function| {
1256            function.expect_no_arguments()?;
1257            let repo = language.repo;
1258            let out_property = self_property.and_then(|commit| {
1259                // The given commit could be hidden in e.g. `jj evolog`.
1260                let maybe_targets = repo.resolve_change_id(commit.change_id())?;
1261                let divergent = maybe_targets.is_some_and(|targets| targets.is_divergent());
1262                Ok(divergent)
1263            });
1264            Ok(out_property.into_dyn_wrapped())
1265        },
1266    );
1267    map.insert(
1268        "hidden",
1269        |language, _diagnostics, _build_ctx, self_property, function| {
1270            function.expect_no_arguments()?;
1271            let repo = language.repo;
1272            let out_property = self_property.and_then(|commit| Ok(commit.is_hidden(repo)?));
1273            Ok(out_property.into_dyn_wrapped())
1274        },
1275    );
1276    map.insert(
1277        "change_offset",
1278        |language, _diagnostics, _build_ctx, self_property, function| {
1279            function.expect_no_arguments()?;
1280            let repo = language.repo;
1281            let out_property = self_property.and_then(|commit| {
1282                // The given commit could be hidden in e.g. `jj evolog`.
1283                let maybe_targets = repo.resolve_change_id(commit.change_id())?;
1284                let offset = maybe_targets
1285                    .and_then(|targets| targets.find_offset(commit.id()))
1286                    .map(i64::try_from)
1287                    .transpose()?;
1288                Ok(offset)
1289            });
1290            Ok(out_property.into_dyn_wrapped())
1291        },
1292    );
1293    map.insert(
1294        "immutable",
1295        |language, _diagnostics, _build_ctx, self_property, function| {
1296            function.expect_no_arguments()?;
1297            let is_immutable = language
1298                .keyword_cache
1299                .is_immutable_fn(language, function.name_span)?
1300                .clone();
1301            let out_property = self_property.and_then(move |commit| Ok(is_immutable(commit.id())?));
1302            Ok(out_property.into_dyn_wrapped())
1303        },
1304    );
1305    map.insert(
1306        "contained_in",
1307        |language, diagnostics, _build_ctx, self_property, function| {
1308            let [revset_node] = function.expect_exact_arguments()?;
1309
1310            let is_contained =
1311                template_parser::catch_aliases(diagnostics, revset_node, |diagnostics, node| {
1312                    let text = template_parser::expect_string_literal(node)?;
1313                    let revset = evaluate_user_revset(language, diagnostics, node.span, text)?;
1314                    Ok(revset.containing_fn())
1315                })?;
1316
1317            let out_property = self_property.and_then(move |commit| Ok(is_contained(commit.id())?));
1318            Ok(out_property.into_dyn_wrapped())
1319        },
1320    );
1321    map.insert(
1322        "conflict",
1323        |_language, _diagnostics, _build_ctx, self_property, function| {
1324            function.expect_no_arguments()?;
1325            let out_property = self_property.map(|commit| commit.has_conflict());
1326            Ok(out_property.into_dyn_wrapped())
1327        },
1328    );
1329    map.insert(
1330        "empty",
1331        |language, _diagnostics, _build_ctx, self_property, function| {
1332            function.expect_no_arguments()?;
1333            let repo = language.repo;
1334            let out_property =
1335                self_property.and_then(|commit| Ok(commit.is_empty(repo).block_on()?));
1336            Ok(out_property.into_dyn_wrapped())
1337        },
1338    );
1339    map.insert(
1340        "diff",
1341        |language, diagnostics, _build_ctx, self_property, function| {
1342            let ([], [files_node]) = function.expect_arguments()?;
1343            let files = if let Some(node) = files_node {
1344                expect_fileset_literal(diagnostics, node, &language.fileset_parse_context())?
1345            } else {
1346                // TODO: defaults to CLI path arguments?
1347                // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731
1348                FilesetExpression::all()
1349            };
1350            let repo = language.repo;
1351            let matcher: Rc<dyn Matcher> = files.to_matcher().into();
1352            let out_property = self_property.and_then(move |commit| {
1353                Ok(TreeDiff::from_commit(repo, &commit, matcher.clone()).block_on()?)
1354            });
1355            Ok(out_property.into_dyn_wrapped())
1356        },
1357    );
1358    map.insert(
1359        "files",
1360        |language, diagnostics, _build_ctx, self_property, function| {
1361            let ([], [files_node]) = function.expect_arguments()?;
1362            let files = if let Some(node) = files_node {
1363                expect_fileset_literal(diagnostics, node, &language.fileset_parse_context())?
1364            } else {
1365                // TODO: defaults to CLI path arguments?
1366                // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731
1367                FilesetExpression::all()
1368            };
1369            let matcher = files.to_matcher();
1370            let out_property = self_property.and_then(move |commit| {
1371                let tree = commit.tree();
1372                let entries: Vec<_> = tree
1373                    .entries_matching(&*matcher)
1374                    .map(|(path, value)| value.map(|value| TreeEntry { path, value }))
1375                    .try_collect()?;
1376                Ok(entries)
1377            });
1378            Ok(out_property.into_dyn_wrapped())
1379        },
1380    );
1381    map.insert(
1382        "conflicted_files",
1383        |_language, _diagnostics, _build_ctx, self_property, function| {
1384            function.expect_no_arguments()?;
1385            let out_property = self_property.and_then(|commit| {
1386                let tree = commit.tree();
1387                let entries: Vec<_> = tree
1388                    .conflicts()
1389                    .map(|(path, value)| value.map(|value| TreeEntry { path, value }))
1390                    .try_collect()?;
1391                Ok(entries)
1392            });
1393            Ok(out_property.into_dyn_wrapped())
1394        },
1395    );
1396    map.insert(
1397        "root",
1398        |language, _diagnostics, _build_ctx, self_property, function| {
1399            function.expect_no_arguments()?;
1400            let repo = language.repo;
1401            let out_property =
1402                self_property.map(|commit| commit.id() == repo.store().root_commit_id());
1403            Ok(out_property.into_dyn_wrapped())
1404        },
1405    );
1406    map
1407}
1408
1409fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> Vec<WorkspaceRef> {
1410    if repo.view().wc_commit_ids().len() <= 1 {
1411        // No non-default working copies, return empty list.
1412        return vec![];
1413    }
1414
1415    repo.view()
1416        .wc_commit_ids()
1417        .iter()
1418        .filter(|(_, wc_commit_id)| *wc_commit_id == commit.id())
1419        .map(|(name, _)| WorkspaceRef::new(name.to_owned(), commit.to_owned()))
1420        .collect()
1421}
1422
1423fn expect_fileset_literal(
1424    diagnostics: &mut TemplateDiagnostics,
1425    node: &ExpressionNode,
1426    context: &FilesetParseContext,
1427) -> Result<FilesetExpression, TemplateParseError> {
1428    template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
1429        let text = template_parser::expect_string_literal(node)?;
1430        let mut inner_diagnostics = FilesetDiagnostics::new();
1431        let expression = fileset::parse(&mut inner_diagnostics, text, context).map_err(|err| {
1432            TemplateParseError::expression("In fileset expression", node.span).with_source(err)
1433        })?;
1434        diagnostics.extend_with(inner_diagnostics, |diag| {
1435            TemplateParseError::expression("In fileset expression", node.span).with_source(diag)
1436        });
1437        Ok(expression)
1438    })
1439}
1440
1441fn evaluate_revset_expression<'repo>(
1442    language: &CommitTemplateLanguage<'repo>,
1443    span: pest::Span<'_>,
1444    expression: &UserRevsetExpression,
1445) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
1446    let make_error = || TemplateParseError::expression("Failed to evaluate revset", span);
1447    let repo = language.repo;
1448    let symbol_resolver = revset_util::default_symbol_resolver(
1449        repo,
1450        language.revset_parse_context.extensions.symbol_resolvers(),
1451        language.id_prefix_context,
1452    );
1453    let revset = expression
1454        .resolve_user_expression(repo, &symbol_resolver)
1455        .map_err(|err| make_error().with_source(err))?
1456        .evaluate(repo)
1457        .map_err(|err| make_error().with_source(err))?;
1458    Ok(revset)
1459}
1460
1461fn evaluate_user_revset<'repo>(
1462    language: &CommitTemplateLanguage<'repo>,
1463    diagnostics: &mut TemplateDiagnostics,
1464    span: pest::Span<'_>,
1465    revset: &str,
1466) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
1467    let mut inner_diagnostics = RevsetDiagnostics::new();
1468    let expression = revset::parse(
1469        &mut inner_diagnostics,
1470        revset,
1471        &language.revset_parse_context,
1472    )
1473    .map_err(|err| TemplateParseError::expression("In revset expression", span).with_source(err))?;
1474    diagnostics.extend_with(inner_diagnostics, |diag| {
1475        TemplateParseError::expression("In revset expression", span).with_source(diag)
1476    });
1477    evaluate_revset_expression(language, span, &expression)
1478}
1479
1480fn builtin_commit_evolution_entry_methods<'repo>()
1481-> CommitTemplateBuildMethodFnMap<'repo, CommitEvolutionEntry> {
1482    // Not using maplit::hashmap!{} or custom declarative macro here because
1483    // code completion inside macro is quite restricted.
1484    let mut map = CommitTemplateBuildMethodFnMap::<CommitEvolutionEntry>::new();
1485    map.insert(
1486        "commit",
1487        |_language, _diagnostics, _build_ctx, self_property, function| {
1488            function.expect_no_arguments()?;
1489            let out_property = self_property.map(|entry| entry.commit);
1490            Ok(out_property.into_dyn_wrapped())
1491        },
1492    );
1493    map.insert(
1494        "operation",
1495        |_language, _diagnostics, _build_ctx, self_property, function| {
1496            function.expect_no_arguments()?;
1497            let out_property = self_property.map(|entry| entry.operation);
1498            Ok(out_property.into_dyn_wrapped())
1499        },
1500    );
1501    map.insert(
1502        "predecessors",
1503        |_language, _diagnostics, _build_ctx, self_property, function| {
1504            function.expect_no_arguments()?;
1505            let out_property = self_property.and_then(|entry| {
1506                let commits = entry.predecessors().block_on()?;
1507                Ok(commits)
1508            });
1509            Ok(out_property.into_dyn_wrapped())
1510        },
1511    );
1512    map.insert(
1513        "inter_diff",
1514        |language, diagnostics, _build_ctx, self_property, function| {
1515            let ([], [files_node]) = function.expect_arguments()?;
1516            let files = if let Some(node) = files_node {
1517                expect_fileset_literal(diagnostics, node, &language.fileset_parse_context())?
1518            } else {
1519                FilesetExpression::all()
1520            };
1521            let repo = language.repo;
1522            let matcher: Rc<dyn Matcher> = files.to_matcher().into();
1523            let out_property = self_property.and_then(move |entry| {
1524                let predecessors = entry.predecessors().block_on()?;
1525                let from_tree =
1526                    rebase_to_dest_parent(repo, &predecessors, &entry.commit).block_on()?;
1527                let to_tree = entry.commit.tree();
1528                Ok(TreeDiff {
1529                    from_tree,
1530                    to_tree,
1531                    matcher: matcher.clone(),
1532                    copy_records: CopyRecords::default(), // TODO: copy tracking
1533                })
1534            });
1535            Ok(out_property.into_dyn_wrapped())
1536        },
1537    );
1538    map
1539}
1540
1541/// Bookmark or tag name with metadata.
1542#[derive(Debug, serde::Serialize)]
1543pub struct CommitRef {
1544    // Not using Ref/GitRef/RemoteName types here because it would be overly
1545    // complex to generalize the name type as T: RefName|GitRefName.
1546    /// Local name.
1547    name: RefSymbolBuf,
1548    /// Remote name if this is a remote or Git-tracking ref.
1549    #[serde(skip_serializing_if = "Option::is_none")] // local ref shouldn't have this field
1550    remote: Option<RefSymbolBuf>,
1551    /// Target commit ids.
1552    target: RefTarget,
1553    /// Local ref metadata which tracks this remote ref.
1554    #[serde(rename = "tracking_target")]
1555    #[serde(skip_serializing_if = "Option::is_none")] // local ref shouldn't have this field
1556    #[serde(serialize_with = "serialize_tracking_target")]
1557    tracking_ref: Option<TrackingRef>,
1558    /// Local ref is synchronized with all tracking remotes, or tracking remote
1559    /// ref is synchronized with the local.
1560    #[serde(skip)] // internal state used mainly for Template impl
1561    synced: bool,
1562}
1563
1564#[derive(Debug)]
1565struct TrackingRef {
1566    /// Local ref target which tracks the other remote ref.
1567    target: RefTarget,
1568    /// Number of commits ahead of the tracking `target`.
1569    ahead_count: OnceCell<SizeHint>,
1570    /// Number of commits behind of the tracking `target`.
1571    behind_count: OnceCell<SizeHint>,
1572}
1573
1574impl CommitRef {
1575    // CommitRef is wrapped by Rc<T> to make it cheaply cloned and share
1576    // lazy-evaluation results across clones.
1577
1578    /// Creates local ref representation which might track some of the
1579    /// `remote_refs`.
1580    pub fn local<'a>(
1581        name: impl Into<String>,
1582        target: RefTarget,
1583        remote_refs: impl IntoIterator<Item = &'a RemoteRef>,
1584    ) -> Rc<Self> {
1585        let synced = remote_refs
1586            .into_iter()
1587            .all(|remote_ref| !remote_ref.is_tracked() || remote_ref.target == target);
1588        Rc::new(Self {
1589            name: RefSymbolBuf(name.into()),
1590            remote: None,
1591            target,
1592            tracking_ref: None,
1593            synced,
1594        })
1595    }
1596
1597    /// Creates local ref representation which doesn't track any remote refs.
1598    pub fn local_only(name: impl Into<String>, target: RefTarget) -> Rc<Self> {
1599        Self::local(name, target, [])
1600    }
1601
1602    /// Creates remote ref representation which might be tracked by a local ref
1603    /// pointing to the `local_target`.
1604    pub fn remote(
1605        name: impl Into<String>,
1606        remote_name: impl Into<String>,
1607        remote_ref: RemoteRef,
1608        local_target: &RefTarget,
1609    ) -> Rc<Self> {
1610        let synced = remote_ref.is_tracked() && remote_ref.target == *local_target;
1611        let tracking_ref = remote_ref.is_tracked().then(|| {
1612            let count = if synced {
1613                OnceCell::from((0, Some(0))) // fast path for synced remotes
1614            } else {
1615                OnceCell::new()
1616            };
1617            TrackingRef {
1618                target: local_target.clone(),
1619                ahead_count: count.clone(),
1620                behind_count: count,
1621            }
1622        });
1623        Rc::new(Self {
1624            name: RefSymbolBuf(name.into()),
1625            remote: Some(RefSymbolBuf(remote_name.into())),
1626            target: remote_ref.target,
1627            tracking_ref,
1628            synced,
1629        })
1630    }
1631
1632    /// Creates remote ref representation which isn't tracked by a local ref.
1633    pub fn remote_only(
1634        name: impl Into<String>,
1635        remote_name: impl Into<String>,
1636        target: RefTarget,
1637    ) -> Rc<Self> {
1638        Rc::new(Self {
1639            name: RefSymbolBuf(name.into()),
1640            remote: Some(RefSymbolBuf(remote_name.into())),
1641            target,
1642            tracking_ref: None,
1643            synced: false, // has no local counterpart
1644        })
1645    }
1646
1647    /// Local name.
1648    pub fn name(&self) -> &str {
1649        self.name.as_ref()
1650    }
1651
1652    /// Remote name if this is a remote or Git-tracking ref.
1653    pub fn remote_name(&self) -> Option<&str> {
1654        self.remote.as_ref().map(AsRef::as_ref)
1655    }
1656
1657    /// Target commit ids.
1658    pub fn target(&self) -> &RefTarget {
1659        &self.target
1660    }
1661
1662    /// Returns true if this is a local ref.
1663    pub fn is_local(&self) -> bool {
1664        self.remote.is_none()
1665    }
1666
1667    /// Returns true if this is a remote ref.
1668    pub fn is_remote(&self) -> bool {
1669        self.remote.is_some()
1670    }
1671
1672    /// Returns true if this ref points to no commit.
1673    pub fn is_absent(&self) -> bool {
1674        self.target.is_absent()
1675    }
1676
1677    /// Returns true if this ref points to any commit.
1678    pub fn is_present(&self) -> bool {
1679        self.target.is_present()
1680    }
1681
1682    /// Whether the ref target has conflicts.
1683    pub fn has_conflict(&self) -> bool {
1684        self.target.has_conflict()
1685    }
1686
1687    /// Returns true if this ref is tracked by a local ref. The local ref might
1688    /// have been deleted (but not pushed yet.)
1689    pub fn is_tracked(&self) -> bool {
1690        self.tracking_ref.is_some()
1691    }
1692
1693    /// Returns true if this ref is tracked by a local ref, and if the local ref
1694    /// is present.
1695    pub fn is_tracking_present(&self) -> bool {
1696        self.tracking_ref
1697            .as_ref()
1698            .is_some_and(|tracking| tracking.target.is_present())
1699    }
1700
1701    /// Number of commits ahead of the tracking local ref.
1702    fn tracking_ahead_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1703        let Some(tracking) = &self.tracking_ref else {
1704            return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1705        };
1706        tracking
1707            .ahead_count
1708            .get_or_try_init(|| {
1709                let self_ids = self.target.added_ids().cloned().collect_vec();
1710                let other_ids = tracking.target.added_ids().cloned().collect_vec();
1711                Ok(revset::walk_revs(repo, &self_ids, &other_ids)?.count_estimate()?)
1712            })
1713            .copied()
1714    }
1715
1716    /// Number of commits behind of the tracking local ref.
1717    fn tracking_behind_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1718        let Some(tracking) = &self.tracking_ref else {
1719            return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1720        };
1721        tracking
1722            .behind_count
1723            .get_or_try_init(|| {
1724                let self_ids = self.target.added_ids().cloned().collect_vec();
1725                let other_ids = tracking.target.added_ids().cloned().collect_vec();
1726                Ok(revset::walk_revs(repo, &other_ids, &self_ids)?.count_estimate()?)
1727            })
1728            .copied()
1729    }
1730}
1731
1732// If wrapping with Rc<T> becomes common, add generic impl for Rc<T>.
1733impl Template for Rc<CommitRef> {
1734    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1735        write!(formatter.labeled("name"), "{}", self.name)?;
1736        if let Some(remote) = &self.remote {
1737            write!(formatter, "@")?;
1738            write!(formatter.labeled("remote"), "{remote}")?;
1739        }
1740        // Don't show both conflict and unsynced sigils as conflicted ref wouldn't
1741        // be pushed.
1742        if self.has_conflict() {
1743            write!(formatter, "??")?;
1744        } else if self.is_local() && !self.synced {
1745            write!(formatter, "*")?;
1746        }
1747        Ok(())
1748    }
1749}
1750
1751impl Template for Vec<Rc<CommitRef>> {
1752    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1753        templater::format_joined(formatter, self, " ")
1754    }
1755}
1756
1757/// Workspace name together with its working-copy commit for templating.
1758#[derive(Debug, Clone, serde::Serialize)]
1759pub struct WorkspaceRef {
1760    /// Workspace name as a symbol.
1761    name: WorkspaceNameBuf,
1762    /// Working-copy commit of this workspace.
1763    target: Commit,
1764}
1765
1766impl WorkspaceRef {
1767    /// Creates a new workspace reference from the workspace name and commit.
1768    pub fn new(name: WorkspaceNameBuf, target: Commit) -> Self {
1769        Self { name, target }
1770    }
1771
1772    /// Returns the workspace name symbol.
1773    pub fn name(&self) -> &WorkspaceName {
1774        self.name.as_ref()
1775    }
1776
1777    /// Returns the working-copy commit of this workspace.
1778    pub fn target(&self) -> &Commit {
1779        &self.target
1780    }
1781
1782    /// Returns the root path of the workspace.
1783    fn root(&self, path_converter: &RepoPathUiConverter) -> Result<String, TemplatePropertyError> {
1784        let RepoPathUiConverter::Fs { cwd: _, base } = path_converter;
1785        // TODO: Stop reconstructing the workspace loader here once we've
1786        // decided which object should own the workspace store.
1787        let workspace_loader = DefaultWorkspaceLoaderFactory.create(base)?;
1788        let repo_path = workspace_loader.repo_path().to_owned();
1789        let workspace_store = SimpleWorkspaceStore::load(&repo_path)?;
1790        let workspace_path = workspace_store
1791            .get_workspace_path(self.name())?
1792            .ok_or_else(|| {
1793                TemplatePropertyError(
1794                    format!(
1795                        "Workspace has no recorded path: {}",
1796                        self.name().as_symbol()
1797                    )
1798                    .into(),
1799                )
1800            })?;
1801        let full_path = repo_path.join(workspace_path);
1802        let path = dunce::canonicalize(&full_path).map_err(|err| {
1803            TemplatePropertyError(
1804                format!(
1805                    "Failed to resolve workspace root: {}: {}: {err}",
1806                    self.name().as_symbol(),
1807                    full_path.display()
1808                )
1809                .into(),
1810            )
1811        })?;
1812        // TODO: Return PathBuf once the templater has a filesystem path type.
1813        path.into_os_string()
1814            .into_string()
1815            .map_err(|_| TemplatePropertyError("Invalid UTF-8 sequence in path".into()))
1816    }
1817}
1818
1819impl Template for WorkspaceRef {
1820    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1821        write!(formatter, "{}@", self.name.as_symbol())
1822    }
1823}
1824
1825impl Template for Vec<WorkspaceRef> {
1826    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1827        templater::format_joined(formatter, self, " ")
1828    }
1829}
1830
1831fn builtin_workspace_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, WorkspaceRef> {
1832    let mut map = CommitTemplateBuildMethodFnMap::<WorkspaceRef>::new();
1833    map.insert(
1834        "name",
1835        |_language, _diagnostics, _build_ctx, self_property, function| {
1836            function.expect_no_arguments()?;
1837            let out_property = self_property.map(|ws_ref| RefSymbolBuf(ws_ref.name.into()));
1838            Ok(out_property.into_dyn_wrapped())
1839        },
1840    );
1841    map.insert(
1842        "target",
1843        |_language, _diagnostics, _build_ctx, self_property, function| {
1844            function.expect_no_arguments()?;
1845            let out_property = self_property.map(|ws_ref| ws_ref.target);
1846            Ok(out_property.into_dyn_wrapped())
1847        },
1848    );
1849    map.insert(
1850        "root",
1851        |language, _diagnostics, _build_ctx, self_property, function| {
1852            function.expect_no_arguments()?;
1853            let path_converter = language.path_converter;
1854            let out_property = self_property.and_then(move |ws_ref| ws_ref.root(path_converter));
1855            Ok(out_property.into_dyn_wrapped())
1856        },
1857    );
1858    map
1859}
1860
1861#[expect(clippy::ref_option)]
1862fn serialize_tracking_target<S>(
1863    tracking_ref: &Option<TrackingRef>,
1864    serializer: S,
1865) -> Result<S::Ok, S::Error>
1866where
1867    S: serde::Serializer,
1868{
1869    let target = tracking_ref.as_ref().map(|tracking| &tracking.target);
1870    target.serialize(serializer)
1871}
1872
1873fn builtin_commit_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>> {
1874    // Not using maplit::hashmap!{} or custom declarative macro here because
1875    // code completion inside macro is quite restricted.
1876    let mut map = CommitTemplateBuildMethodFnMap::<Rc<CommitRef>>::new();
1877    map.insert(
1878        "name",
1879        |_language, _diagnostics, _build_ctx, self_property, function| {
1880            function.expect_no_arguments()?;
1881            let out_property = self_property.map(|commit_ref| commit_ref.name.clone());
1882            Ok(out_property.into_dyn_wrapped())
1883        },
1884    );
1885    map.insert(
1886        "remote",
1887        |_language, _diagnostics, _build_ctx, self_property, function| {
1888            function.expect_no_arguments()?;
1889            let out_property = self_property.map(|commit_ref| commit_ref.remote.clone());
1890            Ok(out_property.into_dyn_wrapped())
1891        },
1892    );
1893    map.insert(
1894        "present",
1895        |_language, _diagnostics, _build_ctx, self_property, function| {
1896            function.expect_no_arguments()?;
1897            let out_property = self_property.map(|commit_ref| commit_ref.is_present());
1898            Ok(out_property.into_dyn_wrapped())
1899        },
1900    );
1901    map.insert(
1902        "conflict",
1903        |_language, _diagnostics, _build_ctx, self_property, function| {
1904            function.expect_no_arguments()?;
1905            let out_property = self_property.map(|commit_ref| commit_ref.has_conflict());
1906            Ok(out_property.into_dyn_wrapped())
1907        },
1908    );
1909    map.insert(
1910        "normal_target",
1911        |language, _diagnostics, _build_ctx, self_property, function| {
1912            function.expect_no_arguments()?;
1913            let repo = language.repo;
1914            let out_property = self_property.and_then(|commit_ref| {
1915                let maybe_id = commit_ref.target.as_normal();
1916                Ok(maybe_id.map(|id| repo.store().get_commit(id)).transpose()?)
1917            });
1918            Ok(out_property.into_dyn_wrapped())
1919        },
1920    );
1921    map.insert(
1922        "removed_targets",
1923        |language, _diagnostics, _build_ctx, self_property, function| {
1924            function.expect_no_arguments()?;
1925            let repo = language.repo;
1926            let out_property = self_property.and_then(|commit_ref| {
1927                let ids = commit_ref.target.removed_ids();
1928                let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?;
1929                Ok(commits)
1930            });
1931            Ok(out_property.into_dyn_wrapped())
1932        },
1933    );
1934    map.insert(
1935        "added_targets",
1936        |language, _diagnostics, _build_ctx, self_property, function| {
1937            function.expect_no_arguments()?;
1938            let repo = language.repo;
1939            let out_property = self_property.and_then(|commit_ref| {
1940                let ids = commit_ref.target.added_ids();
1941                let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?;
1942                Ok(commits)
1943            });
1944            Ok(out_property.into_dyn_wrapped())
1945        },
1946    );
1947    map.insert(
1948        "tracked",
1949        |_language, _diagnostics, _build_ctx, self_property, function| {
1950            function.expect_no_arguments()?;
1951            let out_property = self_property.map(|commit_ref| commit_ref.is_tracked());
1952            Ok(out_property.into_dyn_wrapped())
1953        },
1954    );
1955    map.insert(
1956        "tracking_present",
1957        |_language, _diagnostics, _build_ctx, self_property, function| {
1958            function.expect_no_arguments()?;
1959            let out_property = self_property.map(|commit_ref| commit_ref.is_tracking_present());
1960            Ok(out_property.into_dyn_wrapped())
1961        },
1962    );
1963    map.insert(
1964        "tracking_ahead_count",
1965        |language, _diagnostics, _build_ctx, self_property, function| {
1966            function.expect_no_arguments()?;
1967            let repo = language.repo;
1968            let out_property =
1969                self_property.and_then(|commit_ref| commit_ref.tracking_ahead_count(repo));
1970            Ok(out_property.into_dyn_wrapped())
1971        },
1972    );
1973    map.insert(
1974        "tracking_behind_count",
1975        |language, _diagnostics, _build_ctx, self_property, function| {
1976            function.expect_no_arguments()?;
1977            let repo = language.repo;
1978            let out_property =
1979                self_property.and_then(|commit_ref| commit_ref.tracking_behind_count(repo));
1980            Ok(out_property.into_dyn_wrapped())
1981        },
1982    );
1983    map.insert(
1984        "synced",
1985        |_language, _diagnostics, _build_ctx, self_property, function| {
1986            function.expect_no_arguments()?;
1987            let out_property = self_property.map(|commit_ref| commit_ref.synced);
1988            Ok(out_property.into_dyn_wrapped())
1989        },
1990    );
1991    map
1992}
1993
1994/// Cache for reverse lookup refs.
1995#[derive(Clone, Debug, Default)]
1996pub struct CommitRefsIndex {
1997    index: HashMap<CommitId, Vec<Rc<CommitRef>>>,
1998}
1999
2000impl CommitRefsIndex {
2001    fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: Rc<CommitRef>) {
2002        for id in ids {
2003            let commit_refs = self.index.entry(id.clone()).or_default();
2004            commit_refs.push(name.clone());
2005        }
2006    }
2007
2008    pub fn get(&self, id: &CommitId) -> &[Rc<CommitRef>] {
2009        self.index.get(id).map_or(&[], |refs: &Vec<_>| refs)
2010    }
2011}
2012
2013fn build_local_remote_refs_index<'a>(
2014    local_remote_refs: impl IntoIterator<Item = (&'a RefName, LocalRemoteRefTarget<'a>)>,
2015) -> CommitRefsIndex {
2016    let mut index = CommitRefsIndex::default();
2017    for (name, target) in local_remote_refs {
2018        let local_target = target.local_target;
2019        let remote_refs = target.remote_refs;
2020        if local_target.is_present() {
2021            let commit_ref = CommitRef::local(
2022                name,
2023                local_target.clone(),
2024                remote_refs.iter().map(|&(_, remote_ref)| remote_ref),
2025            );
2026            index.insert(local_target.added_ids(), commit_ref);
2027        }
2028        for &(remote_name, remote_ref) in &remote_refs {
2029            let commit_ref = CommitRef::remote(name, remote_name, remote_ref.clone(), local_target);
2030            index.insert(remote_ref.target.added_ids(), commit_ref);
2031        }
2032    }
2033    index
2034}
2035
2036fn build_commit_refs_index<'a, K: Into<String>>(
2037    ref_pairs: impl IntoIterator<Item = (K, &'a RefTarget)>,
2038) -> CommitRefsIndex {
2039    let mut index = CommitRefsIndex::default();
2040    for (name, target) in ref_pairs {
2041        let commit_ref = CommitRef::local_only(name, target.clone());
2042        index.insert(target.added_ids(), commit_ref);
2043    }
2044    index
2045}
2046
2047fn collect_distinct_refs(commit_refs: &[Rc<CommitRef>]) -> Vec<Rc<CommitRef>> {
2048    commit_refs
2049        .iter()
2050        .filter(|commit_ref| commit_ref.is_local() || !commit_ref.synced)
2051        .cloned()
2052        .collect()
2053}
2054
2055fn collect_local_refs(commit_refs: &[Rc<CommitRef>]) -> Vec<Rc<CommitRef>> {
2056    commit_refs
2057        .iter()
2058        .filter(|commit_ref| commit_ref.is_local())
2059        .cloned()
2060        .collect()
2061}
2062
2063fn collect_remote_refs(commit_refs: &[Rc<CommitRef>]) -> Vec<Rc<CommitRef>> {
2064    commit_refs
2065        .iter()
2066        .filter(|commit_ref| commit_ref.is_remote())
2067        .cloned()
2068        .collect()
2069}
2070
2071/// Wrapper to render ref/remote name in revset syntax.
2072#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
2073#[serde(transparent)]
2074pub struct RefSymbolBuf(String);
2075
2076impl AsRef<str> for RefSymbolBuf {
2077    fn as_ref(&self) -> &str {
2078        &self.0
2079    }
2080}
2081
2082impl Display for RefSymbolBuf {
2083    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2084        f.pad(&revset::format_symbol(&self.0))
2085    }
2086}
2087
2088impl Template for RefSymbolBuf {
2089    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2090        write!(formatter, "{self}")
2091    }
2092}
2093
2094impl Template for RepoPathBuf {
2095    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2096        write!(formatter, "{}", self.as_internal_file_string())
2097    }
2098}
2099
2100fn builtin_repo_path_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf> {
2101    // Not using maplit::hashmap!{} or custom declarative macro here because
2102    // code completion inside macro is quite restricted.
2103    let mut map = CommitTemplateBuildMethodFnMap::<RepoPathBuf>::new();
2104    map.insert(
2105        "absolute",
2106        |language, _diagnostics, _build_ctx, self_property, function| {
2107            function.expect_no_arguments()?;
2108            let path_converter = language.path_converter;
2109            // We handle the absolute path here instead of in a wrapper in
2110            // `RepoPathUiConverter` because absolute paths only make sense for
2111            // filesystem paths. Other cases should fail here.
2112            let out_property = self_property.and_then(move |path| match path_converter {
2113                RepoPathUiConverter::Fs { cwd: _, base } => path
2114                    .to_fs_path(base)?
2115                    .into_os_string()
2116                    .into_string()
2117                    .map_err(|_| TemplatePropertyError("Invalid UTF-8 sequence in path".into())),
2118            });
2119            Ok(out_property.into_dyn_wrapped())
2120        },
2121    );
2122    map.insert(
2123        "display",
2124        |language, _diagnostics, _build_ctx, self_property, function| {
2125            function.expect_no_arguments()?;
2126            let path_converter = language.path_converter;
2127            let out_property = self_property.map(|path| path_converter.format_file_path(&path));
2128            Ok(out_property.into_dyn_wrapped())
2129        },
2130    );
2131    map.insert(
2132        "parent",
2133        |_language, _diagnostics, _build_ctx, self_property, function| {
2134            function.expect_no_arguments()?;
2135            let out_property = self_property.map(|path| Some(path.parent()?.to_owned()));
2136            Ok(out_property.into_dyn_wrapped())
2137        },
2138    );
2139    map
2140}
2141
2142trait ShortestIdPrefixLen {
2143    fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> IndexResult<usize>;
2144}
2145
2146impl ShortestIdPrefixLen for ChangeId {
2147    fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> IndexResult<usize> {
2148        index.shortest_change_prefix_len(repo, self)
2149    }
2150}
2151
2152impl Template for ChangeId {
2153    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2154        write!(formatter, "{self}")
2155    }
2156}
2157
2158fn builtin_change_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, ChangeId> {
2159    let mut map = builtin_commit_or_change_id_methods::<ChangeId>();
2160    map.insert(
2161        "normal_hex",
2162        |_language, _diagnostics, _build_ctx, self_property, function| {
2163            function.expect_no_arguments()?;
2164            // Note: this is _not_ the same as id.to_string(), which returns the
2165            // "reverse" hex (z-k), instead of the "forward" / normal hex
2166            // (0-9a-f) we want here.
2167            let out_property = self_property.map(|id| id.hex());
2168            Ok(out_property.into_dyn_wrapped())
2169        },
2170    );
2171    map
2172}
2173
2174impl ShortestIdPrefixLen for CommitId {
2175    fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> IndexResult<usize> {
2176        index.shortest_commit_prefix_len(repo, self)
2177    }
2178}
2179
2180impl Template for CommitId {
2181    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2182        write!(formatter, "{self}")
2183    }
2184}
2185
2186fn builtin_commit_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, CommitId> {
2187    builtin_commit_or_change_id_methods::<CommitId>()
2188}
2189
2190fn builtin_commit_or_change_id_methods<'repo, O>() -> CommitTemplateBuildMethodFnMap<'repo, O>
2191where
2192    O: Display + ShortestIdPrefixLen + 'repo,
2193{
2194    // Not using maplit::hashmap!{} or custom declarative macro here because
2195    // code completion inside macro is quite restricted.
2196    let mut map = CommitTemplateBuildMethodFnMap::<O>::new();
2197    map.insert(
2198        "short",
2199        |language, diagnostics, build_ctx, self_property, function| {
2200            let ([], [len_node]) = function.expect_arguments()?;
2201            let len_property = len_node
2202                .map(|node| {
2203                    template_builder::expect_usize_expression(
2204                        language,
2205                        diagnostics,
2206                        build_ctx,
2207                        node,
2208                    )
2209                })
2210                .transpose()?;
2211            let out_property = (self_property, len_property)
2212                .map(|(id, len)| format!("{id:.len$}", len = len.unwrap_or(12)));
2213            Ok(out_property.into_dyn_wrapped())
2214        },
2215    );
2216    map.insert(
2217        "shortest",
2218        |language, diagnostics, build_ctx, self_property, function| {
2219            let ([], [len_node]) = function.expect_arguments()?;
2220            let len_property = len_node
2221                .map(|node| {
2222                    template_builder::expect_usize_expression(
2223                        language,
2224                        diagnostics,
2225                        build_ctx,
2226                        node,
2227                    )
2228                })
2229                .transpose()?;
2230            let repo = language.repo;
2231            let index = match language.id_prefix_context.populate(repo) {
2232                Ok(index) => index,
2233                Err(err) => {
2234                    // Not an error because we can still produce somewhat
2235                    // reasonable output.
2236                    diagnostics.add_warning(
2237                        TemplateParseError::expression(
2238                            "Failed to load short-prefixes index",
2239                            function.name_span,
2240                        )
2241                        .with_source(err),
2242                    );
2243                    IdPrefixIndex::empty()
2244                }
2245            };
2246            // The length of the id printed will be the maximum of the minimum
2247            // `len` and the length of the shortest unique prefix.
2248            let out_property = (self_property, len_property).and_then(move |(id, len)| {
2249                let prefix_len = id.shortest_prefix_len(repo, &index)?;
2250                let mut hex = format!("{id:.len$}", len = max(prefix_len, len.unwrap_or(0)));
2251                let rest = hex.split_off(prefix_len);
2252                Ok(ShortestIdPrefix { prefix: hex, rest })
2253            });
2254            Ok(out_property.into_dyn_wrapped())
2255        },
2256    );
2257    map
2258}
2259
2260#[derive(Clone, Debug, serde::Serialize)]
2261pub struct ShortestIdPrefix {
2262    pub prefix: String,
2263    pub rest: String,
2264}
2265
2266impl Template for ShortestIdPrefix {
2267    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2268        write!(formatter.labeled("prefix"), "{}", self.prefix)?;
2269        write!(formatter.labeled("rest"), "{}", self.rest)?;
2270        Ok(())
2271    }
2272}
2273
2274impl ShortestIdPrefix {
2275    fn to_upper(&self) -> Self {
2276        Self {
2277            prefix: self.prefix.to_ascii_uppercase(),
2278            rest: self.rest.to_ascii_uppercase(),
2279        }
2280    }
2281    fn to_lower(&self) -> Self {
2282        Self {
2283            prefix: self.prefix.to_ascii_lowercase(),
2284            rest: self.rest.to_ascii_lowercase(),
2285        }
2286    }
2287}
2288
2289fn builtin_shortest_id_prefix_methods<'repo>()
2290-> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> {
2291    // Not using maplit::hashmap!{} or custom declarative macro here because
2292    // code completion inside macro is quite restricted.
2293    let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new();
2294    map.insert(
2295        "prefix",
2296        |_language, _diagnostics, _build_ctx, self_property, function| {
2297            function.expect_no_arguments()?;
2298            let out_property = self_property.map(|id| id.prefix);
2299            Ok(out_property.into_dyn_wrapped())
2300        },
2301    );
2302    map.insert(
2303        "rest",
2304        |_language, _diagnostics, _build_ctx, self_property, function| {
2305            function.expect_no_arguments()?;
2306            let out_property = self_property.map(|id| id.rest);
2307            Ok(out_property.into_dyn_wrapped())
2308        },
2309    );
2310    map.insert(
2311        "upper",
2312        |_language, _diagnostics, _build_ctx, self_property, function| {
2313            function.expect_no_arguments()?;
2314            let out_property = self_property.map(|id| id.to_upper());
2315            Ok(out_property.into_dyn_wrapped())
2316        },
2317    );
2318    map.insert(
2319        "lower",
2320        |_language, _diagnostics, _build_ctx, self_property, function| {
2321            function.expect_no_arguments()?;
2322            let out_property = self_property.map(|id| id.to_lower());
2323            Ok(out_property.into_dyn_wrapped())
2324        },
2325    );
2326    map
2327}
2328
2329/// Pair of trees to be diffed.
2330#[derive(Debug)]
2331pub struct TreeDiff {
2332    from_tree: MergedTree,
2333    to_tree: MergedTree,
2334    matcher: Rc<dyn Matcher>,
2335    copy_records: CopyRecords,
2336}
2337
2338impl TreeDiff {
2339    async fn from_commit(
2340        repo: &dyn Repo,
2341        commit: &Commit,
2342        matcher: Rc<dyn Matcher>,
2343    ) -> BackendResult<Self> {
2344        let mut copy_records = CopyRecords::default();
2345        for parent in commit.parent_ids() {
2346            let records =
2347                diff_util::get_copy_records(repo.store(), parent, commit.id(), &*matcher).await?;
2348            copy_records.add_records(records);
2349        }
2350        Ok(Self {
2351            from_tree: commit.parent_tree(repo).await?,
2352            to_tree: commit.tree(),
2353            matcher,
2354            copy_records,
2355        })
2356    }
2357
2358    fn diff_stream(&self) -> BoxStream<'_, CopiesTreeDiffEntry> {
2359        self.from_tree
2360            .diff_stream_with_copies(&self.to_tree, &*self.matcher, &self.copy_records)
2361    }
2362
2363    async fn collect_entries(&self) -> BackendResult<Vec<TreeDiffEntry>> {
2364        self.diff_stream()
2365            .map(TreeDiffEntry::from_backend_entry_with_copies)
2366            .try_collect()
2367            .await
2368    }
2369
2370    fn into_formatted<F, E>(self, show: F) -> TreeDiffFormatted<F>
2371    where
2372        F: Fn(
2373            &mut dyn Formatter,
2374            &Store,
2375            BoxStream<CopiesTreeDiffEntry>,
2376            Diff<&ConflictLabels>,
2377        ) -> Result<(), E>,
2378        E: Into<TemplatePropertyError>,
2379    {
2380        TreeDiffFormatted { diff: self, show }
2381    }
2382}
2383
2384/// Tree diff to be rendered by predefined function `F`.
2385struct TreeDiffFormatted<F> {
2386    diff: TreeDiff,
2387    show: F,
2388}
2389
2390impl<F, E> Template for TreeDiffFormatted<F>
2391where
2392    F: Fn(
2393        &mut dyn Formatter,
2394        &Store,
2395        BoxStream<CopiesTreeDiffEntry>,
2396        Diff<&ConflictLabels>,
2397    ) -> Result<(), E>,
2398    E: Into<TemplatePropertyError>,
2399{
2400    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2401        let show = &self.show;
2402        let store = self.diff.from_tree.store();
2403        let tree_diff = self.diff.diff_stream();
2404        let conflict_labels = Diff::new(self.diff.from_tree.labels(), self.diff.to_tree.labels());
2405        show(formatter.as_mut(), store, tree_diff, conflict_labels)
2406            .or_else(|err| formatter.handle_error(err.into()))
2407    }
2408}
2409
2410fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiff> {
2411    type P<'repo> = CommitTemplatePropertyKind<'repo>;
2412    // Not using maplit::hashmap!{} or custom declarative macro here because
2413    // code completion inside macro is quite restricted.
2414    let mut map = CommitTemplateBuildMethodFnMap::<TreeDiff>::new();
2415    map.insert(
2416        "files",
2417        |_language, _diagnostics, _build_ctx, self_property, function| {
2418            function.expect_no_arguments()?;
2419            // TODO: cache and reuse diff entries within the current evaluation?
2420            let out_property =
2421                self_property.and_then(|diff| Ok(diff.collect_entries().block_on()?));
2422            Ok(out_property.into_dyn_wrapped())
2423        },
2424    );
2425    map.insert(
2426        "color_words",
2427        |language, diagnostics, build_ctx, self_property, function| {
2428            let ([], [context_node]) = function.expect_arguments()?;
2429            let context_property = context_node
2430                .map(|node| {
2431                    template_builder::expect_usize_expression(
2432                        language,
2433                        diagnostics,
2434                        build_ctx,
2435                        node,
2436                    )
2437                })
2438                .transpose()?;
2439            let path_converter = language.path_converter;
2440            let options = diff_util::ColorWordsDiffOptions::from_settings(language.settings())
2441                .map_err(|err| {
2442                    let message = "Failed to load diff settings";
2443                    TemplateParseError::expression(message, function.name_span).with_source(err)
2444                })?;
2445            let conflict_marker_style = language.conflict_marker_style;
2446            let template = (self_property, context_property)
2447                .map(move |(diff, context)| {
2448                    let mut options = options.clone();
2449                    if let Some(context) = context {
2450                        options.context = context;
2451                    }
2452                    diff.into_formatted(move |formatter, store, tree_diff, conflict_labels| {
2453                        diff_util::show_color_words_diff(
2454                            formatter,
2455                            store,
2456                            tree_diff,
2457                            conflict_labels,
2458                            path_converter,
2459                            &options,
2460                            conflict_marker_style,
2461                        )
2462                        .block_on()
2463                    })
2464                })
2465                .into_template();
2466            Ok(P::wrap_template(template))
2467        },
2468    );
2469    map.insert(
2470        "git",
2471        |language, diagnostics, build_ctx, self_property, function| {
2472            let ([], [context_node]) = function.expect_arguments()?;
2473            let context_property = context_node
2474                .map(|node| {
2475                    template_builder::expect_usize_expression(
2476                        language,
2477                        diagnostics,
2478                        build_ctx,
2479                        node,
2480                    )
2481                })
2482                .transpose()?;
2483            let options = diff_util::UnifiedDiffOptions::from_settings(language.settings())
2484                .map_err(|err| {
2485                    let message = "Failed to load diff settings";
2486                    TemplateParseError::expression(message, function.name_span).with_source(err)
2487                })?;
2488            let conflict_marker_style = language.conflict_marker_style;
2489            let template = (self_property, context_property)
2490                .map(move |(diff, context)| {
2491                    let mut options = options.clone();
2492                    if let Some(context) = context {
2493                        options.context = context;
2494                    }
2495                    diff.into_formatted(move |formatter, store, trees, tree_diff| {
2496                        diff_util::show_git_diff(
2497                            formatter,
2498                            store,
2499                            trees,
2500                            tree_diff,
2501                            &options,
2502                            conflict_marker_style,
2503                        )
2504                        .block_on()
2505                    })
2506                })
2507                .into_template();
2508            Ok(P::wrap_template(template))
2509        },
2510    );
2511    map.insert(
2512        "stat",
2513        |language, diagnostics, build_ctx, self_property, function| {
2514            let ([], [width_node]) = function.expect_arguments()?;
2515            let width_property = width_node
2516                .map(|node| {
2517                    template_builder::expect_usize_expression(
2518                        language,
2519                        diagnostics,
2520                        build_ctx,
2521                        node,
2522                    )
2523                })
2524                .transpose()?;
2525            let path_converter = language.path_converter;
2526            // No user configuration exists for diff stat.
2527            let options = diff_util::DiffStatOptions::default();
2528            let conflict_marker_style = language.conflict_marker_style;
2529            // TODO: cache and reuse stats within the current evaluation?
2530            let out_property = (self_property, width_property).and_then(move |(diff, width)| {
2531                let store = diff.from_tree.store();
2532                let tree_diff = diff.diff_stream();
2533                let stats = DiffStats::calculate(store, tree_diff, &options, conflict_marker_style)
2534                    .block_on()?;
2535                Ok(DiffStatsFormatted {
2536                    stats,
2537                    path_converter,
2538                    // TODO: fall back to current available width
2539                    width: width.unwrap_or(80),
2540                })
2541            });
2542            Ok(out_property.into_dyn_wrapped())
2543        },
2544    );
2545    map.insert(
2546        "summary",
2547        |language, _diagnostics, _build_ctx, self_property, function| {
2548            function.expect_no_arguments()?;
2549            let path_converter = language.path_converter;
2550            let template = self_property
2551                .map(move |diff| {
2552                    diff.into_formatted(move |formatter, _store, tree_diff, _conflict_labels| {
2553                        diff_util::show_diff_summary(formatter, tree_diff, path_converter)
2554                            .block_on()
2555                    })
2556                })
2557                .into_template();
2558            Ok(P::wrap_template(template))
2559        },
2560    );
2561    // TODO: add support for external tools
2562    map
2563}
2564
2565/// [`MergedTree`] diff entry.
2566#[derive(Clone, Debug)]
2567pub struct TreeDiffEntry {
2568    pub path: CopiesTreeDiffEntryPath,
2569    pub values: Diff<MergedTreeValue>,
2570}
2571
2572impl TreeDiffEntry {
2573    pub fn from_backend_entry_with_copies(entry: CopiesTreeDiffEntry) -> BackendResult<Self> {
2574        Ok(Self {
2575            path: entry.path,
2576            values: entry.values?,
2577        })
2578    }
2579
2580    fn status(&self) -> diff_util::DiffEntryStatus {
2581        diff_util::diff_status(&self.path, &self.values)
2582    }
2583
2584    fn into_source_entry(self) -> TreeEntry {
2585        TreeEntry {
2586            path: self.path.source.map_or(self.path.target, |(path, _)| path),
2587            value: self.values.before,
2588        }
2589    }
2590
2591    fn into_target_entry(self) -> TreeEntry {
2592        TreeEntry {
2593            path: self.path.target,
2594            value: self.values.after,
2595        }
2596    }
2597}
2598
2599fn format_diff_path(
2600    path: &CopiesTreeDiffEntryPath,
2601    path_converter: &RepoPathUiConverter,
2602) -> String {
2603    match path.to_diff() {
2604        Some(paths) => path_converter.format_copied_path(paths),
2605        None => path_converter.format_file_path(path.target()),
2606    }
2607}
2608
2609fn builtin_tree_diff_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>
2610{
2611    // Not using maplit::hashmap!{} or custom declarative macro here because
2612    // code completion inside macro is quite restricted.
2613    let mut map = CommitTemplateBuildMethodFnMap::<TreeDiffEntry>::new();
2614    map.insert(
2615        "path",
2616        |_language, _diagnostics, _build_ctx, self_property, function| {
2617            function.expect_no_arguments()?;
2618            let out_property = self_property.map(|entry| entry.path.target);
2619            Ok(out_property.into_dyn_wrapped())
2620        },
2621    );
2622    map.insert(
2623        "display_diff_path",
2624        |language, _diagnostics, _build_ctx, self_property, function| {
2625            function.expect_no_arguments()?;
2626            let path_converter = language.path_converter;
2627            let out_property =
2628                self_property.map(move |entry| format_diff_path(&entry.path, path_converter));
2629            Ok(out_property.into_dyn_wrapped())
2630        },
2631    );
2632    map.insert(
2633        "status",
2634        |_language, _diagnostics, _build_ctx, self_property, function| {
2635            function.expect_no_arguments()?;
2636            let out_property = self_property.map(|entry| entry.status().label().to_owned());
2637            Ok(out_property.into_dyn_wrapped())
2638        },
2639    );
2640    map.insert(
2641        "status_char",
2642        |_language, _diagnostics, _build_ctx, self_property, function| {
2643            function.expect_no_arguments()?;
2644            let out_property = self_property.map(|entry| entry.status().char().to_string());
2645            Ok(out_property.into_dyn_wrapped())
2646        },
2647    );
2648    map.insert(
2649        "source",
2650        |_language, _diagnostics, _build_ctx, self_property, function| {
2651            function.expect_no_arguments()?;
2652            let out_property = self_property.map(TreeDiffEntry::into_source_entry);
2653            Ok(out_property.into_dyn_wrapped())
2654        },
2655    );
2656    map.insert(
2657        "target",
2658        |_language, _diagnostics, _build_ctx, self_property, function| {
2659            function.expect_no_arguments()?;
2660            let out_property = self_property.map(TreeDiffEntry::into_target_entry);
2661            Ok(out_property.into_dyn_wrapped())
2662        },
2663    );
2664    map
2665}
2666
2667/// [`MergedTree`] entry.
2668#[derive(Clone, Debug)]
2669pub struct TreeEntry {
2670    pub path: RepoPathBuf,
2671    pub value: MergedTreeValue,
2672}
2673
2674fn builtin_tree_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeEntry> {
2675    // Not using maplit::hashmap!{} or custom declarative macro here because
2676    // code completion inside macro is quite restricted.
2677    let mut map = CommitTemplateBuildMethodFnMap::<TreeEntry>::new();
2678    map.insert(
2679        "path",
2680        |_language, _diagnostics, _build_ctx, self_property, function| {
2681            function.expect_no_arguments()?;
2682            let out_property = self_property.map(|entry| entry.path);
2683            Ok(out_property.into_dyn_wrapped())
2684        },
2685    );
2686    map.insert(
2687        "conflict",
2688        |_language, _diagnostics, _build_ctx, self_property, function| {
2689            function.expect_no_arguments()?;
2690            let out_property = self_property.map(|entry| !entry.value.is_resolved());
2691            Ok(out_property.into_dyn_wrapped())
2692        },
2693    );
2694    map.insert(
2695        "conflict_side_count",
2696        |_language, _diagnostics, _build_ctx, self_property, function| {
2697            function.expect_no_arguments()?;
2698            let out_property = self_property
2699                .and_then(|entry| Ok(i64::try_from(entry.value.simplify().num_sides())?));
2700            Ok(out_property.into_dyn_wrapped())
2701        },
2702    );
2703    map.insert(
2704        "file_type",
2705        |_language, _diagnostics, _build_ctx, self_property, function| {
2706            function.expect_no_arguments()?;
2707            let out_property =
2708                self_property.map(|entry| describe_file_type(&entry.value).to_owned());
2709            Ok(out_property.into_dyn_wrapped())
2710        },
2711    );
2712    map.insert(
2713        "executable",
2714        |_language, _diagnostics, _build_ctx, self_property, function| {
2715            function.expect_no_arguments()?;
2716            let out_property =
2717                self_property.map(|entry| is_executable_file(&entry.value).unwrap_or_default());
2718            Ok(out_property.into_dyn_wrapped())
2719        },
2720    );
2721    map
2722}
2723
2724fn describe_file_type(value: &MergedTreeValue) -> &'static str {
2725    match value.as_resolved() {
2726        Some(Some(TreeValue::File { .. })) => "file",
2727        Some(Some(TreeValue::Symlink(_))) => "symlink",
2728        Some(Some(TreeValue::Tree(_))) => "tree",
2729        Some(Some(TreeValue::GitSubmodule(_))) => "git-submodule",
2730        Some(None) => "", // absent
2731        None => "conflict",
2732    }
2733}
2734
2735fn is_executable_file(value: &MergedTreeValue) -> Option<bool> {
2736    let executable = value.to_executable_merge()?;
2737    conflicts::resolve_file_executable(&executable)
2738}
2739
2740/// [`DiffStats`] with rendering parameters.
2741#[derive(Clone, Debug)]
2742pub struct DiffStatsFormatted<'a> {
2743    stats: DiffStats,
2744    path_converter: &'a RepoPathUiConverter,
2745    width: usize,
2746}
2747
2748impl Template for DiffStatsFormatted<'_> {
2749    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2750        diff_util::show_diff_stats(
2751            formatter.as_mut(),
2752            &self.stats,
2753            self.path_converter,
2754            self.width,
2755        )
2756    }
2757}
2758
2759fn builtin_diff_stats_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStats> {
2760    // Not using maplit::hashmap!{} or custom declarative macro here because
2761    // code completion inside macro is quite restricted.
2762    let mut map = CommitTemplateBuildMethodFnMap::<DiffStats>::new();
2763    map.insert(
2764        "files",
2765        |_language, _diagnostics, _build_ctx, self_property, function| {
2766            function.expect_no_arguments()?;
2767            let out_property = self_property.and_then(|diff| Ok(diff.entries().to_vec()));
2768            Ok(out_property.into_dyn_wrapped())
2769        },
2770    );
2771    map.insert(
2772        "total_added",
2773        |_language, _diagnostics, _build_ctx, self_property, function| {
2774            function.expect_no_arguments()?;
2775            let out_property =
2776                self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_added())?));
2777            Ok(out_property.into_dyn_wrapped())
2778        },
2779    );
2780    map.insert(
2781        "total_removed",
2782        |_language, _diagnostics, _build_ctx, self_property, function| {
2783            function.expect_no_arguments()?;
2784            let out_property =
2785                self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_removed())?));
2786            Ok(out_property.into_dyn_wrapped())
2787        },
2788    );
2789    map
2790}
2791
2792fn builtin_diff_stat_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStatEntry>
2793{
2794    // Not using maplit::hashmap!{} or custom declarative macro here because
2795    // code completion inside macro is quite restricted.
2796    let mut map = CommitTemplateBuildMethodFnMap::<DiffStatEntry>::new();
2797    map.insert(
2798        "path",
2799        |_language, _diagnostics, _build_ctx, self_property, function| {
2800            function.expect_no_arguments()?;
2801            let out_property = self_property.map(|entry| entry.path.target);
2802            Ok(out_property.into_dyn_wrapped())
2803        },
2804    );
2805    map.insert(
2806        "display_diff_path",
2807        |language, _diagnostics, _build_ctx, self_property, function| {
2808            function.expect_no_arguments()?;
2809            let path_converter = language.path_converter;
2810            let out_property =
2811                self_property.map(move |entry| format_diff_path(&entry.path, path_converter));
2812            Ok(out_property.into_dyn_wrapped())
2813        },
2814    );
2815    map.insert(
2816        "status",
2817        |_language, _diagnostics, _build_ctx, self_property, function| {
2818            function.expect_no_arguments()?;
2819            let out_property = self_property.map(|entry| entry.status.label().to_owned());
2820            Ok(out_property.into_dyn_wrapped())
2821        },
2822    );
2823    map.insert(
2824        "status_char",
2825        |_language, _diagnostics, _build_ctx, self_property, function| {
2826            function.expect_no_arguments()?;
2827            let out_property = self_property.map(|entry| entry.status.char().to_string());
2828            Ok(out_property.into_dyn_wrapped())
2829        },
2830    );
2831    map.insert(
2832        "lines_added",
2833        |_language, _diagnostics, _build_ctx, self_property, function| {
2834            function.expect_no_arguments()?;
2835            let out_property = self_property.and_then(|entry| {
2836                Ok(i64::try_from(
2837                    entry.added_removed.map_or(0, |(added, _)| added),
2838                )?)
2839            });
2840            Ok(out_property.into_dyn_wrapped())
2841        },
2842    );
2843    map.insert(
2844        "lines_removed",
2845        |_language, _diagnostics, _build_ctx, self_property, function| {
2846            function.expect_no_arguments()?;
2847            let out_property = self_property.and_then(|entry| {
2848                Ok(i64::try_from(
2849                    entry.added_removed.map_or(0, |(_, removed)| removed),
2850                )?)
2851            });
2852            Ok(out_property.into_dyn_wrapped())
2853        },
2854    );
2855    map.insert(
2856        "bytes_delta",
2857        |_language, _diagnostics, _build_ctx, self_property, function| {
2858            function.expect_no_arguments()?;
2859            let out_property =
2860                self_property.and_then(|entry| Ok(i64::try_from(entry.bytes_delta)?));
2861            Ok(out_property.into_dyn_wrapped())
2862        },
2863    );
2864    map
2865}
2866
2867#[derive(Debug)]
2868pub struct CryptographicSignature {
2869    commit: Commit,
2870}
2871
2872impl CryptographicSignature {
2873    fn new(commit: Commit) -> Option<Self> {
2874        commit.is_signed().then_some(Self { commit })
2875    }
2876
2877    fn verify(&self) -> SignResult<Verification> {
2878        self.commit
2879            .verification()
2880            .transpose()
2881            .expect("must have signature")
2882    }
2883
2884    fn status(&self) -> SignResult<SigStatus> {
2885        self.verify().map(|verification| verification.status)
2886    }
2887
2888    /// Defaults to empty string if key is not present.
2889    fn key(&self) -> SignResult<String> {
2890        self.verify()
2891            .map(|verification| verification.key.unwrap_or_default())
2892    }
2893
2894    /// Defaults to empty string if display is not present.
2895    fn display(&self) -> SignResult<String> {
2896        self.verify()
2897            .map(|verification| verification.display.unwrap_or_default())
2898    }
2899}
2900
2901fn builtin_cryptographic_signature_methods<'repo>()
2902-> CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature> {
2903    // Not using maplit::hashmap!{} or custom declarative macro here because
2904    // code completion inside macro is quite restricted.
2905    let mut map = CommitTemplateBuildMethodFnMap::<CryptographicSignature>::new();
2906    map.insert(
2907        "status",
2908        |_language, _diagnostics, _build_ctx, self_property, function| {
2909            function.expect_no_arguments()?;
2910            let out_property = self_property.and_then(|sig| match sig.status() {
2911                Ok(status) => Ok(status.to_string()),
2912                Err(SignError::InvalidSignatureFormat) => Ok("invalid".to_string()),
2913                Err(err) => Err(err.into()),
2914            });
2915            Ok(out_property.into_dyn_wrapped())
2916        },
2917    );
2918    map.insert(
2919        "key",
2920        |_language, _diagnostics, _build_ctx, self_property, function| {
2921            function.expect_no_arguments()?;
2922            let out_property = self_property.and_then(|sig| Ok(sig.key()?));
2923            Ok(out_property.into_dyn_wrapped())
2924        },
2925    );
2926    map.insert(
2927        "display",
2928        |_language, _diagnostics, _build_ctx, self_property, function| {
2929            function.expect_no_arguments()?;
2930            let out_property = self_property.and_then(|sig| Ok(sig.display()?));
2931            Ok(out_property.into_dyn_wrapped())
2932        },
2933    );
2934    map
2935}
2936
2937#[derive(Debug, Clone)]
2938pub struct AnnotationLine {
2939    pub commit: Commit,
2940    pub content: BString,
2941    pub line_number: usize,
2942    pub original_line_number: usize,
2943    pub first_line_in_hunk: bool,
2944}
2945
2946fn builtin_annotation_line_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>
2947{
2948    let mut map = CommitTemplateBuildMethodFnMap::<AnnotationLine>::new();
2949    map.insert(
2950        "commit",
2951        |_language, _diagnostics, _build_ctx, self_property, function| {
2952            function.expect_no_arguments()?;
2953            let out_property = self_property.map(|line| line.commit);
2954            Ok(out_property.into_dyn_wrapped())
2955        },
2956    );
2957    map.insert(
2958        "content",
2959        |_language, _diagnostics, _build_ctx, self_property, function| {
2960            function.expect_no_arguments()?;
2961            let out_property = self_property.map(|line| line.content);
2962            Ok(out_property.into_dyn_wrapped())
2963        },
2964    );
2965    map.insert(
2966        "line_number",
2967        |_language, _diagnostics, _build_ctx, self_property, function| {
2968            function.expect_no_arguments()?;
2969            let out_property = self_property.and_then(|line| Ok(i64::try_from(line.line_number)?));
2970            Ok(out_property.into_dyn_wrapped())
2971        },
2972    );
2973    map.insert(
2974        "original_line_number",
2975        |_language, _diagnostics, _build_ctx, self_property, function| {
2976            function.expect_no_arguments()?;
2977            let out_property =
2978                self_property.and_then(|line| Ok(i64::try_from(line.original_line_number)?));
2979            Ok(out_property.into_dyn_wrapped())
2980        },
2981    );
2982    map.insert(
2983        "first_line_in_hunk",
2984        |_language, _diagnostics, _build_ctx, self_property, function| {
2985            function.expect_no_arguments()?;
2986            let out_property = self_property.map(|line| line.first_line_in_hunk);
2987            Ok(out_property.into_dyn_wrapped())
2988        },
2989    );
2990    map
2991}
2992
2993impl Template for Trailer {
2994    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2995        write!(formatter, "{}: {}", self.key, self.value)
2996    }
2997}
2998
2999impl Template for Vec<Trailer> {
3000    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
3001        templater::format_joined(formatter, self, "\n")
3002    }
3003}
3004
3005fn builtin_trailer_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Trailer> {
3006    let mut map = CommitTemplateBuildMethodFnMap::<Trailer>::new();
3007    map.insert(
3008        "key",
3009        |_language, _diagnostics, _build_ctx, self_property, function| {
3010            function.expect_no_arguments()?;
3011            let out_property = self_property.map(|trailer| trailer.key);
3012            Ok(out_property.into_dyn_wrapped())
3013        },
3014    );
3015    map.insert(
3016        "value",
3017        |_language, _diagnostics, _build_ctx, self_property, function| {
3018            function.expect_no_arguments()?;
3019            let out_property = self_property.map(|trailer| trailer.value);
3020            Ok(out_property.into_dyn_wrapped())
3021        },
3022    );
3023    map
3024}
3025
3026fn builtin_trailer_list_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Vec<Trailer>> {
3027    let mut map: CommitTemplateBuildMethodFnMap<Vec<Trailer>> =
3028        template_builder::builtin_formattable_list_methods();
3029    map.insert(
3030        "contains_key",
3031        |language, diagnostics, build_ctx, self_property, function| {
3032            let [key_node] = function.expect_exact_arguments()?;
3033            let key_property =
3034                expect_stringify_expression(language, diagnostics, build_ctx, key_node)?;
3035            let out_property = (self_property, key_property)
3036                .map(|(trailers, key)| trailers.iter().any(|t| t.key == key));
3037            Ok(out_property.into_dyn_wrapped())
3038        },
3039    );
3040    map
3041}
3042
3043#[cfg(test)]
3044mod tests {
3045    use std::path::Component;
3046    use std::path::Path;
3047    use std::path::PathBuf;
3048
3049    use jj_lib::config::ConfigLayer;
3050    use jj_lib::config::ConfigSource;
3051    use jj_lib::fileset::FilesetAliasesMap;
3052    use jj_lib::revset::RevsetAliasesMap;
3053    use jj_lib::revset::RevsetExpression;
3054    use jj_lib::revset::RevsetExtensions;
3055    use jj_lib::revset::RevsetWorkspaceContext;
3056    use testutils::TestRepoBackend;
3057    use testutils::TestWorkspace;
3058    use testutils::repo_path_buf;
3059
3060    use super::*;
3061    use crate::template_parser::TemplateAliasesMap;
3062    use crate::templater::TemplateRenderer;
3063    use crate::templater::WrapTemplateProperty;
3064
3065    // TemplateBuildFunctionFn defined for<'a>
3066    type BuildFunctionFn = for<'a> fn(
3067        &CommitTemplateLanguage<'a>,
3068        &mut TemplateDiagnostics,
3069        &BuildContext<CommitTemplatePropertyKind<'a>>,
3070        &FunctionCallNode,
3071    ) -> TemplateParseResult<CommitTemplatePropertyKind<'a>>;
3072
3073    struct CommitTemplateTestEnv {
3074        test_workspace: TestWorkspace,
3075        path_converter: RepoPathUiConverter,
3076        revset_extensions: Arc<RevsetExtensions>,
3077        id_prefix_context: IdPrefixContext,
3078        fileset_aliases_map: FilesetAliasesMap,
3079        revset_aliases_map: RevsetAliasesMap,
3080        template_aliases_map: TemplateAliasesMap,
3081        immutable_expression: Arc<UserRevsetExpression>,
3082        extra_functions: HashMap<&'static str, BuildFunctionFn>,
3083    }
3084
3085    impl CommitTemplateTestEnv {
3086        fn init() -> Self {
3087            // Stabilize commit id of the initialized working copy
3088            let settings = stable_settings();
3089            let test_workspace =
3090                TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &settings);
3091            let path_converter = RepoPathUiConverter::Fs {
3092                cwd: test_workspace.workspace.workspace_root().to_owned(),
3093                base: test_workspace.workspace.workspace_root().to_owned(),
3094            };
3095            let revset_extensions = Arc::new(RevsetExtensions::new());
3096            let id_prefix_context = IdPrefixContext::new(revset_extensions.clone());
3097            Self {
3098                test_workspace,
3099                path_converter,
3100                revset_extensions,
3101                id_prefix_context,
3102                fileset_aliases_map: FilesetAliasesMap::new(),
3103                revset_aliases_map: RevsetAliasesMap::new(),
3104                template_aliases_map: TemplateAliasesMap::new(),
3105                immutable_expression: RevsetExpression::none(),
3106                extra_functions: HashMap::new(),
3107            }
3108        }
3109
3110        fn set_base_and_cwd(&mut self, base: PathBuf, cwd: impl AsRef<Path>) {
3111            self.path_converter = RepoPathUiConverter::Fs {
3112                cwd: base.join(cwd),
3113                base,
3114            };
3115        }
3116
3117        fn add_function(&mut self, name: &'static str, f: BuildFunctionFn) {
3118            self.extra_functions.insert(name, f);
3119        }
3120
3121        fn new_language(&self) -> CommitTemplateLanguage<'_> {
3122            let revset_parse_context = RevsetParseContext {
3123                aliases_map: &self.revset_aliases_map,
3124                local_variables: HashMap::new(),
3125                user_email: "test.user@example.com",
3126                date_pattern_context: chrono::DateTime::UNIX_EPOCH.fixed_offset().into(),
3127                default_ignored_remote: None,
3128                fileset_aliases_map: &self.fileset_aliases_map,
3129                use_glob_by_default: true,
3130                extensions: &self.revset_extensions,
3131                workspace: Some(RevsetWorkspaceContext {
3132                    path_converter: &self.path_converter,
3133                    workspace_name: self.test_workspace.workspace.workspace_name(),
3134                }),
3135            };
3136            let mut language = CommitTemplateLanguage::new(
3137                self.test_workspace.repo.as_ref(),
3138                &self.path_converter,
3139                self.test_workspace.workspace.workspace_name(),
3140                revset_parse_context,
3141                &self.id_prefix_context,
3142                self.immutable_expression.clone(),
3143                ConflictMarkerStyle::Diff,
3144                &[] as &[Box<dyn CommitTemplateLanguageExtension>],
3145            );
3146            // Not using .extend() to infer lifetime of f
3147            for (&name, &f) in &self.extra_functions {
3148                language.build_fn_table.core.functions.insert(name, f);
3149            }
3150            language
3151        }
3152
3153        fn parse<'a, C>(&'a self, text: &str) -> TemplateParseResult<TemplateRenderer<'a, C>>
3154        where
3155            C: Clone + 'a,
3156            CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>,
3157        {
3158            let language = self.new_language();
3159            let mut diagnostics = TemplateDiagnostics::new();
3160            template_builder::parse(
3161                &language,
3162                &mut diagnostics,
3163                text,
3164                &self.template_aliases_map,
3165            )
3166        }
3167
3168        fn render_ok<'a, C>(&'a self, text: &str, context: &C) -> BString
3169        where
3170            C: Clone + 'a,
3171            CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>,
3172        {
3173            let template = self.parse(text).unwrap();
3174            template.format_plain_text(context).into()
3175        }
3176    }
3177
3178    fn stable_settings() -> UserSettings {
3179        let mut config = testutils::base_user_config();
3180        let mut layer = ConfigLayer::empty(ConfigSource::User);
3181        layer
3182            .set_value("debug.commit-timestamp", "2001-02-03T04:05:06+07:00")
3183            .unwrap();
3184        config.add_layer(layer);
3185        UserSettings::from_config(config).unwrap()
3186    }
3187
3188    #[test]
3189    fn test_ref_symbol_type() {
3190        let mut env = CommitTemplateTestEnv::init();
3191        env.add_function("sym", |language, diagnostics, build_ctx, function| {
3192            let [value_node] = function.expect_exact_arguments()?;
3193            let value = expect_stringify_expression(language, diagnostics, build_ctx, value_node)?;
3194            let out_property = value.map(RefSymbolBuf);
3195            Ok(out_property.into_dyn_wrapped())
3196        });
3197        let sym = |s: &str| RefSymbolBuf(s.to_owned());
3198
3199        // default formatting
3200        insta::assert_snapshot!(env.render_ok("self", &sym("")), @r#""""#);
3201        insta::assert_snapshot!(env.render_ok("self", &sym("foo")), @"foo");
3202        insta::assert_snapshot!(env.render_ok("self", &sym("foo bar")), @r#""foo bar""#);
3203
3204        // comparison
3205        insta::assert_snapshot!(env.render_ok("self == 'foo'", &sym("foo")), @"true");
3206        insta::assert_snapshot!(env.render_ok("'bar' == self", &sym("foo")), @"false");
3207        insta::assert_snapshot!(env.render_ok("self == self", &sym("foo")), @"true");
3208        insta::assert_snapshot!(env.render_ok("self == sym('bar')", &sym("foo")), @"false");
3209
3210        insta::assert_snapshot!(env.render_ok("self == 'bar'", &Some(sym("foo"))), @"false");
3211        insta::assert_snapshot!(env.render_ok("self == sym('foo')", &Some(sym("foo"))), @"true");
3212        insta::assert_snapshot!(env.render_ok("'foo' == self", &Some(sym("foo"))), @"true");
3213        insta::assert_snapshot!(env.render_ok("sym('bar') == self", &Some(sym("foo"))), @"false");
3214        insta::assert_snapshot!(env.render_ok("self == self", &Some(sym("foo"))), @"true");
3215        insta::assert_snapshot!(env.render_ok("self == ''", &None::<RefSymbolBuf>), @"false");
3216        insta::assert_snapshot!(env.render_ok("sym('') == self", &None::<RefSymbolBuf>), @"false");
3217        insta::assert_snapshot!(env.render_ok("self == self", &None::<RefSymbolBuf>), @"true");
3218
3219        // string cast != formatting: it would be weird if function argument of
3220        // string type were quoted/escaped. (e.g. `"foo".contains(bookmark)`)
3221        insta::assert_snapshot!(env.render_ok("stringify(self)", &sym("a b")), @"a b");
3222        insta::assert_snapshot!(env.render_ok("stringify(self)", &Some(sym("a b"))), @"a b");
3223        insta::assert_snapshot!(env.render_ok("stringify(self)", &None::<RefSymbolBuf>), @"");
3224
3225        // string methods
3226        insta::assert_snapshot!(env.render_ok("self.len()", &sym("a b")), @"3");
3227
3228        // JSON
3229        insta::assert_snapshot!(env.render_ok("json(self)", &sym("foo bar")), @r#""foo bar""#);
3230    }
3231
3232    #[test]
3233    fn test_repo_path_type() {
3234        let mut env = CommitTemplateTestEnv::init();
3235        let mut base = PathBuf::from(Component::RootDir.as_os_str());
3236        base.extend(["path", "to", "repo"]);
3237        env.set_base_and_cwd(base, "dir");
3238
3239        // slash-separated by default
3240        insta::assert_snapshot!(
3241            env.render_ok("self", &repo_path_buf("dir/file")), @"dir/file");
3242
3243        // .absolute() to convert to absolute path.
3244        if cfg!(windows) {
3245            insta::assert_snapshot!(
3246                env.render_ok("self.absolute()", &repo_path_buf("file")),
3247                @"\\path\\to\\repo\\file");
3248            insta::assert_snapshot!(
3249                env.render_ok("self.absolute()", &repo_path_buf("dir/file")),
3250                @"\\path\\to\\repo\\dir\\file");
3251        } else {
3252            insta::assert_snapshot!(
3253                env.render_ok("self.absolute()", &repo_path_buf("file")), @"/path/to/repo/file");
3254            insta::assert_snapshot!(
3255                env.render_ok("self.absolute()", &repo_path_buf("dir/file")),
3256                @"/path/to/repo/dir/file");
3257        }
3258
3259        // .display() to convert to filesystem path
3260        insta::assert_snapshot!(
3261            env.render_ok("self.display()", &repo_path_buf("dir/file")), @"file");
3262        if cfg!(windows) {
3263            insta::assert_snapshot!(
3264                env.render_ok("self.display()", &repo_path_buf("file")), @"..\\file");
3265        } else {
3266            insta::assert_snapshot!(
3267                env.render_ok("self.display()", &repo_path_buf("file")), @"../file");
3268        }
3269
3270        let template = "if(self.parent(), self.parent(), '<none>')";
3271        insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("")), @"<none>");
3272        insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("file")), @"");
3273        insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("dir/file")), @"dir");
3274
3275        // JSON
3276        insta::assert_snapshot!(
3277            env.render_ok("json(self)", &repo_path_buf("dir/file")), @r#""dir/file""#);
3278        insta::assert_snapshot!(
3279            env.render_ok("json(self)", &None::<RepoPathBuf>), @"null");
3280    }
3281
3282    #[test]
3283    fn test_commit_id_type() {
3284        let env = CommitTemplateTestEnv::init();
3285
3286        let id = CommitId::from_hex("08a70ab33d7143b7130ed8594d8216ef688623c0");
3287        insta::assert_snapshot!(
3288            env.render_ok("self", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
3289
3290        insta::assert_snapshot!(env.render_ok("self.short()", &id), @"08a70ab33d71");
3291        insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @"");
3292        insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @"");
3293        insta::assert_snapshot!(
3294            env.render_ok("self.short(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
3295        insta::assert_snapshot!(
3296            env.render_ok("self.short(-100)", &id),
3297            @"<Error: out of range integral type conversion attempted>");
3298
3299        insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"08");
3300        insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"08");
3301        insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"08");
3302        insta::assert_snapshot!(
3303            env.render_ok("self.shortest(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
3304        insta::assert_snapshot!(
3305            env.render_ok("self.shortest(-100)", &id),
3306            @"<Error: out of range integral type conversion attempted>");
3307
3308        // JSON
3309        insta::assert_snapshot!(
3310            env.render_ok("json(self)", &id), @r#""08a70ab33d7143b7130ed8594d8216ef688623c0""#);
3311    }
3312
3313    #[test]
3314    fn test_change_id_type() {
3315        let env = CommitTemplateTestEnv::init();
3316
3317        let id = ChangeId::from_hex("ffdaa62087a280bddc5e3d3ff933b8ae");
3318        insta::assert_snapshot!(
3319            env.render_ok("self", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
3320        insta::assert_snapshot!(
3321            env.render_ok("self.normal_hex()", &id), @"ffdaa62087a280bddc5e3d3ff933b8ae");
3322
3323        insta::assert_snapshot!(env.render_ok("self.short()", &id), @"kkmpptxzrspx");
3324        insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @"");
3325        insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @"");
3326        insta::assert_snapshot!(
3327            env.render_ok("self.short(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
3328        insta::assert_snapshot!(
3329            env.render_ok("self.short(-100)", &id),
3330            @"<Error: out of range integral type conversion attempted>");
3331
3332        insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"k");
3333        insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"k");
3334        insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"k");
3335        insta::assert_snapshot!(
3336            env.render_ok("self.shortest(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
3337        insta::assert_snapshot!(
3338            env.render_ok("self.shortest(-100)", &id),
3339            @"<Error: out of range integral type conversion attempted>");
3340
3341        // JSON
3342        insta::assert_snapshot!(
3343            env.render_ok("json(self)", &id), @r#""kkmpptxzrspxrzommnulwmwkkqwworpl""#);
3344    }
3345
3346    #[test]
3347    fn test_shortest_id_prefix_type() {
3348        let env = CommitTemplateTestEnv::init();
3349
3350        let id = ShortestIdPrefix {
3351            prefix: "012".to_owned(),
3352            rest: "3abcdef".to_owned(),
3353        };
3354
3355        // JSON
3356        insta::assert_snapshot!(
3357            env.render_ok("json(self)", &id), @r#"{"prefix":"012","rest":"3abcdef"}"#);
3358    }
3359}