diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 14:05:51 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 14:05:51 +0000 |
| commit | 817ce37a5ee8d6279a44cf8cce3cc6a1e4bab576 (patch) | |
| tree | 9fd5a6d3969afc33baa900bdab25bff81c5a83a4 /tests | |
| parent | f25eea8cc3b940cbcaa96224485826bfaae82449 (diff) | |
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
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/common/git_server.rs | 4 | ||||
| -rw-r--r-- | tests/test_filter_support.rs | 158 |
2 files changed, 162 insertions, 0 deletions
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( | |||
| 764 | .arg("uploadpack.allowReachableSHA1InWant=true") | 764 | .arg("uploadpack.allowReachableSHA1InWant=true") |
| 765 | .arg("-c") | 765 | .arg("-c") |
| 766 | .arg("uploadpack.allowTipSHA1InWant=true") | 766 | .arg("uploadpack.allowTipSHA1InWant=true") |
| 767 | .arg("-c") | ||
| 768 | .arg("uploadpack.allowFilter=true") | ||
| 767 | .arg("upload-pack") | 769 | .arg("upload-pack") |
| 768 | .arg("--advertise-refs") | 770 | .arg("--advertise-refs") |
| 769 | .arg("--stateless-rpc"); | 771 | .arg("--stateless-rpc"); |
| @@ -854,6 +856,8 @@ async fn handle_upload_pack( | |||
| 854 | .arg("uploadpack.allowReachableSHA1InWant=true") | 856 | .arg("uploadpack.allowReachableSHA1InWant=true") |
| 855 | .arg("-c") | 857 | .arg("-c") |
| 856 | .arg("uploadpack.allowTipSHA1InWant=true") | 858 | .arg("uploadpack.allowTipSHA1InWant=true") |
| 859 | .arg("-c") | ||
| 860 | .arg("uploadpack.allowFilter=true") | ||
| 857 | .arg("upload-pack") | 861 | .arg("upload-pack") |
| 858 | .arg("--stateless-rpc"); | 862 | .arg("--stateless-rpc"); |
| 859 | 863 | ||
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 @@ | |||
| 1 | //! Integration test for git filter support (--filter=blob:none) | ||
| 2 | |||
| 3 | use tempfile::TempDir; | ||
| 4 | use tokio::process::Command; | ||
| 5 | |||
| 6 | mod common; | ||
| 7 | use common::purgatory_helpers::{create_test_repo_with_commit, CommitVariant}; | ||
| 8 | use common::SmartGitServer; | ||
| 9 | |||
| 10 | /// Test that the server advertises filter capability | ||
| 11 | #[tokio::test] | ||
| 12 | async fn test_filter_capability_advertised() { | ||
| 13 | // Create a test repo | ||
| 14 | let temp_dir = TempDir::new().expect("Failed to create temp dir"); | ||
| 15 | create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | ||
| 16 | .expect("Failed to create test repo"); | ||
| 17 | |||
| 18 | // Start smart git server | ||
| 19 | let server = SmartGitServer::start(temp_dir.path()).await; | ||
| 20 | |||
| 21 | // Run git ls-remote to see advertised capabilities | ||
| 22 | let output = Command::new("git") | ||
| 23 | .env("GIT_TRACE_PACKET", "1") | ||
| 24 | .args(["ls-remote", server.url()]) | ||
| 25 | .output() | ||
| 26 | .await | ||
| 27 | .expect("Failed to run git ls-remote"); | ||
| 28 | |||
| 29 | // Capture stderr which contains GIT_TRACE_PACKET output | ||
| 30 | let stderr = String::from_utf8_lossy(&output.stderr); | ||
| 31 | |||
| 32 | // Check for filter capability in the advertisement | ||
| 33 | // The capability is advertised as "filter" in the pkt-line | ||
| 34 | assert!( | ||
| 35 | stderr.contains("filter") || stderr.contains("allow"), | ||
| 36 | "Expected to find 'filter' capability in git protocol advertisement.\nStderr:\n{}", | ||
| 37 | stderr | ||
| 38 | ); | ||
| 39 | |||
| 40 | // Also verify the command succeeded | ||
| 41 | assert!( | ||
| 42 | output.status.success(), | ||
| 43 | "git ls-remote failed: {}", | ||
| 44 | String::from_utf8_lossy(&output.stderr) | ||
| 45 | ); | ||
| 46 | |||
| 47 | server.stop().await; | ||
| 48 | } | ||
| 49 | |||
| 50 | /// Test that filtered clones work (--filter=blob:none) | ||
| 51 | #[tokio::test] | ||
| 52 | async fn test_filtered_clone_succeeds() { | ||
| 53 | // Create a test repo with files | ||
| 54 | let temp_dir = TempDir::new().expect("Failed to create temp dir"); | ||
| 55 | create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | ||
| 56 | .expect("Failed to create test repo"); | ||
| 57 | |||
| 58 | // Start smart git server | ||
| 59 | let server = SmartGitServer::start(temp_dir.path()).await; | ||
| 60 | |||
| 61 | // Create a clone destination | ||
| 62 | let clone_dir = TempDir::new().expect("Failed to create clone dir"); | ||
| 63 | let clone_path = clone_dir.path().join("cloned-repo"); | ||
| 64 | |||
| 65 | // Attempt a filtered clone | ||
| 66 | let output = Command::new("git") | ||
| 67 | .args([ | ||
| 68 | "clone", | ||
| 69 | "--filter=blob:none", | ||
| 70 | server.url(), | ||
| 71 | clone_path.to_str().unwrap(), | ||
| 72 | ]) | ||
| 73 | .output() | ||
| 74 | .await | ||
| 75 | .expect("Failed to run git clone"); | ||
| 76 | |||
| 77 | // Check if clone succeeded | ||
| 78 | if !output.status.success() { | ||
| 79 | eprintln!("git clone --filter=blob:none failed!"); | ||
| 80 | eprintln!("Stdout: {}", String::from_utf8_lossy(&output.stdout)); | ||
| 81 | eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr)); | ||
| 82 | panic!("Filtered clone failed"); | ||
| 83 | } | ||
| 84 | |||
| 85 | // Verify the clone worked | ||
| 86 | assert!(clone_path.exists(), "Clone directory should exist"); | ||
| 87 | assert!( | ||
| 88 | clone_path.join(".git").exists(), | ||
| 89 | "Cloned repo should have .git directory" | ||
| 90 | ); | ||
| 91 | |||
| 92 | // In a filtered clone, we should be able to list files | ||
| 93 | let ls_output = Command::new("git") | ||
| 94 | .current_dir(&clone_path) | ||
| 95 | .args(["ls-files"]) | ||
| 96 | .output() | ||
| 97 | .await | ||
| 98 | .expect("Failed to list files"); | ||
| 99 | |||
| 100 | assert!( | ||
| 101 | ls_output.status.success(), | ||
| 102 | "Should be able to list files in filtered clone" | ||
| 103 | ); | ||
| 104 | |||
| 105 | let files = String::from_utf8_lossy(&ls_output.stdout); | ||
| 106 | assert!( | ||
| 107 | !files.trim().is_empty(), | ||
| 108 | "Should have files in the filtered clone" | ||
| 109 | ); | ||
| 110 | |||
| 111 | server.stop().await; | ||
| 112 | } | ||
| 113 | |||
| 114 | /// Test that filtered fetches work | ||
| 115 | #[tokio::test] | ||
| 116 | async fn test_filtered_fetch_succeeds() { | ||
| 117 | // Create a test repo | ||
| 118 | let temp_dir = TempDir::new().expect("Failed to create temp dir"); | ||
| 119 | create_test_repo_with_commit(temp_dir.path(), CommitVariant::StateTest) | ||
| 120 | .expect("Failed to create test repo"); | ||
| 121 | |||
| 122 | // Start smart git server | ||
| 123 | let server = SmartGitServer::start(temp_dir.path()).await; | ||
| 124 | |||
| 125 | // First, clone normally (to set up tracking) | ||
| 126 | let clone_dir = TempDir::new().expect("Failed to create clone dir"); | ||
| 127 | let clone_path = clone_dir.path().join("repo"); | ||
| 128 | |||
| 129 | let clone_output = Command::new("git") | ||
| 130 | .args(["clone", server.url(), clone_path.to_str().unwrap()]) | ||
| 131 | .output() | ||
| 132 | .await | ||
| 133 | .expect("Failed to run git clone"); | ||
| 134 | |||
| 135 | assert!( | ||
| 136 | clone_output.status.success(), | ||
| 137 | "Initial clone failed: {}", | ||
| 138 | String::from_utf8_lossy(&clone_output.stderr) | ||
| 139 | ); | ||
| 140 | |||
| 141 | // Now try a filtered fetch | ||
| 142 | let fetch_output = Command::new("git") | ||
| 143 | .current_dir(&clone_path) | ||
| 144 | .args(["fetch", "--filter=blob:none", "origin"]) | ||
| 145 | .output() | ||
| 146 | .await | ||
| 147 | .expect("Failed to run git fetch"); | ||
| 148 | |||
| 149 | // Check if fetch succeeded | ||
| 150 | if !fetch_output.status.success() { | ||
| 151 | eprintln!("git fetch --filter=blob:none failed!"); | ||
| 152 | eprintln!("Stdout: {}", String::from_utf8_lossy(&fetch_output.stdout)); | ||
| 153 | eprintln!("Stderr: {}", String::from_utf8_lossy(&fetch_output.stderr)); | ||
| 154 | panic!("Filtered fetch failed"); | ||
| 155 | } | ||
| 156 | |||
| 157 | server.stop().await; | ||
| 158 | } | ||