//! Formatting of commits. use chrono::{FixedOffset, Local, TimeZone}; use git2::{Commit, Time}; use lazy_static::lazy_static; use std::fmt::Write; use std::str::FromStr; use textwrap::Options; use yansi::Paint; /// Commit formatting options. #[derive(Ord, PartialOrd, Eq, PartialEq)] pub enum CommitFormat { OneLine, Short, Medium, Full, Format(String), } impl FromStr for CommitFormat { type Err = String; fn from_str(str: &str) -> Result { match str { "oneline" | "o" => Ok(CommitFormat::OneLine), "short" | "s" => Ok(CommitFormat::Short), "medium" | "m" => Ok(CommitFormat::Medium), "full" | "f" => Ok(CommitFormat::Full), str => Ok(CommitFormat::Format(str.to_string())), } } } const NEW_LINE: usize = 0; const HASH: usize = 1; const HASH_ABBREV: usize = 2; const PARENT_HASHES: usize = 3; const PARENT_HASHES_ABBREV: usize = 4; const REFS: usize = 5; const SUBJECT: usize = 6; const AUTHOR: usize = 7; const AUTHOR_EMAIL: usize = 8; const AUTHOR_DATE: usize = 9; const AUTHOR_DATE_SHORT: usize = 10; const COMMITTER: usize = 11; const COMMITTER_EMAIL: usize = 12; const COMMITTER_DATE: usize = 13; const COMMITTER_DATE_SHORT: usize = 14; const BODY: usize = 15; const BODY_RAW: usize = 16; const MODE_SPACE: usize = 1; const MODE_PLUS: usize = 2; const MODE_MINUS: usize = 3; lazy_static! { pub static ref PLACEHOLDERS: Vec<[String; 4]> = { let base = vec![ "n", "H", "h", "P", "p", "d", "s", "an", "ae", "ad", "as", "cn", "ce", "cd", "cs", "b", "B", ]; base.iter() .map(|b| { [ format!("%{}", b), format!("% {}", b), format!("%+{}", b), format!("%-{}", b), ] }) .collect() }; } /// Format a commit for `CommitFormat::Format(String)`. pub fn format_commit( format: &str, commit: &Commit, branches: String, wrapping: &Option, hash_color: Option, ) -> Result, String> { let mut replacements = vec![]; for (idx, arr) in PLACEHOLDERS.iter().enumerate() { let mut curr = 0; loop { let mut found = false; for (mode, str) in arr.iter().enumerate() { if let Some(start) = &format[curr..format.len()].find(str) { replacements.push((curr + start, str.len(), idx, mode)); curr += start + str.len(); found = true; break; } } if !found { break; } } } replacements.sort_by_key(|p| p.0); let mut lines = vec![]; let mut out = String::new(); if replacements.is_empty() { write!(out, "{}", format).unwrap(); add_line(&mut lines, &mut out, wrapping); } else { let mut curr = 0; for (start, len, idx, mode) in replacements { if idx == NEW_LINE { write!(out, "{}", &format[curr..start]).unwrap(); add_line(&mut lines, &mut out, wrapping); } else { write!(out, "{}", &format[curr..start]).unwrap(); match idx { HASH => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } if let Some(color) = hash_color { write!(out, "{}", Paint::fixed(color, commit.id())) } else { write!(out, "{}", commit.id()) } } HASH_ABBREV => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } if let Some(color) = hash_color { write!( out, "{}", Paint::fixed(color, &commit.id().to_string()[..7]) ) } else { write!(out, "{}", &commit.id().to_string()[..7]) } } PARENT_HASHES => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } for i in 0..commit.parent_count() { write!(out, "{}", commit.parent_id(i).unwrap()).unwrap(); if i < commit.parent_count() - 1 { write!(out, " ").unwrap(); } } Ok(()) } PARENT_HASHES_ABBREV => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } for i in 0..commit.parent_count() { write!( out, "{}", &commit .parent_id(i) .map_err(|err| err.to_string())? .to_string()[..7] ) .unwrap(); if i < commit.parent_count() - 1 { write!(out, " ").unwrap(); } } Ok(()) } REFS => { match mode { MODE_SPACE => { if !branches.is_empty() { write!(out, " ").unwrap() } } MODE_PLUS => { if !branches.is_empty() { add_line(&mut lines, &mut out, wrapping) } } MODE_MINUS => { if branches.is_empty() { out = remove_empty_lines(&mut lines, out) } } _ => {} } write!(out, "{}", branches) } SUBJECT => { let summary = commit.summary().unwrap_or(""); match mode { MODE_SPACE => { if !summary.is_empty() { write!(out, " ").unwrap() } } MODE_PLUS => { if !summary.is_empty() { add_line(&mut lines, &mut out, wrapping) } } MODE_MINUS => { if summary.is_empty() { out = remove_empty_lines(&mut lines, out) } } _ => {} } write!(out, "{}", summary) } AUTHOR => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!(out, "{}", &commit.author().name().unwrap_or("")) } AUTHOR_EMAIL => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!(out, "{}", &commit.author().email().unwrap_or("")) } AUTHOR_DATE => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!( out, "{}", format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z") ) } AUTHOR_DATE_SHORT => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!(out, "{}", format_date(commit.author().when(), "%F")) } COMMITTER => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!(out, "{}", &commit.committer().name().unwrap_or("")) } COMMITTER_EMAIL => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!(out, "{}", &commit.committer().email().unwrap_or("")) } COMMITTER_DATE => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!( out, "{}", format_date(commit.committer().when(), "%a %b %e %H:%M:%S %Y %z") ) } COMMITTER_DATE_SHORT => { match mode { MODE_SPACE => write!(out, " ").unwrap(), MODE_PLUS => add_line(&mut lines, &mut out, wrapping), _ => {} } write!(out, "{}", format_date(commit.committer().when(), "%F")) } BODY => { let message = commit .message() .unwrap_or("") .lines() .collect::>(); let num_parts = message.len(); match mode { MODE_SPACE => { if num_parts > 2 { write!(out, " ").unwrap() } } MODE_PLUS => { if num_parts > 2 { add_line(&mut lines, &mut out, wrapping) } } MODE_MINUS => { if num_parts <= 2 { out = remove_empty_lines(&mut lines, out) } } _ => {} } for (cnt, line) in message.iter().enumerate() { if cnt > 1 && (cnt < num_parts - 1 || !line.is_empty()) { write!(out, "{}", line).unwrap(); add_line(&mut lines, &mut out, wrapping); } } Ok(()) } BODY_RAW => { let message = commit .message() .unwrap_or("") .lines() .collect::>(); let num_parts = message.len(); match mode { MODE_SPACE => { if !message.is_empty() { write!(out, " ").unwrap() } } MODE_PLUS => { if !message.is_empty() { add_line(&mut lines, &mut out, wrapping) } } MODE_MINUS => { if message.is_empty() { out = remove_empty_lines(&mut lines, out) } } _ => {} } for (cnt, line) in message.iter().enumerate() { if cnt < num_parts - 1 || !line.is_empty() { write!(out, "{}", line).unwrap(); add_line(&mut lines, &mut out, wrapping); } } Ok(()) } x => return Err(format!("No commit field at index {}", x)), } .unwrap(); } curr = start + len; } write!(out, "{}", &format[curr..(format.len())]).unwrap(); if !out.is_empty() { add_line(&mut lines, &mut out, wrapping); } } Ok(lines) } /// Format a commit for `CommitFormat::OneLine`. pub fn format_oneline( commit: &Commit, branches: String, wrapping: &Option, hash_color: Option, ) -> Vec { let mut out = String::new(); if let Some(color) = hash_color { write!( out, "{}", Paint::fixed(color, &commit.id().to_string()[..7]) ) } else { write!(out, "{}", &commit.id().to_string()[..7]) } .unwrap(); write!(out, "{} {}", branches, commit.summary().unwrap_or("")).unwrap(); if let Some(wrap) = wrapping { textwrap::fill(&out, wrap) .lines() .map(|str| str.to_string()) .collect() } else { vec![out] } } /// Format a commit for `CommitFormat::Short`, `CommitFormat::Medium` or `CommitFormat::Full`. pub fn format( commit: &Commit, branches: String, wrapping: &Option, hash_color: Option, format: &CommitFormat, ) -> Result, String> { match format { CommitFormat::OneLine => return Ok(format_oneline(commit, branches, wrapping, hash_color)), CommitFormat::Format(format) => { return format_commit(format, commit, branches, wrapping, hash_color) } _ => {} } let mut out_vec = vec![]; let mut out = String::new(); if let Some(color) = hash_color { write!(out, "commit {}", Paint::fixed(color, &commit.id())) } else { write!(out, "commit {}", &commit.id()) } .map_err(|err| err.to_string())?; write!(out, "{}", branches).map_err(|err| err.to_string())?; append_wrapped(&mut out_vec, out, wrapping); if commit.parent_count() > 1 { out = String::new(); write!( out, "Merge: {} {}", &commit.parent_id(0).unwrap().to_string()[..7], &commit.parent_id(1).unwrap().to_string()[..7] ) .map_err(|err| err.to_string())?; append_wrapped(&mut out_vec, out, wrapping); } out = String::new(); write!( out, "Author: {} <{}>", commit.author().name().unwrap_or(""), commit.author().email().unwrap_or("") ) .map_err(|err| err.to_string())?; append_wrapped(&mut out_vec, out, wrapping); if format > &CommitFormat::Medium { out = String::new(); write!( out, "Commit: {} <{}>", commit.committer().name().unwrap_or(""), commit.committer().email().unwrap_or("") ) .map_err(|err| err.to_string())?; append_wrapped(&mut out_vec, out, wrapping); } if format > &CommitFormat::Short { out = String::new(); write!( out, "Date: {}", format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z") ) .map_err(|err| err.to_string())?; append_wrapped(&mut out_vec, out, wrapping); } if format == &CommitFormat::Short { out_vec.push("".to_string()); append_wrapped( &mut out_vec, format!(" {}", commit.summary().unwrap_or("")), wrapping, ); out_vec.push("".to_string()); } else { out_vec.push("".to_string()); let mut add_line = true; for line in commit.message().unwrap_or("").lines() { if line.is_empty() { out_vec.push(line.to_string()); } else { append_wrapped(&mut out_vec, format!(" {}", line), wrapping); } add_line = !line.trim().is_empty(); } if add_line { out_vec.push("".to_string()); } } Ok(out_vec) } pub fn format_date(time: Time, format: &str) -> String { let date = Local::from_offset(&FixedOffset::east(time.offset_minutes())).timestamp(time.seconds(), 0); format!("{}", date.format(format)) } fn append_wrapped(vec: &mut Vec, str: String, wrapping: &Option) { if str.is_empty() { vec.push(str); } else if let Some(wrap) = wrapping { vec.extend( textwrap::fill(&str, wrap) .lines() .map(|str| str.to_string()), ) } else { vec.push(str); } } fn add_line(lines: &mut Vec, line: &mut String, wrapping: &Option) { let mut temp = String::new(); std::mem::swap(&mut temp, line); append_wrapped(lines, temp, wrapping); } fn remove_empty_lines(lines: &mut Vec, mut line: String) -> String { while !lines.is_empty() && lines.last().unwrap().is_empty() { line = lines.remove(lines.len() - 1); } if !lines.is_empty() { line = lines.remove(lines.len() - 1); } line }