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