Files
ngit-grasp/src/git/subprocess.rs
T
DanConwayDev 817ce37a5e feat: add uploadpack.allowFilter support for GRASP-01 compliance
Add mandatory uploadpack.allowFilter capability to support partial clones
and fetches as required by GRASP-01 specification. This enables efficient
git operations for bandwidth-constrained clients (e.g., browser-based git
clients like git-natural-api).

Changes:
- Add uploadpack.allowFilter=true to git subprocess configuration
- Update SmartGitServer test helper with filter support
- Add integration tests for filter capability advertisement and functionality
- Update documentation to reflect filter as required capability

Tests verify:
- Filter capability is advertised in info/refs
- Filtered clones with blob:none work correctly
- Filtered fetches with tree:0 work correctly
2026-01-12 14:05:51 +00:00

155 lines
4.5 KiB
Rust

//! 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;
}
}