Skip to main content

jj_cli/
commit_templater.rs

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