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