use std::path::Path;
use std::process::Stdio;
use async_trait::async_trait;
use tokio::process::Command;
use solid_pod_rs::error::PodError;
use solid_pod_rs::provision::GitInitHook;
#[derive(Debug, Clone)]
pub struct GitAutoInit {
pub default_branch: String,
}
impl GitAutoInit {
pub fn new() -> Self {
Self {
default_branch: "main".into(),
}
}
pub fn with_branch(branch: impl Into<String>) -> Self {
Self {
default_branch: branch.into(),
}
}
}
impl Default for GitAutoInit {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl GitInitHook for GitAutoInit {
async fn try_init_repo(&self, fs_pod_root: &Path) -> Result<(), PodError> {
let init_out = Command::new("git")
.arg("init")
.arg("-b")
.arg(&self.default_branch)
.arg(fs_pod_root)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.await
.map_err(|e| {
PodError::Backend(format!(
"git init failed to spawn (is git installed?): {e}"
))
})?;
if !init_out.status.success() {
let stderr = String::from_utf8_lossy(&init_out.stderr).into_owned();
return Err(PodError::Backend(format!(
"git init -b {} {:?} exited {:?}: {stderr}",
self.default_branch,
fs_pod_root,
init_out.status.code(),
)));
}
let cfg_out = Command::new("git")
.arg("-C")
.arg(fs_pod_root)
.arg("config")
.arg("receive.denyCurrentBranch")
.arg("updateInstead")
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.await;
match cfg_out {
Ok(o) if o.status.success() => {}
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr).into_owned();
tracing::warn!(
target: "solid_pod_rs_git::init",
path = %fs_pod_root.display(),
"git config receive.denyCurrentBranch failed (non-fatal): {stderr}",
);
}
Err(e) => {
tracing::warn!(
target: "solid_pod_rs_git::init",
"git config spawn error (non-fatal): {e}",
);
}
}
tracing::debug!(
target: "solid_pod_rs_git::init",
path = %fs_pod_root.display(),
branch = %self.default_branch,
"pod git repository initialised",
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn git_available() -> bool {
std::process::Command::new("git")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[tokio::test]
async fn git_auto_init_creates_dot_git() {
if !git_available() {
return;
}
let td = TempDir::new().unwrap();
let hook = GitAutoInit::new();
hook.try_init_repo(td.path()).await.unwrap();
assert!(td.path().join(".git").is_dir(), ".git directory must exist");
}
#[tokio::test]
async fn git_auto_init_sets_deny_current_branch() {
if !git_available() {
return;
}
let td = TempDir::new().unwrap();
let hook = GitAutoInit::new();
hook.try_init_repo(td.path()).await.unwrap();
let out = std::process::Command::new("git")
.arg("-C")
.arg(td.path())
.arg("config")
.arg("receive.denyCurrentBranch")
.output()
.unwrap();
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"updateInstead",
);
}
#[tokio::test]
async fn git_auto_init_custom_branch() {
if !git_available() {
return;
}
let td = TempDir::new().unwrap();
let hook = GitAutoInit::with_branch("trunk");
hook.try_init_repo(td.path()).await.unwrap();
let out = std::process::Command::new("git")
.arg("-C")
.arg(td.path())
.arg("symbolic-ref")
.arg("HEAD")
.output()
.unwrap();
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"refs/heads/trunk",
);
}
#[tokio::test]
async fn git_auto_init_idempotent() {
if !git_available() {
return;
}
let td = TempDir::new().unwrap();
let hook = GitAutoInit::new();
hook.try_init_repo(td.path()).await.unwrap();
hook.try_init_repo(td.path()).await.unwrap();
assert!(td.path().join(".git").is_dir());
}
}