Browse Source

Add unpack-objects plumbing command

This implements the unpack-objects command that extracts all objects
from a pack file and writes them as loose objects to the repository's
object store.
Jelmer Vernooij 1 month ago
parent
commit
99af02a86f
4 changed files with 86 additions and 0 deletions
  1. 6 0
      NEWS
  2. 11 0
      dulwich/cli.py
  3. 23 0
      dulwich/porcelain.py
  4. 46 0
      tests/test_porcelain.py

+ 6 - 0
NEWS

@@ -1,5 +1,11 @@
 0.22.9	UNRELEASED
 
+ * Add ``unpack-objects`` plumbing command to unpack objects from pack files
+   into loose objects in the repository. This command extracts all objects
+   from a pack file and writes them to the object store as individual files.
+   Available in both ``dulwich.porcelain.unpack_objects()`` and as a CLI
+   command ``dulwich unpack-objects``. (Jelmer Vernooij)
+
  * Add support for pack index format version 3. This format supports variable
    hash sizes to enable future SHA-256 support. The implementation includes
    reading and writing v3 indexes with proper hash algorithm identification

+ 11 - 0
dulwich/cli.py

@@ -633,6 +633,16 @@ class cmd_pack_objects(Command):
             f.close()
 
 
+class cmd_unpack_objects(Command):
+    def run(self, args) -> None:
+        parser = argparse.ArgumentParser()
+        parser.add_argument("pack_file", help="Pack file to unpack")
+        args = parser.parse_args(args)
+
+        count = porcelain.unpack_objects(args.pack_file)
+        print(f"Unpacked {count} objects")
+
+
 class cmd_pull(Command):
     def run(self, args) -> None:
         parser = argparse.ArgumentParser()
@@ -975,6 +985,7 @@ commands = {
     "symbolic-ref": cmd_symbolic_ref,
     "submodule": cmd_submodule,
     "tag": cmd_tag,
+    "unpack-objects": cmd_unpack_objects,
     "update-server-info": cmd_update_server_info,
     "upload-pack": cmd_upload_pack,
     "web-daemon": cmd_web_daemon,

+ 23 - 0
dulwich/porcelain.py

@@ -2831,3 +2831,26 @@ def merge(
         return _do_merge(
             r, merge_commit_id, no_commit, no_ff, message, author, committer
         )
+
+
+def unpack_objects(pack_path, target="."):
+    """Unpack objects from a pack file into the repository.
+
+    Args:
+      pack_path: Path to the pack file to unpack
+      target: Path to the repository to unpack into
+
+    Returns:
+      Number of objects unpacked
+    """
+    from .pack import Pack
+
+    with open_repo_closing(target) as r:
+        pack_basename = os.path.splitext(pack_path)[0]
+        with Pack(pack_basename) as pack:
+            count = 0
+            for unpacked in pack.iter_unpacked():
+                obj = unpacked.sha_file()
+                r.object_store.add_object(obj)
+                count += 1
+            return count

+ 46 - 0
tests/test_porcelain.py

@@ -4970,3 +4970,49 @@ class ConeModeTests(PorcelainTestCase):
         self.assertIn("/docs/", lines)
         self.assertIn("/src/", lines)
         self.assertIn("/tests/", lines)
+
+
+class UnpackObjectsTest(PorcelainTestCase):
+    def test_unpack_objects(self):
+        """Test unpacking objects from a pack file."""
+        # Create a test repository with some objects
+        b1 = Blob()
+        b1.data = b"test content 1"
+        b2 = Blob()
+        b2.data = b"test content 2"
+
+        # Add objects to the repo
+        self.repo.object_store.add_object(b1)
+        self.repo.object_store.add_object(b2)
+
+        # Create a pack file with these objects
+        pack_path = os.path.join(self.test_dir, "test_pack")
+        with (
+            open(pack_path + ".pack", "wb") as pack_f,
+            open(pack_path + ".idx", "wb") as idx_f,
+        ):
+            porcelain.pack_objects(
+                self.repo,
+                [b1.id, b2.id],
+                pack_f,
+                idx_f,
+            )
+
+        # Create a new repository to unpack into
+        target_repo_path = os.path.join(self.test_dir, "target_repo")
+        target_repo = Repo.init(target_repo_path, mkdir=True)
+        self.addCleanup(target_repo.close)
+
+        # Unpack the objects
+        count = porcelain.unpack_objects(pack_path + ".pack", target_repo_path)
+
+        # Verify the objects were unpacked
+        self.assertEqual(2, count)
+        self.assertIn(b1.id, target_repo.object_store)
+        self.assertIn(b2.id, target_repo.object_store)
+
+        # Verify the content is correct
+        unpacked_b1 = target_repo.object_store[b1.id]
+        unpacked_b2 = target_repo.object_store[b2.id]
+        self.assertEqual(b1.data, unpacked_b1.data)
+        self.assertEqual(b2.data, unpacked_b2.data)