Skip to content

Commit 2ddfb2e

Browse files
LesPrimusclaude
andcommitted
Add clone command with sideband response handling
Implement the git clone command by: - Adding clone subcommand to argument parser - Creating Git.clone() method to orchestrate cloning workflow - Adding _extract_pack_data() to handle sideband-encoded pack responses - Removing test __main__ code from clone.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7648a4f commit 2ddfb2e

4 files changed

Lines changed: 88 additions & 29 deletions

File tree

app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def main():
1919
return git.create_tree()
2020
case "commit-tree":
2121
return git.commit_tree(args.tree_hash, args.message, parent=args.parent)
22+
case "clone":
23+
return git.clone(args.url, args.work_dir)
2224
case _:
2325
raise RuntimeError(f"Unknown command #{args.command}")
2426

app/models/clone.py

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,57 @@ def send_want_request(self):
203203
with urlopen(request) as response:
204204
data = response.read()
205205

206-
if data.startswith(b"0008NAK\n"):
207-
return data[8:]
208-
return data
206+
return self._extract_pack_data(data)
207+
208+
@staticmethod
209+
def _extract_pack_data(data: bytes) -> bytes:
210+
"""Extract pack data from sideband-encoded response."""
211+
offset = 0
212+
pack_data = b""
213+
214+
while offset < len(data):
215+
# Read 4-byte hex length
216+
pkt_len_hex = data[offset:offset + 4]
217+
if len(pkt_len_hex) < 4:
218+
break
219+
220+
# Check if this looks like a hex length or raw PACK data
221+
try:
222+
pkt_len = int(pkt_len_hex, 16)
223+
except ValueError:
224+
# Not hex - might be raw PACK data (no sideband)
225+
if data[offset:offset + 4] == b"PACK":
226+
return data[offset:]
227+
break
228+
229+
if pkt_len == 0: # flush packet
230+
offset += 4
231+
break # end of response
232+
233+
# Get packet content (excluding length prefix)
234+
pkt_content = data[offset + 4:offset + pkt_len]
235+
offset += pkt_len
236+
237+
# Check for NAK/ACK lines
238+
if pkt_content.startswith(b"NAK"):
239+
continue
240+
if pkt_content.startswith(b"ACK"):
241+
continue
242+
243+
# Check for sideband channel byte
244+
if len(pkt_content) > 0:
245+
channel = pkt_content[0]
246+
if channel == 1: # pack data channel
247+
pack_data += pkt_content[1:]
248+
elif channel == 2: # progress channel
249+
continue
250+
elif channel == 3: # error channel
251+
raise RuntimeError(f"Server error: {pkt_content[1:].decode()}")
252+
elif pkt_content.startswith(b"PACK"):
253+
# No sideband, raw pack data starting in this packet
254+
return pkt_content
255+
256+
return pack_data
209257

210258
def parse_pack_header(self, data: bytes) -> PackHeader:
211259
"""Parse pack file header, return (version, num_objects)."""
@@ -375,29 +423,3 @@ def checkout(self, tree_sha: str, objects: dict[str, PackObject], dest: Path):
375423
elif obj.type == OBJ_TREE:
376424
path.mkdir(exist_ok=True)
377425
self.checkout(sha1, objects, path)
378-
379-
380-
if __name__ == "__main__":
381-
work_dir = Path("/tmp/test-clone")
382-
git_dir = work_dir / ".git"
383-
git_dir.mkdir(parents=True, exist_ok=True)
384-
385-
with GitClone(DEFAULT_URL) as clone:
386-
pack_data = clone.send_want_request()
387-
pack_header = clone.parse_pack_header(pack_data)
388-
objects = clone.parse_pack_objects(pack_data, pack_header.num_objects)
389-
stored = clone.store_objects(objects, git_dir)
390-
391-
# Find HEAD commit and checkout
392-
head_sha = clone.refs["HEAD"].sha1
393-
commit_obj = stored[head_sha]
394-
commit_info = clone.parse_commit(commit_obj.data)
395-
tree_sha = commit_info["tree"]
396-
397-
clone.checkout(tree_sha, stored, work_dir)
398-
399-
print(f"Cloned to {work_dir}")
400-
print(f"Files:")
401-
for f in work_dir.iterdir():
402-
if f.name != ".git":
403-
print(f" {f.name}")

app/models/git.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from os import PathLike
1515
from typing import Iterator
1616

17+
from app.models.clone import GitClone
18+
1719
NULL_BYTE = b"\x00"
1820

1921

@@ -237,3 +239,31 @@ def commit_tree(
237239
if pretty_print:
238240
sys.stdout.write(hash_value)
239241
return hash_value
242+
243+
def clone(self, url: str, working_directory: PathLike = "."):
244+
work_dir = pathlib.Path(working_directory)
245+
git_dir = work_dir / ".git"
246+
247+
# Initialize .git directory structure
248+
git_dir.mkdir(parents=True, exist_ok=True)
249+
(git_dir / "objects").mkdir(exist_ok=True)
250+
(git_dir / "refs").mkdir(exist_ok=True)
251+
(git_dir / "refs" / "heads").mkdir(exist_ok=True)
252+
253+
with GitClone(url) as clone:
254+
pack_data = clone.send_want_request()
255+
pack_header = clone.parse_pack_header(pack_data)
256+
objects = clone.parse_pack_objects(pack_data, pack_header.num_objects)
257+
stored = clone.store_objects(objects, git_dir)
258+
259+
# Find HEAD commit and checkout
260+
head_sha = clone.refs["HEAD"].sha1
261+
commit_obj = stored[head_sha]
262+
commit_info = clone.parse_commit(commit_obj.data)
263+
tree_sha = commit_info["tree"]
264+
265+
clone.checkout(tree_sha, stored, work_dir)
266+
267+
# Write refs/heads/main and HEAD
268+
(git_dir / "refs" / "heads" / "main").write_text(f"{head_sha}\n")
269+
(git_dir / "HEAD").write_text("ref: refs/heads/main\n")

app/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def get_parser():
3939
commit_tree_parser.add_argument("-m", "--message", default="Initial commit")
4040
commit_tree_parser.add_argument("-p", "--parent", default="")
4141

42+
# clone
43+
clone_parser = subparsers.add_parser("clone")
44+
clone_parser.add_argument("url")
45+
clone_parser.add_argument("work_dir", type=pathlib.Path)
46+
4247
return parser
4348

4449

0 commit comments

Comments
 (0)