From bfa86c7499f9d415f4eb77f64b749653535a1ea5 Mon Sep 17 00:00:00 2001 From: mahis1067 Date: Tue, 25 Aug 2026 11:20:27 -0400 Subject: [PATCH 1/4] feat/object-store-skeleton-cli-stub --- minigit/cli.py | 4 ++- minigit/objects.py | 53 ++++++++++++++++++++++++++++++++++++ tests/test_objects.py | 62 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/test_objects.py diff --git a/minigit/cli.py b/minigit/cli.py index 8a546de..bd4f3ec 100644 --- a/minigit/cli.py +++ b/minigit/cli.py @@ -11,7 +11,7 @@ import sys from collections.abc import Sequence -from minigit import __version__ +from minigit import __version__, objects from minigit.errors import MiniGitError @@ -27,6 +27,8 @@ def register_subcommands(subparsers): parser.set_defaults(handler=cmd_add) """ + objects.register_subcommands(subparsers) + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="minigit", description="A version control system.") diff --git a/minigit/objects.py b/minigit/objects.py index 860cc9a..3c555e7 100644 --- a/minigit/objects.py +++ b/minigit/objects.py @@ -8,3 +8,56 @@ Build the `ObjectStore` class here, per the interface contract. """ + +import zlib +from pathlib import Path + +from minigit.errors import ObjectNotFoundError + + +class ObjectStore: + root: Path + objects_dir: Path + _fake_store: dict[str, tuple[str, bytes]] + + def __init__(self, repo_path=".") -> None: + """ + Initialize the object store. + """ + self.root = Path(repo_path) + self.objects_dir = self.root / ".minigit" / "objects" + self._fake_store = {} + + def hash_object(self, data: bytes, obj_type: str) -> str: + """ + Return the SHA-1 hash of the object, given its data and type. + """ + # placeholder - real SHA-1 of " \0" lands Week 2 + return f"{zlib.crc32(obj_type.encode() + data):040x}" + + def write_object(self, data: bytes, obj_type: str) -> str: + """ + Writes the object's hash into self._fake_store and returns the hash. + Allow duplicates to be written. + """ + obj_hash = self.hash_object(data, obj_type) + self._fake_store[obj_hash] = (obj_type, data) + return obj_hash + + def read_object(self, hash: str) -> tuple[str, bytes]: + """ + Reads the object from self._fake_store and returns a tuple of (type, data). + Raise ObjectNotFoundError(hash) if the object is not found. + """ + if hash not in self._fake_store: + raise ObjectNotFoundError(hash) + + return self._fake_store[hash] + + +def register_subcommands(subparsers): + hash_parser = subparsers.add_parser("hash-object") + hash_parser.add_argument("path") + + cat_parser = subparsers.add_parser("cat-file") + cat_parser.add_argument("hash") \ No newline at end of file diff --git a/tests/test_objects.py b/tests/test_objects.py new file mode 100644 index 0000000..fa25213 --- /dev/null +++ b/tests/test_objects.py @@ -0,0 +1,62 @@ +"""Object tests: Tests for module 1 - object storage.""" + +from pathlib import Path + +import pytest + +from minigit.errors import ObjectNotFoundError +from minigit.objects import ObjectStore + + +def test_round_trip(tmp_path: Path) -> None: + """ + Test that we can write an object and then read it back. + """ + + store = ObjectStore(tmp_path) + obj_hash = store.write_object(b"hi", "blob") + assert store.read_object(obj_hash) == ("blob", b"hi") + +def test_identical_objects_same_hash(tmp_path: Path): + """ + Test that writing the same object twice returns the same hash. + """ + store = ObjectStore(tmp_path) + + hash1 = store.hash_object(b"hi", "blob") + hash2 = store.hash_object(b"hi", "blob") + + assert hash1 == hash2 + +def test_different_objects_different_hashes(tmp_path: Path): + """ + Test that writing different objects returns different hashes. + """ + store = ObjectStore(tmp_path) + + hash1 = store.hash_object(b"hi", "blob") + hash2 = store.hash_object(b"hello", "blob") + + assert hash1 != hash2 + +def test_idempotent_write(tmp_path: Path): + """ + Test that writing the same object twice returns the same hash and does not raise an error. + """ + store = ObjectStore(tmp_path) + + hash1 = store.write_object(b"hi", "blob") + hash2 = store.write_object(b"hi", "blob") + + assert hash1 == hash2 + assert store.read_object(hash1) == ("blob", b"hi") + + +def test_unknown_hash_raises(tmp_path: Path): + """ + Test that reading an unknown hash raises ObjectNotFoundError. + """ + store = ObjectStore(tmp_path) + + with pytest.raises(ObjectNotFoundError): + store.read_object("does-not-exist") \ No newline at end of file From 4bb844bc70840e817e262f7d2622bcac150cbf31 Mon Sep 17 00:00:00 2001 From: mahis1067 Date: Tue, 25 Aug 2026 15:25:08 -0400 Subject: [PATCH 2/4] fix/hash-cat-file-cli-tests --- minigit/cli.py | 5 +++-- minigit/objects.py | 21 ++++++++++++++++++++- tests/test_objects.py | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/minigit/cli.py b/minigit/cli.py index bd4f3ec..8883138 100644 --- a/minigit/cli.py +++ b/minigit/cli.py @@ -11,8 +11,9 @@ import sys from collections.abc import Sequence -from minigit import __version__, objects +from minigit import __version__ from minigit.errors import MiniGitError +from minigit.objects import register_subcommands def _register_commands(subparsers) -> None: @@ -27,7 +28,7 @@ def register_subcommands(subparsers): parser.set_defaults(handler=cmd_add) """ - objects.register_subcommands(subparsers) + register_subcommands(subparsers) def build_parser() -> argparse.ArgumentParser: diff --git a/minigit/objects.py b/minigit/objects.py index 3c555e7..83f0484 100644 --- a/minigit/objects.py +++ b/minigit/objects.py @@ -55,9 +55,28 @@ def read_object(self, hash: str) -> tuple[str, bytes]: return self._fake_store[hash] +_cli_store = ObjectStore() + + +def run_hash_object(args) -> int: + with open(args.path, "rb") as f: + data = f.read() + obj_hash = _cli_store.write_object(data, "blob") + print(obj_hash) + return 0 + + +def run_cat_file(args) -> int: + _, obj_data = _cli_store.read_object(args.hash) + print(obj_data.decode("utf-8", errors="replace"), end="") + return 0 + + def register_subcommands(subparsers): hash_parser = subparsers.add_parser("hash-object") hash_parser.add_argument("path") + hash_parser.set_defaults(handler=run_hash_object) cat_parser = subparsers.add_parser("cat-file") - cat_parser.add_argument("hash") \ No newline at end of file + cat_parser.add_argument("hash") + cat_parser.set_defaults(handler=run_cat_file) diff --git a/tests/test_objects.py b/tests/test_objects.py index fa25213..556c09d 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -4,6 +4,7 @@ import pytest +from minigit.cli import main from minigit.errors import ObjectNotFoundError from minigit.objects import ObjectStore @@ -17,6 +18,7 @@ def test_round_trip(tmp_path: Path) -> None: obj_hash = store.write_object(b"hi", "blob") assert store.read_object(obj_hash) == ("blob", b"hi") + def test_identical_objects_same_hash(tmp_path: Path): """ Test that writing the same object twice returns the same hash. @@ -28,6 +30,7 @@ def test_identical_objects_same_hash(tmp_path: Path): assert hash1 == hash2 + def test_different_objects_different_hashes(tmp_path: Path): """ Test that writing different objects returns different hashes. @@ -39,6 +42,7 @@ def test_different_objects_different_hashes(tmp_path: Path): assert hash1 != hash2 + def test_idempotent_write(tmp_path: Path): """ Test that writing the same object twice returns the same hash and does not raise an error. @@ -59,4 +63,35 @@ def test_unknown_hash_raises(tmp_path: Path): store = ObjectStore(tmp_path) with pytest.raises(ObjectNotFoundError): - store.read_object("does-not-exist") \ No newline at end of file + store.read_object("does-not-exist") + + +def test_hash_object_cli_print(tmp_path, capsys): + """ + Test that the hash-object CLI command prints the correct hash. + """ + + test_file = tmp_path / "test.txt" + test_file.write_bytes(b"hello minigit") + + assert main(["hash-object", str(test_file)]) == 0 + + captured = capsys.readouterr() + obj_hash = captured.out.strip() + assert len(obj_hash) == 40 # SHA-1 hash length + + +def test_cat_file_cli_print(tmp_path, capsys): + """ + Test that the cat-file CLI command prints the correct object data. + """ + + test_file = tmp_path / "test.txt" + test_file.write_bytes(b"hello minigit") + + assert main(["hash-object", str(test_file)]) == 0 + obj_hash = capsys.readouterr().out.strip() + + assert main(["cat-file", obj_hash]) == 0 + captured = capsys.readouterr() + assert captured.out == "hello minigit" From fc5bfc2968ae49d317ba55a651811231ebde0700 Mon Sep 17 00:00:00 2001 From: mahis1067 Date: Tue, 25 Aug 2026 20:57:52 -0400 Subject: [PATCH 3/4] fix/test-hash-object-cli-print --- tests/test_objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_objects.py b/tests/test_objects.py index 556c09d..d24632c 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -78,7 +78,10 @@ def test_hash_object_cli_print(tmp_path, capsys): captured = capsys.readouterr() obj_hash = captured.out.strip() - assert len(obj_hash) == 40 # SHA-1 hash length + + # Placeholder contract: CRC32-based 8-digit lowercase hex + # TODO: Update this test when we implement real SHA-1 hashing in Week 2. + assert len(obj_hash) == 8 # SHA-1 hash length of 40 def test_cat_file_cli_print(tmp_path, capsys): From 75d9ac49b5bb306f873b1ff583fdfde29bc81a0a Mon Sep 17 00:00:00 2001 From: mahis1067 Date: Tue, 25 Aug 2026 21:07:27 -0400 Subject: [PATCH 4/4] fix/hash-character-placeholder --- tests/test_objects.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_objects.py b/tests/test_objects.py index 556c09d..fce6923 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -78,7 +78,8 @@ def test_hash_object_cli_print(tmp_path, capsys): captured = capsys.readouterr() obj_hash = captured.out.strip() - assert len(obj_hash) == 40 # SHA-1 hash length + + assert len(obj_hash) == 40 # SHA-1 hash length of 40 def test_cat_file_cli_print(tmp_path, capsys):