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