wyag

Write yourself a git
Log | Files | Refs | README

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 }