1+ import hashlib
12import re
3+ import struct
4+ import zlib
25from dataclasses import dataclass
6+ from functools import cache
7+ from pprint import pprint
38
49DEFAULT_URL = "https://github.com/octocat/Hello-World"
510
@@ -30,6 +35,106 @@ def from_line(cls, line: str):
3035 return cls (length , sha1 , ref_name )
3136
3237
38+ @dataclass
39+ class PackHeader :
40+ version : int
41+ num_objects : int
42+
43+ @classmethod
44+ def from_bytes (cls , data : bytes ):
45+ version , num_objects = struct .unpack ('>II' , data [4 :12 ])
46+ return cls (version , num_objects )
47+
48+ @dataclass
49+ class PackObject :
50+ type : int
51+ size : int
52+ data : bytes
53+ pack_offset : int = 0 # offset in pack file where this object starts
54+
55+
56+ # Object type constants
57+ OBJ_COMMIT = 1
58+ OBJ_TREE = 2
59+ OBJ_BLOB = 3
60+ OBJ_TAG = 4
61+ OBJ_OFS_DELTA = 6
62+ OBJ_REF_DELTA = 7
63+
64+ TYPE_NAMES = {
65+ OBJ_COMMIT : "commit" ,
66+ OBJ_TREE : "tree" ,
67+ OBJ_BLOB : "blob" ,
68+ OBJ_TAG : "tag" ,
69+ }
70+
71+
72+ def compute_sha1 (obj_type : int , data : bytes ) -> str :
73+ """Compute git object SHA-1."""
74+ type_name = TYPE_NAMES [obj_type ]
75+ header = f"{ type_name } { len (data )} \x00 " .encode ()
76+ return hashlib .sha1 (header + data ).hexdigest ()
77+
78+
79+ def read_delta_size (delta : bytes , offset : int ) -> tuple [int , int ]:
80+ """Read size varint from delta header."""
81+ size = 0
82+ shift = 0
83+ while True :
84+ byte = delta [offset ]
85+ size |= (byte & 0x7F ) << shift
86+ offset += 1
87+ if not (byte & 0x80 ):
88+ break
89+ shift += 7
90+ return size , offset
91+
92+
93+ def apply_delta (base : bytes , delta : bytes ) -> bytes :
94+ """Apply delta instructions to base object."""
95+ offset = 0
96+ _base_size , offset = read_delta_size (delta , offset )
97+ _result_size , offset = read_delta_size (delta , offset )
98+
99+ result = bytearray ()
100+ while offset < len (delta ):
101+ cmd = delta [offset ]
102+ offset += 1
103+
104+ if cmd & 0x80 : # Copy from base
105+ copy_offset = 0
106+ copy_size = 0
107+ if cmd & 0x01 :
108+ copy_offset |= delta [offset ]
109+ offset += 1
110+ if cmd & 0x02 :
111+ copy_offset |= delta [offset ] << 8
112+ offset += 1
113+ if cmd & 0x04 :
114+ copy_offset |= delta [offset ] << 16
115+ offset += 1
116+ if cmd & 0x08 :
117+ copy_offset |= delta [offset ] << 24
118+ offset += 1
119+ if cmd & 0x10 :
120+ copy_size |= delta [offset ]
121+ offset += 1
122+ if cmd & 0x20 :
123+ copy_size |= delta [offset ] << 8
124+ offset += 1
125+ if cmd & 0x40 :
126+ copy_size |= delta [offset ] << 16
127+ offset += 1
128+ if copy_size == 0 :
129+ copy_size = 0x10000
130+ result .extend (base [copy_offset :copy_offset + copy_size ])
131+ elif cmd : # Insert literal (cmd = number of bytes)
132+ result .extend (delta [offset :offset + cmd ])
133+ offset += cmd
134+
135+ return bytes (result )
136+
137+
33138class RefParser :
34139 @staticmethod
35140 def parse_refs (refs_lines : list [str ]):
@@ -82,10 +187,8 @@ def format_pkt_line(payload: str | bytes) -> bytes:
82187 return hex_length .encode () + payload
83188
84189 def send_want_request (self ):
85- caps = " " .join (self .capabilities ) if self .capabilities else ""
86-
87190 body_parts = [
88- self .format_pkt_line (f"want { self .refs ['HEAD' ].sha1 } { caps } \n " ),
191+ self .format_pkt_line (f"want { self .refs ['HEAD' ].sha1 } " ),
89192 b"0000" ,
90193 self .format_pkt_line ("done\n " ),
91194 ]
@@ -104,10 +207,100 @@ def send_want_request(self):
104207 content = request_body ,
105208 headers = headers ,
106209 )
107- print (response .content )
108- return response
210+ data = response .content
211+ if data .startswith (b"0008NAK\n " ):
212+ return data [8 :]
213+ return response .content
214+
215+ def parse_pack_header (self , data : bytes ) -> PackHeader :
216+ """Parse pack file header, return (version, num_objects)."""
217+ return PackHeader .from_bytes (data )
218+
219+ def parse_pack_objects (self , data : bytes , num_objects : int ) -> list [PackObject ]:
220+ offset = 12 # skip header
221+ objects = []
222+ objects_by_offset = {} # for ofs_delta lookup
223+ objects_by_sha1 = {} # for ref_delta lookup
224+
225+ # First pass: parse all objects
226+ for _ in range (num_objects ):
227+ obj_start = offset
228+
229+ # Read variable-length header
230+ byte = data [offset ]
231+ obj_type = (byte >> 4 ) & 0x07
232+ size = byte & 0x0F
233+ shift = 4
234+ offset += 1
235+
236+ while byte & 0x80 : # continue bit set
237+ byte = data [offset ]
238+ size |= (byte & 0x7F ) << shift
239+ shift += 7
240+ offset += 1
241+
242+ # Handle delta base references
243+ delta_base_offset = None
244+ delta_base_sha1 = None
245+
246+ if obj_type == OBJ_OFS_DELTA :
247+ # Read negative offset (variable-length encoding)
248+ byte = data [offset ]
249+ delta_base_offset = byte & 0x7F
250+ offset += 1
251+ while byte & 0x80 :
252+ byte = data [offset ]
253+ delta_base_offset = ((delta_base_offset + 1 ) << 7 ) | (byte & 0x7F )
254+ offset += 1
255+ delta_base_offset = obj_start - delta_base_offset
256+
257+ elif obj_type == OBJ_REF_DELTA :
258+ # Read 20-byte base SHA-1
259+ delta_base_sha1 = data [offset :offset + 20 ].hex ()
260+ offset += 20
261+
262+ # Decompress zlib data
263+ decompressor = zlib .decompressobj ()
264+ decompressed = decompressor .decompress (data [offset :])
265+ offset += len (data [offset :]) - len (decompressor .unused_data )
266+
267+ obj = PackObject (obj_type , size , decompressed , obj_start )
268+ obj ._delta_base_offset = delta_base_offset
269+ obj ._delta_base_sha1 = delta_base_sha1
270+
271+ objects .append (obj )
272+ objects_by_offset [obj_start ] = obj
273+
274+ # Index non-delta objects by SHA-1
275+ if obj_type in TYPE_NAMES :
276+ sha1 = compute_sha1 (obj_type , decompressed )
277+ objects_by_sha1 [sha1 ] = obj
278+
279+ # Second pass: resolve deltas
280+ def resolve (obj : PackObject ) -> PackObject :
281+ if obj .type == OBJ_OFS_DELTA :
282+ base_obj = resolve (objects_by_offset [obj ._delta_base_offset ])
283+ resolved_data = apply_delta (base_obj .data , obj .data )
284+ obj .data = resolved_data
285+ obj .type = base_obj .type
286+ obj .size = len (resolved_data )
287+ elif obj .type == OBJ_REF_DELTA :
288+ base_obj = resolve (objects_by_sha1 [obj ._delta_base_sha1 ])
289+ resolved_data = apply_delta (base_obj .data , obj .data )
290+ obj .data = resolved_data
291+ obj .type = base_obj .type
292+ obj .size = len (resolved_data )
293+ return obj
294+
295+ for obj in objects :
296+ resolve (obj )
297+
298+ return objects
109299
110300
111301if __name__ == "__main__" :
112302 with GitClone (DEFAULT_URL ) as clone :
113- clone .send_want_request ()
303+ pack_data = clone .send_want_request ()
304+ pack_header = clone .parse_pack_header (pack_data )
305+ objects = clone .parse_pack_objects (pack_data , pack_header .num_objects )
306+ pprint (objects )
0 commit comments