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