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:
M | src/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;
+}