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