Skip to content

Commit b746e55

Browse files
Implement local git reading. (#3)
2 parents fd50de6 + 85bb9b2 commit b746e55

File tree

5 files changed

+268
-41
lines changed

5 files changed

+268
-41
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ types = [
4242

4343
[project.scripts]
4444

45-
commit = "bot.cli.commit:main"
45+
read-commit = "bot.cli.commit:read"
46+
write-commit = "bot.cli.commit:write"
4647

4748
[build-system]
4849

src/bot/cli/commit.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,56 @@
11
from pathlib import Path
22

33
from click import Path as PathType
4-
from click import command, option
4+
from click import command, option, secho
55
from github.Auth import AppAuth
66

7+
from ..dtos import Tree
78
from ..local import read_commit
89
from ..remote import write_commit
910

1011

12+
def print_tree(tree: Tree, parent: Path, depth: int = 0) -> None:
13+
slash = "\\"
14+
space = " " * depth
15+
16+
secho(f"{space}{slash} {tree.path.relative_to(parent)}", fg="green")
17+
18+
for blob in tree.blobs:
19+
operation = "+" if blob.sha else "-"
20+
color = "cyan" if blob.sha else "red"
21+
secho(f"{space}|- ({operation}) {blob.path.relative_to(tree.path)}", fg=color)
22+
23+
for child in tree.trees:
24+
print_tree(child, tree.path, depth + 1)
25+
26+
27+
@command()
28+
@option(
29+
"--repo-path",
30+
"--repo",
31+
type=PathType(
32+
exists=True,
33+
file_okay=False,
34+
dir_okay=True,
35+
path_type=Path,
36+
),
37+
required=True,
38+
)
39+
@option(
40+
"--ref",
41+
required=True,
42+
)
43+
def read(
44+
*,
45+
repo_path: Path,
46+
ref: str,
47+
) -> None:
48+
commit = read_commit(repo_path, ref)
49+
50+
secho(f"Commit: {commit.message}", fg="green")
51+
print_tree(commit.tree, Path())
52+
53+
1154
@command()
1255
@option(
1356
"--application-id",
@@ -36,7 +79,7 @@
3679
"--ref",
3780
required=True,
3881
)
39-
def main(
82+
def write(
4083
*,
4184
application_id: str,
4285
private_key: str,

src/bot/dtos.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
"""Data transfer objects between git implementations."""
2-
from dataclasses import dataclass
3-
4-
from .enums import Mode, Type
2+
from dataclasses import dataclass, field
3+
from pathlib import Path
54

65

76
@dataclass(frozen=True)
87
class Blob:
98
"""A blob references file content."""
109

11-
path: str
12-
sha: str
10+
path: Path
11+
sha: str | None
1312

1413

1514
@dataclass(frozen=True)
1615
class Tree:
1716
"""A tree references one or other trees or blobs."""
1817

19-
path: str
20-
mode: Mode
21-
sha: str
22-
type: Type
18+
path: Path
2319

24-
blobs: list[Blob]
25-
trees: list["Tree"]
20+
blobs: list[Blob] = field(default_factory=list)
21+
trees: list["Tree"] = field(default_factory=list)
2622

2723

2824
@dataclass(frozen=True)

src/bot/local.py

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,138 @@
11
from pathlib import Path
2+
from typing import Generator
23

3-
from git import Repo
4+
from git import Diff, Repo
5+
from git.objects import Commit
46

5-
from .dtos import Commit, Tree
6-
from .enums import Mode, Type
7+
from .dtos import Blob as BlobDTO
8+
from .dtos import Commit as CommitDTO
9+
from .dtos import Tree as TreeDTO
710

11+
ROOT = Path()
812

9-
def read_commit(repo_path: Path, ref: str) -> Commit:
10-
# Find the commit at the given ref in the local repo.
11-
repo = Repo(repo_path)
12-
commit = repo.commit(ref)
1313

14+
def iter_blobs(item: Diff) -> Generator[BlobDTO, None, None]:
15+
match (item.change_type):
16+
case "A":
17+
# File added
18+
assert item.b_path is not None
19+
assert item.b_blob is not None
20+
21+
yield BlobDTO(
22+
path=Path(item.b_path.lstrip("/")),
23+
sha=item.b_blob.hexsha,
24+
)
25+
case "D":
26+
# File deleted
27+
assert item.a_path is not None
28+
29+
yield BlobDTO(
30+
path=Path(item.a_path.lstrip("/")),
31+
sha=None,
32+
)
33+
case "C":
34+
# File copied
35+
assert item.a_path is not None
36+
assert item.a_blob is not None
37+
assert item.b_path is not None
38+
assert item.b_blob is not None
39+
40+
yield BlobDTO(
41+
path=Path(item.b_path.lstrip("/")),
42+
sha=item.b_blob.hexsha,
43+
)
44+
case "R":
45+
# File renamed
46+
assert item.a_path is not None
47+
assert item.a_blob is not None
48+
assert item.b_path is not None
49+
assert item.b_blob is not None
50+
51+
yield BlobDTO(
52+
path=Path(item.a_path.lstrip("/")),
53+
sha=None,
54+
)
55+
yield BlobDTO(
56+
path=Path(item.b_path.lstrip("/")),
57+
sha=item.b_blob.hexsha,
58+
)
59+
case "M":
60+
# File modified
61+
assert item.b_path is not None
62+
assert item.b_blob is not None
63+
64+
yield BlobDTO(
65+
path=Path(item.b_path.lstrip("/")),
66+
sha=item.b_blob.hexsha,
67+
)
68+
case "T":
69+
# File changed type (TODO)
70+
raise NotImplementedError("Diff type 'T' is not supported.")
71+
case _:
72+
raise ValueError(f"Unexpected diff type: {item.change_type}")
73+
74+
75+
def build_trees(blobs: list[BlobDTO]) -> TreeDTO:
76+
trees: dict[Path, TreeDTO] = {}
77+
78+
# Create the root tree
79+
root = trees[ROOT] = TreeDTO(path=ROOT)
80+
81+
# Create all trees
82+
for blob in blobs:
83+
path = blob.path.parent
84+
while path != ROOT:
85+
trees.setdefault(path, TreeDTO(path))
86+
path = path.parent
87+
88+
# Attach blobs to trees
89+
for blob in blobs:
90+
trees[blob.path.parent].blobs.append(blob)
91+
92+
# Attach all trees to their parents
93+
for tree in trees.values():
94+
if tree.path == ROOT:
95+
continue
96+
97+
trees[tree.path.parent].trees.append(tree)
98+
99+
return root
100+
101+
102+
def find_parent(commit: Commit) -> Commit:
14103
if not commit.parents:
15104
raise ValueError("Cannot create a repo's initial commit.")
16105

17106
if len(commit.parents) > 1:
18107
raise ValueError("Cannot create a merge commit.")
19108

20-
parent = commit.parents[0]
109+
return commit.parents[0]
21110

22-
# Compute the diff between the commit and its parent
23-
diff = commit.diff(parent.hexsha)
24-
25-
# Inspect the diff to create a commit
26-
for item in diff:
27-
# TODO
28-
pass
29111

112+
def extract_message(commit: Commit) -> str:
30113
if isinstance(commit.message, str):
31-
message = commit.message
114+
return commit.message
32115
else:
33-
message = commit.message.decode("utf-8")
116+
return commit.message.decode("utf-8")
117+
118+
119+
def read_commit(repo_path: Path, ref: str) -> CommitDTO:
120+
# Find the commit at the given ref in the local repo.
121+
repo = Repo(repo_path)
122+
commit = repo.commit(ref)
123+
124+
# Compute the diff between the parent and this commit
125+
parent = find_parent(commit)
126+
diff = parent.diff(commit.hexsha)
127+
128+
# Inspect the diff to create a commit
129+
blobs = [blob for diff_item in diff for blob in iter_blobs(diff_item)]
130+
131+
root = build_trees(blobs)
34132

35-
return Commit(
133+
return CommitDTO(
36134
base_tree=parent.tree.hexsha,
37-
message=message,
135+
message=extract_message(commit),
38136
parents=[parent.hexsha],
39-
tree=Tree(
40-
path="",
41-
mode=Mode.SUBDIRECTORY,
42-
type=Type.TREE,
43-
blobs=[],
44-
trees=[],
45-
sha="",
46-
),
137+
tree=root,
47138
)

src/bot/tests/test_local.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from pathlib import Path
2+
from unittest.mock import MagicMock
3+
4+
from git import Diff
5+
from pytest import fixture
6+
7+
from bot.dtos import Blob
8+
from bot.local import build_trees, iter_blobs
9+
10+
11+
@fixture
12+
def diff() -> Diff:
13+
item = MagicMock()
14+
item.a_path = "foo"
15+
item.a_blob.hexsha = "0000"
16+
item.b_path = "bar"
17+
item.b_blob.hexsha = "1111"
18+
return item
19+
20+
21+
def test_iter_blobs_a(diff: Diff) -> None:
22+
diff.change_type = "A"
23+
24+
blobs = list(iter_blobs(diff))
25+
assert len(blobs) == 1
26+
assert str(blobs[0].path) == "bar"
27+
assert blobs[0].sha is not None
28+
29+
30+
def test_iter_blobs_d(diff: Diff) -> None:
31+
diff.change_type = "D"
32+
33+
blobs = list(iter_blobs(diff))
34+
assert len(blobs) == 1
35+
assert str(blobs[0].path) == "foo"
36+
assert blobs[0].sha is None
37+
38+
39+
def test_iter_blobs_c(diff: Diff) -> None:
40+
diff.change_type = "C"
41+
42+
blobs = list(iter_blobs(diff))
43+
assert len(blobs) == 1
44+
assert str(blobs[0].path) == "bar"
45+
assert blobs[0].sha is not None
46+
47+
48+
def test_iter_blobs_r(diff: Diff) -> None:
49+
diff.change_type = "R"
50+
51+
blobs = list(iter_blobs(diff))
52+
assert len(blobs) == 2
53+
assert str(blobs[0].path) == "foo"
54+
assert blobs[0].sha is None
55+
assert str(blobs[1].path) == "bar"
56+
assert blobs[1].sha is not None
57+
58+
59+
def test_iter_blobs_m(diff: Diff) -> None:
60+
diff.change_type = "M"
61+
62+
blobs = list(iter_blobs(diff))
63+
assert len(blobs) == 1
64+
assert str(blobs[0].path) == "bar"
65+
assert blobs[0].sha is not None
66+
67+
68+
def test_build_trees() -> None:
69+
blobs = [
70+
Blob(path=Path("foo/bar/baz.txt"), sha="0000"),
71+
Blob(path=Path("foo/qux.html"), sha="1111"),
72+
]
73+
74+
root = build_trees(blobs)
75+
76+
assert root.path == Path()
77+
assert len(root.blobs) == 0
78+
assert len(root.trees) == 1
79+
80+
foo = root.trees[0]
81+
assert foo.path == Path("foo")
82+
assert len(foo.blobs) == 1
83+
assert len(foo.trees) == 1
84+
85+
qux = foo.blobs[0]
86+
assert qux.path == Path("foo/qux.html")
87+
assert qux.sha is not None
88+
89+
bar = foo.trees[0]
90+
assert bar.path == Path("foo/bar")
91+
assert len(bar.blobs) == 1
92+
assert len(bar.trees) == 0
93+
94+
baz = bar.blobs[0]
95+
assert baz.path == Path("foo/bar/baz.txt")
96+
assert baz.sha is not None

0 commit comments

Comments
 (0)