1const std = @import("std");
2const string = []const u8;
3const time = @import("time");
4const extras = @import("extras");
5const tracer = @import("tracer");
6const nfs = @import("nfs");
7const nio = @import("nio");
8const root = @import("root"); // temp
9
10pub const Id = *const [40]u8;
11
12pub const TreeId = struct {
13 id: Id,
14
15 pub const zero: TreeId = .{ .id = "0000000000000000000000000000000000000000" };
16 pub const empty: TreeId = .{ .id = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" }; // echo -n | git hash-object -t tree --stdin
17
18 pub fn eql(self: TreeId, other: TreeId) bool {
19 return std.mem.eql(u8, self.id, other.id);
20 }
21};
22
23pub const CommitId = struct {
24 id: Id,
25
26 pub const zero: CommitId = .{ .id = "0000000000000000000000000000000000000000" };
27
28 pub fn eql(self: CommitId, other: CommitId) bool {
29 return std.mem.eql(u8, self.id, other.id);
30 }
31};
32
33pub const BlobId = struct {
34 id: Id,
35
36 pub const zero: BlobId = .{ .id = "0000000000000000000000000000000000000000" };
37 pub const empty: BlobId = .{ .id = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" }; // echo -n | git hash-object -t blob --stdin
38
39 pub fn eql(self: BlobId, other: BlobId) bool {
40 return std.mem.eql(u8, self.id, other.id);
41 }
42};
43
44pub const TagId = struct {
45 id: Id,
46
47 pub const zero: BlobId = .{ .id = "0000000000000000000000000000000000000000" };
48
49 pub fn eql(self: TagId, other: TagId) bool {
50 return std.mem.eql(u8, self.id, other.id);
51 }
52};
53
54pub const RefType = enum {
55 blob,
56 tree,
57 commit,
58 tag,
59};
60
61pub const AnyId = union(RefType) {
62 blob: BlobId,
63 tree: TreeId,
64 commit: CommitId,
65 tag: TagId,
66
67 pub fn erase(self: AnyId) Id {
68 return switch (self) {
69 inline else => |v| v.id,
70 };
71 }
72};
73
74pub fn version(alloc: std.mem.Allocator) !string {
75 const result = try root.child_process.run(alloc, .cwd(), .ignore, .pipe, .pipe, 1024, &.{ "git", "--version" });
76 defer alloc.free(result.stdout);
77 defer alloc.free(result.stderr);
78 std.debug.assert(result.term == .exited and result.term.exited == 0);
79 return try alloc.dupe(u8, extras.trimPrefixEnsure(std.mem.trimEnd(u8, result.stdout, "\n"), "git version ").?);
80}
81
82/// Returns the result of running `git rev-parse HEAD`
83/// dir must already be pointing at the .git folder
84pub fn getHEAD(alloc: std.mem.Allocator, dir: nfs.Dir) !?CommitId {
85 const t = tracer.trace(@src(), "", .{});
86 defer t.end();
87
88 const headfile = dir.readFileAlloc(alloc, "HEAD", 1024) catch |err| switch (err) {
89 error.ENOENT => return null,
90 else => |e| return e,
91 };
92 defer alloc.free(headfile);
93 const h = std.mem.trimEnd(u8, headfile, "\n");
94
95 if (std.mem.startsWith(u8, h, "ref:")) {
96 blk: {
97 var buf: [std.fs.max_path_bytes]u8 = undefined;
98 @memcpy((&buf).ptr, h[5..]);
99 buf[h[5..].len] = 0;
100 const reffile = dir.readFileAlloc(alloc, buf[0..h[5..].len :0], 1024) catch |err| switch (err) {
101 error.ENOENT => break :blk,
102 else => |e| return e,
103 };
104 defer alloc.free(reffile);
105 return ensureObjId(CommitId, try alloc.dupe(u8, std.mem.trimEnd(u8, reffile, "\n")));
106 }
107 blk: {
108 const pckedrfs = dir.readFileAlloc(alloc, "packed-refs", 1024 * 1024 * 1024) catch |err| switch (err) {
109 error.ENOENT => break :blk,
110 else => |e| return e,
111 };
112 defer alloc.free(pckedrfs);
113 var iter = std.mem.splitScalar(u8, pckedrfs, '\n');
114 while (iter.next()) |line| {
115 if (std.mem.startsWith(u8, line, "#")) continue;
116 if (std.mem.startsWith(u8, line, "^")) continue;
117 if (line.len == 0) continue;
118 var jter = std.mem.splitScalar(u8, line, ' ');
119 const objid = jter.next().?;
120 const ref = jter.next().?;
121 std.debug.assert(jter.next() == null);
122 if (std.mem.eql(u8, h[5..], ref)) return ensureObjId(CommitId, try alloc.dupe(u8, objid));
123 }
124 }
125 return null;
126 }
127
128 return ensureObjId(CommitId, try alloc.dupe(u8, h));
129}
130
131// 40 is length of sha1 hash
132pub fn ensureObjId(comptime T: type, input: string) T {
133 extras.assertLog(input.len == 40, "ensureObjId: {s}", .{input});
134 return .{ .id = input[0..40] };
135}
136
137pub fn parseCommit(alloc: std.mem.Allocator, commitfile: string, mailmap: *const std.hash_map.StringHashMapUnmanaged([]const u8), mailmap_names: *const std.hash_map.StringHashMapUnmanaged([]const u8)) !Commit {
138 const t = tracer.trace(@src(), "", .{});
139 defer t.end();
140
141 var iter = std.mem.splitScalar(u8, commitfile, '\n');
142 var result: Commit = .{
143 .raw = commitfile,
144 .tree = undefined,
145 .parents = undefined,
146 .author = undefined,
147 .committer = undefined,
148 .gpgsig = "",
149 .message = undefined,
150 };
151 var parents = std.array_list.Managed(CommitId).init(alloc);
152 errdefer parents.deinit();
153 var f_start: usize = 0;
154 while (true) {
155 const line_start = iter.index.?;
156 const line = iter.next() orelse break;
157 if (line.len == 0) break;
158 const space = std.mem.indexOfScalar(u8, line, ' ').?;
159 const k = line[0..space];
160
161 if (std.mem.eql(u8, k, "tree")) result.tree = .{ .id = line[space + 1 ..][0..40] };
162 if (std.mem.eql(u8, k, "author")) result.author = try parseCommitUserAndAt(line[space + 1 ..]);
163 if (std.mem.eql(u8, k, "committer")) result.committer = try parseCommitUserAndAt(line[space + 1 ..]);
164 if (std.mem.eql(u8, k, "parent")) try parents.append(.{ .id = line[space + 1 ..][0..40] });
165 if (std.mem.eql(u8, k, "gpgsig")) {
166 f_start = line_start + 7;
167 _ = iter.next().?;
168 while (true) {
169 const line2_start = iter.index.?;
170 const line2 = iter.next() orelse break;
171 if (line2.len == 0 or line2[0] != ' ') {
172 iter.index = line2_start;
173 break;
174 }
175 }
176 result.gpgsig = commitfile[f_start .. iter.index.? - 1];
177 }
178 }
179 result.parents = try parents.toOwnedSlice();
180 result.author.email = mailmap.get(result.author.email) orelse result.author.email;
181 result.author.name = mailmap_names.get(result.author.email) orelse result.author.name;
182 result.committer.email = mailmap.get(result.committer.email) orelse result.committer.email;
183 result.committer.name = mailmap_names.get(result.committer.email) orelse result.committer.name;
184 result.message = iter.rest();
185 return result;
186}
187
188fn parseCommitUserAndAt(input: string) !UserAndAt {
189 const t = tracer.trace(@src(), "", .{});
190 defer t.end();
191
192 // Mitchell Hashimoto <mitchell.hashimoto@gmail.com> 1680797363 -0700
193 // first and second part is https://datatracker.ietf.org/doc/html/rfc5322#section-3.4
194 // third part is unix epoch timestamp
195 // fourth part is TZ
196 var maybe_bad_parser = std.mem.splitBackwardsScalar(u8, input, ' ');
197 const tz_part = maybe_bad_parser.next() orelse return error.BadCommitTz;
198 const time_part = maybe_bad_parser.next() orelse return error.BadCommitTime;
199 var email_part = maybe_bad_parser.next() orelse return error.BadCommitEmail;
200 while (email_part[0] != '<') {
201 const next_len = maybe_bad_parser.next().?.len;
202 email_part.ptr -= next_len + 1;
203 email_part.len += next_len + 1;
204 }
205 const name_part = maybe_bad_parser.rest();
206 std.debug.assert(email_part[0] == '<');
207 std.debug.assert(email_part[email_part.len - 1] == '>');
208 const name = name_part;
209 const email = std.mem.trim(u8, email_part, "<>");
210 const at = parseAt(time_part, tz_part);
211
212 return .{
213 .name = name,
214 .email = email,
215 .at = at,
216 };
217}
218
219fn parseAt(time_part: string, tz_part: string) time.DateTime {
220 var at_s = extras.parseDigits(u64, time_part, 10) catch unreachable;
221 std.debug.assert(tz_part.len == 5);
222 std.debug.assert(tz_part[0] == '-' or tz_part[0] == '+');
223 const sign: i8 = if (tz_part[0] == '+') 1 else -1;
224 const hrs = extras.parseDigits(u7, tz_part[1..][0..2], 10) catch unreachable;
225 const mins = extras.parseDigits(u7, tz_part[3..][0..2], 10) catch unreachable;
226 var z_offset: i8 = 0;
227 z_offset += hrs;
228 z_offset *= 4;
229 z_offset += mins / 15;
230 z_offset *= sign;
231 at_s = extras.safeAdd(at_s, @as(i32, z_offset) * 15 * time.s_per_min);
232 var at: time.DateTime = .initUnix(at_s);
233 at.z_offset = z_offset;
234 return at;
235}
236
237fn parseTreeMode(input: string) !Tree.Object.Mode {
238 const t = tracer.trace(@src(), "", .{});
239 defer t.end();
240
241 std.debug.assert(input.len == 6);
242 return .{
243 .type = @enumFromInt(try extras.parseDigits(u16, input[0..3], 10)),
244 .perm_user = @bitCast(try extras.parseDigits(u3, input[3..][0..1], 8)),
245 .perm_group = @bitCast(try extras.parseDigits(u3, input[4..][0..1], 8)),
246 .perm_other = @bitCast(try extras.parseDigits(u3, input[5..][0..1], 8)),
247 };
248}
249
250// TODO make this inspect .git manually
251// TODO make this return a Reader when we implement it ourselves
252pub fn getTreeDiff(alloc: std.mem.Allocator, dir: nfs.Dir, commitid: CommitId, parentid: ?CommitId) !string {
253 const t = tracer.trace(@src(), "", .{});
254 defer t.end();
255
256 if (parentid == null) {
257 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .pipe, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "-p", "--raw", "--full-index", TreeId.empty.id, commitid.id });
258 std.debug.assert(result.term == .exited and result.term.exited == 0);
259 return std.mem.trim(u8, result.stdout, "\n");
260 }
261 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .pipe, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "-p", "--raw", "--full-index", parentid.?.id, commitid.id });
262 std.debug.assert(result.term == .exited and result.term.exited == 0);
263 return std.mem.trim(u8, result.stdout, "\n");
264}
265
266// TODO make this inspect .git manually
267pub fn getTreeDiffOnlyStat(alloc: std.mem.Allocator, dir: nfs.Dir, commitid: CommitId, parentid: ?CommitId) !string {
268 const t = tracer.trace(@src(), "", .{});
269 defer t.end();
270
271 if (parentid == null) {
272 // 4b825dc642cb6eb9a060e54bf8d69288fbee4904 is a hardcode for the empty tree in git sha1
273 // result of `printf | git hash-object -t tree --stdin`
274 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .ignore, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "--stat=2048", "--stat-graph-width=32", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", commitid.id });
275 std.debug.assert(result.term == .exited and result.term.exited == 0);
276 return result.stdout;
277 }
278 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .ignore, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "--stat=2048", "--stat-graph-width=32", parentid.?.id, commitid.id });
279 std.debug.assert(result.term == .exited and result.term.exited == 0);
280 return result.stdout;
281}
282
283// TODO make this inspect .git manually
284pub fn getTreeDiffOnlyDiff(alloc: std.mem.Allocator, dir: nfs.Dir, commitid: CommitId, parentid: ?CommitId) !string {
285 const t = tracer.trace(@src(), "", .{});
286 defer t.end();
287
288 if (parentid == null) {
289 // 4b825dc642cb6eb9a060e54bf8d69288fbee4904 is a hardcode for the empty tree in git sha1
290 // result of `printf | git hash-object -t tree --stdin`
291 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .ignore, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "--patch", "--full-index", "--patience", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", commitid.id });
292 if (!(result.term == .exited and result.term.exited == 0)) std.log.err("{s}", .{result.stderr});
293 std.debug.assert(result.term == .exited and result.term.exited == 0);
294 return result.stdout;
295 }
296 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .ignore, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "--patch", "--full-index", "--patience", parentid.?.id, commitid.id });
297 if (!(result.term == .exited and result.term.exited == 0)) std.log.err("{s}", .{result.stderr});
298 std.debug.assert(result.term == .exited and result.term.exited == 0);
299 return result.stdout;
300}
301
302// TODO make this inspect .git manually
303pub fn getFormatPatch(alloc: std.mem.Allocator, dir: nfs.Dir, commitid: CommitId, parentid: ?CommitId) !string {
304 const t = tracer.trace(@src(), "", .{});
305 defer t.end();
306
307 if (parentid == null) {
308 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .pipe, 1024 * 1024 * 1024, &.{ "git", "format-patch", "--stdout", "--full-index", "--patience", "--root", commitid.id });
309 if (!(result.term == .exited and result.term.exited == 0)) std.log.err("{s}", .{result.stderr});
310 std.debug.assert(result.term == .exited and result.term.exited == 0);
311 return result.stdout;
312 }
313 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .pipe, 1024 * 1024 * 1024, &.{ "git", "format-patch", "--stdout", "--full-index", "--patience", parentid.?.id ++ "..." ++ commitid.id });
314 if (!(result.term == .exited and result.term.exited == 0)) std.log.err("{s}", .{result.stderr});
315 std.debug.assert(result.term == .exited and result.term.exited == 0);
316 return result.stdout;
317}
318
319// TODO make this inspect .git manually
320// TODO make this return a Reader when we implement it ourselves
321pub fn getTreeDiffPath(alloc: std.mem.Allocator, dir: nfs.Dir, commitid: CommitId, parentid: ?CommitId, path: []const u8) !string {
322 const t = tracer.trace(@src(), "", .{});
323 defer t.end();
324
325 if (parentid == null) {
326 // 4b825dc642cb6eb9a060e54bf8d69288fbee4904 is a hardcode for the empty tree in git sha1
327 // result of `printf | git hash-object -t tree --stdin`
328 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .pipe, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "-p", "--raw", "--full-index", "4b825dc642cb6eb9a060e54bf8d69288fbee4904", commitid.id, "--", path });
329 std.debug.assert(result.term == .exited and result.term.exited == 0);
330 return std.mem.trim(u8, result.stdout, "\n");
331 }
332 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .pipe, 1024 * 1024 * 1024, &.{ "git", "diff-tree", "-p", "--raw", "--full-index", parentid.?.id, commitid.id, "--", path });
333 std.debug.assert(result.term == .exited and result.term.exited == 0);
334 return std.mem.trim(u8, result.stdout, "\n");
335}
336
337pub fn parseTreeDiff(alloc: std.mem.Allocator, input: string) !TreeDiff {
338 const t = tracer.trace(@src(), "", .{});
339 defer t.end();
340
341 var lineiter = std.mem.splitScalar(u8, input, '\n');
342 var overview = std.array_list.Managed(TreeDiff.StateLine).init(alloc);
343 var diffs = std.array_list.Managed(TreeDiff.Diff).init(alloc);
344 var meta = std.mem.zeroes(TreeDiff.Meta);
345
346 while (lineiter.next()) |lin| {
347 if (lin.len == 0) break;
348 std.debug.assert(lin[0] == ':');
349
350 // :100644 100644 c06b41d04c381f1841d445c0072219d9a7f57e17 e8f91cf7dd413ac65a362b0a170951033dba4762 M notes/all_packages.txt
351 var jter = std.mem.tokenizeScalar(u8, lin[1..], ' ');
352 const before_mode = try parseTreeMode(jter.next().?);
353 const after_mode = try parseTreeMode(jter.next().?);
354
355 const before_tree = ensureObjId(BlobId, jter.next().?);
356 const after_tree = ensureObjId(BlobId, jter.next().?);
357
358 var kter = std.mem.splitScalar(u8, jter.rest(), '\t');
359 const action_s = kter.next().?;
360
361 try overview.append(.{
362 .before = .{
363 .mode = before_mode,
364 .blob = before_tree,
365 },
366 .after = .{
367 .mode = after_mode,
368 .blob = after_tree,
369 },
370 .action = std.meta.stringToEnum(TreeDiff.Action, action_s).?,
371 .sub_path = kter.rest(),
372 });
373 meta.files_changed += 1;
374 }
375 if (lineiter.peek() == null) {
376 return TreeDiff{
377 .overview = overview.items,
378 .diffs = diffs.items,
379 .meta = meta,
380 };
381 }
382
383 // diff --git a/notes/all_packages.txt b/notes/all_packages.txt
384 // index c06b41d..e8f91cf 100644
385 // --- a/notes/all_packages.txt
386 // +++ b/notes/all_packages.txt
387 // @@ -89,3 +89,4 @@ freedesktop/xorg/libsm
388 // freedesktop/xorg/libxt
389 // freedesktop/xorg/libxmu
390 // ncompress
391 // +freedesktop/xorg/libxpm
392 blk: while (true) {
393 const first_line = lineiter.next().?;
394 std.debug.assert(std.mem.startsWith(u8, first_line, "diff --git"));
395 var i = diffs.items.len;
396 var n: usize = 0;
397 while (n < i) : (n += 1) i -= @intFromBool(overview.items[n].action == .T);
398 try diffs.append(.{
399 .index = @splat(.{ .id = "0000000000000000000000000000000000000000" }),
400 .before_path = overview.items[i].sub_path,
401 .after_path = overview.items[i].sub_path,
402 .before_mode = overview.items[i].before.mode,
403 .after_mode = overview.items[i].after.mode,
404 .subs = 0,
405 .adds = 0,
406 .content = "",
407 });
408 const diff = &diffs.items[diffs.items.len - 1];
409 if (overview.items[i].action == .T) diff.before_mode = .none;
410 if (overview.items[i].action == .T) diff.after_mode = .none;
411
412 while (true) {
413 if (lineiter.index.? >= input.len) {
414 break :blk;
415 }
416 if (lineiter.peek()) |lin| {
417 if (std.mem.startsWith(u8, lin, "index")) {
418 const index = lin[6..];
419 var iiter = std.mem.splitSequence(u8, index, "..");
420 var xx: [2][]const u8 = .{ iiter.next().?, iiter.next().? };
421 std.debug.assert(iiter.next() == null);
422 if (std.mem.indexOfScalar(u8, xx[1], ' ')) |j| xx[1] = xx[1][0..j];
423 diff.index[0] = ensureObjId(CommitId, xx[0]);
424 diff.index[1] = ensureObjId(CommitId, xx[1]);
425
426 lineiter.index.? += lin.len + 1;
427 break;
428 }
429 if (extras.trimPrefixEnsure(lin, "new file mode ")) |sl| {
430 diff.after_mode = parseTreeMode(sl) catch unreachable;
431 lineiter.index.? += lin.len + 1;
432 continue;
433 }
434 if (extras.trimPrefixEnsure(lin, "deleted file mode ")) |sl| {
435 diff.before_mode = parseTreeMode(sl) catch unreachable;
436 lineiter.index.? += lin.len + 1;
437 continue;
438 }
439 if (extras.trimPrefixEnsure(lin, "old mode ")) |sl| {
440 diff.before_mode = parseTreeMode(sl) catch unreachable;
441 lineiter.index.? += lin.len + 1;
442 continue;
443 }
444 if (extras.trimPrefixEnsure(lin, "new mode ")) |sl| {
445 diff.after_mode = parseTreeMode(sl) catch unreachable;
446 lineiter.index.? += lin.len + 1;
447 continue;
448 }
449 if (std.mem.startsWith(u8, lin, "diff --git")) {
450 continue :blk;
451 }
452 std.log.err("{s}", .{lin});
453 unreachable;
454 }
455 }
456
457 var content_start: usize = 0;
458
459 while (true) {
460 if (lineiter.index.? >= input.len) {
461 break :blk;
462 }
463 if (lineiter.peek()) |lin| {
464 if (std.mem.startsWith(u8, lin, "--- ")) {
465 diff.before_path = extras.trimPrefix(lin[4..], "a/");
466 lineiter.index.? += lin.len + 1;
467 continue;
468 }
469 if (std.mem.startsWith(u8, lin, "+++ ")) {
470 diff.after_path = extras.trimPrefix(lin[4..], "b/");
471 lineiter.index.? += lin.len + 1;
472 continue;
473 }
474 if (std.mem.startsWith(u8, lin, "@@ ")) {
475 content_start = lineiter.index.?;
476 lineiter.index.? += lin.len + 1;
477 break;
478 }
479 if (std.mem.startsWith(u8, lin, "Binary files ")) {
480 content_start = lineiter.index.?;
481 lineiter.index.? += lin.len + 1;
482 break;
483 }
484 if (std.mem.startsWith(u8, lin, "diff --git")) {
485 continue :blk;
486 }
487 std.log.err("{s}", .{lin});
488 unreachable;
489 }
490 }
491 if (lineiter.index.? >= input.len) {
492 diff.content = input[content_start..];
493 break :blk;
494 }
495
496 while (true) {
497 if (lineiter.peek()) |lin| {
498 if (std.mem.startsWith(u8, lin, "diff --git")) {
499 const content_end = lineiter.index.? - 1;
500 diff.content = input[content_start..content_end];
501 continue :blk;
502 }
503 if (lin[0] == '-') {
504 diff.subs += 1;
505 meta.lines_removed += 1;
506 }
507 if (lin[0] == '+') {
508 diff.adds += 1;
509 meta.lines_added += 1;
510 }
511 lineiter.index.? += lin.len + 1;
512
513 if (lineiter.index.? >= input.len) {
514 diff.content = input[content_start..];
515 break :blk;
516 }
517 }
518 }
519 }
520
521 return TreeDiff{
522 .overview = overview.items,
523 .diffs = diffs.items,
524 .meta = meta,
525 };
526}
527
528pub const TreeDiff = struct {
529 overview: []const StateLine,
530 diffs: []const Diff,
531 meta: Meta,
532
533 pub const StateLine = struct {
534 before: State,
535 after: State,
536 action: Action,
537 sub_path: string,
538 };
539
540 pub const State = struct {
541 mode: Tree.Object.Mode,
542 blob: BlobId,
543 };
544
545 pub const Action = enum {
546 A, // Added
547 C, // Copied
548 D, // Deleted
549 M, // Modified
550 R, // Renamed
551 T, // type changed
552 U, // Unmerged
553 X, // Unknown
554 B, // Broken
555
556 pub fn toString(self: Action, alloc: std.mem.Allocator) !string {
557 _ = alloc;
558 return @tagName(self);
559 }
560 };
561
562 pub const Diff = struct {
563 index: [2]CommitId,
564 before_path: string,
565 after_path: string,
566 before_mode: Tree.Object.Mode,
567 after_mode: Tree.Object.Mode,
568 adds: u32,
569 subs: u32,
570 content: string,
571 };
572
573 pub const Meta = struct {
574 files_changed: u32,
575 lines_added: u64,
576 lines_removed: u64,
577 };
578};
579
580pub fn parseTag(tagfile: string) !Tag {
581 const t = tracer.trace(@src(), "", .{});
582 defer t.end();
583
584 var iter = std.mem.splitScalar(u8, tagfile, '\n');
585 var result: Tag = .{
586 .raw = tagfile,
587 .object = undefined,
588 .type = undefined,
589 .tagger = null,
590 .message = undefined,
591 };
592 const object = extras.trimPrefixEnsure(iter.next().?, "object ").?;
593 std.debug.assert(object.len == 40);
594 result.object = object[0..40];
595 const ty = extras.trimPrefixEnsure(iter.next().?, "type ").?;
596 result.type = std.meta.stringToEnum(RefType, ty).?;
597 const tag = extras.trimPrefixEnsure(iter.next().?, "tag ").?;
598 _ = tag;
599 while (true) {
600 const line = iter.next() orelse break;
601 if (line.len == 0) break;
602 const space = std.mem.indexOfScalar(u8, line, ' ').?;
603 const k = line[0..space];
604 if (std.mem.eql(u8, k, "tagger")) result.tagger = try parseCommitUserAndAt(line[space + 1 ..]);
605 }
606 result.message = iter.rest();
607 return result;
608}
609
610// TODO make this inspect .git/objects
611pub fn getBlame(alloc: std.mem.Allocator, dir: nfs.Dir, at: CommitId, sub_path: string) !string {
612 const t = tracer.trace(@src(), " {s} -- {s}", .{ at.id, sub_path });
613 defer t.end();
614
615 const result = try root.child_process.run(alloc, dir, .ignore, .pipe, .pipe, 1024 * 1024 * 1024, &.{ "git", "blame", "-p", at.id, "--", sub_path });
616 extras.assertLog(result.term == .exited and result.term.exited == 0, "{s}", .{result.stderr});
617 return result.stdout;
618}
619
620pub const BlameIterator = struct {
621 inner: std.mem.SplitIterator(u8, .scalar),
622
623 pub fn init(blamefile: []const u8) BlameIterator {
624 return .{
625 .inner = std.mem.splitScalar(u8, blamefile, '\n'),
626 };
627 }
628
629 /// when line.continuity is >0 then .commit will be the same for the next continuity-1 iterations
630 pub fn next(self: *BlameIterator) ?Line {
631 var result: Line = .{
632 .commit = undefined,
633 .prev_line = 0,
634 .curr_line = 0,
635 .continuity = 0,
636 .author = .{ .name = "", .email = "", .at = .initUnix(0) },
637 .committer = .{ .name = "", .email = "", .at = .initUnix(0) },
638 .summary = "",
639 .previous = null,
640 .filename = "",
641 .line = "",
642 };
643 blk: {
644 var it = std.mem.splitScalar(u8, self.inner.next() orelse return null, ' ');
645 result.commit = ensureObjId(CommitId, extras.nullifyS(it.next().?) orelse return null);
646 result.prev_line = extras.parseDigits(u32, it.next().?, 10) catch unreachable;
647 result.curr_line = extras.parseDigits(u32, it.next().?, 10) catch unreachable;
648 result.continuity = extras.parseDigits(u32, it.next() orelse break :blk, 10) catch unreachable;
649 }
650 while (self.inner.next()) |line| {
651 if (line[0] == '\t') {
652 result.line = line[1..];
653 break;
654 }
655 if (extras.trimPrefixEnsure(line, "author ")) |trimmed| {
656 result.author = .{
657 .name = trimmed,
658 .email = extras.trimPrefixEnsure(self.inner.next().?, "author-mail ").?,
659 .at = parseAt(
660 extras.trimPrefixEnsure(self.inner.next().?, "author-time ").?,
661 extras.trimPrefixEnsure(self.inner.next().?, "author-tz ").?,
662 ),
663 };
664 continue;
665 }
666 if (extras.trimPrefixEnsure(line, "committer ")) |trimmed| {
667 result.committer = .{
668 .name = trimmed,
669 .email = extras.trimPrefixEnsure(self.inner.next().?, "committer-mail ").?,
670 .at = parseAt(
671 extras.trimPrefixEnsure(self.inner.next().?, "committer-time ").?,
672 extras.trimPrefixEnsure(self.inner.next().?, "committer-tz ").?,
673 ),
674 };
675 continue;
676 }
677 if (extras.trimPrefixEnsure(line, "summary ")) |trimmed| {
678 result.summary = trimmed;
679 continue;
680 }
681 if (extras.trimPrefixEnsure(line, "previous ")) |trimmed| {
682 var it = std.mem.splitScalar(u8, trimmed, ' ');
683 result.previous = .{
684 ensureObjId(CommitId, it.next().?),
685 it.next().?,
686 };
687 continue;
688 }
689 if (extras.trimPrefixEnsure(line, "filename ")) |trimmed| {
690 result.filename = trimmed;
691 continue;
692 }
693 }
694 return result;
695 }
696
697 pub const Line = struct {
698 commit: CommitId,
699 prev_line: u32,
700 curr_line: u32,
701 continuity: u32,
702 author: UserAndAt,
703 committer: UserAndAt,
704 summary: string,
705 previous: ?struct { CommitId, string },
706 filename: string,
707 line: string,
708 };
709};
710
711pub const Repository = struct {
712 gitdir: nfs.Dir,
713 gpa: std.mem.Allocator,
714 unpacked_loose_objects: std.StringArrayHashMapUnmanaged(GitObject),
715 unpacked_objects: std.AutoArrayHashMapUnmanaged(u64, GitObject),
716 unpacked_objects_window: usize,
717 idx_content: std.StringArrayHashMapUnmanaged([]const u8),
718 pack_content: std.StringArrayHashMapUnmanaged([]const u8),
719 commits: std.StringArrayHashMapUnmanaged(*Commit),
720 trees: std.StringArrayHashMapUnmanaged(*Tree),
721 tags: std.StringArrayHashMapUnmanaged(*Tag),
722 mailmap_content: []const u8,
723 mailmap: std.hash_map.StringHashMapUnmanaged([]const u8),
724 mailmap_names: std.hash_map.StringHashMapUnmanaged([]const u8),
725
726 pub const CacheBehavior = enum { no_cache, cache };
727
728 pub fn init(gitdir: nfs.Dir, gpa: std.mem.Allocator) Repository {
729 return .{
730 .gitdir = gitdir,
731 .gpa = gpa,
732 .unpacked_loose_objects = .empty,
733 .unpacked_objects = .empty,
734 .unpacked_objects_window = 0,
735 .idx_content = .empty,
736 .pack_content = .empty,
737 .commits = .empty,
738 .trees = .empty,
739 .tags = .empty,
740 .mailmap_content = "",
741 .mailmap = .empty,
742 .mailmap_names = .empty,
743 };
744 }
745
746 pub fn deinit(r: *Repository) void {
747 for (r.unpacked_loose_objects.values()) |v| r.gpa.free(v.content);
748 r.unpacked_loose_objects.deinit(r.gpa);
749 for (r.unpacked_objects.values()) |v| r.gpa.free(v.content);
750 r.unpacked_objects.deinit(r.gpa);
751 for (r.idx_content.keys()) |k| r.gpa.free(k);
752 for (r.idx_content.values()) |v| nfs.munmap(v);
753 r.idx_content.deinit(r.gpa);
754 for (r.pack_content.values()) |v| nfs.munmap(v);
755 r.pack_content.deinit(r.gpa);
756 for (r.commits.values()) |v| v.destroy(r);
757 r.commits.deinit(r.gpa);
758 for (r.trees.values()) |v| v.destroy(r);
759 r.trees.deinit(r.gpa);
760 for (r.tags.values()) |v| v.destroy(r);
761 r.tags.deinit(r.gpa);
762 r.mailmap.deinit(r.gpa);
763 r.mailmap_names.deinit(r.gpa);
764 }
765
766 pub fn initMailmap(r: *Repository) !void {
767 const headid = try getHEAD(r.gpa, r.gitdir) orelse return;
768 const head = try r.getCommitA(headid.id, .cache);
769 const tree = try r.getTreeA(head.tree.id, .cache);
770 const blob = tree.get(".mailmap") orelse return;
771 if (blob.id != .blob) return;
772 const mailmap_content = try r.getBlobA(blob.id.blob.id, .cache);
773
774 var mailmap: std.hash_map.StringHashMapUnmanaged([]const u8) = .empty;
775 var mailmap_names: std.hash_map.StringHashMapUnmanaged([]const u8) = .empty;
776 if (mailmap_content.len > 0) {
777 var iter = std.mem.splitScalar(u8, mailmap_content, '\n');
778 while (iter.next()) |line| {
779 if (std.mem.startsWith(u8, line, "#")) continue;
780 var jter = std.mem.splitScalar(u8, line, '<');
781 const name = std.mem.trim(u8, jter.next().?, " ");
782 var first: ?[]const u8 = null;
783 while (jter.next()) |fntry| {
784 const email = fntry[0 .. std.mem.indexOfScalar(u8, fntry, '>') orelse continue];
785 if (first == null) {
786 first = email;
787 try mailmap_names.put(r.gpa, email, name);
788 continue;
789 }
790 try mailmap.put(r.gpa, email, first.?);
791 }
792 }
793 }
794 r.mailmap_content = mailmap_content;
795 r.mailmap = mailmap;
796 r.mailmap_names = mailmap_names;
797 }
798
799 pub fn getObject(r: *Repository, oid: Id, cache_behavior: CacheBehavior) anyerror!?GitObject {
800 const t = tracer.trace(@src(), " {s}", .{oid});
801 defer t.end();
802
803 if (cache_behavior == .cache) if (r.unpacked_loose_objects.get(oid)) |obj| {
804 return obj;
805 };
806 if (oid.len == 40) blk: { //sha1 object
807 var sub_path: [49:0]u8 = "objects/00/00000000000000000000000000000000000000".*;
808 @memcpy(sub_path[8..][0..2], oid[0..2]);
809 @memcpy(sub_path[11..], oid[2..]);
810 const objfile = r.gitdir.openFile(&sub_path, .{}) catch |err| switch (err) {
811 error.ENOENT => break :blk,
812 else => |e| return e,
813 };
814 defer objfile.close();
815 const stat = try objfile.stat();
816 const compressed_content = try objfile.readAlloc(r.gpa, stat.size);
817 defer r.gpa.free(compressed_content);
818 var list: nio.AllocatingWriter = .init(r.gpa);
819 errdefer list.deinit();
820 try list.ensureUnusedCapacity(512);
821 try inflate_decompress(compressed_content, &list);
822 const data = list.items;
823 const header = data[0..std.mem.indexOfScalar(u8, data, 0).?];
824 const _type_s = header[0..std.mem.indexOfScalar(u8, header, ' ').?];
825 const _type = std.meta.stringToEnum(RefType, _type_s).?;
826 const content_len = try extras.parseDigits(u64, header[_type_s.len + 1 ..], 10);
827 list.replaceRangeAssumeCapacity(0, header.len + 1, "");
828 const content = try list.toOwnedSlice();
829 std.debug.assert(content.len == content_len);
830 const obj: GitObject = .{ .type = _type, .content = content };
831 if (cache_behavior == .cache) try r.unpacked_loose_objects.put(r.gpa, oid, obj);
832 return obj;
833 }
834
835 // read .idx
836 if (r.idx_content.count() == 0) {
837 const t2 = tracer.trace(@src(), " read objects/pack", .{});
838 defer t2.end();
839 const packdir = try r.gitdir.openDir("objects/pack", .{});
840 defer packdir.close();
841 var iter = packdir.iterate();
842 while (try iter.next()) |entry| {
843 const t3 = tracer.trace(@src(), " {s}", .{entry.name});
844 defer t3.end();
845 if (entry.type != .REG) continue;
846 if (!std.mem.endsWith(u8, entry.name, ".idx")) continue;
847 // std.log.debug("packdir iterate: {s}", .{entry.name});
848
849 const idx_file = try packdir.openFile(entry.name, .{});
850 defer idx_file.close();
851 const idx_content = try idx_file.mmap();
852 errdefer nfs.munmap(idx_content);
853 try r.idx_content.put(
854 r.gpa,
855 try r.gpa.dupe(u8, entry.name),
856 idx_content,
857 );
858 }
859 }
860 const pack_index, const pack_offset = try r.getObjectPackIndex(oid) orelse return null;
861 // parse .pack
862 return try r.getPackedObject(pack_index, pack_offset, cache_behavior);
863 }
864
865 fn getObjectPackIndex(r: *Repository, oid: Id) !?[2]usize {
866 const t = tracer.trace(@src(), " {s}", .{oid});
867 defer t.end();
868
869 for (r.idx_content.keys(), r.idx_content.values()) |idx_path, idx_content| {
870 if (std.mem.startsWith(u8, idx_content, "\xfftOc")) {
871 var idx_fbs: nio.FixedBufferStream([]const u8) = .init(idx_content);
872 _ = idx_fbs.takeSlice(4);
873 std.debug.assert(idx_fbs.takeInt(u32, .big) == 2);
874 const fanout_be_bytes = idx_fbs.takeSlice(255 * 4);
875 _ = fanout_be_bytes;
876 const object_count = idx_fbs.takeInt(u32, .big);
877 const name_bytes = idx_fbs.takeSlice(object_count * 20);
878 const crc32_bytes = idx_fbs.takeSlice(object_count * 4);
879 _ = crc32_bytes;
880 const offsets_be = idx_fbs.takeIntSlice(u32, object_count);
881 const largeoffsets_be: []align(1) const u64 = @ptrCast(idx_fbs.rest());
882
883 // std.sort.binarySearch;
884 const i = bll: {
885 var low: usize = 0;
886 var high: usize = object_count;
887 while (low < high) {
888 const mid = low + (high - low) / 2;
889 const object_id = &extras.to_hex(name_bytes[mid * 20 ..][0..20].*);
890 switch (std.mem.order(u8, oid, object_id)) {
891 .eq => break :bll mid,
892 .gt => low = mid + 1,
893 .lt => high = mid,
894 }
895 }
896 continue;
897 };
898
899 const pack_offset_candidate = @byteSwap(offsets_be[i]);
900 const pack_offset = if (pack_offset_candidate & 0x80000000 == 0) pack_offset_candidate else @byteSwap(largeoffsets_be[pack_offset_candidate & 0x7fffffff]);
901 // std.log.debug("found {s} in {s} at offset {d} {d}", .{ oid, idx_path, pack_offset_candidate, pack_offset });
902
903 const pack_index = r.pack_content.getIndex(idx_path) orelse clk: {
904 var pack_path: [128]u8 = @splat(0);
905 @memcpy(pack_path[0..13], "objects/pack/");
906 @memcpy(pack_path[13..][0..idx_path.len], idx_path);
907 @memcpy(pack_path[13..][idx_path.len - 4 ..][0..5], ".pack");
908 const pack_name_nidx = std.mem.indexOfScalar(u8, &pack_path, 0).?;
909 const pack_name = pack_path[0..pack_name_nidx :0];
910 const pack_file = try r.gitdir.openFile(pack_name, .{});
911 defer pack_file.close();
912 const pack_content = try pack_file.mmap();
913 errdefer nfs.munmap(pack_content);
914 try r.pack_content.put(r.gpa, idx_path, pack_content);
915 break :clk r.pack_content.count() - 1;
916 };
917 return .{ pack_index, pack_offset };
918 }
919 return error.UnexpectedIdxVersion;
920 }
921 return null;
922 }
923
924 fn getPackedObject(r: *Repository, pack_index: usize, pack_offset: usize, cache_behavior: CacheBehavior) !GitObject {
925 const t = tracer.trace(@src(), " {d} {d} {s}", .{ pack_index, pack_offset, r.pack_content.keys()[pack_index] });
926 defer t.end();
927
928 const key = std.hash.Wyhash.hash(0, &(std.mem.toBytes(pack_index) ++ std.mem.toBytes(pack_offset)));
929 if (cache_behavior == .cache) if (r.unpacked_objects.get(key)) |o| return o;
930
931 const pack_content = r.pack_content.values()[pack_index];
932 if (!std.mem.eql(u8, pack_content[0..4], "PACK")) return error.InvalidGitPack;
933 const pack_version = std.mem.readInt(u32, pack_content[4..][0..4], .big);
934 switch (pack_version) {
935 2 => {
936 const packedobj_content = pack_content[pack_offset..];
937 var packedobj_fbs = nio.FixedBufferStream([]const u8).init(packedobj_content);
938 const PackedObjType = enum(u3) { none, commit, tree, blob, tag, reserved, ofs_delta, ref_delta };
939 var c: usize = packedobj_fbs.takeArray(1)[0];
940 const ty: PackedObjType = @enumFromInt((c >> 4) & 7);
941 var size: usize = c & 15;
942 var shift: u6 = 4;
943 while (c & 0x80 > 0) {
944 c = packedobj_fbs.takeArray(1)[0];
945 size += (c & 0x7f) << shift;
946 shift += 7;
947 }
948 // std.log.debug("pack_index={d} pack_offset={d} type={s} size={d}", .{ pack_index, pack_offset, @tagName(ty), size });
949 const t2 = tracer.trace(@src(), " 2 {s} {d}", .{ @tagName(ty), size });
950 defer t2.end();
951 switch (ty) {
952 .none => {
953 unreachable;
954 },
955 .reserved => {
956 unreachable;
957 },
958 .commit, .tree, .blob, .tag => {
959 const compressed_content = packedobj_fbs.rest();
960 var list: nio.AllocatingWriter = .init(r.gpa);
961 errdefer list.deinit();
962 try list.ensureUnusedCapacity(512);
963 try inflate_decompress(compressed_content, &list);
964 const _type = std.meta.stringToEnum(RefType, @tagName(ty)).?;
965 const content = try list.toOwnedSlice();
966 const obj: GitObject = .{ .type = _type, .content = content };
967 const window_max = 1024 * 128;
968 if (cache_behavior == .cache) {
969 if (r.unpacked_objects.count() < window_max) {
970 try r.unpacked_objects.put(r.gpa, key, obj);
971 } else {
972 const item = &r.unpacked_objects.values()[r.unpacked_objects_window];
973 r.gpa.free(item.content);
974 r.unpacked_objects.keys()[r.unpacked_objects_window] = key;
975 item.* = obj;
976 }
977 r.unpacked_objects_window += 1;
978 r.unpacked_objects_window %= window_max;
979 }
980 return obj;
981 },
982 .ofs_delta => {
983 var offset: usize = 0;
984 while (true) {
985 const c2: usize = packedobj_fbs.takeArray(1)[0];
986 offset = (offset << 7) | (c2 & 0x7f);
987 if (c2 & 0x80 == 0) break;
988 offset += 1;
989 }
990 const base_pack_offset = pack_offset - offset;
991 const base_obj = try r.getPackedObject(pack_index, base_pack_offset, cache_behavior);
992 defer if (cache_behavior == .no_cache) r.gpa.free(base_obj.content);
993 return r.getDeltadObject(key, &packedobj_fbs, size, base_obj, cache_behavior);
994 },
995 .ref_delta => {
996 const base_oid = extras.to_hex(packedobj_fbs.takeSlice(20)[0..20].*);
997 const base_obj = (try r.getObject(&base_oid, cache_behavior)).?;
998 defer if (cache_behavior == .no_cache) r.gpa.free(base_obj.content);
999 return r.getDeltadObject(key, &packedobj_fbs, size, base_obj, cache_behavior);
1000 },
1001 }
1002 comptime unreachable;
1003 },
1004 3 => {
1005 return error.ReservedPackVersion;
1006 },
1007 else => return error.InvalidGitPack,
1008 }
1009 }
1010
1011 fn getDeltadObject(r: *Repository, key: u64, packedobj_fbs: *nio.FixedBufferStream([]const u8), size: usize, base_obj: GitObject, cache_behavior: CacheBehavior) !GitObject {
1012 const compressed_content = packedobj_fbs.rest();
1013 var list: nio.AllocatingWriter = .init(r.gpa);
1014 defer list.deinit();
1015 try list.ensureUnusedCapacity(size);
1016 try inflate_decompress(compressed_content, &list);
1017 std.debug.assert(list.items.len == size);
1018 // std.log.debug("maybe_oid={?s} size={d}", .{ maybe_oid, size });
1019 // std.log.debug("transformation data=[{d}]{d}", .{ list.items.len, list.items });
1020
1021 var unpackedobj_fbs = nio.FixedBufferStream([]const u8).init(list.items);
1022
1023 var list2: std.ArrayListUnmanaged(u8) = .empty;
1024 errdefer list2.deinit(r.gpa);
1025
1026 var base_size: usize = 0;
1027 while (true) {
1028 const c2: usize = unpackedobj_fbs.takeArray(1)[0];
1029 base_size = (base_size << 7) | (c2 & 0x7f);
1030 if (c2 & 0x80 == 0) break;
1031 base_size += 1;
1032 }
1033 // std.log.debug("base_size={d}", .{base_size});
1034
1035 var obj_size: usize = 0;
1036 while (true) {
1037 const c2: usize = unpackedobj_fbs.takeArray(1)[0];
1038 obj_size = (obj_size << 7) | (c2 & 0x7f);
1039 if (c2 & 0x80 == 0) break;
1040 obj_size += 1;
1041 }
1042 // std.log.debug("obj_size={d}", .{obj_size});
1043
1044 while (unpackedobj_fbs.pos < unpackedobj_fbs.buffer.len) {
1045 const c2 = unpackedobj_fbs.takeArray(1)[0];
1046 if (c2 & 0x80 > 0) {
1047 // copy range from base
1048 var b: extras.RingBuffer(u8, 7) = .{};
1049 for (0..7) |i| {
1050 const mask = @as(u8, 1) << @intCast(i);
1051 b.append(if (c2 & mask > 0) unpackedobj_fbs.takeArray(1)[0] else 0);
1052 }
1053 const start: u32 = @bitCast(b.items[0..4].*);
1054 var nbytes: u24 = @bitCast(b.items[4..7].*);
1055 if (nbytes == 0) nbytes = 0x10000;
1056 // std.log.debug("- copy from base: start={d} nbytes={d}", .{ start, nbytes });
1057 // std.log.debug(" - {d} {d}", .{ c2, b.items });
1058 const bytes = base_obj.content[start..][0..nbytes];
1059 // std.log.debug("{s}\n", .{bytes});
1060 try list2.appendSlice(r.gpa, bytes);
1061 } else {
1062 // append new data
1063 const nbytes = c2 & 0x7f;
1064 // std.log.debug("- append new bytes={d}", .{nbytes});
1065 if (nbytes == 0) continue;
1066 const bytes = unpackedobj_fbs.takeSlice(nbytes);
1067 // std.log.debug("{s}\n", .{bytes});
1068 try list2.appendSlice(r.gpa, bytes);
1069 }
1070 }
1071
1072 // std.log.debug("- done {d}", .{list2.items.len});
1073 // std.log.debug("{s}\n", .{list2.items});
1074 const _type = base_obj.type;
1075 const content = try list2.toOwnedSlice(r.gpa);
1076 const obj: GitObject = .{ .type = _type, .content = content };
1077 const window_max = 1024 * 128;
1078 if (cache_behavior == .cache) {
1079 if (r.unpacked_objects.count() < window_max) {
1080 try r.unpacked_objects.put(r.gpa, key, obj);
1081 } else {
1082 const item = &r.unpacked_objects.values()[r.unpacked_objects_window];
1083 r.gpa.free(item.content);
1084 r.unpacked_objects.keys()[r.unpacked_objects_window] = key;
1085 item.* = obj;
1086 }
1087 r.unpacked_objects_window += 1;
1088 r.unpacked_objects_window %= window_max;
1089 }
1090 return obj;
1091 }
1092
1093 pub fn getObjectA(r: *Repository, oid: Id, cache_behavior: CacheBehavior) !GitObject {
1094 return (try r.getObject(oid, cache_behavior)).?;
1095 }
1096
1097 pub fn getObjectC(r: *Repository, oid: Id, cache_behavior: CacheBehavior) ![]const u8 {
1098 return (try r.getObjectA(oid, cache_behavior)).content;
1099 }
1100
1101 pub fn getObjectS(r: *Repository, oid: Id, cache_behavior: CacheBehavior) !usize {
1102 const content = try r.getObjectC(oid, cache_behavior);
1103 defer if (cache_behavior == .no_cache) r.gpa.free(content);
1104 return content.len;
1105 }
1106
1107 const GitObject = struct {
1108 type: RefType,
1109 content: []const u8,
1110 };
1111
1112 pub fn getBlob(r: *Repository, id: BlobId, cache_behavior: CacheBehavior) !?[]const u8 {
1113 if (try r.getObject(id.id, cache_behavior)) |obj| {
1114 if (obj.type == .blob) {
1115 if (cache_behavior == .cache) return try r.gpa.dupe(u8, obj.content);
1116 return obj.content;
1117 }
1118 }
1119 return null;
1120 }
1121
1122 pub fn getBlobA(r: *Repository, id: Id, cache_behavior: CacheBehavior) ![]const u8 {
1123 return (try r.getBlob(.{ .id = id }, cache_behavior)).?;
1124 }
1125
1126 pub fn getCommit(r: *Repository, id: CommitId, cache_behavior: CacheBehavior) !?struct { CommitId, *Commit } {
1127 const t = tracer.trace(@src(), " {s}", .{id.id});
1128 defer t.end();
1129
1130 if (cache_behavior == .cache) if (r.commits.get(id.id)) |val| {
1131 return .{ id, val };
1132 };
1133 if (try r.getObject(id.id, .no_cache)) |obj| {
1134 if (obj.type == .commit) {
1135 const raw = obj.content;
1136 errdefer r.gpa.free(raw);
1137 const commit = try r.gpa.create(Commit);
1138 errdefer r.gpa.destroy(commit);
1139 commit.* = try parseCommit(r.gpa, raw, &r.mailmap, &r.mailmap_names);
1140 if (cache_behavior == .cache) try r.commits.put(r.gpa, id.id, commit);
1141 return .{ id, commit };
1142 }
1143 r.gpa.free(obj.content);
1144 }
1145 return null;
1146 }
1147
1148 pub fn getCommitA(r: *Repository, id: Id, cache_behavior: CacheBehavior) !*Commit {
1149 return (try r.getCommit(.{ .id = id }, cache_behavior)).?.@"1";
1150 }
1151
1152 pub fn getTree(r: *Repository, id: TreeId, cache_behavior: CacheBehavior) !?struct { TreeId, *Tree } {
1153 const t = tracer.trace(@src(), " {s}", .{id.id});
1154 defer t.end();
1155
1156 if (cache_behavior == .cache) if (r.trees.get(id.id)) |val| {
1157 return .{ id, val };
1158 };
1159 if (try r.getObject(id.id, .cache)) |obj| {
1160 if (obj.type == .tree) {
1161 const raw = try r.gpa.dupe(u8, obj.content);
1162 errdefer r.gpa.free(raw);
1163 var children: std.ArrayList(Tree.Object) = .empty;
1164 errdefer children.deinit(r.gpa);
1165 try children.ensureUnusedCapacity(r.gpa, 33);
1166 var i: usize = 0;
1167 while (i < raw.len) {
1168 const mode_end = std.mem.indexOfScalar(u8, raw[i..], ' ').?;
1169 const mode = raw[i..][0..mode_end];
1170 i += mode_end + 1;
1171
1172 const name_end = std.mem.indexOfScalar(u8, raw[i..], 0).?;
1173 const name = raw[i..][0..name_end :0];
1174 i += name_end + 1;
1175
1176 const oid_raw = raw[i..][0..20].*;
1177 const oid_hex = extras.to_hex(oid_raw);
1178 i += 20;
1179
1180 var mode_buf: [6]u8 = @splat('0');
1181 @memcpy(mode_buf[6 - mode.len ..], mode);
1182 const mode_real = try parseTreeMode(&mode_buf);
1183
1184 try children.append(r.gpa, .{
1185 .mode = mode_real,
1186 .name = name,
1187 .id_bytes = oid_hex,
1188 .id = undefined,
1189 });
1190 }
1191
1192 const children_slice = try children.toOwnedSlice(r.gpa);
1193 errdefer r.gpa.free(children_slice);
1194 for (children_slice) |*item| item.id = switch (item.mode.type) {
1195 .file => .{ .blob = .{ .id = &item.id_bytes } },
1196 .directory => .{ .tree = .{ .id = &item.id_bytes } },
1197 .submodule => .{ .commit = .{ .id = &item.id_bytes } },
1198 .symlink => .{ .blob = .{ .id = &item.id_bytes } },
1199 .none => unreachable,
1200 };
1201 const tree = try r.gpa.create(Tree);
1202 errdefer r.gpa.destroy(tree);
1203 tree.* = .{ .raw = raw, .children = children_slice };
1204 if (cache_behavior == .cache) try r.trees.put(r.gpa, id.id, tree);
1205 return .{ id, tree };
1206 }
1207 }
1208 return null;
1209 }
1210
1211 pub fn getTreeA(r: *Repository, id: Id, cache_behavior: CacheBehavior) !*Tree {
1212 return (try r.getTree(.{ .id = id }, cache_behavior)).?.@"1";
1213 }
1214
1215 pub fn getTag(r: *Repository, id: TagId, cache_behavior: CacheBehavior) !?struct { TagId, *Tag } {
1216 const t = tracer.trace(@src(), " {s}", .{id.id});
1217 defer t.end();
1218
1219 if (cache_behavior == .cache) if (r.tags.get(id.id)) |val| {
1220 return .{ id, val };
1221 };
1222 if (try r.getObject(id.id, .cache)) |obj| {
1223 if (obj.type == .tag) {
1224 const raw = try r.gpa.dupe(u8, obj.content);
1225 errdefer r.gpa.free(raw);
1226 const tag = try r.gpa.create(Tag);
1227 errdefer r.gpa.destroy(tag);
1228 tag.* = try parseTag(raw);
1229 if (cache_behavior == .cache) try r.tags.put(r.gpa, id.id, tag);
1230 return .{ id, tag };
1231 }
1232 }
1233 return null;
1234 }
1235
1236 pub fn getTagA(r: *Repository, id: Id, cache_behavior: CacheBehavior) !*Tag {
1237 return (try r.getTag(.{ .id = id }, cache_behavior)).?.@"1";
1238 }
1239
1240 pub fn getHeads(r: *Repository, arena: std.mem.Allocator) ![]Ref {
1241 const t = tracer.trace(@src(), "", .{});
1242 defer t.end();
1243
1244 const refs = try r.getRefs(arena, "heads");
1245 for (refs) |*e| e.commit = e.oid;
1246 return refs;
1247 }
1248
1249 pub fn getTags(r: *Repository, arena: std.mem.Allocator) ![]Ref {
1250 const t = tracer.trace(@src(), "", .{});
1251 defer t.end();
1252
1253 return r.getRefs(arena, "tags");
1254 }
1255
1256 pub fn getRefs(r: *Repository, arena: std.mem.Allocator, comptime kind: [:0]const u8) ![]Ref {
1257 const t = tracer.trace(@src(), "", .{});
1258 defer t.end();
1259
1260 var map: std.StringArrayHashMapUnmanaged(struct { Id, ?Id }) = .empty;
1261 try r.addPackedRefs(&map, arena, kind);
1262 try r.addDirRefs(&map, arena, kind);
1263 var list: std.ArrayListUnmanaged(Ref) = .empty;
1264 try list.ensureUnusedCapacity(arena, map.count());
1265 for (map.keys(), map.values()) |label, vals| list.appendAssumeCapacity(.{ .label = label, .oid = vals[0], .commit = vals[1] });
1266 return list.items;
1267 }
1268
1269 fn addPackedRefs(r: *Repository, map: *std.StringArrayHashMapUnmanaged(struct { Id, ?Id }), arena: std.mem.Allocator, comptime kind: [:0]const u8) !void {
1270 const t = tracer.trace(@src(), "", .{});
1271 defer t.end();
1272
1273 var file = r.gitdir.openFile("packed-refs", .{}) catch |err| switch (err) {
1274 error.ENOENT => return,
1275 else => |e| return e,
1276 };
1277 defer file.close();
1278 const content = try file.mmap();
1279 defer nfs.munmap(content);
1280 var iter = std.mem.splitScalar(u8, content, '\n');
1281 var prev_line: []const u8 = "";
1282 while (iter.next()) |line| {
1283 defer prev_line = line;
1284 if (line.len == 0) break;
1285 if (line[0] == '#') continue;
1286 if (line[0] == '^') {
1287 if (!std.mem.startsWith(u8, prev_line[41..], "refs/" ++ kind ++ "/")) continue;
1288 map.values()[map.count() - 1][1] = ensureObjId(CommitId, try arena.dupe(u8, line[1..])).id;
1289 continue;
1290 }
1291 std.debug.assert(extras.matchesAll(u8, line[0..40], std.ascii.isHex));
1292 std.debug.assert(line[40] == ' ');
1293 const rest = extras.trimPrefixEnsure(line[41..], "refs/" ++ kind ++ "/") orelse continue;
1294 const oid = try arena.dupe(u8, line[0..40]);
1295 const label = try arena.dupeZ(u8, rest);
1296 try map.put(arena, label, .{ oid[0..40], null });
1297 }
1298 }
1299
1300 fn addDirRefs(r: *Repository, map: *std.StringArrayHashMapUnmanaged(struct { Id, ?Id }), arena: std.mem.Allocator, comptime kind: [:0]const u8) !void {
1301 const t = tracer.trace(@src(), "", .{});
1302 defer t.end();
1303
1304 var dir = try r.gitdir.openDir("refs/" ++ kind, .{});
1305 defer dir.close();
1306 var walker = try dir.walk(arena);
1307 defer walker.deinit();
1308 while (try walker.next()) |entry| {
1309 if (entry.type != .REG) continue;
1310 var file = try dir.openFile(entry.path, .{});
1311 defer file.close();
1312 const label = try arena.dupeZ(u8, entry.path);
1313 const oid = try file.readAlloc(arena, 40);
1314 try map.put(arena, label, .{ oid[0..40], null });
1315 }
1316 }
1317
1318 pub fn getTreeCommits(r: *Repository, arena: std.mem.Allocator, base_oid: CommitId, dir_path: []const u8, timeout_ms: u64) ![]const CommitId {
1319 const t = tracer.trace(@src(), "", .{});
1320 defer t.end();
1321
1322 const start = time.milliTimestamp();
1323 var timeout_ended = false;
1324
1325 const base = try r.getCommitA(base_oid.id, .no_cache);
1326 const base_tree_id, const base_tree_id_parent = try traverseTo(r, base.tree, dir_path);
1327 defer if (base_tree_id_parent) |p| p.destroy(r);
1328 const base_tree = try r.getTreeA(base_tree_id.?.id, .cache);
1329 const total = base_tree.children.len;
1330
1331 var found: usize = 0;
1332 var result: std.StringArrayHashMapUnmanaged(CommitId) = .empty;
1333 defer result.deinit(r.gpa);
1334 for (base_tree.children) |obj| try result.put(r.gpa, obj.name, .zero);
1335
1336 var set: std.bit_set.DynamicBitSetUnmanaged = try .initEmpty(r.gpa, total);
1337 defer set.deinit(r.gpa);
1338
1339 var searched: usize = 1;
1340 var commit_id_prev = base_oid;
1341 var commit_id = base_oid;
1342 var commit_prev_prev: ?*Commit = null;
1343 var commit_prev: ?*Commit = null;
1344 var commit = base;
1345 var tree_id = base_tree_id.?;
1346 while (true) : ({
1347 if (commit_prev_prev) |p| p.destroy(r);
1348 commit_prev_prev = commit_prev;
1349 commit_prev = commit;
1350 commit_id_prev = commit_id;
1351 commit_id, commit = (try r.getCommit(commit.parents[0], .no_cache)).?;
1352 }) {
1353 searched += 1;
1354 if (commit.parents.len == 0) break;
1355 if (timeout_ms > 0 and time.milliTimestamp() - start > timeout_ms) {
1356 timeout_ended = true;
1357 break;
1358 }
1359 const new_tree_id, const new_tree_id_parent = try traverseTo(r, commit.tree, dir_path);
1360 defer if (new_tree_id_parent) |p| p.destroy(r);
1361 if (new_tree_id == null) {
1362 var i: usize = 0;
1363 while (findFirstUnset(set, i)) |j| : (i += 1) {
1364 i = j;
1365 const k = result.keys()[i];
1366 found += 1;
1367 result.putAssumeCapacity(k, .{ .id = (try r.gpa.dupe(u8, commit_id_prev.id))[0..40] });
1368 set.set(i);
1369 // std.log.debug("found [{d}/{d}] objects after searching {d} commits, found {s}", .{ found, total, searched, k });
1370 continue;
1371 }
1372 break;
1373 }
1374 if (new_tree_id.?.eql(tree_id)) continue;
1375 tree_id = new_tree_id.?;
1376 const tree = try r.getTreeA(tree_id.id, .no_cache);
1377 defer tree.destroy(r);
1378 var i: usize = 0;
1379 while (findFirstUnset(set, i)) |j| : (i += 1) {
1380 i = j;
1381 const k = result.keys()[i];
1382 const new = tree.get(k);
1383 if (new == null) {
1384 found += 1;
1385 result.putAssumeCapacity(k, .{ .id = (try r.gpa.dupe(u8, commit_id_prev.id))[0..40] });
1386 set.set(i);
1387 // std.log.debug("found [{d}/{d}] objects after searching {d} commits, at {d} found {s}", .{ found, total, searched, i, k });
1388 continue;
1389 }
1390 if (!std.mem.eql(u8, new.?.id.erase(), base_tree.children[i].id.erase())) {
1391 found += 1;
1392 result.putAssumeCapacity(k, .{ .id = (try r.gpa.dupe(u8, commit_id_prev.id))[0..40] });
1393 set.set(i);
1394 // std.log.debug("found [{d}/{d}] objects after searching {d} commits, at {d} found {s}", .{ found, total, searched, i, k });
1395 continue;
1396 }
1397 }
1398 if (set.count() == total) {
1399 break;
1400 }
1401 }
1402 if (!timeout_ended) for (0..total, result.values()) |i, *v| {
1403 if (!set.isSet(i)) {
1404 v.* = commit_id;
1405 }
1406 };
1407
1408 // const end = time.milliTimestamp();
1409 // std.log.debug("found {d} in {d}ms", .{ total, end - start });
1410
1411 return try arena.dupe(CommitId, result.values());
1412 }
1413
1414 pub fn diffFileIterator(r: *Repository, writable: anytype, commitid_from: ?CommitId, commitid_to: CommitId, S: type) !void {
1415 const A = struct {
1416 fn item(e: *Repository, w: anytype, mode: Tree.Object.Mode, id: Id, p: ?*const PathListNode, name: []const u8) !void {
1417 try S.item(e, w, .none, mode, &@splat('0'), id, .A, p, name);
1418 }
1419 fn dir(e: *Repository, w: anytype, t: Id, p: ?*const PathListNode, o: usize) !void {
1420 const tree = try e.getTreeA(t, .cache);
1421 for (tree.children[o..]) |obj| {
1422 if (obj.mode.type == .directory) {
1423 try dir(e, w, obj.id.tree.id, &.{ .prev = p, .data = obj.name }, 0);
1424 continue;
1425 }
1426 try item(e, w, obj.mode, obj.id.erase(), p, obj.name);
1427 }
1428 }
1429 pub fn either(e: *Repository, w: anytype, p: ?*const PathListNode, obj: Tree.Object) !void {
1430 if (obj.mode.type == .directory) {
1431 return dir(e, w, obj.id.tree.id, &.{ .prev = p, .data = obj.name }, 0);
1432 }
1433 return item(e, w, obj.mode, obj.id.erase(), p, obj.name);
1434 }
1435 };
1436 const D = struct {
1437 fn item(e: *Repository, w: anytype, mode: Tree.Object.Mode, id: Id, p: ?*const PathListNode, name: []const u8) !void {
1438 try S.item(e, w, mode, .none, id, &@splat('0'), .D, p, name);
1439 }
1440 fn dir(e: *Repository, w: anytype, t: Id, p: ?*const PathListNode, o: usize) !void {
1441 const tree = try e.getTreeA(t, .cache);
1442 for (tree.children[o..]) |obj| {
1443 if (obj.mode.type == .directory) {
1444 try dir(e, w, obj.id.tree.id, &.{ .prev = p, .data = obj.name }, 0);
1445 continue;
1446 }
1447 try item(e, w, obj.mode, obj.id.erase(), p, obj.name);
1448 }
1449 }
1450 pub fn either(e: *Repository, w: anytype, p: ?*const PathListNode, obj: Tree.Object) !void {
1451 if (obj.mode.type == .directory) {
1452 return dir(e, w, obj.id.tree.id, &.{ .prev = p, .data = obj.name }, 0);
1453 }
1454 return item(e, w, obj.mode, obj.id.erase(), p, obj.name);
1455 }
1456 };
1457 const M = struct {
1458 fn dir(e: *Repository, w: anytype, b_t: Id, a_t: Id, p: ?*const PathListNode) !void {
1459 var before_i: usize = 0;
1460 const before_tree = try e.getTreeA(b_t, .cache);
1461 const before_children = before_tree.children;
1462
1463 var after_i: usize = 0;
1464 const after_tree = try e.getTreeA(a_t, .cache);
1465 const after_children = after_tree.children;
1466
1467 while (true) {
1468 if (after_i == after_tree.children.len) {
1469 try D.dir(e, w, b_t, p, before_i);
1470 break;
1471 }
1472 if (before_i == before_tree.children.len) {
1473 try A.dir(e, w, a_t, p, after_i);
1474 break;
1475 }
1476
1477 const before = before_children[before_i];
1478 const after = after_children[after_i];
1479
1480 switch (before.order(after)) {
1481 .eq => {
1482 if (std.mem.eql(u8, before.id.erase(), after.id.erase())) {
1483 if (!std.mem.eql(u8, &before.mode.intbytes(), &after.mode.intbytes())) {
1484 try S.item(
1485 e,
1486 w,
1487 before.mode,
1488 after.mode,
1489 before.id.erase(),
1490 after.id.erase(),
1491 .M,
1492 p,
1493 after.name,
1494 );
1495 }
1496 before_i += 1;
1497 after_i += 1;
1498 continue;
1499 }
1500 if (before.mode.type != after.mode.type) {
1501 try S.item(
1502 e,
1503 w,
1504 before.mode,
1505 after.mode,
1506 before.id.erase(),
1507 after.id.erase(),
1508 .T,
1509 p,
1510 after.name,
1511 );
1512 before_i += 1;
1513 after_i += 1;
1514 continue;
1515 }
1516 if (before.mode.type == .directory) {
1517 try dir(
1518 e,
1519 w,
1520 before.id.tree.id,
1521 after.id.tree.id,
1522 &.{ .prev = p, .data = after.name },
1523 );
1524 before_i += 1;
1525 after_i += 1;
1526 continue;
1527 }
1528 try S.item(
1529 e,
1530 w,
1531 before.mode,
1532 after.mode,
1533 before.id.erase(),
1534 after.id.erase(),
1535 .M,
1536 p,
1537 after.name,
1538 );
1539 before_i += 1;
1540 after_i += 1;
1541 continue;
1542 },
1543 .lt => {
1544 const after_item = after_tree.get(before.name) orelse {
1545 try D.either(e, w, p, before);
1546 before_i += 1;
1547 continue;
1548 };
1549 const before_item = before_tree.get(after.name) orelse {
1550 try A.either(e, w, p, after);
1551 after_i += 1;
1552 continue;
1553 };
1554 if (std.mem.eql(u8, &after_item.id_bytes, &before.id_bytes)) {
1555 before_i += 1;
1556 continue;
1557 }
1558 if (std.mem.eql(u8, &before_item.id_bytes, &after.id_bytes)) {
1559 after_i += 1;
1560 continue;
1561 }
1562 {
1563 try D.either(e, w, p, before);
1564 before_i += 1;
1565 try A.either(e, w, p, after);
1566 after_i += 1;
1567 continue;
1568 }
1569 comptime unreachable;
1570 },
1571 .gt => {
1572 const before_item = before_tree.get(after.name) orelse {
1573 try A.either(e, w, p, after);
1574 after_i += 1;
1575 continue;
1576 };
1577 const after_item = after_tree.get(before.name) orelse {
1578 try D.either(e, w, p, before);
1579 before_i += 1;
1580 continue;
1581 };
1582 if (std.mem.eql(u8, &before_item.id_bytes, &after.id_bytes)) {
1583 after_i += 1;
1584 continue;
1585 }
1586 if (std.mem.eql(u8, &after_item.id_bytes, &before.id_bytes)) {
1587 before_i += 1;
1588 continue;
1589 }
1590 {
1591 try A.either(e, w, p, after);
1592 before_i += 1;
1593 try D.either(e, w, p, before);
1594 after_i += 1;
1595 continue;
1596 }
1597 comptime unreachable;
1598 },
1599 }
1600 comptime unreachable;
1601 }
1602 }
1603 };
1604 if (commitid_from == null) {
1605 const commit = try r.getCommitA(commitid_to.id, .cache);
1606 try A.dir(r, writable, commit.tree.id, null, 0);
1607 return;
1608 }
1609 const before_commit = try r.getCommitA(commitid_from.?.id, .cache);
1610 const after_commit = try r.getCommitA(commitid_to.id, .cache);
1611 try M.dir(r, writable, before_commit.tree.id, after_commit.tree.id, null);
1612 }
1613
1614 pub fn writeTreeDiffOnlyRaw(r: *Repository, writable: anytype, commitid: CommitId, parentid: ?CommitId) !void {
1615 const S = struct {
1616 fn item(e: *Repository, w: anytype, b_mode: Tree.Object.Mode, a_mode: Tree.Object.Mode, b_id: Id, a_id: Id, action: TreeDiff.Action, p: ?*const PathListNode, name: []const u8) !void {
1617 _ = e;
1618 if (p == null) {
1619 try w.writevAll(&.{ ":", &b_mode.intbytes(), " ", &a_mode.intbytes(), " ", b_id, " ", a_id, " ", @tagName(action), "\t", name, "\n" });
1620 return;
1621 }
1622 try w.writevAll(&.{ ":", &b_mode.intbytes(), " ", &a_mode.intbytes(), " ", b_id, " ", a_id, " ", @tagName(action), "\t" });
1623 try p.?.nprint(w);
1624 try w.writevAll(&.{ "/", name, "\n" });
1625 }
1626 };
1627 return diffFileIterator(r, writable, parentid, commitid, S);
1628 }
1629
1630 pub fn writeTreeDiffOnlySummary(r: *Repository, writable: anytype, commitid: CommitId, parentid: ?CommitId) !void {
1631 const S = struct {
1632 fn item(e: *Repository, w: anytype, b_mode: Tree.Object.Mode, a_mode: Tree.Object.Mode, b_id: Id, a_id: Id, action: TreeDiff.Action, p: ?*const PathListNode, name: []const u8) !void {
1633 _ = e;
1634 _ = b_id;
1635 _ = a_id;
1636 switch (action) {
1637 .A => {
1638 if (p == null) {
1639 try w.writevAll(&.{ " create mode ", &a_mode.intbytes(), " ", name, "\n" });
1640 return;
1641 }
1642 try w.writevAll(&.{ " create mode ", &a_mode.intbytes(), " " });
1643 try p.?.nprint(w);
1644 try w.writevAll(&.{ "/", name, "\n" });
1645 },
1646 .D => {
1647 if (p == null) {
1648 try w.writevAll(&.{ " delete mode ", &b_mode.intbytes(), " ", name, "\n" });
1649 return;
1650 }
1651 try w.writevAll(&.{ " delete mode ", &b_mode.intbytes(), " " });
1652 try p.?.nprint(w);
1653 try w.writevAll(&.{ "/", name, "\n" });
1654 },
1655 .M => {
1656 if (!std.mem.eql(u8, &b_mode.intbytes(), &a_mode.intbytes())) {
1657 if (p == null) {
1658 try w.writevAll(&.{ " mode change ", &b_mode.intbytes(), " => ", &a_mode.intbytes(), " ", name, "\n" });
1659 return;
1660 }
1661 try w.writevAll(&.{ " mode change ", &b_mode.intbytes(), " => ", &a_mode.intbytes(), " " });
1662 try p.?.nprint(w);
1663 try w.writevAll(&.{ "/", name, "\n" });
1664 }
1665 },
1666 .T => {
1667 if (p == null) {
1668 try w.writevAll(&.{ " mode change ", &b_mode.intbytes(), " => ", &a_mode.intbytes(), " ", name, "\n" });
1669 return;
1670 }
1671 try w.writevAll(&.{ " mode change ", &b_mode.intbytes(), " => ", &a_mode.intbytes(), " " });
1672 try p.?.nprint(w);
1673 try w.writevAll(&.{ "/", name, "\n" });
1674 },
1675 else => {},
1676 }
1677 }
1678 };
1679 return diffFileIterator(r, writable, parentid, commitid, S);
1680 }
1681
1682 pub fn revListAll(r: *Repository, alloc: std.mem.Allocator, from: CommitId, sub_path: string) ![]const u8 {
1683 const t = tracer.trace(@src(), " {s} -- {s}", .{ from.id, sub_path });
1684 defer t.end();
1685
1686 var list: std.ArrayList(u8) = .empty;
1687 errdefer list.deinit(alloc);
1688
1689 const base = try r.getCommitA(from.id, .no_cache);
1690 const base_tree = try r.getTreeA(base.tree.id, .no_cache);
1691 const base_obj, const base_id_parent = (try idFor(r, base_tree, sub_path)).?;
1692
1693 var prev_prev_commit: ?*Commit = null;
1694 var prev_commit = base;
1695 var prev_tree: ?*Tree = base_id_parent;
1696 var prev_obj = try r.gpa.create(Tree.Object);
1697 prev_obj.* = base_obj.*;
1698 defer r.gpa.destroy(prev_obj);
1699
1700 while (true) {
1701 if (prev_commit.parents.len == 0) {
1702 if (prev_tree != null) {
1703 try list.appendSlice(alloc, (if (prev_prev_commit) |p| p.parents[0].id else from.id) ++ "\n");
1704 }
1705 break;
1706 }
1707 const next_commit = try r.getCommitA(prev_commit.parents[0].id, .no_cache);
1708 const next_commit_tree = try r.getTreeA(next_commit.tree.id, .no_cache);
1709 const next_obj, const next_tree = try idFor(r, next_commit_tree, sub_path) orelse {
1710 if (prev_tree == null) {
1711 if (prev_prev_commit) |p| p.destroy(r);
1712 prev_prev_commit = prev_commit;
1713 prev_commit = next_commit;
1714 prev_tree = null;
1715 continue;
1716 }
1717 try list.appendSlice(alloc, (if (prev_prev_commit) |p| p.parents[0].id else from.id) ++ "\n");
1718 // break; // we don't pass --remove-empty
1719 if (prev_prev_commit) |p| p.destroy(r);
1720 prev_prev_commit = prev_commit;
1721 prev_commit = next_commit;
1722 prev_tree = null;
1723 continue;
1724 };
1725 if (prev_tree != null and std.mem.eql(u8, &next_obj.id_bytes, &prev_obj.id_bytes) and next_obj.mode.eql(prev_obj.mode)) {
1726 if (prev_prev_commit) |p| p.destroy(r);
1727 prev_prev_commit = prev_commit;
1728 prev_commit = next_commit;
1729 prev_tree = next_tree;
1730 continue;
1731 }
1732 try list.appendSlice(alloc, (if (prev_prev_commit) |p| p.parents[0].id else from.id) ++ "\n");
1733 if (prev_prev_commit) |p| p.destroy(r);
1734 prev_prev_commit = prev_commit;
1735 prev_commit = next_commit;
1736 prev_tree = next_tree;
1737 prev_obj.* = next_obj.*;
1738 continue;
1739 }
1740
1741 return list.toOwnedSlice(alloc);
1742 }
1743};
1744
1745const z = @cImport({
1746 @cInclude("zlib.h");
1747});
1748
1749fn inflate_decompress(in: []const u8, out: *nio.AllocatingWriter) !void {
1750 // {
1751 // var reader: std.Io.Reader = .fixed(in);
1752 // var buf: [std.compress.flate.max_window_len]u8 = @splat(0);
1753 // var d: std.compress.flate.Decompress = .init(&reader, .zlib, &buf);
1754 // var w = out.anyWritable().toStd(&.{});
1755 // _ = d.reader.streamRemaining(&w.sw) catch |err| switch (err) {
1756 // error.ReadFailed => return d.err.?,
1757 // error.WriteFailed => return error.OutOfMemory,
1758 // };
1759 // return;
1760 // }
1761 var strm: z.z_stream = std.mem.zeroes(z.z_stream);
1762 {
1763 const ret: ZlibCode = @enumFromInt(z.inflateInit(&strm));
1764 if (ret == .Z_MEM_ERROR) return error.OutOfMemory;
1765 std.debug.assert(ret != .Z_VERSION_ERROR);
1766 std.debug.assert(ret != .Z_STREAM_ERROR);
1767 }
1768 defer {
1769 const ret: ZlibCode = @enumFromInt(z.inflateEnd(&strm));
1770 std.debug.assert(ret != .Z_STREAM_ERROR);
1771 std.debug.assert(ret == .Z_OK);
1772 }
1773 // std.log.debug("inflate_decompress: -> {*} {d}", .{ in.ptr, in.len });
1774 strm.next_in = @constCast(in.ptr);
1775 strm.avail_in = @truncate(in.len);
1776
1777 while (true) {
1778 var buf: [16384]u8 = @splat(0);
1779 strm.next_out = &buf;
1780 strm.avail_out = buf.len;
1781 // std.log.debug("inflate_decompress: -> {*} {*} {d} {d}", .{ strm.next_in, strm.next_out, strm.avail_in, strm.avail_out });
1782 const ret: ZlibCode = @enumFromInt(z.inflate(&strm, z.Z_SYNC_FLUSH));
1783 // std.log.debug("inflate_decompress: <- {*} {*} {d} {d} {s}", .{ strm.next_in, strm.next_out, strm.avail_in, strm.avail_out, @tagName(ret) });
1784 std.debug.assert(ret != .Z_STREAM_ERROR);
1785 std.debug.assert(ret != .Z_BUF_ERROR);
1786 if (ret == .Z_MEM_ERROR) return error.OutOfMemory;
1787 if (ret == .Z_DATA_ERROR) return error.Z_DATA_ERROR;
1788 if (ret == .Z_NEED_DICT) return error.Z_NEED_DICT;
1789 // Z_ERRNO
1790 // Z_VERSION_ERROR
1791 std.debug.assert(ret == .Z_OK or ret == .Z_STREAM_END);
1792 try out.writeAll(buf[0 .. buf.len - strm.avail_out]);
1793 if (ret == .Z_STREAM_END) break;
1794 }
1795}
1796
1797const ZlibCode = enum(c_int) {
1798 Z_OK = 0,
1799 Z_STREAM_END = 1,
1800 Z_NEED_DICT = 2,
1801 Z_ERRNO = -1,
1802 Z_STREAM_ERROR = -2,
1803 Z_DATA_ERROR = -3,
1804 Z_MEM_ERROR = -4,
1805 Z_BUF_ERROR = -5,
1806 Z_VERSION_ERROR = -6,
1807};
1808
1809fn traverseTo(r: *Repository, treestart_id: TreeId, dir_path: []const u8) !struct { ?TreeId, ?*Tree } {
1810 var id = treestart_id;
1811 if (dir_path.len == 0) return .{ id, null };
1812 var iter = std.mem.splitScalar(u8, dir_path, '/');
1813 var prev: ?*Tree = null;
1814 while (iter.next()) |segment| {
1815 const p = prev;
1816 const tree = try r.getTreeA(id.id, .no_cache);
1817 defer prev = tree;
1818 if (p) |_| p.?.destroy(r);
1819 const o = tree.get(segment) orelse return .{ null, tree };
1820 if (o.id != .tree) return .{ null, tree };
1821 id = o.id.tree;
1822 }
1823 return .{ id, prev };
1824}
1825
1826pub const Tree = struct {
1827 raw: []const u8,
1828 children: []const Object,
1829
1830 pub fn destroy(t: *Tree, r: *Repository) void {
1831 r.gpa.free(t.children);
1832 r.gpa.free(t.raw);
1833 r.gpa.destroy(t);
1834 }
1835
1836 pub fn get(self: *Tree, name: string) ?*const Object {
1837 // modified std.sort.binarySearch
1838 const i = blk: {
1839 var low: usize = 0;
1840 var high: usize = self.children.len;
1841 while (low < high) {
1842 const mid = low + (high - low) / 2;
1843 switch (Object.search(name, self.children[mid])) {
1844 .eq => break :blk mid,
1845 .gt => low = mid + 1,
1846 .lt => high = mid,
1847 }
1848 }
1849 for (self.children[low..], 0..) |item, i| {
1850 if (std.mem.startsWith(u8, item.name, name)) {
1851 if (item.name[name.len..].len == 0) {
1852 break :blk low + i;
1853 }
1854 if (std.math.order(item.name[name.len..][0], '/') == .gt) {
1855 return null;
1856 }
1857 continue;
1858 }
1859 break;
1860 }
1861 return null;
1862 };
1863 return &self.children[i];
1864 }
1865
1866 pub fn getBlob(self: *Tree, name: string, hint: Object.Type) ?*const Object {
1867 const o = self.get(name, hint) orelse return null;
1868 if (o.id != .blob) return null;
1869 return o;
1870 }
1871
1872 pub fn find(self: *Tree, name: string) ?Object {
1873 for (self.children, 0..) |item, i| {
1874 if (std.ascii.eqlIgnoreCase(item.name, name)) {
1875 return self.children[i];
1876 }
1877 }
1878 return null;
1879 }
1880
1881 pub fn findBlob(self: *Tree, name: string) ?Object {
1882 const o = self.find(name) orelse return null;
1883 if (o.id != .blob) return null;
1884 return o;
1885 }
1886
1887 pub const Object = struct {
1888 mode: Mode,
1889 id_bytes: [40]u8,
1890 id: AnyId,
1891 name: [:0]const u8,
1892
1893 fn search(a: []const u8, b: Object) std.math.Order {
1894 if (a.ptr != b.name.ptr) {
1895 const n = @min(a.len, b.name.len);
1896 for (a[0..n], b.name[0..n]) |lhs_elem, rhs_elem| {
1897 switch (std.math.order(lhs_elem, rhs_elem)) {
1898 .eq => continue,
1899 .lt => return .lt,
1900 .gt => return .gt,
1901 }
1902 }
1903 }
1904 return switch (std.math.order(a.len, b.name.len)) {
1905 .lt => .lt,
1906 .gt => if (b.mode.type == .directory) std.math.order(a[b.name.len], '/') else .gt,
1907 .eq => .eq,
1908 };
1909 }
1910
1911 pub fn order(lhs: Object, rhs: Object) std.math.Order {
1912 return order_bare(
1913 lhs.name,
1914 rhs.name,
1915 lhs.mode.type == .directory,
1916 rhs.mode.type == .directory,
1917 );
1918 }
1919
1920 pub fn order_bare(l: []const u8, r: []const u8, l_is_dir: bool, r_is_dir: bool) std.math.Order {
1921 if (l.ptr != r.ptr) {
1922 const n = @min(l.len, r.len);
1923 for (l[0..n], r[0..n]) |lhs_elem, rhs_elem| {
1924 switch (std.math.order(lhs_elem, rhs_elem)) {
1925 .eq => continue,
1926 .lt => return .lt,
1927 .gt => return .gt,
1928 }
1929 }
1930 }
1931 return switch (std.math.order(l.len, r.len)) {
1932 .lt => if (l_is_dir) std.math.order('/', r[l.len]) else .lt,
1933 .gt => if (r_is_dir) std.math.order(l[r.len], '/') else .gt,
1934 .eq => if (l_is_dir and r_is_dir) .eq else if (l_is_dir) .gt else if (r_is_dir) .lt else .eq,
1935 };
1936 }
1937
1938 pub const Mode = struct {
1939 type: Type,
1940 perm_user: Perm,
1941 perm_group: Perm,
1942 perm_other: Perm,
1943
1944 pub const none = std.mem.zeroes(Mode);
1945
1946 pub fn format(self: Mode, comptime fmt: string, options: std.fmt.FormatOptions, writer: anytype) !void {
1947 _ = fmt;
1948 _ = options;
1949 try writer.print("{}", .{self.type});
1950 try writer.print("{}", .{self.perm_user});
1951 try writer.print("{}", .{self.perm_group});
1952 try writer.print("{}", .{self.perm_other});
1953 }
1954
1955 pub fn nprint(self: Mode, writer: anytype) !void {
1956 try self.type.nprint(writer);
1957 try self.perm_user.nprint(writer);
1958 try self.perm_group.nprint(writer);
1959 try self.perm_other.nprint(writer);
1960 }
1961
1962 pub fn eql(self: Mode, other: Mode) bool {
1963 if (self.type != other.type) return false;
1964 if (self.perm_user != other.perm_user) return false;
1965 if (self.perm_group != other.perm_group) return false;
1966 if (self.perm_other != other.perm_other) return false;
1967 return true;
1968 }
1969
1970 pub fn intbytes(self: Mode) [6]u8 {
1971 var b: [6]u8 = @splat('-');
1972 @memcpy(b[0..3], switch (self.type) {
1973 .file => "100",
1974 .directory => "040",
1975 .submodule => "160",
1976 .symlink => "120",
1977 .none => "000",
1978 });
1979 b[3] = @as(u8, @as(u3, @bitCast(self.perm_user))) + '0';
1980 b[4] = @as(u8, @as(u3, @bitCast(self.perm_group))) + '0';
1981 b[5] = @as(u8, @as(u3, @bitCast(self.perm_other))) + '0';
1982 return b;
1983 }
1984
1985 pub fn octal(self: Mode) u9 {
1986 const O = packed struct {
1987 other: Perm,
1988 group: Perm,
1989 user: Perm,
1990 };
1991 const o: O = .{
1992 .other = self.perm_other,
1993 .group = self.perm_group,
1994 .user = self.perm_user,
1995 };
1996 return @bitCast(o);
1997 }
1998 };
1999
2000 pub const Type = enum(u8) {
2001 file = 100,
2002 directory = 40,
2003 submodule = 160,
2004 symlink = 120,
2005 none = 0,
2006
2007 pub fn format(self: Type, comptime fmt: string, options: std.fmt.FormatOptions, writer: anytype) !void {
2008 _ = fmt;
2009 _ = options;
2010 try writer.writeByte(switch (self) {
2011 .file => '-',
2012 .directory => 'd',
2013 .submodule => 'm',
2014 .symlink => '-',
2015 .none => '-',
2016 });
2017 }
2018
2019 pub fn nprint(self: Type, writer: anytype) !void {
2020 try writer.writeAll(&.{switch (self) {
2021 .file => '-',
2022 .directory => 'd',
2023 .submodule => 'm',
2024 .symlink => '-',
2025 .none => '-',
2026 }});
2027 }
2028 };
2029
2030 pub const Perm = packed struct(u3) {
2031 execute: bool,
2032 write: bool,
2033 read: bool,
2034
2035 pub fn format(self: Perm, comptime fmt: string, options: std.fmt.FormatOptions, writer: anytype) !void {
2036 _ = fmt;
2037 _ = options;
2038 try writer.writeByte(if (self.read) 'r' else '-');
2039 try writer.writeByte(if (self.write) 'w' else '-');
2040 try writer.writeByte(if (self.execute) 'x' else '-');
2041 }
2042
2043 pub fn nprint(self: Perm, writer: anytype) !void {
2044 try writer.writeAll(&.{
2045 if (self.read) 'r' else '-',
2046 if (self.write) 'w' else '-',
2047 if (self.execute) 'x' else '-',
2048 });
2049 }
2050 };
2051 };
2052
2053 pub fn walk(self: *Tree, r: *Repository) !Walker {
2054 var stack: std.ArrayListUnmanaged(Walker.StackItem) = .empty;
2055
2056 try stack.append(r.gpa, .{
2057 .tree = self,
2058 .idx = 0,
2059 .dirname_len = 0,
2060 });
2061 return .{
2062 .repo = r,
2063 .stack = stack,
2064 .name_buffer = .empty,
2065 };
2066 }
2067
2068 pub const Walker = struct {
2069 repo: *Repository,
2070 stack: std.ArrayListUnmanaged(StackItem),
2071 name_buffer: std.ArrayListUnmanaged(u8),
2072
2073 pub const Entry = struct {
2074 obj: Object,
2075 path: [:0]const u8,
2076 };
2077
2078 const StackItem = struct {
2079 tree: *Tree,
2080 idx: usize,
2081 dirname_len: usize,
2082 };
2083
2084 pub fn next(self: *Walker) !?Walker.Entry {
2085 const gpa = self.repo.gpa;
2086 while (self.stack.items.len != 0) {
2087 var top = &self.stack.items[self.stack.items.len - 1];
2088 var containing = top;
2089 var dirname_len = top.dirname_len;
2090 if (top.idx < top.tree.children.len) {
2091 const base = top.tree.children[top.idx];
2092 top.idx += 1;
2093 self.name_buffer.shrinkRetainingCapacity(dirname_len);
2094 if (self.name_buffer.items.len != 0) {
2095 try self.name_buffer.append(gpa, '/');
2096 dirname_len += 1;
2097 }
2098 try self.name_buffer.ensureUnusedCapacity(gpa, base.name.len + 1);
2099 self.name_buffer.appendSliceAssumeCapacity(base.name);
2100 self.name_buffer.appendAssumeCapacity(0);
2101 if (base.id == .tree) {
2102 const new_tree = try self.repo.getTreeA(base.id.tree.id, .cache);
2103 {
2104 // errdefer new_dir.close();
2105 try self.stack.append(gpa, .{
2106 .tree = new_tree,
2107 .idx = 0,
2108 .dirname_len = self.name_buffer.items.len - 1,
2109 });
2110 top = &self.stack.items[self.stack.items.len - 1];
2111 containing = &self.stack.items[self.stack.items.len - 2];
2112 }
2113 }
2114 return .{
2115 .obj = base,
2116 .path = self.name_buffer.items[0 .. self.name_buffer.items.len - 1 :0],
2117 };
2118 } else {
2119 var item = self.stack.pop().?;
2120 _ = &item;
2121 // if (self.stack.items.len != 0) item.iter.dir.close();
2122 }
2123 }
2124 return null;
2125 }
2126
2127 pub fn deinit(self: *Walker) void {
2128 const gpa = self.repo.gpa;
2129 // for (self.stack.items) |*item| item.iter.dir.close();
2130 self.stack.deinit(gpa);
2131 self.name_buffer.deinit(gpa);
2132 }
2133 };
2134};
2135
2136pub const Commit = struct {
2137 raw: []const u8,
2138 tree: TreeId,
2139 parents: []const CommitId,
2140 author: UserAndAt,
2141 committer: UserAndAt,
2142 gpgsig: []const u8,
2143 message: string,
2144
2145 pub fn destroy(t: *Commit, r: *Repository) void {
2146 r.gpa.free(t.parents);
2147 r.gpa.free(t.raw);
2148 r.gpa.destroy(t);
2149 }
2150
2151 pub fn signature(t: *const Commit, allocator: std.mem.Allocator) !Signature {
2152 return Signature.fromHeader(allocator, t.gpgsig, t.raw) catch |err| switch (err) {
2153 error.OutOfMemory => |e| e,
2154 error.EndOfStream, error.InvalidCharacter => .unrecognized,
2155 };
2156 }
2157};
2158
2159pub const UserAndAt = struct {
2160 name: string,
2161 email: string,
2162 at: time.DateTime,
2163};
2164
2165pub const Tag = struct {
2166 raw: []const u8,
2167 object: Id,
2168 type: RefType,
2169 tagger: ?UserAndAt,
2170 message: string,
2171
2172 pub fn destroy(t: *Tag, r: *Repository) void {
2173 r.gpa.free(t.raw);
2174 r.gpa.destroy(t);
2175 }
2176};
2177
2178pub const Ref = struct {
2179 label: string,
2180 oid: Id,
2181 commit: ?Id,
2182};
2183
2184pub const Signature = union(enum) {
2185 none,
2186 unrecognized,
2187 pgp: Pgp,
2188 ssh: Ssh,
2189
2190 pub const Pgp = struct {
2191 packet_length: u32,
2192 version: u8,
2193 type_id: Pgp.Type,
2194 material: Material,
2195 hash_algorithm: Pgp.HashAlgorithm,
2196 keyid: [16]u8,
2197 creation_time: time.DateTime,
2198 signed_hash_value_prefix: [4]u8,
2199 pubkey: ?PubKey,
2200 valid: ?bool,
2201
2202 /// https://datatracker.ietf.org/doc/html/rfc9580#name-signature-types
2203 pub const Type = enum(u8) {
2204 binary = 0x00,
2205 text = 0x01,
2206 standalone = 0x02,
2207 generic_certification = 0x10,
2208 persona_certification = 0x11,
2209 casual_certification = 0x12,
2210 positive_certification = 0x13,
2211 subkey_binding = 0x18,
2212 primary_key_binding = 0x19,
2213 direct_key = 0x1F,
2214 key_revocation = 0x20,
2215 subkey_revocation = 0x28,
2216 certification_revocation = 0x30,
2217 timestamp = 0x40,
2218 third_party_confirmation = 0x50,
2219 reserved = 0xFF,
2220 _,
2221
2222 pub fn stringifyJson(e: Type, writer: anytype, options: std.json.Stringify.Options, json: type) !void {
2223 return switch (e) {
2224 _ => json.stringify(writer, @intFromEnum(e), options),
2225 else => json.stringify(writer, @tagName(e), options),
2226 };
2227 }
2228 };
2229
2230 /// https://datatracker.ietf.org/doc/html/rfc9580#name-public-key-algorithms
2231 pub const PublicKeyAlgorithm = enum(u8) {
2232 reserved = 0,
2233 rsa_encrypt_or_sign = 1,
2234 rsa_encrypt = 2,
2235 rsa_sign = 3,
2236 elgamal_encrypt = 16,
2237 dsa = 17,
2238 ecdh = 18,
2239 ecdsa = 19,
2240 x25519 = 25,
2241 x448 = 26,
2242 ed25519 = 27,
2243 ed448 = 28,
2244 _,
2245
2246 pub fn stringifyJson(e: PublicKeyAlgorithm, writer: anytype, options: std.json.Stringify.Options, json: type) !void {
2247 return switch (e) {
2248 _ => json.stringify(writer, @intFromEnum(e), options),
2249 else => json.stringify(writer, @tagName(e), options),
2250 };
2251 }
2252 };
2253
2254 /// https://datatracker.ietf.org/doc/html/rfc9580#name-hash-algorithms
2255 pub const HashAlgorithm = enum(u8) {
2256 reserved = 0,
2257 md5 = 1,
2258 sha1 = 2,
2259 ripemd160 = 3,
2260 sha2_256 = 8,
2261 sha2_384 = 9,
2262 sha2_512 = 10,
2263 sha2_224 = 11,
2264 sha3_256 = 12,
2265 sha3_512 = 14,
2266 _,
2267
2268 pub fn stringifyJson(e: HashAlgorithm, writer: anytype, options: std.json.Stringify.Options, json: type) !void {
2269 return switch (e) {
2270 _ => json.stringify(writer, @intFromEnum(e), options),
2271 else => json.stringify(writer, @tagName(e), options),
2272 };
2273 }
2274 };
2275
2276 pub const SubType = enum(u8) {
2277 signature_creation_time = 2,
2278 signature_expiration_time = 3,
2279 exportable_certification = 4,
2280 trust_signature = 5,
2281 revocable_expression = 6,
2282 revocable = 7,
2283 key_expiration_time = 9,
2284 preferred_v1_seipd_ciphers = 11,
2285 issuer_key_id = 16,
2286 notation_data = 20,
2287 preferred_hash_algorithms = 21,
2288 preferred_compression_algorithms = 22,
2289 key_server_preferences = 23,
2290 preferred_key_server = 24,
2291 primary_user_id = 25,
2292 policy_uri = 26,
2293 key_flags = 27,
2294 signer_user_id = 28,
2295 revocation_reason = 29,
2296 features = 30,
2297 signature_target = 31,
2298 embedded_signature = 32,
2299 issuer_fingerprint = 33,
2300 intended_recipient_fingerprint = 35,
2301 preferred_aead_ciphersuites = 39,
2302 _,
2303 };
2304
2305 pub const Material = union(PublicKeyAlgorithm) {
2306 reserved: void,
2307 rsa_encrypt_or_sign: []const u8,
2308 rsa_encrypt: void,
2309 rsa_sign: void,
2310 elgamal_encrypt: void,
2311 dsa: void,
2312 ecdh: void,
2313 ecdsa: void,
2314 x25519: void,
2315 x448: void,
2316 ed25519: void,
2317 ed448: void,
2318 };
2319
2320 const PubKey = struct {
2321 version: u8,
2322 creation_time: time.DateTime,
2323 days_valid: u16,
2324 material: PubKey.Material,
2325
2326 const Material = union(Pgp.PublicKeyAlgorithm) {
2327 reserved: void,
2328 rsa_encrypt_or_sign: RSA,
2329 rsa_encrypt: void,
2330 rsa_sign: void,
2331 elgamal_encrypt: void,
2332 dsa: void,
2333 ecdh: void,
2334 ecdsa: void,
2335 x25519: void,
2336 x448: void,
2337 ed25519: void,
2338 ed448: void,
2339
2340 const RSA = struct {
2341 n: []const u8,
2342 e: []const u8,
2343 };
2344 };
2345 };
2346 };
2347
2348 pub const Ssh = struct {
2349 publickey: []const u8,
2350 hash_algorithm: []const u8,
2351 signature: []const u8,
2352 valid: ?bool,
2353 };
2354
2355 pub fn fromHeader(allocator: std.mem.Allocator, pem_sig: []const u8, content_plus_gpgsig: []const u8) !Signature {
2356 if (pem_sig.len == 0) {
2357 return .none;
2358 }
2359
2360 var message = extras.ManyArrayList(u8).init(allocator);
2361 defer message.deinit();
2362 try message.appendSlice(try message.add(), content_plus_gpgsig);
2363 message.lengths.items.len = 0;
2364 {
2365 var iter = std.mem.splitScalar(u8, content_plus_gpgsig, '\n');
2366 while (iter.next()) |line| {
2367 try message.lengths.append(allocator, line.len + 1);
2368 }
2369 var skipping = false;
2370 var i: usize = 0;
2371 while (i < message.lengths.items.len) : (i += 1) {
2372 if (!skipping and std.mem.startsWith(u8, message.items(i), "gpgsig ")) {
2373 skipping = true;
2374 message.remove(i);
2375 i -= 1;
2376 continue;
2377 }
2378 if (!skipping) {
2379 continue;
2380 }
2381 if (skipping and std.mem.startsWith(u8, message.items(i), " ")) {
2382 message.remove(i);
2383 i -= 1;
2384 continue;
2385 }
2386 break;
2387 }
2388 }
2389
2390 if (std.mem.startsWith(u8, pem_sig, "-----BEGIN PGP SIGNATURE-----\n \n ") and (std.mem.endsWith(u8, pem_sig, "\n -----END PGP SIGNATURE-----\n ") or std.mem.endsWith(u8, pem_sig, "\n -----END PGP SIGNATURE-----"))) {
2391 const pembody = pem_sig[33..std.mem.indexOf(u8, pem_sig, "\n -----END").?];
2392 const sigcontent = pembody[0..std.mem.lastIndexOf(u8, pembody, "\n ").?];
2393 var fixed: nio.FixedBufferStream([]const u8) = .init(sigcontent);
2394 var skip = nio.SkipReader(void).from(&fixed, "\n ");
2395 var b64r = nio.Base64Reader(void).from(&skip);
2396 return fromReader(.pgp, allocator, &b64r, message.list.items);
2397 }
2398
2399 if (std.mem.startsWith(u8, pem_sig, "-----BEGIN SSH SIGNATURE-----\n") and std.mem.endsWith(u8, pem_sig, "\n -----END SSH SIGNATURE-----")) {
2400 const sigcontent = pem_sig[30 .. pem_sig.len - 29];
2401 var fixed: nio.FixedBufferStream([]const u8) = .init(sigcontent);
2402 var skip = nio.SkipReader(void).from(&fixed, "\n ");
2403 var b64r = nio.Base64Reader(void).from(&skip);
2404 return fromReader(.ssh, allocator, &b64r, message.list.items);
2405 }
2406
2407 return .unrecognized;
2408 }
2409
2410 // https://www.ietf.org/archive/id/draft-josefsson-sshsig-format-03.html
2411 // https://datatracker.ietf.org/doc/html/rfc4251#section-5
2412 // https://pkg.go.dev/golang.org/x/crypto/ssh#pkg-constants
2413 // https://datatracker.ietf.org/doc/html/rfc9580#section-4
2414 // https://datatracker.ietf.org/doc/html/rfc9580#signature-packet
2415 // https://datatracker.ietf.org/doc/html/rfc5656
2416 // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
2417 // https://datatracker.ietf.org/doc/html/rfc8709
2418 pub fn fromReader(kind: NonVoidUnionFieldEnum(Signature), allocator: std.mem.Allocator, b64r: anytype, message: []const u8) !Signature {
2419 if (kind == .pgp) {
2420 var signed_data: nio.AllocatingWriter = .init(allocator);
2421 defer signed_data.deinit();
2422 try signed_data.writeAll(message);
2423
2424 const packet_type: packed struct { id: u6, format: u1, reserved: u1 } = @bitCast(try b64r.readByte());
2425 if (packet_type.reserved != 1) return .unrecognized;
2426 if (packet_type.format != 1) return .unrecognized; // legacy non-OpenPGP format
2427 if (packet_type.id != 2) return .unrecognized; // not a signature
2428
2429 _, const packet_len = pgp_read_packet_len(b64r) catch return .unrecognized;
2430
2431 const sig_version = try b64r.readByte();
2432 try signed_data.writeAll(&.{sig_version});
2433 var type_id: Pgp.Type = .reserved;
2434 var pk_algo: Pgp.PublicKeyAlgorithm = .reserved;
2435 var hash_algo: Pgp.HashAlgorithm = .reserved;
2436 var creation_time: [4]u8 = @splat(0);
2437 var keyid: [8]u8 = @splat(0);
2438 var signed_hash_value_prefix: [2]u8 = @splat(0);
2439
2440 if (sig_version == 3) blk: {
2441 const hashed_len = try b64r.readByte();
2442 if (hashed_len != 5) break :blk;
2443 type_id = @enumFromInt(try b64r.readByte());
2444 creation_time = try b64r.readArray(4);
2445 keyid = try b64r.readArray(8);
2446 pk_algo = @enumFromInt(try b64r.readByte());
2447 hash_algo = @enumFromInt(try b64r.readByte());
2448 signed_hash_value_prefix = try b64r.readArray(2);
2449 }
2450 if (sig_version == 4) {
2451 type_id = @enumFromInt(try b64r.readByte());
2452 pk_algo = @enumFromInt(try b64r.readByte());
2453 hash_algo = @enumFromInt(try b64r.readByte());
2454 try signed_data.writeAll(&.{ @intFromEnum(type_id), @intFromEnum(pk_algo), @intFromEnum(hash_algo) });
2455 const subpacket_len_hashed = try b64r.readInt(u16, .big);
2456 try signed_data.writeInt(u16, subpacket_len_hashed, .big);
2457 const subpacket_bytes = try b64r.readAlloc(allocator, subpacket_len_hashed);
2458 try signed_data.writeAll(subpacket_bytes);
2459 try signed_data.writeAll(&.{0x04});
2460 try signed_data.writeAll(&.{0xFF});
2461 try signed_data.writeInt(u32, @truncate(signed_data.items.len - 2 - message.len), .big);
2462 {
2463 var lr = nio.FixedBufferStream([]u8).init(subpacket_bytes);
2464 while (lr.rest().len > 0) {
2465 const subpacket_len = try pgp_read_subpacket_len(&lr);
2466 const subpacket_typeid: Pgp.SubType = @enumFromInt(try lr.readByte() & 127);
2467 if (subpacket_typeid == .signature_creation_time and subpacket_len == 1 + 4) {
2468 creation_time = try lr.readArray(4);
2469 continue;
2470 }
2471 if (subpacket_typeid == .issuer_key_id and subpacket_len == 1 + 8) {
2472 keyid = try lr.readArray(8);
2473 continue;
2474 }
2475 try lr.skipBytes(subpacket_len - 1, .{});
2476 }
2477 }
2478 const subpacket_len_unhashed = try b64r.readInt(u16, .big);
2479 {
2480 try b64r.skipBytes(subpacket_len_unhashed, .{}); // TODO read this
2481 }
2482 signed_hash_value_prefix = try b64r.readArray(2);
2483 }
2484 if (sig_version == 6) {
2485 type_id = @enumFromInt(try b64r.readByte());
2486 pk_algo = @enumFromInt(try b64r.readByte());
2487 hash_algo = @enumFromInt(try b64r.readByte());
2488 const subpacket_len_hashed = try b64r.readInt(u32, .big);
2489 {
2490 try b64r.skipBytes(subpacket_len_hashed, .{}); // TODO read this
2491 }
2492 const subpacket_len_unhashed = try b64r.readInt(u32, .big);
2493 {
2494 try b64r.skipBytes(subpacket_len_unhashed, .{}); // TODO read this
2495 }
2496 signed_hash_value_prefix = try b64r.readArray(2);
2497 }
2498
2499 const material = try pgp_read_signature_material(b64r, allocator, pk_algo);
2500
2501 var valid: ?bool = null;
2502 _ = &valid;
2503 var pubkey_t: ?Pgp.PubKey = null;
2504
2505 // PGP keys are not stored in-band, need to fetch them and cache them
2506 // TODO check https://keys.openpgp.org
2507 // TODO check https://keyserver.ubuntu.com
2508 // TODO check manually uploaded keys in database
2509 if (known_pgp_keys.get(&extras.to_HEX(keyid))) |pubkey_bytes| blk: {
2510 const ns = std.crypto.Certificate.rsa;
2511 var pubkey_fbs: nio.FixedBufferStream([]const u8) = .init(pubkey_bytes);
2512 pubkey_t = pgp_parse_pubkey(&pubkey_fbs, allocator) catch break :blk;
2513 if (pubkey_t.?.material != pk_algo) {
2514 valid = false;
2515 break :blk;
2516 }
2517 switch (pubkey_t.?.material) {
2518 .rsa_encrypt_or_sign => |*m| {
2519 const pubkey = ns.PublicKey.fromBytes(m.e, m.n) catch break :blk;
2520 valid = if (pgp_rsa_verify(m.n.len, material.rsa_encrypt_or_sign, signed_data.items, pubkey, hash_algo)) true else |err| if (err == error.unrecognized) null else false;
2521 },
2522 else => {},
2523 }
2524 }
2525
2526 return .{ .pgp = .{
2527 .packet_length = packet_len,
2528 .version = sig_version,
2529 .type_id = type_id,
2530 .material = material,
2531 .hash_algorithm = hash_algo,
2532 .keyid = extras.to_HEX(keyid),
2533 .creation_time = .initUnix(std.mem.readInt(u32, &creation_time, .big)),
2534 .signed_hash_value_prefix = extras.to_hex(signed_hash_value_prefix),
2535 .pubkey = pubkey_t,
2536 .valid = valid,
2537 } };
2538 }
2539
2540 if (kind == .ssh) {
2541 if (!std.mem.eql(u8, &try b64r.readArray(6), "SSHSIG")) return .unrecognized;
2542 const sigversion = try b64r.readInt(u32, .big);
2543 if (sigversion != 1) return .unrecognized;
2544 const publickey = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2545 errdefer allocator.free(publickey);
2546 const namespace = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2547 defer allocator.free(namespace);
2548 const reserved = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2549 defer allocator.free(reserved);
2550 const hash_algorithm = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2551 errdefer allocator.free(hash_algorithm);
2552 const signature = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2553 errdefer allocator.free(signature);
2554 if (!std.mem.eql(u8, namespace, "git")) return .unrecognized;
2555 if (reserved.len > 0) return .unrecognized;
2556
2557 var signed_data: nio.AllocatingWriter = .init(allocator);
2558 defer signed_data.deinit();
2559 try signed_data.writeAll("SSHSIG");
2560 try signed_data.writeInt(u32, @intCast(namespace.len), .big);
2561 try signed_data.writeAll(namespace);
2562 try signed_data.writeInt(u32, @intCast(reserved.len), .big);
2563 try signed_data.writeAll(reserved);
2564 try signed_data.writeInt(u32, @intCast(hash_algorithm.len), .big);
2565 try signed_data.writeAll(hash_algorithm);
2566 if (std.mem.eql(u8, hash_algorithm, "sha256")) {
2567 const H = std.crypto.hash.sha2.Sha256;
2568 try signed_data.writeInt(u32, H.digest_length, .big);
2569 try signed_data.writeAll(&extras.hashBytes(H, message));
2570 }
2571 if (std.mem.eql(u8, hash_algorithm, "sha512")) {
2572 const H = std.crypto.hash.sha2.Sha512;
2573 try signed_data.writeInt(u32, H.digest_length, .big);
2574 try signed_data.writeAll(&extras.hashBytes(H, message));
2575 }
2576
2577 var valid: ?bool = null;
2578 var sigfixed: nio.FixedBufferStream([]const u8) = .init(signature);
2579 const sigformat = try sigfixed.readSlice(try sigfixed.readInt(u32, .big));
2580 var pkfixed: nio.FixedBufferStream([]const u8) = .init(publickey);
2581 const pkformat = try pkfixed.readSlice(try pkfixed.readInt(u32, .big));
2582
2583 if (std.mem.eql(u8, sigformat, "ssh-rsa")) {
2584 // intentionally skipped, rsa with sha1
2585 }
2586 if (std.mem.eql(u8, sigformat, "rsa-sha2-256")) blk: {
2587 if (!(std.mem.eql(u8, pkformat, "ssh-rsa"))) break :blk;
2588 const ns = std.crypto.Certificate.rsa;
2589 const pk_e_len = pkfixed.readInt(u32, .big) catch break :blk;
2590 var pk_e = pkfixed.readSlice(pk_e_len) catch break :blk;
2591 if (pk_e[0] == 0) pk_e = pk_e[1..];
2592 const pk_n_len = pkfixed.readInt(u32, .big) catch break :blk;
2593 var pk_n = pkfixed.readSlice(pk_n_len) catch break :blk;
2594 if (pk_n[0] == 0) pk_n = pk_n[1..];
2595 const pk = ns.PublicKey.fromBytes(pk_e, pk_n) catch break :blk;
2596 const sig_len = sigfixed.readInt(u32, .big) catch break :blk;
2597 const sig = sigfixed.readSlice(sig_len) catch break :blk;
2598 valid = if (pgp_rsa_verify(pk_n.len, sig, signed_data.items, pk, .sha2_256)) true else |err| if (err == error.unrecognized) null else false;
2599 }
2600 if (std.mem.eql(u8, sigformat, "rsa-sha2-512")) blk: {
2601 if (!(std.mem.eql(u8, pkformat, "ssh-rsa"))) break :blk;
2602 const ns = std.crypto.Certificate.rsa;
2603 const pk_e_len = pkfixed.readInt(u32, .big) catch break :blk;
2604 var pk_e = pkfixed.readSlice(pk_e_len) catch break :blk;
2605 if (pk_e[0] == 0) pk_e = pk_e[1..];
2606 const pk_n_len = pkfixed.readInt(u32, .big) catch break :blk;
2607 var pk_n = pkfixed.readSlice(pk_n_len) catch break :blk;
2608 if (pk_n[0] == 0) pk_n = pk_n[1..];
2609 const pk = ns.PublicKey.fromBytes(pk_e, pk_n) catch break :blk;
2610 const sig_len = sigfixed.readInt(u32, .big) catch break :blk;
2611 const sig = sigfixed.readSlice(sig_len) catch break :blk;
2612 valid = if (pgp_rsa_verify(pk_n.len, sig, signed_data.items, pk, .sha2_512)) true else |err| if (err == error.unrecognized) null else false;
2613 }
2614 if (std.mem.eql(u8, sigformat, "ssh-ed25519")) blk: {
2615 if (!(std.mem.eql(u8, pkformat, "ssh-ed25519"))) break :blk;
2616 const ed = std.crypto.sign.Ed25519;
2617 const pk_len = pkfixed.readInt(u32, .big) catch break :blk;
2618 if (pk_len != ed.PublicKey.encoded_length) break :blk;
2619 const pk_bytes = pkfixed.readArray(ed.PublicKey.encoded_length) catch break :blk;
2620 const pk = ed.PublicKey.fromBytes(pk_bytes) catch break :blk;
2621 const sig_len = sigfixed.readInt(u32, .big) catch break :blk;
2622 if (sig_len != ed.Signature.encoded_length) break :blk;
2623 const sig_bytes = sigfixed.readArray(ed.Signature.encoded_length) catch break :blk;
2624 const sig = ed.Signature.fromBytes(sig_bytes);
2625 valid = if (sig.verifyStrict(signed_data.items, pk)) true else |_| false;
2626 }
2627 if (std.mem.eql(u8, sigformat, "ecdsa-sha2-nistp256")) blk: {
2628 if (!(std.mem.eql(u8, pkformat, "ecdsa-sha2-nistp256"))) break :blk;
2629 const C = std.crypto.ecc.P256;
2630 const H = std.crypto.hash.sha2.Sha256;
2631 const ns = std.crypto.sign.ecdsa.Ecdsa(C, H);
2632 const ident_len = pkfixed.readInt(u32, .big) catch break :blk;
2633 if (ident_len != "nistp256".len) break :blk;
2634 if (!(pkfixed.readExpected("nistp256") catch break :blk)) break :blk;
2635 const q_len = pkfixed.readInt(u32, .big) catch break :blk;
2636 const q = pkfixed.readSlice(q_len) catch break :blk;
2637 const pk = ns.PublicKey.fromSec1(q) catch break :blk;
2638 const sigblob_len = sigfixed.readInt(u32, .big) catch break :blk;
2639 _ = sigblob_len;
2640 const r_len = sigfixed.readInt(u32, .big) catch break :blk;
2641 var r = sigfixed.readSlice(r_len) catch break :blk;
2642 if (r[0] == 0) r = r[1..];
2643 if (r.len != C.scalar.encoded_length) break :blk;
2644 const s_len = sigfixed.readInt(u32, .big) catch break :blk;
2645 var s = sigfixed.readSlice(s_len) catch break :blk;
2646 if (s[0] == 0) s = s[1..];
2647 if (s.len != C.scalar.encoded_length) break :blk;
2648 const sig = ns.Signature.fromBytes((r[0..C.scalar.encoded_length] ++ s[0..C.scalar.encoded_length]).*);
2649 valid = if (sig.verify(signed_data.items, pk)) true else |_| false;
2650 }
2651 if (std.mem.eql(u8, sigformat, "ecdsa-sha2-nistp384")) blk: {
2652 if (!(std.mem.eql(u8, pkformat, "ecdsa-sha2-nistp384"))) break :blk;
2653 const C = std.crypto.ecc.P384;
2654 const H = std.crypto.hash.sha2.Sha384;
2655 const ns = std.crypto.sign.ecdsa.Ecdsa(C, H);
2656 const ident_len = pkfixed.readInt(u32, .big) catch break :blk;
2657 if (ident_len != "nistp384".len) break :blk;
2658 if (!(pkfixed.readExpected("nistp384") catch break :blk)) break :blk;
2659 const q_len = pkfixed.readInt(u32, .big) catch break :blk;
2660 const q = pkfixed.readSlice(q_len) catch break :blk;
2661 const pk = ns.PublicKey.fromSec1(q) catch break :blk;
2662 const sigblob_len = sigfixed.readInt(u32, .big) catch break :blk;
2663 _ = sigblob_len;
2664 const r_len = sigfixed.readInt(u32, .big) catch break :blk;
2665 var r = sigfixed.readSlice(r_len) catch break :blk;
2666 if (r[0] == 0) r = r[1..];
2667 if (r.len != C.scalar.encoded_length) break :blk;
2668 const s_len = sigfixed.readInt(u32, .big) catch break :blk;
2669 var s = sigfixed.readSlice(s_len) catch break :blk;
2670 if (s[0] == 0) s = s[1..];
2671 if (s.len != C.scalar.encoded_length) break :blk;
2672 const sig = ns.Signature.fromBytes((r[0..C.scalar.encoded_length] ++ s[0..C.scalar.encoded_length]).*);
2673 valid = if (sig.verify(signed_data.items, pk)) true else |_| false;
2674 }
2675 // ssh-dss (dsa)
2676 // sk-ecdsa-sha2-nistp256@openssh.com
2677 // sk-ssh-ed25519@openssh.com
2678
2679 return .{ .ssh = .{
2680 .publickey = publickey,
2681 .hash_algorithm = hash_algorithm,
2682 .signature = signature,
2683 .valid = valid,
2684 } };
2685 }
2686
2687 switch (kind) {
2688 .pgp => {}, //above
2689 .ssh => {}, //above
2690 }
2691 return .unrecognized;
2692 }
2693
2694 fn pgp_read_packet_len(r: anytype) !struct { bool, u32 } {
2695 const oct1: u32 = try r.readByte();
2696 if (oct1 <= 191) return .{ false, oct1 };
2697 if (oct1 >= 224 and oct1 < 255) return .{ true, @as(u32, 1) << @intCast(oct1 & 0x1F) };
2698 const oct2: u32 = try r.readByte();
2699 if (oct1 <= 223) return .{ false, ((oct1 - 192) << 8) + (oct2) + 192 };
2700 const oct3: u32 = try r.readByte();
2701 const oct4: u32 = try r.readByte();
2702 const oct5: u32 = try r.readByte();
2703 std.debug.assert(oct1 == 255);
2704 return .{ false, (oct2 << 24) | (oct3 << 16) | (oct4 << 8) | oct5 };
2705 }
2706
2707 fn pgp_read_subpacket_len(r: anytype) !u32 {
2708 const oct1: u32 = try r.readByte();
2709 if (oct1 < 192) return oct1;
2710 const oct2: u32 = try r.readByte();
2711 if (oct1 < 255) return ((oct1 - 192) << 8) + (oct2) + 192;
2712 const oct3: u32 = try r.readByte();
2713 const oct4: u32 = try r.readByte();
2714 const oct5: u32 = try r.readByte();
2715 return (oct2 << 24) | (oct3 << 16) | (oct4 << 8) | oct5;
2716 }
2717
2718 fn pgp_parse_pubkey(r: anytype, allocator: std.mem.Allocator) !Pgp.PubKey {
2719 const packet_type: packed struct { id: u6, format: u1, reserved: u1 } = @bitCast(try r.readByte());
2720 if (packet_type.reserved != 1) return error.unrecognized;
2721 if (packet_type.format != 1) return error.unrecognized; // legacy non-OpenPGP format
2722 if (packet_type.id != 6) return error.unrecognized; // not a public key
2723 _, const packet_len = pgp_read_packet_len(r) catch return error.unrecognized;
2724 var lr = nio.LimitedReader(void).from(r, packet_len);
2725 const key_version = try lr.readByte();
2726 if (key_version == 3) {
2727 // TODO
2728 }
2729 if (key_version == 4) {
2730 const creation_time = try lr.readArray(4);
2731 const pk_algo: Pgp.PublicKeyAlgorithm = @enumFromInt(try lr.readByte());
2732 const material = try pgp_parse_pubkey_material(r, allocator, pk_algo);
2733 return .{
2734 .version = key_version,
2735 .creation_time = .initUnix(std.mem.readInt(u32, &creation_time, .big)),
2736 .days_valid = 0,
2737 .material = material,
2738 };
2739 }
2740 if (key_version == 6) {
2741 // TODO
2742 }
2743 return .{
2744 .version = key_version,
2745 .creation_time = .initUnix(0),
2746 .days_valid = 0,
2747 .material = .reserved,
2748 };
2749 }
2750
2751 fn pgp_parse_pubkey_material(r: anytype, allocator: std.mem.Allocator, pk_algo: Pgp.PublicKeyAlgorithm) !Pgp.PubKey.Material {
2752 switch (pk_algo) {
2753 .rsa_encrypt_or_sign => {
2754 const n_len = try r.readInt(u16, .big);
2755 const n = try r.readAlloc(allocator, (n_len + 7) / 8);
2756 const e_len = try r.readInt(u16, .big);
2757 const e = try r.readAlloc(allocator, (e_len + 7) / 8);
2758 return .{ .rsa_encrypt_or_sign = .{ .n = n, .e = e } };
2759 },
2760 inline else => |t| return @unionInit(Pgp.PubKey.Material, @tagName(t), {}),
2761 _ => return .reserved,
2762 }
2763 }
2764
2765 fn pgp_read_signature_material(r: anytype, allocator: std.mem.Allocator, pk_algo: Pgp.PublicKeyAlgorithm) !Pgp.Material {
2766 switch (pk_algo) {
2767 .rsa_encrypt_or_sign => {
2768 const len = try r.readInt(u16, .big);
2769 const bytes = try r.readAlloc(allocator, (len + 7) / 8);
2770 return .{ .rsa_encrypt_or_sign = bytes };
2771 },
2772 inline else => |t| return @unionInit(Pgp.Material, @tagName(t), {}),
2773 _ => return .reserved,
2774 }
2775 }
2776
2777 // https://datatracker.ietf.org/doc/html/rfc9580#name-rsa
2778 // An implementation SHOULD NOT encrypt, sign, or verify using RSA keys of a size less than 3072 bits.
2779 // An implementation that decrypts a message using an RSA secret key of a size less than 3072 bits SHOULD generate a deprecation warning that the key is too weak for modern use.
2780 fn pgp_rsa_verify(modulus_len_r: usize, sig: []const u8, message: []const u8, pubkey: std.crypto.Certificate.rsa.PublicKey, hash_algorithm: Pgp.HashAlgorithm) !void {
2781 return switch (modulus_len_r) {
2782 inline 384, 512 => |modulus_len| pgp_rsa_verify_inner(modulus_len, sig, message, pubkey, hash_algorithm),
2783 else => error.unrecognized,
2784 };
2785 }
2786
2787 fn pgp_rsa_verify_inner(comptime modulus_len: usize, sig: []const u8, message: []const u8, pubkey: std.crypto.Certificate.rsa.PublicKey, hash_algorithm: Pgp.HashAlgorithm) !void {
2788 const ns = std.crypto.Certificate.rsa;
2789 const signature = ns.PKCS1v1_5Signature.fromBytes(modulus_len, sig);
2790 return switch (hash_algorithm) {
2791 .reserved => error.unrecognized,
2792 .md5 => error.unrecognized, //ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.Md5),
2793 .sha1 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.Sha1),
2794 .ripemd160 => error.unrecognized,
2795 .sha2_224 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha224),
2796 .sha2_256 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha256),
2797 .sha2_384 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha384),
2798 .sha2_512 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha512),
2799 .sha3_256 => error.unrecognized, //ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha3.Sha3_256),
2800 .sha3_512 => error.unrecognized, //ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha3.Sha3_512),
2801 _ => error.unrecognized,
2802 };
2803 }
2804
2805 // curl https://keys.openpgp.org/vks/v1/by-keyid/<KEYID> | head -n -2 | tail +4 | tr -d '\n' | base64 -d | xxd -p
2806 pub const known_pgp_keys: std.StaticStringMap([]const u8) = blk: {
2807 @setEvalBranchQuota(std.math.maxInt(u32));
2808 break :blk .initComptime(.{
2809 // GitHub <noreply@github.com>
2810 .{ "B5690EEEBB952194", &extras.from_hex("c6c14d0465a6c576011000b237ee2f88540ce904282abd00d96d2b19d3286d3270399b28d903e97b06f206cb8bdeb356e4e987ad6c170213ca36a508c8ecddabc3284b1a134eea8b92ca1a862c442ff5f69f284e07147d1a4633fe823d4779d870b45a150a1226855f3adeb5a2990b5336d601e8696e04fc10a967f09b0436ad3ed270e55908b487643da069aa960573b24c26559f0b7b2bf8320ee137ab30c99ee6a197e49290f9f94ed9d9756eac2ce44927e0e336f694c44b268b3678201cef8f440ac5f4b7b3817ed17e62a8232178b15ec9b646b52abd4e3b3a43ad65e021bc061e9db0b7a6bf4585ae4dc6805411afb5d4c2bd5ba39c461c2fc55094acc511e854d26720bffd5a574d3b6b4653aaa54e7018c6cfdf3a67f607aa3970a5f2b17bfb58003c6fe8f901504c1b1512bbb08b8256a20890b90c8ec0022515effb76f6b15991a9bffc96dbe2f782e2fbc5a1ca1551ac6b28658ad410e576be3f589280a4970f245e09b5a8b5fa807a6a4fc92236e4b1e1dd271723a3607474ab481685a23c007e0c5b3d3110e9a748b0a1c24287eb9cbacd6b750e7860c688e38ad1a615de38840c6028b9ab6435fdaaf39128b23d5ff65682a801f760b243edd7129ded620a12b9a914c1c8cbc8c9e483d0d6cd9d5305386a57c92c96b4a6f695da6ef00581409b09b3d166601716006040da9a7e2316829faac120722c51515137c39564ab04b01916cb0011010001") },
2811 });
2812 };
2813};
2814
2815pub fn findFirstUnset(set: std.bit_set.DynamicBitSetUnmanaged, after: usize) ?usize {
2816 const MaskInt = std.bit_set.DynamicBitSetUnmanaged.MaskInt;
2817 if (after >= set.bit_length) return null;
2818 if (!set.isSet(after)) return after;
2819 var maski = after / @bitSizeOf(MaskInt);
2820 while (set.masks[maski] == std.math.maxInt(MaskInt)) maski += 1;
2821 var mask = set.masks[maski];
2822 mask |= (@as(usize, 1) << @intCast((after -| (maski * @bitSizeOf(MaskInt))) % @bitSizeOf(MaskInt))) - 1;
2823 if (mask == std.math.maxInt(MaskInt)) maski += 1;
2824 while (set.masks[maski] == std.math.maxInt(MaskInt)) maski += 1;
2825 if (mask == std.math.maxInt(MaskInt)) mask = set.masks[maski];
2826 const candidate = maski * @bitSizeOf(MaskInt) + @ctz(~mask);
2827 if (candidate >= set.bit_length) return null;
2828 return candidate;
2829}
2830
2831const PathListNode = struct {
2832 prev: ?*const @This(),
2833 data: []const u8,
2834
2835 pub fn nprint(node: *const PathListNode, writable: anytype) !void {
2836 if (node.prev == null) {
2837 try writable.writeAll(node.data);
2838 return;
2839 }
2840 try nprint(node.prev.?, writable);
2841 try writable.writevAll(&.{ "/", node.data });
2842 }
2843};
2844
2845/// Consumes 'tree' and returns a new owned 'Tree' in the tuple.
2846pub fn idFor(r: *Repository, tree: *Tree, sub_path: []const u8) !?struct { *const Tree.Object, *Tree } {
2847 const s_idx = std.mem.indexOfScalar(u8, sub_path, '/');
2848 const seg = sub_path[0 .. s_idx orelse sub_path.len];
2849 const obj = tree.get(seg) orelse {
2850 tree.destroy(r);
2851 return null;
2852 };
2853 switch (obj.id) {
2854 .tree => |id| {
2855 defer tree.destroy(r);
2856 if (s_idx == null) return null;
2857 const new_tree = try r.getTreeA(id.id, .no_cache);
2858 return idFor(r, new_tree, sub_path[s_idx.? + 1 ..]);
2859 },
2860 else => {
2861 if (s_idx != null) {
2862 tree.destroy(r);
2863 return null;
2864 }
2865 return .{ obj, tree };
2866 },
2867 }
2868}
2869
2870fn NonVoidUnionFieldEnum(U: type) type {
2871 const info = @typeInfo(U).@"union";
2872 var names: [info.fields.len][:0]const u8 = @splat("");
2873 var count: usize = 0;
2874 for (info.fields) |f| {
2875 if (f.type == void) continue;
2876 names[count] = f.name;
2877 count += 1;
2878 }
2879 const T = std.math.IntFittingRange(0, count - 1);
2880 return @Enum(T, .exhaustive, names[0..count], &std.simd.iota(T, count));
2881}