// 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::borrow::BorrowMut; use std::collections::{HashMap, HashSet}; use std::io; use std::ops::{Add, AddAssign}; use itertools::Itertools; use jujutsu_lib::backend::{ChangeId, CommitId, ObjectId, Signature, Timestamp}; use jujutsu_lib::commit::Commit; use jujutsu_lib::op_store::WorkspaceId; use jujutsu_lib::repo::RepoRef; use jujutsu_lib::revset::RevsetExpression; use crate::formatter::Formatter; pub trait Template { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()>; } // TODO: Extract a trait for this type? pub struct TemplateFormatter<'f, 't: 'f, C> { template: Box + 't>, formatter: &'f mut dyn Formatter, } impl<'f, 't: 'f, C> TemplateFormatter<'f, 't, C> { pub fn new(template: Box + 't>, formatter: &'f mut dyn Formatter) -> Self { TemplateFormatter { template, formatter, } } pub fn format<'c, 'a: 'c>(&'a mut self, context: &'c C) -> io::Result<()> { self.template.format(context, self.formatter.borrow_mut()) } } pub struct LiteralTemplate(pub String); impl Template for LiteralTemplate { fn format(&self, _context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { formatter.write_str(&self.0) } } // TODO: figure out why this lifetime is needed pub struct LabelTemplate<'a, C> { content: Box + 'a>, labels: Vec, } impl<'a, C> LabelTemplate<'a, C> { pub fn new(content: Box + 'a>, labels: Vec) -> Self { LabelTemplate { content, labels } } } impl<'a, C> Template for LabelTemplate<'a, C> { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { for label in &self.labels { formatter.add_label(label)?; } self.content.format(context, formatter)?; for _label in &self.labels { formatter.remove_label()?; } Ok(()) } } pub type DynamicLabelFunction<'a, C> = Box Vec + 'a>; // TODO: figure out why this lifetime is needed pub struct DynamicLabelTemplate<'a, C> { content: Box + 'a>, label_property: DynamicLabelFunction<'a, C>, } impl<'a, C> DynamicLabelTemplate<'a, C> { pub fn new( content: Box + 'a>, label_property: DynamicLabelFunction<'a, C>, ) -> Self { DynamicLabelTemplate { content, label_property, } } } impl<'a, C> Template for DynamicLabelTemplate<'a, C> { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { let labels = self.label_property.as_ref()(context); for label in &labels { formatter.add_label(label)?; } self.content.format(context, formatter)?; for _label in &labels { formatter.remove_label()?; } Ok(()) } } // TODO: figure out why this lifetime is needed pub struct ListTemplate<'a, C>(pub Vec + 'a>>); impl<'a, C> Template for ListTemplate<'a, C> { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { for template in &self.0 { template.format(context, formatter)? } Ok(()) } } pub trait TemplateProperty { fn extract(&self, context: &C) -> O; } pub struct ConstantTemplateProperty { pub output: O, } impl TemplateProperty for ConstantTemplateProperty { fn extract(&self, _context: &C) -> O { self.output.clone() } } // TODO: figure out why this lifetime is needed pub struct StringPropertyTemplate<'a, C> { pub property: Box + 'a>, } impl<'a, C> Template for StringPropertyTemplate<'a, C> { fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> { let text = self.property.extract(context); formatter.write_str(&text) } } pub struct ChangeIdProperty; impl TemplateProperty for ChangeIdProperty { fn extract(&self, context: &Commit) -> String { context.change_id().hex() } } pub struct DescriptionProperty; impl TemplateProperty for DescriptionProperty { fn extract(&self, context: &Commit) -> String { let description = context.description().to_owned(); if description.ends_with('\n') { description } else if description.is_empty() { "(no description set)\n".to_string() } else { description.add("\n") } } } pub struct AuthorProperty; impl TemplateProperty for AuthorProperty { fn extract(&self, context: &Commit) -> Signature { context.author().clone() } } pub struct CommitterProperty; impl TemplateProperty for CommitterProperty { fn extract(&self, context: &Commit) -> Signature { context.committer().clone() } } pub struct WorkingCopiesProperty<'a> { pub repo: RepoRef<'a>, } impl TemplateProperty for WorkingCopiesProperty<'_> { fn extract(&self, context: &Commit) -> String { 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 IsWorkingCopyProperty<'a> { pub repo: RepoRef<'a>, pub workspace_id: WorkspaceId, } impl TemplateProperty for IsWorkingCopyProperty<'_> { fn extract(&self, context: &Commit) -> bool { Some(context.id()) == self.repo.view().get_wc_commit_id(&self.workspace_id) } } pub struct BranchProperty<'a> { pub repo: RepoRef<'a>, } impl TemplateProperty for BranchProperty<'_> { fn extract(&self, context: &Commit) -> String { 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<'_> { fn extract(&self, context: &Commit) -> String { 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<'_> { fn extract(&self, context: &Commit) -> String { // 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 IsGitHeadProperty<'a> { repo: RepoRef<'a>, } impl<'a> IsGitHeadProperty<'a> { pub fn new(repo: RepoRef<'a>) -> Self { Self { repo } } } impl TemplateProperty for IsGitHeadProperty<'_> { fn extract(&self, context: &Commit) -> bool { self.repo.view().git_head().as_ref() == Some(context.id()) } } pub struct DivergentProperty { divergent_changes: HashSet, } impl DivergentProperty { pub fn new(repo: RepoRef) -> Self { // TODO: Create a persistent index from change id to commit ids. let mut commit_count_by_change: HashMap = HashMap::new(); for index_entry in RevsetExpression::all().evaluate(repo, None).unwrap().iter() { let change_id = index_entry.change_id(); commit_count_by_change .entry(change_id) .or_default() .add_assign(1); } let mut divergent_changes = HashSet::new(); for (change_id, count) in commit_count_by_change { if count > 1 { divergent_changes.insert(change_id); } } Self { divergent_changes } } } impl TemplateProperty for DivergentProperty { fn extract(&self, context: &Commit) -> bool { self.divergent_changes.contains(context.change_id()) } } pub struct ConflictProperty; impl TemplateProperty for ConflictProperty { fn extract(&self, context: &Commit) -> bool { context.tree().has_conflict() } } pub struct ConditionalTemplate<'a, C> { pub condition: Box + 'a>, pub true_template: Box + 'a>, pub false_template: Option + 'a>>, } // TODO: figure out why this lifetime is needed impl<'a, C> ConditionalTemplate<'a, C> { pub fn new( condition: Box + 'a>, true_template: Box + 'a>, false_template: Option + 'a>>, ) -> Self { ConditionalTemplate { condition, true_template, false_template, } } } impl<'a, C> Template for ConditionalTemplate<'a, C> { 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(()) } } // TODO: If needed, add a ContextualTemplateFunction where the function also // gets the context pub struct TemplateFunction<'a, C, I, O> { pub property: Box + 'a>, pub function: Box O + 'a>, } // TODO: figure out why this lifetime is needed impl<'a, C, I, O> TemplateFunction<'a, C, I, O> { pub fn new( template: Box + 'a>, function: Box O + 'a>, ) -> Self { TemplateFunction { property: template, function, } } } impl<'a, C, I, O> TemplateProperty for TemplateFunction<'a, C, I, O> { fn extract(&self, context: &C) -> O { (self.function)(self.property.extract(context)) } } pub struct CommitIdKeyword; impl CommitIdKeyword { pub fn default_format(commit_id: CommitId) -> String { commit_id.hex() } pub fn shortest_format(commit_id: CommitId) -> String { // TODO: make this actually be the shortest unambiguous prefix commit_id.hex()[..12].to_string() } } impl TemplateProperty for CommitIdKeyword { fn extract(&self, context: &Commit) -> CommitId { context.id().clone() } } pub struct SignatureTimestamp; impl TemplateProperty for SignatureTimestamp { fn extract(&self, context: &Signature) -> Timestamp { context.timestamp.clone() } }