1use std::path::Path;
22use std::process::Stdio;
23
24use async_trait::async_trait;
25use tokio::process::Command;
26
27use solid_pod_rs::error::PodError;
28use solid_pod_rs::provision::GitInitHook;
29
30#[derive(Debug, Clone)]
59pub struct GitAutoInit {
60 pub default_branch: String,
64}
65
66impl GitAutoInit {
67 pub fn new() -> Self {
69 Self {
70 default_branch: "main".into(),
71 }
72 }
73
74 pub fn with_branch(branch: impl Into<String>) -> Self {
76 Self {
77 default_branch: branch.into(),
78 }
79 }
80}
81
82impl Default for GitAutoInit {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88#[async_trait]
89impl GitInitHook for GitAutoInit {
90 async fn try_init_repo(&self, fs_pod_root: &Path) -> Result<(), PodError> {
91 let init_out = Command::new("git")
93 .arg("init")
94 .arg("-b")
95 .arg(&self.default_branch)
96 .arg(fs_pod_root)
97 .stdout(Stdio::null())
98 .stderr(Stdio::piped())
99 .output()
100 .await
101 .map_err(|e| {
102 PodError::Backend(format!(
103 "git init failed to spawn (is git installed?): {e}"
104 ))
105 })?;
106
107 if !init_out.status.success() {
108 let stderr = String::from_utf8_lossy(&init_out.stderr).into_owned();
109 return Err(PodError::Backend(format!(
110 "git init -b {} {:?} exited {:?}: {stderr}",
111 self.default_branch,
112 fs_pod_root,
113 init_out.status.code(),
114 )));
115 }
116
117 let cfg_out = Command::new("git")
122 .arg("-C")
123 .arg(fs_pod_root)
124 .arg("config")
125 .arg("receive.denyCurrentBranch")
126 .arg("updateInstead")
127 .stdout(Stdio::null())
128 .stderr(Stdio::piped())
129 .output()
130 .await;
131
132 match cfg_out {
133 Ok(o) if o.status.success() => {}
134 Ok(o) => {
135 let stderr = String::from_utf8_lossy(&o.stderr).into_owned();
136 tracing::warn!(
137 target: "solid_pod_rs_git::init",
138 path = %fs_pod_root.display(),
139 "git config receive.denyCurrentBranch failed (non-fatal): {stderr}",
140 );
141 }
142 Err(e) => {
143 tracing::warn!(
144 target: "solid_pod_rs_git::init",
145 "git config spawn error (non-fatal): {e}",
146 );
147 }
148 }
149
150 tracing::debug!(
151 target: "solid_pod_rs_git::init",
152 path = %fs_pod_root.display(),
153 branch = %self.default_branch,
154 "pod git repository initialised",
155 );
156
157 Ok(())
158 }
159}
160
161#[cfg(test)]
166mod tests {
167 use super::*;
168 use tempfile::TempDir;
169
170 fn git_available() -> bool {
171 std::process::Command::new("git")
172 .arg("--version")
173 .stdout(Stdio::null())
174 .stderr(Stdio::null())
175 .status()
176 .map(|s| s.success())
177 .unwrap_or(false)
178 }
179
180 #[tokio::test]
181 async fn git_auto_init_creates_dot_git() {
182 if !git_available() {
183 return;
184 }
185 let td = TempDir::new().unwrap();
186 let hook = GitAutoInit::new();
187 hook.try_init_repo(td.path()).await.unwrap();
188 assert!(td.path().join(".git").is_dir(), ".git directory must exist");
189 }
190
191 #[tokio::test]
192 async fn git_auto_init_sets_deny_current_branch() {
193 if !git_available() {
194 return;
195 }
196 let td = TempDir::new().unwrap();
197 let hook = GitAutoInit::new();
198 hook.try_init_repo(td.path()).await.unwrap();
199
200 let out = std::process::Command::new("git")
201 .arg("-C")
202 .arg(td.path())
203 .arg("config")
204 .arg("receive.denyCurrentBranch")
205 .output()
206 .unwrap();
207 assert_eq!(
208 String::from_utf8_lossy(&out.stdout).trim(),
209 "updateInstead",
210 );
211 }
212
213 #[tokio::test]
214 async fn git_auto_init_custom_branch() {
215 if !git_available() {
216 return;
217 }
218 let td = TempDir::new().unwrap();
219 let hook = GitAutoInit::with_branch("trunk");
220 hook.try_init_repo(td.path()).await.unwrap();
221
222 let out = std::process::Command::new("git")
223 .arg("-C")
224 .arg(td.path())
225 .arg("symbolic-ref")
226 .arg("HEAD")
227 .output()
228 .unwrap();
229 assert_eq!(
230 String::from_utf8_lossy(&out.stdout).trim(),
231 "refs/heads/trunk",
232 );
233 }
234
235 #[tokio::test]
236 async fn git_auto_init_idempotent() {
237 if !git_available() {
238 return;
239 }
240 let td = TempDir::new().unwrap();
241 let hook = GitAutoInit::new();
242 hook.try_init_repo(td.path()).await.unwrap();
243 hook.try_init_repo(td.path()).await.unwrap();
245 assert!(td.path().join(".git").is_dir());
246 }
247}