jj_cli/
commit_templater.rs

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