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;
}