root.zig (38550B)
1 const std = @import("std"); 2 const argparse = @import("argparse.zig"); 3 const IniFile = @import("inifile.zig"); 4 const Dir = std.fs.Dir; 5 6 // A horrible, buggy, version of git. 7 8 pub fn doMain() !void { 9 prng = std.rand.DefaultPrng.init(@intCast(std.time.timestamp())); 10 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 11 defer _ = gpa.deinit(); 12 const a = gpa.allocator(); 13 var ap = argparse.init(a, "wyag", "Write Yourself A Git: a bad version of git for educational purposes"); 14 defer ap.deinit(); 15 var init: argparse.Subcommand = .{ .parent = &ap, .description = "Initialize a new git repository", .name = "init" }; 16 try ap.addSubcommand(&init); 17 var init_path: argparse.Positional = .{ .name = "path", .description = "The directory in which to make a git repository" }; 18 try init.addPositional(&init_path); 19 20 var cat_file: argparse.Subcommand = .{ .parent = &ap, .description = "Read the contents of an object", .name = "cat-file" }; 21 try ap.addSubcommand(&cat_file); 22 var cat_file_kind: argparse.Positional = .{ .name = "type", .description = "The type of the object to be read" }; 23 try cat_file.addPositional(&cat_file_kind); 24 var cat_file_ref: argparse.Positional = .{ .name = "object", .description = "The object reference to read" }; 25 try cat_file.addPositional(&cat_file_ref); 26 27 var hash_object: argparse.Subcommand = .{ .parent = &ap, .description = "Create a blob from a file", .name = "hash-object" }; 28 try ap.addSubcommand(&hash_object); 29 var hash_object_write: argparse.Flag = .{ .long = "write", .short = "w", .description = "Actually write the file to the repository", .hasarg = false }; 30 try hash_object.addFlag(&hash_object_write); 31 var hash_object_input: argparse.Positional = .{ .name = "file", .description = "The file to create a blob from" }; 32 try hash_object.addPositional(&hash_object_input); 33 34 var log: argparse.Subcommand = .{ .parent = &ap, .description = "Log commits", .name = "log" }; 35 try ap.addSubcommand(&log); 36 var log_commit: argparse.Positional = .{ .name = "commit", .description = "The commit to log from" }; 37 try log.addPositional(&log_commit); 38 39 var ls_tree: argparse.Subcommand = .{ .parent = &ap, .description = "List items in a tree", .name = "ls-tree" }; 40 try ap.addSubcommand(&ls_tree); 41 var ls_tree_recursive: argparse.Flag = .{ .long = "recursive", .short = "r", .description = "List contents recursively", .hasarg = false }; 42 try ls_tree.addFlag(&ls_tree_recursive); 43 var ls_tree_ish: argparse.Positional = .{ .name = "tree-ish", .description = "The tree-ish to log the tree for" }; 44 try ls_tree.addPositional(&ls_tree_ish); 45 46 var checkout: argparse.Subcommand = .{ .parent = &ap, .description = "Checkout a commit inside of a directory", .name = "checkout" }; 47 try ap.addSubcommand(&checkout); 48 var checkout_ref: argparse.Positional = .{ .name = "commit", .description = "The ref to checkout (commit id, tag, or branch name" }; 49 try checkout.addPositional(&checkout_ref); 50 var checkout_directory: argparse.Positional = .{ .name = "directory", .description = "The directory to checkout into" }; 51 try checkout.addPositional(&checkout_directory); 52 53 var show_ref: argparse.Subcommand = .{ .parent = &ap, .description = "List references in a local repository", .name = "show-ref" }; 54 try ap.addSubcommand(&show_ref); 55 56 var tag: argparse.Subcommand = .{ .parent = &ap, .description = "Manage tags", .name = "tag" }; 57 try ap.addSubcommand(&tag); 58 var tag_name: argparse.Positional = .{ .name = "name", .description = "name for the new tag" }; 59 try tag.addPositional(&tag_name); 60 var tag_object: argparse.Positional = .{ .name = "object", .description = "the object to add a tag for" }; 61 try tag.addPositional(&tag_object); 62 var tag_annotated: argparse.Flag = .{ .long = "annotate", .short = "a", .description = "whether to create an annotated tag", .hasarg = false }; 63 try tag.addFlag(&tag_annotated); 64 65 if (!try ap.parseOrHelp()) { 66 return; 67 } 68 if (init.wasExecuted) { 69 if (init_path.value) |path| { 70 var repo = try repoCreate(a, path); 71 defer repo.deinit(); 72 } else { 73 std.log.err("No 'path' provided to init", .{}); 74 return error.InvalidArgs; 75 } 76 } else if (cat_file.wasExecuted) { 77 if (cat_file_ref.value) |ref| { 78 if (cat_file_kind.value) |kind_str| { 79 try catFile(a, std.fs.cwd(), kind_str, ref, std.io.getStdOut().writer().any()); 80 } else { 81 std.log.err("No type provided to cat-file", .{}); 82 return error.InvalidArgs; 83 } 84 } else { 85 std.log.err("No ref provided to cat-file", .{}); 86 return error.InvalidArgs; 87 } 88 } else if (hash_object.wasExecuted) { 89 if (hash_object_input.value) |input| { 90 try hashObject(a, std.fs.cwd(), input, hash_object_write.waspresent, std.io.getStdOut().writer().any()); 91 } else { 92 std.log.err("No file supplied to hash-object", .{}); 93 return error.InvalidArgs; 94 } 95 } else if (log.wasExecuted) { 96 if (log_commit.value) |ref| { 97 try gitLog(a, std.fs.cwd(), ref, std.io.getStdOut().writer()); 98 } else { 99 std.log.err("No commit supplied to log", .{}); 100 } 101 } else if (ls_tree.wasExecuted) { 102 if (ls_tree_ish.value) |ref| { 103 try lsTree(a, std.fs.cwd(), ref, std.io.getStdOut().writer(), ls_tree_recursive.waspresent); 104 } else { 105 std.log.err("No commit supplied to ls-tree", .{}); 106 } 107 } else if (checkout.wasExecuted) { 108 if (checkout_ref.value) |commit_ref| { 109 if (checkout_directory.value) |directory| { 110 try doCheckout(a, std.fs.cwd(), commit_ref, directory); 111 } else { 112 std.log.err("No directory was supplied to checkout", .{}); 113 } 114 } else { 115 std.log.err("No commit supplied to checkout", .{}); 116 } 117 } else if (show_ref.wasExecuted) { 118 try doShowRef(a, std.fs.cwd(), std.io.getStdOut().writer()); 119 } else if (tag.wasExecuted) { 120 if (tag_name.value) |tagname| { 121 try doAddTag(a, std.fs.cwd(), tagname, tag_object.value, tag_annotated.waspresent); 122 } else { 123 try doListTags(a, std.fs.cwd(), std.io.getStdOut().writer()); 124 } 125 } else { 126 if (ap.excess.items.len > 0) { 127 std.log.err("Unsupported sub-command {s}, have you tried implementing it yourself?", .{ap.excess.items[0]}); 128 } else { 129 std.log.err("No sub-command requested", .{}); 130 } 131 return error.InvalidArgs; 132 } 133 } 134 135 fn doListTags(a: std.mem.Allocator, dir: std.fs.Dir, writer: anytype) !void { 136 var repo = try repo_find(a, dir); 137 defer repo.deinit(); 138 var refs = try repo.ref_list(a); 139 defer { 140 // TODO could do with a more convenient wrapper probably, maybe even an arena 141 for (refs.keys()) |k| a.free(k); 142 refs.deinit(); 143 } 144 for (refs.keys()) |k| { 145 const tags_prefix = "refs/tags/"; 146 if (std.mem.startsWith(u8, k, tags_prefix)) { 147 try std.fmt.format(writer, "{s}\n", .{k[tags_prefix.len..]}); 148 } 149 } 150 } 151 152 fn doAddTag(a: std.mem.Allocator, dir: std.fs.Dir, tagname: []const u8, maybe_ref: ?[]const u8, create_tag_obj: bool) !void { 153 var repo = try repo_find(a, dir); 154 defer repo.deinit(); 155 const ref: [20]u8 = 156 if (maybe_ref) |rr| 157 try repo.resolve(a, rr) 158 else 159 try repo.head(a); 160 var ref_str = std.fmt.bytesToHex(ref, .lower); 161 162 if (create_tag_obj) { 163 const f = try runEditor(a, repo); 164 defer f.close(); 165 166 var al = std.ArrayList(u8).init(a); 167 defer al.deinit(); 168 const writer = al.writer(); 169 try std.fmt.format(writer, "object {s}\n", .{&ref_str}); 170 try std.fmt.format(writer, "type commit\n", .{}); 171 try std.fmt.format(writer, "tag {s}\n", .{tagname}); 172 try std.fmt.format(writer, "tagger \"Martin Ashby\"\n", .{}); 173 try writer.writeByte('\n'); 174 try pump(f.reader(), writer); 175 try writer.writeByte('\n'); 176 var fbs = std.io.fixedBufferStream(al.items); 177 const res = try repo.write_object(a, al.items.len, fbs.reader(), .tag, true); 178 defer a.free(res); 179 std.mem.copyForwards(u8, &ref_str, res); 180 } 181 182 const tagfilepath = try std.fs.path.join(a, &.{ "refs", "tags", tagname }); 183 defer a.free(tagfilepath); 184 var tagfile = try repo.gitdir.createFile(tagfilepath, .{}); 185 defer tagfile.close(); 186 var wtr = tagfile.writer(); 187 try wtr.writeAll(&ref_str); 188 try wtr.writeByte('\n'); 189 } 190 191 // Caller must close the file 192 fn runEditor(a: std.mem.Allocator, repo: GitRepository) !std.fs.File { 193 const tempfilepath = try repo.makeTempFilePath(a); 194 defer a.free(tempfilepath); 195 // check the config, then the EDITOR 196 const editor = if (repo.conf.get("core.editor")) |ed| ed else if (std.posix.getenv("EDITOR")) |ed2| ed2 else "nano"; 197 const args: []const []const u8 = &.{ editor, tempfilepath }; 198 var env = try std.process.getEnvMap(a); 199 defer env.deinit(); 200 std.log.warn("Launching {s}", .{args}); 201 var child = std.process.Child.init(args, a); 202 child.stdin_behavior = .Inherit; 203 child.stdout_behavior = .Inherit; 204 child.stderr_behavior = .Inherit; 205 child.cwd_dir = repo.gitdir; 206 child.env_map = &env; 207 try child.spawn(); 208 const term = try child.wait(); 209 switch (term) { 210 .Exited => |code| { 211 if (code != 0) { 212 return error.SubprocessError; 213 } 214 }, 215 else => { 216 return error.SubprocessError; 217 }, 218 } 219 return try repo.gitdir.openFile(tempfilepath, .{}); 220 } 221 222 fn doShowRef(a: std.mem.Allocator, dir: std.fs.Dir, writer: anytype) !void { 223 var repo = try repo_find(a, dir); 224 defer repo.deinit(); 225 var refs = try repo.ref_list(a); 226 defer { 227 var it = refs.iterator(); 228 while (it.next()) |e| { 229 a.free(e.key_ptr.*); 230 } 231 refs.deinit(); 232 } 233 var it = refs.iterator(); 234 while (it.next()) |e| { 235 try std.fmt.format(writer, "{} {s}\n", .{ std.fmt.fmtSliceHexLower(&(e.value_ptr.*)), e.key_ptr.* }); 236 } 237 } 238 239 fn doCheckout(a: std.mem.Allocator, dir: std.fs.Dir, checkout_ref: []const u8, checkout_directory: []const u8) !void { 240 var repo = try repo_find(a, dir); 241 defer repo.deinit(); 242 var checkoutDir = try dir.makeOpenPath(checkout_directory, .{ .iterate = true }); 243 defer checkoutDir.close(); 244 const ref = try repo.resolve(a, checkout_ref); 245 var go = try repo.read_object_sha(a, ref); 246 defer go.deinit(); 247 if (go.kind != .commit) return error.NotACommit; 248 var it = checkoutDir.iterate(); 249 if (try it.next() != null) return error.DirNotEmpty; 250 251 var commit = try Commit.parse(a, go.reader()); 252 defer commit.deinit(a); 253 var go2 = try repo.read_object(a, commit.tree); 254 defer go2.deinit(); 255 try doCheckoutInternal(a, &repo, &go2, checkoutDir); 256 } 257 258 fn doCheckoutInternal(a: std.mem.Allocator, repo: *GitRepository, go: *GitObject, dir: std.fs.Dir) !void { 259 if (go.kind != .tree) return error.NotATree; 260 var gt = try Tree.parse(a, go.reader()); 261 defer gt.deinit(); 262 for (gt.leaves.items) |leaf| { 263 var go2 = try repo.read_object_sha(a, leaf.sha); 264 defer go2.deinit(); 265 switch (leaf.filetype) { 266 .file => { 267 var f = try dir.createFile(leaf.path, .{ .mode = leaf.mode }); 268 defer f.close(); 269 try pump(go2.reader(), f.writer()); 270 }, 271 .directory => { 272 var subdir = try dir.makeOpenPath(leaf.path, .{}); 273 defer subdir.close(); 274 try doCheckoutInternal(a, repo, &go2, subdir); 275 }, 276 .symlink => { 277 return error.Unimplemented; 278 }, 279 .submodule => { 280 return error.Unimplemented; 281 }, 282 } 283 } 284 } 285 286 fn lsTree(a: std.mem.Allocator, dir: Dir, ref: []const u8, writer: anytype, recurse: bool) !void { 287 var repo = try repo_find(a, dir); 288 defer repo.deinit(); 289 var go = try repo.read_object(a, ref); 290 defer go.deinit(); 291 var go2: ?GitObject = go; 292 defer if (go2) |*g| g.deinit(); 293 if (go.kind == .commit) { 294 var gc = try Commit.parse(a, go.reader()); 295 defer gc.deinit(a); 296 go2 = try repo.read_object(a, gc.tree); 297 } else if (go.kind != .tree) { 298 return error.NotATreeish; 299 } 300 try lsTreeInternal(a, &repo, &go2.?, writer, recurse, ""); 301 } 302 303 fn lsTreeInternal(a: std.mem.Allocator, repo: *GitRepository, go: *GitObject, writer: anytype, recurse: bool, path_prefix: []const u8) !void { 304 if (go.kind != .tree) return error.NotATree; 305 var gt = try Tree.parse(a, go.reader()); 306 defer gt.deinit(); 307 for (gt.leaves.items) |leaf| { 308 const filetype_str = switch (leaf.filetype) { 309 .file => "blob", 310 .directory => "tree", 311 .symlink => "blob", 312 .submodule => "submodule", 313 }; 314 const fullpath = try std.fs.path.join(a, &.{ path_prefix, leaf.path }); 315 defer a.free(fullpath); 316 if (recurse and leaf.filetype == .directory) { 317 var go2 = try repo.read_object_sha(a, leaf.sha); 318 defer go2.deinit(); 319 try lsTreeInternal(a, repo, &go2, writer, recurse, fullpath); 320 } else { 321 try std.fmt.format(writer, "{}{o:0>4} {s} {s} {s}\n", .{ leaf.filetype, leaf.mode, filetype_str, std.fmt.fmtSliceHexLower(&leaf.sha), fullpath }); 322 } 323 } 324 } 325 326 fn gitLog(a: std.mem.Allocator, dir: Dir, ref: []const u8, writer: anytype) !void { 327 var repo = try repo_find(a, dir); 328 defer repo.deinit(); 329 var seen = std.AutoHashMap([20]u8, void).init(a); 330 defer seen.deinit(); 331 var sha: [20]u8 = undefined; 332 _ = try std.fmt.hexToBytes(&sha, ref); 333 try writer.writeAll("digraph wyaglog{\n"); 334 try writer.writeAll(" node[shape=rect]\n"); 335 try logGraphvis(a, repo, sha, &seen, writer.any()); 336 try writer.writeAll("}"); 337 } 338 339 fn logGraphvis(a: std.mem.Allocator, repo: GitRepository, sha: [20]u8, seen: *std.AutoHashMap([20]u8, void), writer: std.io.AnyWriter) !void { 340 const gpr = try seen.getOrPut(sha); 341 if (gpr.found_existing) return; // short circuit we've already seen it 342 343 var go = try repo.read_object_sha(a, sha); 344 defer go.deinit(); 345 if (go.kind != .commit) return error.NotACommit; 346 var commit = try Commit.parse(a, go.reader()); 347 defer commit.deinit(a); 348 349 const ref = std.fmt.bytesToHex(sha, .lower); 350 var spl = std.mem.splitScalar(u8, commit.message, '\n'); 351 const short_msg_bare = std.mem.trim(u8, spl.first(), " "); 352 353 const short_msg_1 = try std.mem.replaceOwned(u8, a, short_msg_bare, "\\", "\\\\"); 354 defer a.free(short_msg_1); 355 const short_msg_2 = try std.mem.replaceOwned(u8, a, short_msg_1, "\"", "\\\""); 356 defer a.free(short_msg_2); 357 try std.fmt.format(writer, " c_{s} [label=\"{s}: {s}\"]\n", .{ ref, ref[0..7], short_msg_2 }); 358 for (commit.parents.items) |parent_ref| { 359 try std.fmt.format(writer, " c_{s} -> c_{s};\n", .{ ref, parent_ref }); 360 var child_sha: [20]u8 = undefined; 361 _ = try std.fmt.hexToBytes(&child_sha, parent_ref); 362 try logGraphvis(a, repo, child_sha, seen, writer); 363 } 364 } 365 366 fn repoCreate(ca: std.mem.Allocator, path: []const u8) !GitRepository { 367 var worktree: Dir = undefined; 368 { 369 var aa = std.heap.ArenaAllocator.init(ca); 370 defer aa.deinit(); 371 const a = aa.allocator(); 372 const cwd = std.fs.cwd(); 373 worktree = try cwd.makeOpenPath(path, .{}); 374 errdefer worktree.close(); 375 var gitdir = try worktree.makeOpenPath(".git", .{}); 376 defer gitdir.close(); 377 try gitdir.makePath("branches"); 378 try gitdir.makePath("objects"); 379 try gitdir.makePath(try std.fs.path.join(a, &.{ "refs", "tags" })); 380 try gitdir.makePath(try std.fs.path.join(a, &.{ "refs", "heads" })); 381 var headfile = try gitdir.createFile("HEAD", .{}); 382 defer headfile.close(); 383 try headfile.writeAll("ref: refs/heads/main\n"); 384 var configfile = try gitdir.createFile("config", .{}); 385 defer configfile.close(); 386 try configfile.writeAll( 387 \\[core] 388 \\ repositoryformatversion = 0 389 \\ filemode = false 390 \\ bare = false 391 ); 392 } 393 return try GitRepository.init(ca, worktree); 394 } 395 396 fn catFile(a: std.mem.Allocator, cwd: std.fs.Dir, kind_str: []const u8, ref: []const u8, writer: std.io.AnyWriter) !void { 397 const kind = try enumFromString(kind_str, ObjectKind); 398 var repo = try repo_find(a, cwd); 399 defer repo.deinit(); 400 var git_object = try repo.read_object(a, ref); 401 defer git_object.deinit(); 402 if (git_object.kind != kind) { 403 return error.WrongKind; 404 } 405 var rdr = git_object.reader(); 406 var buf = [_]u8{0} ** std.mem.page_size; 407 while (true) { 408 const sz = try rdr.read(&buf); 409 if (sz == 0) break; 410 try writer.writeAll(buf[0..sz]); 411 } 412 } 413 414 fn hashObject(a: std.mem.Allocator, cwd: std.fs.Dir, file: []const u8, write: bool, writer: std.io.AnyWriter) !void { 415 var repo = try repo_find(a, cwd); 416 defer repo.deinit(); 417 const stat = try cwd.statFile(file); 418 var infile = try std.fs.cwd().openFile(file, .{}); 419 const rdr = infile.reader(); 420 const sha = try repo.write_object(a, stat.size, rdr, .blob, write); 421 defer a.free(sha); 422 try writer.writeAll(sha); 423 try writer.writeByte('\n'); 424 } 425 426 pub const GitObject = struct { 427 const ReadError = std.compress.zlib.Decompressor(std.fs.File.Reader).Error || error{ TooLong, InvalidHash }; 428 429 kind: ObjectKind, 430 len: u64, 431 sha1: [20]u8, 432 433 _file: std.fs.File, 434 _decomp: std.compress.zlib.Decompressor(std.fs.File.Reader), 435 _sha1: std.crypto.hash.Sha1, 436 _read: u64, 437 438 pub fn read(self: *GitObject, buffer: []u8) ReadError!usize { 439 const sz = try self._decomp.read(buffer); 440 if (self._read + sz > self.len) { 441 return error.TooLong; 442 } 443 self._read += sz; 444 if (sz > 0) { 445 self._sha1.update(buffer[0..sz]); 446 } else { 447 const final = self._sha1.finalResult(); 448 if (!std.mem.eql(u8, &self.sha1, &final)) { 449 return error.InvalidHash; 450 } 451 } 452 return sz; 453 } 454 455 pub fn reader(self: *GitObject) std.io.Reader(*GitObject, ReadError, GitObject.read) { 456 return .{ 457 .context = self, 458 }; 459 } 460 461 pub fn deinit(self: *GitObject) void { 462 self._file.close(); 463 } 464 }; 465 466 pub const GitRepository = struct { 467 worktree: Dir, 468 gitdir: Dir, 469 conf: IniFile, 470 _aa: std.heap.ArenaAllocator, 471 472 // Note: takes 'ownership' of dir; callers should not use it again (including closing it) after calling 473 // this function. 474 pub fn init(ca: std.mem.Allocator, dir: Dir) !GitRepository { 475 var self: GitRepository = undefined; 476 self._aa = std.heap.ArenaAllocator.init(ca); 477 errdefer self._aa.deinit(); 478 const a = self._aa.allocator(); 479 480 self.worktree = dir; 481 errdefer self.worktree.close(); 482 self.gitdir = try self.worktree.openDir(".git", .{ .iterate = true }); 483 errdefer self.gitdir.close(); 484 485 const configcontent = try self.gitdir.readFileAlloc(a, "config", 10_000_000); 486 self.conf = try IniFile.parse(a, configcontent); 487 return self; 488 } 489 490 pub fn deinit(self: *GitRepository) void { 491 safeclose(&self.gitdir); 492 safeclose(&self.worktree); 493 self._aa.deinit(); 494 } 495 496 pub fn read_object_sha(self: GitRepository, a: std.mem.Allocator, sha: [20]u8) !GitObject { 497 var ref = std.fmt.bytesToHex(sha, .lower); 498 return self.read_object(a, &ref); 499 } 500 501 pub fn read_object( 502 self: GitRepository, 503 a: std.mem.Allocator, 504 ref: []const u8, 505 ) !GitObject { 506 if (ref.len != 40) { 507 return error.InvalidArgs; 508 } 509 const path = try std.fs.path.join(a, &.{ "objects", ref[0..2], ref[2..] }); 510 defer a.free(path); 511 var file = try self.gitdir.openFile(path, .{}); 512 errdefer file.close(); 513 const file_reader = file.reader(); 514 var decomp = std.compress.zlib.decompressor(file_reader); 515 const decomp_reader = decomp.reader(); 516 var sha1 = std.crypto.hash.Sha1.init(.{}); 517 518 const thehead = try decomp_reader.readUntilDelimiterAlloc(a, '\x00', 1024); 519 defer a.free(thehead); 520 sha1.update(thehead); 521 sha1.update(&.{'\x00'}); 522 var spl = std.mem.splitScalar(u8, thehead, ' '); 523 const kind_str = spl.first(); 524 const len_str = spl.rest(); 525 const kind = try enumFromString(kind_str, ObjectKind); 526 const len = try std.fmt.parseInt(u64, len_str, 10); 527 var expected_sha1: [20]u8 = undefined; 528 _ = try std.fmt.hexToBytes(&expected_sha1, ref); 529 return .{ 530 .kind = kind, 531 .len = len, 532 .sha1 = expected_sha1, 533 ._file = file, 534 ._decomp = decomp, 535 ._sha1 = sha1, 536 ._read = 0, 537 }; 538 } 539 540 // Caller must free the result 541 pub fn makeTempFilePath(self: GitRepository, a: std.mem.Allocator) ![]const u8 { 542 try self.gitdir.makePath("tmp"); 543 var rndm = prng.random(); 544 const tmpfilename = try std.fmt.allocPrint(a, "wyag-{}", .{rndm.int(u16)}); 545 defer a.free(tmpfilename); 546 return try std.fs.path.join(a, &.{ "tmp", tmpfilename }); 547 } 548 549 // Caller owns the response 550 pub fn write_object(self: GitRepository, ca: std.mem.Allocator, len: u64, reader: anytype, kind: ObjectKind, write: bool) ![]const u8 { 551 var aa = std.heap.ArenaAllocator.init(ca); // 3 lines saves many little 'free' calls through this function. 552 defer aa.deinit(); 553 const a = aa.allocator(); 554 // Write the stuff to a temporary file and calculate the hash 555 var sha1 = std.crypto.hash.Sha1.init(.{}); 556 const tmpfilepath = try self.makeTempFilePath(a); 557 defer a.free(tmpfilepath); 558 defer self.gitdir.deleteFile(tmpfilepath) catch {}; // not much we can do about this. 559 { 560 var tmpfile = try self.gitdir.createFile(tmpfilepath, .{}); 561 defer tmpfile.close(); 562 const tmpfile_writer = tmpfile.writer(); 563 var comp = try std.compress.zlib.compressor(tmpfile_writer, .{}); 564 const comp_writer = comp.writer(); 565 var comp_and_hash = std.compress.hashedWriter(comp_writer, &sha1); 566 const comp_and_hash_writer = comp_and_hash.writer(); 567 568 try std.fmt.format(comp_and_hash_writer, "{s} {}\x00", .{ @tagName(kind), len }); // header 569 var limited = std.io.limitedReader(reader, len); 570 var limited_reader = limited.reader(); 571 var buf = [_]u8{0} ** std.mem.page_size; 572 while (true) { 573 const sz = try limited_reader.read(&buf); 574 if (sz == 0) break; 575 try comp_and_hash_writer.writeAll(buf[0..sz]); 576 } 577 try comp.finish(); 578 } 579 580 const ref = try std.fmt.allocPrint(a, "{s}", .{std.fmt.fmtSliceHexLower(&sha1.finalResult())}); 581 // now rename it into place once you have the hash 582 if (write) { 583 const path = try std.fs.path.join(a, &.{ "objects", ref[0..2], ref[2..] }); 584 const dn = std.fs.path.dirname(path).?; 585 try self.gitdir.makePath(dn); 586 try self.gitdir.rename(tmpfilepath, path); 587 } 588 return try ca.dupe(u8, ref); 589 } 590 591 pub fn head(self: GitRepository, a: std.mem.Allocator) ![20]u8 { 592 const ref = try self.gitdir.readFileAlloc(a, "HEAD", 1024); 593 defer a.free(ref); 594 return try self.resolve(a, ref); 595 } 596 597 // Accepts 598 // a hexidecimal sha1 599 // or a branch name 600 // or a tag name 601 // or a ref string like "ref: refs/heads/blahh" 602 // and returns the binary format sha1 after resolving that reference, following indirections 603 // and tags 604 pub fn resolve(self: GitRepository, a: std.mem.Allocator, ref: []const u8) ![20]u8 { 605 const indirection_prefix = "ref: "; 606 const max_ref_file_size = 1024; 607 608 var trm = std.mem.trim(u8, ref, &std.ascii.whitespace); 609 var obj = self.read_object(a, trm) catch |e| switch (e) { 610 error.InvalidArgs, error.FileNotFound => { 611 if (std.mem.startsWith(u8, trm, indirection_prefix)) { 612 trm = trm[indirection_prefix.len..]; 613 } 614 if (std.mem.startsWith(u8, trm, "refs/")) { 615 const ref2 = try self.gitdir.readFileAlloc(a, trm, max_ref_file_size); 616 defer a.free(ref2); 617 return try self.resolve(a, ref2); 618 } else { 619 const path1 = try std.fs.path.join(a, &.{ "refs", "heads", trm }); 620 defer a.free(path1); 621 if (self.gitdir.readFileAlloc(a, path1, max_ref_file_size)) |ref2| { 622 defer a.free(ref2); 623 return try self.resolve(a, ref2); 624 } else |e2| { 625 if (e2 != error.FileNotFound) { 626 return e2; 627 } 628 const path2 = try std.fs.path.join(a, &.{ "refs", "tags", trm }); 629 defer a.free(path2); 630 const ref2 = try self.gitdir.readFileAlloc(a, path2, max_ref_file_size); 631 defer a.free(ref2); 632 return try self.resolve(a, ref2); 633 } 634 } 635 }, 636 else => return e, 637 }; 638 639 defer obj.deinit(); 640 if (obj.kind == .tag) { 641 var tag = try Tag.parse(a, obj.reader()); 642 defer tag.deinit(a); 643 return h2bref(tag.object); 644 } else { 645 return h2bref(trm); 646 } 647 } 648 649 test "resolve things" { 650 const a = std.testing.allocator; 651 var repo = try GitRepository.init(a, std.fs.cwd()); 652 defer repo.deinit(); 653 _ = try repo.resolve(a, "871ebbae8d66341336b5e29535300d7c284d0fa1"); 654 _ = try repo.resolve(a, "refs/heads/main"); 655 _ = try repo.resolve(a, "main"); 656 _ = try repo.resolve(a, "foo"); 657 _ = try repo.resolve(a, "ref: foo"); 658 try std.testing.expectError(error.FileNotFound, repo.resolve(a, "nope")); 659 } 660 661 // Caller owns the map and the keys to the map. 662 pub fn ref_list(self: GitRepository, a: std.mem.Allocator) !std.StringArrayHashMap([20]u8) { 663 var res = std.StringArrayHashMap([20]u8).init(a); 664 errdefer { 665 var it = res.iterator(); 666 while (it.next()) |e| { 667 a.free(e.key_ptr.*); 668 } 669 res.deinit(); 670 } 671 672 const ref_dir = try self.gitdir.openDir("refs", .{ .iterate = true }); 673 var walker = try ref_dir.walk(a); 674 defer walker.deinit(); 675 while (try walker.next()) |we| { 676 if (we.kind == .file) { 677 const key = try std.fs.path.join(a, &.{ "refs", we.path }); 678 errdefer a.free(key); 679 const indirect_ref = try std.fmt.allocPrint(a, "ref: {s}", .{key}); 680 defer a.free(indirect_ref); 681 const ref = try self.resolve(a, indirect_ref); 682 try res.put(key, ref); 683 } 684 } 685 const Srt = struct { 686 map: std.StringArrayHashMap([20]u8), 687 pub fn lessThan(slf: *@This(), ai: usize, bi: usize) bool { 688 const keys = slf.map.keys(); 689 const ka = keys[ai]; 690 const kb = keys[bi]; 691 return std.mem.lessThan(u8, ka, kb); 692 } 693 }; 694 var srt = Srt{ .map = res }; 695 res.sort(&srt); 696 return res; 697 } 698 }; 699 700 test "init repo" { 701 var gr = try GitRepository.init(std.testing.allocator, std.fs.cwd()); 702 defer gr.deinit(); 703 } 704 705 // takes ownership of "dir", the variable should not be used 706 // by any other code after calling this function. 707 fn repo_find(a: std.mem.Allocator, dir: Dir) !GitRepository { 708 const stat = dir.statFile(".git") catch |e| switch (e) { 709 error.FileNotFound => { 710 // try the parent 711 var parentdir = dir.openDir("..", .{ .iterate = true }) catch |e2| switch (e2) { 712 error.FileNotFound => return error.NoGitDirFound, 713 else => return e2, 714 }; 715 errdefer parentdir.close(); 716 return repo_find(a, parentdir); 717 }, 718 else => return e, 719 }; 720 if (stat.kind == .directory) { 721 return try GitRepository.init(a, dir); 722 } else { 723 return error.NoGitDirFound; 724 } 725 } 726 727 // test "repo_find" { 728 // const srcdir = try std.fs.cwd().openDir("src/foo/bar/baz", .{ .iterate = true }); 729 // var gr = try repo_find(std.testing.allocator, srcdir); 730 // defer gr.deinit(); 731 // } 732 733 fn safeclose(dir: *Dir) void { 734 if (std.fs.cwd().fd != dir.fd) { 735 dir.close(); 736 } 737 } 738 739 // makes an enum from a string. Ronseal. 740 fn enumFromString(str: []const u8, enum_type: type) !enum_type { 741 const ti = @typeInfo(enum_type); 742 inline for (ti.Enum.fields) |field| { 743 if (std.mem.eql(u8, str, field.name)) { 744 return @enumFromInt(field.value); 745 } 746 } else { 747 return error.InvalidEnum; 748 } 749 } 750 751 var prng: std.rand.DefaultPrng = undefined; 752 753 const ObjectKind = enum { 754 commit, 755 tree, 756 tag, 757 blob, 758 }; 759 760 const Kvlm = struct { 761 headers: std.StringArrayHashMapUnmanaged(std.ArrayListUnmanaged([]const u8)), 762 message: []const u8, 763 764 pub fn parse(a: std.mem.Allocator, z_reader: anytype) !Kvlm { 765 var cr = std.io.countingReader(z_reader); 766 var reader = cr.reader(); 767 768 var al = std.ArrayList(u8).init(a); 769 defer al.deinit(); 770 771 var headers = std.StringArrayHashMapUnmanaged(std.ArrayListUnmanaged([]const u8)){}; 772 errdefer { 773 var it = headers.iterator(); 774 while (it.next()) |i| { 775 a.free(i.key_ptr.*); 776 for (i.value_ptr.*.items) |ii| { 777 a.free(ii); 778 } 779 i.value_ptr.*.deinit(a); 780 } 781 headers.deinit(a); 782 } 783 784 headers_loop: while (true) { 785 const b = try reader.readByte(); 786 switch (b) { 787 '\n' => { 788 const b2 = try reader.readByte(); 789 if (b2 == ' ') { 790 try al.append(b); 791 continue :headers_loop; 792 } 793 794 var spl = std.mem.splitScalar(u8, al.items, ' '); 795 const key = try a.dupe(u8, spl.first()); 796 errdefer a.free(key); 797 const val = try a.dupe(u8, spl.rest()); 798 errdefer a.free(val); 799 var gpr = try headers.getOrPut(a, key); 800 if (gpr.found_existing) { 801 a.free(key); 802 try gpr.value_ptr.*.append(a, val); 803 } else { 804 gpr.value_ptr.* = .{}; 805 try gpr.value_ptr.append(a, val); 806 } 807 808 al.clearRetainingCapacity(); 809 if (b2 == '\n') { 810 break :headers_loop; 811 } else { 812 try al.append(b2); 813 } 814 }, 815 else => { 816 try al.append(b); 817 }, 818 } 819 } 820 821 // And the message is everything else 822 try reader.readAllArrayList(&al, 1_000_000); 823 const message = try al.toOwnedSlice(); 824 errdefer a.free(message); 825 826 return .{ 827 .headers = headers, 828 .message = message, 829 }; 830 } 831 832 pub fn deinit(self: *Kvlm, a: std.mem.Allocator) void { 833 var it = self.headers.iterator(); 834 while (it.next()) |i| { 835 a.free(i.key_ptr.*); 836 for (i.value_ptr.*.items) |ii| { 837 a.free(ii); 838 } 839 i.value_ptr.*.deinit(a); 840 } 841 self.headers.deinit(a); 842 a.free(self.message); 843 } 844 }; 845 846 const Commit = struct { 847 _kvlm: Kvlm, 848 tree: []const u8, 849 parents: std.ArrayListUnmanaged([]const u8), 850 author: []const u8, 851 committer: ?[]const u8, 852 gpgsig: ?[]const u8, 853 message: []const u8, 854 855 pub fn parse(a: std.mem.Allocator, z_reader: anytype) !Commit { 856 var kvlm = try Kvlm.parse(a, z_reader); 857 errdefer kvlm.deinit(a); 858 return .{ 859 ._kvlm = kvlm, 860 .tree = if (kvlm.headers.get("tree")) |tree| tree.items[0] else return error.InvalidCommit, 861 .parents = kvlm.headers.get("parent") orelse .{}, 862 .author = if (kvlm.headers.get("author")) |tree| tree.items[0] else return error.InvalidCommit, 863 .committer = if (kvlm.headers.get("committer")) |tree| tree.items[0] else null, 864 .gpgsig = if (kvlm.headers.get("gpgsig")) |tree| tree.items[0] else null, 865 .message = kvlm.message, 866 }; 867 } 868 869 pub fn deinit(self: *Commit, a: std.mem.Allocator) void { 870 self._kvlm.deinit(a); 871 } 872 }; 873 874 const Tag = struct { 875 _kvlm: Kvlm, 876 object: []const u8, 877 tag: []const u8, 878 message: []const u8, 879 pub fn parse(a: std.mem.Allocator, z_reader: anytype) !Tag { 880 var kvlm = try Kvlm.parse(a, z_reader); 881 errdefer kvlm.deinit(a); 882 return .{ 883 ._kvlm = kvlm, 884 .object = if (kvlm.headers.get("object")) |object| object.items[0] else return error.InvalidTag, 885 .tag = if (kvlm.headers.get("tag")) |tag| tag.items[0] else return error.InvalidTag, 886 .message = kvlm.message, 887 }; 888 } 889 pub fn deinit(self: *Tag, a: std.mem.Allocator) void { 890 self._kvlm.deinit(a); 891 } 892 }; 893 894 test "parse commit" { 895 const commit_str = 896 \\tree 29ff16c9c14e2652b22f8b78bb08a5a07930c147 897 \\parent 206941306e8a8af65b66eaaaea388a7ae24d49a0 898 \\author Thibault Polge <thibault@thb.lt> 1527025023 +0200 899 \\committer Thibault Polge <thibault@thb.lt> 1527025044 +0200 900 \\gpgsig -----BEGIN PGP SIGNATURE----- 901 \\ 902 \\ iQIzBAABCAAdFiEExwXquOM8bWb4Q2zVGxM2FxoLkGQFAlsEjZQACgkQGxM2FxoL 903 \\ kGQdcBAAqPP+ln4nGDd2gETXjvOpOxLzIMEw4A9gU6CzWzm+oB8mEIKyaH0UFIPh 904 \\ rNUZ1j7/ZGFNeBDtT55LPdPIQw4KKlcf6kC8MPWP3qSu3xHqx12C5zyai2duFZUU 905 \\ wqOt9iCFCscFQYqKs3xsHI+ncQb+PGjVZA8+jPw7nrPIkeSXQV2aZb1E68wa2YIL 906 \\ 3eYgTUKz34cB6tAq9YwHnZpyPx8UJCZGkshpJmgtZ3mCbtQaO17LoihnqPn4UOMr 907 \\ V75R/7FjSuPLS8NaZF4wfi52btXMSxO/u7GuoJkzJscP3p4qtwe6Rl9dc1XC8P7k 908 \\ NIbGZ5Yg5cEPcfmhgXFOhQZkD0yxcJqBUcoFpnp2vu5XJl2E5I/quIyVxUXi6O6c 909 \\ /obspcvace4wy8uO0bdVhc4nJ+Rla4InVSJaUaBeiHTW8kReSFYyMmDCzLjGIu1q 910 \\ doU61OM3Zv1ptsLu3gUE6GU27iWYj2RWN3e3HE4Sbd89IFwLXNdSuM0ifDLZk7AQ 911 \\ WBhRhipCCgZhkj9g2NEk7jRVslti1NdN5zoQLaJNqSwO1MtxTmJ15Ksk3QP6kfLB 912 \\ Q52UWybBzpaP9HEd4XnR+HuQ4k2K0ns2KgNImsNvIyFwbpMUyUWLMPimaV1DWUXo 913 \\ 5SBjDB/V/W2JBFR+XKHFJeFwYhj7DD/ocsGr4ZMx/lgc8rjIBkI= 914 \\ =lgTX 915 \\ -----END PGP SIGNATURE----- 916 \\ 917 \\Create first draft 918 ; 919 var fbs = std.io.fixedBufferStream(commit_str); 920 const rdr = fbs.reader(); 921 var commit = try Commit.parse(std.testing.allocator, rdr); 922 defer commit.deinit(std.testing.allocator); 923 } 924 925 pub const Tree = struct { 926 pub const Leaf = struct { 927 pub const FileType = enum { 928 file, 929 directory, 930 symlink, 931 submodule, 932 933 pub fn parse(filetype_str: []const u8) !FileType { 934 return if (std.mem.eql(u8, filetype_str, "04")) .directory else if (std.mem.eql(u8, filetype_str, "10")) .file else if (std.mem.eql(u8, filetype_str, "12")) .symlink else if (std.mem.eql(u8, filetype_str, "16")) .submodule else return error.BadFiletype; 935 } 936 pub fn format(self: FileType, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 937 try writer.writeAll(switch (self) { 938 .file => "10", 939 .directory => "04", 940 .symlink => "12", 941 .submodule => "16", 942 }); 943 } 944 }; 945 filetype: FileType, 946 mode: u32, 947 path: []const u8, 948 sha: [20]u8, 949 }; 950 aa: std.heap.ArenaAllocator, 951 leaves: std.ArrayListUnmanaged(Leaf) = .{}, 952 953 pub fn parse(ca: std.mem.Allocator, reader: anytype) !Tree { 954 var result: Tree = .{ 955 .aa = std.heap.ArenaAllocator.init(ca), 956 }; 957 errdefer result.aa.deinit(); 958 const a = result.aa.allocator(); 959 960 while (try reader.readUntilDelimiterOrEofAlloc(a, '\x00', 1024)) |entry| { 961 var spl = std.mem.splitScalar(u8, entry, ' '); 962 const mode_str_bare = spl.first(); 963 const mode_str = try std.fmt.allocPrint(a, "{s:0>6}", .{mode_str_bare}); 964 const filetype_str = mode_str[0..2]; 965 const filetype: Leaf.FileType = try Leaf.FileType.parse(filetype_str); 966 const path = spl.rest(); 967 var sha: [20]u8 = undefined; 968 _ = try reader.readAll(&sha); 969 try result.leaves.append(a, .{ 970 .filetype = filetype, 971 .mode = try std.fmt.parseInt(u32, mode_str[2..], 8), 972 .path = path, 973 .sha = sha, 974 }); 975 } 976 return result; 977 } 978 979 pub fn deinit(self: *Tree) void { 980 self.aa.deinit(); 981 } 982 }; 983 984 test "parse tree" { 985 const a = std.testing.allocator; 986 var fbs = std.io.fixedBufferStream(@embedFile("sample.tree")); 987 var tree = try Tree.parse(a, fbs.reader()); 988 defer tree.deinit(); 989 } 990 991 fn pump(reader: anytype, writer: anytype) !void { 992 var buf = [_]u8{0} ** std.mem.page_size; 993 while (true) { 994 const sz = try reader.read(&buf); 995 if (sz == 0) break; 996 try writer.writeAll(buf[0..sz]); 997 } 998 } 999 1000 fn h2bref(hex: []const u8) ![20]u8 { 1001 var res: [20]u8 = undefined; 1002 _ = try std.fmt.hexToBytes(&res, hex); 1003 return res; 1004 }