upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/git/subprocess.rs
blob: 37fa38255ca4c6fd4e927fba4884449850723dec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Git Subprocess Management
//!
//! This module provides utilities for spawning and managing Git subprocesses
//! for upload-pack and receive-pack operations.

use std::path::Path;
use std::process::Stdio;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::process::{Child, Command};

use super::protocol::GitService;

/// Git subprocess wrapper
pub struct GitSubprocess {
    child: Child,
}

impl GitSubprocess {
    /// Spawn a git subprocess for the given service and repository path
    ///
    /// # Arguments
    /// * `service` - The Git service (upload-pack or receive-pack)
    /// * `repo_path` - Path to the bare Git repository
    /// * `advertise` - If true, run with --advertise-refs flag
    /// * `git_protocol` - Optional Git protocol version (e.g., "version=2")
    pub fn spawn(
        service: GitService,
        repo_path: impl AsRef<Path>,
        advertise: bool,
        git_protocol: Option<&str>,
    ) -> std::io::Result<Self> {
        let repo_path = repo_path.as_ref();

        let mut cmd = Command::new("git");

        // GRASP-01 requirement: MUST include `allow-reachable-sha1-in-want`,
        // `allow-tip-sha1-in-want`, and `uploadpack.allowFilter` in advertisement
        // and serve available oids and filtered requests.
        // These config options must be passed before the command name.
        cmd.arg("-c");
        cmd.arg("uploadpack.allowReachableSHA1InWant=true");
        cmd.arg("-c");
        cmd.arg("uploadpack.allowTipSHA1InWant=true");
        cmd.arg("-c");
        cmd.arg("uploadpack.allowFilter=true");

        cmd.arg(service.command_name());

        if advertise {
            cmd.arg("--advertise-refs");
        }

        cmd.arg("--stateless-rpc");
        cmd.arg(repo_path);

        cmd.stdin(Stdio::piped());
        cmd.stdout(Stdio::piped());
        cmd.stderr(Stdio::piped());

        // Set GIT_PROTOCOL environment variable if provided
        // This enables Git protocol v2 support for modern git clients
        if let Some(protocol) = git_protocol {
            cmd.env("GIT_PROTOCOL", protocol);
        }

        let child = cmd.spawn()?;

        Ok(Self { child })
    }

    /// Get a mutable reference to stdin
    pub fn stdin(&mut self) -> Option<&mut (impl AsyncWrite + Unpin)> {
        self.child.stdin.as_mut()
    }

    /// Get a mutable reference to stdout
    pub fn stdout(&mut self) -> Option<&mut (impl AsyncRead + Unpin)> {
        self.child.stdout.as_mut()
    }

    /// Get a mutable reference to stderr
    pub fn stderr(&mut self) -> Option<&mut (impl AsyncRead + Unpin)> {
        self.child.stderr.as_mut()
    }

    /// Take ownership of stdin
    pub fn take_stdin(&mut self) -> Option<impl AsyncWrite> {
        self.child.stdin.take()
    }

    /// Take ownership of stdout
    pub fn take_stdout(&mut self) -> Option<impl AsyncRead> {
        self.child.stdout.take()
    }

    /// Take ownership of stderr  
    pub fn take_stderr(&mut self) -> Option<impl AsyncRead> {
        self.child.stderr.take()
    }

    /// Wait for the subprocess to complete
    pub async fn wait(mut self) -> std::io::Result<std::process::ExitStatus> {
        self.child.wait().await
    }

    /// Kill the subprocess
    pub async fn kill(&mut self) -> std::io::Result<()> {
        self.child.kill().await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::process::Command as StdCommand;
    use tempfile::TempDir;

    fn create_bare_repo() -> TempDir {
        let dir = TempDir::new().unwrap();
        let status = StdCommand::new("git")
            .args(["init", "--bare"])
            .arg(dir.path())
            .status()
            .expect("Failed to run git init");
        assert!(status.success());
        dir
    }

    #[tokio::test]
    async fn test_spawn_upload_pack_advertise() {
        let repo = create_bare_repo();
        let mut proc = GitSubprocess::spawn(GitService::UploadPack, repo.path(), true, None)
            .expect("Failed to spawn git");

        // Should have spawned successfully
        assert!(proc.stdout().is_some());
        assert!(proc.stdin().is_some());

        // Clean up
        let _ = proc.kill().await;
    }

    #[tokio::test]
    async fn test_spawn_receive_pack() {
        let repo = create_bare_repo();
        let mut proc = GitSubprocess::spawn(GitService::ReceivePack, repo.path(), false, None)
            .expect("Failed to spawn git");

        assert!(proc.stdout().is_some());
        assert!(proc.stdin().is_some());

        let _ = proc.kill().await;
    }
}