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