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