use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::ffi::{OsStr, OsString};
use std::ops::Add;
use std::time::{Duration, SystemTime};
use cursive::theme::BaseColor;
use cursive::utils::markup::StyledString;
use lazy_static::lazy_static;
use regex::Regex;
use tracing::instrument;
use crate::core::config::{
get_commit_metadata_branches, get_commit_metadata_differential_revision,
get_commit_metadata_relative_time,
};
use crate::git::{CategorizedReferenceName, Commit, NonZeroOid, Repo};
use super::eventlog::{Event, EventCursor, EventReplayer};
use super::formatting::StyledStringBuilder;
use super::graph::CommitGraph;
use super::rewrite::find_rewrite_target;
pub trait CommitMetadataProvider {
fn describe_commit(&mut self, commit: &Commit) -> eyre::Result<Option<StyledString>>;
}
#[instrument(skip(commit_metadata_providers))]
pub fn render_commit_metadata(
commit: &Commit,
commit_metadata_providers: &mut [&mut dyn CommitMetadataProvider],
) -> eyre::Result<StyledString> {
let descriptions = commit_metadata_providers
.iter_mut()
.filter_map(|provider: &mut &mut dyn CommitMetadataProvider| {
provider.describe_commit(commit).transpose()
})
.collect::<eyre::Result<Vec<_>>>()?;
let result = StyledStringBuilder::join(" ", descriptions);
Ok(result)
}
#[derive(Debug)]
pub struct CommitOidProvider {
use_color: bool,
}
impl CommitOidProvider {
pub fn new(use_color: bool) -> eyre::Result<Self> {
Ok(CommitOidProvider { use_color })
}
}
impl CommitMetadataProvider for CommitOidProvider {
#[instrument]
fn describe_commit(&mut self, commit: &Commit) -> eyre::Result<Option<StyledString>> {
let oid = commit.get_oid();
let oid = &oid.to_string()[..8];
let oid = if self.use_color {
StyledString::styled(oid, BaseColor::Yellow.dark())
} else {
StyledString::plain(oid)
};
Ok(Some(oid))
}
}
#[derive(Debug)]
pub struct CommitMessageProvider;
impl CommitMessageProvider {
pub fn new() -> eyre::Result<Self> {
Ok(CommitMessageProvider)
}
}
impl CommitMetadataProvider for CommitMessageProvider {
#[instrument]
fn describe_commit(&mut self, commit: &Commit) -> eyre::Result<Option<StyledString>> {
Ok(Some(StyledString::plain(
commit.get_summary()?.to_string_lossy(),
)))
}
}
pub struct HiddenExplanationProvider<'a> {
graph: &'a CommitGraph<'a>,
event_replayer: &'a EventReplayer,
event_cursor: EventCursor,
}
impl<'a> HiddenExplanationProvider<'a> {
pub fn new(
graph: &'a CommitGraph,
event_replayer: &'a EventReplayer,
event_cursor: EventCursor,
) -> eyre::Result<Self> {
Ok(HiddenExplanationProvider {
graph,
event_replayer,
event_cursor,
})
}
}
impl<'a> CommitMetadataProvider for HiddenExplanationProvider<'a> {
fn describe_commit(&mut self, commit: &Commit) -> eyre::Result<Option<StyledString>> {
let event = self
.event_replayer
.get_cursor_commit_latest_event(self.event_cursor, commit.get_oid());
let event = match event {
Some(event) => event,
None => return Ok(None),
};
let result = match event {
Event::RewriteEvent { .. } => {
let rewrite_target = find_rewrite_target(
self.graph,
self.event_replayer,
self.event_cursor,
commit.get_oid(),
);
rewrite_target.map(|rewritten_oid| {
StyledString::styled(
format!("(rewritten as {})", &rewritten_oid.to_string()[..8]),
BaseColor::Black.light(),
)
})
}
Event::HideEvent { .. } => Some(StyledString::styled(
"(manually hidden)",
BaseColor::Black.light(),
)),
Event::RefUpdateEvent { .. }
| Event::CommitEvent { .. }
| Event::UnhideEvent { .. } => None,
};
Ok(result)
}
}
#[derive(Debug)]
pub struct BranchesProvider<'a> {
is_enabled: bool,
branch_oid_to_names: &'a HashMap<NonZeroOid, HashSet<OsString>>,
}
impl<'a> BranchesProvider<'a> {
pub fn new(
repo: &Repo,
branch_oid_to_names: &'a HashMap<NonZeroOid, HashSet<OsString>>,
) -> eyre::Result<Self> {
let is_enabled = get_commit_metadata_branches(repo)?;
Ok(BranchesProvider {
is_enabled,
branch_oid_to_names,
})
}
}
impl<'a> CommitMetadataProvider for BranchesProvider<'a> {
#[instrument]
fn describe_commit(&mut self, commit: &Commit) -> eyre::Result<Option<StyledString>> {
if !self.is_enabled {
return Ok(None);
}
let branch_names: HashSet<&OsStr> = match self.branch_oid_to_names.get(&commit.get_oid()) {
Some(branch_names) => branch_names
.iter()
.map(|branch_name| branch_name.as_os_str())
.collect(),
None => HashSet::new(),
};
if branch_names.is_empty() {
Ok(None)
} else {
let mut branch_names: Vec<String> = branch_names
.into_iter()
.map(
|branch_name| match CategorizedReferenceName::new(branch_name) {
reference_name @ CategorizedReferenceName::LocalBranch { .. } => {
reference_name.render_suffix()
}
reference_name @ CategorizedReferenceName::RemoteBranch { .. } => {
format!("remote {}", reference_name.render_suffix())
}
reference_name @ CategorizedReferenceName::OtherRef { .. } => {
format!("ref {}", reference_name.render_suffix())
}
},
)
.collect();
branch_names.sort_unstable();
let result = StyledString::styled(
format!("({})", branch_names.join(", ")),
BaseColor::Green.light(),
);
Ok(Some(result))
}
}
}
#[derive(Debug)]
pub struct DifferentialRevisionProvider {
is_enabled: bool,
}
impl DifferentialRevisionProvider {
pub fn new(repo: &Repo) -> eyre::Result<Self> {
let is_enabled = get_commit_metadata_differential_revision(repo)?;
Ok(DifferentialRevisionProvider { is_enabled })
}
}
fn extract_diff_number(message: &str) -> Option<String> {
lazy_static! {
static ref RE: Regex = Regex::new(
r"(?mx)
^
Differential[\ ]Revision:[\ ]
(.+ /)?
(?P<diff>D[0-9]+)
$",
)
.expect("Failed to compile DifferentialRevisionProvider regex");
}
let captures = RE.captures(message)?;
let diff_number = &captures["diff"];
Some(diff_number.to_owned())
}
impl CommitMetadataProvider for DifferentialRevisionProvider {
#[instrument]
fn describe_commit(&mut self, commit: &Commit) -> eyre::Result<Option<StyledString>> {
if !self.is_enabled {
return Ok(None);
}
let diff_number = match extract_diff_number(&commit.get_message_raw()?.to_string_lossy()) {
Some(diff_number) => diff_number,
None => return Ok(None),
};
let result = StyledString::styled(diff_number, BaseColor::Green.dark());
Ok(Some(result))
}
}
#[derive(Debug)]
pub struct RelativeTimeProvider {
is_enabled: bool,
now: SystemTime,
}
impl RelativeTimeProvider {
pub fn new(repo: &Repo, now: SystemTime) -> eyre::Result<Self> {
let is_enabled = get_commit_metadata_relative_time(repo)?;
Ok(RelativeTimeProvider { is_enabled, now })
}
pub fn is_enabled(&self) -> bool {
self.is_enabled
}
pub fn describe_time_delta(now: SystemTime, previous_time: SystemTime) -> eyre::Result<String> {
let mut delta: i64 = if previous_time < now {
let delta = now.duration_since(previous_time)?;
delta.as_secs().try_into()?
} else {
let delta = previous_time.duration_since(now)?;
-(delta.as_secs().try_into()?)
};
if delta < 60 {
return Ok(format!("{}s", delta));
}
delta /= 60;
if delta < 60 {
return Ok(format!("{}m", delta));
}
delta /= 60;
if delta < 24 {
return Ok(format!("{}h", delta));
}
delta /= 24;
if delta < 365 {
return Ok(format!("{}d", delta));
}
delta /= 365;
Ok(format!("{}y", delta))
}
}
impl CommitMetadataProvider for RelativeTimeProvider {
#[instrument]
fn describe_commit(&mut self, commit: &Commit) -> eyre::Result<Option<StyledString>> {
if !self.is_enabled {
return Ok(None);
}
let previous_time = SystemTime::UNIX_EPOCH
.add(Duration::from_secs(commit.get_time().seconds().try_into()?));
let description = Self::describe_time_delta(self.now, previous_time)?;
let result = StyledString::styled(description, BaseColor::Green.dark());
Ok(Some(result))
}
}
#[cfg(test)]
mod tests {
use std::ops::Sub;
use super::*;
#[test]
fn test_extract_diff_number() -> eyre::Result<()> {
let message = "\
This is a message
Differential Revision: D123";
assert_eq!(extract_diff_number(message), Some(String::from("D123")));
let message = "\
This is a message
Differential Revision: phabricator.com/D123";
assert_eq!(extract_diff_number(message), Some(String::from("D123")));
let message = "This is a message";
assert_eq!(extract_diff_number(message), None);
Ok(())
}
#[test]
fn test_describe_time_delta() -> eyre::Result<()> {
let test_cases: Vec<(isize, &str)> = vec![
(-100000, "-100000s"),
(-1, "-1s"),
(0, "0s"),
(10, "10s"),
(60, "1m"),
(90, "1m"),
(120, "2m"),
(135, "2m"),
(60 * 45, "45m"),
(60 * 60 - 1, "59m"),
(60 * 60, "1h"),
(60 * 60 * 24 * 3, "3d"),
(60 * 60 * 24 * 300, "300d"),
(60 * 60 * 24 * 400, "1y"),
];
for (delta, expected) in test_cases {
let now = SystemTime::now();
let previous_time = if delta < 0 {
let delta = -delta;
now.add(Duration::from_secs(delta.try_into()?))
} else {
now.sub(Duration::from_secs(delta.try_into()?))
};
let delta = RelativeTimeProvider::describe_time_delta(now, previous_time)?;
assert_eq!(delta, expected);
}
Ok(())
}
}