just playing with tangled
1// Copyright 2020-2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::any::Any;
16use std::cmp::max;
17use std::cmp::Ordering;
18use std::collections::HashMap;
19use std::io;
20use std::rc::Rc;
21
22use bstr::BString;
23use futures::stream::BoxStream;
24use futures::StreamExt as _;
25use futures::TryStreamExt as _;
26use itertools::Itertools as _;
27use jj_lib::backend::BackendResult;
28use jj_lib::backend::ChangeId;
29use jj_lib::backend::CommitId;
30use jj_lib::backend::TreeValue;
31use jj_lib::commit::Commit;
32use jj_lib::conflicts::ConflictMarkerStyle;
33use jj_lib::copies::CopiesTreeDiffEntry;
34use jj_lib::copies::CopiesTreeDiffEntryPath;
35use jj_lib::copies::CopyRecords;
36use jj_lib::extensions_map::ExtensionsMap;
37use jj_lib::fileset;
38use jj_lib::fileset::FilesetDiagnostics;
39use jj_lib::fileset::FilesetExpression;
40use jj_lib::id_prefix::IdPrefixContext;
41use jj_lib::id_prefix::IdPrefixIndex;
42use jj_lib::matchers::Matcher;
43use jj_lib::merge::MergedTreeValue;
44use jj_lib::merged_tree::MergedTree;
45use jj_lib::object_id::ObjectId as _;
46use jj_lib::op_store::RefTarget;
47use jj_lib::op_store::RemoteRef;
48use jj_lib::ref_name::WorkspaceName;
49use jj_lib::ref_name::WorkspaceNameBuf;
50use jj_lib::repo::Repo;
51use jj_lib::repo_path::RepoPathBuf;
52use jj_lib::repo_path::RepoPathUiConverter;
53use jj_lib::revset;
54use jj_lib::revset::Revset;
55use jj_lib::revset::RevsetContainingFn;
56use jj_lib::revset::RevsetDiagnostics;
57use jj_lib::revset::RevsetModifier;
58use jj_lib::revset::RevsetParseContext;
59use jj_lib::revset::UserRevsetExpression;
60use jj_lib::settings::UserSettings;
61use jj_lib::signing::SigStatus;
62use jj_lib::signing::SignError;
63use jj_lib::signing::SignResult;
64use jj_lib::signing::Verification;
65use jj_lib::store::Store;
66use once_cell::unsync::OnceCell;
67use pollster::FutureExt as _;
68
69use crate::diff_util;
70use crate::diff_util::DiffStats;
71use crate::formatter::Formatter;
72use crate::revset_util;
73use crate::template_builder;
74use crate::template_builder::merge_fn_map;
75use crate::template_builder::BuildContext;
76use crate::template_builder::CoreTemplateBuildFnTable;
77use crate::template_builder::CoreTemplatePropertyKind;
78use crate::template_builder::IntoTemplateProperty;
79use crate::template_builder::TemplateBuildMethodFnMap;
80use crate::template_builder::TemplateLanguage;
81use crate::template_parser;
82use crate::template_parser::ExpressionNode;
83use crate::template_parser::FunctionCallNode;
84use crate::template_parser::TemplateDiagnostics;
85use crate::template_parser::TemplateParseError;
86use crate::template_parser::TemplateParseResult;
87use crate::templater;
88use crate::templater::PlainTextFormattedProperty;
89use crate::templater::SizeHint;
90use crate::templater::Template;
91use crate::templater::TemplateFormatter;
92use crate::templater::TemplateProperty;
93use crate::templater::TemplatePropertyError;
94use crate::templater::TemplatePropertyExt as _;
95use crate::text_util;
96
97pub trait CommitTemplateLanguageExtension {
98 fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>;
99
100 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
101}
102
103pub struct CommitTemplateLanguage<'repo> {
104 repo: &'repo dyn Repo,
105 path_converter: &'repo RepoPathUiConverter,
106 workspace_name: WorkspaceNameBuf,
107 // RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime
108 // anyway to capture it to evaluate dynamically-constructed user expression
109 // such as `revset("ancestors(" ++ commit_id ++ ")")`.
110 // TODO: Maybe refactor context structs? RepoPathUiConverter and
111 // WorkspaceName are contained in RevsetParseContext for example.
112 revset_parse_context: RevsetParseContext<'repo>,
113 id_prefix_context: &'repo IdPrefixContext,
114 immutable_expression: Rc<UserRevsetExpression>,
115 conflict_marker_style: ConflictMarkerStyle,
116 build_fn_table: CommitTemplateBuildFnTable<'repo>,
117 keyword_cache: CommitKeywordCache<'repo>,
118 cache_extensions: ExtensionsMap,
119}
120
121impl<'repo> CommitTemplateLanguage<'repo> {
122 /// Sets up environment where commit template will be transformed to
123 /// evaluation tree.
124 #[expect(clippy::too_many_arguments)]
125 pub fn new(
126 repo: &'repo dyn Repo,
127 path_converter: &'repo RepoPathUiConverter,
128 workspace_name: &WorkspaceName,
129 revset_parse_context: RevsetParseContext<'repo>,
130 id_prefix_context: &'repo IdPrefixContext,
131 immutable_expression: Rc<UserRevsetExpression>,
132 conflict_marker_style: ConflictMarkerStyle,
133 extensions: &[impl AsRef<dyn CommitTemplateLanguageExtension>],
134 ) -> Self {
135 let mut build_fn_table = CommitTemplateBuildFnTable::builtin();
136 let mut cache_extensions = ExtensionsMap::empty();
137
138 for extension in extensions {
139 build_fn_table.merge(extension.as_ref().build_fn_table());
140 extension
141 .as_ref()
142 .build_cache_extensions(&mut cache_extensions);
143 }
144
145 CommitTemplateLanguage {
146 repo,
147 path_converter,
148 workspace_name: workspace_name.to_owned(),
149 revset_parse_context,
150 id_prefix_context,
151 immutable_expression,
152 conflict_marker_style,
153 build_fn_table,
154 keyword_cache: CommitKeywordCache::default(),
155 cache_extensions,
156 }
157 }
158}
159
160impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
161 type Property = CommitTemplatePropertyKind<'repo>;
162
163 template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core);
164
165 fn settings(&self) -> &UserSettings {
166 self.repo.base_repo().settings()
167 }
168
169 fn build_function(
170 &self,
171 diagnostics: &mut TemplateDiagnostics,
172 build_ctx: &BuildContext<Self::Property>,
173 function: &FunctionCallNode,
174 ) -> TemplateParseResult<Self::Property> {
175 let table = &self.build_fn_table.core;
176 table.build_function(self, diagnostics, build_ctx, function)
177 }
178
179 fn build_method(
180 &self,
181 diagnostics: &mut TemplateDiagnostics,
182 build_ctx: &BuildContext<Self::Property>,
183 property: Self::Property,
184 function: &FunctionCallNode,
185 ) -> TemplateParseResult<Self::Property> {
186 let type_name = property.type_name();
187 match property {
188 CommitTemplatePropertyKind::Core(property) => {
189 let table = &self.build_fn_table.core;
190 table.build_method(self, diagnostics, build_ctx, property, function)
191 }
192 CommitTemplatePropertyKind::Commit(property) => {
193 let table = &self.build_fn_table.commit_methods;
194 let build = template_parser::lookup_method(type_name, table, function)?;
195 build(self, diagnostics, build_ctx, property, function)
196 }
197 CommitTemplatePropertyKind::CommitOpt(property) => {
198 let type_name = "Commit";
199 let table = &self.build_fn_table.commit_methods;
200 let build = template_parser::lookup_method(type_name, table, function)?;
201 let inner_property = property.try_unwrap(type_name);
202 build(
203 self,
204 diagnostics,
205 build_ctx,
206 Box::new(inner_property),
207 function,
208 )
209 }
210 CommitTemplatePropertyKind::CommitList(property) => {
211 // TODO: migrate to table?
212 template_builder::build_unformattable_list_method(
213 self,
214 diagnostics,
215 build_ctx,
216 property,
217 function,
218 Self::wrap_commit,
219 Self::wrap_commit_list,
220 )
221 }
222 CommitTemplatePropertyKind::CommitRef(property) => {
223 let table = &self.build_fn_table.commit_ref_methods;
224 let build = template_parser::lookup_method(type_name, table, function)?;
225 build(self, diagnostics, build_ctx, property, function)
226 }
227 CommitTemplatePropertyKind::CommitRefOpt(property) => {
228 let type_name = "CommitRef";
229 let table = &self.build_fn_table.commit_ref_methods;
230 let build = template_parser::lookup_method(type_name, table, function)?;
231 let inner_property = property.try_unwrap(type_name);
232 build(
233 self,
234 diagnostics,
235 build_ctx,
236 Box::new(inner_property),
237 function,
238 )
239 }
240 CommitTemplatePropertyKind::CommitRefList(property) => {
241 // TODO: migrate to table?
242 template_builder::build_formattable_list_method(
243 self,
244 diagnostics,
245 build_ctx,
246 property,
247 function,
248 Self::wrap_commit_ref,
249 Self::wrap_commit_ref_list,
250 )
251 }
252 CommitTemplatePropertyKind::RepoPath(property) => {
253 let table = &self.build_fn_table.repo_path_methods;
254 let build = template_parser::lookup_method(type_name, table, function)?;
255 build(self, diagnostics, build_ctx, property, function)
256 }
257 CommitTemplatePropertyKind::RepoPathOpt(property) => {
258 let type_name = "RepoPath";
259 let table = &self.build_fn_table.repo_path_methods;
260 let build = template_parser::lookup_method(type_name, table, function)?;
261 let inner_property = property.try_unwrap(type_name);
262 build(
263 self,
264 diagnostics,
265 build_ctx,
266 Box::new(inner_property),
267 function,
268 )
269 }
270 CommitTemplatePropertyKind::CommitOrChangeId(property) => {
271 let table = &self.build_fn_table.commit_or_change_id_methods;
272 let build = template_parser::lookup_method(type_name, table, function)?;
273 build(self, diagnostics, build_ctx, property, function)
274 }
275 CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
276 let table = &self.build_fn_table.shortest_id_prefix_methods;
277 let build = template_parser::lookup_method(type_name, table, function)?;
278 build(self, diagnostics, build_ctx, property, function)
279 }
280 CommitTemplatePropertyKind::TreeDiff(property) => {
281 let table = &self.build_fn_table.tree_diff_methods;
282 let build = template_parser::lookup_method(type_name, table, function)?;
283 build(self, diagnostics, build_ctx, property, function)
284 }
285 CommitTemplatePropertyKind::TreeDiffEntry(property) => {
286 let table = &self.build_fn_table.tree_diff_entry_methods;
287 let build = template_parser::lookup_method(type_name, table, function)?;
288 build(self, diagnostics, build_ctx, property, function)
289 }
290 CommitTemplatePropertyKind::TreeDiffEntryList(property) => {
291 // TODO: migrate to table?
292 template_builder::build_unformattable_list_method(
293 self,
294 diagnostics,
295 build_ctx,
296 property,
297 function,
298 Self::wrap_tree_diff_entry,
299 Self::wrap_tree_diff_entry_list,
300 )
301 }
302 CommitTemplatePropertyKind::TreeEntry(property) => {
303 let table = &self.build_fn_table.tree_entry_methods;
304 let build = template_parser::lookup_method(type_name, table, function)?;
305 build(self, diagnostics, build_ctx, property, function)
306 }
307 CommitTemplatePropertyKind::DiffStats(property) => {
308 let table = &self.build_fn_table.diff_stats_methods;
309 let build = template_parser::lookup_method(type_name, table, function)?;
310 // Strip off formatting parameters which are needed only for the
311 // default template output.
312 let property = Box::new(property.map(|formatted| formatted.stats));
313 build(self, diagnostics, build_ctx, property, function)
314 }
315 CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => {
316 let type_name = "CryptographicSignature";
317 let table = &self.build_fn_table.cryptographic_signature_methods;
318 let build = template_parser::lookup_method(type_name, table, function)?;
319 let inner_property = property.try_unwrap(type_name);
320 build(
321 self,
322 diagnostics,
323 build_ctx,
324 Box::new(inner_property),
325 function,
326 )
327 }
328 CommitTemplatePropertyKind::AnnotationLine(property) => {
329 let type_name = "AnnotationLine";
330 let table = &self.build_fn_table.annotation_line_methods;
331 let build = template_parser::lookup_method(type_name, table, function)?;
332 build(self, diagnostics, build_ctx, property, function)
333 }
334 }
335 }
336}
337
338// If we need to add multiple languages that support Commit types, this can be
339// turned into a trait which extends TemplateLanguage.
340impl<'repo> CommitTemplateLanguage<'repo> {
341 pub fn repo(&self) -> &'repo dyn Repo {
342 self.repo
343 }
344
345 pub fn workspace_name(&self) -> &WorkspaceName {
346 &self.workspace_name
347 }
348
349 pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> {
350 &self.keyword_cache
351 }
352
353 pub fn cache_extension<T: Any>(&self) -> Option<&T> {
354 self.cache_extensions.get::<T>()
355 }
356
357 pub fn wrap_commit(
358 property: impl TemplateProperty<Output = Commit> + 'repo,
359 ) -> CommitTemplatePropertyKind<'repo> {
360 CommitTemplatePropertyKind::Commit(Box::new(property))
361 }
362
363 pub fn wrap_commit_opt(
364 property: impl TemplateProperty<Output = Option<Commit>> + 'repo,
365 ) -> CommitTemplatePropertyKind<'repo> {
366 CommitTemplatePropertyKind::CommitOpt(Box::new(property))
367 }
368
369 pub fn wrap_commit_list(
370 property: impl TemplateProperty<Output = Vec<Commit>> + 'repo,
371 ) -> CommitTemplatePropertyKind<'repo> {
372 CommitTemplatePropertyKind::CommitList(Box::new(property))
373 }
374
375 pub fn wrap_commit_ref(
376 property: impl TemplateProperty<Output = Rc<CommitRef>> + 'repo,
377 ) -> CommitTemplatePropertyKind<'repo> {
378 CommitTemplatePropertyKind::CommitRef(Box::new(property))
379 }
380
381 pub fn wrap_commit_ref_opt(
382 property: impl TemplateProperty<Output = Option<Rc<CommitRef>>> + 'repo,
383 ) -> CommitTemplatePropertyKind<'repo> {
384 CommitTemplatePropertyKind::CommitRefOpt(Box::new(property))
385 }
386
387 pub fn wrap_commit_ref_list(
388 property: impl TemplateProperty<Output = Vec<Rc<CommitRef>>> + 'repo,
389 ) -> CommitTemplatePropertyKind<'repo> {
390 CommitTemplatePropertyKind::CommitRefList(Box::new(property))
391 }
392
393 pub fn wrap_repo_path(
394 property: impl TemplateProperty<Output = RepoPathBuf> + 'repo,
395 ) -> CommitTemplatePropertyKind<'repo> {
396 CommitTemplatePropertyKind::RepoPath(Box::new(property))
397 }
398
399 pub fn wrap_repo_path_opt(
400 property: impl TemplateProperty<Output = Option<RepoPathBuf>> + 'repo,
401 ) -> CommitTemplatePropertyKind<'repo> {
402 CommitTemplatePropertyKind::RepoPathOpt(Box::new(property))
403 }
404
405 pub fn wrap_commit_or_change_id(
406 property: impl TemplateProperty<Output = CommitOrChangeId> + 'repo,
407 ) -> CommitTemplatePropertyKind<'repo> {
408 CommitTemplatePropertyKind::CommitOrChangeId(Box::new(property))
409 }
410
411 pub fn wrap_shortest_id_prefix(
412 property: impl TemplateProperty<Output = ShortestIdPrefix> + 'repo,
413 ) -> CommitTemplatePropertyKind<'repo> {
414 CommitTemplatePropertyKind::ShortestIdPrefix(Box::new(property))
415 }
416
417 pub fn wrap_tree_diff(
418 property: impl TemplateProperty<Output = TreeDiff> + 'repo,
419 ) -> CommitTemplatePropertyKind<'repo> {
420 CommitTemplatePropertyKind::TreeDiff(Box::new(property))
421 }
422
423 pub fn wrap_tree_diff_entry(
424 property: impl TemplateProperty<Output = TreeDiffEntry> + 'repo,
425 ) -> CommitTemplatePropertyKind<'repo> {
426 CommitTemplatePropertyKind::TreeDiffEntry(Box::new(property))
427 }
428
429 pub fn wrap_tree_diff_entry_list(
430 property: impl TemplateProperty<Output = Vec<TreeDiffEntry>> + 'repo,
431 ) -> CommitTemplatePropertyKind<'repo> {
432 CommitTemplatePropertyKind::TreeDiffEntryList(Box::new(property))
433 }
434
435 pub fn wrap_tree_entry(
436 property: impl TemplateProperty<Output = TreeEntry> + 'repo,
437 ) -> CommitTemplatePropertyKind<'repo> {
438 CommitTemplatePropertyKind::TreeEntry(Box::new(property))
439 }
440
441 pub fn wrap_diff_stats(
442 property: impl TemplateProperty<Output = DiffStatsFormatted<'repo>> + 'repo,
443 ) -> CommitTemplatePropertyKind<'repo> {
444 CommitTemplatePropertyKind::DiffStats(Box::new(property))
445 }
446
447 fn wrap_cryptographic_signature_opt(
448 property: impl TemplateProperty<Output = Option<CryptographicSignature>> + 'repo,
449 ) -> CommitTemplatePropertyKind<'repo> {
450 CommitTemplatePropertyKind::CryptographicSignatureOpt(Box::new(property))
451 }
452
453 pub fn wrap_annotation_line(
454 property: impl TemplateProperty<Output = AnnotationLine> + 'repo,
455 ) -> CommitTemplatePropertyKind<'repo> {
456 CommitTemplatePropertyKind::AnnotationLine(Box::new(property))
457 }
458}
459
460pub enum CommitTemplatePropertyKind<'repo> {
461 Core(CoreTemplatePropertyKind<'repo>),
462 Commit(Box<dyn TemplateProperty<Output = Commit> + 'repo>),
463 CommitOpt(Box<dyn TemplateProperty<Output = Option<Commit>> + 'repo>),
464 CommitList(Box<dyn TemplateProperty<Output = Vec<Commit>> + 'repo>),
465 CommitRef(Box<dyn TemplateProperty<Output = Rc<CommitRef>> + 'repo>),
466 CommitRefOpt(Box<dyn TemplateProperty<Output = Option<Rc<CommitRef>>> + 'repo>),
467 CommitRefList(Box<dyn TemplateProperty<Output = Vec<Rc<CommitRef>>> + 'repo>),
468 RepoPath(Box<dyn TemplateProperty<Output = RepoPathBuf> + 'repo>),
469 RepoPathOpt(Box<dyn TemplateProperty<Output = Option<RepoPathBuf>> + 'repo>),
470 CommitOrChangeId(Box<dyn TemplateProperty<Output = CommitOrChangeId> + 'repo>),
471 ShortestIdPrefix(Box<dyn TemplateProperty<Output = ShortestIdPrefix> + 'repo>),
472 TreeDiff(Box<dyn TemplateProperty<Output = TreeDiff> + 'repo>),
473 TreeDiffEntry(Box<dyn TemplateProperty<Output = TreeDiffEntry> + 'repo>),
474 TreeDiffEntryList(Box<dyn TemplateProperty<Output = Vec<TreeDiffEntry>> + 'repo>),
475 TreeEntry(Box<dyn TemplateProperty<Output = TreeEntry> + 'repo>),
476 DiffStats(Box<dyn TemplateProperty<Output = DiffStatsFormatted<'repo>> + 'repo>),
477 CryptographicSignatureOpt(
478 Box<dyn TemplateProperty<Output = Option<CryptographicSignature>> + 'repo>,
479 ),
480 AnnotationLine(Box<dyn TemplateProperty<Output = AnnotationLine> + 'repo>),
481}
482
483impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
484 fn type_name(&self) -> &'static str {
485 match self {
486 CommitTemplatePropertyKind::Core(property) => property.type_name(),
487 CommitTemplatePropertyKind::Commit(_) => "Commit",
488 CommitTemplatePropertyKind::CommitOpt(_) => "Option<Commit>",
489 CommitTemplatePropertyKind::CommitList(_) => "List<Commit>",
490 CommitTemplatePropertyKind::CommitRef(_) => "CommitRef",
491 CommitTemplatePropertyKind::CommitRefOpt(_) => "Option<CommitRef>",
492 CommitTemplatePropertyKind::CommitRefList(_) => "List<CommitRef>",
493 CommitTemplatePropertyKind::RepoPath(_) => "RepoPath",
494 CommitTemplatePropertyKind::RepoPathOpt(_) => "Option<RepoPath>",
495 CommitTemplatePropertyKind::CommitOrChangeId(_) => "CommitOrChangeId",
496 CommitTemplatePropertyKind::ShortestIdPrefix(_) => "ShortestIdPrefix",
497 CommitTemplatePropertyKind::TreeDiff(_) => "TreeDiff",
498 CommitTemplatePropertyKind::TreeDiffEntry(_) => "TreeDiffEntry",
499 CommitTemplatePropertyKind::TreeDiffEntryList(_) => "List<TreeDiffEntry>",
500 CommitTemplatePropertyKind::TreeEntry(_) => "TreeEntry",
501 CommitTemplatePropertyKind::DiffStats(_) => "DiffStats",
502 CommitTemplatePropertyKind::CryptographicSignatureOpt(_) => {
503 "Option<CryptographicSignature>"
504 }
505 CommitTemplatePropertyKind::AnnotationLine(_) => "AnnotationLine",
506 }
507 }
508
509 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'repo>> {
510 match self {
511 CommitTemplatePropertyKind::Core(property) => property.try_into_boolean(),
512 CommitTemplatePropertyKind::Commit(_) => None,
513 CommitTemplatePropertyKind::CommitOpt(property) => {
514 Some(Box::new(property.map(|opt| opt.is_some())))
515 }
516 CommitTemplatePropertyKind::CommitList(property) => {
517 Some(Box::new(property.map(|l| !l.is_empty())))
518 }
519 CommitTemplatePropertyKind::CommitRef(_) => None,
520 CommitTemplatePropertyKind::CommitRefOpt(property) => {
521 Some(Box::new(property.map(|opt| opt.is_some())))
522 }
523 CommitTemplatePropertyKind::CommitRefList(property) => {
524 Some(Box::new(property.map(|l| !l.is_empty())))
525 }
526 CommitTemplatePropertyKind::RepoPath(_) => None,
527 CommitTemplatePropertyKind::RepoPathOpt(property) => {
528 Some(Box::new(property.map(|opt| opt.is_some())))
529 }
530 CommitTemplatePropertyKind::CommitOrChangeId(_) => None,
531 CommitTemplatePropertyKind::ShortestIdPrefix(_) => None,
532 // TODO: boolean cast could be implemented, but explicit
533 // diff.empty() method might be better.
534 CommitTemplatePropertyKind::TreeDiff(_) => None,
535 CommitTemplatePropertyKind::TreeDiffEntry(_) => None,
536 CommitTemplatePropertyKind::TreeDiffEntryList(property) => {
537 Some(Box::new(property.map(|l| !l.is_empty())))
538 }
539 CommitTemplatePropertyKind::TreeEntry(_) => None,
540 CommitTemplatePropertyKind::DiffStats(_) => None,
541 CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => {
542 Some(Box::new(property.map(|sig| sig.is_some())))
543 }
544 CommitTemplatePropertyKind::AnnotationLine(_) => None,
545 }
546 }
547
548 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'repo>> {
549 match self {
550 CommitTemplatePropertyKind::Core(property) => property.try_into_integer(),
551 _ => None,
552 }
553 }
554
555 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'repo>> {
556 match self {
557 CommitTemplatePropertyKind::Core(property) => property.try_into_plain_text(),
558 _ => {
559 let template = self.try_into_template()?;
560 Some(Box::new(PlainTextFormattedProperty::new(template)))
561 }
562 }
563 }
564
565 fn try_into_template(self) -> Option<Box<dyn Template + 'repo>> {
566 match self {
567 CommitTemplatePropertyKind::Core(property) => property.try_into_template(),
568 CommitTemplatePropertyKind::Commit(_) => None,
569 CommitTemplatePropertyKind::CommitOpt(_) => None,
570 CommitTemplatePropertyKind::CommitList(_) => None,
571 CommitTemplatePropertyKind::CommitRef(property) => Some(property.into_template()),
572 CommitTemplatePropertyKind::CommitRefOpt(property) => Some(property.into_template()),
573 CommitTemplatePropertyKind::CommitRefList(property) => Some(property.into_template()),
574 CommitTemplatePropertyKind::RepoPath(property) => Some(property.into_template()),
575 CommitTemplatePropertyKind::RepoPathOpt(property) => Some(property.into_template()),
576 CommitTemplatePropertyKind::CommitOrChangeId(property) => {
577 Some(property.into_template())
578 }
579 CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
580 Some(property.into_template())
581 }
582 CommitTemplatePropertyKind::TreeDiff(_) => None,
583 CommitTemplatePropertyKind::TreeDiffEntry(_) => None,
584 CommitTemplatePropertyKind::TreeDiffEntryList(_) => None,
585 CommitTemplatePropertyKind::TreeEntry(_) => None,
586 CommitTemplatePropertyKind::DiffStats(property) => Some(property.into_template()),
587 CommitTemplatePropertyKind::CryptographicSignatureOpt(_) => None,
588 CommitTemplatePropertyKind::AnnotationLine(_) => None,
589 }
590 }
591
592 fn try_into_eq(self, other: Self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'repo>> {
593 match (self, other) {
594 (CommitTemplatePropertyKind::Core(lhs), CommitTemplatePropertyKind::Core(rhs)) => {
595 lhs.try_into_eq(rhs)
596 }
597 (CommitTemplatePropertyKind::Core(_), _) => None,
598 (CommitTemplatePropertyKind::Commit(_), _) => None,
599 (CommitTemplatePropertyKind::CommitOpt(_), _) => None,
600 (CommitTemplatePropertyKind::CommitList(_), _) => None,
601 (CommitTemplatePropertyKind::CommitRef(_), _) => None,
602 (CommitTemplatePropertyKind::CommitRefOpt(_), _) => None,
603 (CommitTemplatePropertyKind::CommitRefList(_), _) => None,
604 (CommitTemplatePropertyKind::RepoPath(_), _) => None,
605 (CommitTemplatePropertyKind::RepoPathOpt(_), _) => None,
606 (CommitTemplatePropertyKind::CommitOrChangeId(_), _) => None,
607 (CommitTemplatePropertyKind::ShortestIdPrefix(_), _) => None,
608 (CommitTemplatePropertyKind::TreeDiff(_), _) => None,
609 (CommitTemplatePropertyKind::TreeDiffEntry(_), _) => None,
610 (CommitTemplatePropertyKind::TreeDiffEntryList(_), _) => None,
611 (CommitTemplatePropertyKind::TreeEntry(_), _) => None,
612 (CommitTemplatePropertyKind::DiffStats(_), _) => None,
613 (CommitTemplatePropertyKind::CryptographicSignatureOpt(_), _) => None,
614 (CommitTemplatePropertyKind::AnnotationLine(_), _) => None,
615 }
616 }
617
618 fn try_into_cmp(
619 self,
620 other: Self,
621 ) -> Option<Box<dyn TemplateProperty<Output = Ordering> + 'repo>> {
622 match (self, other) {
623 (CommitTemplatePropertyKind::Core(lhs), CommitTemplatePropertyKind::Core(rhs)) => {
624 lhs.try_into_cmp(rhs)
625 }
626 (CommitTemplatePropertyKind::Core(_), _) => None,
627 (CommitTemplatePropertyKind::Commit(_), _) => None,
628 (CommitTemplatePropertyKind::CommitOpt(_), _) => None,
629 (CommitTemplatePropertyKind::CommitList(_), _) => None,
630 (CommitTemplatePropertyKind::CommitRef(_), _) => None,
631 (CommitTemplatePropertyKind::CommitRefOpt(_), _) => None,
632 (CommitTemplatePropertyKind::CommitRefList(_), _) => None,
633 (CommitTemplatePropertyKind::RepoPath(_), _) => None,
634 (CommitTemplatePropertyKind::RepoPathOpt(_), _) => None,
635 (CommitTemplatePropertyKind::CommitOrChangeId(_), _) => None,
636 (CommitTemplatePropertyKind::ShortestIdPrefix(_), _) => None,
637 (CommitTemplatePropertyKind::TreeDiff(_), _) => None,
638 (CommitTemplatePropertyKind::TreeDiffEntry(_), _) => None,
639 (CommitTemplatePropertyKind::TreeDiffEntryList(_), _) => None,
640 (CommitTemplatePropertyKind::TreeEntry(_), _) => None,
641 (CommitTemplatePropertyKind::DiffStats(_), _) => None,
642 (CommitTemplatePropertyKind::CryptographicSignatureOpt(_), _) => None,
643 (CommitTemplatePropertyKind::AnnotationLine(_), _) => None,
644 }
645 }
646}
647
648/// Table of functions that translate method call node of self type `T`.
649pub type CommitTemplateBuildMethodFnMap<'repo, T> =
650 TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>;
651
652/// Symbol table of methods available in the commit template.
653pub struct CommitTemplateBuildFnTable<'repo> {
654 pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>,
655 pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>,
656 pub commit_ref_methods: CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>>,
657 pub repo_path_methods: CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf>,
658 pub commit_or_change_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId>,
659 pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>,
660 pub tree_diff_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiff>,
661 pub tree_diff_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>,
662 pub tree_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeEntry>,
663 pub diff_stats_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStats>,
664 pub cryptographic_signature_methods:
665 CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature>,
666 pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>,
667}
668
669impl<'repo> CommitTemplateBuildFnTable<'repo> {
670 /// Creates new symbol table containing the builtin methods.
671 fn builtin() -> Self {
672 CommitTemplateBuildFnTable {
673 core: CoreTemplateBuildFnTable::builtin(),
674 commit_methods: builtin_commit_methods(),
675 commit_ref_methods: builtin_commit_ref_methods(),
676 repo_path_methods: builtin_repo_path_methods(),
677 commit_or_change_id_methods: builtin_commit_or_change_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_entry_methods: builtin_tree_entry_methods(),
682 diff_stats_methods: builtin_diff_stats_methods(),
683 cryptographic_signature_methods: builtin_cryptographic_signature_methods(),
684 annotation_line_methods: builtin_annotation_line_methods(),
685 }
686 }
687
688 pub fn empty() -> Self {
689 CommitTemplateBuildFnTable {
690 core: CoreTemplateBuildFnTable::empty(),
691 commit_methods: HashMap::new(),
692 commit_ref_methods: HashMap::new(),
693 repo_path_methods: HashMap::new(),
694 commit_or_change_id_methods: HashMap::new(),
695 shortest_id_prefix_methods: HashMap::new(),
696 tree_diff_methods: HashMap::new(),
697 tree_diff_entry_methods: HashMap::new(),
698 tree_entry_methods: HashMap::new(),
699 diff_stats_methods: HashMap::new(),
700 cryptographic_signature_methods: HashMap::new(),
701 annotation_line_methods: HashMap::new(),
702 }
703 }
704
705 fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) {
706 let CommitTemplateBuildFnTable {
707 core,
708 commit_methods,
709 commit_ref_methods,
710 repo_path_methods,
711 commit_or_change_id_methods,
712 shortest_id_prefix_methods,
713 tree_diff_methods,
714 tree_diff_entry_methods,
715 tree_entry_methods,
716 diff_stats_methods,
717 cryptographic_signature_methods,
718 annotation_line_methods,
719 } = extension;
720
721 self.core.merge(core);
722 merge_fn_map(&mut self.commit_methods, commit_methods);
723 merge_fn_map(&mut self.commit_ref_methods, commit_ref_methods);
724 merge_fn_map(&mut self.repo_path_methods, repo_path_methods);
725 merge_fn_map(
726 &mut self.commit_or_change_id_methods,
727 commit_or_change_id_methods,
728 );
729 merge_fn_map(
730 &mut self.shortest_id_prefix_methods,
731 shortest_id_prefix_methods,
732 );
733 merge_fn_map(&mut self.tree_diff_methods, tree_diff_methods);
734 merge_fn_map(&mut self.tree_diff_entry_methods, tree_diff_entry_methods);
735 merge_fn_map(&mut self.tree_entry_methods, tree_entry_methods);
736 merge_fn_map(&mut self.diff_stats_methods, diff_stats_methods);
737 merge_fn_map(
738 &mut self.cryptographic_signature_methods,
739 cryptographic_signature_methods,
740 );
741 merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods);
742 }
743}
744
745#[derive(Default)]
746pub struct CommitKeywordCache<'repo> {
747 // Build index lazily, and Rc to get away from &self lifetime.
748 bookmarks_index: OnceCell<Rc<CommitRefsIndex>>,
749 tags_index: OnceCell<Rc<CommitRefsIndex>>,
750 git_refs_index: OnceCell<Rc<CommitRefsIndex>>,
751 is_immutable_fn: OnceCell<Rc<RevsetContainingFn<'repo>>>,
752}
753
754impl<'repo> CommitKeywordCache<'repo> {
755 pub fn bookmarks_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
756 self.bookmarks_index
757 .get_or_init(|| Rc::new(build_bookmarks_index(repo)))
758 }
759
760 pub fn tags_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
761 self.tags_index
762 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().tags())))
763 }
764
765 pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> {
766 self.git_refs_index
767 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().git_refs())))
768 }
769
770 pub fn is_immutable_fn(
771 &self,
772 language: &CommitTemplateLanguage<'repo>,
773 span: pest::Span<'_>,
774 ) -> TemplateParseResult<&Rc<RevsetContainingFn<'repo>>> {
775 // Alternatively, a negated (i.e. visible mutable) set could be computed.
776 // It's usually smaller than the immutable set. The revset engine can also
777 // optimize "::<recent_heads>" query to use bitset-based implementation.
778 self.is_immutable_fn.get_or_try_init(|| {
779 let expression = &language.immutable_expression;
780 let revset = evaluate_revset_expression(language, span, expression)?;
781 Ok(revset.containing_fn().into())
782 })
783 }
784}
785
786fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> {
787 type L<'repo> = CommitTemplateLanguage<'repo>;
788 // Not using maplit::hashmap!{} or custom declarative macro here because
789 // code completion inside macro is quite restricted.
790 let mut map = CommitTemplateBuildMethodFnMap::<Commit>::new();
791 map.insert(
792 "description",
793 |_language, _diagnostics, _build_ctx, self_property, function| {
794 function.expect_no_arguments()?;
795 let out_property =
796 self_property.map(|commit| text_util::complete_newline(commit.description()));
797 Ok(L::wrap_string(out_property))
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 =
805 self_property.map(|commit| CommitOrChangeId::Change(commit.change_id().to_owned()));
806 Ok(L::wrap_commit_or_change_id(out_property))
807 },
808 );
809 map.insert(
810 "commit_id",
811 |_language, _diagnostics, _build_ctx, self_property, function| {
812 function.expect_no_arguments()?;
813 let out_property =
814 self_property.map(|commit| CommitOrChangeId::Commit(commit.id().to_owned()));
815 Ok(L::wrap_commit_or_change_id(out_property))
816 },
817 );
818 map.insert(
819 "parents",
820 |_language, _diagnostics, _build_ctx, self_property, function| {
821 function.expect_no_arguments()?;
822 let out_property =
823 self_property.and_then(|commit| Ok(commit.parents().try_collect()?));
824 Ok(L::wrap_commit_list(out_property))
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(L::wrap_signature(out_property))
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(L::wrap_signature(out_property))
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(L::wrap_boolean(out_property))
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(L::wrap_cryptographic_signature_opt(out_property))
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(L::wrap_string(out_property))
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(L::wrap_boolean(out_property))
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()
895 });
896 Ok(L::wrap_commit_ref_list(out_property))
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()
914 });
915 Ok(L::wrap_commit_ref_list(out_property))
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()
933 });
934 Ok(L::wrap_commit_ref_list(out_property))
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(L::wrap_commit_ref_list(out_property))
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(L::wrap_commit_ref_list(out_property))
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(L::wrap_boolean(out_property))
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 // The given commit could be hidden in e.g. `jj evolog`.
974 let maybe_entries = repo.resolve_change_id(commit.change_id());
975 maybe_entries.map_or(0, |entries| entries.len()) > 1
976 });
977 Ok(L::wrap_boolean(out_property))
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(L::wrap_boolean(out_property))
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(L::wrap_boolean(out_property))
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(L::wrap_boolean(out_property))
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(L::wrap_boolean(out_property))
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(L::wrap_boolean(out_property))
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 // TODO: defaults to CLI path arguments?
1040 // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731
1041 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(L::wrap_tree_diff(out_property))
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(L::wrap_boolean(out_property))
1058 },
1059 );
1060 map
1061}
1062
1063// TODO: return Vec<String>
1064fn 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/// Bookmark or tag name with metadata.
1138#[derive(Debug)]
1139pub struct CommitRef {
1140 /// Local name.
1141 name: String,
1142 /// Remote name if this is a remote or Git-tracking ref.
1143 remote: Option<String>,
1144 /// Target commit ids.
1145 target: RefTarget,
1146 /// Local ref metadata which tracks this remote ref.
1147 tracking_ref: Option<TrackingRef>,
1148 /// Local ref is synchronized with all tracking remotes, or tracking remote
1149 /// ref is synchronized with the local.
1150 synced: bool,
1151}
1152
1153#[derive(Debug)]
1154struct TrackingRef {
1155 /// Local ref target which tracks the other remote ref.
1156 target: RefTarget,
1157 /// Number of commits ahead of the tracking `target`.
1158 ahead_count: OnceCell<SizeHint>,
1159 /// Number of commits behind of the tracking `target`.
1160 behind_count: OnceCell<SizeHint>,
1161}
1162
1163impl CommitRef {
1164 // CommitRef is wrapped by Rc<T> to make it cheaply cloned and share
1165 // lazy-evaluation results across clones.
1166
1167 /// Creates local ref representation which might track some of the
1168 /// `remote_refs`.
1169 pub fn local<'a>(
1170 name: impl Into<String>,
1171 target: RefTarget,
1172 remote_refs: impl IntoIterator<Item = &'a RemoteRef>,
1173 ) -> Rc<Self> {
1174 let synced = remote_refs
1175 .into_iter()
1176 .all(|remote_ref| !remote_ref.is_tracked() || remote_ref.target == target);
1177 Rc::new(CommitRef {
1178 name: name.into(),
1179 remote: None,
1180 target,
1181 tracking_ref: None,
1182 synced,
1183 })
1184 }
1185
1186 /// Creates local ref representation which doesn't track any remote refs.
1187 pub fn local_only(name: impl Into<String>, target: RefTarget) -> Rc<Self> {
1188 Self::local(name, target, [])
1189 }
1190
1191 /// Creates remote ref representation which might be tracked by a local ref
1192 /// pointing to the `local_target`.
1193 pub fn remote(
1194 name: impl Into<String>,
1195 remote_name: impl Into<String>,
1196 remote_ref: RemoteRef,
1197 local_target: &RefTarget,
1198 ) -> Rc<Self> {
1199 let synced = remote_ref.is_tracked() && remote_ref.target == *local_target;
1200 let tracking_ref = remote_ref.is_tracked().then(|| {
1201 let count = if synced {
1202 OnceCell::from((0, Some(0))) // fast path for synced remotes
1203 } else {
1204 OnceCell::new()
1205 };
1206 TrackingRef {
1207 target: local_target.clone(),
1208 ahead_count: count.clone(),
1209 behind_count: count,
1210 }
1211 });
1212 Rc::new(CommitRef {
1213 name: name.into(),
1214 remote: Some(remote_name.into()),
1215 target: remote_ref.target,
1216 tracking_ref,
1217 synced,
1218 })
1219 }
1220
1221 /// Creates remote ref representation which isn't tracked by a local ref.
1222 pub fn remote_only(
1223 name: impl Into<String>,
1224 remote_name: impl Into<String>,
1225 target: RefTarget,
1226 ) -> Rc<Self> {
1227 Rc::new(CommitRef {
1228 name: name.into(),
1229 remote: Some(remote_name.into()),
1230 target,
1231 tracking_ref: None,
1232 synced: false, // has no local counterpart
1233 })
1234 }
1235
1236 /// Local name.
1237 pub fn name(&self) -> &str {
1238 &self.name
1239 }
1240
1241 /// Remote name if this is a remote or Git-tracking ref.
1242 pub fn remote_name(&self) -> Option<&str> {
1243 self.remote.as_deref()
1244 }
1245
1246 /// Target commit ids.
1247 pub fn target(&self) -> &RefTarget {
1248 &self.target
1249 }
1250
1251 /// Returns true if this is a local ref.
1252 pub fn is_local(&self) -> bool {
1253 self.remote.is_none()
1254 }
1255
1256 /// Returns true if this is a remote ref.
1257 pub fn is_remote(&self) -> bool {
1258 self.remote.is_some()
1259 }
1260
1261 /// Returns true if this ref points to no commit.
1262 pub fn is_absent(&self) -> bool {
1263 self.target.is_absent()
1264 }
1265
1266 /// Returns true if this ref points to any commit.
1267 pub fn is_present(&self) -> bool {
1268 self.target.is_present()
1269 }
1270
1271 /// Whether the ref target has conflicts.
1272 pub fn has_conflict(&self) -> bool {
1273 self.target.has_conflict()
1274 }
1275
1276 /// Returns true if this ref is tracked by a local ref. The local ref might
1277 /// have been deleted (but not pushed yet.)
1278 pub fn is_tracked(&self) -> bool {
1279 self.tracking_ref.is_some()
1280 }
1281
1282 /// Returns true if this ref is tracked by a local ref, and if the local ref
1283 /// is present.
1284 pub fn is_tracking_present(&self) -> bool {
1285 self.tracking_ref
1286 .as_ref()
1287 .is_some_and(|tracking| tracking.target.is_present())
1288 }
1289
1290 /// Number of commits ahead of the tracking local ref.
1291 fn tracking_ahead_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1292 let Some(tracking) = &self.tracking_ref else {
1293 return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1294 };
1295 tracking
1296 .ahead_count
1297 .get_or_try_init(|| {
1298 let self_ids = self.target.added_ids().cloned().collect_vec();
1299 let other_ids = tracking.target.added_ids().cloned().collect_vec();
1300 Ok(revset::walk_revs(repo, &self_ids, &other_ids)?.count_estimate()?)
1301 })
1302 .copied()
1303 }
1304
1305 /// Number of commits behind of the tracking local ref.
1306 fn tracking_behind_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> {
1307 let Some(tracking) = &self.tracking_ref else {
1308 return Err(TemplatePropertyError("Not a tracked remote ref".into()));
1309 };
1310 tracking
1311 .behind_count
1312 .get_or_try_init(|| {
1313 let self_ids = self.target.added_ids().cloned().collect_vec();
1314 let other_ids = tracking.target.added_ids().cloned().collect_vec();
1315 Ok(revset::walk_revs(repo, &other_ids, &self_ids)?.count_estimate()?)
1316 })
1317 .copied()
1318 }
1319}
1320
1321// If wrapping with Rc<T> becomes common, add generic impl for Rc<T>.
1322impl Template for Rc<CommitRef> {
1323 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1324 write!(formatter.labeled("name"), "{}", self.name)?;
1325 if let Some(remote) = &self.remote {
1326 write!(formatter, "@")?;
1327 write!(formatter.labeled("remote"), "{remote}")?;
1328 }
1329 // Don't show both conflict and unsynced sigils as conflicted ref wouldn't
1330 // be pushed.
1331 if self.has_conflict() {
1332 write!(formatter, "??")?;
1333 } else if self.is_local() && !self.synced {
1334 write!(formatter, "*")?;
1335 }
1336 Ok(())
1337 }
1338}
1339
1340impl Template for Vec<Rc<CommitRef>> {
1341 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1342 templater::format_joined(formatter, self, " ")
1343 }
1344}
1345
1346fn builtin_commit_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>> {
1347 type L<'repo> = CommitTemplateLanguage<'repo>;
1348 // Not using maplit::hashmap!{} or custom declarative macro here because
1349 // code completion inside macro is quite restricted.
1350 let mut map = CommitTemplateBuildMethodFnMap::<Rc<CommitRef>>::new();
1351 map.insert(
1352 "name",
1353 |_language, _diagnostics, _build_ctx, self_property, function| {
1354 function.expect_no_arguments()?;
1355 let out_property = self_property.map(|commit_ref| commit_ref.name.clone());
1356 Ok(L::wrap_string(out_property))
1357 },
1358 );
1359 map.insert(
1360 "remote",
1361 |_language, _diagnostics, _build_ctx, self_property, function| {
1362 function.expect_no_arguments()?;
1363 let out_property =
1364 self_property.map(|commit_ref| commit_ref.remote.clone().unwrap_or_default());
1365 Ok(L::wrap_string(out_property))
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(L::wrap_boolean(out_property))
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(L::wrap_boolean(out_property))
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(L::wrap_commit_opt(out_property))
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 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?)
1404 });
1405 Ok(L::wrap_commit_list(out_property))
1406 },
1407 );
1408 map.insert(
1409 "added_targets",
1410 |language, _diagnostics, _build_ctx, self_property, function| {
1411 function.expect_no_arguments()?;
1412 let repo = language.repo;
1413 let out_property = self_property.and_then(|commit_ref| {
1414 let ids = commit_ref.target.added_ids();
1415 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?)
1416 });
1417 Ok(L::wrap_commit_list(out_property))
1418 },
1419 );
1420 map.insert(
1421 "tracked",
1422 |_language, _diagnostics, _build_ctx, self_property, function| {
1423 function.expect_no_arguments()?;
1424 let out_property = self_property.map(|commit_ref| commit_ref.is_tracked());
1425 Ok(L::wrap_boolean(out_property))
1426 },
1427 );
1428 map.insert(
1429 "tracking_present",
1430 |_language, _diagnostics, _build_ctx, self_property, function| {
1431 function.expect_no_arguments()?;
1432 let out_property = self_property.map(|commit_ref| commit_ref.is_tracking_present());
1433 Ok(L::wrap_boolean(out_property))
1434 },
1435 );
1436 map.insert(
1437 "tracking_ahead_count",
1438 |language, _diagnostics, _build_ctx, self_property, function| {
1439 function.expect_no_arguments()?;
1440 let repo = language.repo;
1441 let out_property =
1442 self_property.and_then(|commit_ref| commit_ref.tracking_ahead_count(repo));
1443 Ok(L::wrap_size_hint(out_property))
1444 },
1445 );
1446 map.insert(
1447 "tracking_behind_count",
1448 |language, _diagnostics, _build_ctx, self_property, function| {
1449 function.expect_no_arguments()?;
1450 let repo = language.repo;
1451 let out_property =
1452 self_property.and_then(|commit_ref| commit_ref.tracking_behind_count(repo));
1453 Ok(L::wrap_size_hint(out_property))
1454 },
1455 );
1456 map
1457}
1458
1459/// Cache for reverse lookup refs.
1460#[derive(Clone, Debug, Default)]
1461pub struct CommitRefsIndex {
1462 index: HashMap<CommitId, Vec<Rc<CommitRef>>>,
1463}
1464
1465impl CommitRefsIndex {
1466 fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: Rc<CommitRef>) {
1467 for id in ids {
1468 let commit_refs = self.index.entry(id.clone()).or_default();
1469 commit_refs.push(name.clone());
1470 }
1471 }
1472
1473 pub fn get(&self, id: &CommitId) -> &[Rc<CommitRef>] {
1474 self.index.get(id).map_or(&[], |refs: &Vec<_>| refs)
1475 }
1476}
1477
1478fn build_bookmarks_index(repo: &dyn Repo) -> CommitRefsIndex {
1479 let mut index = CommitRefsIndex::default();
1480 for (bookmark_name, bookmark_target) in repo.view().bookmarks() {
1481 let local_target = bookmark_target.local_target;
1482 let remote_refs = bookmark_target.remote_refs;
1483 if local_target.is_present() {
1484 let commit_ref = CommitRef::local(
1485 bookmark_name,
1486 local_target.clone(),
1487 remote_refs.iter().map(|&(_, remote_ref)| remote_ref),
1488 );
1489 index.insert(local_target.added_ids(), commit_ref);
1490 }
1491 for &(remote_name, remote_ref) in &remote_refs {
1492 let commit_ref =
1493 CommitRef::remote(bookmark_name, remote_name, remote_ref.clone(), local_target);
1494 index.insert(remote_ref.target.added_ids(), commit_ref);
1495 }
1496 }
1497 index
1498}
1499
1500fn build_commit_refs_index<'a, K: Into<String>>(
1501 ref_pairs: impl IntoIterator<Item = (K, &'a RefTarget)>,
1502) -> CommitRefsIndex {
1503 let mut index = CommitRefsIndex::default();
1504 for (name, target) in ref_pairs {
1505 let commit_ref = CommitRef::local_only(name, target.clone());
1506 index.insert(target.added_ids(), commit_ref);
1507 }
1508 index
1509}
1510
1511impl Template for RepoPathBuf {
1512 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1513 write!(formatter, "{}", self.as_internal_file_string())
1514 }
1515}
1516
1517fn builtin_repo_path_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf> {
1518 type L<'repo> = CommitTemplateLanguage<'repo>;
1519 // Not using maplit::hashmap!{} or custom declarative macro here because
1520 // code completion inside macro is quite restricted.
1521 let mut map = CommitTemplateBuildMethodFnMap::<RepoPathBuf>::new();
1522 map.insert(
1523 "display",
1524 |language, _diagnostics, _build_ctx, self_property, function| {
1525 function.expect_no_arguments()?;
1526 let path_converter = language.path_converter;
1527 let out_property = self_property.map(|path| path_converter.format_file_path(&path));
1528 Ok(L::wrap_string(out_property))
1529 },
1530 );
1531 map.insert(
1532 "parent",
1533 |_language, _diagnostics, _build_ctx, self_property, function| {
1534 function.expect_no_arguments()?;
1535 let out_property = self_property.map(|path| Some(path.parent()?.to_owned()));
1536 Ok(L::wrap_repo_path_opt(out_property))
1537 },
1538 );
1539 map
1540}
1541
1542#[derive(Clone, Debug, Eq, PartialEq)]
1543pub enum CommitOrChangeId {
1544 Commit(CommitId),
1545 Change(ChangeId),
1546}
1547
1548impl CommitOrChangeId {
1549 pub fn hex(&self) -> String {
1550 match self {
1551 CommitOrChangeId::Commit(id) => id.hex(),
1552 CommitOrChangeId::Change(id) => id.reverse_hex(),
1553 }
1554 }
1555
1556 pub fn short(&self, total_len: usize) -> String {
1557 let mut hex = self.hex();
1558 hex.truncate(total_len);
1559 hex
1560 }
1561
1562 /// The length of the id printed will be the maximum of `total_len` and the
1563 /// length of the shortest unique prefix
1564 pub fn shortest(
1565 &self,
1566 repo: &dyn Repo,
1567 index: &IdPrefixIndex,
1568 total_len: usize,
1569 ) -> ShortestIdPrefix {
1570 let mut hex = self.hex();
1571 let prefix_len = match self {
1572 CommitOrChangeId::Commit(id) => index.shortest_commit_prefix_len(repo, id),
1573 CommitOrChangeId::Change(id) => index.shortest_change_prefix_len(repo, id),
1574 };
1575 hex.truncate(max(prefix_len, total_len));
1576 let rest = hex.split_off(prefix_len);
1577 ShortestIdPrefix { prefix: hex, rest }
1578 }
1579}
1580
1581impl Template for CommitOrChangeId {
1582 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1583 write!(formatter, "{}", self.hex())
1584 }
1585}
1586
1587fn builtin_commit_or_change_id_methods<'repo>(
1588) -> CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId> {
1589 type L<'repo> = CommitTemplateLanguage<'repo>;
1590 // Not using maplit::hashmap!{} or custom declarative macro here because
1591 // code completion inside macro is quite restricted.
1592 let mut map = CommitTemplateBuildMethodFnMap::<CommitOrChangeId>::new();
1593 map.insert(
1594 "normal_hex",
1595 |_language, _diagnostics, _build_ctx, self_property, function| {
1596 function.expect_no_arguments()?;
1597 Ok(L::wrap_string(self_property.map(|id| {
1598 // Note: this is _not_ the same as id.hex() for ChangeId, which
1599 // returns the "reverse" hex (z-k), instead of the "forward" /
1600 // normal hex (0-9a-f) we want here.
1601 match id {
1602 CommitOrChangeId::Commit(id) => id.hex(),
1603 CommitOrChangeId::Change(id) => id.hex(),
1604 }
1605 })))
1606 },
1607 );
1608 map.insert(
1609 "short",
1610 |language, diagnostics, build_ctx, self_property, function| {
1611 let ([], [len_node]) = function.expect_arguments()?;
1612 let len_property = len_node
1613 .map(|node| {
1614 template_builder::expect_usize_expression(
1615 language,
1616 diagnostics,
1617 build_ctx,
1618 node,
1619 )
1620 })
1621 .transpose()?;
1622 let out_property =
1623 (self_property, len_property).map(|(id, len)| id.short(len.unwrap_or(12)));
1624 Ok(L::wrap_string(out_property))
1625 },
1626 );
1627 map.insert(
1628 "shortest",
1629 |language, diagnostics, build_ctx, self_property, function| {
1630 let ([], [len_node]) = function.expect_arguments()?;
1631 let len_property = len_node
1632 .map(|node| {
1633 template_builder::expect_usize_expression(
1634 language,
1635 diagnostics,
1636 build_ctx,
1637 node,
1638 )
1639 })
1640 .transpose()?;
1641 let repo = language.repo;
1642 let index = match language.id_prefix_context.populate(repo) {
1643 Ok(index) => index,
1644 Err(err) => {
1645 // Not an error because we can still produce somewhat
1646 // reasonable output.
1647 diagnostics.add_warning(
1648 TemplateParseError::expression(
1649 "Failed to load short-prefixes index",
1650 function.name_span,
1651 )
1652 .with_source(err),
1653 );
1654 IdPrefixIndex::empty()
1655 }
1656 };
1657 let out_property = (self_property, len_property)
1658 .map(move |(id, len)| id.shortest(repo, &index, len.unwrap_or(0)));
1659 Ok(L::wrap_shortest_id_prefix(out_property))
1660 },
1661 );
1662 map
1663}
1664
1665pub struct ShortestIdPrefix {
1666 pub prefix: String,
1667 pub rest: String,
1668}
1669
1670impl Template for ShortestIdPrefix {
1671 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1672 write!(formatter.labeled("prefix"), "{}", self.prefix)?;
1673 write!(formatter.labeled("rest"), "{}", self.rest)?;
1674 Ok(())
1675 }
1676}
1677
1678impl ShortestIdPrefix {
1679 fn to_upper(&self) -> Self {
1680 Self {
1681 prefix: self.prefix.to_ascii_uppercase(),
1682 rest: self.rest.to_ascii_uppercase(),
1683 }
1684 }
1685 fn to_lower(&self) -> Self {
1686 Self {
1687 prefix: self.prefix.to_ascii_lowercase(),
1688 rest: self.rest.to_ascii_lowercase(),
1689 }
1690 }
1691}
1692
1693fn builtin_shortest_id_prefix_methods<'repo>(
1694) -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> {
1695 type L<'repo> = CommitTemplateLanguage<'repo>;
1696 // Not using maplit::hashmap!{} or custom declarative macro here because
1697 // code completion inside macro is quite restricted.
1698 let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new();
1699 map.insert(
1700 "prefix",
1701 |_language, _diagnostics, _build_ctx, self_property, function| {
1702 function.expect_no_arguments()?;
1703 let out_property = self_property.map(|id| id.prefix);
1704 Ok(L::wrap_string(out_property))
1705 },
1706 );
1707 map.insert(
1708 "rest",
1709 |_language, _diagnostics, _build_ctx, self_property, function| {
1710 function.expect_no_arguments()?;
1711 let out_property = self_property.map(|id| id.rest);
1712 Ok(L::wrap_string(out_property))
1713 },
1714 );
1715 map.insert(
1716 "upper",
1717 |_language, _diagnostics, _build_ctx, self_property, function| {
1718 function.expect_no_arguments()?;
1719 let out_property = self_property.map(|id| id.to_upper());
1720 Ok(L::wrap_shortest_id_prefix(out_property))
1721 },
1722 );
1723 map.insert(
1724 "lower",
1725 |_language, _diagnostics, _build_ctx, self_property, function| {
1726 function.expect_no_arguments()?;
1727 let out_property = self_property.map(|id| id.to_lower());
1728 Ok(L::wrap_shortest_id_prefix(out_property))
1729 },
1730 );
1731 map
1732}
1733
1734/// Pair of trees to be diffed.
1735#[derive(Debug)]
1736pub struct TreeDiff {
1737 from_tree: MergedTree,
1738 to_tree: MergedTree,
1739 matcher: Rc<dyn Matcher>,
1740 copy_records: CopyRecords,
1741}
1742
1743impl TreeDiff {
1744 fn from_commit(
1745 repo: &dyn Repo,
1746 commit: &Commit,
1747 matcher: Rc<dyn Matcher>,
1748 ) -> BackendResult<Self> {
1749 let mut copy_records = CopyRecords::default();
1750 for parent in commit.parent_ids() {
1751 let records =
1752 diff_util::get_copy_records(repo.store(), parent, commit.id(), &*matcher)?;
1753 copy_records.add_records(records)?;
1754 }
1755 Ok(TreeDiff {
1756 from_tree: commit.parent_tree(repo)?,
1757 to_tree: commit.tree()?,
1758 matcher,
1759 copy_records,
1760 })
1761 }
1762
1763 fn diff_stream(&self) -> BoxStream<'_, CopiesTreeDiffEntry> {
1764 self.from_tree
1765 .diff_stream_with_copies(&self.to_tree, &*self.matcher, &self.copy_records)
1766 }
1767
1768 async fn collect_entries(&self) -> BackendResult<Vec<TreeDiffEntry>> {
1769 self.diff_stream()
1770 .map(TreeDiffEntry::from_backend_entry_with_copies)
1771 .try_collect()
1772 .await
1773 }
1774
1775 fn into_formatted<F, E>(self, show: F) -> TreeDiffFormatted<F>
1776 where
1777 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>,
1778 E: Into<TemplatePropertyError>,
1779 {
1780 TreeDiffFormatted { diff: self, show }
1781 }
1782}
1783
1784/// Tree diff to be rendered by predefined function `F`.
1785struct TreeDiffFormatted<F> {
1786 diff: TreeDiff,
1787 show: F,
1788}
1789
1790impl<F, E> Template for TreeDiffFormatted<F>
1791where
1792 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>,
1793 E: Into<TemplatePropertyError>,
1794{
1795 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1796 let show = &self.show;
1797 let store = self.diff.from_tree.store();
1798 let tree_diff = self.diff.diff_stream();
1799 show(formatter.as_mut(), store, tree_diff).or_else(|err| formatter.handle_error(err.into()))
1800 }
1801}
1802
1803fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiff> {
1804 type L<'repo> = CommitTemplateLanguage<'repo>;
1805 // Not using maplit::hashmap!{} or custom declarative macro here because
1806 // code completion inside macro is quite restricted.
1807 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiff>::new();
1808 map.insert(
1809 "files",
1810 |_language, _diagnostics, _build_ctx, self_property, function| {
1811 function.expect_no_arguments()?;
1812 // TODO: cache and reuse diff entries within the current evaluation?
1813 let out_property =
1814 self_property.and_then(|diff| Ok(diff.collect_entries().block_on()?));
1815 Ok(L::wrap_tree_diff_entry_list(out_property))
1816 },
1817 );
1818 map.insert(
1819 "color_words",
1820 |language, diagnostics, build_ctx, self_property, function| {
1821 let ([], [context_node]) = function.expect_arguments()?;
1822 let context_property = context_node
1823 .map(|node| {
1824 template_builder::expect_usize_expression(
1825 language,
1826 diagnostics,
1827 build_ctx,
1828 node,
1829 )
1830 })
1831 .transpose()?;
1832 let path_converter = language.path_converter;
1833 let options = diff_util::ColorWordsDiffOptions::from_settings(language.settings())
1834 .map_err(|err| {
1835 let message = "Failed to load diff settings";
1836 TemplateParseError::expression(message, function.name_span).with_source(err)
1837 })?;
1838 let conflict_marker_style = language.conflict_marker_style;
1839 let template = (self_property, context_property)
1840 .map(move |(diff, context)| {
1841 let mut options = options.clone();
1842 if let Some(context) = context {
1843 options.context = context;
1844 }
1845 diff.into_formatted(move |formatter, store, tree_diff| {
1846 diff_util::show_color_words_diff(
1847 formatter,
1848 store,
1849 tree_diff,
1850 path_converter,
1851 &options,
1852 conflict_marker_style,
1853 )
1854 })
1855 })
1856 .into_template();
1857 Ok(L::wrap_template(template))
1858 },
1859 );
1860 map.insert(
1861 "git",
1862 |language, diagnostics, build_ctx, self_property, function| {
1863 let ([], [context_node]) = function.expect_arguments()?;
1864 let context_property = context_node
1865 .map(|node| {
1866 template_builder::expect_usize_expression(
1867 language,
1868 diagnostics,
1869 build_ctx,
1870 node,
1871 )
1872 })
1873 .transpose()?;
1874 let options = diff_util::UnifiedDiffOptions::from_settings(language.settings())
1875 .map_err(|err| {
1876 let message = "Failed to load diff settings";
1877 TemplateParseError::expression(message, function.name_span).with_source(err)
1878 })?;
1879 let conflict_marker_style = language.conflict_marker_style;
1880 let template = (self_property, context_property)
1881 .map(move |(diff, context)| {
1882 let mut options = options.clone();
1883 if let Some(context) = context {
1884 options.context = context;
1885 }
1886 diff.into_formatted(move |formatter, store, tree_diff| {
1887 diff_util::show_git_diff(
1888 formatter,
1889 store,
1890 tree_diff,
1891 &options,
1892 conflict_marker_style,
1893 )
1894 })
1895 })
1896 .into_template();
1897 Ok(L::wrap_template(template))
1898 },
1899 );
1900 map.insert(
1901 "stat",
1902 |language, diagnostics, build_ctx, self_property, function| {
1903 let ([], [width_node]) = function.expect_arguments()?;
1904 let width_property = width_node
1905 .map(|node| {
1906 template_builder::expect_usize_expression(
1907 language,
1908 diagnostics,
1909 build_ctx,
1910 node,
1911 )
1912 })
1913 .transpose()?;
1914 let path_converter = language.path_converter;
1915 // No user configuration exists for diff stat.
1916 let options = diff_util::DiffStatOptions::default();
1917 let conflict_marker_style = language.conflict_marker_style;
1918 // TODO: cache and reuse stats within the current evaluation?
1919 let out_property = (self_property, width_property).and_then(move |(diff, width)| {
1920 let store = diff.from_tree.store();
1921 let tree_diff = diff.diff_stream();
1922 let stats = DiffStats::calculate(store, tree_diff, &options, conflict_marker_style)
1923 .block_on()?;
1924 Ok(DiffStatsFormatted {
1925 stats,
1926 path_converter,
1927 // TODO: fall back to current available width
1928 width: width.unwrap_or(80),
1929 })
1930 });
1931 Ok(L::wrap_diff_stats(out_property))
1932 },
1933 );
1934 map.insert(
1935 "summary",
1936 |language, _diagnostics, _build_ctx, self_property, function| {
1937 function.expect_no_arguments()?;
1938 let path_converter = language.path_converter;
1939 let template = self_property
1940 .map(move |diff| {
1941 diff.into_formatted(move |formatter, _store, tree_diff| {
1942 diff_util::show_diff_summary(formatter, tree_diff, path_converter)
1943 })
1944 })
1945 .into_template();
1946 Ok(L::wrap_template(template))
1947 },
1948 );
1949 // TODO: add support for external tools
1950 map
1951}
1952
1953/// [`MergedTree`] diff entry.
1954#[derive(Clone, Debug)]
1955pub struct TreeDiffEntry {
1956 pub path: CopiesTreeDiffEntryPath,
1957 pub source_value: MergedTreeValue,
1958 pub target_value: MergedTreeValue,
1959}
1960
1961impl TreeDiffEntry {
1962 fn from_backend_entry_with_copies(entry: CopiesTreeDiffEntry) -> BackendResult<Self> {
1963 let (source_value, target_value) = entry.values?;
1964 Ok(TreeDiffEntry {
1965 path: entry.path,
1966 source_value,
1967 target_value,
1968 })
1969 }
1970
1971 fn status_label(&self) -> &'static str {
1972 let (label, _sigil) = diff_util::diff_status_label_and_char(
1973 &self.path,
1974 &self.source_value,
1975 &self.target_value,
1976 );
1977 label
1978 }
1979
1980 fn into_source_entry(self) -> TreeEntry {
1981 TreeEntry {
1982 path: self.path.source.map_or(self.path.target, |(path, _)| path),
1983 value: self.source_value,
1984 }
1985 }
1986
1987 fn into_target_entry(self) -> TreeEntry {
1988 TreeEntry {
1989 path: self.path.target,
1990 value: self.target_value,
1991 }
1992 }
1993}
1994
1995fn builtin_tree_diff_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>
1996{
1997 type L<'repo> = CommitTemplateLanguage<'repo>;
1998 // Not using maplit::hashmap!{} or custom declarative macro here because
1999 // code completion inside macro is quite restricted.
2000 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiffEntry>::new();
2001 map.insert(
2002 "path",
2003 |_language, _diagnostics, _build_ctx, self_property, function| {
2004 function.expect_no_arguments()?;
2005 let out_property = self_property.map(|entry| entry.path.target);
2006 Ok(L::wrap_repo_path(out_property))
2007 },
2008 );
2009 map.insert(
2010 "status",
2011 |_language, _diagnostics, _build_ctx, self_property, function| {
2012 function.expect_no_arguments()?;
2013 let out_property = self_property.map(|entry| entry.status_label().to_owned());
2014 Ok(L::wrap_string(out_property))
2015 },
2016 );
2017 // TODO: add status_code() or status_char()?
2018 map.insert(
2019 "source",
2020 |_language, _diagnostics, _build_ctx, self_property, function| {
2021 function.expect_no_arguments()?;
2022 let out_property = self_property.map(TreeDiffEntry::into_source_entry);
2023 Ok(L::wrap_tree_entry(out_property))
2024 },
2025 );
2026 map.insert(
2027 "target",
2028 |_language, _diagnostics, _build_ctx, self_property, function| {
2029 function.expect_no_arguments()?;
2030 let out_property = self_property.map(TreeDiffEntry::into_target_entry);
2031 Ok(L::wrap_tree_entry(out_property))
2032 },
2033 );
2034 map
2035}
2036
2037/// [`MergedTree`] entry.
2038#[derive(Clone, Debug)]
2039pub struct TreeEntry {
2040 pub path: RepoPathBuf,
2041 pub value: MergedTreeValue,
2042}
2043
2044fn builtin_tree_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeEntry> {
2045 type L<'repo> = CommitTemplateLanguage<'repo>;
2046 // Not using maplit::hashmap!{} or custom declarative macro here because
2047 // code completion inside macro is quite restricted.
2048 let mut map = CommitTemplateBuildMethodFnMap::<TreeEntry>::new();
2049 map.insert(
2050 "path",
2051 |_language, _diagnostics, _build_ctx, self_property, function| {
2052 function.expect_no_arguments()?;
2053 let out_property = self_property.map(|entry| entry.path);
2054 Ok(L::wrap_repo_path(out_property))
2055 },
2056 );
2057 map.insert(
2058 "conflict",
2059 |_language, _diagnostics, _build_ctx, self_property, function| {
2060 function.expect_no_arguments()?;
2061 let out_property = self_property.map(|entry| !entry.value.is_resolved());
2062 Ok(L::wrap_boolean(out_property))
2063 },
2064 );
2065 map.insert(
2066 "file_type",
2067 |_language, _diagnostics, _build_ctx, self_property, function| {
2068 function.expect_no_arguments()?;
2069 let out_property =
2070 self_property.map(|entry| describe_file_type(&entry.value).to_owned());
2071 Ok(L::wrap_string(out_property))
2072 },
2073 );
2074 map.insert(
2075 "executable",
2076 |_language, _diagnostics, _build_ctx, self_property, function| {
2077 function.expect_no_arguments()?;
2078 let out_property =
2079 self_property.map(|entry| is_executable_file(&entry.value).unwrap_or_default());
2080 Ok(L::wrap_boolean(out_property))
2081 },
2082 );
2083 map
2084}
2085
2086fn describe_file_type(value: &MergedTreeValue) -> &'static str {
2087 match value.as_resolved() {
2088 Some(Some(TreeValue::File { .. })) => "file",
2089 Some(Some(TreeValue::Symlink(_))) => "symlink",
2090 Some(Some(TreeValue::Tree(_))) => "tree",
2091 Some(Some(TreeValue::GitSubmodule(_))) => "git-submodule",
2092 Some(None) => "", // absent
2093 None | Some(Some(TreeValue::Conflict(_))) => "conflict",
2094 }
2095}
2096
2097fn is_executable_file(value: &MergedTreeValue) -> Option<bool> {
2098 let executable = value.to_executable_merge()?;
2099 executable.resolve_trivial().copied()
2100}
2101
2102/// [`DiffStats`] with rendering parameters.
2103#[derive(Clone, Debug)]
2104pub struct DiffStatsFormatted<'a> {
2105 stats: DiffStats,
2106 path_converter: &'a RepoPathUiConverter,
2107 width: usize,
2108}
2109
2110impl Template for DiffStatsFormatted<'_> {
2111 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
2112 diff_util::show_diff_stats(
2113 formatter.as_mut(),
2114 &self.stats,
2115 self.path_converter,
2116 self.width,
2117 )
2118 }
2119}
2120
2121fn builtin_diff_stats_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStats> {
2122 type L<'repo> = CommitTemplateLanguage<'repo>;
2123 // Not using maplit::hashmap!{} or custom declarative macro here because
2124 // code completion inside macro is quite restricted.
2125 let mut map = CommitTemplateBuildMethodFnMap::<DiffStats>::new();
2126 // TODO: add files() -> List<DiffStatEntry> ?
2127 map.insert(
2128 "total_added",
2129 |_language, _diagnostics, _build_ctx, self_property, function| {
2130 function.expect_no_arguments()?;
2131 let out_property =
2132 self_property.and_then(|stats| Ok(stats.count_total_added().try_into()?));
2133 Ok(L::wrap_integer(out_property))
2134 },
2135 );
2136 map.insert(
2137 "total_removed",
2138 |_language, _diagnostics, _build_ctx, self_property, function| {
2139 function.expect_no_arguments()?;
2140 let out_property =
2141 self_property.and_then(|stats| Ok(stats.count_total_removed().try_into()?));
2142 Ok(L::wrap_integer(out_property))
2143 },
2144 );
2145 map
2146}
2147
2148#[derive(Debug)]
2149pub struct CryptographicSignature {
2150 commit: Commit,
2151}
2152
2153impl CryptographicSignature {
2154 fn new(commit: Commit) -> Option<Self> {
2155 commit.is_signed().then_some(Self { commit })
2156 }
2157
2158 fn verify(&self) -> SignResult<Verification> {
2159 self.commit
2160 .verification()
2161 .transpose()
2162 .expect("must have signature")
2163 }
2164
2165 fn status(&self) -> SignResult<SigStatus> {
2166 self.verify().map(|verification| verification.status)
2167 }
2168
2169 /// Defaults to empty string if key is not present.
2170 fn key(&self) -> SignResult<String> {
2171 self.verify()
2172 .map(|verification| verification.key.unwrap_or_default())
2173 }
2174
2175 /// Defaults to empty string if display is not present.
2176 fn display(&self) -> SignResult<String> {
2177 self.verify()
2178 .map(|verification| verification.display.unwrap_or_default())
2179 }
2180}
2181
2182pub fn builtin_cryptographic_signature_methods<'repo>(
2183) -> CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature> {
2184 type L<'repo> = CommitTemplateLanguage<'repo>;
2185 // Not using maplit::hashmap!{} or custom declarative macro here because
2186 // code completion inside macro is quite restricted.
2187 let mut map = CommitTemplateBuildMethodFnMap::<CryptographicSignature>::new();
2188 map.insert(
2189 "status",
2190 |_language, _diagnostics, _build_ctx, self_property, function| {
2191 function.expect_no_arguments()?;
2192 let out_property = self_property.and_then(|sig| match sig.status() {
2193 Ok(status) => Ok(status.to_string()),
2194 Err(SignError::InvalidSignatureFormat) => Ok("invalid".to_string()),
2195 Err(err) => Err(err.into()),
2196 });
2197 Ok(L::wrap_string(out_property))
2198 },
2199 );
2200 map.insert(
2201 "key",
2202 |_language, _diagnostics, _build_ctx, self_property, function| {
2203 function.expect_no_arguments()?;
2204 let out_property = self_property.and_then(|sig| Ok(sig.key()?));
2205 Ok(L::wrap_string(out_property))
2206 },
2207 );
2208 map.insert(
2209 "display",
2210 |_language, _diagnostics, _build_ctx, self_property, function| {
2211 function.expect_no_arguments()?;
2212 let out_property = self_property.and_then(|sig| Ok(sig.display()?));
2213 Ok(L::wrap_string(out_property))
2214 },
2215 );
2216 map
2217}
2218
2219#[derive(Debug, Clone)]
2220pub struct AnnotationLine {
2221 pub commit: Commit,
2222 pub content: BString,
2223 pub line_number: usize,
2224 pub first_line_in_hunk: bool,
2225}
2226
2227pub fn builtin_annotation_line_methods<'repo>(
2228) -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine> {
2229 type L<'repo> = CommitTemplateLanguage<'repo>;
2230 let mut map = CommitTemplateBuildMethodFnMap::<AnnotationLine>::new();
2231 map.insert(
2232 "commit",
2233 |_language, _diagnostics, _build_ctx, self_property, function| {
2234 function.expect_no_arguments()?;
2235 let out_property = self_property.map(|line| line.commit);
2236 Ok(L::wrap_commit(out_property))
2237 },
2238 );
2239 map.insert(
2240 "content",
2241 |_language, _diagnostics, _build_ctx, self_property, function| {
2242 function.expect_no_arguments()?;
2243 let out_property = self_property.map(|line| line.content);
2244 // TODO: Add Bytes or BString template type?
2245 Ok(L::wrap_template(out_property.into_template()))
2246 },
2247 );
2248 map.insert(
2249 "line_number",
2250 |_language, _diagnostics, _build_ctx, self_property, function| {
2251 function.expect_no_arguments()?;
2252 let out_property = self_property.and_then(|line| Ok(line.line_number.try_into()?));
2253 Ok(L::wrap_integer(out_property))
2254 },
2255 );
2256 map.insert(
2257 "first_line_in_hunk",
2258 |_language, _diagnostics, _build_ctx, self_property, function| {
2259 function.expect_no_arguments()?;
2260 let out_property = self_property.map(|line| line.first_line_in_hunk);
2261 Ok(L::wrap_boolean(out_property))
2262 },
2263 );
2264 map
2265}