Skip to content

Commit 5eda156

Browse files
committed
Add TreeEntry model and update ls_tree tests for improved tree parsing
- Introduced `TreeEntry` dataclass to encapsulate tree entry properties for `ls_tree`. - Refactored `_parse_tree_content` to yield `TreeEntry` objects, enhancing type safety and clarity. - Updated `ls_tree` tests with detailed assertions for file and directory structure using Git tree entries. - Added `create_git_tree` fixture for consistent test repository setup.
1 parent 4f85afa commit 5eda156

3 files changed

Lines changed: 103 additions & 30 deletions

File tree

app/models/blob.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,3 @@
77
class Blob:
88
header: bytes
99
body: bytes
10-
11-
12-
@dataclass(frozen=True, kw_only=True)
13-
class Tree:
14-
entries: list[Blob]

app/models/git.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import sys
66
import zlib
7+
from dataclasses import dataclass
78
from enum import StrEnum, auto
89

910
__all__ = ["Git"]
@@ -29,6 +30,17 @@ def mode(self):
2930
raise ValueError(f"Invalid GitObject: {self}")
3031

3132

33+
@dataclass(frozen=True, kw_only=True)
34+
class TreeEntry:
35+
mode: bytes
36+
file_name: bytes
37+
raw_hash: bytes
38+
39+
@property
40+
def hash(self):
41+
return binascii.hexlify(self.raw_hash).decode()
42+
43+
3244
class Git:
3345
ignore_patterns = {".git", "__pycache__", ".pytest_cache", ".venv", "HEAD"}
3446

@@ -141,7 +153,7 @@ def create_tree(
141153
return tree_hash
142154

143155
@staticmethod
144-
def _parse_tree_content(content: bytes) -> Iterator[dict[str, bytes]]:
156+
def _parse_tree_content(content: bytes) -> Iterator[TreeEntry]:
145157
pattern = re.compile(
146158
rb"""
147159
(?P<mode>\d+)
@@ -153,7 +165,7 @@ def _parse_tree_content(content: bytes) -> Iterator[dict[str, bytes]]:
153165
re.VERBOSE,
154166
)
155167
for match in pattern.finditer(content):
156-
yield match.groupdict()
168+
yield TreeEntry(**match.groupdict())
157169

158170
def ls_tree(self, hash_value: str, *, name_only: bool = False):
159171
object_path = self.objects_folder / hash_value[:2] / hash_value[2:]
@@ -168,5 +180,5 @@ def ls_tree(self, hash_value: str, *, name_only: bool = False):
168180
entries = list(self._parse_tree_content(content))
169181
if name_only:
170182
for entry in entries:
171-
print(entry["file_name"].decode())
183+
print(entry.file_name.decode())
172184
return entries

tests/test_git.py

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import contextlib
22
import pathlib
3+
import subprocess
4+
from operator import attrgetter
35

46
import pytest
57

@@ -12,6 +14,52 @@ def change_to_tmp_dir(tmp_path):
1214
yield tmp_path
1315

1416

17+
@pytest.fixture
18+
def create_git_tree(change_to_tmp_dir):
19+
"""Create a test Git repository with a predefined structure.
20+
21+
Creates:
22+
parent_folder/
23+
├── file1.txt ("hello")
24+
└── child_folder/
25+
└── file2.txt ("world")
26+
27+
Returns:
28+
str: Hash value of the created Git tree
29+
"""
30+
31+
parent_folder = change_to_tmp_dir / "parent_folder"
32+
parent_folder.mkdir()
33+
file1 = parent_folder / "file1.txt"
34+
file1.write_text("hello")
35+
#
36+
child_folder = parent_folder / "child_folder"
37+
child_folder.mkdir()
38+
file2 = child_folder / "file2.txt"
39+
file2.write_text("world")
40+
41+
for cmd in [
42+
["git", "init", "."],
43+
["git", "add", "."],
44+
]:
45+
result = subprocess.run(
46+
cmd, cwd=change_to_tmp_dir, capture_output=True, text=True
47+
)
48+
if result.returncode != 0:
49+
raise RuntimeError(f"Failed to run git command: {cmd}\n{result.stderr}")
50+
51+
result = subprocess.run(
52+
["git", "write-tree"], cwd=change_to_tmp_dir, capture_output=True, text=True
53+
)
54+
if result.returncode != 0:
55+
raise RuntimeError(f"Failed to run git command: {cmd}\n{result.stderr}")
56+
hash_value = result.stdout.strip()
57+
if not hash_value or len(hash_value) != 40:
58+
raise RuntimeError(f"Failed to get tree hash: {hash_value}")
59+
60+
return hash_value
61+
62+
1563
class TestGit:
1664
def test_init_repo(self, change_to_tmp_dir):
1765
git = Git()
@@ -50,27 +98,45 @@ def test_hash_object(
5098
assert expected_path.exists() == write
5199
assert capsys.readouterr().out == hash_value
52100

53-
def test_write_tree(self, change_to_tmp_dir):
101+
def test_write_tree(self, create_git_tree):
54102
git = Git()
55-
git.init_repo()
103+
entries = git.ls_tree(create_git_tree)
104+
105+
# Test that we have exactly one entry (parent_folder)
106+
assert len(entries) == 1
107+
108+
# Test parent_folder properties
109+
parent_entry = entries[0]
110+
assert parent_entry.mode == b"40000" # Directory mode
111+
assert parent_entry.file_name == b"parent_folder"
112+
113+
# Get the contents of parent_folder
114+
parent_entries = git.ls_tree(parent_entry.hash)
115+
116+
# Should have two entries: file1.txt and child_folder
117+
assert len(parent_entries) == 2
118+
119+
# Sort entries by filename for consistent testing
120+
parent_entries.sort(key=attrgetter("file_name"))
121+
122+
# Test child_folder
123+
child_folder_entry = parent_entries[0]
124+
assert child_folder_entry.mode == b"40000" # Directory mode
125+
assert child_folder_entry.file_name == b"child_folder"
126+
127+
# Test file1.txt
128+
file1_entry = parent_entries[1]
129+
assert file1_entry.mode == b"100644" # File mode
130+
assert file1_entry.file_name == b"file1.txt"
131+
132+
# Test contents of child_folder
133+
child_entries = git.ls_tree(child_folder_entry.hash)
134+
135+
# Should have one file: file2.txt
136+
assert len(child_entries) == 1
137+
file2_entry = child_entries[0]
138+
assert file2_entry.mode == b"100644" # File mode
139+
assert file2_entry.file_name == b"file2.txt"
56140

57-
parent_dir = change_to_tmp_dir / "parent_dir"
58-
parent_dir.mkdir()
59-
file1 = parent_dir / "file1.txt"
60-
file1.write_text("hello")
61-
file2 = parent_dir / "file2.txt"
62-
file2.write_text("World")
63-
inner_dir = parent_dir / "inner_dir"
64-
inner_dir.mkdir()
65-
66-
inner_file1 = inner_dir / "inner_file1.txt"
67-
inner_file1.write_text("Hello World!")
68-
69-
# Create tree specifically for parent_dir
70-
hash_values = git.create_tree("parent_dir")
71-
72-
# Now we should see both files
73-
entries = git.ls_tree(hash_values, name_only=True)
74-
print(entries)
75-
return
76-
assert len(entries) == 2
141+
assert git.cat_file(file1_entry.hash).body == b"hello"
142+
assert git.cat_file(file2_entry.hash).body == b"world"

0 commit comments

Comments
 (0)