1use std::any::Any;
16use std::cmp::max;
17use std::cmp::Ordering;
18use std::collections::HashMap;
19use std::fmt;
20use std::fmt::Display;
21use std::io;
22use std::rc::Rc;
23
24use bstr::BString;
25use futures::stream::BoxStream;
26use futures::StreamExt as _;
27use futures::TryStreamExt as _;
28use itertools::Itertools as _;
29use jj_lib::backend::BackendResult;
30use jj_lib::backend::ChangeId;
31use jj_lib::backend::CommitId;
32use jj_lib::backend::TreeValue;
33use jj_lib::commit::Commit;
34use jj_lib::conflicts;
35use jj_lib::conflicts::ConflictMarkerStyle;
36use jj_lib::copies::CopiesTreeDiffEntry;
37use jj_lib::copies::CopiesTreeDiffEntryPath;
38use jj_lib::copies::CopyRecords;
39use jj_lib::extensions_map::ExtensionsMap;
40use jj_lib::fileset;
41use jj_lib::fileset::FilesetDiagnostics;
42use jj_lib::fileset::FilesetExpression;
43use jj_lib::id_prefix::IdPrefixContext;
44use jj_lib::id_prefix::IdPrefixIndex;
45use jj_lib::matchers::Matcher;
46use jj_lib::merge::MergedTreeValue;
47use jj_lib::merged_tree::MergedTree;
48use jj_lib::object_id::ObjectId as _;
49use jj_lib::op_store::RefTarget;
50use jj_lib::op_store::RemoteRef;
51use jj_lib::ref_name::WorkspaceName;
52use jj_lib::ref_name::WorkspaceNameBuf;
53use jj_lib::repo::Repo;
54use jj_lib::repo_path::RepoPathBuf;
55use jj_lib::repo_path::RepoPathUiConverter;
56use jj_lib::revset;
57use jj_lib::revset::Revset;
58use jj_lib::revset::RevsetContainingFn;
59use jj_lib::revset::RevsetDiagnostics;
60use jj_lib::revset::RevsetModifier;
61use jj_lib::revset::RevsetParseContext;
62use jj_lib::revset::UserRevsetExpression;
63use jj_lib::settings::UserSettings;
64use jj_lib::signing::SigStatus;
65use jj_lib::signing::SignError;
66use jj_lib::signing::SignResult;
67use jj_lib::signing::Verification;
68use jj_lib::store::Store;
69use jj_lib::trailer;
70use jj_lib::trailer::Trailer;
71use once_cell::unsync::OnceCell;
72use pollster::FutureExt as _;
73use serde::Serialize as _;
74
75use crate::diff_util;
76use crate::diff_util::DiffStats;
77use crate::formatter::Formatter;
78use crate::revset_util;
79use crate::template_builder;
80use crate::template_builder::expect_stringify_expression;
81use crate::template_builder::merge_fn_map;
82use crate::template_builder::BuildContext;
83use crate::template_builder::CoreTemplateBuildFnTable;
84use crate::template_builder::CoreTemplatePropertyKind;
85use crate::template_builder::CoreTemplatePropertyVar;
86use crate::template_builder::TemplateBuildMethodFnMap;
87use crate::template_builder::TemplateLanguage;
88use crate::template_parser;
89use crate::template_parser::ExpressionNode;
90use crate::template_parser::FunctionCallNode;
91use crate::template_parser::TemplateDiagnostics;
92use crate::template_parser::TemplateParseError;
93use crate::template_parser::TemplateParseResult;
94use crate::templater;
95use crate::templater::BoxedSerializeProperty;
96use crate::templater::BoxedTemplateProperty;
97use crate::templater::ListTemplate;
98use crate::templater::PlainTextFormattedProperty;
99use crate::templater::SizeHint;
100use crate::templater::Template;
101use crate::templater::TemplateFormatter;
102use crate::templater::TemplatePropertyError;
103use crate::templater::TemplatePropertyExt as _;
104
105pub trait CommitTemplateLanguageExtension {
106 fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>;
107
108 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
109}
110
111pub struct CommitTemplateLanguage<'repo> {
112 repo: &'repo dyn Repo,
113 path_converter: &'repo RepoPathUiConverter,
114 workspace_name: WorkspaceNameBuf,
115 revset_parse_context: RevsetParseContext<'repo>,
121 id_prefix_context: &'repo IdPrefixContext,
122 immutable_expression: Rc<UserRevsetExpression>,
123 conflict_marker_style: ConflictMarkerStyle,
124 build_fn_table: CommitTemplateBuildFnTable<'repo>,
125 keyword_cache: CommitKeywordCache<'repo>,
126 cache_extensions: ExtensionsMap,
127}
128
129impl<'repo> CommitTemplateLanguage<'repo> {
130 #[expect(clippy::too_many_arguments)]
133 pub fn new(
134 repo: &'repo dyn Repo,
135 path_converter: &'repo RepoPathUiConverter,
136 workspace_name: &WorkspaceName,
137 revset_parse_context: RevsetParseContext<'repo>,
138 id_prefix_context: &'repo IdPrefixContext,
139 immutable_expression: Rc<UserRevsetExpression>,
140 conflict_marker_style: ConflictMarkerStyle,
141 extensions: &[impl AsRef<dyn CommitTemplateLanguageExtension>],
142 ) -> Self {
143 let mut build_fn_table = CommitTemplateBuildFnTable::builtin();
144 let mut cache_extensions = ExtensionsMap::empty();
145
146 for extension in extensions {
147 build_fn_table.merge(extension.as_ref().build_fn_table());
148 extension
149 .as_ref()
150 .build_cache_extensions(&mut cache_extensions);
151 }
152
153 CommitTemplateLanguage {
154 repo,
155 path_converter,
156 workspace_name: workspace_name.to_owned(),
157 revset_parse_context,
158 id_prefix_context,
159 immutable_expression,
160 conflict_marker_style,
161 build_fn_table,
162 keyword_cache: CommitKeywordCache::default(),
163 cache_extensions,
164 }
165 }
166}
167
168impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
169 type Property = CommitTemplatePropertyKind<'repo>;
170
171 fn settings(&self) -> &UserSettings {
172 self.repo.base_repo().settings()
173 }
174
175 fn build_function(
176 &self,
177 diagnostics: &mut TemplateDiagnostics,
178 build_ctx: &BuildContext<Self::Property>,
179 function: &FunctionCallNode,
180 ) -> TemplateParseResult<Self::Property> {
181 let table = &self.build_fn_table.core;
182 table.build_function(self, diagnostics, build_ctx, function)
183 }
184
185 fn build_method(
186 &self,
187 diagnostics: &mut TemplateDiagnostics,
188 build_ctx: &BuildContext<Self::Property>,
189 property: Self::Property,
190 function: &FunctionCallNode,
191 ) -> TemplateParseResult<Self::Property> {
192 let type_name = property.type_name();
193 match property {
194 CommitTemplatePropertyKind::Core(property) => {
195 let table = &self.build_fn_table.core;
196 table.build_method(self, diagnostics, build_ctx, property, function)
197 }
198 CommitTemplatePropertyKind::Commit(property) => {
199 let table = &self.build_fn_table.commit_methods;
200 let build = template_parser::lookup_method(type_name, table, function)?;
201 build(self, diagnostics, build_ctx, property, function)
202 }
203 CommitTemplatePropertyKind::CommitOpt(property) => {
204 let type_name = "Commit";
205 let table = &self.build_fn_table.commit_methods;
206 let build = template_parser::lookup_method(type_name, table, function)?;
207 let inner_property = property.try_unwrap(type_name).into_dyn();
208 build(self, diagnostics, build_ctx, inner_property, function)
209 }
210 CommitTemplatePropertyKind::CommitList(property) => {
211 let table = &self.build_fn_table.commit_list_methods;
212 let build = template_parser::lookup_method(type_name, table, function)?;
213 build(self, diagnostics, build_ctx, property, function)
214 }
215 CommitTemplatePropertyKind::CommitRef(property) => {
216 let table = &self.build_fn_table.commit_ref_methods;
217 let build = template_parser::lookup_method(type_name, table, function)?;
218 build(self, diagnostics, build_ctx, property, function)
219 }
220 CommitTemplatePropertyKind::CommitRefOpt(property) => {
221 let type_name = "CommitRef";
222 let table = &self.build_fn_table.commit_ref_methods;
223 let build = template_parser::lookup_method(type_name, table, function)?;
224 let inner_property = property.try_unwrap(type_name).into_dyn();
225 build(self, diagnostics, build_ctx, inner_property, function)
226 }
227 CommitTemplatePropertyKind::CommitRefList(property) => {
228 let table = &self.build_fn_table.commit_ref_list_methods;
229 let build = template_parser::lookup_method(type_name, table, function)?;
230 build(self, diagnostics, build_ctx, property, function)
231 }
232 CommitTemplatePropertyKind::RefSymbol(property) => {
233 let table = &self.build_fn_table.core.string_methods;
234 let build = template_parser::lookup_method(type_name, table, function)?;
235 let inner_property = property.map(|RefSymbolBuf(s)| s).into_dyn();
236 build(self, diagnostics, build_ctx, inner_property, function)
237 }
238 CommitTemplatePropertyKind::RefSymbolOpt(property) => {
239 let type_name = "RefSymbol";
240 let table = &self.build_fn_table.core.string_methods;
241 let build = template_parser::lookup_method(type_name, table, function)?;
242 let inner_property = property
243 .try_unwrap(type_name)
244 .map(|RefSymbolBuf(s)| s)
245 .into_dyn();
246 build(self, diagnostics, build_ctx, inner_property, function)
247 }
248 CommitTemplatePropertyKind::RepoPath(property) => {
249 let table = &self.build_fn_table.repo_path_methods;
250 let build = template_parser::lookup_method(type_name, table, function)?;
251 build(self, diagnostics, build_ctx, property, function)
252 }
253 CommitTemplatePropertyKind::RepoPathOpt(property) => {
254 let type_name = "RepoPath";
255 let table = &self.build_fn_table.repo_path_methods;
256 let build = template_parser::lookup_method(type_name, table, function)?;
257 let inner_property = property.try_unwrap(type_name).into_dyn();
258 build(self, diagnostics, build_ctx, inner_property, function)
259 }
260 CommitTemplatePropertyKind::ChangeId(property) => {
261 let table = &self.build_fn_table.change_id_methods;
262 let build = template_parser::lookup_method(type_name, table, function)?;
263 build(self, diagnostics, build_ctx, property, function)
264 }
265 CommitTemplatePropertyKind::CommitId(property) => {
266 let table = &self.build_fn_table.commit_id_methods;
267 let build = template_parser::lookup_method(type_name, table, function)?;
268 build(self, diagnostics, build_ctx, property, function)
269 }
270 CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
271 let table = &self.build_fn_table.shortest_id_prefix_methods;
272 let build = template_parser::lookup_method(type_name, table, function)?;
273 build(self, diagnostics, build_ctx, property, function)
274 }
275 CommitTemplatePropertyKind::TreeDiff(property) => {
276 let table = &self.build_fn_table.tree_diff_methods;
277 let build = template_parser::lookup_method(type_name, table, function)?;
278 build(self, diagnostics, build_ctx, property, function)
279 }
280 CommitTemplatePropertyKind::TreeDiffEntry(property) => {
281 let table = &self.build_fn_table.tree_diff_entry_methods;
282 let build = template_parser::lookup_method(type_name, table, function)?;
283 build(self, diagnostics, build_ctx, property, function)
284 }
285 CommitTemplatePropertyKind::TreeDiffEntryList(property) => {
286 let table = &self.build_fn_table.tree_diff_entry_list_methods;
287 let build = template_parser::lookup_method(type_name, table, function)?;
288 build(self, diagnostics, build_ctx, property, function)
289 }
290 CommitTemplatePropertyKind::TreeEntry(property) => {
291 let table = &self.build_fn_table.tree_entry_methods;
292 let build = template_parser::lookup_method(type_name, table, function)?;
293 build(self, diagnostics, build_ctx, property, function)
294 }
295 CommitTemplatePropertyKind::DiffStats(property) => {
296 let table = &self.build_fn_table.diff_stats_methods;
297 let build = template_parser::lookup_method(type_name, table, function)?;
298 let property = property.map(|formatted| formatted.stats).into_dyn();
301 build(self, diagnostics, build_ctx, property, function)
302 }
303 CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => {
304 let type_name = "CryptographicSignature";
305 let table = &self.build_fn_table.cryptographic_signature_methods;
306 let build = template_parser::lookup_method(type_name, table, function)?;
307 let inner_property = property.try_unwrap(type_name).into_dyn();
308 build(self, diagnostics, build_ctx, inner_property, function)
309 }
310 CommitTemplatePropertyKind::AnnotationLine(property) => {
311 let type_name = "AnnotationLine";
312 let table = &self.build_fn_table.annotation_line_methods;
313 let build = template_parser::lookup_method(type_name, table, function)?;
314 build(self, diagnostics, build_ctx, property, function)
315 }
316 CommitTemplatePropertyKind::Trailer(property) => {
317 let table = &self.build_fn_table.trailer_methods;
318 let build = template_parser::lookup_method(type_name, table, function)?;
319 build(self, diagnostics, build_ctx, property, function)
320 }
321 CommitTemplatePropertyKind::TrailerList(property) => {
322 let table = &self.build_fn_table.trailer_list_methods;
323 let build = template_parser::lookup_method(type_name, table, function)?;
324 build(self, diagnostics, build_ctx, property, function)
325 }
326 }
327 }
328}
329
330impl<'repo> CommitTemplateLanguage<'repo> {
333 pub fn repo(&self) -> &'repo dyn Repo {
334 self.repo
335 }
336
337 pub fn workspace_name(&self) -> &WorkspaceName {
338 &self.workspace_name
339 }
340
341 pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> {
342 &self.keyword_cache
343 }
344
345 pub fn cache_extension<T: Any>(&self) -> Option<&T> {
346 self.cache_extensions.get::<T>()
347 }
348}
349
350pub enum CommitTemplatePropertyKind<'repo> {
351 Core(CoreTemplatePropertyKind<'repo>),
352 Commit(BoxedTemplateProperty<'repo, Commit>),
353 CommitOpt(BoxedTemplateProperty<'repo, Option<Commit>>),
354 CommitList(BoxedTemplateProperty<'repo, Vec<Commit>>),
355 CommitRef(BoxedTemplateProperty<'repo, Rc<CommitRef>>),
356 CommitRefOpt(BoxedTemplateProperty<'repo, Option<Rc<CommitRef>>>),
357 CommitRefList(BoxedTemplateProperty<'repo, Vec<Rc<CommitRef>>>),
358 RefSymbol(BoxedTemplateProperty<'repo, RefSymbolBuf>),
359 RefSymbolOpt(BoxedTemplateProperty<'repo, Option<RefSymbolBuf>>),
360 RepoPath(BoxedTemplateProperty<'repo, RepoPathBuf>),
361 RepoPathOpt(BoxedTemplateProperty<'repo, Option<RepoPathBuf>>),
362 ChangeId(BoxedTemplateProperty<'repo, ChangeId>),
363 CommitId(BoxedTemplateProperty<'repo, CommitId>),
364 ShortestIdPrefix(BoxedTemplateProperty<'repo, ShortestIdPrefix>),
365 TreeDiff(BoxedTemplateProperty<'repo, TreeDiff>),
366 TreeDiffEntry(BoxedTemplateProperty<'repo, TreeDiffEntry>),
367 TreeDiffEntryList(BoxedTemplateProperty<'repo, Vec<TreeDiffEntry>>),
368 TreeEntry(BoxedTemplateProperty<'repo, TreeEntry>),
369 DiffStats(BoxedTemplateProperty<'repo, DiffStatsFormatted<'repo>>),
370 CryptographicSignatureOpt(BoxedTemplateProperty<'repo, Option<CryptographicSignature>>),
371 AnnotationLine(BoxedTemplateProperty<'repo, AnnotationLine>),
372 Trailer(BoxedTemplateProperty<'repo, Trailer>),
373 TrailerList(BoxedTemplateProperty<'repo, Vec<Trailer>>),
374}
375
376template_builder::impl_core_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> => Core);
377template_builder::impl_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> {
378 Commit(Commit),
379 CommitOpt(Option<Commit>),
380 CommitList(Vec<Commit>),
381 CommitRef(Rc<CommitRef>),
382 CommitRefOpt(Option<Rc<CommitRef>>),
383 CommitRefList(Vec<Rc<CommitRef>>),
384 RefSymbol(RefSymbolBuf),
385 RefSymbolOpt(Option<RefSymbolBuf>),
386 RepoPath(RepoPathBuf),
387 RepoPathOpt(Option<RepoPathBuf>),
388 ChangeId(ChangeId),
389 CommitId(CommitId),
390 ShortestIdPrefix(ShortestIdPrefix),
391 TreeDiff(TreeDiff),
392 TreeDiffEntry(TreeDiffEntry),
393 TreeDiffEntryList(Vec<TreeDiffEntry>),
394 TreeEntry(TreeEntry),
395 DiffStats(DiffStatsFormatted<'repo>),
396 CryptographicSignatureOpt(Option<CryptographicSignature>),
397 AnnotationLine(AnnotationLine),
398 Trailer(Trailer),
399 TrailerList(Vec<Trailer>),
400});
401
402impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> {
403 fn wrap_template(template: Box<dyn Template + 'repo>) -> Self {
404 Self::Core(CoreTemplatePropertyKind::wrap_template(template))
405 }
406
407 fn wrap_list_template(template: Box<dyn ListTemplate + 'repo>) -> Self {
408 Self::Core(CoreTemplatePropertyKind::wrap_list_template(template))
409 }
410
411 fn type_name(&self) -> &'static str {
412 match self {
413 Self::Core(property) => property.type_name(),
414 Self::Commit(_) => "Commit",
415 Self::CommitOpt(_) => "Option<Commit>",
416 Self::CommitList(_) => "List<Commit>",
417 Self::CommitRef(_) => "CommitRef",
418 Self::CommitRefOpt(_) => "Option<CommitRef>",
419 Self::CommitRefList(_) => "List<CommitRef>",
420 Self::RefSymbol(_) => "RefSymbol",
421 Self::RefSymbolOpt(_) => "Option<RefSymbol>",
422 Self::RepoPath(_) => "RepoPath",
423 Self::RepoPathOpt(_) => "Option<RepoPath>",
424 Self::ChangeId(_) => "ChangeId",
425 Self::CommitId(_) => "CommitId",
426 Self::ShortestIdPrefix(_) => "ShortestIdPrefix",
427 Self::TreeDiff(_) => "TreeDiff",
428 Self::TreeDiffEntry(_) => "TreeDiffEntry",
429 Self::TreeDiffEntryList(_) => "List<TreeDiffEntry>",
430 Self::TreeEntry(_) => "TreeEntry",
431 Self::DiffStats(_) => "DiffStats",
432 Self::CryptographicSignatureOpt(_) => "Option<CryptographicSignature>",
433 Self::AnnotationLine(_) => "AnnotationLine",
434 Self::Trailer(_) => "Trailer",
435 Self::TrailerList(_) => "List<Trailer>",
436 }
437 }
438
439 fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'repo, bool>> {
440 match self {
441 Self::Core(property) => property.try_into_boolean(),
442 Self::Commit(_) => None,
443 Self::CommitOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
444 Self::CommitList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
445 Self::CommitRef(_) => None,
446 Self::CommitRefOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
447 Self::CommitRefList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
448 Self::RefSymbol(_) => None,
449 Self::RefSymbolOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
450 Self::RepoPath(_) => None,
451 Self::RepoPathOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
452 Self::ChangeId(_) => None,
453 Self::CommitId(_) => None,
454 Self::ShortestIdPrefix(_) => None,
455 Self::TreeDiff(_) => None,
458 Self::TreeDiffEntry(_) => None,
459 Self::TreeDiffEntryList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
460 Self::TreeEntry(_) => None,
461 Self::DiffStats(_) => None,
462 Self::CryptographicSignatureOpt(property) => {
463 Some(property.map(|sig| sig.is_some()).into_dyn())
464 }
465 Self::AnnotationLine(_) => None,
466 Self::Trailer(_) => None,
467 Self::TrailerList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
468 }
469 }
470
471 fn try_into_integer(self) -> Option<BoxedTemplateProperty<'repo, i64>> {
472 match self {
473 Self::Core(property) => property.try_into_integer(),
474 _ => None,
475 }
476 }
477
478 fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'repo, String>> {
479 match self {
480 Self::Core(property) => property.try_into_stringify(),
481 Self::RefSymbol(property) => Some(property.map(|RefSymbolBuf(s)| s).into_dyn()),
482 Self::RefSymbolOpt(property) => Some(
483 property
484 .try_unwrap("RefSymbol")
485 .map(|RefSymbolBuf(s)| s)
486 .into_dyn(),
487 ),
488 _ => {
489 let template = self.try_into_template()?;
490 Some(PlainTextFormattedProperty::new(template).into_dyn())
491 }
492 }
493 }
494
495 fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'repo>> {
496 match self {
497 Self::Core(property) => property.try_into_serialize(),
498 Self::Commit(property) => Some(property.into_serialize()),
499 Self::CommitOpt(property) => Some(property.into_serialize()),
500 Self::CommitList(property) => Some(property.into_serialize()),
501 Self::CommitRef(property) => Some(property.into_serialize()),
502 Self::CommitRefOpt(property) => Some(property.into_serialize()),
503 Self::CommitRefList(property) => Some(property.into_serialize()),
504 Self::RefSymbol(property) => Some(property.into_serialize()),
505 Self::RefSymbolOpt(property) => Some(property.into_serialize()),
506 Self::RepoPath(property) => Some(property.into_serialize()),
507 Self::RepoPathOpt(property) => Some(property.into_serialize()),
508 Self::ChangeId(property) => Some(property.into_serialize()),
509 Self::CommitId(property) => Some(property.into_serialize()),
510 Self::ShortestIdPrefix(property) => Some(property.into_serialize()),
511 Self::TreeDiff(_) => None,
512 Self::TreeDiffEntry(_) => None,
513 Self::TreeDiffEntryList(_) => None,
514 Self::TreeEntry(_) => None,
515 Self::DiffStats(_) => None,
516 Self::CryptographicSignatureOpt(_) => None,
517 Self::AnnotationLine(_) => None,
518 Self::Trailer(_) => None,
519 Self::TrailerList(_) => None,
520 }
521 }
522
523 fn try_into_template(self) -> Option<Box<dyn Template + 'repo>> {
524 match self {
525 Self::Core(property) => property.try_into_template(),
526 Self::Commit(_) => None,
527 Self::CommitOpt(_) => None,
528 Self::CommitList(_) => None,
529 Self::CommitRef(property) => Some(property.into_template()),
530 Self::CommitRefOpt(property) => Some(property.into_template()),
531 Self::CommitRefList(property) => Some(property.into_template()),
532 Self::RefSymbol(property) => Some(property.into_template()),
533 Self::RefSymbolOpt(property) => Some(property.into_template()),
534 Self::RepoPath(property) => Some(property.into_template()),
535 Self::RepoPathOpt(property) => Some(property.into_template()),
536 Self::ChangeId(property) => Some(property.into_template()),
537 Self::CommitId(property) => Some(property.into_template()),
538 Self::ShortestIdPrefix(property) => Some(property.into_template()),
539 Self::TreeDiff(_) => None,
540 Self::TreeDiffEntry(_) => None,
541 Self::TreeDiffEntryList(_) => None,
542 Self::TreeEntry(_) => None,
543 Self::DiffStats(property) => Some(property.into_template()),
544 Self::CryptographicSignatureOpt(_) => None,
545 Self::AnnotationLine(_) => None,
546 Self::Trailer(property) => Some(property.into_template()),
547 Self::TrailerList(property) => Some(property.into_template()),
548 }
549 }
550
551 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'repo, bool>> {
552 type Core<'repo> = CoreTemplatePropertyKind<'repo>;
553 match (self, other) {
554 (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_eq(rhs),
555 (Self::Core(Core::String(lhs)), Self::RefSymbol(rhs)) => {
556 Some((lhs, rhs).map(|(l, r)| RefSymbolBuf(l) == r).into_dyn())
557 }
558 (Self::Core(Core::String(lhs)), Self::RefSymbolOpt(rhs)) => Some(
559 (lhs, rhs)
560 .map(|(l, r)| Some(RefSymbolBuf(l)) == r)
561 .into_dyn(),
562 ),
563 (Self::RefSymbol(lhs), Self::Core(Core::String(rhs))) => {
564 Some((lhs, rhs).map(|(l, r)| l == RefSymbolBuf(r)).into_dyn())
565 }
566 (Self::RefSymbol(lhs), Self::RefSymbol(rhs)) => {
567 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
568 }
569 (Self::RefSymbol(lhs), Self::RefSymbolOpt(rhs)) => {
570 Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn())
571 }
572 (Self::RefSymbolOpt(lhs), Self::Core(Core::String(rhs))) => Some(
573 (lhs, rhs)
574 .map(|(l, r)| l == Some(RefSymbolBuf(r)))
575 .into_dyn(),
576 ),
577 (Self::RefSymbolOpt(lhs), Self::RefSymbol(rhs)) => {
578 Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn())
579 }
580 (Self::RefSymbolOpt(lhs), Self::RefSymbolOpt(rhs)) => {
581 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
582 }
583 (Self::Core(_), _) => None,
584 (Self::Commit(_), _) => None,
585 (Self::CommitOpt(_), _) => None,
586 (Self::CommitList(_), _) => None,
587 (Self::CommitRef(_), _) => None,
588 (Self::CommitRefOpt(_), _) => None,
589 (Self::CommitRefList(_), _) => None,
590 (Self::RefSymbol(_), _) => None,
591 (Self::RefSymbolOpt(_), _) => None,
592 (Self::RepoPath(_), _) => None,
593 (Self::RepoPathOpt(_), _) => None,
594 (Self::ChangeId(_), _) => None,
595 (Self::CommitId(_), _) => None,
596 (Self::ShortestIdPrefix(_), _) => None,
597 (Self::TreeDiff(_), _) => None,
598 (Self::TreeDiffEntry(_), _) => None,
599 (Self::TreeDiffEntryList(_), _) => None,
600 (Self::TreeEntry(_), _) => None,
601 (Self::DiffStats(_), _) => None,
602 (Self::CryptographicSignatureOpt(_), _) => None,
603 (Self::AnnotationLine(_), _) => None,
604 (Self::Trailer(_), _) => None,
605 (Self::TrailerList(_), _) => None,
606 }
607 }
608
609 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'repo, Ordering>> {
610 match (self, other) {
611 (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_cmp(rhs),
612 (Self::Core(_), _) => None,
613 (Self::Commit(_), _) => None,
614 (Self::CommitOpt(_), _) => None,
615 (Self::CommitList(_), _) => None,
616 (Self::CommitRef(_), _) => None,
617 (Self::CommitRefOpt(_), _) => None,
618 (Self::CommitRefList(_), _) => None,
619 (Self::RefSymbol(_), _) => None,
620 (Self::RefSymbolOpt(_), _) => None,
621 (Self::RepoPath(_), _) => None,
622 (Self::RepoPathOpt(_), _) => None,
623 (Self::ChangeId(_), _) => None,
624 (Self::CommitId(_), _) => None,
625 (Self::ShortestIdPrefix(_), _) => None,
626 (Self::TreeDiff(_), _) => None,
627 (Self::TreeDiffEntry(_), _) => None,
628 (Self::TreeDiffEntryList(_), _) => None,
629 (Self::TreeEntry(_), _) => None,
630 (Self::DiffStats(_), _) => None,
631 (Self::CryptographicSignatureOpt(_), _) => None,
632 (Self::AnnotationLine(_), _) => None,
633 (Self::Trailer(_), _) => None,
634 (Self::TrailerList(_), _) => None,
635 }
636 }
637}
638
639pub type CommitTemplateBuildMethodFnMap<'repo, T> =
641 TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>;
642
643pub struct CommitTemplateBuildFnTable<'repo> {
645 pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>,
646 pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>,
647 pub commit_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Commit>>,
648 pub commit_ref_methods: CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>>,
649 pub commit_ref_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Rc<CommitRef>>>,
650 pub repo_path_methods: CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf>,
651 pub change_id_methods: CommitTemplateBuildMethodFnMap<'repo, ChangeId>,
652 pub commit_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitId>,
653 pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>,
654 pub tree_diff_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiff>,
655 pub tree_diff_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>,
656 pub tree_diff_entry_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<TreeDiffEntry>>,
657 pub tree_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeEntry>,
658 pub diff_stats_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStats>,
659 pub cryptographic_signature_methods:
660 CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature>,
661 pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>,
662 pub trailer_methods: CommitTemplateBuildMethodFnMap<'repo, Trailer>,
663 pub trailer_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Trailer>>,
664}
665
666impl<'repo> CommitTemplateBuildFnTable<'repo> {
667 fn builtin() -> Self {
669 CommitTemplateBuildFnTable {
670 core: CoreTemplateBuildFnTable::builtin(),
671 commit_methods: builtin_commit_methods(),
672 commit_list_methods: template_builder::builtin_unformattable_list_methods(),
673 commit_ref_methods: builtin_commit_ref_methods(),
674 commit_ref_list_methods: template_builder::builtin_formattable_list_methods(),
675 repo_path_methods: builtin_repo_path_methods(),
676 change_id_methods: builtin_change_id_methods(),
677 commit_id_methods: builtin_commit_id_methods(),
678 shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(),
679 tree_diff_methods: builtin_tree_diff_methods(),
680 tree_diff_entry_methods: builtin_tree_diff_entry_methods(),
681 tree_diff_entry_list_methods: template_builder::builtin_unformattable_list_methods(),
682 tree_entry_methods: builtin_tree_entry_methods(),
683 diff_stats_methods: builtin_diff_stats_methods(),
684 cryptographic_signature_methods: builtin_cryptographic_signature_methods(),
685 annotation_line_methods: builtin_annotation_line_methods(),
686 trailer_methods: builtin_trailer_methods(),
687 trailer_list_methods: builtin_trailer_list_methods(),
688 }
689 }
690
691 pub fn empty() -> Self {
692 CommitTemplateBuildFnTable {
693 core: CoreTemplateBuildFnTable::empty(),
694 commit_methods: HashMap::new(),
695 commit_list_methods: HashMap::new(),
696 commit_ref_methods: HashMap::new(),
697 commit_ref_list_methods: HashMap::new(),
698 repo_path_methods: HashMap::new(),
699 change_id_methods: HashMap::new(),
700 commit_id_methods: HashMap::new(),
701 shortest_id_prefix_methods: HashMap::new(),
702 tree_diff_methods: HashMap::new(),
703 tree_diff_entry_methods: HashMap::new(),
704 tree_diff_entry_list_methods: HashMap::new(),
705 tree_entry_methods: HashMap::new(),
706 diff_stats_methods: HashMap::new(),
707 cryptographic_signature_methods: HashMap::new(),
708 annotation_line_methods: HashMap::new(),
709 trailer_methods: HashMap::new(),
710 trailer_list_methods: HashMap::new(),
711 }
712 }
713
714 fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) {
715 let CommitTemplateBuildFnTable {
716 core,
717 commit_methods,
718 commit_list_methods,
719 commit_ref_methods,
720 commit_ref_list_methods,
721 repo_path_methods,
722 change_id_methods,
723 commit_id_methods,
724 shortest_id_prefix_methods,
725 tree_diff_methods,
726 tree_diff_entry_methods,
727 tree_diff_entry_list_methods,
728 tree_entry_methods,
729 diff_stats_methods,
730 cryptographic_signature_methods,
731 annotation_line_methods,
732 trailer_methods,
733 trailer_list_methods,
734 } = extension;
735
736 self.core.merge(core);
737 merge_fn_map(&mut self.commit_methods, commit_methods);
738 merge_fn_map(&mut self.commit_list_methods, commit_list_methods);
739 merge_fn_map(&mut self.commit_ref_methods, commit_ref_methods);
740 merge_fn_map(&mut self.commit_ref_list_methods, commit_ref_list_methods);
741 merge_fn_map(&mut self.repo_path_methods, repo_path_methods);
742 merge_fn_map(&mut self.change_id_methods, change_id_methods);
743 merge_fn_map(&mut self.commit_id_methods, commit_id_methods);
744 merge_fn_map(
745 &mut self.shortest_id_prefix_methods,
746 shortest_id_prefix_methods,
747 );
748 merge_fn_map(&mut self.tree_diff_methods, tree_diff_methods);
749 merge_fn_map(&mut self.tree_diff_entry_methods, tree_diff_entry_methods);
750 merge_fn_map(
751 &mut self.tree_diff_entry_list_methods,
752 tree_diff_entry_list_methods,
753 );
754 merge_fn_map(&mut self.tree_entry_methods, tree_entry_methods);
755 merge_fn_map(&mut self.diff_stats_methods, diff_stats_methods);
756 merge_fn_map(
757 &mut self.cryptographic_signature_methods,
758 cryptographic_signature_methods,
759 );
760 merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods);
761 merge_fn_map(&mut self.trailer_methods, trailer_methods);
762 merge_fn_map(&mut self.trailer_list_methods, trailer_list_methods);
763 }
764}
765
766#[derive(Default)]
767pub struct CommitKeywordCache<'repo> {
768 bookmarks_index: OnceCell<Rc<CommitRefsIndex>>,
770 tags_index: OnceCell<Rc<CommitRefsIndex>>,
771 git_refs_index: OnceCell<Rc<CommitRefsIndex>>,
772 is_immutable_fn: OnceCell<Rc<RevsetContainingFn<'repo>>>,
773}
774
775impl<'repo> CommitKeywordCache<'repo> {
776 pub fn bookmarks_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
777 self.bookmarks_index
778 .get_or_init(|| Rc::new(build_bookmarks_index(repo)))
779 }
780
781 pub fn tags_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
782 self.tags_index
783 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().tags())))
784 }
785
786 pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
787 self.git_refs_index
788 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().git_refs())))
789 }
790
791 pub fn is_immutable_fn(
792 &self,
793 language: &CommitTemplateLanguage<'repo>,
794 span: pest::Span<'_>,
795 ) -> TemplateParseResult<&Rc<RevsetContainingFn<'repo>>> {
796 self.is_immutable_fn.get_or_try_init(|| {
800 let expression = &language.immutable_expression;
801 let revset = evaluate_revset_expression(language, span, expression)?;
802 Ok(revset.containing_fn().into())
803 })
804 }
805}
806
807fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> {
808 let mut map = CommitTemplateBuildMethodFnMap::<Commit>::new();
811 map.insert(
812 "description",
813 |_language, _diagnostics, _build_ctx, self_property, function| {
814 function.expect_no_arguments()?;
815 let out_property = self_property.map(|commit| commit.description().to_owned());
816 Ok(out_property.into_dyn_wrapped())
817 },
818 );
819 map.insert(
820 "trailers",
821 |_language, _diagnostics, _build_ctx, self_property, function| {
822 function.expect_no_arguments()?;
823 let out_property = self_property
824 .map(|commit| trailer::parse_description_trailers(commit.description()));
825 Ok(out_property.into_dyn_wrapped())
826 },
827 );
828 map.insert(
829 "change_id",
830 |_language, _diagnostics, _build_ctx, self_property, function| {
831 function.expect_no_arguments()?;
832 let out_property = self_property.map(|commit| commit.change_id().to_owned());
833 Ok(out_property.into_dyn_wrapped())
834 },
835 );
836 map.insert(
837 "commit_id",
838 |_language, _diagnostics, _build_ctx, self_property, function| {
839 function.expect_no_arguments()?;
840 let out_property = self_property.map(|commit| commit.id().to_owned());
841 Ok(out_property.into_dyn_wrapped())
842 },
843 );
844 map.insert(
845 "parents",
846 |_language, _diagnostics, _build_ctx, self_property, function| {
847 function.expect_no_arguments()?;
848 let out_property = self_property.and_then(|commit| {
849 let commits: Vec<_> = commit.parents().try_collect()?;
850 Ok(commits)
851 });
852 Ok(out_property.into_dyn_wrapped())
853 },
854 );
855 map.insert(
856 "author",
857 |_language, _diagnostics, _build_ctx, self_property, function| {
858 function.expect_no_arguments()?;
859 let out_property = self_property.map(|commit| commit.author().clone());
860 Ok(out_property.into_dyn_wrapped())
861 },
862 );
863 map.insert(
864 "committer",
865 |_language, _diagnostics, _build_ctx, self_property, function| {
866 function.expect_no_arguments()?;
867 let out_property = self_property.map(|commit| commit.committer().clone());
868 Ok(out_property.into_dyn_wrapped())
869 },
870 );
871 map.insert(
872 "mine",
873 |language, _diagnostics, _build_ctx, self_property, function| {
874 function.expect_no_arguments()?;
875 let user_email = language.revset_parse_context.user_email.to_owned();
876 let out_property = self_property.map(move |commit| commit.author().email == user_email);
877 Ok(out_property.into_dyn_wrapped())
878 },
879 );
880 map.insert(
881 "signature",
882 |_language, _diagnostics, _build_ctx, self_property, function| {
883 function.expect_no_arguments()?;
884 let out_property = self_property.map(CryptographicSignature::new);
885 Ok(out_property.into_dyn_wrapped())
886 },
887 );
888 map.insert(
889 "working_copies",
890 |language, _diagnostics, _build_ctx, self_property, function| {
891 function.expect_no_arguments()?;
892 let repo = language.repo;
893 let out_property = self_property.map(|commit| extract_working_copies(repo, &commit));
894 Ok(out_property.into_dyn_wrapped())
895 },
896 );
897 map.insert(
898 "current_working_copy",
899 |language, _diagnostics, _build_ctx, self_property, function| {
900 function.expect_no_arguments()?;
901 let repo = language.repo;
902 let name = language.workspace_name.clone();
903 let out_property = self_property
904 .map(move |commit| Some(commit.id()) == repo.view().get_wc_commit_id(&name));
905 Ok(out_property.into_dyn_wrapped())
906 },
907 );
908 map.insert(
909 "bookmarks",
910 |language, _diagnostics, _build_ctx, self_property, function| {
911 function.expect_no_arguments()?;
912 let index = language
913 .keyword_cache
914 .bookmarks_index(language.repo)
915 .clone();
916 let out_property = self_property.map(move |commit| {
917 index
918 .get(commit.id())
919 .iter()
920 .filter(|commit_ref| commit_ref.is_local() || !commit_ref.synced)
921 .cloned()
922 .collect_vec()
923 });
924 Ok(out_property.into_dyn_wrapped())
925 },
926 );
927 map.insert(
928 "local_bookmarks",
929 |language, _diagnostics, _build_ctx, self_property, function| {
930 function.expect_no_arguments()?;
931 let index = language
932 .keyword_cache
933 .bookmarks_index(language.repo)
934 .clone();
935 let out_property = self_property.map(move |commit| {
936 index
937 .get(commit.id())
938 .iter()
939 .filter(|commit_ref| commit_ref.is_local())
940 .cloned()
941 .collect_vec()
942 });
943 Ok(out_property.into_dyn_wrapped())
944 },
945 );
946 map.insert(
947 "remote_bookmarks",
948 |language, _diagnostics, _build_ctx, self_property, function| {
949 function.expect_no_arguments()?;
950 let index = language
951 .keyword_cache
952 .bookmarks_index(language.repo)
953 .clone();
954 let out_property = self_property.map(move |commit| {
955 index
956 .get(commit.id())
957 .iter()
958 .filter(|commit_ref| commit_ref.is_remote())
959 .cloned()
960 .collect_vec()
961 });
962 Ok(out_property.into_dyn_wrapped())
963 },
964 );
965 map.insert(
966 "tags",
967 |language, _diagnostics, _build_ctx, self_property, function| {
968 function.expect_no_arguments()?;
969 let index = language.keyword_cache.tags_index(language.repo).clone();
970 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec());
971 Ok(out_property.into_dyn_wrapped())
972 },
973 );
974 map.insert(
975 "git_refs",
976 |language, _diagnostics, _build_ctx, self_property, function| {
977 function.expect_no_arguments()?;
978 let index = language.keyword_cache.git_refs_index(language.repo).clone();
979 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec());
980 Ok(out_property.into_dyn_wrapped())
981 },
982 );
983 map.insert(
984 "git_head",
985 |language, _diagnostics, _build_ctx, self_property, function| {
986 function.expect_no_arguments()?;
987 let repo = language.repo;
988 let out_property = self_property.map(|commit| {
989 let target = repo.view().git_head();
990 target.added_ids().contains(commit.id())
991 });
992 Ok(out_property.into_dyn_wrapped())
993 },
994 );
995 map.insert(
996 "divergent",
997 |language, _diagnostics, _build_ctx, self_property, function| {
998 function.expect_no_arguments()?;
999 let repo = language.repo;
1000 let out_property = self_property.map(|commit| {
1001 let maybe_entries = repo.resolve_change_id(commit.change_id());
1003 maybe_entries.map_or(0, |entries| entries.len()) > 1
1004 });
1005 Ok(out_property.into_dyn_wrapped())
1006 },
1007 );
1008 map.insert(
1009 "hidden",
1010 |language, _diagnostics, _build_ctx, self_property, function| {
1011 function.expect_no_arguments()?;
1012 let repo = language.repo;
1013 let out_property = self_property.map(|commit| commit.is_hidden(repo));
1014 Ok(out_property.into_dyn_wrapped())
1015 },
1016 );
1017 map.insert(
1018 "immutable",
1019 |language, _diagnostics, _build_ctx, self_property, function| {
1020 function.expect_no_arguments()?;
1021 let is_immutable = language
1022 .keyword_cache
1023 .is_immutable_fn(language, function.name_span)?
1024 .clone();
1025 let out_property = self_property.and_then(move |commit| Ok(is_immutable(commit.id())?));
1026 Ok(out_property.into_dyn_wrapped())
1027 },
1028 );
1029 map.insert(
1030 "contained_in",
1031 |language, diagnostics, _build_ctx, self_property, function| {
1032 let [revset_node] = function.expect_exact_arguments()?;
1033
1034 let is_contained =
1035 template_parser::catch_aliases(diagnostics, revset_node, |diagnostics, node| {
1036 let text = template_parser::expect_string_literal(node)?;
1037 let revset = evaluate_user_revset(language, diagnostics, node.span, text)?;
1038 Ok(revset.containing_fn())
1039 })?;
1040
1041 let out_property = self_property.and_then(move |commit| Ok(is_contained(commit.id())?));
1042 Ok(out_property.into_dyn_wrapped())
1043 },
1044 );
1045 map.insert(
1046 "conflict",
1047 |_language, _diagnostics, _build_ctx, self_property, function| {
1048 function.expect_no_arguments()?;
1049 let out_property = self_property.and_then(|commit| Ok(commit.has_conflict()?));
1050 Ok(out_property.into_dyn_wrapped())
1051 },
1052 );
1053 map.insert(
1054 "empty",
1055 |language, _diagnostics, _build_ctx, self_property, function| {
1056 function.expect_no_arguments()?;
1057 let repo = language.repo;
1058 let out_property = self_property.and_then(|commit| Ok(commit.is_empty(repo)?));
1059 Ok(out_property.into_dyn_wrapped())
1060 },
1061 );
1062 map.insert(
1063 "diff",
1064 |language, diagnostics, _build_ctx, self_property, function| {
1065 let ([], [files_node]) = function.expect_arguments()?;
1066 let files = if let Some(node) = files_node {
1067 expect_fileset_literal(diagnostics, node, language.path_converter)?
1068 } else {
1069 FilesetExpression::all()
1072 };
1073 let repo = language.repo;
1074 let matcher: Rc<dyn Matcher> = files.to_matcher().into();
1075 let out_property = self_property
1076 .and_then(move |commit| Ok(TreeDiff::from_commit(repo, &commit, matcher.clone())?));
1077 Ok(out_property.into_dyn_wrapped())
1078 },
1079 );
1080 map.insert(
1081 "root",
1082 |language, _diagnostics, _build_ctx, self_property, function| {
1083 function.expect_no_arguments()?;
1084 let repo = language.repo;
1085 let out_property =
1086 self_property.map(|commit| commit.id() == repo.store().root_commit_id());
1087 Ok(out_property.into_dyn_wrapped())
1088 },
1089 );
1090 map
1091}
1092
1093fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String {
1095 let wc_commit_ids = repo.view().wc_commit_ids();
1096 if wc_commit_ids.len() <= 1 {
1097 return "".to_string();
1098 }
1099 let mut names = vec![];
1100 for (name, wc_commit_id) in wc_commit_ids {
1101 if wc_commit_id == commit.id() {
1102 names.push(format!("{}@", name.as_symbol()));
1103 }
1104 }
1105 names.join(" ")
1106}
1107
1108fn expect_fileset_literal(
1109 diagnostics: &mut TemplateDiagnostics,
1110 node: &ExpressionNode,
1111 path_converter: &RepoPathUiConverter,
1112) -> Result<FilesetExpression, TemplateParseError> {
1113 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
1114 let text = template_parser::expect_string_literal(node)?;
1115 let mut inner_diagnostics = FilesetDiagnostics::new();
1116 let expression =
1117 fileset::parse(&mut inner_diagnostics, text, path_converter).map_err(|err| {
1118 TemplateParseError::expression("In fileset expression", node.span).with_source(err)
1119 })?;
1120 diagnostics.extend_with(inner_diagnostics, |diag| {
1121 TemplateParseError::expression("In fileset expression", node.span).with_source(diag)
1122 });
1123 Ok(expression)
1124 })
1125}
1126
1127fn evaluate_revset_expression<'repo>(
1128 language: &CommitTemplateLanguage<'repo>,
1129 span: pest::Span<'_>,
1130 expression: &UserRevsetExpression,
1131) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
1132 let make_error = || TemplateParseError::expression("Failed to evaluate revset", span);
1133 let repo = language.repo;
1134 let symbol_resolver = revset_util::default_symbol_resolver(
1135 repo,
1136 language.revset_parse_context.extensions.symbol_resolvers(),
1137 language.id_prefix_context,
1138 );
1139 let revset = expression
1140 .resolve_user_expression(repo, &symbol_resolver)
1141 .map_err(|err| make_error().with_source(err))?
1142 .evaluate(repo)
1143 .map_err(|err| make_error().with_source(err))?;
1144 Ok(revset)
1145}
1146
1147fn evaluate_user_revset<'repo>(
1148 language: &CommitTemplateLanguage<'repo>,
1149 diagnostics: &mut TemplateDiagnostics,
1150 span: pest::Span<'_>,
1151 revset: &str,
1152) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
1153 let mut inner_diagnostics = RevsetDiagnostics::new();
1154 let (expression, modifier) = revset::parse_with_modifier(
1155 &mut inner_diagnostics,
1156 revset,
1157 &language.revset_parse_context,
1158 )
1159 .map_err(|err| TemplateParseError::expression("In revset expression", span).with_source(err))?;
1160 diagnostics.extend_with(inner_diagnostics, |diag| {
1161 TemplateParseError::expression("In revset expression", span).with_source(diag)
1162 });
1163 let (None | Some(RevsetModifier::All)) = modifier;
1164
1165 evaluate_revset_expression(language, span, &expression)
1166}
1167
1168#[derive(Debug, serde::Serialize)]
1170pub struct CommitRef {
1171 name: RefSymbolBuf,
1175 #[serde(skip_serializing_if = "Option::is_none")] remote: Option<RefSymbolBuf>,
1178 target: RefTarget,
1180 #[serde(rename = "tracking_target")]
1182 #[serde(skip_serializing_if = "Option::is_none")] #[serde(serialize_with = "serialize_tracking_target")]
1184 tracking_ref: Option<TrackingRef>,
1185 #[serde(skip)] synced: bool,
1189}
1190
1191#[derive(Debug)]
1192struct TrackingRef {
1193 target: RefTarget,
1195 ahead_count: OnceCell<SizeHint>,
1197 behind_count: OnceCell<SizeHint>,
1199}
1200
1201impl CommitRef {
1202 pub fn local<'a>(
1208 name: impl Into<String>,
1209 target: RefTarget,
1210 remote_refs: impl IntoIterator<Item = &'a RemoteRef>,
1211 ) -> Rc<Self> {
1212 let synced = remote_refs
1213 .into_iter()
1214 .all(|remote_ref| !remote_ref.is_tracked() || remote_ref.target == target);
1215 Rc::new(CommitRef {
1216 name: RefSymbolBuf(name.into()),
1217 remote: None,
1218 target,
1219 tracking_ref: None,
1220 synced,
1221 })
1222 }
1223
1224 pub fn local_only(name: impl Into<String>, target: RefTarget) -> Rc<Self> {
1226 Self::local(name, target, [])
1227 }
1228
1229 pub fn remote(
1232 name: impl Into<String>,
1233 remote_name: impl Into<String>,
1234 remote_ref: RemoteRef,
1235 local_target: &RefTarget,
1236 ) -> Rc<Self> {
1237 let synced = remote_ref.is_tracked() && remote_ref.target == *local_target;
1238 let tracking_ref = remote_ref.is_tracked().then(|| {
1239 let count = if synced {
1240 OnceCell::from((0, Some(0))) } else {
1242 OnceCell::new()
1243 };
1244 TrackingRef {
1245 target: local_target.clone(),
1246 ahead_count: count.clone(),
1247 behind_count: count,
1248 }
1249 });
1250 Rc::new(CommitRef {
1251 name: RefSymbolBuf(name.into()),
1252 remote: Some(RefSymbolBuf(remote_name.into())),
1253 target: remote_ref.target,
1254 tracking_ref,
1255 synced,
1256 })
1257 }
1258
1259 pub fn remote_only(
1261 name: impl Into<String>,
1262 remote_name: impl Into<String>,
1263 target: RefTarget,
1264 ) -> Rc<Self> {
1265 Rc::new(CommitRef {
1266 name: RefSymbolBuf(name.into()),
1267 remote: Some(RefSymbolBuf(remote_name.into())),
1268 target,
1269 tracking_ref: None,
1270 synced: false, })
1272 }
1273
1274 pub fn name(&self) -> &str {
1276 self.name.as_ref()
1277 }
1278
1279 pub fn remote_name(&self) -> Option<&str> {
1281 self.remote.as_ref().map(AsRef::as_ref)
1282 }
1283
1284 pub fn target(&self) -> &RefTarget {
1286 &self.target
1287 }
1288
1289 pub fn is_local(&self) -> bool {
1291 self.remote.is_none()
1292 }
1293
1294 pub fn is_remote(&self) -> bool {
1296 self.remote.is_some()
1297 }
1298
1299 pub fn is_absent(&self) -> bool {
1301 self.target.is_absent()
1302 }
1303
1304 pub fn is_present(&self) -> bool {
1306 self.target.is_present()
1307 }
1308
1309 pub fn has_conflict(&self) -> bool {
1311 self.target.has_conflict()
1312 }
1313
1314 pub fn is_tracked(&self) -> bool {
1317 self.tracking_ref.is_some()
1318 }
1319
1320 pub fn is_tracking_present(&self) -> bool {
1323 self.tracking_ref
1324 .as_ref()
1325 .is_some_and(|tracking| tracking.target.is_present())
1326 }
1327
1328 fn tracking_ahead_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1330 let Some(tracking) = &self.tracking_ref else {
1331 return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1332 };
1333 tracking
1334 .ahead_count
1335 .get_or_try_init(|| {
1336 let self_ids = self.target.added_ids().cloned().collect_vec();
1337 let other_ids = tracking.target.added_ids().cloned().collect_vec();
1338 Ok(revset::walk_revs(repo, &self_ids, &other_ids)?.count_estimate()?)
1339 })
1340 .copied()
1341 }
1342
1343 fn tracking_behind_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1345 let Some(tracking) = &self.tracking_ref else {
1346 return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1347 };
1348 tracking
1349 .behind_count
1350 .get_or_try_init(|| {
1351 let self_ids = self.target.added_ids().cloned().collect_vec();
1352 let other_ids = tracking.target.added_ids().cloned().collect_vec();
1353 Ok(revset::walk_revs(repo, &other_ids, &self_ids)?.count_estimate()?)
1354 })
1355 .copied()
1356 }
1357}
1358
1359impl Template for Rc<CommitRef> {
1361 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1362 write!(formatter.labeled("name"), "{}", self.name)?;
1363 if let Some(remote) = &self.remote {
1364 write!(formatter, "@")?;
1365 write!(formatter.labeled("remote"), "{remote}")?;
1366 }
1367 if self.has_conflict() {
1370 write!(formatter, "??")?;
1371 } else if self.is_local() && !self.synced {
1372 write!(formatter, "*")?;
1373 }
1374 Ok(())
1375 }
1376}
1377
1378impl Template for Vec<Rc<CommitRef>> {
1379 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1380 templater::format_joined(formatter, self, " ")
1381 }
1382}
1383
1384fn serialize_tracking_target<S>(
1385 tracking_ref: &Option<TrackingRef>,
1386 serializer: S,
1387) -> Result<S::Ok, S::Error>
1388where
1389 S: serde::Serializer,
1390{
1391 let target = tracking_ref.as_ref().map(|tracking| &tracking.target);
1392 target.serialize(serializer)
1393}
1394
1395fn builtin_commit_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>> {
1396 let mut map = CommitTemplateBuildMethodFnMap::<Rc<CommitRef>>::new();
1399 map.insert(
1400 "name",
1401 |_language, _diagnostics, _build_ctx, self_property, function| {
1402 function.expect_no_arguments()?;
1403 let out_property = self_property.map(|commit_ref| commit_ref.name.clone());
1404 Ok(out_property.into_dyn_wrapped())
1405 },
1406 );
1407 map.insert(
1408 "remote",
1409 |_language, _diagnostics, _build_ctx, self_property, function| {
1410 function.expect_no_arguments()?;
1411 let out_property = self_property.map(|commit_ref| commit_ref.remote.clone());
1412 Ok(out_property.into_dyn_wrapped())
1413 },
1414 );
1415 map.insert(
1416 "present",
1417 |_language, _diagnostics, _build_ctx, self_property, function| {
1418 function.expect_no_arguments()?;
1419 let out_property = self_property.map(|commit_ref| commit_ref.is_present());
1420 Ok(out_property.into_dyn_wrapped())
1421 },
1422 );
1423 map.insert(
1424 "conflict",
1425 |_language, _diagnostics, _build_ctx, self_property, function| {
1426 function.expect_no_arguments()?;
1427 let out_property = self_property.map(|commit_ref| commit_ref.has_conflict());
1428 Ok(out_property.into_dyn_wrapped())
1429 },
1430 );
1431 map.insert(
1432 "normal_target",
1433 |language, _diagnostics, _build_ctx, self_property, function| {
1434 function.expect_no_arguments()?;
1435 let repo = language.repo;
1436 let out_property = self_property.and_then(|commit_ref| {
1437 let maybe_id = commit_ref.target.as_normal();
1438 Ok(maybe_id.map(|id| repo.store().get_commit(id)).transpose()?)
1439 });
1440 Ok(out_property.into_dyn_wrapped())
1441 },
1442 );
1443 map.insert(
1444 "removed_targets",
1445 |language, _diagnostics, _build_ctx, self_property, function| {
1446 function.expect_no_arguments()?;
1447 let repo = language.repo;
1448 let out_property = self_property.and_then(|commit_ref| {
1449 let ids = commit_ref.target.removed_ids();
1450 let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?;
1451 Ok(commits)
1452 });
1453 Ok(out_property.into_dyn_wrapped())
1454 },
1455 );
1456 map.insert(
1457 "added_targets",
1458 |language, _diagnostics, _build_ctx, self_property, function| {
1459 function.expect_no_arguments()?;
1460 let repo = language.repo;
1461 let out_property = self_property.and_then(|commit_ref| {
1462 let ids = commit_ref.target.added_ids();
1463 let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?;
1464 Ok(commits)
1465 });
1466 Ok(out_property.into_dyn_wrapped())
1467 },
1468 );
1469 map.insert(
1470 "tracked",
1471 |_language, _diagnostics, _build_ctx, self_property, function| {
1472 function.expect_no_arguments()?;
1473 let out_property = self_property.map(|commit_ref| commit_ref.is_tracked());
1474 Ok(out_property.into_dyn_wrapped())
1475 },
1476 );
1477 map.insert(
1478 "tracking_present",
1479 |_language, _diagnostics, _build_ctx, self_property, function| {
1480 function.expect_no_arguments()?;
1481 let out_property = self_property.map(|commit_ref| commit_ref.is_tracking_present());
1482 Ok(out_property.into_dyn_wrapped())
1483 },
1484 );
1485 map.insert(
1486 "tracking_ahead_count",
1487 |language, _diagnostics, _build_ctx, self_property, function| {
1488 function.expect_no_arguments()?;
1489 let repo = language.repo;
1490 let out_property =
1491 self_property.and_then(|commit_ref| commit_ref.tracking_ahead_count(repo));
1492 Ok(out_property.into_dyn_wrapped())
1493 },
1494 );
1495 map.insert(
1496 "tracking_behind_count",
1497 |language, _diagnostics, _build_ctx, self_property, function| {
1498 function.expect_no_arguments()?;
1499 let repo = language.repo;
1500 let out_property =
1501 self_property.and_then(|commit_ref| commit_ref.tracking_behind_count(repo));
1502 Ok(out_property.into_dyn_wrapped())
1503 },
1504 );
1505 map
1506}
1507
1508#[derive(Clone, Debug, Default)]
1510pub struct CommitRefsIndex {
1511 index: HashMap<CommitId, Vec<Rc<CommitRef>>>,
1512}
1513
1514impl CommitRefsIndex {
1515 fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: Rc<CommitRef>) {
1516 for id in ids {
1517 let commit_refs = self.index.entry(id.clone()).or_default();
1518 commit_refs.push(name.clone());
1519 }
1520 }
1521
1522 pub fn get(&self, id: &CommitId) -> &[Rc<CommitRef>] {
1523 self.index.get(id).map_or(&[], |refs: &Vec<_>| refs)
1524 }
1525}
1526
1527fn build_bookmarks_index(repo: &dyn Repo) -> CommitRefsIndex {
1528 let mut index = CommitRefsIndex::default();
1529 for (bookmark_name, bookmark_target) in repo.view().bookmarks() {
1530 let local_target = bookmark_target.local_target;
1531 let remote_refs = bookmark_target.remote_refs;
1532 if local_target.is_present() {
1533 let commit_ref = CommitRef::local(
1534 bookmark_name,
1535 local_target.clone(),
1536 remote_refs.iter().map(|&(_, remote_ref)| remote_ref),
1537 );
1538 index.insert(local_target.added_ids(), commit_ref);
1539 }
1540 for &(remote_name, remote_ref) in &remote_refs {
1541 let commit_ref =
1542 CommitRef::remote(bookmark_name, remote_name, remote_ref.clone(), local_target);
1543 index.insert(remote_ref.target.added_ids(), commit_ref);
1544 }
1545 }
1546 index
1547}
1548
1549fn build_commit_refs_index<'a, K: Into<String>>(
1550 ref_pairs: impl IntoIterator<Item = (K, &'a RefTarget)>,
1551) -> CommitRefsIndex {
1552 let mut index = CommitRefsIndex::default();
1553 for (name, target) in ref_pairs {
1554 let commit_ref = CommitRef::local_only(name, target.clone());
1555 index.insert(target.added_ids(), commit_ref);
1556 }
1557 index
1558}
1559
1560#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
1562#[serde(transparent)]
1563pub struct RefSymbolBuf(String);
1564
1565impl AsRef<str> for RefSymbolBuf {
1566 fn as_ref(&self) -> &str {
1567 &self.0
1568 }
1569}
1570
1571impl Display for RefSymbolBuf {
1572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1573 f.pad(&revset::format_symbol(&self.0))
1574 }
1575}
1576
1577impl Template for RefSymbolBuf {
1578 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1579 write!(formatter, "{self}")
1580 }
1581}
1582
1583impl Template for RepoPathBuf {
1584 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1585 write!(formatter, "{}", self.as_internal_file_string())
1586 }
1587}
1588
1589fn builtin_repo_path_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf> {
1590 let mut map = CommitTemplateBuildMethodFnMap::<RepoPathBuf>::new();
1593 map.insert(
1594 "display",
1595 |language, _diagnostics, _build_ctx, self_property, function| {
1596 function.expect_no_arguments()?;
1597 let path_converter = language.path_converter;
1598 let out_property = self_property.map(|path| path_converter.format_file_path(&path));
1599 Ok(out_property.into_dyn_wrapped())
1600 },
1601 );
1602 map.insert(
1603 "parent",
1604 |_language, _diagnostics, _build_ctx, self_property, function| {
1605 function.expect_no_arguments()?;
1606 let out_property = self_property.map(|path| Some(path.parent()?.to_owned()));
1607 Ok(out_property.into_dyn_wrapped())
1608 },
1609 );
1610 map
1611}
1612
1613trait ShortestIdPrefixLen {
1614 fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize;
1615}
1616
1617impl ShortestIdPrefixLen for ChangeId {
1618 fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize {
1619 index.shortest_change_prefix_len(repo, self)
1620 }
1621}
1622
1623impl Template for ChangeId {
1624 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1625 write!(formatter, "{self}")
1626 }
1627}
1628
1629fn builtin_change_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, ChangeId> {
1630 let mut map = builtin_commit_or_change_id_methods::<ChangeId>();
1631 map.insert(
1632 "normal_hex",
1633 |_language, _diagnostics, _build_ctx, self_property, function| {
1634 function.expect_no_arguments()?;
1635 let out_property = self_property.map(|id| id.hex());
1639 Ok(out_property.into_dyn_wrapped())
1640 },
1641 );
1642 map
1643}
1644
1645impl ShortestIdPrefixLen for CommitId {
1646 fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize {
1647 index.shortest_commit_prefix_len(repo, self)
1648 }
1649}
1650
1651impl Template for CommitId {
1652 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1653 write!(formatter, "{self}")
1654 }
1655}
1656
1657fn builtin_commit_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, CommitId> {
1658 let mut map = builtin_commit_or_change_id_methods::<CommitId>();
1659 map.insert(
1661 "normal_hex",
1662 |_language, diagnostics, _build_ctx, self_property, function| {
1663 diagnostics.add_warning(TemplateParseError::expression(
1664 "commit_id.normal_hex() is deprecated; use stringify(commit_id) instead",
1665 function.name_span,
1666 ));
1667 function.expect_no_arguments()?;
1668 let out_property = self_property.map(|id| id.hex());
1669 Ok(out_property.into_dyn_wrapped())
1670 },
1671 );
1672 map
1673}
1674
1675fn builtin_commit_or_change_id_methods<'repo, O>() -> CommitTemplateBuildMethodFnMap<'repo, O>
1676where
1677 O: Display + ShortestIdPrefixLen + 'repo,
1678{
1679 let mut map = CommitTemplateBuildMethodFnMap::<O>::new();
1682 map.insert(
1683 "short",
1684 |language, diagnostics, build_ctx, self_property, function| {
1685 let ([], [len_node]) = function.expect_arguments()?;
1686 let len_property = len_node
1687 .map(|node| {
1688 template_builder::expect_usize_expression(
1689 language,
1690 diagnostics,
1691 build_ctx,
1692 node,
1693 )
1694 })
1695 .transpose()?;
1696 let out_property = (self_property, len_property)
1697 .map(|(id, len)| format!("{id:.len$}", len = len.unwrap_or(12)));
1698 Ok(out_property.into_dyn_wrapped())
1699 },
1700 );
1701 map.insert(
1702 "shortest",
1703 |language, diagnostics, build_ctx, self_property, function| {
1704 let ([], [len_node]) = function.expect_arguments()?;
1705 let len_property = len_node
1706 .map(|node| {
1707 template_builder::expect_usize_expression(
1708 language,
1709 diagnostics,
1710 build_ctx,
1711 node,
1712 )
1713 })
1714 .transpose()?;
1715 let repo = language.repo;
1716 let index = match language.id_prefix_context.populate(repo) {
1717 Ok(index) => index,
1718 Err(err) => {
1719 diagnostics.add_warning(
1722 TemplateParseError::expression(
1723 "Failed to load short-prefixes index",
1724 function.name_span,
1725 )
1726 .with_source(err),
1727 );
1728 IdPrefixIndex::empty()
1729 }
1730 };
1731 let out_property = (self_property, len_property).map(move |(id, len)| {
1734 let prefix_len = id.shortest_prefix_len(repo, &index);
1735 let mut hex = format!("{id:.len$}", len = max(prefix_len, len.unwrap_or(0)));
1736 let rest = hex.split_off(prefix_len);
1737 ShortestIdPrefix { prefix: hex, rest }
1738 });
1739 Ok(out_property.into_dyn_wrapped())
1740 },
1741 );
1742 map
1743}
1744
1745#[derive(Clone, Debug, serde::Serialize)]
1746pub struct ShortestIdPrefix {
1747 pub prefix: String,
1748 pub rest: String,
1749}
1750
1751impl Template for ShortestIdPrefix {
1752 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1753 write!(formatter.labeled("prefix"), "{}", self.prefix)?;
1754 write!(formatter.labeled("rest"), "{}", self.rest)?;
1755 Ok(())
1756 }
1757}
1758
1759impl ShortestIdPrefix {
1760 fn to_upper(&self) -> Self {
1761 Self {
1762 prefix: self.prefix.to_ascii_uppercase(),
1763 rest: self.rest.to_ascii_uppercase(),
1764 }
1765 }
1766 fn to_lower(&self) -> Self {
1767 Self {
1768 prefix: self.prefix.to_ascii_lowercase(),
1769 rest: self.rest.to_ascii_lowercase(),
1770 }
1771 }
1772}
1773
1774fn builtin_shortest_id_prefix_methods<'repo>(
1775) -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> {
1776 let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new();
1779 map.insert(
1780 "prefix",
1781 |_language, _diagnostics, _build_ctx, self_property, function| {
1782 function.expect_no_arguments()?;
1783 let out_property = self_property.map(|id| id.prefix);
1784 Ok(out_property.into_dyn_wrapped())
1785 },
1786 );
1787 map.insert(
1788 "rest",
1789 |_language, _diagnostics, _build_ctx, self_property, function| {
1790 function.expect_no_arguments()?;
1791 let out_property = self_property.map(|id| id.rest);
1792 Ok(out_property.into_dyn_wrapped())
1793 },
1794 );
1795 map.insert(
1796 "upper",
1797 |_language, _diagnostics, _build_ctx, self_property, function| {
1798 function.expect_no_arguments()?;
1799 let out_property = self_property.map(|id| id.to_upper());
1800 Ok(out_property.into_dyn_wrapped())
1801 },
1802 );
1803 map.insert(
1804 "lower",
1805 |_language, _diagnostics, _build_ctx, self_property, function| {
1806 function.expect_no_arguments()?;
1807 let out_property = self_property.map(|id| id.to_lower());
1808 Ok(out_property.into_dyn_wrapped())
1809 },
1810 );
1811 map
1812}
1813
1814#[derive(Debug)]
1816pub struct TreeDiff {
1817 from_tree: MergedTree,
1818 to_tree: MergedTree,
1819 matcher: Rc<dyn Matcher>,
1820 copy_records: CopyRecords,
1821}
1822
1823impl TreeDiff {
1824 fn from_commit(
1825 repo: &dyn Repo,
1826 commit: &Commit,
1827 matcher: Rc<dyn Matcher>,
1828 ) -> BackendResult<Self> {
1829 let mut copy_records = CopyRecords::default();
1830 for parent in commit.parent_ids() {
1831 let records =
1832 diff_util::get_copy_records(repo.store(), parent, commit.id(), &*matcher)?;
1833 copy_records.add_records(records)?;
1834 }
1835 Ok(TreeDiff {
1836 from_tree: commit.parent_tree(repo)?,
1837 to_tree: commit.tree()?,
1838 matcher,
1839 copy_records,
1840 })
1841 }
1842
1843 fn diff_stream(&self) -> BoxStream<'_, CopiesTreeDiffEntry> {
1844 self.from_tree
1845 .diff_stream_with_copies(&self.to_tree, &*self.matcher, &self.copy_records)
1846 }
1847
1848 async fn collect_entries(&self) -> BackendResult<Vec<TreeDiffEntry>> {
1849 self.diff_stream()
1850 .map(TreeDiffEntry::from_backend_entry_with_copies)
1851 .try_collect()
1852 .await
1853 }
1854
1855 fn into_formatted<F, E>(self, show: F) -> TreeDiffFormatted<F>
1856 where
1857 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>,
1858 E: Into<TemplatePropertyError>,
1859 {
1860 TreeDiffFormatted { diff: self, show }
1861 }
1862}
1863
1864struct TreeDiffFormatted<F> {
1866 diff: TreeDiff,
1867 show: F,
1868}
1869
1870impl<F, E> Template for TreeDiffFormatted<F>
1871where
1872 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>,
1873 E: Into<TemplatePropertyError>,
1874{
1875 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1876 let show = &self.show;
1877 let store = self.diff.from_tree.store();
1878 let tree_diff = self.diff.diff_stream();
1879 show(formatter.as_mut(), store, tree_diff).or_else(|err| formatter.handle_error(err.into()))
1880 }
1881}
1882
1883fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiff> {
1884 type P<'repo> = CommitTemplatePropertyKind<'repo>;
1885 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiff>::new();
1888 map.insert(
1889 "files",
1890 |_language, _diagnostics, _build_ctx, self_property, function| {
1891 function.expect_no_arguments()?;
1892 let out_property =
1894 self_property.and_then(|diff| Ok(diff.collect_entries().block_on()?));
1895 Ok(out_property.into_dyn_wrapped())
1896 },
1897 );
1898 map.insert(
1899 "color_words",
1900 |language, diagnostics, build_ctx, self_property, function| {
1901 let ([], [context_node]) = function.expect_arguments()?;
1902 let context_property = context_node
1903 .map(|node| {
1904 template_builder::expect_usize_expression(
1905 language,
1906 diagnostics,
1907 build_ctx,
1908 node,
1909 )
1910 })
1911 .transpose()?;
1912 let path_converter = language.path_converter;
1913 let options = diff_util::ColorWordsDiffOptions::from_settings(language.settings())
1914 .map_err(|err| {
1915 let message = "Failed to load diff settings";
1916 TemplateParseError::expression(message, function.name_span).with_source(err)
1917 })?;
1918 let conflict_marker_style = language.conflict_marker_style;
1919 let template = (self_property, context_property)
1920 .map(move |(diff, context)| {
1921 let mut options = options.clone();
1922 if let Some(context) = context {
1923 options.context = context;
1924 }
1925 diff.into_formatted(move |formatter, store, tree_diff| {
1926 diff_util::show_color_words_diff(
1927 formatter,
1928 store,
1929 tree_diff,
1930 path_converter,
1931 &options,
1932 conflict_marker_style,
1933 )
1934 .block_on()
1935 })
1936 })
1937 .into_template();
1938 Ok(P::wrap_template(template))
1939 },
1940 );
1941 map.insert(
1942 "git",
1943 |language, diagnostics, build_ctx, self_property, function| {
1944 let ([], [context_node]) = function.expect_arguments()?;
1945 let context_property = context_node
1946 .map(|node| {
1947 template_builder::expect_usize_expression(
1948 language,
1949 diagnostics,
1950 build_ctx,
1951 node,
1952 )
1953 })
1954 .transpose()?;
1955 let options = diff_util::UnifiedDiffOptions::from_settings(language.settings())
1956 .map_err(|err| {
1957 let message = "Failed to load diff settings";
1958 TemplateParseError::expression(message, function.name_span).with_source(err)
1959 })?;
1960 let conflict_marker_style = language.conflict_marker_style;
1961 let template = (self_property, context_property)
1962 .map(move |(diff, context)| {
1963 let mut options = options.clone();
1964 if let Some(context) = context {
1965 options.context = context;
1966 }
1967 diff.into_formatted(move |formatter, store, tree_diff| {
1968 diff_util::show_git_diff(
1969 formatter,
1970 store,
1971 tree_diff,
1972 &options,
1973 conflict_marker_style,
1974 )
1975 .block_on()
1976 })
1977 })
1978 .into_template();
1979 Ok(P::wrap_template(template))
1980 },
1981 );
1982 map.insert(
1983 "stat",
1984 |language, diagnostics, build_ctx, self_property, function| {
1985 let ([], [width_node]) = function.expect_arguments()?;
1986 let width_property = width_node
1987 .map(|node| {
1988 template_builder::expect_usize_expression(
1989 language,
1990 diagnostics,
1991 build_ctx,
1992 node,
1993 )
1994 })
1995 .transpose()?;
1996 let path_converter = language.path_converter;
1997 let options = diff_util::DiffStatOptions::default();
1999 let conflict_marker_style = language.conflict_marker_style;
2000 let out_property = (self_property, width_property).and_then(move |(diff, width)| {
2002 let store = diff.from_tree.store();
2003 let tree_diff = diff.diff_stream();
2004 let stats = DiffStats::calculate(store, tree_diff, &options, conflict_marker_style)
2005 .block_on()?;
2006 Ok(DiffStatsFormatted {
2007 stats,
2008 path_converter,
2009 width: width.unwrap_or(80),
2011 })
2012 });
2013 Ok(out_property.into_dyn_wrapped())
2014 },
2015 );
2016 map.insert(
2017 "summary",
2018 |language, _diagnostics, _build_ctx, self_property, function| {
2019 function.expect_no_arguments()?;
2020 let path_converter = language.path_converter;
2021 let template = self_property
2022 .map(move |diff| {
2023 diff.into_formatted(move |formatter, _store, tree_diff| {
2024 diff_util::show_diff_summary(formatter, tree_diff, path_converter)
2025 .block_on()
2026 })
2027 })
2028 .into_template();
2029 Ok(P::wrap_template(template))
2030 },
2031 );
2032 map
2034}
2035
2036#[derive(Clone, Debug)]
2038pub struct TreeDiffEntry {
2039 pub path: CopiesTreeDiffEntryPath,
2040 pub source_value: MergedTreeValue,
2041 pub target_value: MergedTreeValue,
2042}
2043
2044impl TreeDiffEntry {
2045 pub fn from_backend_entry_with_copies(entry: CopiesTreeDiffEntry) -> BackendResult<Self> {
2046 let (source_value, target_value) = entry.values?;
2047 Ok(TreeDiffEntry {
2048 path: entry.path,
2049 source_value,
2050 target_value,
2051 })
2052 }
2053
2054 fn status_label(&self) -> &'static str {
2055 let (label, _sigil) = diff_util::diff_status_label_and_char(
2056 &self.path,
2057 &self.source_value,
2058 &self.target_value,
2059 );
2060 label
2061 }
2062
2063 fn into_source_entry(self) -> TreeEntry {
2064 TreeEntry {
2065 path: self.path.source.map_or(self.path.target, |(path, _)| path),
2066 value: self.source_value,
2067 }
2068 }
2069
2070 fn into_target_entry(self) -> TreeEntry {
2071 TreeEntry {
2072 path: self.path.target,
2073 value: self.target_value,
2074 }
2075 }
2076}
2077
2078fn builtin_tree_diff_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>
2079{
2080 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiffEntry>::new();
2083 map.insert(
2084 "path",
2085 |_language, _diagnostics, _build_ctx, self_property, function| {
2086 function.expect_no_arguments()?;
2087 let out_property = self_property.map(|entry| entry.path.target);
2088 Ok(out_property.into_dyn_wrapped())
2089 },
2090 );
2091 map.insert(
2092 "status",
2093 |_language, _diagnostics, _build_ctx, self_property, function| {
2094 function.expect_no_arguments()?;
2095 let out_property = self_property.map(|entry| entry.status_label().to_owned());
2096 Ok(out_property.into_dyn_wrapped())
2097 },
2098 );
2099 map.insert(
2101 "source",
2102 |_language, _diagnostics, _build_ctx, self_property, function| {
2103 function.expect_no_arguments()?;
2104 let out_property = self_property.map(TreeDiffEntry::into_source_entry);
2105 Ok(out_property.into_dyn_wrapped())
2106 },
2107 );
2108 map.insert(
2109 "target",
2110 |_language, _diagnostics, _build_ctx, self_property, function| {
2111 function.expect_no_arguments()?;
2112 let out_property = self_property.map(TreeDiffEntry::into_target_entry);
2113 Ok(out_property.into_dyn_wrapped())
2114 },
2115 );
2116 map
2117}
2118
2119#[derive(Clone, Debug)]
2121pub struct TreeEntry {
2122 pub path: RepoPathBuf,
2123 pub value: MergedTreeValue,
2124}
2125
2126fn builtin_tree_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeEntry> {
2127 let mut map = CommitTemplateBuildMethodFnMap::<TreeEntry>::new();
2130 map.insert(
2131 "path",
2132 |_language, _diagnostics, _build_ctx, self_property, function| {
2133 function.expect_no_arguments()?;
2134 let out_property = self_property.map(|entry| entry.path);
2135 Ok(out_property.into_dyn_wrapped())
2136 },
2137 );
2138 map.insert(
2139 "conflict",
2140 |_language, _diagnostics, _build_ctx, self_property, function| {
2141 function.expect_no_arguments()?;
2142 let out_property = self_property.map(|entry| !entry.value.is_resolved());
2143 Ok(out_property.into_dyn_wrapped())
2144 },
2145 );
2146 map.insert(
2147 "file_type",
2148 |_language, _diagnostics, _build_ctx, self_property, function| {
2149 function.expect_no_arguments()?;
2150 let out_property =
2151 self_property.map(|entry| describe_file_type(&entry.value).to_owned());
2152 Ok(out_property.into_dyn_wrapped())
2153 },
2154 );
2155 map.insert(
2156 "executable",
2157 |_language, _diagnostics, _build_ctx, self_property, function| {
2158 function.expect_no_arguments()?;
2159 let out_property =
2160 self_property.map(|entry| is_executable_file(&entry.value).unwrap_or_default());
2161 Ok(out_property.into_dyn_wrapped())
2162 },
2163 );
2164 map
2165}
2166
2167fn describe_file_type(value: &MergedTreeValue) -> &'static str {
2168 match value.as_resolved() {
2169 Some(Some(TreeValue::File { .. })) => "file",
2170 Some(Some(TreeValue::Symlink(_))) => "symlink",
2171 Some(Some(TreeValue::Tree(_))) => "tree",
2172 Some(Some(TreeValue::GitSubmodule(_))) => "git-submodule",
2173 Some(None) => "", None | Some(Some(TreeValue::Conflict(_))) => "conflict",
2175 }
2176}
2177
2178fn is_executable_file(value: &MergedTreeValue) -> Option<bool> {
2179 let executable = value.to_executable_merge()?;
2180 conflicts::resolve_file_executable(&executable)
2181}
2182
2183#[derive(Clone, Debug)]
2185pub struct DiffStatsFormatted<'a> {
2186 stats: DiffStats,
2187 path_converter: &'a RepoPathUiConverter,
2188 width: usize,
2189}
2190
2191impl Template for DiffStatsFormatted<'_> {
2192 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2193 diff_util::show_diff_stats(
2194 formatter.as_mut(),
2195 &self.stats,
2196 self.path_converter,
2197 self.width,
2198 )
2199 }
2200}
2201
2202fn builtin_diff_stats_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStats> {
2203 let mut map = CommitTemplateBuildMethodFnMap::<DiffStats>::new();
2206 map.insert(
2208 "total_added",
2209 |_language, _diagnostics, _build_ctx, self_property, function| {
2210 function.expect_no_arguments()?;
2211 let out_property =
2212 self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_added())?));
2213 Ok(out_property.into_dyn_wrapped())
2214 },
2215 );
2216 map.insert(
2217 "total_removed",
2218 |_language, _diagnostics, _build_ctx, self_property, function| {
2219 function.expect_no_arguments()?;
2220 let out_property =
2221 self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_removed())?));
2222 Ok(out_property.into_dyn_wrapped())
2223 },
2224 );
2225 map
2226}
2227
2228#[derive(Debug)]
2229pub struct CryptographicSignature {
2230 commit: Commit,
2231}
2232
2233impl CryptographicSignature {
2234 fn new(commit: Commit) -> Option<Self> {
2235 commit.is_signed().then_some(Self { commit })
2236 }
2237
2238 fn verify(&self) -> SignResult<Verification> {
2239 self.commit
2240 .verification()
2241 .transpose()
2242 .expect("must have signature")
2243 }
2244
2245 fn status(&self) -> SignResult<SigStatus> {
2246 self.verify().map(|verification| verification.status)
2247 }
2248
2249 fn key(&self) -> SignResult<String> {
2251 self.verify()
2252 .map(|verification| verification.key.unwrap_or_default())
2253 }
2254
2255 fn display(&self) -> SignResult<String> {
2257 self.verify()
2258 .map(|verification| verification.display.unwrap_or_default())
2259 }
2260}
2261
2262fn builtin_cryptographic_signature_methods<'repo>(
2263) -> CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature> {
2264 let mut map = CommitTemplateBuildMethodFnMap::<CryptographicSignature>::new();
2267 map.insert(
2268 "status",
2269 |_language, _diagnostics, _build_ctx, self_property, function| {
2270 function.expect_no_arguments()?;
2271 let out_property = self_property.and_then(|sig| match sig.status() {
2272 Ok(status) => Ok(status.to_string()),
2273 Err(SignError::InvalidSignatureFormat) => Ok("invalid".to_string()),
2274 Err(err) => Err(err.into()),
2275 });
2276 Ok(out_property.into_dyn_wrapped())
2277 },
2278 );
2279 map.insert(
2280 "key",
2281 |_language, _diagnostics, _build_ctx, self_property, function| {
2282 function.expect_no_arguments()?;
2283 let out_property = self_property.and_then(|sig| Ok(sig.key()?));
2284 Ok(out_property.into_dyn_wrapped())
2285 },
2286 );
2287 map.insert(
2288 "display",
2289 |_language, _diagnostics, _build_ctx, self_property, function| {
2290 function.expect_no_arguments()?;
2291 let out_property = self_property.and_then(|sig| Ok(sig.display()?));
2292 Ok(out_property.into_dyn_wrapped())
2293 },
2294 );
2295 map
2296}
2297
2298#[derive(Debug, Clone)]
2299pub struct AnnotationLine {
2300 pub commit: Commit,
2301 pub content: BString,
2302 pub line_number: usize,
2303 pub first_line_in_hunk: bool,
2304}
2305
2306fn builtin_annotation_line_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>
2307{
2308 type P<'repo> = CommitTemplatePropertyKind<'repo>;
2309 let mut map = CommitTemplateBuildMethodFnMap::<AnnotationLine>::new();
2310 map.insert(
2311 "commit",
2312 |_language, _diagnostics, _build_ctx, self_property, function| {
2313 function.expect_no_arguments()?;
2314 let out_property = self_property.map(|line| line.commit);
2315 Ok(out_property.into_dyn_wrapped())
2316 },
2317 );
2318 map.insert(
2319 "content",
2320 |_language, _diagnostics, _build_ctx, self_property, function| {
2321 function.expect_no_arguments()?;
2322 let out_property = self_property.map(|line| line.content);
2323 Ok(P::wrap_template(out_property.into_template()))
2325 },
2326 );
2327 map.insert(
2328 "line_number",
2329 |_language, _diagnostics, _build_ctx, self_property, function| {
2330 function.expect_no_arguments()?;
2331 let out_property = self_property.and_then(|line| Ok(i64::try_from(line.line_number)?));
2332 Ok(out_property.into_dyn_wrapped())
2333 },
2334 );
2335 map.insert(
2336 "first_line_in_hunk",
2337 |_language, _diagnostics, _build_ctx, self_property, function| {
2338 function.expect_no_arguments()?;
2339 let out_property = self_property.map(|line| line.first_line_in_hunk);
2340 Ok(out_property.into_dyn_wrapped())
2341 },
2342 );
2343 map
2344}
2345
2346impl Template for Trailer {
2347 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2348 write!(formatter, "{}: {}", self.key, self.value)
2349 }
2350}
2351
2352impl Template for Vec<Trailer> {
2353 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2354 templater::format_joined(formatter, self, "\n")
2355 }
2356}
2357
2358fn builtin_trailer_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Trailer> {
2359 let mut map = CommitTemplateBuildMethodFnMap::<Trailer>::new();
2360 map.insert(
2361 "key",
2362 |_language, _diagnostics, _build_ctx, self_property, function| {
2363 function.expect_no_arguments()?;
2364 let out_property = self_property.map(|trailer| trailer.key);
2365 Ok(out_property.into_dyn_wrapped())
2366 },
2367 );
2368 map.insert(
2369 "value",
2370 |_language, _diagnostics, _build_ctx, self_property, function| {
2371 function.expect_no_arguments()?;
2372 let out_property = self_property.map(|trailer| trailer.value);
2373 Ok(out_property.into_dyn_wrapped())
2374 },
2375 );
2376 map
2377}
2378
2379fn builtin_trailer_list_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Vec<Trailer>> {
2380 let mut map: CommitTemplateBuildMethodFnMap<Vec<Trailer>> =
2381 template_builder::builtin_formattable_list_methods();
2382 map.insert(
2383 "contains_key",
2384 |language, diagnostics, build_ctx, self_property, function| {
2385 let [key_node] = function.expect_exact_arguments()?;
2386 let key_property =
2387 expect_stringify_expression(language, diagnostics, build_ctx, key_node)?;
2388 let out_property = (self_property, key_property)
2389 .map(|(trailers, key)| trailers.iter().any(|t| t.key == key));
2390 Ok(out_property.into_dyn_wrapped())
2391 },
2392 );
2393 map
2394}
2395
2396#[cfg(test)]
2397mod tests {
2398 use std::path::Path;
2399 use std::sync::Arc;
2400
2401 use jj_lib::config::ConfigLayer;
2402 use jj_lib::config::ConfigSource;
2403 use jj_lib::revset::RevsetAliasesMap;
2404 use jj_lib::revset::RevsetExpression;
2405 use jj_lib::revset::RevsetExtensions;
2406 use jj_lib::revset::RevsetWorkspaceContext;
2407 use testutils::repo_path_buf;
2408 use testutils::TestRepoBackend;
2409 use testutils::TestWorkspace;
2410
2411 use super::*;
2412 use crate::template_parser::TemplateAliasesMap;
2413 use crate::templater::TemplateRenderer;
2414 use crate::templater::WrapTemplateProperty;
2415
2416 type BuildFunctionFn = for<'a> fn(
2418 &CommitTemplateLanguage<'a>,
2419 &mut TemplateDiagnostics,
2420 &BuildContext<CommitTemplatePropertyKind<'a>>,
2421 &FunctionCallNode,
2422 ) -> TemplateParseResult<CommitTemplatePropertyKind<'a>>;
2423
2424 struct CommitTemplateTestEnv {
2425 test_workspace: TestWorkspace,
2426 path_converter: RepoPathUiConverter,
2427 revset_extensions: Arc<RevsetExtensions>,
2428 id_prefix_context: IdPrefixContext,
2429 revset_aliases_map: RevsetAliasesMap,
2430 template_aliases_map: TemplateAliasesMap,
2431 immutable_expression: Rc<UserRevsetExpression>,
2432 extra_functions: HashMap<&'static str, BuildFunctionFn>,
2433 }
2434
2435 impl CommitTemplateTestEnv {
2436 fn init() -> Self {
2437 let settings = stable_settings();
2439 let test_workspace =
2440 TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &settings);
2441 let path_converter = RepoPathUiConverter::Fs {
2442 cwd: test_workspace.workspace.workspace_root().to_owned(),
2443 base: test_workspace.workspace.workspace_root().to_owned(),
2444 };
2445 #[expect(clippy::arc_with_non_send_sync)]
2447 let revset_extensions = Arc::new(RevsetExtensions::new());
2448 let id_prefix_context = IdPrefixContext::new(revset_extensions.clone());
2449 CommitTemplateTestEnv {
2450 test_workspace,
2451 path_converter,
2452 revset_extensions,
2453 id_prefix_context,
2454 revset_aliases_map: RevsetAliasesMap::new(),
2455 template_aliases_map: TemplateAliasesMap::new(),
2456 immutable_expression: RevsetExpression::none(),
2457 extra_functions: HashMap::new(),
2458 }
2459 }
2460
2461 fn set_cwd(&mut self, path: impl AsRef<Path>) {
2462 self.path_converter = RepoPathUiConverter::Fs {
2463 cwd: self.test_workspace.workspace.workspace_root().join(path),
2464 base: self.test_workspace.workspace.workspace_root().to_owned(),
2465 };
2466 }
2467
2468 fn add_function(&mut self, name: &'static str, f: BuildFunctionFn) {
2469 self.extra_functions.insert(name, f);
2470 }
2471
2472 fn new_language(&self) -> CommitTemplateLanguage<'_> {
2473 let revset_parse_context = RevsetParseContext {
2474 aliases_map: &self.revset_aliases_map,
2475 local_variables: HashMap::new(),
2476 user_email: "test.user@example.com",
2477 date_pattern_context: chrono::DateTime::UNIX_EPOCH.fixed_offset().into(),
2478 extensions: &self.revset_extensions,
2479 workspace: Some(RevsetWorkspaceContext {
2480 path_converter: &self.path_converter,
2481 workspace_name: self.test_workspace.workspace.workspace_name(),
2482 }),
2483 };
2484 let mut language = CommitTemplateLanguage::new(
2485 self.test_workspace.repo.as_ref(),
2486 &self.path_converter,
2487 self.test_workspace.workspace.workspace_name(),
2488 revset_parse_context,
2489 &self.id_prefix_context,
2490 self.immutable_expression.clone(),
2491 ConflictMarkerStyle::default(),
2492 &[] as &[Box<dyn CommitTemplateLanguageExtension>],
2493 );
2494 for (&name, &f) in &self.extra_functions {
2496 language.build_fn_table.core.functions.insert(name, f);
2497 }
2498 language
2499 }
2500
2501 fn parse<'a, C>(&'a self, text: &str) -> TemplateParseResult<TemplateRenderer<'a, C>>
2502 where
2503 C: Clone + 'a,
2504 CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>,
2505 {
2506 let language = self.new_language();
2507 let mut diagnostics = TemplateDiagnostics::new();
2508 template_builder::parse(
2509 &language,
2510 &mut diagnostics,
2511 text,
2512 &self.template_aliases_map,
2513 )
2514 }
2515
2516 fn render_ok<'a, C>(&'a self, text: &str, context: &C) -> String
2517 where
2518 C: Clone + 'a,
2519 CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>,
2520 {
2521 let template = self.parse(text).unwrap();
2522 let output = template.format_plain_text(context);
2523 String::from_utf8(output).unwrap()
2524 }
2525 }
2526
2527 fn stable_settings() -> UserSettings {
2528 let mut config = testutils::base_user_config();
2529 let mut layer = ConfigLayer::empty(ConfigSource::User);
2530 layer
2531 .set_value("debug.commit-timestamp", "2001-02-03T04:05:06+07:00")
2532 .unwrap();
2533 config.add_layer(layer);
2534 UserSettings::from_config(config).unwrap()
2535 }
2536
2537 #[test]
2538 fn test_ref_symbol_type() {
2539 let mut env = CommitTemplateTestEnv::init();
2540 env.add_function("sym", |language, diagnostics, build_ctx, function| {
2541 let [value_node] = function.expect_exact_arguments()?;
2542 let value = expect_stringify_expression(language, diagnostics, build_ctx, value_node)?;
2543 let out_property = value.map(RefSymbolBuf);
2544 Ok(out_property.into_dyn_wrapped())
2545 });
2546 let sym = |s: &str| RefSymbolBuf(s.to_owned());
2547
2548 insta::assert_snapshot!(env.render_ok("self", &sym("")), @r#""""#);
2550 insta::assert_snapshot!(env.render_ok("self", &sym("foo")), @"foo");
2551 insta::assert_snapshot!(env.render_ok("self", &sym("foo bar")), @r#""foo bar""#);
2552
2553 insta::assert_snapshot!(env.render_ok("self == 'foo'", &sym("foo")), @"true");
2555 insta::assert_snapshot!(env.render_ok("'bar' == self", &sym("foo")), @"false");
2556 insta::assert_snapshot!(env.render_ok("self == self", &sym("foo")), @"true");
2557 insta::assert_snapshot!(env.render_ok("self == sym('bar')", &sym("foo")), @"false");
2558
2559 insta::assert_snapshot!(env.render_ok("self == 'bar'", &Some(sym("foo"))), @"false");
2560 insta::assert_snapshot!(env.render_ok("self == sym('foo')", &Some(sym("foo"))), @"true");
2561 insta::assert_snapshot!(env.render_ok("'foo' == self", &Some(sym("foo"))), @"true");
2562 insta::assert_snapshot!(env.render_ok("sym('bar') == self", &Some(sym("foo"))), @"false");
2563 insta::assert_snapshot!(env.render_ok("self == self", &Some(sym("foo"))), @"true");
2564 insta::assert_snapshot!(env.render_ok("self == ''", &None::<RefSymbolBuf>), @"false");
2565 insta::assert_snapshot!(env.render_ok("sym('') == self", &None::<RefSymbolBuf>), @"false");
2566 insta::assert_snapshot!(env.render_ok("self == self", &None::<RefSymbolBuf>), @"true");
2567
2568 insta::assert_snapshot!(env.render_ok("stringify(self)", &sym("a b")), @"a b");
2571 insta::assert_snapshot!(env.render_ok("stringify(self)", &Some(sym("a b"))), @"a b");
2572 insta::assert_snapshot!(
2573 env.render_ok("stringify(self)", &None::<RefSymbolBuf>),
2574 @"<Error: No RefSymbol available>");
2575
2576 insta::assert_snapshot!(env.render_ok("self.len()", &sym("a b")), @"3");
2578
2579 insta::assert_snapshot!(env.render_ok("json(self)", &sym("foo bar")), @r#""foo bar""#);
2581 }
2582
2583 #[test]
2584 fn test_repo_path_type() {
2585 let mut env = CommitTemplateTestEnv::init();
2586 env.set_cwd("dir");
2587
2588 insta::assert_snapshot!(
2590 env.render_ok("self", &repo_path_buf("dir/file")), @"dir/file");
2591
2592 insta::assert_snapshot!(
2594 env.render_ok("self.display()", &repo_path_buf("dir/file")), @"file");
2595 if cfg!(windows) {
2596 insta::assert_snapshot!(
2597 env.render_ok("self.display()", &repo_path_buf("file")), @"..\\file");
2598 } else {
2599 insta::assert_snapshot!(
2600 env.render_ok("self.display()", &repo_path_buf("file")), @"../file");
2601 }
2602
2603 let template = "if(self.parent(), self.parent(), '<none>')";
2604 insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("")), @"<none>");
2605 insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("file")), @"");
2606 insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("dir/file")), @"dir");
2607
2608 insta::assert_snapshot!(
2610 env.render_ok("json(self)", &repo_path_buf("dir/file")), @r#""dir/file""#);
2611 insta::assert_snapshot!(
2612 env.render_ok("json(self)", &None::<RepoPathBuf>), @"null");
2613 }
2614
2615 #[test]
2616 fn test_commit_id_type() {
2617 let env = CommitTemplateTestEnv::init();
2618
2619 let id = CommitId::from_hex("08a70ab33d7143b7130ed8594d8216ef688623c0");
2620 insta::assert_snapshot!(
2621 env.render_ok("self", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2622 insta::assert_snapshot!(
2623 env.render_ok("self.normal_hex()", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2624
2625 insta::assert_snapshot!(env.render_ok("self.short()", &id), @"08a70ab33d71");
2626 insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @"");
2627 insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @"");
2628 insta::assert_snapshot!(
2629 env.render_ok("self.short(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2630 insta::assert_snapshot!(
2631 env.render_ok("self.short(-100)", &id),
2632 @"<Error: out of range integral type conversion attempted>");
2633
2634 insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"08");
2635 insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"08");
2636 insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"08");
2637 insta::assert_snapshot!(
2638 env.render_ok("self.shortest(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0");
2639 insta::assert_snapshot!(
2640 env.render_ok("self.shortest(-100)", &id),
2641 @"<Error: out of range integral type conversion attempted>");
2642
2643 insta::assert_snapshot!(
2645 env.render_ok("json(self)", &id), @r#""08a70ab33d7143b7130ed8594d8216ef688623c0""#);
2646 }
2647
2648 #[test]
2649 fn test_change_id_type() {
2650 let env = CommitTemplateTestEnv::init();
2651
2652 let id = ChangeId::from_hex("ffdaa62087a280bddc5e3d3ff933b8ae");
2653 insta::assert_snapshot!(
2654 env.render_ok("self", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
2655 insta::assert_snapshot!(
2656 env.render_ok("self.normal_hex()", &id), @"ffdaa62087a280bddc5e3d3ff933b8ae");
2657
2658 insta::assert_snapshot!(env.render_ok("self.short()", &id), @"kkmpptxzrspx");
2659 insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @"");
2660 insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @"");
2661 insta::assert_snapshot!(
2662 env.render_ok("self.short(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
2663 insta::assert_snapshot!(
2664 env.render_ok("self.short(-100)", &id),
2665 @"<Error: out of range integral type conversion attempted>");
2666
2667 insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"k");
2668 insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"k");
2669 insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"k");
2670 insta::assert_snapshot!(
2671 env.render_ok("self.shortest(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl");
2672 insta::assert_snapshot!(
2673 env.render_ok("self.shortest(-100)", &id),
2674 @"<Error: out of range integral type conversion attempted>");
2675
2676 insta::assert_snapshot!(
2678 env.render_ok("json(self)", &id), @r#""kkmpptxzrspxrzommnulwmwkkqwworpl""#);
2679 }
2680
2681 #[test]
2682 fn test_shortest_id_prefix_type() {
2683 let env = CommitTemplateTestEnv::init();
2684
2685 let id = ShortestIdPrefix {
2686 prefix: "012".to_owned(),
2687 rest: "3abcdef".to_owned(),
2688 };
2689
2690 insta::assert_snapshot!(
2692 env.render_ok("json(self)", &id), @r#"{"prefix":"012","rest":"3abcdef"}"#);
2693 }
2694}