#![warn(missing_docs)]
#![warn(rustdoc::missing_doc_code_examples)]
use anyhow::Result;
use chrono::{DateTime, Utc};
use cmd_lib::run_fun;
use serde::{Deserialize, Serialize};
use serde_json::{from_str, json};
use std::{collections::HashMap, path::PathBuf};
#[derive(Debug, Clone)]
pub struct Status {
pub error: Option<String>,
pub git_dirty: Option<bool>,
pub summary: HashMap<String, bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Commit {
#[serde(with = "my_date_format")]
pub commit_date: Option<DateTime<Utc>>,
pub commit_message: Option<String>,
pub author_name: Option<String>,
pub author_email: Option<String>,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
pub tree_hash: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Info {
pub dir: String,
pub is_git: bool,
pub branch: Option<String>,
pub status: Option<Status>,
pub commits: Option<Vec<Commit>>,
}
impl Commit {
pub fn new() -> Commit {
Commit {
commit_date: None,
commit_message: None,
author_name: None,
author_email: None,
committer_name: None,
committer_email: None,
tree_hash: None,
}
}
}
impl Info {
pub fn new(dir: &str) -> Info {
let mut project_path = PathBuf::from(dir);
project_path.push(".git");
let is_git = project_path.exists();
Info {
dir: dir.into(),
is_git: is_git,
status: None,
commits: None,
branch: None,
}
}
pub fn commit_info(&self) -> Result<Info> {
let mut git_info = self.clone();
if git_info.is_git {
let dir = &git_info.dir;
let branch = match run_fun!(
cd ${dir};
git branch -r | grep -v HEAD | head -n 1 ;
) {
Ok(resp) => {
let r = resp.clone();
r
}
_ => "".into(),
};
let branch = &branch[..];
let branch = branch.trim();
git_info.branch = Some(branch.into());
let format = format!("{{\"commit_date\":\"%ci\", \"commit_message\":\"%s\", \"author_name\":\"%an\", \"author_email\":\"%ae\", \"committer_name\":\"%cn\", \"committer_email\":\"%ce\", \"tree_hash\":\"%t\"}}");
let empty_commit = json!(Commit::new());
let commits = match run_fun!(
cd ${dir};
git log --format="$format" $branch
) {
Ok(resp) => resp,
Err(_) => {
empty_commit.to_string()
}
};
let commits = commits.split("\n").collect::<Vec<&str>>();
let len: usize = if commits.len() > 5 { 5 } else { commits.len() };
let top_commits: Vec<Commit> = commits[0..len]
.to_vec()
.iter()
.map(|s| {
let commit: Commit = match from_str(s) {
Ok(c) => c,
_ => Commit::new(),
};
commit
})
.filter(|e: &Commit| {
e.commit_date != None
})
.collect();
git_info.commits = if top_commits.len() > 0 {
Some(top_commits)
} else {
None
};
}
Ok(git_info)
}
pub fn status_info(&self) -> Result<Info> {
let mut git_info = self.clone();
let mut status = Status {
error: None,
git_dirty: None,
summary: HashMap::new(),
};
if git_info.is_git {
let dir = &git_info.dir;
match run_fun!( cd ${dir}; git status -s; ) {
Ok(resp) => {
let is_modified = resp.len() > 0;
let resp = match run_fun!( cd ${dir}; git diff --stat; ) {
Ok(r) => r,
_ => "ERR".into(),
};
let is_dirty = resp.len() > 0;
status.summary.insert("is_modified".into(), is_modified);
status.summary.insert("is_dirty".into(), is_dirty);
status.git_dirty = Some(is_dirty || is_modified);
}
Err(e) => {
status.error = Some(format!("{:?}", e));
}
};
}
git_info.status = Some(status);
Ok(git_info)
}
}
mod my_date_format {
use chrono::{DateTime, TimeZone, Utc};
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &'static str = "%Y-%m-%d %H:%M:%S %Z";
pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = match date {
Some(dt) => {
let s = format!("{}", dt.format(FORMAT));
s
}
_ => "null".into(),
};
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let dt = Utc
.datetime_from_str(&s, FORMAT)
.map_err(serde::de::Error::custom)?;
Ok(Some(dt))
}
}
#[cfg(test)]
mod tests {
use super::Info;
use std::env;
fn test_dir() -> String {
let mut path = env::current_dir().unwrap();
path.push("test_project");
path.to_string_lossy().to_string()
}
#[test]
fn it_works() {
let dir = test_dir();
let info = Info::new(&dir)
.status_info()
.expect("Unable to get status info")
.commit_info()
.expect("Unable to get commit info");
assert_eq!(None, info.commits);
assert_eq!(Some(true), info.status.expect("err").git_dirty);
}
}