From 817ce37a5ee8d6279a44cf8cce3cc6a1e4bab576 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 12 Jan 2026 14:05:51 +0000 Subject: 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 --- tests/common/git_server.rs | 4 ++ tests/test_filter_support.rs | 158 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 tests/test_filter_support.rs (limited to 'tests') diff --git a/tests/common/git_server.rs b/tests/common/git_server.rs index 3190901..7634968 100644 --- a/tests/common/git_server.rs +++ b/tests/common/git_server.rs @@ -764,6 +764,8 @@ async fn handle_info_refs_upload_pack( .arg("uploadpack.allowReachableSHA1InWant=true") .arg("-c") .arg("uploadpack.allowTipSHA1InWant=true") + .arg("-c") + .arg("uploadpack.allowFilter=true") .arg("upload-pack") .arg("--advertise-refs") .arg("--stateless-rpc"); @@ -854,6 +856,8 @@ async fn handle_upload_pack( .arg("uploadpack.allowReachableSHA1InWant=true") .arg("-c") .arg("uploadpack.allowTipSHA1InWant=true") + .arg("-c") + .arg("uploadpack.allowFilter=true") .arg("upload-pack") .arg("--stateless-rpc"); diff --git a/tests/test_filter_support.rs b/tests/test_filter_support.rs new file mode 100644 index 0000000..58c6352 --- /dev/null +++ b/tests/test_filter_support.rs @@ -0,0 +1,158 @@ +//! Integration test for git filter support (--filter=blob:none) + +use tempfile::TempDir; +use tokio::process::Command; + +mod common; +use common::purgatory_helpers::{create_test_repo_with_commit, CommitVariant}; +use common::SmartGitServer; + +/// Test that the server advertises filter capability +#[tokio::test] +async fn test_filter_capability_advertised() { + // Create a test repo + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) + .expect("Failed to create test repo"); + + // Start smart git server + let server = SmartGitServer::start(temp_dir.path()).await; + + // Run git ls-remote to see advertised capabilities + let output = Command::new("git") + .env("GIT_TRACE_PACKET", "1") + .args(["ls-remote", server.url()]) + .output() + .await + .expect("Failed to run git ls-remote"); + + // Capture stderr which contains GIT_TRACE_PACKET output + let stderr = String::from_utf8_lossy(&output.stderr); + + // Check for filter capability in the advertisement + // The capability is advertised as "filter" in the pkt-line + assert!( + stderr.contains("filter") || stderr.contains("allow"), + "Expected to find 'filter' capability in git protocol advertisement.\nStderr:\n{}", + stderr + ); + + // Also verify the command succeeded + assert!( + output.status.success(), + "git ls-remote failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + server.stop().await; +} + +/// Test that filtered clones work (--filter=blob:none) +#[tokio::test] +async fn test_filtered_clone_succeeds() { + // Create a test repo with files + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) + .expect("Failed to create test repo"); + + // Start smart git server + let server = SmartGitServer::start(temp_dir.path()).await; + + // Create a clone destination + let clone_dir = TempDir::new().expect("Failed to create clone dir"); + let clone_path = clone_dir.path().join("cloned-repo"); + + // Attempt a filtered clone + let output = Command::new("git") + .args([ + "clone", + "--filter=blob:none", + server.url(), + clone_path.to_str().unwrap(), + ]) + .output() + .await + .expect("Failed to run git clone"); + + // Check if clone succeeded + if !output.status.success() { + eprintln!("git clone --filter=blob:none failed!"); + eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout)); + eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); + panic!("Filtered clone failed"); + } + + // Verify the clone worked + assert!(clone_path.exists(), "Clone directory should exist"); + assert!( + clone_path.join(".git").exists(), + "Cloned repo should have .git directory" + ); + + // In a filtered clone, we should be able to list files + let ls_output = Command::new("git") + .current_dir(&clone_path) + .args(["ls-files"]) + .output() + .await + .expect("Failed to list files"); + + assert!( + ls_output.status.success(), + "Should be able to list files in filtered clone" + ); + + let files = String::from_utf8_lossy(&ls_output.stdout); + assert!( + !files.trim().is_empty(), + "Should have files in the filtered clone" + ); + + server.stop().await; +} + +/// Test that filtered fetches work +#[tokio::test] +async fn test_filtered_fetch_succeeds() { + // Create a test repo + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) + .expect("Failed to create test repo"); + + // Start smart git server + let server = SmartGitServer::start(temp_dir.path()).await; + + // First, clone normally (to set up tracking) + let clone_dir = TempDir::new().expect("Failed to create clone dir"); + let clone_path = clone_dir.path().join("repo"); + + let clone_output = Command::new("git") + .args(["clone", server.url(), clone_path.to_str().unwrap()]) + .output() + .await + .expect("Failed to run git clone"); + + assert!( + clone_output.status.success(), + "Initial clone failed: {}", + String::from_utf8_lossy(&clone_output.stderr) + ); + + // Now try a filtered fetch + let fetch_output = Command::new("git") + .current_dir(&clone_path) + .args(["fetch", "--filter=blob:none", "origin"]) + .output() + .await + .expect("Failed to run git fetch"); + + // Check if fetch succeeded + if !fetch_output.status.success() { + eprintln!("git fetch --filter=blob:none failed!"); + eprintln!("Stdout: {}", String::from_utf8_lossy(&fetch_output.stdout)); + eprintln!("Stderr: {}", String::from_utf8_lossy(&fetch_output.stderr)); + panic!("Filtered fetch failed"); + } + + server.stop().await; +} -- cgit v1.2.3