From 246bc02377c736e955f920b20a491e992bb8808f Mon Sep 17 00:00:00 2001 From: Saanvi Tyagi Date: Sat, 19 Sep 2026 16:32:45 -0400 Subject: [PATCH 1/3] worked on tree and added tests --- minigit/index.py | 131 ++++++++++++++++++++--- tests/test_index.py | 249 ++++++++++++++++++++++++++++++++------------ 2 files changed, 303 insertions(+), 77 deletions(-) diff --git a/minigit/index.py b/minigit/index.py index 3566ca2..abcdb5d 100644 --- a/minigit/index.py +++ b/minigit/index.py @@ -91,17 +91,84 @@ def stage_file(self, path) -> None: self.write_index(entries) def build_tree_from_index(self) -> str: - return self.store.write_object(b"", "tree") + from minigit.objects import TreeEntry + + entries = self.read_index() + + root: dict = {} + for entry in entries: + parts = entry.path.split("/") + current = root + for part in parts[:-1]: + current = current.setdefault(part, {}) + current[parts[-1]] = entry + + def write_dir(node: dict) -> str: + tree_entries = [] + for name in sorted(node.keys()): + value = node[name] + if isinstance(value, dict): + subtree_hash = write_dir(value) + tree_entries.append( + TreeEntry(mode="40000", type="tree", hash=subtree_hash, name=name) + ) + else: + tree_entries.append( + TreeEntry(mode=value.mode, type="blob", hash=value.hash, name=name) + ) + return self.store.write_tree(tree_entries) + + return write_dir(root) + + def read_tree_entries(self, tree_hash: str) -> list[IndexEntry]: + entries = [] + + def walk(hash_, prefix): + for te in self.store.read_tree(hash_): + path = f"{prefix}/{te.name}" if prefix else te.name + if te.type == "tree": + walk(te.hash, path) + else: + entries.append(IndexEntry(te.mode, te.hash, path)) + + walk(tree_hash, "") + return sorted(entries, key=lambda e: e.path) def diff_working_tree_vs(self, tree_hash) -> DiffResult: - return DiffResult([], [], []) + result = DiffResult([], [], []) + tree_entries = self.read_tree_entries(tree_hash) + known_paths = {e.path for e in tree_entries} + + for entry in tree_entries: + full_path = os.path.join(self.root, entry.path) + if not os.path.isfile(full_path): + result.deleted.append(entry.path) + else: + with open(full_path, "rb") as f: + data = f.read() + current_hash = self.store.write_object(data, "blob") + current_mode = "100755" if os.access(full_path, os.X_OK) else "100644" + if current_hash != entry.hash or current_mode != entry.mode: + result.modified.append(entry.path) + + for dirpath, dirnames, filenames in os.walk(self.root): + dirnames[:] = [d for d in dirnames if d != ".minigit"] + for filename in filenames: + full_path = os.path.join(dirpath, filename) + rel_path = os.path.relpath(full_path, self.root).replace(os.sep, "/") + if rel_path not in known_paths: + result.added.append(rel_path) + + result.added.sort() + result.deleted.sort() + result.modified.sort() + return result def _working_status(self) -> DiffResult: result = DiffResult([], [], []) entries = self.read_index() known_paths = {e.path for e in entries} - # check staged entries against disk for entry in entries: full_path = f"{self.root}/{entry.path}" if not os.path.isfile(full_path): @@ -113,7 +180,6 @@ def _working_status(self) -> DiffResult: if current_hash != entry.hash: result.modified.append(entry.path) - # walk the working tree for untracked files for dirpath, dirnames, filenames in os.walk(self.root): dirnames[:] = [d for d in dirnames if d != ".minigit"] for filename in filenames: @@ -136,14 +202,53 @@ def cmd_add(args) -> int: def cmd_status(args) -> int: wt = WorkingTree() - result = wt._working_status() - entries = wt.read_index() - - # build each category once, upfront - changed_paths = result.modified + result.deleted - staged = [e.path for e in entries if e.path not in changed_paths] - not_staged = sorted(changed_paths) - untracked = sorted(result.added) + index_entries = wt.read_index() + index_by_path = {e.path: e for e in index_entries} + + from minigit.commits import get_head_tree + + head_tree_hash = get_head_tree() + head_entries = wt.read_tree_entries(head_tree_hash) if head_tree_hash else [] + head_by_path = {e.path: e for e in head_entries} + + # staged: HEAD tree vs index + staged_paths = set() + for path, entry in index_by_path.items(): + head_entry = head_by_path.get(path) + if head_entry is None or head_entry.hash != entry.hash or head_entry.mode != entry.mode: + staged_paths.add(path) + for path in head_by_path: + if path not in index_by_path: + staged_paths.add(path) + + # not staged: index vs disk + not_staged_paths = set() + for entry in index_entries: + full_path = os.path.join(wt.root, entry.path) + if not os.path.isfile(full_path): + not_staged_paths.add(entry.path) + else: + with open(full_path, "rb") as f: + data = f.read() + current_hash = wt.store.write_object(data, "blob") + current_mode = "100755" if os.access(full_path, os.X_OK) else "100644" + if current_hash != entry.hash or current_mode != entry.mode: + not_staged_paths.add(entry.path) + + # untracked: on disk, absent from both index and HEAD + known_paths = set(index_by_path) | set(head_by_path) + untracked_paths = set() + for dirpath, dirnames, filenames in os.walk(wt.root): + dirnames[:] = [d for d in dirnames if d != ".minigit"] + for filename in filenames: + full_path = os.path.join(dirpath, filename) + rel_path = os.path.relpath(full_path, wt.root).replace(os.sep, "/") + if rel_path not in known_paths: + untracked_paths.add(rel_path) + + staged = sorted(staged_paths) + not_staged = sorted(not_staged_paths) + untracked = sorted(untracked_paths) printed_anything = False @@ -177,4 +282,4 @@ def register_index_commands(subparsers) -> None: add_parser.set_defaults(handler=cmd_add) status_parser = subparsers.add_parser("status", help="show staged files") - status_parser.set_defaults(handler=cmd_status) + status_parser.set_defaults(handler=cmd_status) \ No newline at end of file diff --git a/tests/test_index.py b/tests/test_index.py index f2c4e18..81d9fa7 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,125 +1,246 @@ -"""Tests for minigit/index.py - WorkingTree, IndexEntry, DiffResult.""" +"""Tests for minigit/index.py - WorkingTree, trees, diffing, and status.""" -from minigit.index import WorkingTree +import sys +import types + +from minigit.index import WorkingTree, cmd_status from minigit.objects import ObjectStore -def test_index_works_new_workingtree(tmp_path): - (tmp_path / "hello.txt").write_text("hi") +def test_build_tree_empty_index(tmp_path): store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) - wt1 = WorkingTree(repo_path=str(tmp_path), store=store) - wt1.stage_file("hello.txt") + tree_hash = wt.build_tree_from_index() - # a brand new WorkingTree, simulating a separate CLI invocation - wt2 = WorkingTree(repo_path=str(tmp_path), store=store) - entries = wt2.read_index() + obj_type, data = store.read_object(tree_hash) + assert obj_type == "tree" + assert data == b"" - assert len(entries) == 1 - assert entries[0].path == "hello.txt" +def test_build_tree_nested_directories(tmp_path): + (tmp_path / "a.txt").write_text("hello") + (tmp_path / "src").mkdir() + (tmp_path / "src" / "b.txt").write_text("world") -def test_staging_twice(tmp_path): - (tmp_path / "hello.txt").write_text("hi") store = ObjectStore(str(tmp_path)) wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("a.txt") + wt.stage_file("src/b.txt") - wt.stage_file("hello.txt") - wt.stage_file("hello.txt") + tree_hash = wt.build_tree_from_index() + entries = wt.read_tree_entries(tree_hash) - entries = wt.read_index() - assert len(entries) == 1 + assert len(entries) == 2 + paths = [e.path for e in entries] + assert "a.txt" in paths + assert "src/b.txt" in paths -def test_staging_equivalent_paths_updates_one_entry(tmp_path): - file = tmp_path / "hello.txt" - file.write_text("original") - wt = WorkingTree(repo_path=str(tmp_path)) - wt.stage_file("hello.txt") +def test_build_tree_deterministic(tmp_path): + (tmp_path / "a.txt").write_text("hello") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("a.txt") + + hash1 = wt.build_tree_from_index() + hash2 = wt.build_tree_from_index() + + assert hash1 == hash2 + - file.write_text("updated") - wt.stage_file("./hello.txt") +def test_build_tree_path_with_space(tmp_path): + (tmp_path / "my notes.txt").write_text("hi") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("my notes.txt") + + tree_hash = wt.build_tree_from_index() + entries = wt.read_tree_entries(tree_hash) - entries = wt.read_index() assert len(entries) == 1 - assert entries[0].path == "hello.txt" - assert wt.store.read_object(entries[0].hash) == ("blob", b"updated") - status = wt._working_status() - assert status.added == [] - assert status.modified == [] + assert entries[0].path == "my notes.txt" -def test_index_file(tmp_path): - (tmp_path / "z.txt").write_text("z") +def test_build_tree_executable_mode_preserved(tmp_path): + file = tmp_path / "run.sh" + file.write_text("#!/bin/sh\necho hi") + file.chmod(0o755) + store = ObjectStore(str(tmp_path)) wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("run.sh") - wt.stage_file("z.txt") + tree_hash = wt.build_tree_from_index() + entries = wt.read_tree_entries(tree_hash) + + assert len(entries) == 1 + assert entries[0].mode == "100755" - index_path = tmp_path / ".minigit" / "index" - lines = index_path.read_text(encoding="utf-8").splitlines() - assert len(lines) == 1 +def test_read_tree_entries_sorted(tmp_path): + (tmp_path / "z.txt").write_text("z") (tmp_path / "a.txt").write_text("a") + + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("z.txt") wt.stage_file("a.txt") - lines = index_path.read_text(encoding="utf-8").splitlines() - assert len(lines) == 2 - assert lines[0].endswith("a.txt") - assert lines[1].endswith("z.txt") + tree_hash = wt.build_tree_from_index() + entries = wt.read_tree_entries(tree_hash) + assert entries[0].path == "a.txt" + assert entries[1].path == "z.txt" + + +def test_diff_detects_modified_after_staging(tmp_path): + file = tmp_path / "hello.txt" + file.write_text("original") -def test_path_with_space(tmp_path): - (tmp_path / "my notes.txt").write_text("hello") store = ObjectStore(str(tmp_path)) wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("hello.txt") - wt.stage_file("my notes.txt") + tree_hash = wt.build_tree_from_index() - entries = wt.read_index() - assert len(entries) == 1 - assert entries[0].path == "my notes.txt" + file.write_text("changed!!!") + diff = wt.diff_working_tree_vs(tree_hash) + assert "hello.txt" in diff.modified + assert "hello.txt" not in diff.added + assert "hello.txt" not in diff.deleted -def test_edit_after_staging(tmp_path): + +def test_diff_detects_deleted(tmp_path): file = tmp_path / "hello.txt" - file.write_text("original") + file.write_text("hi") + store = ObjectStore(str(tmp_path)) wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("hello.txt") + tree_hash = wt.build_tree_from_index() + file.unlink() + + diff = wt.diff_working_tree_vs(tree_hash) + assert "hello.txt" in diff.deleted + + +def test_diff_detects_added(tmp_path): + (tmp_path / "hello.txt").write_text("hi") + + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) wt.stage_file("hello.txt") - hash_before = wt.read_index()[0].hash + tree_hash = wt.build_tree_from_index() - file.write_text("changed!!!") + (tmp_path / "extra.txt").write_text("new file, never staged") + + diff = wt.diff_working_tree_vs(tree_hash) + assert "extra.txt" in diff.added + assert "hello.txt" not in diff.added + + +def test_diff_excludes_minigit_folder(tmp_path): + (tmp_path / "hello.txt").write_text("hi") - hash_after = wt.read_index()[0].hash - assert hash_before == hash_after # snapshot rule still holds + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("hello.txt") + tree_hash = wt.build_tree_from_index() - status = wt._working_status() - assert "hello.txt" in status.modified + diff = wt.diff_working_tree_vs(tree_hash) + assert not any(p.startswith(".minigit") for p in diff.added) -def test_untracked(tmp_path): +def test_diff_no_changes_reports_nothing(tmp_path): (tmp_path / "hello.txt").write_text("hi") - (tmp_path / "extra.txt").write_text("not staged at all") + store = ObjectStore(str(tmp_path)) wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("hello.txt") + tree_hash = wt.build_tree_from_index() + + diff = wt.diff_working_tree_vs(tree_hash) + assert diff.added == [] + assert diff.deleted == [] + assert diff.modified == [] + + +# --- cmd_status --- +# minigit.commits.get_head_tree isn't merged yet, so these tests fake it with +# monkeypatch + +def _fake_commits_module(monkeypatch, head_tree_hash): + """Install a fake minigit.commits module with a get_head_tree() that + returns the given value (None for 'unborn HEAD', or a tree hash str).""" + fake_module = types.ModuleType("minigit.commits") + fake_module.get_head_tree = lambda: head_tree_hash + monkeypatch.setitem(sys.modules, "minigit.commits", fake_module) + + +class _Args: + """Minimal stand-in for argparse's Namespace, since cmd_status(args) + doesn't actually read any attributes off args.""" + +def test_status_clean_after_commit(tmp_path, monkeypatch, capsys): + (tmp_path / "hello.txt").write_text("hi") + + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) wt.stage_file("hello.txt") + tree_hash = wt.build_tree_from_index() + + monkeypatch.chdir(tmp_path) + _fake_commits_module(monkeypatch, tree_hash) + + cmd_status(_Args()) - status = wt._working_status() - assert "extra.txt" in status.added - assert "hello.txt" not in status.added + output = capsys.readouterr().out + assert output.strip() == "clean" -def test_deleted(tmp_path): +def test_status_staged_and_unstaged_on_one_file(tmp_path, monkeypatch, capsys): file = tmp_path / "hello.txt" - file.write_text("hi") + file.write_text("v1") + store = ObjectStore(str(tmp_path)) wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("hello.txt") + committed_tree_hash = wt.build_tree_from_index() # simulates "last commit" + # stage a change (now differs from the committed tree -> staged) + file.write_text("v2") wt.stage_file("hello.txt") - file.unlink() - status = wt._working_status() - assert "hello.txt" in status.deleted + # edit again without re-staging (now differs from index -> not staged) + file.write_text("v3") + + monkeypatch.chdir(tmp_path) + _fake_commits_module(monkeypatch, committed_tree_hash) + + cmd_status(_Args()) + + output = capsys.readouterr().out + assert "staged:" in output + assert "not staged:" in output + assert "hello.txt" in output + + +def test_status_unborn_head_shows_all_staged(tmp_path, monkeypatch, capsys): + (tmp_path / "hello.txt").write_text("hi") + + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + wt.stage_file("hello.txt") + + monkeypatch.chdir(tmp_path) + _fake_commits_module(monkeypatch, None) # unborn HEAD + + cmd_status(_Args()) + + output = capsys.readouterr().out + assert "staged:" in output + assert "hello.txt" in output \ No newline at end of file From 5529eaf8b9a02fbd8da1e3d7d2f8310e2bc118a3 Mon Sep 17 00:00:00 2001 From: Saanvi Tyagi Date: Sat, 19 Sep 2026 16:51:05 -0400 Subject: [PATCH 2/3] Fixed formatting --- minigit/index.py | 2 +- tests/test_index.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/minigit/index.py b/minigit/index.py index abcdb5d..922bf51 100644 --- a/minigit/index.py +++ b/minigit/index.py @@ -282,4 +282,4 @@ def register_index_commands(subparsers) -> None: add_parser.set_defaults(handler=cmd_add) status_parser = subparsers.add_parser("status", help="show staged files") - status_parser.set_defaults(handler=cmd_status) \ No newline at end of file + status_parser.set_defaults(handler=cmd_status) diff --git a/tests/test_index.py b/tests/test_index.py index 81d9fa7..4cbfd0c 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -172,6 +172,7 @@ def test_diff_no_changes_reports_nothing(tmp_path): # minigit.commits.get_head_tree isn't merged yet, so these tests fake it with # monkeypatch + def _fake_commits_module(monkeypatch, head_tree_hash): """Install a fake minigit.commits module with a get_head_tree() that returns the given value (None for 'unborn HEAD', or a tree hash str).""" @@ -243,4 +244,4 @@ def test_status_unborn_head_shows_all_staged(tmp_path, monkeypatch, capsys): output = capsys.readouterr().out assert "staged:" in output - assert "hello.txt" in output \ No newline at end of file + assert "hello.txt" in output From 0a518647d89941a9454a8f5940c4d75f8fc78ebf Mon Sep 17 00:00:00 2001 From: aman shah Date: Mon, 21 Sep 2026 14:55:51 -0400 Subject: [PATCH 3/3] Fix status integration and preserve index regression coverage --- minigit/index.py | 17 +++-- tests/test_index.py | 48 +++++++----- tests/test_index_persistence.py | 125 ++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 tests/test_index_persistence.py diff --git a/minigit/index.py b/minigit/index.py index 922bf51..8ecdcd5 100644 --- a/minigit/index.py +++ b/minigit/index.py @@ -146,7 +146,7 @@ def diff_working_tree_vs(self, tree_hash) -> DiffResult: else: with open(full_path, "rb") as f: data = f.read() - current_hash = self.store.write_object(data, "blob") + current_hash = self.store.hash_object(data, "blob") current_mode = "100755" if os.access(full_path, os.X_OK) else "100644" if current_hash != entry.hash or current_mode != entry.mode: result.modified.append(entry.path) @@ -176,8 +176,9 @@ def _working_status(self) -> DiffResult: else: with open(full_path, "rb") as f: data = f.read() - current_hash = self.store.write_object(data, "blob") - if current_hash != entry.hash: + current_hash = self.store.hash_object(data, "blob") + current_mode = "100755" if os.access(full_path, os.X_OK) else "100644" + if current_hash != entry.hash or current_mode != entry.mode: result.modified.append(entry.path) for dirpath, dirnames, filenames in os.walk(self.root): @@ -205,9 +206,9 @@ def cmd_status(args) -> int: index_entries = wt.read_index() index_by_path = {e.path: e for e in index_entries} - from minigit.commits import get_head_tree + from minigit.commits import CommitManager - head_tree_hash = get_head_tree() + head_tree_hash = CommitManager(wt.root, store=wt.store, tree=wt).get_head_tree() head_entries = wt.read_tree_entries(head_tree_hash) if head_tree_hash else [] head_by_path = {e.path: e for e in head_entries} @@ -230,13 +231,13 @@ def cmd_status(args) -> int: else: with open(full_path, "rb") as f: data = f.read() - current_hash = wt.store.write_object(data, "blob") + current_hash = wt.store.hash_object(data, "blob") current_mode = "100755" if os.access(full_path, os.X_OK) else "100644" if current_hash != entry.hash or current_mode != entry.mode: not_staged_paths.add(entry.path) - # untracked: on disk, absent from both index and HEAD - known_paths = set(index_by_path) | set(head_by_path) + # untracked: on disk, absent from the index + known_paths = set(index_by_path) untracked_paths = set() for dirpath, dirnames, filenames in os.walk(wt.root): dirnames[:] = [d for d in dirnames if d != ".minigit"] diff --git a/tests/test_index.py b/tests/test_index.py index 4cbfd0c..8e0df5d 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,8 +1,6 @@ """Tests for minigit/index.py - WorkingTree, trees, diffing, and status.""" -import sys -import types - +from minigit.commits import CommitManager from minigit.index import WorkingTree, cmd_status from minigit.objects import ObjectStore @@ -168,17 +166,7 @@ def test_diff_no_changes_reports_nothing(tmp_path): assert diff.modified == [] -# --- cmd_status --- -# minigit.commits.get_head_tree isn't merged yet, so these tests fake it with -# monkeypatch - - -def _fake_commits_module(monkeypatch, head_tree_hash): - """Install a fake minigit.commits module with a get_head_tree() that - returns the given value (None for 'unborn HEAD', or a tree hash str).""" - fake_module = types.ModuleType("minigit.commits") - fake_module.get_head_tree = lambda: head_tree_hash - monkeypatch.setitem(sys.modules, "minigit.commits", fake_module) +# --- cmd_status against real commit objects --- class _Args: @@ -195,7 +183,7 @@ def test_status_clean_after_commit(tmp_path, monkeypatch, capsys): tree_hash = wt.build_tree_from_index() monkeypatch.chdir(tmp_path) - _fake_commits_module(monkeypatch, tree_hash) + CommitManager(tmp_path).create_commit(tree_hash, [], "Test", "initial") cmd_status(_Args()) @@ -220,7 +208,7 @@ def test_status_staged_and_unstaged_on_one_file(tmp_path, monkeypatch, capsys): file.write_text("v3") monkeypatch.chdir(tmp_path) - _fake_commits_module(monkeypatch, committed_tree_hash) + CommitManager(tmp_path).create_commit(committed_tree_hash, [], "Test", "initial") cmd_status(_Args()) @@ -238,10 +226,36 @@ def test_status_unborn_head_shows_all_staged(tmp_path, monkeypatch, capsys): wt.stage_file("hello.txt") monkeypatch.chdir(tmp_path) - _fake_commits_module(monkeypatch, None) # unborn HEAD cmd_status(_Args()) output = capsys.readouterr().out assert "staged:" in output assert "hello.txt" in output + + +def test_status_and_diff_do_not_store_unstaged_content(tmp_path, monkeypatch, capsys): + file = tmp_path / "file" + file.write_text("committed") + wt = WorkingTree(tmp_path) + wt.stage_file("file") + tree = wt.build_tree_from_index() + CommitManager(tmp_path).create_commit(tree, [], "Test", "initial") + before = set(wt.store.objects_dir.rglob("*")) + file.write_text("unstaged") + monkeypatch.chdir(tmp_path) + cmd_status(_Args()) + assert capsys.readouterr().out == "not staged:\n file\n" + assert wt.diff_working_tree_vs(tree).modified == ["file"] + assert set(wt.store.objects_dir.rglob("*")) == before + + +def test_status_reports_file_removed_from_index_as_untracked(tmp_path, monkeypatch, capsys): + (tmp_path / "file").write_text("content") + wt = WorkingTree(tmp_path) + wt.stage_file("file") + CommitManager(tmp_path).create_commit(wt.build_tree_from_index(), [], "Test", "initial") + wt.write_index([]) + monkeypatch.chdir(tmp_path) + cmd_status(_Args()) + assert capsys.readouterr().out == "staged:\n file\nuntracked:\n file\n" diff --git a/tests/test_index_persistence.py b/tests/test_index_persistence.py new file mode 100644 index 0000000..f2c4e18 --- /dev/null +++ b/tests/test_index_persistence.py @@ -0,0 +1,125 @@ +"""Tests for minigit/index.py - WorkingTree, IndexEntry, DiffResult.""" + +from minigit.index import WorkingTree +from minigit.objects import ObjectStore + + +def test_index_works_new_workingtree(tmp_path): + (tmp_path / "hello.txt").write_text("hi") + store = ObjectStore(str(tmp_path)) + + wt1 = WorkingTree(repo_path=str(tmp_path), store=store) + wt1.stage_file("hello.txt") + + # a brand new WorkingTree, simulating a separate CLI invocation + wt2 = WorkingTree(repo_path=str(tmp_path), store=store) + entries = wt2.read_index() + + assert len(entries) == 1 + assert entries[0].path == "hello.txt" + + +def test_staging_twice(tmp_path): + (tmp_path / "hello.txt").write_text("hi") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + + wt.stage_file("hello.txt") + wt.stage_file("hello.txt") + + entries = wt.read_index() + assert len(entries) == 1 + + +def test_staging_equivalent_paths_updates_one_entry(tmp_path): + file = tmp_path / "hello.txt" + file.write_text("original") + wt = WorkingTree(repo_path=str(tmp_path)) + wt.stage_file("hello.txt") + + file.write_text("updated") + wt.stage_file("./hello.txt") + + entries = wt.read_index() + assert len(entries) == 1 + assert entries[0].path == "hello.txt" + assert wt.store.read_object(entries[0].hash) == ("blob", b"updated") + status = wt._working_status() + assert status.added == [] + assert status.modified == [] + + +def test_index_file(tmp_path): + (tmp_path / "z.txt").write_text("z") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + + wt.stage_file("z.txt") + + index_path = tmp_path / ".minigit" / "index" + lines = index_path.read_text(encoding="utf-8").splitlines() + assert len(lines) == 1 + + (tmp_path / "a.txt").write_text("a") + wt.stage_file("a.txt") + + lines = index_path.read_text(encoding="utf-8").splitlines() + assert len(lines) == 2 + assert lines[0].endswith("a.txt") + assert lines[1].endswith("z.txt") + + +def test_path_with_space(tmp_path): + (tmp_path / "my notes.txt").write_text("hello") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + + wt.stage_file("my notes.txt") + + entries = wt.read_index() + assert len(entries) == 1 + assert entries[0].path == "my notes.txt" + + +def test_edit_after_staging(tmp_path): + file = tmp_path / "hello.txt" + file.write_text("original") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + + wt.stage_file("hello.txt") + hash_before = wt.read_index()[0].hash + + file.write_text("changed!!!") + + hash_after = wt.read_index()[0].hash + assert hash_before == hash_after # snapshot rule still holds + + status = wt._working_status() + assert "hello.txt" in status.modified + + +def test_untracked(tmp_path): + (tmp_path / "hello.txt").write_text("hi") + (tmp_path / "extra.txt").write_text("not staged at all") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + + wt.stage_file("hello.txt") + + status = wt._working_status() + assert "extra.txt" in status.added + assert "hello.txt" not in status.added + + +def test_deleted(tmp_path): + file = tmp_path / "hello.txt" + file.write_text("hi") + store = ObjectStore(str(tmp_path)) + wt = WorkingTree(repo_path=str(tmp_path), store=store) + + wt.stage_file("hello.txt") + file.unlink() + + status = wt._working_status() + assert "hello.txt" in status.deleted