1 const std = @import("std");
3 const Alloc = std.mem.Allocator;
4 const Reader = std.io.AnyReader;
5 const Writer = std.io.AnyWriter;
7 const MaxFileSize = 1024 * 1024;
10 const Commit = struct {
17 const TreeEntry = struct {
22 const Tree = std.ArrayList(TreeEntry);
26 const Object = struct {
30 pub fn init(kind: u3, data: []u8) Object {
36 // pub fn getCommit(self: *Object) Commit {}
37 // pub fn getBlob(self: *Object) Blob {}
40 fn decompress(alloc: Alloc, r: Reader) ![]u8 {
41 var buffer = std.ArrayList(u8).init(alloc);
43 try std.compress.zlib.decompress(r, buffer.writer().any());
45 return alloc.realloc(buffer.allocatedSlice(), buffer.items.len);
48 const PackFile = struct {
52 objectOffsets: std.AutoArrayHashMap(Id, u32),
54 pub fn open(alloc: Alloc, dir: std.fs.Dir) !?PackFile {
59 .objectOffsets = std.AutoArrayHashMap(Id, u32).init(alloc),
62 var packDir = try dir.openDir("objects/pack", .{ .iterate = true });
63 defer packDir.close();
65 var packFileFound = false;
67 var packIt = packDir.iterate();
68 while (try packIt.next()) |f| {
69 if (std.mem.endsWith(u8, f.name, ".idx")) {
70 const idxFilename = f.name;
71 var pckFilename = try std.BoundedArray(u8, std.fs.max_path_bytes).init(0);
75 .{idxFilename[0 .. idxFilename.len - 4]},
78 self.idxFile = try packDir.openFile(idxFilename, .{});
79 self.pckFile = try packDir.openFile(pckFilename.constSlice(), .{});
81 try self.parseIndex();
93 pub fn close(self: *PackFile) void {
94 self.objectOffsets.deinit();
99 pub fn parseIndex(self: *PackFile) !void {
100 const idxReader = self.idxFile.reader().any();
102 var fanoutTable: [256]u32 = undefined;
105 try self.idxFile.seekTo(8 + i * 4);
106 fanoutTable[i] = try idxReader.readVarInt(u32, .big, 4);
109 if (i > 0) fanoutTable[i] - fanoutTable[i - 1] else fanoutTable[i];
111 for (0..numObjects) |j| {
113 4 + 4 + 4 * 256 + (j + if (i > 0) fanoutTable[i - 1] else 0) * 20;
114 try self.idxFile.seekTo(idOffset);
115 const id = try idxReader.readVarInt(Id, .big, 20);
117 try self.objectOffsets.put(id, 0);
121 const numObjects = self.objectOffsets.keys().len;
122 for (0..numObjects) |i| {
124 4 + 4 + 4 * 256 + numObjects * (20 + 4) + i * 4;
125 try self.idxFile.seekTo(offsetOffset);
126 const offset = try idxReader.readVarInt(u32, .big, 4);
128 self.objectOffsets.values()[i] = offset;
132 fn getSize(reader: Reader, ignoreTypeBits: bool) !struct { size: u64, bytelen: u64 } {
136 const byte = try reader.readByte();
139 if (ignoreTypeBits) {
140 const bits: u4 = @truncate(byte);
143 const bits: u7 = @truncate(byte);
147 if (ignoreTypeBits) {
148 const bits: u7 = @truncate(byte);
149 size += @as(u64, bits) << (7 * (counter - 1) + 4);
151 const bits: u7 = @truncate(byte);
152 size += @as(u64, bits) << (7 * (counter));
156 if (byte & 0b10000000 == 0) {
163 const nBytes = counter + 1;
171 fn getOffset(reader: Reader) !struct { offset: u64, bytelen: u64 } {
175 const byte = try reader.readByte();
177 const bits: u7 = @truncate(byte);
179 offset += @as(u64, bits);
181 if (byte & 0b10000000 == 0) {
188 const nBytes = counter + 1;
191 for (1..nBytes) |i| {
192 offset += std.math.pow(u64, 2, 7 * i);
201 fn applyDelta(alloc: Alloc, baseData: []const u8, deltData: []const u8) ![]u8 {
202 var fbs = std.io.fixedBufferStream(deltData);
203 const deltDataReader = fbs.reader().any();
204 const baseObjectSize = try getSize(deltDataReader, false);
205 const resultObjectSize = try getSize(deltDataReader, false);
206 const deltaDataOffset = baseObjectSize.bytelen + resultObjectSize.bytelen;
208 const result = try alloc.alloc(u8, resultObjectSize.size);
209 var resultCounter: u64 = 0;
211 var counter: u64 = 0;
213 const b = deltData[deltaDataOffset + counter];
215 if (b & 0b10000000 != 0) {
216 var dataOffset: u64 = 0;
217 var dataSize: u64 = 0;
219 for (0..4) |i| { // offset bits
220 if (b & (@as(u64, 1) << @min(3, i)) != 0) {
221 dataOffset += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(3 * 8, i * 8);
225 for (4..7) |i| { // size bits
226 if (b & (@as(u64, 1) << @min(6, i)) != 0) {
227 dataSize += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(6 * 8, (i - 4) * 8);
233 std.mem.copyForwards(
235 result[resultCounter..result.len],
236 baseData[dataOffset .. dataOffset + dataSize],
239 resultCounter += dataSize;
241 const dataSize: u7 = @truncate(b);
243 std.mem.copyForwards(
245 result[resultCounter..result.len],
246 deltData[deltaDataOffset + counter + 1 .. deltaDataOffset + counter + 1 + dataSize],
248 resultCounter += dataSize;
253 if (deltaDataOffset + counter >= deltData.len)
260 fn ofsDelta(self: *PackFile, offset: i64) anyerror!Object {
261 const pckReader = self.pckFile.reader().any();
263 const pos = try self.pckFile.getPos();
265 try self.pckFile.seekBy(-offset);
266 const baseObject = try self.readObject(pckReader);
267 defer self.alloc.free(baseObject.data);
269 try self.pckFile.seekTo(pos);
270 const deltaData = try decompress(self.alloc, pckReader);
271 defer self.alloc.free(deltaData);
273 const objectData = try applyDelta(self.alloc, baseObject.data, deltaData);
274 return Object.init(baseObject.kind, objectData);
277 fn readObject(self: *PackFile, reader: Reader) anyerror!Object {
278 const firstByte = try reader.readByte();
279 const objectKind: u3 = @truncate(firstByte >> 4);
280 try self.pckFile.seekBy(-1);
281 const objectSize = try getSize(reader, true);
283 if (objectKind == 6) {
284 const offset = try getOffset(reader);
285 return try self.ofsDelta(
286 @intCast(offset.offset + objectSize.bytelen + offset.bytelen),
289 const objectData = try decompress(self.alloc, reader);
290 return Object.init(objectKind, objectData);
294 pub fn getObject(self: *PackFile, id: Id) !?Object {
295 if (self.objectOffsets.get(id)) |offset| {
296 const pckReader = self.pckFile.reader().any();
297 try self.pckFile.seekTo(offset);
299 return try self.readObject(pckReader);
305 const Repo = struct {
310 pub fn open(alloc: Alloc, path: []const u8) !Repo {
311 const dir = try std.fs.cwd().openDir(path, .{});
313 const packfile = try PackFile.open(alloc, dir);
318 .packfile = packfile,
322 pub fn close(self: *Repo) void {
324 if (self.packfile != null) {
325 self.packfile.?.close();
329 pub fn getHead(self: *Repo) !Id {
331 const head = try self.dir.readFileAlloc(self.alloc, "HEAD", 1024);
332 defer self.alloc.free(head);
334 // read file pointed at by HEAD
335 const headPath = head[5 .. head.len - 1];
336 var idBuffer: [40]u8 = undefined;
337 const idStr = try self.dir.readFile(headPath, &idBuffer);
339 // parse id from file
340 return try std.fmt.parseUnsigned(u160, idStr, 16);
343 pub fn getObject(self: *Repo, id: Id) !?Object {
344 if (self.packfile) |*packfile| {
345 return packfile.getObject(id);
352 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
355 const head = try repo.getHead();
357 std.debug.print("HEAD: {}\n", .{head});
361 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
364 if (repo.packfile) |packfile| {
365 std.debug.print("{}\n", .{packfile.objectOffsets.keys().len});
366 std.debug.print("{}\n", .{packfile.objectOffsets.values().len});
371 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
374 const head = try repo.getHead();
376 if (try repo.getObject(head)) |o| {
377 defer std.testing.allocator.free(o.data);
379 std.debug.print("object: {s}\n", .{o.data});
384 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
387 if (try repo.getObject(0xceb2b2c62d6f8f3686dcacecd5be931839b02c77)) |o| {
388 defer std.testing.allocator.free(o.data);
390 std.debug.print("tree: {s}\n", .{o.data});
393 // test "list commits" {
394 // var repo = Repo.open(std.testing.allocator, "../imgui/.git");
395 // defer repo.close();
397 // const head = repo.getObject(repo.head);
398 // defer head.deinit();
400 // var c = head.getCommit();
402 // std.debug.print("{}\n", .{c});
408 // var repo = Repo.open(std.testing.allocator, "../imgui/.git");
409 // defer repo.close();
411 // const head = repo.getObject(repo.head);
412 // defer head.deinit();
414 // const commit = head.getCommit();
416 // std.debug.print("{}\n", .{commit.tree});
420 // var repo = Repo.open(std.testing.allocator, "../imgui/.git");
421 // defer repo.close();
423 // const head = repo.getObject(repo.head);
424 // defer head.deinit();
426 // const commit = head.getCommit();
427 // const blob = repo.getBlob(commit.files[0].id);
429 // std.debug.print("{}\n", .{blob});