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