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::cmp::max;
16use std::io;
17
18use itertools::Itertools as _;
19use jujutsu_lib::backend::{ChangeId, CommitId, ObjectId as _};
20use jujutsu_lib::commit::Commit;
21use jujutsu_lib::hex_util::to_reverse_hex;
22use jujutsu_lib::op_store::WorkspaceId;
23use jujutsu_lib::repo::Repo;
24use jujutsu_lib::rewrite;
25
26use crate::formatter::Formatter;
27use crate::template_builder::{
28 self, BuildContext, CoreTemplatePropertyKind, IntoTemplateProperty, TemplateLanguage,
29};
30use crate::template_parser::{
31 self, FunctionCallNode, TemplateAliasesMap, TemplateParseError, TemplateParseResult,
32};
33use crate::templater::{
34 IntoTemplate, PlainTextFormattedProperty, Template, TemplateFunction, TemplateProperty,
35 TemplatePropertyFn,
36};
37use crate::text_util;
38
39struct CommitTemplateLanguage<'repo, 'b> {
40 repo: &'repo dyn Repo,
41 workspace_id: &'b WorkspaceId,
42}
43
44impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo, '_> {
45 type Context = Commit;
46 type Property = CommitTemplatePropertyKind<'repo>;
47
48 template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core);
49
50 fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult<Self::Property> {
51 build_commit_keyword(self, name, span)
52 }
53
54 fn build_method(
55 &self,
56 build_ctx: &BuildContext<Self::Property>,
57 property: Self::Property,
58 function: &FunctionCallNode,
59 ) -> TemplateParseResult<Self::Property> {
60 match property {
61 CommitTemplatePropertyKind::Core(property) => {
62 template_builder::build_core_method(self, build_ctx, property, function)
63 }
64 CommitTemplatePropertyKind::Commit(property) => {
65 build_commit_method(self, build_ctx, property, function)
66 }
67 CommitTemplatePropertyKind::CommitList(property) => {
68 template_builder::build_unformattable_list_method(
69 self,
70 build_ctx,
71 property,
72 function,
73 |item| self.wrap_commit(item),
74 )
75 }
76 CommitTemplatePropertyKind::CommitOrChangeId(property) => {
77 build_commit_or_change_id_method(self, build_ctx, property, function)
78 }
79 CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
80 build_shortest_id_prefix_method(self, build_ctx, property, function)
81 }
82 }
83 }
84}
85
86// If we need to add multiple languages that support Commit types, this can be
87// turned into a trait which extends TemplateLanguage.
88impl<'repo> CommitTemplateLanguage<'repo, '_> {
89 fn wrap_commit(
90 &self,
91 property: impl TemplateProperty<Commit, Output = Commit> + 'repo,
92 ) -> CommitTemplatePropertyKind<'repo> {
93 CommitTemplatePropertyKind::Commit(Box::new(property))
94 }
95
96 fn wrap_commit_list(
97 &self,
98 property: impl TemplateProperty<Commit, Output = Vec<Commit>> + 'repo,
99 ) -> CommitTemplatePropertyKind<'repo> {
100 CommitTemplatePropertyKind::CommitList(Box::new(property))
101 }
102
103 fn wrap_commit_or_change_id(
104 &self,
105 property: impl TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo,
106 ) -> CommitTemplatePropertyKind<'repo> {
107 CommitTemplatePropertyKind::CommitOrChangeId(Box::new(property))
108 }
109
110 fn wrap_shortest_id_prefix(
111 &self,
112 property: impl TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo,
113 ) -> CommitTemplatePropertyKind<'repo> {
114 CommitTemplatePropertyKind::ShortestIdPrefix(Box::new(property))
115 }
116}
117
118enum CommitTemplatePropertyKind<'repo> {
119 Core(CoreTemplatePropertyKind<'repo, Commit>),
120 Commit(Box<dyn TemplateProperty<Commit, Output = Commit> + 'repo>),
121 CommitList(Box<dyn TemplateProperty<Commit, Output = Vec<Commit>> + 'repo>),
122 CommitOrChangeId(Box<dyn TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo>),
123 ShortestIdPrefix(Box<dyn TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo>),
124}
125
126impl<'repo> IntoTemplateProperty<'repo, Commit> for CommitTemplatePropertyKind<'repo> {
127 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Commit, Output = bool> + 'repo>> {
128 match self {
129 CommitTemplatePropertyKind::Core(property) => property.try_into_boolean(),
130 // TODO: should we allow implicit cast of List type?
131 _ => None,
132 }
133 }
134
135 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Commit, Output = i64> + 'repo>> {
136 match self {
137 CommitTemplatePropertyKind::Core(property) => property.try_into_integer(),
138 _ => None,
139 }
140 }
141
142 fn try_into_plain_text(
143 self,
144 ) -> Option<Box<dyn TemplateProperty<Commit, Output = String> + 'repo>> {
145 match self {
146 CommitTemplatePropertyKind::Core(property) => property.try_into_plain_text(),
147 _ => {
148 let template = self.try_into_template()?;
149 Some(Box::new(PlainTextFormattedProperty::new(template)))
150 }
151 }
152 }
153
154 fn try_into_template(self) -> Option<Box<dyn Template<Commit> + 'repo>> {
155 match self {
156 CommitTemplatePropertyKind::Core(property) => property.try_into_template(),
157 CommitTemplatePropertyKind::Commit(_) => None,
158 CommitTemplatePropertyKind::CommitList(_) => None,
159 CommitTemplatePropertyKind::CommitOrChangeId(property) => {
160 Some(property.into_template())
161 }
162 CommitTemplatePropertyKind::ShortestIdPrefix(property) => {
163 Some(property.into_template())
164 }
165 }
166 }
167}
168
169fn build_commit_keyword<'repo>(
170 language: &CommitTemplateLanguage<'repo, '_>,
171 name: &str,
172 span: pest::Span,
173) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> {
174 // Commit object is lightweight (a few Arc + CommitId), so just clone it
175 // to turn into a property type. Abstraction over "for<'a> (&'a T) -> &'a T"
176 // and "(&T) -> T" wouldn't be simple. If we want to remove Clone/Rc/Arc,
177 // maybe we can add an abstraction that takes "Fn(&Commit) -> O" and returns
178 // "TemplateProperty<Commit, Output = O>".
179 let property = TemplatePropertyFn(|commit: &Commit| commit.clone());
180 build_commit_keyword_opt(language, property, name)
181 .ok_or_else(|| TemplateParseError::no_such_keyword(name, span))
182}
183
184fn build_commit_method<'repo>(
185 language: &CommitTemplateLanguage<'repo, '_>,
186 _build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>,
187 self_property: impl TemplateProperty<Commit, Output = Commit> + 'repo,
188 function: &FunctionCallNode,
189) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> {
190 if let Some(property) = build_commit_keyword_opt(language, self_property, function.name) {
191 template_parser::expect_no_arguments(function)?;
192 Ok(property)
193 } else {
194 Err(TemplateParseError::no_such_method("Commit", function))
195 }
196}
197
198fn build_commit_keyword_opt<'repo>(
199 language: &CommitTemplateLanguage<'repo, '_>,
200 property: impl TemplateProperty<Commit, Output = Commit> + 'repo,
201 name: &str,
202) -> Option<CommitTemplatePropertyKind<'repo>> {
203 fn wrap_fn<'repo, O>(
204 property: impl TemplateProperty<Commit, Output = Commit> + 'repo,
205 f: impl Fn(&Commit) -> O + 'repo,
206 ) -> impl TemplateProperty<Commit, Output = O> + 'repo {
207 TemplateFunction::new(property, move |commit| f(&commit))
208 }
209 fn wrap_repo_fn<'repo, O>(
210 repo: &'repo dyn Repo,
211 property: impl TemplateProperty<Commit, Output = Commit> + 'repo,
212 f: impl Fn(&dyn Repo, &Commit) -> O + 'repo,
213 ) -> impl TemplateProperty<Commit, Output = O> + 'repo {
214 TemplateFunction::new(property, move |commit| f(repo, &commit))
215 }
216
217 let repo = language.repo;
218 let property = match name {
219 "description" => language.wrap_string(wrap_fn(property, |commit| {
220 text_util::complete_newline(commit.description())
221 })),
222 "change_id" => language.wrap_commit_or_change_id(wrap_fn(property, |commit| {
223 CommitOrChangeId::Change(commit.change_id().to_owned())
224 })),
225 "commit_id" => language.wrap_commit_or_change_id(wrap_fn(property, |commit| {
226 CommitOrChangeId::Commit(commit.id().to_owned())
227 })),
228 "parents" => language.wrap_commit_list(wrap_fn(property, |commit| commit.parents())),
229 "author" => language.wrap_signature(wrap_fn(property, |commit| commit.author().clone())),
230 "committer" => {
231 language.wrap_signature(wrap_fn(property, |commit| commit.committer().clone()))
232 }
233 "working_copies" => {
234 language.wrap_string(wrap_repo_fn(repo, property, extract_working_copies))
235 }
236 "current_working_copy" => {
237 let workspace_id = language.workspace_id.clone();
238 language.wrap_boolean(wrap_fn(property, move |commit| {
239 Some(commit.id()) == repo.view().get_wc_commit_id(&workspace_id)
240 }))
241 }
242 "branches" => language.wrap_string(wrap_repo_fn(repo, property, extract_branches)),
243 "tags" => language.wrap_string(wrap_repo_fn(repo, property, extract_tags)),
244 "git_refs" => language.wrap_string(wrap_repo_fn(repo, property, extract_git_refs)),
245 "git_head" => language.wrap_string(wrap_repo_fn(repo, property, extract_git_head)),
246 "divergent" => language.wrap_boolean(wrap_fn(property, |commit| {
247 // The given commit could be hidden in e.g. obslog.
248 let maybe_entries = repo.resolve_change_id(commit.change_id());
249 maybe_entries.map_or(0, |entries| entries.len()) > 1
250 })),
251 "hidden" => language.wrap_boolean(wrap_fn(property, |commit| {
252 let maybe_entries = repo.resolve_change_id(commit.change_id());
253 maybe_entries.map_or(true, |entries| !entries.contains(commit.id()))
254 })),
255 "conflict" => {
256 language.wrap_boolean(wrap_fn(property, |commit| commit.tree().has_conflict()))
257 }
258 "empty" => language.wrap_boolean(wrap_fn(property, |commit| {
259 commit.tree().id() == rewrite::merge_commit_trees(repo, &commit.parents()).id()
260 })),
261 _ => return None,
262 };
263 Some(property)
264}
265
266// TODO: return Vec<String>
267fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String {
268 let wc_commit_ids = repo.view().wc_commit_ids();
269 if wc_commit_ids.len() <= 1 {
270 return "".to_string();
271 }
272 let mut names = vec![];
273 for (workspace_id, wc_commit_id) in wc_commit_ids.iter().sorted() {
274 if wc_commit_id == commit.id() {
275 names.push(format!("{}@", workspace_id.as_str()));
276 }
277 }
278 names.join(" ")
279}
280
281// TODO: return Vec<Branch>?
282fn extract_branches(repo: &dyn Repo, commit: &Commit) -> String {
283 let mut names = vec![];
284 for (branch_name, branch_target) in repo.view().branches() {
285 let local_target = branch_target.local_target.as_ref();
286 if let Some(local_target) = local_target {
287 if local_target.has_add(commit.id()) {
288 if local_target.is_conflict() {
289 names.push(format!("{branch_name}??"));
290 } else if branch_target
291 .remote_targets
292 .values()
293 .any(|remote_target| remote_target != local_target)
294 {
295 names.push(format!("{branch_name}*"));
296 } else {
297 names.push(branch_name.clone());
298 }
299 }
300 }
301 for (remote_name, remote_target) in &branch_target.remote_targets {
302 if Some(remote_target) != local_target && remote_target.has_add(commit.id()) {
303 if remote_target.is_conflict() {
304 names.push(format!("{branch_name}@{remote_name}?"));
305 } else {
306 names.push(format!("{branch_name}@{remote_name}"));
307 }
308 }
309 }
310 }
311 names.join(" ")
312}
313
314// TODO: return Vec<NameRef>?
315fn extract_tags(repo: &dyn Repo, commit: &Commit) -> String {
316 let mut names = vec![];
317 for (tag_name, target) in repo.view().tags() {
318 if target.has_add(commit.id()) {
319 if target.is_conflict() {
320 names.push(format!("{tag_name}?"));
321 } else {
322 names.push(tag_name.clone());
323 }
324 }
325 }
326 names.join(" ")
327}
328
329// TODO: return Vec<NameRef>?
330fn extract_git_refs(repo: &dyn Repo, commit: &Commit) -> String {
331 // TODO: We should keep a map from commit to ref names so we don't have to walk
332 // all refs here.
333 let mut names = vec![];
334 for (name, target) in repo.view().git_refs() {
335 if target.has_add(commit.id()) {
336 if target.is_conflict() {
337 names.push(format!("{name}?"));
338 } else {
339 names.push(name.clone());
340 }
341 }
342 }
343 names.join(" ")
344}
345
346// TODO: return NameRef?
347fn extract_git_head(repo: &dyn Repo, commit: &Commit) -> String {
348 match repo.view().git_head() {
349 Some(ref_target) if ref_target.has_add(commit.id()) => {
350 if ref_target.is_conflict() {
351 "HEAD@git?".to_string()
352 } else {
353 "HEAD@git".to_string()
354 }
355 }
356 _ => "".to_string(),
357 }
358}
359
360#[derive(Clone, Debug, Eq, PartialEq)]
361enum CommitOrChangeId {
362 Commit(CommitId),
363 Change(ChangeId),
364}
365
366impl CommitOrChangeId {
367 pub fn hex(&self) -> String {
368 match self {
369 CommitOrChangeId::Commit(id) => id.hex(),
370 CommitOrChangeId::Change(id) => {
371 // TODO: We can avoid the unwrap() and make this more efficient by converting
372 // straight from bytes.
373 to_reverse_hex(&id.hex()).unwrap()
374 }
375 }
376 }
377
378 pub fn short(&self, total_len: usize) -> String {
379 let mut hex = self.hex();
380 hex.truncate(total_len);
381 hex
382 }
383
384 /// The length of the id printed will be the maximum of `total_len` and the
385 /// length of the shortest unique prefix
386 pub fn shortest(&self, repo: &dyn Repo, total_len: usize) -> ShortestIdPrefix {
387 let mut hex = self.hex();
388 let prefix_len = match self {
389 CommitOrChangeId::Commit(id) => repo.index().shortest_unique_commit_id_prefix_len(id),
390 CommitOrChangeId::Change(id) => repo.shortest_unique_change_id_prefix_len(id),
391 };
392 hex.truncate(max(prefix_len, total_len));
393 let rest = hex.split_off(prefix_len);
394 ShortestIdPrefix { prefix: hex, rest }
395 }
396}
397
398impl Template<()> for CommitOrChangeId {
399 fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> {
400 formatter.write_str(&self.hex())
401 }
402}
403
404fn build_commit_or_change_id_method<'repo>(
405 language: &CommitTemplateLanguage<'repo, '_>,
406 build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>,
407 self_property: impl TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo,
408 function: &FunctionCallNode,
409) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> {
410 let parse_optional_integer = |function| -> Result<Option<_>, TemplateParseError> {
411 let ([], [len_node]) = template_parser::expect_arguments(function)?;
412 len_node
413 .map(|node| template_builder::expect_integer_expression(language, build_ctx, node))
414 .transpose()
415 };
416 let property = match function.name {
417 "short" => {
418 let len_property = parse_optional_integer(function)?;
419 language.wrap_string(TemplateFunction::new(
420 (self_property, len_property),
421 |(id, len)| id.short(len.and_then(|l| l.try_into().ok()).unwrap_or(12)),
422 ))
423 }
424 "shortest" => {
425 let repo = language.repo;
426 let len_property = parse_optional_integer(function)?;
427 language.wrap_shortest_id_prefix(TemplateFunction::new(
428 (self_property, len_property),
429 |(id, len)| id.shortest(repo, len.and_then(|l| l.try_into().ok()).unwrap_or(0)),
430 ))
431 }
432 _ => {
433 return Err(TemplateParseError::no_such_method(
434 "CommitOrChangeId",
435 function,
436 ))
437 }
438 };
439 Ok(property)
440}
441
442struct ShortestIdPrefix {
443 pub prefix: String,
444 pub rest: String,
445}
446
447impl Template<()> for ShortestIdPrefix {
448 fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> {
449 formatter.with_label("prefix", |fmt| fmt.write_str(&self.prefix))?;
450 formatter.with_label("rest", |fmt| fmt.write_str(&self.rest))
451 }
452}
453
454impl ShortestIdPrefix {
455 fn to_upper(&self) -> Self {
456 Self {
457 prefix: self.prefix.to_ascii_uppercase(),
458 rest: self.rest.to_ascii_uppercase(),
459 }
460 }
461 fn to_lower(&self) -> Self {
462 Self {
463 prefix: self.prefix.to_ascii_lowercase(),
464 rest: self.rest.to_ascii_lowercase(),
465 }
466 }
467}
468
469fn build_shortest_id_prefix_method<'repo>(
470 language: &CommitTemplateLanguage<'repo, '_>,
471 _build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>,
472 self_property: impl TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo,
473 function: &FunctionCallNode,
474) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> {
475 let property = match function.name {
476 "prefix" => {
477 template_parser::expect_no_arguments(function)?;
478 language.wrap_string(TemplateFunction::new(self_property, |id| id.prefix))
479 }
480 "rest" => {
481 template_parser::expect_no_arguments(function)?;
482 language.wrap_string(TemplateFunction::new(self_property, |id| id.rest))
483 }
484 "upper" => {
485 template_parser::expect_no_arguments(function)?;
486 language
487 .wrap_shortest_id_prefix(TemplateFunction::new(self_property, |id| id.to_upper()))
488 }
489 "lower" => {
490 template_parser::expect_no_arguments(function)?;
491 language
492 .wrap_shortest_id_prefix(TemplateFunction::new(self_property, |id| id.to_lower()))
493 }
494 _ => {
495 return Err(TemplateParseError::no_such_method(
496 "ShortestIdPrefix",
497 function,
498 ))
499 }
500 };
501 Ok(property)
502}
503
504pub fn parse<'repo>(
505 repo: &'repo dyn Repo,
506 workspace_id: &WorkspaceId,
507 template_text: &str,
508 aliases_map: &TemplateAliasesMap,
509) -> TemplateParseResult<Box<dyn Template<Commit> + 'repo>> {
510 let language = CommitTemplateLanguage { repo, workspace_id };
511 let node = template_parser::parse(template_text, aliases_map)?;
512 template_builder::build(&language, &node)
513}