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::collections::HashMap;
18use std::io;
19use std::rc::Rc;
20
21use itertools::Itertools as _;
22use jj_lib::backend::{ChangeId, CommitId};
23use jj_lib::commit::Commit;
24use jj_lib::extensions_map::ExtensionsMap;
25use jj_lib::hex_util::to_reverse_hex;
26use jj_lib::id_prefix::IdPrefixContext;
27use jj_lib::object_id::ObjectId as _;
28use jj_lib::op_store::{RefTarget, WorkspaceId};
29use jj_lib::repo::Repo;
30use jj_lib::revset::{self, Revset, RevsetExpression, RevsetParseContext};
31use jj_lib::{git, rewrite};
32use once_cell::unsync::OnceCell;
33
34use crate::template_builder::{
35 self, merge_fn_map, BuildContext, CoreTemplateBuildFnTable, CoreTemplatePropertyKind,
36 IntoTemplateProperty, TemplateBuildMethodFnMap, TemplateLanguage,
37};
38use crate::template_parser::{self, FunctionCallNode, TemplateParseError, TemplateParseResult};
39use crate::templater::{
40 self, IntoTemplate, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty,
41 TemplatePropertyError, TemplatePropertyExt as _,
42};
43use crate::{revset_util, text_util};
44
45pub trait CommitTemplateLanguageExtension {
46 fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>;
47
48 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
49}
50
51pub struct CommitTemplateLanguage<'repo> {
52 repo: &'repo dyn Repo,
53 workspace_id: WorkspaceId,
54 // RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime
55 // anyway to capture it to evaluate dynamically-constructed user expression
56 // such as `revset("ancestors(" ++ commit_id ++ ")")`.
57 // TODO: Maybe refactor context structs? WorkspaceId is contained in
58 // RevsetParseContext for example.
59 revset_parse_context: RevsetParseContext<'repo>,
60 id_prefix_context: &'repo IdPrefixContext,
61 build_fn_table: CommitTemplateBuildFnTable<'repo>,
62 keyword_cache: CommitKeywordCache<'repo>,
63 cache_extensions: ExtensionsMap,
64}
65
66impl<'repo> CommitTemplateLanguage<'repo> {
67 /// Sets up environment where commit template will be transformed to
68 /// evaluation tree.
69 pub fn new(
70 repo: &'repo dyn Repo,
71 workspace_id: &WorkspaceId,
72 revset_parse_context: RevsetParseContext<'repo>,
73 id_prefix_context: &'repo IdPrefixContext,
74 extensions: &[impl AsRef<dyn CommitTemplateLanguageExtension>],
75 ) -> Self {
76 let mut build_fn_table = CommitTemplateBuildFnTable::builtin();
77 let mut cache_extensions = ExtensionsMap::empty();
78
79 for extension in extensions {
80 build_fn_table.merge(extension.as_ref().build_fn_table());
81 extension
82 .as_ref()
83 .build_cache_extensions(&mut cache_extensions);
84 }
85
86 CommitTemplateLanguage {
87 repo,
88 workspace_id: workspace_id.clone(),
89 revset_parse_context,
90 id_prefix_context,
91 build_fn_table,
92 keyword_cache: CommitKeywordCache::default(),
93 cache_extensions,
94 }
95 }
96}
97
98impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
99 type Property = CommitTemplatePropertyKind<'repo>;
100
101 template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core);
102
103 fn build_function(
104 &self,
105 build_ctx: &BuildContext<Self::Property>,
106 function: &FunctionCallNode,
107 ) -> TemplateParseResult<Self::Property> {
108 let table = &self.build_fn_table.core;
109 table.build_function(self, build_ctx, function)
110 }
111
112 fn build_method(
113 &self,
114 build_ctx: &BuildContext<Self::Property>,
115 property: Self::Property,
116 function: &FunctionCallNode,
117 ) -> TemplateParseResult<Self::Property> {
118 let type_name = property.type_name();
119 match property {
120 CommitTemplatePropertyKind::Core(property) => {
121 let table = &self.build_fn_table.core;
122 table.build_method(self, build_ctx, property, function)
123 }
124 CommitTemplatePropertyKind::Commit(property) => {
125 let table = &self.build_fn_table.commit_methods;
126 let build = template_parser::lookup_method(type_name, table, function)?;
127 build(self, build_ctx, property, function)
128 }
129 CommitTemplatePropertyKind::CommitOpt(property) => {
130 let type_name = "Commit";
131 let table = &self.build_fn_table.commit_methods;
132 let build = template_parser::lookup_method(type_name, table, function)?;
133 let inner_property = property.and_then(|opt| {
134 opt.ok_or_else(|| TemplatePropertyError("No commit available".into()))
135 });
136 build(self, build_ctx, Box::new(inner_property), function)
137 }
138 CommitTemplatePropertyKind::CommitList(property) => {
139 // TODO: migrate to table?
140 template_builder::build_unformattable_list_method(
141 self,
142 build_ctx,
143 property,
144 function,
145 Self::wrap_commit,
146 )
147 }
148 CommitTemplatePropertyKind::RefName(property) => {
149 let table = &self.build_fn_table.ref_name_methods;
150 let build = template_parser::lookup_method(type_name, table, function)?;
151 build(self, build_ctx, property, function)
152 }
153 CommitTemplatePropertyKind::RefNameOpt(property) => {
154 let type_name = "RefName";
155 let table = &self.build_fn_table.ref_name_methods;
156 let build = template_parser::lookup_method(type_name, table, function)?;
157 let inner_property = property.and_then(|opt| {
158 opt.ok_or_else(|| TemplatePropertyError("No RefName available".into()))
159 });
160 build(self, build_ctx, Box::new(inner_property), function)
161 }
162 CommitTemplatePropertyKind::RefNameList(property) => {
163 // TODO: migrate to table?
164 template_builder::build_formattable_list_method(
165 self,
166 build_ctx,
167 property,
168 function,
169 Self::wrap_ref_name,
170 )
171 }
172 CommitTemplatePropertyKind::CommitOrChangeId(property) => {
173 let table = &self.build_fn_table.commit_or_change_id_methods;
174 let build = template_parser::lookup_method(type_name, table, function)?;
175 build(self, build_ctx, property, function)
176 }
177 CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
178 let table = &self.build_fn_table.shortest_id_prefix_methods;
179 let build = template_parser::lookup_method(type_name, table, function)?;
180 build(self, build_ctx, property, function)
181 }
182 }
183 }
184}
185
186// If we need to add multiple languages that support Commit types, this can be
187// turned into a trait which extends TemplateLanguage.
188impl<'repo> CommitTemplateLanguage<'repo> {
189 pub fn repo(&self) -> &'repo dyn Repo {
190 self.repo
191 }
192
193 pub fn workspace_id(&self) -> &WorkspaceId {
194 &self.workspace_id
195 }
196
197 pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> {
198 &self.keyword_cache
199 }
200
201 pub fn cache_extension<T: Any>(&self) -> Option<&T> {
202 self.cache_extensions.get::<T>()
203 }
204
205 pub fn wrap_commit(
206 property: impl TemplateProperty<Output = Commit> + 'repo,
207 ) -> CommitTemplatePropertyKind<'repo> {
208 CommitTemplatePropertyKind::Commit(Box::new(property))
209 }
210
211 pub fn wrap_commit_opt(
212 property: impl TemplateProperty<Output = Option<Commit>> + 'repo,
213 ) -> CommitTemplatePropertyKind<'repo> {
214 CommitTemplatePropertyKind::CommitOpt(Box::new(property))
215 }
216
217 pub fn wrap_commit_list(
218 property: impl TemplateProperty<Output = Vec<Commit>> + 'repo,
219 ) -> CommitTemplatePropertyKind<'repo> {
220 CommitTemplatePropertyKind::CommitList(Box::new(property))
221 }
222
223 pub fn wrap_ref_name(
224 property: impl TemplateProperty<Output = RefName> + 'repo,
225 ) -> CommitTemplatePropertyKind<'repo> {
226 CommitTemplatePropertyKind::RefName(Box::new(property))
227 }
228
229 pub fn wrap_ref_name_opt(
230 property: impl TemplateProperty<Output = Option<RefName>> + 'repo,
231 ) -> CommitTemplatePropertyKind<'repo> {
232 CommitTemplatePropertyKind::RefNameOpt(Box::new(property))
233 }
234
235 pub fn wrap_ref_name_list(
236 property: impl TemplateProperty<Output = Vec<RefName>> + 'repo,
237 ) -> CommitTemplatePropertyKind<'repo> {
238 CommitTemplatePropertyKind::RefNameList(Box::new(property))
239 }
240
241 pub fn wrap_commit_or_change_id(
242 property: impl TemplateProperty<Output = CommitOrChangeId> + 'repo,
243 ) -> CommitTemplatePropertyKind<'repo> {
244 CommitTemplatePropertyKind::CommitOrChangeId(Box::new(property))
245 }
246
247 pub fn wrap_shortest_id_prefix(
248 property: impl TemplateProperty<Output = ShortestIdPrefix> + 'repo,
249 ) -> CommitTemplatePropertyKind<'repo> {
250 CommitTemplatePropertyKind::ShortestIdPrefix(Box::new(property))
251 }
252}
253
254pub enum CommitTemplatePropertyKind<'repo> {
255 Core(CoreTemplatePropertyKind<'repo>),
256 Commit(Box<dyn TemplateProperty<Output = Commit> + 'repo>),
257 CommitOpt(Box<dyn TemplateProperty<Output = Option<Commit>> + 'repo>),
258 CommitList(Box<dyn TemplateProperty<Output = Vec<Commit>> + 'repo>),
259 RefName(Box<dyn TemplateProperty<Output = RefName> + 'repo>),
260 RefNameOpt(Box<dyn TemplateProperty<Output = Option<RefName>> + 'repo>),
261 RefNameList(Box<dyn TemplateProperty<Output = Vec<RefName>> + 'repo>),
262 CommitOrChangeId(Box<dyn TemplateProperty<Output = CommitOrChangeId> + 'repo>),
263 ShortestIdPrefix(Box<dyn TemplateProperty<Output = ShortestIdPrefix> + 'repo>),
264}
265
266impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
267 fn type_name(&self) -> &'static str {
268 match self {
269 CommitTemplatePropertyKind::Core(property) => property.type_name(),
270 CommitTemplatePropertyKind::Commit(_) => "Commit",
271 CommitTemplatePropertyKind::CommitOpt(_) => "Option<Commit>",
272 CommitTemplatePropertyKind::CommitList(_) => "List<Commit>",
273 CommitTemplatePropertyKind::RefName(_) => "RefName",
274 CommitTemplatePropertyKind::RefNameOpt(_) => "Option<RefName>",
275 CommitTemplatePropertyKind::RefNameList(_) => "List<RefName>",
276 CommitTemplatePropertyKind::CommitOrChangeId(_) => "CommitOrChangeId",
277 CommitTemplatePropertyKind::ShortestIdPrefix(_) => "ShortestIdPrefix",
278 }
279 }
280
281 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'repo>> {
282 match self {
283 CommitTemplatePropertyKind::Core(property) => property.try_into_boolean(),
284 CommitTemplatePropertyKind::Commit(_) => None,
285 CommitTemplatePropertyKind::CommitOpt(property) => {
286 Some(Box::new(property.map(|opt| opt.is_some())))
287 }
288 CommitTemplatePropertyKind::CommitList(property) => {
289 Some(Box::new(property.map(|l| !l.is_empty())))
290 }
291 CommitTemplatePropertyKind::RefName(_) => None,
292 CommitTemplatePropertyKind::RefNameOpt(property) => {
293 Some(Box::new(property.map(|opt| opt.is_some())))
294 }
295 CommitTemplatePropertyKind::RefNameList(property) => {
296 Some(Box::new(property.map(|l| !l.is_empty())))
297 }
298 CommitTemplatePropertyKind::CommitOrChangeId(_) => None,
299 CommitTemplatePropertyKind::ShortestIdPrefix(_) => None,
300 }
301 }
302
303 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'repo>> {
304 match self {
305 CommitTemplatePropertyKind::Core(property) => property.try_into_integer(),
306 _ => None,
307 }
308 }
309
310 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'repo>> {
311 match self {
312 CommitTemplatePropertyKind::Core(property) => property.try_into_plain_text(),
313 _ => {
314 let template = self.try_into_template()?;
315 Some(Box::new(PlainTextFormattedProperty::new(template)))
316 }
317 }
318 }
319
320 fn try_into_template(self) -> Option<Box<dyn Template + 'repo>> {
321 match self {
322 CommitTemplatePropertyKind::Core(property) => property.try_into_template(),
323 CommitTemplatePropertyKind::Commit(_) => None,
324 CommitTemplatePropertyKind::CommitOpt(_) => None,
325 CommitTemplatePropertyKind::CommitList(_) => None,
326 CommitTemplatePropertyKind::RefName(property) => Some(property.into_template()),
327 CommitTemplatePropertyKind::RefNameOpt(property) => Some(property.into_template()),
328 CommitTemplatePropertyKind::RefNameList(property) => Some(property.into_template()),
329 CommitTemplatePropertyKind::CommitOrChangeId(property) => {
330 Some(property.into_template())
331 }
332 CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
333 Some(property.into_template())
334 }
335 }
336 }
337}
338
339/// Table of functions that translate method call node of self type `T`.
340pub type CommitTemplateBuildMethodFnMap<'repo, T> =
341 TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>;
342
343/// Symbol table of methods available in the commit template.
344pub struct CommitTemplateBuildFnTable<'repo> {
345 pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>,
346 pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>,
347 pub ref_name_methods: CommitTemplateBuildMethodFnMap<'repo, RefName>,
348 pub commit_or_change_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId>,
349 pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>,
350}
351
352impl<'repo> CommitTemplateBuildFnTable<'repo> {
353 /// Creates new symbol table containing the builtin methods.
354 fn builtin() -> Self {
355 CommitTemplateBuildFnTable {
356 core: CoreTemplateBuildFnTable::builtin(),
357 commit_methods: builtin_commit_methods(),
358 ref_name_methods: builtin_ref_name_methods(),
359 commit_or_change_id_methods: builtin_commit_or_change_id_methods(),
360 shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(),
361 }
362 }
363
364 pub fn empty() -> Self {
365 CommitTemplateBuildFnTable {
366 core: CoreTemplateBuildFnTable::empty(),
367 commit_methods: HashMap::new(),
368 ref_name_methods: HashMap::new(),
369 commit_or_change_id_methods: HashMap::new(),
370 shortest_id_prefix_methods: HashMap::new(),
371 }
372 }
373
374 fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) {
375 let CommitTemplateBuildFnTable {
376 core,
377 commit_methods,
378 ref_name_methods,
379 commit_or_change_id_methods,
380 shortest_id_prefix_methods,
381 } = extension;
382
383 self.core.merge(core);
384 merge_fn_map(&mut self.commit_methods, commit_methods);
385 merge_fn_map(&mut self.ref_name_methods, ref_name_methods);
386 merge_fn_map(
387 &mut self.commit_or_change_id_methods,
388 commit_or_change_id_methods,
389 );
390 merge_fn_map(
391 &mut self.shortest_id_prefix_methods,
392 shortest_id_prefix_methods,
393 );
394 }
395}
396
397#[derive(Default)]
398pub struct CommitKeywordCache<'repo> {
399 // Build index lazily, and Rc to get away from &self lifetime.
400 branches_index: OnceCell<Rc<RefNamesIndex>>,
401 tags_index: OnceCell<Rc<RefNamesIndex>>,
402 git_refs_index: OnceCell<Rc<RefNamesIndex>>,
403 is_immutable_fn: OnceCell<Rc<RevsetContainingFn<'repo>>>,
404}
405
406impl<'repo> CommitKeywordCache<'repo> {
407 pub fn branches_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> {
408 self.branches_index
409 .get_or_init(|| Rc::new(build_branches_index(repo)))
410 }
411
412 pub fn tags_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> {
413 self.tags_index
414 .get_or_init(|| Rc::new(build_ref_names_index(repo.view().tags())))
415 }
416
417 pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> {
418 self.git_refs_index
419 .get_or_init(|| Rc::new(build_ref_names_index(repo.view().git_refs())))
420 }
421
422 pub fn is_immutable_fn(
423 &self,
424 language: &CommitTemplateLanguage<'repo>,
425 span: pest::Span<'_>,
426 ) -> TemplateParseResult<&Rc<RevsetContainingFn<'repo>>> {
427 self.is_immutable_fn.get_or_try_init(|| {
428 let revset = evaluate_immutable_revset(language, span)?;
429 Ok(revset.containing_fn().into())
430 })
431 }
432}
433
434fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> {
435 type L<'repo> = CommitTemplateLanguage<'repo>;
436 // Not using maplit::hashmap!{} or custom declarative macro here because
437 // code completion inside macro is quite restricted.
438 let mut map = CommitTemplateBuildMethodFnMap::<Commit>::new();
439 map.insert(
440 "description",
441 |_language, _build_ctx, self_property, function| {
442 template_parser::expect_no_arguments(function)?;
443 let out_property =
444 self_property.map(|commit| text_util::complete_newline(commit.description()));
445 Ok(L::wrap_string(out_property))
446 },
447 );
448 map.insert(
449 "change_id",
450 |_language, _build_ctx, self_property, function| {
451 template_parser::expect_no_arguments(function)?;
452 let out_property =
453 self_property.map(|commit| CommitOrChangeId::Change(commit.change_id().to_owned()));
454 Ok(L::wrap_commit_or_change_id(out_property))
455 },
456 );
457 map.insert(
458 "commit_id",
459 |_language, _build_ctx, self_property, function| {
460 template_parser::expect_no_arguments(function)?;
461 let out_property =
462 self_property.map(|commit| CommitOrChangeId::Commit(commit.id().to_owned()));
463 Ok(L::wrap_commit_or_change_id(out_property))
464 },
465 );
466 map.insert(
467 "parents",
468 |_language, _build_ctx, self_property, function| {
469 template_parser::expect_no_arguments(function)?;
470 let out_property = self_property.map(|commit| commit.parents());
471 Ok(L::wrap_commit_list(out_property))
472 },
473 );
474 map.insert(
475 "author",
476 |_language, _build_ctx, self_property, function| {
477 template_parser::expect_no_arguments(function)?;
478 let out_property = self_property.map(|commit| commit.author().clone());
479 Ok(L::wrap_signature(out_property))
480 },
481 );
482 map.insert(
483 "committer",
484 |_language, _build_ctx, self_property, function| {
485 template_parser::expect_no_arguments(function)?;
486 let out_property = self_property.map(|commit| commit.committer().clone());
487 Ok(L::wrap_signature(out_property))
488 },
489 );
490 map.insert("mine", |language, _build_ctx, self_property, function| {
491 template_parser::expect_no_arguments(function)?;
492 let user_email = language.revset_parse_context.user_email.clone();
493 let out_property = self_property.map(move |commit| commit.author().email == user_email);
494 Ok(L::wrap_boolean(out_property))
495 });
496 map.insert(
497 "working_copies",
498 |language, _build_ctx, self_property, function| {
499 template_parser::expect_no_arguments(function)?;
500 let repo = language.repo;
501 let out_property = self_property.map(|commit| extract_working_copies(repo, &commit));
502 Ok(L::wrap_string(out_property))
503 },
504 );
505 map.insert(
506 "current_working_copy",
507 |language, _build_ctx, self_property, function| {
508 template_parser::expect_no_arguments(function)?;
509 let repo = language.repo;
510 let workspace_id = language.workspace_id.clone();
511 let out_property = self_property.map(move |commit| {
512 Some(commit.id()) == repo.view().get_wc_commit_id(&workspace_id)
513 });
514 Ok(L::wrap_boolean(out_property))
515 },
516 );
517 map.insert(
518 "branches",
519 |language, _build_ctx, self_property, function| {
520 template_parser::expect_no_arguments(function)?;
521 let index = language.keyword_cache.branches_index(language.repo).clone();
522 let out_property = self_property.map(move |commit| {
523 index
524 .get(commit.id())
525 .iter()
526 .filter(|ref_name| ref_name.is_local() || !ref_name.synced)
527 .cloned()
528 .collect()
529 });
530 Ok(L::wrap_ref_name_list(out_property))
531 },
532 );
533 map.insert(
534 "local_branches",
535 |language, _build_ctx, self_property, function| {
536 template_parser::expect_no_arguments(function)?;
537 let index = language.keyword_cache.branches_index(language.repo).clone();
538 let out_property = self_property.map(move |commit| {
539 index
540 .get(commit.id())
541 .iter()
542 .filter(|ref_name| ref_name.is_local())
543 .cloned()
544 .collect()
545 });
546 Ok(L::wrap_ref_name_list(out_property))
547 },
548 );
549 map.insert(
550 "remote_branches",
551 |language, _build_ctx, self_property, function| {
552 template_parser::expect_no_arguments(function)?;
553 let index = language.keyword_cache.branches_index(language.repo).clone();
554 let out_property = self_property.map(move |commit| {
555 index
556 .get(commit.id())
557 .iter()
558 .filter(|ref_name| ref_name.is_remote())
559 .cloned()
560 .collect()
561 });
562 Ok(L::wrap_ref_name_list(out_property))
563 },
564 );
565 map.insert("tags", |language, _build_ctx, self_property, function| {
566 template_parser::expect_no_arguments(function)?;
567 let index = language.keyword_cache.tags_index(language.repo).clone();
568 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec());
569 Ok(L::wrap_ref_name_list(out_property))
570 });
571 map.insert(
572 "git_refs",
573 |language, _build_ctx, self_property, function| {
574 template_parser::expect_no_arguments(function)?;
575 let index = language.keyword_cache.git_refs_index(language.repo).clone();
576 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec());
577 Ok(L::wrap_ref_name_list(out_property))
578 },
579 );
580 map.insert(
581 "git_head",
582 |language, _build_ctx, self_property, function| {
583 template_parser::expect_no_arguments(function)?;
584 let repo = language.repo;
585 let out_property = self_property.map(|commit| extract_git_head(repo, &commit));
586 Ok(L::wrap_ref_name_opt(out_property))
587 },
588 );
589 map.insert(
590 "divergent",
591 |language, _build_ctx, self_property, function| {
592 template_parser::expect_no_arguments(function)?;
593 let repo = language.repo;
594 let out_property = self_property.map(|commit| {
595 // The given commit could be hidden in e.g. obslog.
596 let maybe_entries = repo.resolve_change_id(commit.change_id());
597 maybe_entries.map_or(0, |entries| entries.len()) > 1
598 });
599 Ok(L::wrap_boolean(out_property))
600 },
601 );
602 map.insert("hidden", |language, _build_ctx, self_property, function| {
603 template_parser::expect_no_arguments(function)?;
604 let repo = language.repo;
605 let out_property = self_property.map(|commit| {
606 let maybe_entries = repo.resolve_change_id(commit.change_id());
607 maybe_entries.map_or(true, |entries| !entries.contains(commit.id()))
608 });
609 Ok(L::wrap_boolean(out_property))
610 });
611 map.insert(
612 "immutable",
613 |language, _build_ctx, self_property, function| {
614 template_parser::expect_no_arguments(function)?;
615 let is_immutable = language
616 .keyword_cache
617 .is_immutable_fn(language, function.name_span)?
618 .clone();
619 let out_property = self_property.map(move |commit| is_immutable(commit.id()));
620 Ok(L::wrap_boolean(out_property))
621 },
622 );
623 map.insert(
624 "contained_in",
625 |language, _build_ctx, self_property, function| {
626 let [revset_node] = template_parser::expect_exact_arguments(function)?;
627
628 let is_contained =
629 template_parser::expect_string_literal_with(revset_node, |revset, span| {
630 Ok(evaluate_user_revset(language, span, revset)?.containing_fn())
631 })?;
632
633 let out_property = self_property.map(move |commit| is_contained(commit.id()));
634 Ok(L::wrap_boolean(out_property))
635 },
636 );
637 map.insert(
638 "conflict",
639 |_language, _build_ctx, self_property, function| {
640 template_parser::expect_no_arguments(function)?;
641 let out_property = self_property.and_then(|commit| Ok(commit.has_conflict()?));
642 Ok(L::wrap_boolean(out_property))
643 },
644 );
645 map.insert("empty", |language, _build_ctx, self_property, function| {
646 template_parser::expect_no_arguments(function)?;
647 let repo = language.repo;
648 let out_property = self_property.and_then(|commit| {
649 if let [parent] = &commit.parents()[..] {
650 return Ok(parent.tree_id() == commit.tree_id());
651 }
652 let parent_tree = rewrite::merge_commit_trees(repo, &commit.parents())?;
653 Ok(*commit.tree_id() == parent_tree.id())
654 });
655 Ok(L::wrap_boolean(out_property))
656 });
657 map.insert("root", |language, _build_ctx, self_property, function| {
658 template_parser::expect_no_arguments(function)?;
659 let repo = language.repo;
660 let out_property = self_property.map(|commit| commit.id() == repo.store().root_commit_id());
661 Ok(L::wrap_boolean(out_property))
662 });
663 map
664}
665
666// TODO: return Vec<String>
667fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String {
668 let wc_commit_ids = repo.view().wc_commit_ids();
669 if wc_commit_ids.len() <= 1 {
670 return "".to_string();
671 }
672 let mut names = vec![];
673 for (workspace_id, wc_commit_id) in wc_commit_ids.iter().sorted() {
674 if wc_commit_id == commit.id() {
675 names.push(format!("{}@", workspace_id.as_str()));
676 }
677 }
678 names.join(" ")
679}
680
681type RevsetContainingFn<'repo> = dyn Fn(&CommitId) -> bool + 'repo;
682
683fn evaluate_revset_expression<'repo>(
684 language: &CommitTemplateLanguage<'repo>,
685 span: pest::Span<'_>,
686 expression: Rc<RevsetExpression>,
687) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
688 let symbol_resolver = revset_util::default_symbol_resolver(
689 language.repo,
690 language.revset_parse_context.extensions.symbol_resolvers(),
691 language.id_prefix_context,
692 );
693 let revset =
694 revset_util::evaluate(language.repo, &symbol_resolver, expression).map_err(|err| {
695 TemplateParseError::expression("Failed to evaluate revset", span).with_source(err)
696 })?;
697 Ok(revset)
698}
699
700fn evaluate_immutable_revset<'repo>(
701 language: &CommitTemplateLanguage<'repo>,
702 span: pest::Span<'_>,
703) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
704 // Alternatively, a negated (i.e. visible mutable) set could be computed.
705 // It's usually smaller than the immutable set. The revset engine can also
706 // optimize "::<recent_heads>" query to use bitset-based implementation.
707 let expression = revset_util::parse_immutable_expression(&language.revset_parse_context)
708 .map_err(|err| {
709 TemplateParseError::expression("Failed to parse revset", span).with_source(err)
710 })?;
711
712 evaluate_revset_expression(language, span, expression)
713}
714
715fn evaluate_user_revset<'repo>(
716 language: &CommitTemplateLanguage<'repo>,
717 span: pest::Span<'_>,
718 revset: &str,
719) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> {
720 let expression = revset::parse(revset, &language.revset_parse_context).map_err(|err| {
721 TemplateParseError::expression("Failed to parse revset", span).with_source(err)
722 })?;
723
724 evaluate_revset_expression(language, span, expression)
725}
726
727/// Branch or tag name with metadata.
728#[derive(Clone, Debug)]
729pub struct RefName {
730 /// Local name.
731 name: String,
732 /// Remote name if this is a remote or Git-tracking ref.
733 remote: Option<String>,
734 /// Target commit ids.
735 target: RefTarget,
736 /// Local ref is synchronized with all tracking remotes, or tracking remote
737 /// ref is synchronized with the local.
738 synced: bool,
739}
740
741impl RefName {
742 /// Creates local ref representation which doesn't track any remote refs.
743 pub fn local_only(name: impl Into<String>, target: RefTarget) -> Self {
744 RefName {
745 name: name.into(),
746 remote: None,
747 target,
748 synced: true, // has no tracking remotes
749 }
750 }
751
752 fn is_local(&self) -> bool {
753 self.remote.is_none()
754 }
755
756 fn is_remote(&self) -> bool {
757 self.remote.is_some()
758 }
759
760 fn is_present(&self) -> bool {
761 self.target.is_present()
762 }
763
764 /// Whether the ref target has conflicts.
765 fn has_conflict(&self) -> bool {
766 self.target.has_conflict()
767 }
768}
769
770impl Template for RefName {
771 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
772 write!(formatter.labeled("name"), "{}", self.name)?;
773 if let Some(remote) = &self.remote {
774 write!(formatter, "@")?;
775 write!(formatter.labeled("remote"), "{remote}")?;
776 }
777 // Don't show both conflict and unsynced sigils as conflicted ref wouldn't
778 // be pushed.
779 if self.has_conflict() {
780 write!(formatter, "??")?;
781 } else if self.is_local() && !self.synced {
782 write!(formatter, "*")?;
783 }
784 Ok(())
785 }
786}
787
788impl Template for Vec<RefName> {
789 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
790 templater::format_joined(formatter, self, " ")
791 }
792}
793
794fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RefName> {
795 type L<'repo> = CommitTemplateLanguage<'repo>;
796 // Not using maplit::hashmap!{} or custom declarative macro here because
797 // code completion inside macro is quite restricted.
798 let mut map = CommitTemplateBuildMethodFnMap::<RefName>::new();
799 map.insert("name", |_language, _build_ctx, self_property, function| {
800 template_parser::expect_no_arguments(function)?;
801 let out_property = self_property.map(|ref_name| ref_name.name);
802 Ok(L::wrap_string(out_property))
803 });
804 map.insert(
805 "remote",
806 |_language, _build_ctx, self_property, function| {
807 template_parser::expect_no_arguments(function)?;
808 let out_property = self_property.map(|ref_name| ref_name.remote.unwrap_or_default());
809 Ok(L::wrap_string(out_property))
810 },
811 );
812 map.insert(
813 "present",
814 |_language, _build_ctx, self_property, function| {
815 template_parser::expect_no_arguments(function)?;
816 let out_property = self_property.map(|ref_name| ref_name.is_present());
817 Ok(L::wrap_boolean(out_property))
818 },
819 );
820 map.insert(
821 "conflict",
822 |_language, _build_ctx, self_property, function| {
823 template_parser::expect_no_arguments(function)?;
824 let out_property = self_property.map(|ref_name| ref_name.has_conflict());
825 Ok(L::wrap_boolean(out_property))
826 },
827 );
828 map.insert(
829 "normal_target",
830 |language, _build_ctx, self_property, function| {
831 template_parser::expect_no_arguments(function)?;
832 let repo = language.repo;
833 let out_property = self_property.and_then(|ref_name| {
834 let maybe_id = ref_name.target.as_normal();
835 Ok(maybe_id.map(|id| repo.store().get_commit(id)).transpose()?)
836 });
837 Ok(L::wrap_commit_opt(out_property))
838 },
839 );
840 map.insert(
841 "removed_targets",
842 |language, _build_ctx, self_property, function| {
843 template_parser::expect_no_arguments(function)?;
844 let repo = language.repo;
845 let out_property = self_property.and_then(|ref_name| {
846 let ids = ref_name.target.removed_ids();
847 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?)
848 });
849 Ok(L::wrap_commit_list(out_property))
850 },
851 );
852 map.insert(
853 "added_targets",
854 |language, _build_ctx, self_property, function| {
855 template_parser::expect_no_arguments(function)?;
856 let repo = language.repo;
857 let out_property = self_property.and_then(|ref_name| {
858 let ids = ref_name.target.added_ids();
859 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?)
860 });
861 Ok(L::wrap_commit_list(out_property))
862 },
863 );
864 map
865}
866
867/// Cache for reverse lookup refs.
868#[derive(Clone, Debug, Default)]
869pub struct RefNamesIndex {
870 index: HashMap<CommitId, Vec<RefName>>,
871}
872
873impl RefNamesIndex {
874 fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: RefName) {
875 for id in ids {
876 let ref_names = self.index.entry(id.clone()).or_default();
877 ref_names.push(name.clone());
878 }
879 }
880
881 #[allow(unknown_lints)] // XXX FIXME (aseipp): nightly bogons; re-test this occasionally
882 #[allow(clippy::manual_unwrap_or_default)]
883 pub fn get(&self, id: &CommitId) -> &[RefName] {
884 if let Some(names) = self.index.get(id) {
885 names
886 } else {
887 &[]
888 }
889 }
890}
891
892fn build_branches_index(repo: &dyn Repo) -> RefNamesIndex {
893 let mut index = RefNamesIndex::default();
894 for (branch_name, branch_target) in repo.view().branches() {
895 let local_target = branch_target.local_target;
896 let remote_refs = branch_target.remote_refs;
897 if local_target.is_present() {
898 let ref_name = RefName {
899 name: branch_name.to_owned(),
900 remote: None,
901 target: local_target.clone(),
902 synced: remote_refs.iter().all(|&(_, remote_ref)| {
903 !remote_ref.is_tracking() || remote_ref.target == *local_target
904 }),
905 };
906 index.insert(local_target.added_ids(), ref_name);
907 }
908 for &(remote_name, remote_ref) in &remote_refs {
909 let ref_name = RefName {
910 name: branch_name.to_owned(),
911 remote: Some(remote_name.to_owned()),
912 target: remote_ref.target.clone(),
913 synced: remote_ref.is_tracking() && remote_ref.target == *local_target,
914 };
915 index.insert(remote_ref.target.added_ids(), ref_name);
916 }
917 }
918 index
919}
920
921fn build_ref_names_index<'a>(
922 ref_pairs: impl IntoIterator<Item = (&'a String, &'a RefTarget)>,
923) -> RefNamesIndex {
924 let mut index = RefNamesIndex::default();
925 for (name, target) in ref_pairs {
926 let ref_name = RefName::local_only(name, target.clone());
927 index.insert(target.added_ids(), ref_name);
928 }
929 index
930}
931
932fn extract_git_head(repo: &dyn Repo, commit: &Commit) -> Option<RefName> {
933 let target = repo.view().git_head();
934 target.added_ids().contains(commit.id()).then(|| {
935 RefName {
936 name: "HEAD".to_owned(),
937 remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO.to_owned()),
938 target: target.clone(),
939 synced: false, // has no local counterpart
940 }
941 })
942}
943
944#[derive(Clone, Debug, Eq, PartialEq)]
945pub enum CommitOrChangeId {
946 Commit(CommitId),
947 Change(ChangeId),
948}
949
950impl CommitOrChangeId {
951 pub fn hex(&self) -> String {
952 match self {
953 CommitOrChangeId::Commit(id) => id.hex(),
954 CommitOrChangeId::Change(id) => {
955 // TODO: We can avoid the unwrap() and make this more efficient by converting
956 // straight from bytes.
957 to_reverse_hex(&id.hex()).unwrap()
958 }
959 }
960 }
961
962 pub fn short(&self, total_len: usize) -> String {
963 let mut hex = self.hex();
964 hex.truncate(total_len);
965 hex
966 }
967
968 /// The length of the id printed will be the maximum of `total_len` and the
969 /// length of the shortest unique prefix
970 pub fn shortest(
971 &self,
972 repo: &dyn Repo,
973 id_prefix_context: &IdPrefixContext,
974 total_len: usize,
975 ) -> ShortestIdPrefix {
976 let mut hex = self.hex();
977 let prefix_len = match self {
978 CommitOrChangeId::Commit(id) => id_prefix_context.shortest_commit_prefix_len(repo, id),
979 CommitOrChangeId::Change(id) => id_prefix_context.shortest_change_prefix_len(repo, id),
980 };
981 hex.truncate(max(prefix_len, total_len));
982 let rest = hex.split_off(prefix_len);
983 ShortestIdPrefix { prefix: hex, rest }
984 }
985}
986
987impl Template for CommitOrChangeId {
988 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
989 write!(formatter, "{}", self.hex())
990 }
991}
992
993fn builtin_commit_or_change_id_methods<'repo>(
994) -> CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId> {
995 type L<'repo> = CommitTemplateLanguage<'repo>;
996 // Not using maplit::hashmap!{} or custom declarative macro here because
997 // code completion inside macro is quite restricted.
998 let mut map = CommitTemplateBuildMethodFnMap::<CommitOrChangeId>::new();
999 map.insert("short", |language, build_ctx, self_property, function| {
1000 let ([], [len_node]) = template_parser::expect_arguments(function)?;
1001 let len_property = len_node
1002 .map(|node| template_builder::expect_usize_expression(language, build_ctx, node))
1003 .transpose()?;
1004 let out_property =
1005 (self_property, len_property).map(|(id, len)| id.short(len.unwrap_or(12)));
1006 Ok(L::wrap_string(out_property))
1007 });
1008 map.insert(
1009 "shortest",
1010 |language, build_ctx, self_property, function| {
1011 let id_prefix_context = &language.id_prefix_context;
1012 let ([], [len_node]) = template_parser::expect_arguments(function)?;
1013 let len_property = len_node
1014 .map(|node| template_builder::expect_usize_expression(language, build_ctx, node))
1015 .transpose()?;
1016 let out_property = (self_property, len_property)
1017 .map(|(id, len)| id.shortest(language.repo, id_prefix_context, len.unwrap_or(0)));
1018 Ok(L::wrap_shortest_id_prefix(out_property))
1019 },
1020 );
1021 map
1022}
1023
1024pub struct ShortestIdPrefix {
1025 pub prefix: String,
1026 pub rest: String,
1027}
1028
1029impl Template for ShortestIdPrefix {
1030 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
1031 write!(formatter.labeled("prefix"), "{}", self.prefix)?;
1032 write!(formatter.labeled("rest"), "{}", self.rest)?;
1033 Ok(())
1034 }
1035}
1036
1037impl ShortestIdPrefix {
1038 fn to_upper(&self) -> Self {
1039 Self {
1040 prefix: self.prefix.to_ascii_uppercase(),
1041 rest: self.rest.to_ascii_uppercase(),
1042 }
1043 }
1044 fn to_lower(&self) -> Self {
1045 Self {
1046 prefix: self.prefix.to_ascii_lowercase(),
1047 rest: self.rest.to_ascii_lowercase(),
1048 }
1049 }
1050}
1051
1052fn builtin_shortest_id_prefix_methods<'repo>(
1053) -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> {
1054 type L<'repo> = CommitTemplateLanguage<'repo>;
1055 // Not using maplit::hashmap!{} or custom declarative macro here because
1056 // code completion inside macro is quite restricted.
1057 let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new();
1058 map.insert(
1059 "prefix",
1060 |_language, _build_ctx, self_property, function| {
1061 template_parser::expect_no_arguments(function)?;
1062 let out_property = self_property.map(|id| id.prefix);
1063 Ok(L::wrap_string(out_property))
1064 },
1065 );
1066 map.insert("rest", |_language, _build_ctx, self_property, function| {
1067 template_parser::expect_no_arguments(function)?;
1068 let out_property = self_property.map(|id| id.rest);
1069 Ok(L::wrap_string(out_property))
1070 });
1071 map.insert("upper", |_language, _build_ctx, self_property, function| {
1072 template_parser::expect_no_arguments(function)?;
1073 let out_property = self_property.map(|id| id.to_upper());
1074 Ok(L::wrap_shortest_id_prefix(out_property))
1075 });
1076 map.insert("lower", |_language, _build_ctx, self_property, function| {
1077 template_parser::expect_no_arguments(function)?;
1078 let out_property = self_property.map(|id| id.to_lower());
1079 Ok(L::wrap_shortest_id_prefix(out_property))
1080 });
1081 map
1082}