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