]> gitweb.ps.run Git - ziggit/blob - git.zig
change test repo
[ziggit] / git.zig
1 const std = @import("std");
2
3 const Alloc = std.mem.Allocator;
4 const Reader = std.io.AnyReader;
5 const Writer = std.io.AnyWriter;
6
7 const Id = u160;
8 const Commit = struct {
9     author: []u8,
10     message: []u8,
11     parent: Id,
12     tree: Id,
13 };
14 const Blob = struct {
15     data: []u8,
16 };
17 const Object = struct {
18     kind: u3,
19     data: []u8,
20
21     pub fn init(kind: u3, data: []u8) Object {
22         return .{
23             .kind = kind,
24             .data = data,
25         };
26     }
27     // pub fn getCommit(self: *Object) Commit {}
28     // pub fn getBlob(self: *Object) Blob {}
29 };
30
31 const PackFile = struct {
32     alloc: Alloc,
33     idxFile: std.fs.File,
34     pckFile: std.fs.File,
35     objectOffsets: std.AutoArrayHashMap(Id, u32),
36
37     pub fn open(alloc: Alloc, dir: std.fs.Dir) !?PackFile {
38         var self = PackFile{
39             .alloc = alloc,
40             .idxFile = undefined,
41             .pckFile = undefined,
42             .objectOffsets = std.AutoArrayHashMap(Id, u32).init(alloc),
43         };
44
45         var packDir = try dir.openDir("objects/pack", .{ .iterate = true });
46         defer packDir.close();
47
48         var packFileFound = false;
49
50         var packIt = packDir.iterate();
51         while (try packIt.next()) |f| {
52             if (std.mem.endsWith(u8, f.name, ".idx")) {
53                 const idxFilename = f.name;
54                 var pckFilename = try std.BoundedArray(u8, std.fs.max_path_bytes).init(0);
55                 try std.fmt.format(
56                     pckFilename.writer(),
57                     "{s}.pack",
58                     .{idxFilename[0 .. idxFilename.len - 4]},
59                 );
60
61                 self.idxFile = try packDir.openFile(idxFilename, .{});
62                 self.pckFile = try packDir.openFile(pckFilename.constSlice(), .{});
63
64                 try self.parseIndex();
65
66                 packFileFound = true;
67             }
68         }
69
70         if (!packFileFound)
71             return null;
72
73         return self;
74     }
75
76     pub fn close(self: *PackFile) void {
77         self.objectOffsets.deinit();
78         self.idxFile.close();
79         self.pckFile.close();
80     }
81
82     pub fn parseIndex(self: *PackFile) !void {
83         const idxReader = self.idxFile.reader().any();
84
85         var fanoutTable: [256]u32 = undefined;
86
87         for (0..256) |i| {
88             try self.idxFile.seekTo(8 + i * 4);
89             fanoutTable[i] = try idxReader.readVarInt(u32, .big, 4);
90
91             const numObjects =
92                 if (i > 0) fanoutTable[i] - fanoutTable[i - 1] else fanoutTable[i];
93
94             for (0..numObjects) |j| {
95                 const idOffset =
96                     4 + 4 + 4 * 256 + (j + if (i > 0) fanoutTable[i - 1] else 0) * 20;
97                 try self.idxFile.seekTo(idOffset);
98                 const id = try idxReader.readVarInt(Id, .big, 20);
99
100                 try self.objectOffsets.put(id, 0);
101             }
102         }
103
104         const numObjects = self.objectOffsets.keys().len;
105         for (0..numObjects) |i| {
106             const offsetOffset =
107                 4 + 4 + 4 * 256 + numObjects * (20 + 4) + i * 4;
108             try self.idxFile.seekTo(offsetOffset);
109             const offset = try idxReader.readVarInt(u32, .big, 4);
110
111             self.objectOffsets.values()[i] = offset;
112         }
113     }
114
115     fn getSize(reader: Reader, ignoreTypeBits: bool) !struct { size: u64, bytelen: u64 } {
116         var size: u64 = 0;
117         var counter: u6 = 0;
118         while (true) {
119             const byte = try reader.readByte();
120
121             if (counter == 0) {
122                 if (ignoreTypeBits) {
123                     const bits: u4 = @truncate(byte);
124                     size = bits;
125                 } else {
126                     const bits: u7 = @truncate(byte);
127                     size = bits;
128                 }
129             } else {
130                 if (ignoreTypeBits) {
131                     const bits: u7 = @truncate(byte);
132                     size += @as(u64, bits) << (7 * (counter - 1) + 4);
133                 } else {
134                     const bits: u7 = @truncate(byte);
135                     size += @as(u64, bits) << (7 * (counter));
136                 }
137             }
138
139             if (byte & 0b10000000 == 0) {
140                 break;
141             }
142
143             counter += 1;
144         }
145
146         const nBytes = counter + 1;
147
148         return .{
149             .size = size,
150             .bytelen = nBytes,
151         };
152     }
153
154     fn getOffset(reader: Reader) !struct { offset: u64, bytelen: u64 } {
155         var offset: u64 = 0;
156         var counter: u4 = 0;
157         while (true) {
158             const byte = try reader.readByte();
159
160             const bits: u7 = @truncate(byte);
161             offset <<= 7;
162             offset += @as(u64, bits);
163
164             if (byte & 0b10000000 == 0) {
165                 break;
166             }
167
168             counter += 1;
169         }
170
171         const nBytes = counter + 1;
172
173         if (nBytes >= 2) {
174             for (1..nBytes) |i| {
175                 offset += std.math.pow(u64, 2, 7 * i);
176             }
177         }
178         return .{
179             .offset = offset,
180             .bytelen = nBytes,
181         };
182     }
183
184     fn applyDelta(alloc: Alloc, baseData: []const u8, deltData: []const u8) ![]u8 {
185         var fbs = std.io.fixedBufferStream(deltData);
186         const deltDataReader = fbs.reader().any();
187         const baseObjectSize = try getSize(deltDataReader, false);
188         const resultObjectSize = try getSize(deltDataReader, false);
189         const deltaDataOffset = baseObjectSize.bytelen + resultObjectSize.bytelen;
190
191         const result = try alloc.alloc(u8, resultObjectSize.size);
192         var resultCounter: u64 = 0;
193
194         var counter: u64 = 0;
195         while (true) {
196             const b = deltData[deltaDataOffset + counter];
197
198             if (b & 0b10000000 != 0) {
199                 var dataOffset: u64 = 0;
200                 var dataSize: u64 = 0;
201                 var bitsSet: u8 = 0;
202                 for (0..4) |i| { // offset bits
203                     if (b & (@as(u64, 1) << @min(3, i)) != 0) {
204                         dataOffset += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(3 * 8, i * 8);
205                         bitsSet += 1;
206                     }
207                 }
208                 for (4..7) |i| { // size bits
209                     if (b & (@as(u64, 1) << @min(6, i)) != 0) {
210                         dataSize += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(6 * 8, (i - 4) * 8);
211                         bitsSet += 1;
212                     }
213                 }
214                 counter += bitsSet;
215
216                 std.mem.copyForwards(
217                     u8,
218                     result[resultCounter..result.len],
219                     baseData[dataOffset .. dataOffset + dataSize],
220                 );
221
222                 resultCounter += dataSize;
223             } else {
224                 const dataSize: u7 = @truncate(b);
225
226                 std.mem.copyForwards(
227                     u8,
228                     result[resultCounter..result.len],
229                     deltData[deltaDataOffset + counter + 1 .. deltaDataOffset + counter + 1 + dataSize],
230                 );
231                 resultCounter += dataSize;
232                 counter += dataSize;
233             }
234
235             counter += 1;
236             if (deltaDataOffset + counter >= deltData.len)
237                 break;
238         }
239
240         return result;
241     }
242
243     fn ofsDelta(self: *PackFile, offset: i64) anyerror!Object {
244         const pckReader = self.pckFile.reader().any();
245
246         const pos = try self.pckFile.getPos();
247
248         try self.pckFile.seekBy(-offset);
249         const baseObject = try self.readObject(pckReader);
250         defer self.alloc.free(baseObject.data);
251
252         try self.pckFile.seekTo(pos);
253         const deltaData = try decompress(self.alloc, pckReader);
254         defer self.alloc.free(deltaData);
255
256         const objectData = try applyDelta(self.alloc, baseObject.data, deltaData);
257         return Object.init(baseObject.kind, objectData);
258     }
259
260     fn readObject(self: *PackFile, reader: Reader) anyerror!Object {
261         const firstByte = try reader.readByte();
262         const objectKind: u3 = @truncate(firstByte >> 4);
263         try self.pckFile.seekBy(-1);
264         const objectSize = try getSize(reader, true);
265
266         if (objectKind == 6) {
267             const offset = try getOffset(reader);
268             return try self.ofsDelta(
269                 @intCast(offset.offset + objectSize.bytelen + offset.bytelen),
270             );
271         } else {
272             const objectData = try decompress(self.alloc, reader);
273             return Object.init(objectKind, objectData);
274         }
275     }
276
277     pub fn getObject(self: *PackFile, id: Id) !?Object {
278         if (self.objectOffsets.get(id)) |offset| {
279             const pckReader = self.pckFile.reader().any();
280             try self.pckFile.seekTo(offset);
281
282             return try self.readObject(pckReader);
283         }
284         return null;
285     }
286 };
287
288 const Repo = struct {
289     alloc: Alloc,
290     dir: std.fs.Dir,
291     packfile: ?PackFile,
292
293     pub fn open(alloc: Alloc, path: []const u8) !Repo {
294         const dir = try std.fs.cwd().openDir(path, .{});
295
296         const packfile = try PackFile.open(alloc, dir);
297
298         return .{
299             .alloc = alloc,
300             .dir = dir,
301             .packfile = packfile,
302         };
303     }
304
305     pub fn close(self: *Repo) void {
306         self.dir.close();
307         if (self.packfile != null) {
308             self.packfile.?.close();
309         }
310     }
311
312     pub fn getHead(self: *Repo) !Id {
313         // read file HEAD
314         const head = try self.dir.readFileAlloc(self.alloc, "HEAD", 1024);
315         defer self.alloc.free(head);
316
317         // read file pointed at by HEAD
318         const headPath = head[5 .. head.len - 1];
319         var idBuffer: [40]u8 = undefined;
320         const idStr = try self.dir.readFile(headPath, &idBuffer);
321
322         // parse id from file
323         return try std.fmt.parseUnsigned(u160, idStr, 16);
324     }
325
326     pub fn getObject(self: *Repo, id: Id) !?Object {
327         return self.packfile.getObject(id);
328     }
329 };
330
331 test "print HEAD" {
332     var repo = try Repo.open(std.testing.allocator, "../microwindows/.git");
333     defer repo.close();
334
335     const head = try repo.getHead();
336
337     std.debug.print("HEAD: {}\n", .{head});
338 }
339
340 test "parse idx" {
341     var repo = try Repo.open(std.testing.allocator, "../microwindows/.git");
342     defer repo.close();
343
344     std.debug.print("{}\n", .{repo.packfile.objectOffsets.keys().len});
345     std.debug.print("{}\n", .{repo.packfile.objectOffsets.values().len});
346 }
347
348 test "get object" {
349     var repo = try Repo.open(std.testing.allocator, "../microwindows/.git");
350     defer repo.close();
351
352     const head = try repo.getHead();
353
354     if (try repo.getObject(head)) |o| {
355         defer std.testing.allocator.free(o.data);
356
357         std.debug.print("object: {s}\n", .{o.data});
358     }
359 }
360
361 test "get tree" {
362     var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
363     defer repo.close();
364
365     if (try repo.getObject(0xceb2b2c62d6f8f3686dcacecd5be931839b02c77)) |o| {
366         defer std.testing.allocator.free(o.data);
367
368         std.debug.print("tree: {s}\n", .{o.data});
369     }
370 }
371 // test "list commits" {
372 //     var repo = Repo.open(std.testing.allocator, "../imgui/.git");
373 //     defer repo.close();
374
375 //     const head = repo.getObject(repo.head);
376 //     defer head.deinit();
377
378 //     var c = head.getCommit();
379 //     for (0..3) |_| {
380 //         std.debug.print("{}\n", .{c});
381 //         c = c.parent;
382 //     }
383 // }
384
385 // test "tree" {
386 //     var repo = Repo.open(std.testing.allocator, "../imgui/.git");
387 //     defer repo.close();
388
389 //     const head = repo.getObject(repo.head);
390 //     defer head.deinit();
391
392 //     const commit = head.getCommit();
393
394 //     std.debug.print("{}\n", .{commit.tree});
395 // }
396
397 // test "blob" {
398 //     var repo = Repo.open(std.testing.allocator, "../imgui/.git");
399 //     defer repo.close();
400
401 //     const head = repo.getObject(repo.head);
402 //     defer head.deinit();
403
404 //     const commit = head.getCommit();
405 //     const blob = repo.getBlob(commit.files[0].id);
406
407 //     std.debug.print("{}\n", .{blob});
408 // }