use std::path::Path; use tokio::process::Command; /// Create a git Command isolated from parent git environment. /// Prevents interference when tests run inside git hooks /// (e.g., pre-commit hook running `cargo test`). fn git_cmd() -> Command { let mut cmd = Command::new("git"); cmd.env_remove("GIT_DIR") .env_remove("GIT_WORK_TREE") .env_remove("GIT_INDEX_FILE"); cmd } /// Check if git is available on this system. pub fn is_git_available() -> bool { std::process::Command::new("git") .arg("--version") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } /// Create a local bare git repository with an initial commit. /// Returns a `file://` URL usable by `git clone --depth 1`. pub async fn create_local_repo(parent_dir: &Path, branch: &str) -> String { let bare_repo = parent_dir.join("origin.git"); tokio::fs::create_dir_all(&bare_repo).await.unwrap(); // Init bare repo let output = git_cmd() .args(["init", "--bare", "--initial-branch", branch]) .current_dir(&bare_repo) .output() .await .unwrap(); assert!(output.status.success(), "git init --bare failed"); // Create working copy for initial commit let work_dir = parent_dir.join("work"); let output = git_cmd() .args([ "clone", bare_repo.to_str().unwrap(), work_dir.to_str().unwrap(), ]) .output() .await .unwrap(); assert!( output.status.success(), "git clone failed: {}", String::from_utf8_lossy(&output.stderr) ); // Configure git user for args in [ &["config", "user.email", "test@test.local"][..], &["config", "user.name", "Test"], ] { let out = git_cmd() .args(args) .current_dir(&work_dir) .output() .await .unwrap(); assert!(out.status.success()); } // Checkout target branch let output = git_cmd() .args(["checkout", "-B", branch]) .current_dir(&work_dir) .output() .await .unwrap(); assert!(output.status.success(), "git checkout failed"); // Create witryna.yaml + initial content tokio::fs::write( work_dir.join("witryna.yaml"), "image: alpine:latest\ncommand: \"mkdir -p out && echo '

test

' > out/index.html\"\npublic: out\n", ) .await .unwrap(); tokio::fs::create_dir_all(work_dir.join("out")) .await .unwrap(); tokio::fs::write(work_dir.join("out/index.html"), "

initial

") .await .unwrap(); // Stage and commit let output = git_cmd() .args(["add", "-A"]) .current_dir(&work_dir) .output() .await .unwrap(); assert!(output.status.success(), "git add failed"); let output = git_cmd() .args(["commit", "-m", "Initial commit"]) .current_dir(&work_dir) .output() .await .unwrap(); assert!( output.status.success(), "git commit failed: {}", String::from_utf8_lossy(&output.stderr) ); // Push let output = git_cmd() .args(["push", "-u", "origin", branch]) .current_dir(&work_dir) .output() .await .unwrap(); assert!( output.status.success(), "git push failed: {}", String::from_utf8_lossy(&output.stderr) ); // Cleanup working copy let _ = tokio::fs::remove_dir_all(&work_dir).await; format!("file://{}", bare_repo.to_str().unwrap()) } /// Create a local bare repo without a witryna.yaml (for override-only tests). pub async fn create_bare_repo(parent_dir: &Path, branch: &str) -> String { let bare_repo = parent_dir.join("bare-origin.git"); tokio::fs::create_dir_all(&bare_repo).await.unwrap(); let output = git_cmd() .args(["init", "--bare", "--initial-branch", branch]) .current_dir(&bare_repo) .output() .await .unwrap(); assert!(output.status.success()); let work_dir = parent_dir.join("bare-work"); let output = git_cmd() .args([ "clone", bare_repo.to_str().unwrap(), work_dir.to_str().unwrap(), ]) .output() .await .unwrap(); assert!(output.status.success()); for args in [ &["config", "user.email", "test@test.local"][..], &["config", "user.name", "Test"], ] { git_cmd() .args(args) .current_dir(&work_dir) .output() .await .unwrap(); } let output = git_cmd() .args(["checkout", "-B", branch]) .current_dir(&work_dir) .output() .await .unwrap(); assert!(output.status.success()); tokio::fs::write(work_dir.join("README.md"), "# Test\n") .await .unwrap(); git_cmd() .args(["add", "-A"]) .current_dir(&work_dir) .output() .await .unwrap(); let output = git_cmd() .args(["commit", "-m", "Initial commit"]) .current_dir(&work_dir) .output() .await .unwrap(); assert!(output.status.success()); let output = git_cmd() .args(["push", "-u", "origin", branch]) .current_dir(&work_dir) .output() .await .unwrap(); assert!(output.status.success()); let _ = tokio::fs::remove_dir_all(&work_dir).await; format!("file://{}", bare_repo.to_str().unwrap()) } /// Push a new commit to a bare repo (clone, commit, push). pub async fn push_new_commit(bare_repo_url: &str, parent_dir: &Path, branch: &str) { let work_dir = parent_dir.join("push-work"); let _ = tokio::fs::remove_dir_all(&work_dir).await; let output = git_cmd() .args([ "clone", "--branch", branch, bare_repo_url, work_dir.to_str().unwrap(), ]) .output() .await .unwrap(); assert!(output.status.success(), "clone for push failed"); for args in [ &["config", "user.email", "test@test.local"][..], &["config", "user.name", "Test"], ] { git_cmd() .args(args) .current_dir(&work_dir) .output() .await .unwrap(); } let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); tokio::fs::write(work_dir.join("update.txt"), format!("update-{timestamp}")) .await .unwrap(); git_cmd() .args(["add", "-A"]) .current_dir(&work_dir) .output() .await .unwrap(); let output = git_cmd() .args(["commit", "-m", "Test update"]) .current_dir(&work_dir) .output() .await .unwrap(); assert!(output.status.success(), "commit failed"); let output = git_cmd() .args(["push", "origin", branch]) .current_dir(&work_dir) .output() .await .unwrap(); assert!(output.status.success(), "push failed"); let _ = tokio::fs::remove_dir_all(&work_dir).await; }