mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
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
155 lines
4.5 KiB
Rust
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;
|
|
}
|
|
}
|