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