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