summaryrefslogtreecommitdiff
path: root/src/git.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/git.rs')
-rw-r--r--src/git.rs89
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,
+ }
+ }
+}