diff --git a/minigit/cli.py b/minigit/cli.py index 8a546de..8883138 100644 --- a/minigit/cli.py +++ b/minigit/cli.py @@ -13,6 +13,7 @@ from minigit import __version__ from minigit.errors import MiniGitError +from minigit.objects import register_subcommands def _register_commands(subparsers) -> None: @@ -27,6 +28,8 @@ def register_subcommands(subparsers): parser.set_defaults(handler=cmd_add) """ + 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..83f0484 100644 --- a/minigit/objects.py +++ b/minigit/objects.py @@ -8,3 +8,75 @@ 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] + + +_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") + cat_parser.set_defaults(handler=run_cat_file) diff --git a/tests/test_objects.py b/tests/test_objects.py new file mode 100644 index 0000000..dd98d84 --- /dev/null +++ b/tests/test_objects.py @@ -0,0 +1,98 @@ +"""Object tests: Tests for module 1 - object storage.""" + +from pathlib import Path + +import pytest + +from minigit.cli import main +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") + + +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 # current placeholder hash has length 40, like SHA-1 + + +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"