Skip to main content

jj_cli/
commit_templater.rs

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