use std::env;
use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result};
use super::bin_override;
pub fn is_git_clean(repo_root: &Path) -> Result<bool> {
if let Ok(git_program) = env::var("SHIPPER_GIT_BIN") {
return bin_override::local_is_git_clean(repo_root, &git_program);
}
is_git_clean_default(repo_root).map_err(|err| anyhow::anyhow!("git status failed: {err}"))
}
pub(super) fn is_git_clean_default(path: &Path) -> Result<bool> {
let output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(path)
.output()
.context("failed to run git status")?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"git status failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
Ok(output.stdout.is_empty())
}
pub fn ensure_git_clean(repo_root: &Path) -> Result<()> {
if !is_git_clean(repo_root)? {
anyhow::bail!("git working tree is not clean; commit/stash changes or use --allow-dirty");
}
Ok(())
}
#[cfg(test)]
pub(super) fn ensure_git_clean_legacy(path: &Path) -> Result<()> {
if !is_git_clean_default(path)? {
return Err(anyhow::anyhow!(
"git working tree has uncommitted changes. Use --allow-dirty to bypass."
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn init_git_repo(dir: &Path) {
Command::new("git")
.args(["init"])
.current_dir(dir)
.output()
.expect("git init");
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(dir)
.output()
.expect("git config");
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(dir)
.output()
.expect("git config");
}
fn make_commit(dir: &Path, msg: &str) {
Command::new("git")
.args(["commit", "--allow-empty", "-m", msg])
.current_dir(dir)
.output()
.expect("git commit");
}
#[test]
fn is_git_clean_for_empty_repo() {
let td = tempdir().expect("tempdir");
init_git_repo(td.path());
assert!(is_git_clean_default(td.path()).unwrap_or(false));
}
#[test]
fn is_git_clean_dirty_with_untracked_file() {
let td = tempdir().expect("tempdir");
init_git_repo(td.path());
make_commit(td.path(), "initial");
fs::write(td.path().join("untracked.txt"), "hello").expect("write file");
assert!(!is_git_clean_default(td.path()).expect("git status"));
}
#[test]
fn is_git_clean_dirty_with_modified_tracked_file() {
let td = tempdir().expect("tempdir");
init_git_repo(td.path());
fs::write(td.path().join("file.txt"), "initial").expect("write file");
Command::new("git")
.args(["add", "."])
.current_dir(td.path())
.output()
.expect("git add");
make_commit(td.path(), "initial");
fs::write(td.path().join("file.txt"), "modified").expect("write file");
assert!(!is_git_clean_default(td.path()).expect("git status"));
}
#[test]
fn ensure_git_clean_ok_on_clean_repo() {
let td = tempdir().expect("tempdir");
init_git_repo(td.path());
assert!(ensure_git_clean_legacy(td.path()).is_ok());
}
#[test]
fn ensure_git_clean_errors_with_allow_dirty_hint() {
let td = tempdir().expect("tempdir");
init_git_repo(td.path());
make_commit(td.path(), "initial");
fs::write(td.path().join("dirty.txt"), "x").expect("write");
let err = ensure_git_clean_legacy(td.path()).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("--allow-dirty"));
assert!(msg.contains("uncommitted changes"));
}
#[test]
fn ensure_git_clean_new_phrasing() {
let td = tempdir().expect("tempdir");
init_git_repo(td.path());
make_commit(td.path(), "initial");
fs::write(td.path().join("dirty.txt"), "x").expect("write");
let err = ensure_git_clean(td.path()).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("--allow-dirty"));
assert!(msg.contains("not clean"));
}
}