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;
#[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<Self, Self::Err> {
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 AUTHOR_DATE_RELATIVE: usize = 11;
const COMMITTER: usize = 12;
const COMMITTER_EMAIL: usize = 13;
const COMMITTER_DATE: usize = 14;
const COMMITTER_DATE_SHORT: usize = 15;
const COMMITTER_DATE_RELATIVE: usize = 16;
const BODY: usize = 17;
const BODY_RAW: usize = 18;
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", "ar", "cn", "ce", "cd",
"cs", "cr", "b", "B",
];
base.iter()
.map(|b| {
[
format!("%{}", b),
format!("% {}", b),
format!("%+{}", b),
format!("%-{}", b),
]
})
.collect()
};
}
pub fn format_commit(
format: &str,
commit: &Commit,
branches: String,
wrapping: &Option<Options>,
hash_color: Option<u8>,
) -> Result<Vec<String>, 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..].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();
let mut formatter = CommitFieldFormatter::new(
commit, &branches, hash_color, wrapping, &mut out, &mut lines,
);
let mut curr = 0;
for &(start, len, idx, mode) in &replacements {
formatter.out.push_str(&format[curr..start]);
formatter.format_field(idx, mode)?;
curr = start + len;
}
formatter.out.push_str(&format[curr..]);
formatter.finalize_tail();
Ok(lines)
}
struct CommitFieldFormatter<'a> {
commit: &'a Commit<'a>,
branches: &'a str,
hash_color: Option<u8>,
wrapping: &'a Option<Options<'a>>,
out: &'a mut String,
lines: &'a mut Vec<String>,
}
impl<'a> CommitFieldFormatter<'a> {
fn new(
commit: &'a Commit,
branches: &'a str,
hash_color: Option<u8>,
wrapping: &'a Option<Options>,
out: &'a mut String,
lines: &'a mut Vec<String>,
) -> Self {
Self {
commit,
branches,
hash_color,
wrapping,
out,
lines,
}
}
fn handle_mode(&mut self, mode: usize, field_has_content: bool) {
match mode {
MODE_SPACE => {
if field_has_content {
self.out.push(' ');
}
}
MODE_PLUS => {
if field_has_content {
self.add_line();
}
}
MODE_MINUS => {
if !field_has_content {
*self.out = remove_empty_lines(self.lines, self.out.clone());
}
}
_ => {}
}
}
pub fn format_field(&mut self, idx: usize, mode: usize) -> Result<(), String> {
match idx {
NEW_LINE => self.add_line(),
HASH => self.format_hash(mode),
HASH_ABBREV => self.format_hash_abbrev(mode),
PARENT_HASHES => self.format_parent_hashes(mode)?,
PARENT_HASHES_ABBREV => self.format_parent_hashes_abbrev(mode)?,
REFS => self.format_refs(mode),
SUBJECT => self.format_subject(mode)?,
AUTHOR => self.format_author(mode),
AUTHOR_EMAIL => self.format_author_email(mode),
AUTHOR_DATE => self.format_author_date(mode),
AUTHOR_DATE_SHORT => self.format_author_date_short(mode),
AUTHOR_DATE_RELATIVE => self.format_author_date_relative(mode),
COMMITTER => self.format_committer(mode),
COMMITTER_EMAIL => self.format_committer_email(mode),
COMMITTER_DATE => self.format_committer_date(mode),
COMMITTER_DATE_SHORT => self.format_committer_date_short(mode),
COMMITTER_DATE_RELATIVE => self.format_committer_date_relative(mode),
BODY => self.format_body(mode)?,
BODY_RAW => self.format_body_raw(mode)?,
x => return Err(format!("No commit field at index {}", x)),
}
Ok(())
}
fn add_line(&mut self) {
if !self.out.is_empty() {
add_line(self.lines, self.out, self.wrapping);
}
}
pub fn finalize_tail(&mut self) {
self.add_line();
}
fn format_hash(&mut self, mode: usize) {
self.handle_mode(mode, true);
let id = self.commit.id();
if let Some(color) = self.hash_color {
self.out.push_str(&id.to_string().fixed(color).to_string());
} else {
self.out.push_str(&id.to_string());
}
}
fn format_hash_abbrev(&mut self, mode: usize) {
self.handle_mode(mode, true);
let id = self.commit.id().to_string();
let s = &id[..7];
if let Some(color) = self.hash_color {
self.out.push_str(&s.fixed(color).to_string());
} else {
self.out.push_str(s);
}
}
fn format_parent_hashes(&mut self, mode: usize) -> Result<(), String> {
self.handle_mode(mode, true);
for i in 0..self.commit.parent_count() {
self.out
.push_str(&self.commit.parent_id(i).unwrap().to_string());
if i < self.commit.parent_count() - 1 {
self.out.push(' ');
}
}
Ok(())
}
fn format_parent_hashes_abbrev(&mut self, mode: usize) -> Result<(), String> {
self.handle_mode(mode, true);
for i in 0..self.commit.parent_count() {
let parent_id = self.commit.parent_id(i).map_err(|err| err.to_string())?;
self.out.push_str(&parent_id.to_string()[..7]);
if i < self.commit.parent_count() - 1 {
self.out.push(' ');
}
}
Ok(())
}
fn format_refs(&mut self, mode: usize) {
self.handle_mode(mode, !self.branches.is_empty());
self.out.push_str(self.branches);
}
fn format_subject(&mut self, mode: usize) -> Result<(), String> {
let summary = self.commit.summary().unwrap_or("");
self.handle_mode(mode, !summary.is_empty());
self.out.push_str(summary);
Ok(())
}
fn format_author(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out.push_str(self.commit.author().name().unwrap_or(""));
}
fn format_author_email(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out
.push_str(self.commit.author().email().unwrap_or(""));
}
fn format_author_date(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out.push_str(&format_date(
self.commit.author().when(),
"%a %b %e %H:%M:%S %Y %z",
));
}
fn format_author_date_short(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out
.push_str(&format_date(self.commit.author().when(), "%F"));
}
fn format_author_date_relative(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out
.push_str(&format_relative_time(self.commit.author().when()));
}
fn format_committer(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out
.push_str(self.commit.committer().name().unwrap_or(""));
}
fn format_committer_email(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out
.push_str(self.commit.committer().email().unwrap_or(""));
}
fn format_committer_date(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out.push_str(&format_date(
self.commit.committer().when(),
"%a %b %e %H:%M:%S %Y %z",
));
}
fn format_committer_date_short(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out
.push_str(&format_date(self.commit.committer().when(), "%F"));
}
fn format_committer_date_relative(&mut self, mode: usize) {
self.handle_mode(mode, true);
self.out
.push_str(&format_relative_time(self.commit.committer().when()));
}
fn format_body(&mut self, mode: usize) -> Result<(), String> {
let message = self
.commit
.message()
.unwrap_or("")
.lines()
.collect::<Vec<&str>>();
let num_parts = message.len();
self.handle_mode(mode, num_parts > 2);
for (cnt, line) in message.iter().enumerate() {
if cnt > 1 && (cnt < num_parts - 1 || !line.is_empty()) {
self.out.push_str(line);
self.add_line();
}
}
Ok(())
}
fn format_body_raw(&mut self, mode: usize) -> Result<(), String> {
let message = self
.commit
.message()
.unwrap_or("")
.lines()
.collect::<Vec<&str>>();
let num_parts = message.len();
self.handle_mode(mode, !message.is_empty());
for (cnt, line) in message.iter().enumerate() {
if cnt < num_parts - 1 || !line.is_empty() {
self.out.push_str(line);
self.add_line();
}
}
Ok(())
}
}
pub fn format_oneline(
commit: &Commit,
branches: String,
wrapping: &Option<Options>,
hash_color: Option<u8>,
) -> Vec<String> {
let mut out = String::new();
let id = commit.id();
if let Some(color) = hash_color {
write!(out, "{}", id.to_string()[..7].fixed(color))
} else {
write!(out, "{}", &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]
}
}
pub fn format_commit_metadata(
commit: &Commit,
branches: String,
wrapping: &Option<Options>,
hash_color: Option<u8>,
format: &CommitFormat,
) -> Result<Vec<String>, 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();
let id = commit.id();
if let Some(color) = hash_color {
write!(out, "commit {}", id.to_string().fixed(color))
} else {
write!(out, "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 offset = FixedOffset::east_opt(time.offset_minutes() * 60).expect("Invalid offset minutes");
let date = offset
.timestamp_opt(time.seconds(), 0)
.single()
.expect("Invalid timestamp, maybe a fold or gap in local time");
date.format(format).to_string()
}
pub fn format_relative_time(time: Time) -> String {
let offset = FixedOffset::east_opt(time.offset_minutes() * 60).expect("Invalid offset minutes");
let commit_time = Local::from_offset(&offset)
.timestamp_opt(time.seconds(), 0)
.single()
.expect("Invalid timestamp");
let now = Local::now();
let duration = now.signed_duration_since(commit_time);
let seconds = duration.num_seconds();
let minutes = duration.num_minutes();
let hours = duration.num_hours();
let days = duration.num_hours() / 24;
let weeks = days / 7;
let months = days / 30;
let years = days / 365;
if seconds < 60 {
format!("{} seconds ago", seconds)
} else if minutes < 60 {
format!("{} minutes ago", minutes)
} else if hours < 24 {
format!("{} hours ago", hours)
} else if days < 7 {
format!("{} days ago", days)
} else if weeks < 4 {
format!("{} weeks ago", weeks)
} else if months < 12 {
format!("{} months ago", months)
} else {
format!("{} years ago", years)
}
}
fn append_wrapped(vec: &mut Vec<String>, str: String, wrapping: &Option<Options>) {
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<String>, line: &mut String, wrapping: &Option<Options>) {
let mut temp = String::new();
std::mem::swap(&mut temp, line);
append_wrapped(lines, temp, wrapping);
}
fn remove_empty_lines(lines: &mut Vec<String>, 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
}