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