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