diff options
Diffstat (limited to 'src/git.rs')
-rw-r--r-- | src/git.rs | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..89e84db --- /dev/null +++ b/src/git.rs @@ -0,0 +1,89 @@ +use crate::error::SiteError; +use regex::Regex; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub fn git(args: &[&str], cwd: &Path) -> Result<String, SiteError> { + assert!(!args.is_empty()); + let output = Command::new("git") + .args(args) + .current_dir(cwd) + .output() + .map_err(|e| SiteError::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(SiteError::GitError(args[0].into(), cwd.into(), stderr)) + } +} + +pub fn git_whatchanged(cwd: &Path) -> Result<HashMap<PathBuf, SystemTime>, SiteError> { + 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<secs>\d+)$"#).expect("regex compilation"); + let filepat = Regex::new(r#"^:\S+ \S+ \S+ \S+ (?P<flag>\S)\t(?P<filename>\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::<u64>() + .map_err(|e| SiteError::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<Vec<PathBuf>, SiteError> { + let mut dirty = vec![]; + if cwd.join(".git").is_dir() { + let output = git(&["status", "--short"], cwd)?; + + let pat = Regex::new(r#"^.(?P<status>.) (?P<filename>\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, + } + } +} |