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