Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions minigit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from minigit import __version__
from minigit.errors import MiniGitError
from minigit.objects import register_subcommands


def _register_commands(subparsers) -> None:
Expand All @@ -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.")
Expand Down
72 changes: 72 additions & 0 deletions minigit/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<type> <len>\0<content>" 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)
98 changes: 98 additions & 0 deletions tests/test_objects.py
Original file line number Diff line number Diff line change
@@ -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"
Loading