wyag

Write yourself a git
Log | Files | Refs | README

commit b7c4702aba7684a5380945dc54d40a44750fbf2d
parent 871ebbae8d66341336b5e29535300d7c284d0fa1
Author: Martin Ashby <martin@ashbysoft.com>
Date:   Fri, 30 Aug 2024 23:04:17 +0100

Better resolution of refs

Implement adding an extended tag

Diffstat:
Msrc/root.zig | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 139 insertions(+), 34 deletions(-)

diff --git a/src/root.zig b/src/root.zig @@ -45,8 +45,8 @@ pub fn doMain() !void { var checkout: argparse.Subcommand = .{ .parent = &ap, .description = "Checkout a commit inside of a directory", .name = "checkout" }; try ap.addSubcommand(&checkout); - var checkout_commit: argparse.Positional = .{ .name = "commit", .description = "The commit to checkout" }; - try checkout.addPositional(&checkout_commit); + var checkout_ref: argparse.Positional = .{ .name = "commit", .description = "The ref to checkout (commit id, tag, or branch name" }; + try checkout.addPositional(&checkout_ref); var checkout_directory: argparse.Positional = .{ .name = "directory", .description = "The directory to checkout into" }; try checkout.addPositional(&checkout_directory); @@ -59,7 +59,7 @@ pub fn doMain() !void { try tag.addPositional(&tag_name); var tag_object: argparse.Positional = .{ .name = "object", .description = "the object to add a tag for" }; try tag.addPositional(&tag_object); - var tag_kind: argparse.Flag = .{ .long = "add", .short = "a", .description = "whether to create a tag object", .hasarg = false }; + var tag_kind: argparse.Flag = .{ .long = "annotated", .short = "a", .description = "whether to create a tag object", .hasarg = false }; try tag.addFlag(&tag_kind); if (!try ap.parseOrHelp()) { @@ -105,7 +105,7 @@ pub fn doMain() !void { std.log.err("No commit supplied to ls-tree", .{}); } } else if (checkout.wasExecuted) { - if (checkout_commit.value) |commit_ref| { + if (checkout_ref.value) |commit_ref| { if (checkout_directory.value) |directory| { try doCheckout(a, std.fs.cwd(), commit_ref, directory); } else { @@ -118,7 +118,7 @@ pub fn doMain() !void { try doShowRef(a, std.fs.cwd(), std.io.getStdOut().writer()); } else if (tag.wasExecuted) { if (tag_name.value) |tagname| { - try doAddTag(a, std.fs.cwd(), std.io.getStdOut().writer(), tagname, tag_object.value); + try doAddTag(a, std.fs.cwd(), tagname, tag_object.value); } else { try doListTags(a, std.fs.cwd(), std.io.getStdOut().writer()); } @@ -142,21 +142,41 @@ fn doListTags(a: std.mem.Allocator, dir: std.fs.Dir, writer: anytype) !void { refs.deinit(); } for (refs.keys()) |k| { - // tags/ - const tags_prefix = "tags/"; + const tags_prefix = "refs/tags/"; if (std.mem.startsWith(u8, k, tags_prefix)) { try std.fmt.format(writer, "{s}\n", .{k[tags_prefix.len..]}); } } } -fn doAddTag(a: std.mem.Allocator, dir: std.fs.Dir, writer: anytype, tagname: []const u8, tagobj: ?[]const u8) !void { - _ = a; - _ = dir; - _ = writer; - _ = tagname; - _ = tagobj; - return error.Unimplemented; +fn doAddTag(a: std.mem.Allocator, dir: std.fs.Dir, tagname: []const u8, tagobj: ?[]const u8) !void { + var repo = try repo_find(a, dir); + defer repo.deinit(); + const ref: [20]u8 = + if (tagobj) |to| + try repo.resolve(a, to) + else + try repo.head(a); + const ref_str = std.fmt.bytesToHex(ref, .lower); + var al = std.ArrayList(u8).init(a); + defer al.deinit(); + const writer = al.writer(); + try std.fmt.format(writer, "object {s}\n", .{&ref_str}); + try std.fmt.format(writer, "type tag\n", .{}); + try std.fmt.format(writer, "tag {s}\n", .{tagname}); + try std.fmt.format(writer, "tagger \"Martin Ashby\"\n", .{}); + try std.fmt.format(writer, "\n{s}\n", .{"Foo Message"}); + var fbs = std.io.fixedBufferStream(al.items); + const res = try repo.write_object(a, al.items.len, fbs.reader(), .tag, true); + defer a.free(res); + + const tagfilepath = try std.fs.path.join(a, &.{ "refs", "tags", tagname }); + defer a.free(tagfilepath); + var tagfile = try repo.gitdir.createFile(tagfilepath, .{}); + defer tagfile.close(); + var wtr = tagfile.writer(); + try wtr.writeAll(res); + try wtr.writeByte('\n'); } fn doShowRef(a: std.mem.Allocator, dir: std.fs.Dir, writer: anytype) !void { @@ -172,16 +192,17 @@ fn doShowRef(a: std.mem.Allocator, dir: std.fs.Dir, writer: anytype) !void { } var it = refs.iterator(); while (it.next()) |e| { - try std.fmt.format(writer, "{} refs/{s}\n", .{ std.fmt.fmtSliceHexLower(&(e.value_ptr.*)), e.key_ptr.* }); + try std.fmt.format(writer, "{} {s}\n", .{ std.fmt.fmtSliceHexLower(&(e.value_ptr.*)), e.key_ptr.* }); } } -fn doCheckout(a: std.mem.Allocator, dir: std.fs.Dir, commit_ref: []const u8, checkout_directory: []const u8) !void { +fn doCheckout(a: std.mem.Allocator, dir: std.fs.Dir, checkout_ref: []const u8, checkout_directory: []const u8) !void { var repo = try repo_find(a, dir); defer repo.deinit(); var checkoutDir = try dir.makeOpenPath(checkout_directory, .{ .iterate = true }); defer checkoutDir.close(); - var go = try repo.read_object(a, commit_ref); + const ref = try repo.resolve(a, checkout_ref); + var go = try repo.read_object_sha(a, ref); defer go.deinit(); if (go.kind != .commit) return error.NotACommit; var it = checkoutDir.iterate(); @@ -443,7 +464,6 @@ pub const GitRepository = struct { ref: []const u8, ) !GitObject { if (ref.len != 40) { - std.log.err("Invalid ref in read_object {s}", .{ref}); return error.InvalidArgs; } const path = try std.fs.path.join(a, &.{ "objects", ref[0..2], ref[2..] }); @@ -455,11 +475,11 @@ pub const GitRepository = struct { const decomp_reader = decomp.reader(); var sha1 = std.crypto.hash.Sha1.init(.{}); - const head = try decomp_reader.readUntilDelimiterAlloc(a, '\x00', 1024); - defer a.free(head); - sha1.update(head); + const thehead = try decomp_reader.readUntilDelimiterAlloc(a, '\x00', 1024); + defer a.free(thehead); + sha1.update(thehead); sha1.update(&.{'\x00'}); - var spl = std.mem.splitScalar(u8, head, ' '); + var spl = std.mem.splitScalar(u8, thehead, ' '); const kind_str = spl.first(); const len_str = spl.rest(); const kind = try enumFromString(kind_str, ObjectKind); @@ -491,6 +511,7 @@ pub const GitRepository = struct { defer self.gitdir.deleteFile(tmpfilepath) catch {}; // not much we can do about this. { var tmpfile = try self.gitdir.createFile(tmpfilepath, .{}); + defer tmpfile.close(); const tmpfile_writer = tmpfile.writer(); var comp = try std.compress.zlib.compressor(tmpfile_writer, .{}); const comp_writer = comp.writer(); @@ -513,25 +534,83 @@ pub const GitRepository = struct { // now rename it into place once you have the hash if (write) { const path = try std.fs.path.join(a, &.{ "objects", ref[0..2], ref[2..] }); + const dn = std.fs.path.dirname(path).?; + try self.gitdir.makePath(dn); try self.gitdir.rename(tmpfilepath, path); } return try ca.dupe(u8, ref); } + pub fn head(self: GitRepository, a: std.mem.Allocator) ![20]u8 { + const ref = try self.gitdir.readFileAlloc(a, "HEAD", 1024); + defer a.free(ref); + return try self.resolve(a, ref); + } + + // Accepts + // a hexidecimal sha1 + // or a branch name + // or a tag name + // or a ref string like "ref: refs/heads/blahh" + // and returns the binary format sha1 after resolving that reference, following indirections + // and tags pub fn resolve(self: GitRepository, a: std.mem.Allocator, ref: []const u8) ![20]u8 { - const ref_file_path = try std.fs.path.join(a, &.{ "refs", ref }); - defer a.free(ref_file_path); - const ref_file_content = try self.gitdir.readFileAlloc(a, ref_file_path, 1024); - defer a.free(ref_file_content); - if (std.mem.startsWith(u8, ref_file_content, "ref: ")) { - return try resolve(self, a, ref_file_content[5..]); + const indirection_prefix = "ref: "; + const max_ref_file_size = 1024; + + var trm = std.mem.trim(u8, ref, &std.ascii.whitespace); + var obj = self.read_object(a, trm) catch |e| switch (e) { + error.InvalidArgs, error.FileNotFound => { + if (std.mem.startsWith(u8, trm, indirection_prefix)) { + trm = trm[indirection_prefix.len..]; + } + if (std.mem.startsWith(u8, trm, "refs/")) { + const ref2 = try self.gitdir.readFileAlloc(a, trm, max_ref_file_size); + defer a.free(ref2); + return try self.resolve(a, ref2); + } else { + const path1 = try std.fs.path.join(a, &.{ "refs", "heads", trm }); + defer a.free(path1); + if (self.gitdir.readFileAlloc(a, path1, max_ref_file_size)) |ref2| { + defer a.free(ref2); + return try self.resolve(a, ref2); + } else |e2| { + if (e2 != error.FileNotFound) { + return e2; + } + const path2 = try std.fs.path.join(a, &.{ "refs", "tags", trm }); + defer a.free(path2); + const ref2 = try self.gitdir.readFileAlloc(a, path2, max_ref_file_size); + defer a.free(ref2); + return try self.resolve(a, ref2); + } + } + }, + else => return e, + }; + + defer obj.deinit(); + if (obj.kind == .tag) { + var tag = try Tag.parse(a, obj.reader()); + defer tag.deinit(a); + return h2bref(tag.object); } else { - var res: [20]u8 = undefined; - _ = try std.fmt.hexToBytes(&res, std.mem.trim(u8, ref_file_content, &std.ascii.whitespace)); - return res; + return h2bref(trm); } } + test "resolve things" { + const a = std.testing.allocator; + var repo = try GitRepository.init(a, std.fs.cwd()); + defer repo.deinit(); + _ = try repo.resolve(a, "871ebbae8d66341336b5e29535300d7c284d0fa1"); + _ = try repo.resolve(a, "refs/heads/main"); + _ = try repo.resolve(a, "main"); + _ = try repo.resolve(a, "foo"); + _ = try repo.resolve(a, "ref: foo"); + try std.testing.expectError(error.FileNotFound, repo.resolve(a, "nope")); + } + // Caller owns the map and the keys to the map. pub fn ref_list(self: GitRepository, a: std.mem.Allocator) !std.StringArrayHashMap([20]u8) { var res = std.StringArrayHashMap([20]u8).init(a); @@ -548,9 +627,11 @@ pub const GitRepository = struct { defer walker.deinit(); while (try walker.next()) |we| { if (we.kind == .file) { - const ref = try self.resolve(a, we.path); - const key = try a.dupe(u8, we.path); + const key = try std.fs.path.join(a, &.{ "refs", we.path }); errdefer a.free(key); + const indirect_ref = try std.fmt.allocPrint(a, "ref: {s}", .{key}); + defer a.free(indirect_ref); + const ref = try self.resolve(a, indirect_ref); try res.put(key, ref); } } @@ -743,7 +824,25 @@ const Commit = struct { } }; -const Tag = Commit; // same structure and everything +const Tag = struct { + _kvlm: Kvlm, + object: []const u8, + tag: []const u8, + message: []const u8, + pub fn parse(a: std.mem.Allocator, z_reader: anytype) !Tag { + var kvlm = try Kvlm.parse(a, z_reader); + errdefer kvlm.deinit(a); + return .{ + ._kvlm = kvlm, + .object = if (kvlm.headers.get("object")) |object| object.items[0] else return error.InvalidTag, + .tag = if (kvlm.headers.get("tag")) |tag| tag.items[0] else return error.InvalidTag, + .message = kvlm.message, + }; + } + pub fn deinit(self: *Tag, a: std.mem.Allocator) void { + self._kvlm.deinit(a); + } +}; test "parse commit" { const commit_str = @@ -850,3 +949,9 @@ fn pump(reader: anytype, writer: anytype) !void { try writer.writeAll(buf[0..sz]); } } + +fn h2bref(hex: []const u8) ![20]u8 { + var res: [20]u8 = undefined; + _ = try std.fmt.hexToBytes(&res, hex); + return res; +}