diff options
Diffstat (limited to 'src/git/subprocess.rs')
| -rw-r--r-- | src/git/subprocess.rs | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/src/git/subprocess.rs b/src/git/subprocess.rs new file mode 100644 index 0000000..96eed26 --- /dev/null +++ b/src/git/subprocess.rs | |||
| @@ -0,0 +1,140 @@ | |||
| 1 | //! Git Subprocess Management | ||
| 2 | //! | ||
| 3 | //! This module provides utilities for spawning and managing Git subprocesses | ||
| 4 | //! for upload-pack and receive-pack operations. | ||
| 5 | |||
| 6 | use std::path::Path; | ||
| 7 | use std::process::Stdio; | ||
| 8 | use tokio::io::{AsyncRead, AsyncWrite}; | ||
| 9 | use tokio::process::{Child, Command}; | ||
| 10 | |||
| 11 | use super::protocol::GitService; | ||
| 12 | |||
| 13 | /// Git subprocess wrapper | ||
| 14 | pub struct GitSubprocess { | ||
| 15 | child: Child, | ||
| 16 | } | ||
| 17 | |||
| 18 | impl GitSubprocess { | ||
| 19 | /// Spawn a git subprocess for the given service and repository path | ||
| 20 | /// | ||
| 21 | /// # Arguments | ||
| 22 | /// * `service` - The Git service (upload-pack or receive-pack) | ||
| 23 | /// * `repo_path` - Path to the bare Git repository | ||
| 24 | /// * `advertise` - If true, run with --advertise-refs flag | ||
| 25 | pub fn spawn( | ||
| 26 | service: GitService, | ||
| 27 | repo_path: impl AsRef<Path>, | ||
| 28 | advertise: bool, | ||
| 29 | ) -> std::io::Result<Self> { | ||
| 30 | let repo_path = repo_path.as_ref(); | ||
| 31 | |||
| 32 | let mut cmd = Command::new("git"); | ||
| 33 | cmd.arg(service.as_str()); | ||
| 34 | |||
| 35 | if advertise { | ||
| 36 | cmd.arg("--advertise-refs"); | ||
| 37 | } | ||
| 38 | |||
| 39 | cmd.arg("--stateless-rpc"); | ||
| 40 | cmd.arg(repo_path); | ||
| 41 | |||
| 42 | cmd.stdin(Stdio::piped()); | ||
| 43 | cmd.stdout(Stdio::piped()); | ||
| 44 | cmd.stderr(Stdio::piped()); | ||
| 45 | |||
| 46 | let child = cmd.spawn()?; | ||
| 47 | |||
| 48 | Ok(Self { child }) | ||
| 49 | } | ||
| 50 | |||
| 51 | /// Get a mutable reference to stdin | ||
| 52 | pub fn stdin(&mut self) -> Option<&mut (impl AsyncWrite + Unpin)> { | ||
| 53 | self.child.stdin.as_mut() | ||
| 54 | } | ||
| 55 | |||
| 56 | /// Get a mutable reference to stdout | ||
| 57 | pub fn stdout(&mut self) -> Option<&mut (impl AsyncRead + Unpin)> { | ||
| 58 | self.child.stdout.as_mut() | ||
| 59 | } | ||
| 60 | |||
| 61 | /// Get a mutable reference to stderr | ||
| 62 | pub fn stderr(&mut self) -> Option<&mut (impl AsyncRead + Unpin)> { | ||
| 63 | self.child.stderr.as_mut() | ||
| 64 | } | ||
| 65 | |||
| 66 | /// Take ownership of stdin | ||
| 67 | pub fn take_stdin(&mut self) -> Option<impl AsyncWrite> { | ||
| 68 | self.child.stdin.take() | ||
| 69 | } | ||
| 70 | |||
| 71 | /// Take ownership of stdout | ||
| 72 | pub fn take_stdout(&mut self) -> Option<impl AsyncRead> { | ||
| 73 | self.child.stdout.take() | ||
| 74 | } | ||
| 75 | |||
| 76 | /// Take ownership of stderr | ||
| 77 | pub fn take_stderr(&mut self) -> Option<impl AsyncRead> { | ||
| 78 | self.child.stderr.take() | ||
| 79 | } | ||
| 80 | |||
| 81 | /// Wait for the subprocess to complete | ||
| 82 | pub async fn wait(mut self) -> std::io::Result<std::process::ExitStatus> { | ||
| 83 | self.child.wait().await | ||
| 84 | } | ||
| 85 | |||
| 86 | /// Kill the subprocess | ||
| 87 | pub async fn kill(&mut self) -> std::io::Result<()> { | ||
| 88 | self.child.kill().await | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | #[cfg(test)] | ||
| 93 | mod tests { | ||
| 94 | use super::*; | ||
| 95 | use tempfile::TempDir; | ||
| 96 | use std::process::Command as StdCommand; | ||
| 97 | |||
| 98 | fn create_bare_repo() -> TempDir { | ||
| 99 | let dir = TempDir::new().unwrap(); | ||
| 100 | let status = StdCommand::new("git") | ||
| 101 | .args(["init", "--bare"]) | ||
| 102 | .arg(dir.path()) | ||
| 103 | .status() | ||
| 104 | .expect("Failed to run git init"); | ||
| 105 | assert!(status.success()); | ||
| 106 | dir | ||
| 107 | } | ||
| 108 | |||
| 109 | #[tokio::test] | ||
| 110 | async fn test_spawn_upload_pack_advertise() { | ||
| 111 | let repo = create_bare_repo(); | ||
| 112 | let mut proc = GitSubprocess::spawn( | ||
| 113 | GitService::UploadPack, | ||
| 114 | repo.path(), | ||
| 115 | true, | ||
| 116 | ).expect("Failed to spawn git"); | ||
| 117 | |||
| 118 | // Should have spawned successfully | ||
| 119 | assert!(proc.stdout().is_some()); | ||
| 120 | assert!(proc.stdin().is_some()); | ||
| 121 | |||
| 122 | // Clean up | ||
| 123 | let _ = proc.kill().await; | ||
| 124 | } | ||
| 125 | |||
| 126 | #[tokio::test] | ||
| 127 | async fn test_spawn_receive_pack() { | ||
| 128 | let repo = create_bare_repo(); | ||
| 129 | let mut proc = GitSubprocess::spawn( | ||
| 130 | GitService::ReceivePack, | ||
| 131 | repo.path(), | ||
| 132 | false, | ||
| 133 | ).expect("Failed to spawn git"); | ||
| 134 | |||
| 135 | assert!(proc.stdout().is_some()); | ||
| 136 | assert!(proc.stdin().is_some()); | ||
| 137 | |||
| 138 | let _ = proc.kill().await; | ||
| 139 | } | ||
| 140 | } \ No newline at end of file | ||