Bladeren bron

Add explicit protocol v0 and v2 compatibility tests for object filters

Jelmer Vernooij 3 weken geleden
bovenliggende
commit
81f066615d
4 gewijzigde bestanden met toevoegingen van 116 en 5 verwijderingen
  1. 6 1
      dulwich/object_filters.py
  2. 4 2
      dulwich/protocol.py
  3. 3 1
      dulwich/server.py
  4. 103 1
      tests/compat/test_partial_clone.py

+ 6 - 1
dulwich/object_filters.py

@@ -90,7 +90,7 @@ class FilterSpec(ABC):
         """Convert filter spec back to string format.
 
         Returns:
-            Filter specification string (e.g., 'blob:none', 'blob:limit=1m')
+            Filter specification string (e.g., ``blob:none``, ``blob:limit=1m``)
         """
         ...
 
@@ -111,6 +111,7 @@ class BlobNoneFilter(FilterSpec):
         return "blob:none"
 
     def __repr__(self) -> str:
+        """Return string representation of the filter."""
         return "BlobNoneFilter()"
 
 
@@ -146,6 +147,7 @@ class BlobLimitFilter(FilterSpec):
             return f"blob:limit={size}"
 
     def __repr__(self) -> str:
+        """Return string representation of the filter."""
         return f"BlobLimitFilter(limit={self.limit})"
 
 
@@ -173,6 +175,7 @@ class TreeDepthFilter(FilterSpec):
         return f"tree:{self.max_depth}"
 
     def __repr__(self) -> str:
+        """Return string representation of the filter."""
         return f"TreeDepthFilter(max_depth={self.max_depth})"
 
 
@@ -254,6 +257,7 @@ class SparseOidFilter(FilterSpec):
         return f"sparse:oid={self.oid.decode('ascii') if isinstance(self.oid, bytes) else self.oid}"
 
     def __repr__(self) -> str:
+        """Return string representation of the filter."""
         oid_str = self.oid.decode("ascii") if isinstance(self.oid, bytes) else self.oid
         return f"SparseOidFilter(oid={oid_str!r})"
 
@@ -282,6 +286,7 @@ class CombineFilter(FilterSpec):
         return "combine:" + "+".join(f.to_spec_string() for f in self.filters)
 
     def __repr__(self) -> str:
+        """Return string representation of the filter."""
         return f"CombineFilter(filters={self.filters!r})"
 
 

+ 4 - 2
dulwich/protocol.py

@@ -725,7 +725,9 @@ def ack_type(capabilities: Iterable[bytes]) -> int:
     return SINGLE_ACK
 
 
-def find_capability(capabilities: Iterable[bytes], *capability_names: bytes) -> bytes | None:
+def find_capability(
+    capabilities: Iterable[bytes], *capability_names: bytes
+) -> bytes | None:
     """Find a capability value in a list of capabilities.
 
     This function looks for capabilities that may include arguments after an equals sign
@@ -753,7 +755,7 @@ def find_capability(capabilities: Iterable[bytes], *capability_names: bytes) ->
             if cap == name:
                 return cap
             elif cap.startswith(name + b"="):
-                return cap[len(name) + 1:]
+                return cap[len(name) + 1 :]
     return None
 
 

+ 3 - 1
dulwich/server.py

@@ -657,7 +657,9 @@ class UploadPackHandler(PackHandler):
                 )
                 # Reconstruct tuples with hints for pack generation
                 filtered_oid_set = set(filtered_oids)
-                object_ids = [(oid, hint) for oid, hint in object_ids if oid in filtered_oid_set]
+                object_ids = [
+                    (oid, hint) for oid, hint in object_ids if oid in filtered_oid_set
+                ]
 
             filtered_count = original_count - len(object_ids)
             if filtered_count > 0:

+ 103 - 1
tests/compat/test_partial_clone.py

@@ -165,7 +165,13 @@ class PartialCloneServerTestCase(CompatTestCase):
         clone_dir = os.path.join(clone_path, "cloned_repo")
 
         run_git_or_fail(
-            ["clone", "--filter=blob:limit=100", "--no-checkout", self.url(port), clone_dir],
+            [
+                "clone",
+                "--filter=blob:limit=100",
+                "--no-checkout",
+                self.url(port),
+                clone_dir,
+            ],
             cwd=clone_path,
         )
 
@@ -231,6 +237,102 @@ class PartialCloneServerTestCase(CompatTestCase):
 
         source_repo.close()
 
+    def test_clone_with_filter_protocol_v0(self) -> None:
+        """Test that git client can clone with filter using protocol v0."""
+        # Create repository with dulwich
+        repo_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, repo_path)
+        source_repo = Repo.init(repo_path, mkdir=False)
+
+        # Create test content
+        blob = Blob.from_string(b"test content")
+        tree = Tree()
+        tree.add(b"file.txt", 0o100644, blob.id)
+
+        source_repo.object_store.add_object(blob)
+        source_repo.object_store.add_object(tree)
+
+        commit = make_commit(tree=tree.id, message=b"Test commit")
+        source_repo.object_store.add_object(commit)
+        source_repo.refs[b"refs/heads/master"] = commit.id
+
+        # Start server
+        port = self._start_server(source_repo)
+
+        # Clone with protocol v0 and blob:none filter
+        clone_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, clone_path)
+        clone_dir = os.path.join(clone_path, "cloned_repo")
+
+        run_git_or_fail(
+            [
+                "-c",
+                "protocol.version=0",
+                "clone",
+                "--filter=blob:none",
+                "--no-checkout",
+                self.url(port),
+                clone_dir,
+            ],
+            cwd=clone_path,
+        )
+
+        # Verify partial clone
+        cloned_repo = Repo(clone_dir)
+        self.addCleanup(cloned_repo.close)
+        self.assertIn(commit.id, cloned_repo.object_store)
+        self.assertIn(tree.id, cloned_repo.object_store)
+
+        source_repo.close()
+
+    def test_clone_with_filter_protocol_v2(self) -> None:
+        """Test that git client can clone with filter using protocol v2."""
+        # Create repository with dulwich
+        repo_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, repo_path)
+        source_repo = Repo.init(repo_path, mkdir=False)
+
+        # Create test content
+        blob = Blob.from_string(b"test content")
+        tree = Tree()
+        tree.add(b"file.txt", 0o100644, blob.id)
+
+        source_repo.object_store.add_object(blob)
+        source_repo.object_store.add_object(tree)
+
+        commit = make_commit(tree=tree.id, message=b"Test commit")
+        source_repo.object_store.add_object(commit)
+        source_repo.refs[b"refs/heads/master"] = commit.id
+
+        # Start server
+        port = self._start_server(source_repo)
+
+        # Clone with protocol v2 and blob:none filter
+        clone_path = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, clone_path)
+        clone_dir = os.path.join(clone_path, "cloned_repo")
+
+        run_git_or_fail(
+            [
+                "-c",
+                "protocol.version=2",
+                "clone",
+                "--filter=blob:none",
+                "--no-checkout",
+                self.url(port),
+                clone_dir,
+            ],
+            cwd=clone_path,
+        )
+
+        # Verify partial clone
+        cloned_repo = Repo(clone_dir)
+        self.addCleanup(cloned_repo.close)
+        self.assertIn(commit.id, cloned_repo.object_store)
+        self.assertIn(tree.id, cloned_repo.object_store)
+
+        source_repo.close()
+
 
 @skipIf(sys.platform == "win32", "Broken on windows, with very long fail time.")
 class PartialCloneClientTestCase(CompatTestCase):