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 ParsedObject = union(enum) {
31 const Object = struct {
35 pub fn init(kind: u3, data: []u8) Object {
41 pub fn parse(self: Object, alloc: Alloc) !ParsedObject {
44 const authorOffset = std.mem.indexOf(u8, self.data, "author ") orelse return error.InvalidCommitFormat;
45 const authorNewline = std.mem.indexOfScalarPos(u8, self.data, authorOffset, '\n') orelse return error.InvalidCommitFormat;
46 const committerOffset = std.mem.indexOf(u8, self.data, "committer ") orelse return error.InvalidCommitFormat;
47 const committerNewline = std.mem.indexOfScalarPos(u8, self.data, committerOffset, '\n') orelse return error.InvalidCommitFormat;
51 .tree = try std.fmt.parseUnsigned(Id, self.data[5..45], 16),
52 .parent = try std.fmt.parseUnsigned(Id, self.data[53..93], 16),
53 .author = self.data[authorOffset..authorNewline],
54 .committer = self.data[committerOffset..committerNewline],
55 .message = self.data[committerNewline + 1 .. self.data.len],
60 var t = Tree.init(alloc);
62 var offset: usize = 0;
64 while (offset < self.data.len - 1) {
65 const spaceOffset = std.mem.indexOfScalarPos(u8, self.data, offset, ' ') orelse return error.InvalidTreeFormat;
66 const zeroOffset = std.mem.indexOfScalarPos(u8, self.data, spaceOffset, 0) orelse return error.InvalidTreeFormat;
69 .permissions = self.data[offset..spaceOffset],
70 .name = self.data[spaceOffset + 1 .. zeroOffset],
71 .id = std.mem.readVarInt(Id, self.data[zeroOffset + 1 .. zeroOffset + 21], .big),
74 offset = zeroOffset + 21;
81 .b = Blob{ .data = self.data },
85 return error.TagNotImplemented;
87 else => return error.UnknownGitObjectType,
90 // pub fn getCommit(self: *Object) Commit {}
91 // pub fn getBlob(self: *Object) Blob {}
94 fn decompress(alloc: Alloc, r: Reader) ![]u8 {
95 var buffer = std.ArrayList(u8).init(alloc);
97 try std.compress.zlib.decompress(r, buffer.writer().any());
99 return alloc.realloc(buffer.allocatedSlice(), buffer.items.len);
102 const PackFile = struct {
104 idxFile: std.fs.File,
105 pckFile: std.fs.File,
106 objectOffsets: std.AutoArrayHashMap(Id, u32),
108 pub fn open(alloc: Alloc, dir: std.fs.Dir) !?PackFile {
111 .idxFile = undefined,
112 .pckFile = undefined,
113 .objectOffsets = std.AutoArrayHashMap(Id, u32).init(alloc),
116 var packDir = try dir.openDir("objects/pack", .{ .iterate = true });
117 defer packDir.close();
119 var packFileFound = false;
121 var packIt = packDir.iterate();
122 while (try packIt.next()) |f| {
123 if (std.mem.endsWith(u8, f.name, ".idx")) {
124 const idxFilename = f.name;
125 var pckFilename = try std.BoundedArray(u8, std.fs.max_path_bytes).init(0);
127 pckFilename.writer(),
129 .{idxFilename[0 .. idxFilename.len - 4]},
132 self.idxFile = try packDir.openFile(idxFilename, .{});
133 self.pckFile = try packDir.openFile(pckFilename.constSlice(), .{});
135 try self.parseIndex();
137 packFileFound = true;
147 pub fn close(self: *PackFile) void {
148 self.objectOffsets.deinit();
149 self.idxFile.close();
150 self.pckFile.close();
153 pub fn parseIndex(self: *PackFile) !void {
154 const idxReader = self.idxFile.reader().any();
156 var fanoutTable: [256]u32 = undefined;
159 try self.idxFile.seekTo(8 + i * 4);
160 fanoutTable[i] = try idxReader.readVarInt(u32, .big, 4);
163 if (i > 0) fanoutTable[i] - fanoutTable[i - 1] else fanoutTable[i];
165 for (0..numObjects) |j| {
167 4 + 4 + 4 * 256 + (j + if (i > 0) fanoutTable[i - 1] else 0) * 20;
168 try self.idxFile.seekTo(idOffset);
169 const id = try idxReader.readVarInt(Id, .big, 20);
171 try self.objectOffsets.put(id, 0);
175 const numObjects = self.objectOffsets.keys().len;
176 for (0..numObjects) |i| {
178 4 + 4 + 4 * 256 + numObjects * (20 + 4) + i * 4;
179 try self.idxFile.seekTo(offsetOffset);
180 const offset = try idxReader.readVarInt(u32, .big, 4);
182 self.objectOffsets.values()[i] = offset;
186 fn getSize(reader: Reader, ignoreTypeBits: bool) !struct { size: u64, bytelen: u64 } {
190 const byte = try reader.readByte();
193 if (ignoreTypeBits) {
194 const bits: u4 = @truncate(byte);
197 const bits: u7 = @truncate(byte);
201 if (ignoreTypeBits) {
202 const bits: u7 = @truncate(byte);
203 size += @as(u64, bits) << (7 * (counter - 1) + 4);
205 const bits: u7 = @truncate(byte);
206 size += @as(u64, bits) << (7 * (counter));
210 if (byte & 0b10000000 == 0) {
217 const nBytes = counter + 1;
225 fn getOffset(reader: Reader) !struct { offset: u64, bytelen: u64 } {
229 const byte = try reader.readByte();
231 const bits: u7 = @truncate(byte);
233 offset += @as(u64, bits);
235 if (byte & 0b10000000 == 0) {
242 const nBytes = counter + 1;
245 for (1..nBytes) |i| {
246 offset += std.math.pow(u64, 2, 7 * i);
255 fn applyDelta(alloc: Alloc, baseData: []const u8, deltData: []const u8) ![]u8 {
256 var fbs = std.io.fixedBufferStream(deltData);
257 const deltDataReader = fbs.reader().any();
258 const baseObjectSize = try getSize(deltDataReader, false);
259 const resultObjectSize = try getSize(deltDataReader, false);
260 const deltaDataOffset = baseObjectSize.bytelen + resultObjectSize.bytelen;
262 const result = try alloc.alloc(u8, resultObjectSize.size);
263 var resultCounter: u64 = 0;
265 var counter: u64 = 0;
267 const b = deltData[deltaDataOffset + counter];
269 if (b & 0b10000000 != 0) {
270 var dataOffset: u64 = 0;
271 var dataSize: u64 = 0;
273 for (0..4) |i| { // offset bits
274 if (b & (@as(u64, 1) << @min(3, i)) != 0) {
275 dataOffset += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(3 * 8, i * 8);
279 for (4..7) |i| { // size bits
280 if (b & (@as(u64, 1) << @min(6, i)) != 0) {
281 dataSize += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(6 * 8, (i - 4) * 8);
290 std.mem.copyForwards(
292 result[resultCounter..result.len],
293 baseData[dataOffset .. dataOffset + dataSize],
296 resultCounter += dataSize;
298 const dataSize: u7 = @truncate(b);
300 std.mem.copyForwards(
302 result[resultCounter..result.len],
303 deltData[deltaDataOffset + counter + 1 .. deltaDataOffset + counter + 1 + dataSize],
306 resultCounter += dataSize;
311 if (deltaDataOffset + counter >= deltData.len)
318 fn ofsDelta(self: *PackFile, offset: i64) anyerror!Object {
319 const pckReader = self.pckFile.reader().any();
321 const pos = try self.pckFile.getPos();
323 try self.pckFile.seekBy(-offset);
324 const baseObject = try self.readObject(pckReader);
325 defer self.alloc.free(baseObject.data);
327 try self.pckFile.seekTo(pos);
328 const deltaData = try decompress(self.alloc, pckReader);
329 defer self.alloc.free(deltaData);
331 const objectData = try applyDelta(self.alloc, baseObject.data, deltaData);
332 return Object.init(baseObject.kind, objectData);
335 fn readObject(self: *PackFile, reader: Reader) anyerror!Object {
336 const firstByte = try reader.readByte();
337 const objectKind: u3 = @truncate(firstByte >> 4);
338 try self.pckFile.seekBy(-1);
339 const objectSize = try getSize(reader, true);
341 if (objectKind == 6) {
342 const offset = try getOffset(reader);
344 return try self.ofsDelta(
345 @intCast(offset.offset + objectSize.bytelen + offset.bytelen),
348 const objectData = try decompress(self.alloc, reader);
349 return Object.init(objectKind, objectData);
353 pub fn getObject(self: *PackFile, id: Id) !?Object {
354 if (self.objectOffsets.get(id)) |offset| {
355 const pckReader = self.pckFile.reader().any();
356 try self.pckFile.seekTo(offset);
358 const o = try self.readObject(pckReader);
366 const Repo = struct {
371 pub fn open(alloc: Alloc, path: []const u8) !Repo {
372 const dir = try std.fs.cwd().openDir(path, .{});
374 const packfile = try PackFile.open(alloc, dir);
379 .packfile = packfile,
383 pub fn close(self: *Repo) void {
385 if (self.packfile != null) {
386 self.packfile.?.close();
390 pub fn getHead(self: *Repo) !Id {
392 const head = try self.dir.readFileAlloc(self.alloc, "HEAD", 1024);
393 defer self.alloc.free(head);
395 // read file pointed at by HEAD
396 const headPath = head[5 .. head.len - 1];
397 var idBuffer: [40]u8 = undefined;
398 const idStr = try self.dir.readFile(headPath, &idBuffer);
400 // parse id from file
401 return try std.fmt.parseUnsigned(u160, idStr, 16);
404 pub fn getObject(self: *Repo, id: Id) !?Object {
405 if (self.packfile) |*packfile| {
406 return packfile.getObject(id);
413 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
416 const head = try repo.getHead();
418 std.debug.print("HEAD: {}\n", .{head});
422 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
425 if (repo.packfile) |packfile| {
426 std.debug.print("{}\n", .{packfile.objectOffsets.keys().len});
427 std.debug.print("{}\n", .{packfile.objectOffsets.values().len});
432 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
435 const head = try repo.getHead();
437 if (try repo.getObject(head)) |o| {
438 defer std.testing.allocator.free(o.data);
440 std.debug.print("object({}): {s}\n", .{ o.kind, o.data });
444 test "parse commit" {
445 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
448 const head = try repo.getHead();
450 if (try repo.getObject(head)) |o| {
451 defer std.testing.allocator.free(o.data);
453 switch (try o.parse(std.testing.allocator)) {
455 std.debug.print("commit:\n tree: {x}\n parent: {x}\n author: {s}\n committer: {s}\n message: {s}\n", .{ c.tree, c.parent, c.author, c.committer, c.message });
463 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
466 if (try repo.getObject(0xceb2b2c62d6f8f3686dcacecd5be931839b02c77)) |o| {
467 defer std.testing.allocator.free(o.data);
469 // std.debug.print("tree({}): {any}\n", .{ o.kind, o.data });
474 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
477 if (try repo.getObject(0xceb2b2c62d6f8f3686dcacecd5be931839b02c77)) |o| {
478 defer std.testing.allocator.free(o.data);
480 switch (try o.parse(std.testing.allocator)) {
483 for (t.items) |treeEntry| {
484 std.debug.print("{s} {s} {x}\n", .{ treeEntry.permissions, treeEntry.name, treeEntry.id });
492 test "list commits" {
493 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
496 const head = try repo.getHead();
501 if (try repo.getObject(id)) |o| {
502 defer std.testing.allocator.free(o.data);
504 switch (try o.parse(std.testing.allocator)) {
506 std.debug.print("commit {x}:\n tree: {x}\n parent: {x}\n author: {s}\n committer: {s}\n message: {s}\n", .{ id, c.tree, c.parent, c.author, c.committer, c.message });
516 var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
519 if (try repo.getObject(0xceb2b2c62d6f8f3686dcacecd5be931839b02c77)) |o| {
520 defer std.testing.allocator.free(o.data);
522 switch (try o.parse(std.testing.allocator)) {
525 for (t.items) |treeEntry| {
526 if (try repo.getObject(treeEntry.id)) |bo| {
527 defer std.testing.allocator.free(bo.data);
529 if (treeEntry.permissions.len == 6) {
530 std.debug.print("{s}: [{x} {}]{s}\n", .{ treeEntry.name, treeEntry.id, bo.data.len, bo.data[0..50] });
532 std.debug.print("[{s}]\n", .{treeEntry.name});