use regex::Regex; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[derive(Debug, thiserror::Error)] pub enum GitError { #[error("failed to parse Unix timetamp: {0}")] ParseUnixTimestamp(String, #[source] std::num::ParseIntError), #[error("faileed to invoked git with subcommand {0} in {1}")] GitInvoke(String, PathBuf, #[source] std::io::Error), #[error("git {0} in in {1}:\n{2}")] GitError(String, PathBuf, String), } pub fn git(args: &[&str], cwd: &Path) -> Result { assert!(!args.is_empty()); let output = Command::new("git") .args(args) .current_dir(cwd) .output() .map_err(|e| GitError::GitInvoke(args[0].into(), cwd.into(), e))?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).into()) } else { let stderr = String::from_utf8_lossy(&output.stderr).into(); Err(GitError::GitError(args[0].into(), cwd.into(), stderr)) } } pub fn git_whatchanged(cwd: &Path) -> Result, GitError> { let mut files = HashMap::new(); if cwd.join(".git").is_dir() { let output = git(&["whatchanged", "--pretty=format:%ad", "--date=unix"], cwd)?; let timepat = Regex::new(r#"^(?P\d+)$"#).expect("regex compilation"); let filepat = Regex::new(r#"^:\S+ \S+ \S+ \S+ (?P\S)\t(?P\S+)$"#) .expect("regex compilation"); let mut mtime = None; for line in output.lines() { if let Some(caps) = timepat.captures(line) { let secs = caps.name("secs").unwrap().as_str(); let timestamp = secs .parse::() .map_err(|e| GitError::ParseUnixTimestamp(secs.into(), e))?; mtime = Some(UNIX_EPOCH + Duration::new(timestamp, 0)); } else if let Some(caps) = filepat.captures(line) { let flag = caps.name("flag").unwrap().as_str(); let filename = PathBuf::from(caps.name("filename").unwrap().as_str()); if (flag == "M" || flag == "A") && !files.contains_key(&filename) { assert!(mtime.is_some()); files.insert(filename, mtime.unwrap()); } } else if line.trim().is_empty() { mtime = None; } } } Ok(files) } pub fn git_dirty(cwd: &Path) -> Result, GitError> { let mut dirty = vec![]; if cwd.join(".git").is_dir() { let output = git(&["status", "--short"], cwd)?; let pat = Regex::new(r#"^.(?P.) (?P\S.+)$"#).expect("regex compilation"); for line in output.lines() { if let Some(caps) = pat.captures(line) { let status = GitStatus::from(caps.name("status").unwrap().as_str()); let filename = caps.name("filename").unwrap().as_str(); if status == GitStatus::Dirty { dirty.push(filename.into()); } } } } Ok(dirty) } #[derive(Debug, Eq, PartialEq)] pub enum GitStatus { Clean, Dirty, Unknown, } impl From<&str> for GitStatus { fn from(status: &str) -> Self { match status { "M" => Self::Dirty, "?" => Self::Unknown, _ => Self::Clean, } } }