// Copyright 2020 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::cmp::{max, min}; use std::io; use itertools::Itertools; use jujutsu_lib::backend::{ObjectId, Signature, Timestamp}; use jujutsu_lib::commit::Commit; use jujutsu_lib::repo::RepoRef; use crate::formatter::{Formatter, PlainTextFormatter}; use crate::time_util; pub trait Template { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()>; /// Returns true if `format()` will generate output other than labels. fn has_content(&self, context: &C) -> bool; } impl + ?Sized> Template for Box { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { >::format(self, context, formatter) } fn has_content(&self, context: &C) -> bool { >::has_content(self, context) } } impl Template<()> for Signature { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { write!(formatter.labeled("name"), "{}", self.name)?; write!(formatter, " <")?; write!(formatter.labeled("email"), "{}", self.email)?; write!(formatter, ">")?; Ok(()) } fn has_content(&self, _: &()) -> bool { true } } impl Template<()> for String { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { formatter.write_str(self) } fn has_content(&self, _: &()) -> bool { !self.is_empty() } } impl Template<()> for Timestamp { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { formatter.write_str(&time_util::format_absolute_timestamp(self)) } fn has_content(&self, _: &()) -> bool { true } } impl Template<()> for bool { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { formatter.write_str(if *self { "true" } else { "false" }) } fn has_content(&self, _: &()) -> bool { true } } pub struct LabelTemplate { content: T, labels: L, } impl LabelTemplate { pub fn new(content: T, labels: L) -> Self where T: Template, L: TemplateProperty>, { LabelTemplate { content, labels } } } impl Template for LabelTemplate where T: Template, L: TemplateProperty>, { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { let labels = self.labels.extract(context); for label in &labels { formatter.push_label(label)?; } self.content.format(context, formatter)?; for _label in &labels { formatter.pop_label()?; } Ok(()) } fn has_content(&self, context: &C) -> bool { self.content.has_content(context) } } pub struct ListTemplate(pub Vec); impl> Template for ListTemplate { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { for template in &self.0 { template.format(context, formatter)? } Ok(()) } fn has_content(&self, context: &C) -> bool { self.0.iter().any(|template| template.has_content(context)) } } /// Like `ListTemplate`, but inserts a separator between non-empty templates. pub struct SeparateTemplate { separator: S, contents: Vec, } impl SeparateTemplate { pub fn new(separator: S, contents: Vec) -> Self where S: Template, T: Template, { SeparateTemplate { separator, contents, } } } impl Template for SeparateTemplate where S: Template, T: Template, { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { // TemplateProperty may be evaluated twice, by has_content() and format(). // If that's too expensive, we can instead create a buffered formatter // inheriting the state, and write to it to test the emptiness. In this case, // the formatter should guarantee push/pop_label() is noop without content. let mut content_templates = self .contents .iter() .filter(|template| template.has_content(context)) .fuse(); if let Some(template) = content_templates.next() { template.format(context, formatter)?; } for template in content_templates { self.separator.format(context, formatter)?; template.format(context, formatter)?; } Ok(()) } fn has_content(&self, context: &C) -> bool { self.contents .iter() .any(|template| template.has_content(context)) } } pub trait TemplateProperty { type Output; fn extract(&self, context: &C) -> Self::Output; } impl + ?Sized> TemplateProperty for Box

{ type Output =

>::Output; fn extract(&self, context: &C) -> Self::Output {

>::extract(self, context) } } /// Adapter to drop template context. pub struct Literal(pub O); impl> Template for Literal { fn format(&self, _context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { self.0.format(&(), formatter) } fn has_content(&self, _context: &C) -> bool { self.0.has_content(&()) } } impl TemplateProperty for Literal { type Output = O; fn extract(&self, _context: &C) -> O { self.0.clone() } } /// Adapter to turn closure into property. pub struct TemplatePropertyFn(pub F); impl O> TemplateProperty for TemplatePropertyFn { type Output = O; fn extract(&self, context: &C) -> Self::Output { (self.0)(context) } } /// Adapter to extract context-less template value from property for displaying. pub struct FormattablePropertyTemplate

{ property: P, } impl

FormattablePropertyTemplate

{ pub fn new(property: P) -> Self where P: TemplateProperty, P::Output: Template<()>, { FormattablePropertyTemplate { property } } } impl Template for FormattablePropertyTemplate

where P: TemplateProperty, P::Output: Template<()>, { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { let template = self.property.extract(context); template.format(&(), formatter) } fn has_content(&self, context: &C) -> bool { let template = self.property.extract(context); template.has_content(&()) } } /// Adapter to turn template back to string property. pub struct PlainTextFormattedProperty { template: T, } impl PlainTextFormattedProperty { pub fn new(template: T) -> Self { PlainTextFormattedProperty { template } } } impl> TemplateProperty for PlainTextFormattedProperty { type Output = String; fn extract(&self, context: &C) -> Self::Output { let mut output = vec![]; self.template .format(context, &mut PlainTextFormatter::new(&mut output)) .expect("write() to PlainTextFormatter should never fail"); // TODO: Use from_utf8_lossy() if we added template that embeds file content String::from_utf8(output).expect("template output should be utf-8 bytes") } } pub struct WorkingCopiesProperty<'a> { pub repo: RepoRef<'a>, } impl TemplateProperty for WorkingCopiesProperty<'_> { type Output = String; fn extract(&self, context: &Commit) -> Self::Output { let wc_commit_ids = self.repo.view().wc_commit_ids(); if wc_commit_ids.len() <= 1 { return "".to_string(); } let mut names = vec![]; for (workspace_id, wc_commit_id) in wc_commit_ids.iter().sorted() { if wc_commit_id == context.id() { names.push(format!("{}@", workspace_id.as_str())); } } names.join(" ") } } pub struct BranchProperty<'a> { pub repo: RepoRef<'a>, } impl TemplateProperty for BranchProperty<'_> { type Output = String; fn extract(&self, context: &Commit) -> Self::Output { let mut names = vec![]; for (branch_name, branch_target) in self.repo.view().branches() { let local_target = branch_target.local_target.as_ref(); if let Some(local_target) = local_target { if local_target.has_add(context.id()) { if local_target.is_conflict() { names.push(format!("{branch_name}??")); } else if branch_target .remote_targets .values() .any(|remote_target| remote_target != local_target) { names.push(format!("{branch_name}*")); } else { names.push(branch_name.clone()); } } } for (remote_name, remote_target) in &branch_target.remote_targets { if Some(remote_target) != local_target && remote_target.has_add(context.id()) { if remote_target.is_conflict() { names.push(format!("{branch_name}@{remote_name}?")); } else { names.push(format!("{branch_name}@{remote_name}")); } } } } names.join(" ") } } pub struct TagProperty<'a> { pub repo: RepoRef<'a>, } impl TemplateProperty for TagProperty<'_> { type Output = String; fn extract(&self, context: &Commit) -> Self::Output { let mut names = vec![]; for (tag_name, target) in self.repo.view().tags() { if target.has_add(context.id()) { if target.is_conflict() { names.push(format!("{tag_name}?")); } else { names.push(tag_name.clone()); } } } names.join(" ") } } pub struct GitRefsProperty<'a> { pub repo: RepoRef<'a>, } impl TemplateProperty for GitRefsProperty<'_> { type Output = String; fn extract(&self, context: &Commit) -> Self::Output { // TODO: We should keep a map from commit to ref names so we don't have to walk // all refs here. let mut names = vec![]; for (name, target) in self.repo.view().git_refs() { if target.has_add(context.id()) { if target.is_conflict() { names.push(format!("{name}?")); } else { names.push(name.clone()); } } } names.join(" ") } } pub struct GitHeadProperty<'a> { repo: RepoRef<'a>, } impl<'a> GitHeadProperty<'a> { pub fn new(repo: RepoRef<'a>) -> Self { Self { repo } } } impl TemplateProperty for GitHeadProperty<'_> { type Output = String; fn extract(&self, context: &Commit) -> String { match self.repo.view().git_head() { Some(ref_target) if ref_target.has_add(context.id()) => { if ref_target.is_conflict() { "HEAD@git?".to_string() } else { "HEAD@git".to_string() } } _ => "".to_string(), } } } pub struct ConditionalTemplate { pub condition: P, pub true_template: T, pub false_template: Option, } impl ConditionalTemplate { pub fn new(condition: P, true_template: T, false_template: Option) -> Self where P: TemplateProperty, T: Template, U: Template, { ConditionalTemplate { condition, true_template, false_template, } } } impl Template for ConditionalTemplate where P: TemplateProperty, T: Template, U: Template, { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { if self.condition.extract(context) { self.true_template.format(context, formatter)?; } else if let Some(false_template) = &self.false_template { false_template.format(context, formatter)?; } Ok(()) } fn has_content(&self, context: &C) -> bool { if self.condition.extract(context) { self.true_template.has_content(context) } else if let Some(false_template) = &self.false_template { false_template.has_content(context) } else { false } } } // TODO: If needed, add a ContextualTemplateFunction where the function also // gets the context pub struct TemplateFunction { pub property: P, pub function: F, } impl TemplateFunction { pub fn new(property: P, function: F) -> Self where P: TemplateProperty, F: Fn(P::Output) -> O, { TemplateFunction { property, function } } } impl TemplateProperty for TemplateFunction where P: TemplateProperty, F: Fn(P::Output) -> O, { type Output = O; fn extract(&self, context: &C) -> Self::Output { (self.function)(self.property.extract(context)) } } /// Type-erased `CommitId`/`ChangeId`. #[derive(Clone)] pub struct CommitOrChangeId<'a> { repo: RepoRef<'a>, id_bytes: Vec, } impl<'a> CommitOrChangeId<'a> { pub fn new(repo: RepoRef<'a>, id: &impl ObjectId) -> Self { CommitOrChangeId { repo, id_bytes: id.to_bytes(), } } pub fn as_bytes(&self) -> &[u8] { &self.id_bytes } pub fn hex(&self) -> String { hex::encode(&self.id_bytes) } pub fn short(&self) -> String { let mut hex = self.hex(); hex.truncate(12); hex } pub fn shortest_prefix_and_brackets(&self) -> String { let hex = self.hex(); let (prefix, rest) = extract_entire_prefix_and_trimmed_tail( &hex, self.repo.shortest_unique_id_prefix_len(self.as_bytes()), 12 - 2, ); if rest.is_empty() { prefix.to_string() } else { format!("{prefix}[{rest}]") } } pub fn shortest_styled_prefix(&self) -> IdWithHighlightedPrefix { let hex = self.hex(); let (prefix, rest) = extract_entire_prefix_and_trimmed_tail( &hex, self.repo.shortest_unique_id_prefix_len(self.as_bytes()), 12, ); IdWithHighlightedPrefix { prefix: prefix.to_string(), rest: rest.to_string(), } } } impl Template<()> for CommitOrChangeId<'_> { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { formatter.write_str(&self.hex()) } fn has_content(&self, _: &()) -> bool { !self.id_bytes.is_empty() } } /// This function supports short `total_len` by ensuring that the entire /// unique prefix is always printed fn extract_entire_prefix_and_trimmed_tail( s: &str, prefix_len: usize, total_len: usize, ) -> (&str, &str) { let prefix_len = min(prefix_len, s.len()); let total_len = max(prefix_len, min(total_len, s.len())); (&s[0..prefix_len], &s[prefix_len..total_len]) } #[cfg(test)] mod tests { use super::extract_entire_prefix_and_trimmed_tail; #[test] fn test_prefix() { let s = "0123456789"; insta::assert_debug_snapshot!(extract_entire_prefix_and_trimmed_tail(s, 2, 5), @r###" ( "01", "234", ) "###); insta::assert_debug_snapshot!(extract_entire_prefix_and_trimmed_tail(s, 2, 11), @r###" ( "01", "23456789", ) "###); insta::assert_debug_snapshot!(extract_entire_prefix_and_trimmed_tail(s, 11, 2), @r###" ( "0123456789", "", ) "###); insta::assert_debug_snapshot!(extract_entire_prefix_and_trimmed_tail(s, 11, 11), @r###" ( "0123456789", "", ) "###); } } pub struct IdWithHighlightedPrefix { prefix: String, rest: String, } impl Template<()> for IdWithHighlightedPrefix { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { formatter.with_label("prefix", |fmt| fmt.write_str(&self.prefix))?; formatter.with_label("rest", |fmt| fmt.write_str(&self.rest)) } fn has_content(&self, _: &()) -> bool { !self.prefix.is_empty() || !self.rest.is_empty() } }