use std::path::PathBuf;
use std::process::{Command, Output};
use crate::error::VcsError;
use crate::vcs::{Vcs, VcsType};
pub struct GitVcs {
root: PathBuf,
}
impl GitVcs {
pub fn new(root: PathBuf) -> Self {
Self { root }
}
fn run_git(&self, args: &[&str]) -> Result<Output, VcsError> {
Command::new("git")
.args(args)
.current_dir(&self.root)
.output()
.map_err(|e| VcsError::CommandFailed {
command: format!("git {}", args.join(" ")),
error: e.to_string(),
})
}
}
impl Vcs for GitVcs {
fn vcs_type(&self) -> VcsType {
VcsType::Git
}
fn has_changes(&self) -> Result<bool, VcsError> {
let output = self.run_git(&["status", "--porcelain"])?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(VcsError::CommandFailed {
command: "git status --porcelain".to_string(),
error: stderr.to_string(),
});
}
Ok(!output.stdout.is_empty())
}
fn stage_all(&self) -> Result<(), VcsError> {
let output = self.run_git(&["add", "-A"])?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(VcsError::CommandFailed {
command: "git add -A".to_string(),
error: stderr.to_string(),
});
}
Ok(())
}
fn commit(&self, message: &str) -> Result<String, VcsError> {
let output = self.run_git(&["commit", "-m", message, "--no-verify"])?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("nothing to commit") || stderr.contains("nothing to commit") {
return Err(VcsError::NothingToCommit);
}
return Err(VcsError::CommitFailed(stderr.to_string()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let hash = stdout
.lines()
.next()
.and_then(|line| {
let start = line.find('[')? + 1;
let end = line.find(']')?;
let bracket_content = &line[start..end];
bracket_content.split_whitespace().last()
})
.unwrap_or("unknown")
.to_string();
Ok(hash)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vcs::Vcs;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn setup_git_repo() -> (TempDir, GitVcs) {
let dir = TempDir::new().expect("temp dir");
Command::new("git")
.args(["init"])
.current_dir(dir.path())
.output()
.expect("git init");
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(dir.path())
.output()
.expect("git config email");
Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(dir.path())
.output()
.expect("git config name");
let vcs = GitVcs::new(dir.path().to_path_buf());
(dir, vcs)
}
#[test]
fn test_git_has_no_changes_in_clean_repo() {
let (_dir, vcs) = setup_git_repo();
let has_changes = vcs.has_changes().expect("has_changes");
assert!(!has_changes);
}
#[test]
fn test_git_has_changes_with_new_file() {
let (dir, vcs) = setup_git_repo();
fs::write(dir.path().join("test.txt"), "hello").expect("write");
assert!(vcs.has_changes().expect("has_changes"));
}
#[test]
fn test_git_commit_all() {
let (dir, vcs) = setup_git_repo();
fs::write(dir.path().join("test.txt"), "hello").expect("write");
let result = vcs.commit_all("Test commit");
assert!(result.is_ok());
let hash = result.unwrap();
assert!(hash.is_some()); }
#[test]
fn test_git_commit_all_no_changes() {
let (dir, vcs) = setup_git_repo();
fs::write(dir.path().join("test.txt"), "hello").expect("write");
vcs.commit_all("Initial").expect("initial commit");
let result = vcs.commit_all("No changes");
assert!(result.is_ok());
assert!(result.unwrap().is_none()); }
}