From bfa86c7499f9d415f4eb77f64b749653535a1ea5 Mon Sep 17 00:00:00 2001 From: mahis1067 Date: Tue, 25 Aug 2026 11:20:27 -0400 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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): From 3218c4b10b3ebd849d0db0abf3ab9a3eb874ed2c Mon Sep 17 00:00:00 2001 From: mahis1067 Date: Sun, 30 Aug 2026 15:13:25 -0400 Subject: [PATCH 5/9] docs/cli-functions --- minigit/objects.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/minigit/objects.py b/minigit/objects.py index 83f0484..b33069c 100644 --- a/minigit/objects.py +++ b/minigit/objects.py @@ -59,6 +59,9 @@ def read_object(self, hash: str) -> tuple[str, bytes]: def run_hash_object(args) -> int: + """ + Hash the file at args.path, write it to the object store, and print the hash. + """ with open(args.path, "rb") as f: data = f.read() obj_hash = _cli_store.write_object(data, "blob") @@ -67,12 +70,18 @@ def run_hash_object(args) -> int: def run_cat_file(args) -> int: + """ + Print the contents of the object with the given hash. + """ _, obj_data = _cli_store.read_object(args.hash) print(obj_data.decode("utf-8", errors="replace"), end="") return 0 def register_subcommands(subparsers): + """ + Register the "hash-object" and "cat-file" subcommands with the given subparsers object. + """ hash_parser = subparsers.add_parser("hash-object") hash_parser.add_argument("path") hash_parser.set_defaults(handler=run_hash_object) From 92c43038d455570f95e46b932576bc26c3587389 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:16:51 +0000 Subject: [PATCH 6/9] fix: remove trailing whitespace in cat-file docstring Co-authored-by: mahis1067 <151894644+mahis1067@users.noreply.github.com> --- minigit/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minigit/objects.py b/minigit/objects.py index b33069c..2f19f75 100644 --- a/minigit/objects.py +++ b/minigit/objects.py @@ -71,7 +71,7 @@ def run_hash_object(args) -> int: def run_cat_file(args) -> int: """ - Print the contents of the object with the given hash. + Print the contents of the object with the given hash. """ _, obj_data = _cli_store.read_object(args.hash) print(obj_data.decode("utf-8", errors="replace"), end="") From cb2d605b5c0c51d56fd4233fe23efef33b3aba70 Mon Sep 17 00:00:00 2001 From: mahis1067 Date: Sun, 30 Aug 2026 15:18:39 -0400 Subject: [PATCH 7/9] fix/trailing-space --- minigit/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minigit/objects.py b/minigit/objects.py index b33069c..2f19f75 100644 --- a/minigit/objects.py +++ b/minigit/objects.py @@ -71,7 +71,7 @@ def run_hash_object(args) -> int: def run_cat_file(args) -> int: """ - Print the contents of the object with the given hash. + Print the contents of the object with the given hash. """ _, obj_data = _cli_store.read_object(args.hash) print(obj_data.decode("utf-8", errors="replace"), end="") From f476e9309e8f7ffbe4404356ddd259352ae2fc44 Mon Sep 17 00:00:00 2001 From: Saanvi Tyagi Date: Mon, 31 Aug 2026 16:49:50 -0400 Subject: [PATCH 8/9] WorkingTree + CLI + pytest --- minigit/cli.py | 2 ++ minigit/index.py | 78 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_index.py | 68 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 tests/test_index.py diff --git a/minigit/cli.py b/minigit/cli.py index 8a546de..3409674 100644 --- a/minigit/cli.py +++ b/minigit/cli.py @@ -13,6 +13,7 @@ from minigit import __version__ from minigit.errors import MiniGitError +from minigit.index import register_index_commands def _register_commands(subparsers) -> None: @@ -26,6 +27,7 @@ def register_subcommands(subparsers): parser.add_argument("path") parser.set_defaults(handler=cmd_add) """ + register_index_commands(subparsers) def build_parser() -> argparse.ArgumentParser: diff --git a/minigit/index.py b/minigit/index.py index 3c59474..bd2b35d 100644 --- a/minigit/index.py +++ b/minigit/index.py @@ -8,3 +8,81 @@ Build the `WorkingTree` class here, per the interface contract. """ + +from dataclasses import dataclass +from typing import NamedTuple + + +class IndexEntry(NamedTuple): + mode: str + hash: str + path: str + + +@dataclass +class DiffResult: + added: list + deleted: list + modified: list + + +class WorkingTree: + def __init__(self, repo_path=".", store=None): + self.root = repo_path + self.index_path = f"{repo_path}/.minigit/index" + + if store is None: + from minigit.objects import ObjectStore + + store = ObjectStore(repo_path) + + self.store = store + self._entries: list[IndexEntry] = [] + + def read_index(self) -> list[IndexEntry]: + return sorted(self._entries, key=lambda e: e.path) + + def write_index(self, entries) -> None: + self._entries = sorted(entries, key=lambda e: e.path) + + def stage_file(self, path) -> None: + with open(f"{self.root}/{path}", "rb") as f: + data = f.read() + + blob_hash = self.store.write_object(data, "blob") + + self._entries = [e for e in self._entries if e.path != path] + + self._entries.append(IndexEntry("100644", blob_hash, path)) + self._entries.sort(key=lambda e: e.path) + + def build_tree_from_index(self) -> str: + return self.store.write_object(b"", "tree") + + def diff_working_tree_vs(self, tree_hash) -> DiffResult: + return DiffResult([], [], []) + + def checkout(self, tree_hash) -> None: + pass + + +def cmd_add(args) -> int: + wt = WorkingTree() + wt.stage_file(args.path) + return 0 + + +def cmd_status(args) -> int: + wt = WorkingTree() + for entry in wt.read_index(): + print(entry.path) + return 0 + + +def register_index_commands(subparsers) -> None: + add_parser = subparsers.add_parser("add", help="stage a file") + add_parser.add_argument("path") + add_parser.set_defaults(handler=cmd_add) + + status_parser = subparsers.add_parser("status", help="show staged files") + status_parser.set_defaults(handler=cmd_status) diff --git a/tests/test_index.py b/tests/test_index.py new file mode 100644 index 0000000..d5a62f2 --- /dev/null +++ b/tests/test_index.py @@ -0,0 +1,68 @@ +"""Tests for minigit/index.py - WorkingTree, IndexEntry, DiffResult.""" + +from minigit.index import WorkingTree + + +class FakeObjectStore: + """Stand-in for the real ObjectStore - keeps everything in memory.""" + + def __init__(self): + self.objects = {} + + def write_object(self, data, obj_type): + fake_hash = f"hash{len(data)}" + self.objects[fake_hash] = data + return fake_hash + + +def test_staging_adds_one_entry(tmp_path): + file = tmp_path / "hello.txt" + file.write_text("hi") + + wt = WorkingTree(repo_path=str(tmp_path), store=FakeObjectStore()) + wt.stage_file("hello.txt") + + entries = wt.read_index() + assert len(entries) == 1 + assert entries[0].path == "hello.txt" + + +def test_staging_twice_replaces_not_duplicates(tmp_path): + file = tmp_path / "hello.txt" + file.write_text("hi") + + wt = WorkingTree(repo_path=str(tmp_path), store=FakeObjectStore()) + wt.stage_file("hello.txt") + wt.stage_file("hello.txt") + + entries = wt.read_index() + assert len(entries) == 1 + + +def test_read_index_sorted(tmp_path): + (tmp_path / "z.txt").write_text("z") + (tmp_path / "a.txt").write_text("a") + + wt = WorkingTree(repo_path=str(tmp_path), store=FakeObjectStore()) + wt.stage_file("z.txt") + wt.stage_file("a.txt") + + entries = wt.read_index() + assert entries[0].path == "a.txt" + assert entries[1].path == "z.txt" + + +def test_editing_after_staging_does_not_change_entry(tmp_path): + file = tmp_path / "hello.txt" + file.write_text("original content") + + wt = WorkingTree(repo_path=str(tmp_path), store=FakeObjectStore()) + wt.stage_file("hello.txt") + + hash_before = wt.read_index()[0].hash + + file.write_text("changed content!!!") + + hash_after = wt.read_index()[0].hash + + assert hash_before == hash_after From 9ff19c53d5b477c8c70c5569947225d42b652f73 Mon Sep 17 00:00:00 2001 From: Mahi Shah <151894644+mahis1067@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:04:32 -0400 Subject: [PATCH 9/9] feat/m1-object-store-skeleton (#6) * feat/object-store-skeleton-cli-stub * fix/hash-cat-file-cli-tests * fix/test-hash-object-cli-print * fix/hash-character-placeholder * docs/cli-functions * fix: remove trailing whitespace in cat-file docstring Co-authored-by: mahis1067 <151894644+mahis1067@users.noreply.github.com> * fix/trailing-space --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- minigit/cli.py | 3 ++ minigit/objects.py | 81 +++++++++++++++++++++++++++++++++++ tests/test_objects.py | 98 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 tests/test_objects.py 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..2f19f75 100644 --- a/minigit/objects.py +++ b/minigit/objects.py @@ -8,3 +8,84 @@ 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: + """ + Hash the file at args.path, write it to the object store, and print the hash. + """ + 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: + """ + Print the contents of the object with the given hash. + """ + _, obj_data = _cli_store.read_object(args.hash) + print(obj_data.decode("utf-8", errors="replace"), end="") + return 0 + + +def register_subcommands(subparsers): + """ + Register the "hash-object" and "cat-file" subcommands with the given subparsers object. + """ + 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"