Skip to main content

solid_pod_rs_git/
init.rs

1//! Git repository auto-initialisation at pod provisioning time.
2//!
3//! Implements the [`solid_pod_rs::provision::GitInitHook`] trait
4//! (feature `git-auto-init` on `solid-pod-rs`) via
5//! `tokio::process::Command`.
6//!
7//! ## Parity
8//!
9//! Mirrors JSS `src/handlers/git.js` `tryAutoInitRepo` (issues #466,
10//! #469, #471). JSS runs:
11//!
12//! ```text
13//! git init -b main <pod_path>
14//! git -C <pod_path> config receive.denyCurrentBranch updateInstead
15//! ```
16//!
17//! We do the same. Errors are logged and swallowed so a missing `git`
18//! binary (or a CF Workers–style environment) does not fail pod
19//! provisioning.
20
21use 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/// Runs `git init -b <branch>` + `git config receive.denyCurrentBranch
31/// updateInstead` in the pod directory at provisioning time.
32///
33/// Mirrors JSS `tryAutoInitRepo` (#466/#469/#471).
34///
35/// # Usage
36///
37/// ```ignore
38/// use std::path::PathBuf;
39/// use solid_pod_rs_git::init::GitAutoInit;
40/// use solid_pod_rs::provision::{provision_pod_ext, ProvisionPlan};
41///
42/// // storage is any type implementing solid_pod_rs::storage::Storage
43/// // (e.g. FsBackend with feature "fs-backend").
44/// # async fn run(storage: impl solid_pod_rs::storage::Storage) {
45/// let plan = ProvisionPlan {
46///     pubkey: "abcd1234".into(),
47///     display_name: None,
48///     pod_base: "https://pods.example".into(),
49///     containers: vec![],
50///     root_acl: None,
51///     quota_bytes: None,
52/// };
53/// let hook = GitAutoInit::new();
54/// let fs_root = PathBuf::from("/var/lib/pods/abcd1234");
55/// provision_pod_ext(&storage, &plan, Some((&hook, fs_root.as_path()))).await.unwrap();
56/// # }
57/// ```
58#[derive(Debug, Clone)]
59pub struct GitAutoInit {
60    /// Branch name passed to `git init -b`. JSS uses `main`; configurable
61    /// here so agentbox / VisionClaw can override to `trunk` etc. if
62    /// desired.
63    pub default_branch: String,
64}
65
66impl GitAutoInit {
67    /// Create with `main` as the default branch — the JSS default.
68    pub fn new() -> Self {
69        Self {
70            default_branch: "main".into(),
71        }
72    }
73
74    /// Override the initial branch name.
75    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        // ── Step 1: git init -b <branch> <path> ────────────────────────────
92        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        // ── Step 2: git config receive.denyCurrentBranch updateInstead ─────
118        // Allows HTTP push to a non-bare repo (the checked-out branch gets
119        // updated in-place). Matches JSS lines 147-149. Best-effort —
120        // swallow errors so the pod is not rolled back.
121        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// ---------------------------------------------------------------------------
162// Tests
163// ---------------------------------------------------------------------------
164
165#[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        // Second call must not fail (git init on an existing repo is safe).
244        hook.try_init_repo(td.path()).await.unwrap();
245        assert!(td.path().join(".git").is_dir());
246    }
247}