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