use url::Url;
use crossterm::style::{style, Attribute, ContentStyle, StyledContent};
use getset::{CopyGetters, Getters, Setters};
use git_stree::SubtreeConfig;
use crate::actors::fork_point::ForkPointCalculation;
use crate::commit::{parse_remote_url, Commit, GitRef, Oid};
use crate::default_styles::{DATE_STYLE, ID_STYLE, MOD_STYLE, NAME_STYLE, REF_STYLE};
use crate::ui::base::StyledLine;
use git_wrapper::Remote;
use lazy_static::lazy_static;
use subject_classifier::{Subject, SubtreeOperation};
use unicode_truncate::UnicodeTruncateStr;
use unicode_width::UnicodeWidthStr;
struct IgnoredRefWildcard(String);
lazy_static! {
static ref TIME_SPLIT_REGEX: regex::Regex =
regex::Regex::new(r#".+{8,} \d\d:\d\d$"#).expect("Valid RegEx");
}
pub enum EntryKind {
IncomingOnly,
IncomingAndOutgoing,
Link,
Orphan,
OutgoingOnly,
}
impl EntryKind {
pub fn new(c: &Commit, has_above: bool, is_link: bool) -> Self {
if is_link {
Self::Link
} else {
match (has_above, c.parents().is_empty()) {
(true, true) => Self::IncomingOnly,
(true, false) => Self::IncomingAndOutgoing,
(false, true) => Self::Orphan,
(false, false) => Self::OutgoingOnly,
}
}
}
const fn to_char(&self) -> char {
match self {
Self::IncomingOnly => '◉',
Self::IncomingAndOutgoing => '●',
Self::Link => '⭞',
Self::Orphan => '○',
Self::OutgoingOnly => '◒',
}
}
}
#[derive(CopyGetters, Getters, Setters)]
pub struct HistoryEntry {
#[getset(get = "pub")]
commit: Commit,
#[getset(get = "pub", set = "pub")]
visible_children: usize,
#[getset(get_copy = "pub")]
level: u8,
kind: EntryKind,
remotes: Vec<Remote>,
subject: Subject,
#[getset(get = "pub", set = "pub")]
subtrees: Vec<SubtreeConfig>,
#[getset(get = "pub", set = "pub")]
forge_url: Option<Url>,
#[getset(get = "pub")]
fork_point: ForkPointCalculation,
#[getset(get = "pub")]
debug: bool,
}
impl HistoryEntry {
#[must_use]
pub fn new(
commit: Commit,
level: u8,
forge_url: Option<Url>,
fork_point: ForkPointCalculation,
repo_remotes: &[Remote],
kind: EntryKind,
debug: bool,
) -> Self {
let subject_struct = Subject::from(commit.subject().as_str());
let remotes = if commit.references().is_empty() {
vec![]
} else {
let mut result = vec![];
for remote in repo_remotes {
for git_ref in commit.references() {
if git_ref.to_string().starts_with(&remote.name) {
result.push(remote.clone());
break;
}
}
}
result
};
Self {
commit,
visible_children: 0,
kind,
level,
remotes,
subject: subject_struct,
subtrees: vec![],
forge_url,
fork_point,
debug,
}
}
pub const fn is_link(&self) -> bool {
matches!(self.kind, EntryKind::Link)
}
}
impl HistoryEntry {
fn render_id(&self) -> StyledContent<String> {
let id = self.commit.short_id();
StyledContent::new(*ID_STYLE, id.clone())
}
fn render_date(&self) -> StyledContent<String> {
let date = self.author_rel_date();
let mut end = date.len();
#[allow(clippy::arithmetic)]
if end - 6 > 0 && (&date[end - 6..end - 4] == " +" || &date[end - 6..end - 4] == " -") {
end -= 6;
}
#[allow(clippy::arithmetic)]
if TIME_SPLIT_REGEX.is_match(date) {
end -= 5;
}
StyledContent::new(*DATE_STYLE, date[0..end].to_owned())
}
fn render_name(&self) -> StyledContent<String> {
let name = self.commit.author_name();
StyledContent::new(*NAME_STYLE, name.clone())
}
fn render_icon(&self) -> StyledContent<String> {
style(self.subject.icon().to_owned())
}
fn render_graph(&self) -> StyledContent<String> {
let mut text = "".to_owned();
for _ in 0..self.level {
text.push('│');
}
text.push(self.kind.to_char());
#[allow(clippy::else_if_without_else)]
if self.has_children() {
if self.is_subtree_import() || self.is_subtree_update() {
if self.is_fork_point() {
text.push_str("⇤┤");
} else {
text.push_str("⇤╮");
}
} else if self.is_fork_point() {
text.push('┤');
} else {
text.push('┐');
}
} else if self.is_fork_point() {
text.push('┘');
}
style(text)
}
fn render_modules(&self, max_len: usize) -> Option<StyledContent<String>> {
if self.subtrees.is_empty() {
None
} else {
let mut text = ":".to_owned();
let subtree_modules: Vec<String> =
self.subtrees.iter().map(|e| e.id().clone()).collect();
text.push_str(&subtree_modules.join(" :"));
if text.width() > max_len {
match subtree_modules.len() {
1 => {
text = text
.unicode_truncate(max_len.saturating_sub(1))
.0
.to_owned();
text.push('…');
}
x => text = format!("({} strees)", x),
}
}
Some(StyledContent::new(*MOD_STYLE, text))
}
}
fn shorten_references(remotes: &[Remote], references: &[&GitRef]) -> Vec<String> {
let mut result = vec![];
if !references.is_empty() {
if remotes.is_empty() {
for r in references {
result.push(r.to_string());
}
} else {
let mut mut_refs = references.to_vec();
let mut tmp_result = vec![];
for remote in remotes {
let mut remote_branches = vec![];
if mut_refs.is_empty() {
break;
}
for git_ref in references {
if git_ref.to_string().starts_with(&remote.name) {
remote_branches.push(git_ref);
mut_refs.retain(|x| x != git_ref);
}
}
if !remote_branches.is_empty() {
if remote_branches.len() == 1 {
result.push(remote_branches[0].to_string());
} else {
let prefix_len = remote.name.len().saturating_add(1);
let mut text = remote.name.clone();
text.push('/');
text.push('{');
text.push_str(
&remote_branches
.iter()
.map(|r| r.to_string().split_off(prefix_len))
.collect::<Vec<_>>()
.join(","),
);
text.push('}');
tmp_result.push(text);
}
}
}
result.extend(mut_refs.iter().map(std::string::ToString::to_string));
result.extend(tmp_result);
}
}
result
}
fn format_scope(scope: &str) -> StyledContent<String> {
let mut text = "(".to_owned();
text.push_str(scope);
text.push(')');
let style = if is_hex(scope) {
let mut style = ContentStyle::default();
style.attributes.set(Attribute::Underlined);
style
} else {
*DATE_STYLE
};
StyledContent::new(style, text)
}
fn render_references(&self) -> Vec<StyledContent<String>> {
let mut result = vec![];
let references = self.filtered_references();
for r in Self::shorten_references(&self.remotes, &references) {
let separator = style(" ".to_owned());
result.push(separator);
let text = format!("«{}»", r);
let sc = StyledContent::new(*REF_STYLE, text);
result.push(sc);
}
result
}
pub fn filtered_references(&self) -> Vec<&GitRef> {
let references = self.commit.references();
references
.iter()
.filter(|r| {
for prefix in &ignored_refs() {
if r.0.starts_with(&prefix.0) {
log::info!("Branch {} hidden", r.0);
return false;
}
}
true
})
.collect()
}
fn render_subject(&self) -> Vec<StyledContent<String>> {
let mut buf = vec![];
let separator = style(" ".to_owned());
match &self.subject {
Subject::ConventionalCommit {
scope,
description,
category,
..
} => {
if let Some(s) = scope {
buf.push(Self::format_scope(s));
buf.push(separator);
}
if *category == subject_classifier::Type::Deps {
if let Some(word) = description.split_whitespace().last() {
#[allow(clippy::arithmetic)]
if word.starts_with('v')
|| word.starts_with('V')
|| word.chars().next().unwrap().is_numeric()
{
let l = description.len() - word.len();
buf.push(style(description[..l].to_owned()));
let mut bold_style = ContentStyle::default();
bold_style.attributes.set(Attribute::Bold);
let sc = StyledContent::new(bold_style, word.to_owned());
buf.push(sc);
} else {
buf.push(style(description.clone()));
}
} else {
buf.push(style(description.clone()));
}
} else {
buf.push(StyledContent::new(
ContentStyle::default(),
description.clone(),
));
}
}
Subject::Release { description, .. }
| Subject::Fixup(description)
| Subject::Remove(description)
| Subject::Rename(description)
| Subject::Revert(description)
| Subject::Simple(description) => buf.push(StyledContent::new(
ContentStyle::default(),
description.clone(),
)),
Subject::PullRequest { .. } => buf.push(style(self.subject.description().to_owned())),
Subject::SubtreeCommit { operation, .. } => {
let mut bold_style = ContentStyle::default();
bold_style.attributes.set(Attribute::Bold);
let (text, git_ref) = match operation {
SubtreeOperation::Import { git_ref, .. } => ("Import from ", git_ref),
SubtreeOperation::Split { git_ref, .. } => ("Split into commit ", git_ref),
SubtreeOperation::Update { git_ref, .. } => ("Update to ", git_ref),
};
buf.push(style(text.to_owned()));
let sc = StyledContent::new(bold_style, git_ref.clone());
buf.push(sc);
}
}
buf
}
pub fn render(&self, selected: bool) -> StyledLine<String> {
let separator = style(" ".to_owned());
let mut result: StyledLine<String> = StyledLine {
content: vec![
self.render_id(),
separator.clone(),
self.render_date(),
separator.clone(),
self.render_name(),
separator.clone(),
self.render_icon(),
self.render_graph(),
],
};
let references = self.render_references();
if !references.is_empty() {
result.content.extend(references);
}
result.content.push(separator.clone());
if let Some(modules) = self.render_modules(32) {
result.content.push(modules);
result.content.push(separator);
}
result.content.extend(self.render_subject());
if selected {
for part in &mut result.content {
part.style_mut().attributes.set(Attribute::Reverse);
}
};
result
}
const fn is_subtree_import(&self) -> bool {
matches!(
&self.subject,
Subject::SubtreeCommit {
operation: SubtreeOperation::Import { .. },
..
}
)
}
const fn is_subtree_update(&self) -> bool {
matches!(
&self.subject,
Subject::SubtreeCommit {
operation: SubtreeOperation::Update { .. },
..
}
)
}
}
fn is_hex(s: &str) -> bool {
s.chars()
.all(|c| ['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F'].contains(&c))
}
impl HistoryEntry {
pub fn set_subject(&mut self, subject: &str) {
self.subject = Subject::from(subject);
}
pub fn set_fork_point(&mut self, t: bool) {
self.fork_point = ForkPointCalculation::Done(t);
}
#[must_use]
pub const fn special(&self) -> &Subject {
&self.subject
}
#[must_use]
pub fn id(&self) -> &Oid {
self.commit.id()
}
#[must_use]
pub fn short_id(&self) -> &String {
self.commit.short_id()
}
#[must_use]
pub fn author_rel_date(&self) -> &String {
self.commit.author_rel_date()
}
#[must_use]
pub const fn is_fork_point(&self) -> bool {
match self.fork_point {
ForkPointCalculation::Done(t) => t,
ForkPointCalculation::InProgress => false,
}
}
#[must_use]
pub const fn is_folded(&self) -> bool {
self.visible_children == 0
}
#[must_use]
pub fn is_foldable(&self) -> bool {
self.commit.is_merge()
}
#[must_use]
pub fn has_children(&self) -> bool {
self.commit.is_merge()
}
#[must_use]
#[allow(dead_code)]
pub fn search_matches(&self, needle: &str, ignore_case: bool) -> bool {
let subject = self.subject.description().to_owned();
let mut candidates = vec![
self.commit.author_name(),
self.commit.short_id(),
&self.commit.id().0,
self.commit.author_name(),
self.commit.author_email(),
self.commit.committer_name(),
self.commit.committer_email(),
&subject,
];
let x: Vec<String> = self.subtrees.iter().map(|e| e.id().clone()).collect();
candidates.extend(&x);
for r in self.commit.references().iter() {
candidates.push(&r.0);
}
for cand in candidates {
if ignore_case {
if cand.to_lowercase().contains(&needle.to_lowercase()) {
return true;
}
} else {
return cand.contains(needle);
}
}
false
}
#[must_use]
pub fn url(&self) -> Option<Url> {
if let Some(module) = self.subtrees.first() {
let url_option = if module.upstream().is_some() {
module.upstream()
} else {
module.origin()
};
if let Some(v) = url_option {
if let Some(u) = parse_remote_url(v) {
return Some(u);
};
}
}
self.forge_url.clone()
}
}
fn ignored_refs() -> Vec<IgnoredRefWildcard> {
vec![IgnoredRefWildcard("refs/prefetch/".to_owned())]
}