commit 06bbaa657da60958a0ac2629b36a72ad6beaed53
Author: Martin Ashby <martin@ashbysoft.com>
Date: Sat, 10 Aug 2024 22:44:15 +0100
Initial
Diffstat:
8 files changed, 737 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+zig-out
+.zig-cache
diff --git a/README.md b/README.md
@@ -0,0 +1,5 @@
+# wyag
+
+Write Yourself a Git
+
+Following along with [wyag](https://wyag.thb.lt/), but in Zig just because.
+\ No newline at end of file
diff --git a/build.zig b/build.zig
@@ -0,0 +1,53 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ const lib = b.addStaticLibrary(.{
+ .name = "wyag",
+ .root_source_file = b.path("src/root.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ b.installArtifact(lib);
+
+ const exe = b.addExecutable(.{
+ .name = "wyag",
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ b.installArtifact(exe);
+
+ const run_cmd = b.addRunArtifact(exe);
+
+ run_cmd.step.dependOn(b.getInstallStep());
+
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
+
+ const lib_unit_tests = b.addTest(.{
+ .root_source_file = b.path("src/root.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
+
+ const exe_unit_tests = b.addTest(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
+
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_lib_unit_tests.step);
+ test_step.dependOn(&run_exe_unit_tests.step);
+}
diff --git a/build.zig.zon b/build.zig.zon
@@ -0,0 +1,13 @@
+.{
+ .name = "wyag",
+ .version = "0.0.1",
+ .minimum_zig_version = "0.13.0",
+ .dependencies = .{
+ },
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ "README.md",
+ },
+}
diff --git a/src/argparse.zig b/src/argparse.zig
@@ -0,0 +1,508 @@
+const std = @import("std");
+
+const Self = @This();
+
+const ArgParse = Self;
+
+pub const Positional = struct {
+ name: []const u8,
+ description: []const u8,
+ default: ?[]const u8 = null,
+
+ value: ?[]const u8 = null,
+};
+
+pub const Flag = struct {
+ long: []const u8,
+ short: ?[]const u8 = null,
+ description: []const u8,
+ hasarg: bool,
+
+ waspresent: bool = false,
+ defaultargvalue: ?[]const u8 = null,
+ argvalue: ?[]const u8 = null,
+};
+
+aa: std.heap.ArenaAllocator,
+progname: []const u8,
+description: []const u8,
+positionals: std.ArrayListUnmanaged(*Positional) = .{},
+flags: std.ArrayListUnmanaged(*Flag) = .{},
+excess: std.ArrayListUnmanaged([]const u8) = .{},
+
+pub fn init(ca: std.mem.Allocator, progname: []const u8, description: []const u8) Self {
+ return .{
+ .aa = std.heap.ArenaAllocator.init(ca),
+ .progname = progname,
+ .description = description,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ self.aa.deinit();
+}
+
+pub fn addPositional(self: *Self, pos: *Positional) !void {
+ try self.positionals.append(self.aa.allocator(), pos);
+}
+
+pub fn addFlag(self: *Self, flag: *Flag) !void {
+ try self.flags.append(self.aa.allocator(), flag);
+}
+
+// Return false if args were not parsed (but it wasn't an error)
+pub fn parseOrHelp(self: *Self) !bool {
+ self.parse() catch |e| {
+ switch (e) {
+ error.HelpWanted => {
+ try self.help(std.io.getStdOut().writer());
+ return false;
+ },
+ error.InvalidArgs => {
+ try self.help(std.io.getStdErr().writer());
+ return e;
+ },
+ else => return e,
+ }
+ };
+ return true;
+}
+
+pub fn parse(self: *Self) !void {
+ var it = try std.process.argsWithAllocator(self.aa.allocator());
+ defer it.deinit();
+ try self.parseInternal(&it);
+}
+
+pub fn help(self: *Self, writer: anytype) !void {
+ // Add the default help argument
+ var helpflag: Flag = .{
+ .long = "help",
+ .short = "h",
+ .description = "Show this help",
+ .hasarg = false,
+ };
+ try self.flags.append(self.aa.allocator(), &helpflag);
+
+ try std.fmt.format(writer, "Usage: {s}", .{self.progname});
+ for (self.positionals.items) |pos| {
+ try std.fmt.format(writer, " [{s}]", .{pos.name});
+ }
+ try std.fmt.format(writer, "\n{s}\n", .{self.description});
+ for (self.positionals.items) |pos| {
+ try std.fmt.format(writer, " {s}: {s}", .{ pos.name, pos.description });
+ if (pos.default) |df| {
+ try std.fmt.format(writer, " (Default: {s})", .{df});
+ }
+ try writer.writeByte('\n');
+ }
+ try std.fmt.format(writer, "\nFlags:\n", .{});
+ var longestFlag: u64 = 0;
+ for (self.flags.items) |flag| {
+ var cw = std.io.countingWriter(std.io.null_writer);
+ try writeFlagsLongShort(flag, cw.writer());
+ longestFlag = @max(longestFlag, cw.bytes_written);
+ }
+ longestFlag += 3;
+ for (self.flags.items) |flag| {
+ var cw = std.io.countingWriter(writer);
+ const w = cw.writer();
+ try w.writeByteNTimes(' ', 2);
+ try writeFlagsLongShort(flag, w);
+ const pad = (longestFlag - cw.bytes_written);
+ try writer.writeByteNTimes(' ', pad);
+ try std.fmt.format(writer, "{s}", .{flag.description});
+ if (flag.defaultargvalue) |dav| {
+ try std.fmt.format(writer, " (Default: {s})", .{dav});
+ }
+ try writer.writeByte('\n');
+ }
+}
+
+fn writeFlagsLongShort(flag: *Flag, writer: anytype) !void {
+ if (flag.short) |short| {
+ try std.fmt.format(writer, "-{s},", .{short});
+ }
+ try std.fmt.format(writer, "--{s}", .{flag.long});
+ if (flag.hasarg) {
+ try std.fmt.format(writer, "=ARG", .{});
+ }
+}
+
+fn parseInternal(self: *Self, it: anytype) !void {
+ var pos_i: usize = 0;
+ if (!it.skip()) return error.InvalidArgs;
+ while (it.next()) |nxt| {
+ if (std.mem.startsWith(u8, nxt, "-")) {
+ var flag = nxt[1..];
+ const longflag = std.mem.startsWith(u8, nxt, "--");
+ if (longflag) {
+ flag = nxt[2..];
+ }
+ var spl = std.mem.splitScalar(u8, flag, '=');
+ const key = spl.first();
+ if (std.mem.eql(u8, key, "help") or std.mem.eql(u8, key, "h")) {
+ return error.HelpWanted;
+ }
+
+ for (self.flags.items) |aflag| {
+ const ismatch = if (longflag) std.mem.eql(u8, key, aflag.long) else if (aflag.short) |srt| std.mem.eql(u8, key, srt) else false;
+ if (ismatch) {
+ if (aflag.waspresent) {
+ return error.InvalidArgs;
+ }
+ aflag.waspresent = true;
+ if (aflag.hasarg) {
+ if (spl.rest().len > 0) {
+ aflag.argvalue = try self.aa.allocator().dupe(u8, spl.rest());
+ } else {
+ const val = it.next() orelse return error.InvalidArgs;
+ aflag.argvalue = try self.aa.allocator().dupe(u8, val);
+ }
+ } else {
+ if (spl.rest().len > 0) {
+ return error.InvalidArgs;
+ }
+ }
+ break;
+ }
+ } else {
+ return error.InvalidArgs;
+ }
+ } else {
+ if (pos_i < self.positionals.items.len) {
+ self.positionals.items[pos_i].value = try self.aa.allocator().dupe(u8, nxt);
+ pos_i += 1;
+ } else {
+ try self.excess.append(self.aa.allocator(), try self.aa.allocator().dupe(u8, nxt));
+ }
+ }
+ }
+ for (self.flags.items) |aflag| {
+ if (aflag.argvalue) |_| {} else if (aflag.defaultargvalue) |dav| {
+ aflag.argvalue = try self.aa.allocator().dupe(u8, dav);
+ }
+ }
+ for (self.positionals.items) |pos| {
+ if (pos.value) |_| {} else if (pos.default) |dav| {
+ pos.value = try self.aa.allocator().dupe(u8, dav);
+ }
+ }
+}
+
+const TestArgs = struct {
+ args: []const []const u8,
+ pos: usize = 0,
+ pub fn next(self: *TestArgs) ?[]const u8 {
+ if (self.pos < self.args.len) {
+ defer self.pos += 1;
+ return self.args[self.pos];
+ } else {
+ return null;
+ }
+ }
+ pub fn first(self: *TestArgs) []const u8 {
+ return self.next() orelse @panic("bad test args, no first");
+ }
+ pub fn skip(self: *TestArgs) bool {
+ if (self.next()) |_| {
+ return true;
+ } else {
+ return false;
+ }
+ }
+};
+
+test "flag not present" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = false,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "foobar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(!flag.waspresent);
+}
+
+test "flag with no arg" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = false,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+}
+
+test "flag with arg, but arg not supplied" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "flag with no arg, but an arg was supplie" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = false,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=foobar" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "flag with arg, separated by space" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag", "myvalue" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+ try std.testing.expectEqualStrings("myvalue", flag.argvalue.?);
+}
+
+test "flag with an arg, separated by an equals" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=myvalue" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+ try std.testing.expectEqualStrings("myvalue", flag.argvalue.?);
+}
+
+test "short flag" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "-m=myvalue" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(flag.waspresent);
+ try std.testing.expectEqualStrings("myvalue", flag.argvalue.?);
+}
+
+test "unexpected flag" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=myvalue" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "duplicate flag" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var flag: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .description = "Hello there",
+ .hasarg = true,
+ };
+ try ap.addFlag(&flag);
+ var ta: TestArgs = .{ .args = &.{ "prog", "--myflag=myvalue", "--myflag", "someothervalue" } };
+ try std.testing.expectError(error.InvalidArgs, ap.parseInternal(&ta));
+}
+
+test "postitional argument not supplied" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var ta: TestArgs = .{ .args = &.{"prog"} };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqual(null, pos.value);
+}
+
+test "postitional argument" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+}
+
+test "two postitional arguments, first is supplied" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument",
+ };
+ try ap.addPositional(&pos2);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+ try std.testing.expectEqual(null, pos2.value);
+}
+
+test "two positional arguments, both are supplied and some extras" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument",
+ };
+ try ap.addPositional(&pos2);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar", "baz", "bing", "bam" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+ try std.testing.expectEqualStrings("baz", pos2.value.?);
+ try std.testing.expectEqual(@as(usize, 2), ap.excess.items.len);
+ try std.testing.expectEqualStrings("bing", ap.excess.items[0]);
+ try std.testing.expectEqualStrings("bam", ap.excess.items[1]);
+}
+
+test "positional argument with a default" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument",
+ .default = "defaultbar",
+ };
+ try ap.addPositional(&pos2);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expectEqualStrings("bar", pos.value.?);
+ try std.testing.expectEqualStrings("defaultbar", pos2.value.?);
+}
+
+test "help" {
+ const a = std.testing.allocator;
+ var ap = ArgParse.init(a, "Myprog", "my program to do things");
+ defer ap.deinit();
+ var pos1: Positional = .{
+ .name = "foo",
+ .description = "A positional argument",
+ };
+ try ap.addPositional(&pos1);
+ var pos2: Positional = .{
+ .name = "foo2",
+ .description = "A second positional argument, with a default",
+ .default = "defaultbar",
+ };
+ try ap.addPositional(&pos2);
+ var flag1: Flag = .{
+ .long = "myflag",
+ .short = "m",
+ .hasarg = true,
+ .description = "My flag with an argument",
+ };
+ try ap.addFlag(&flag1);
+ var flag2: Flag = .{
+ .long = "twoflag",
+ .short = "t",
+ .hasarg = true,
+ .description = "My flag with an argument and a default",
+ .defaultargvalue = "twoflagdefault",
+ };
+ try ap.addFlag(&flag2);
+ var flag3: Flag = .{
+ .long = "threeflag",
+ .short = "tf",
+ .hasarg = false,
+ .description = "My flag with no argument",
+ };
+ try ap.addFlag(&flag3);
+
+ var out = std.ArrayList(u8).init(a);
+ defer out.deinit();
+ try ap.help(out.writer());
+ const expected =
+ \\Usage: Myprog [foo] [foo2]
+ \\my program to do things
+ \\ foo: A positional argument
+ \\ foo2: A second positional argument, with a default (Default: defaultbar)
+ \\
+ \\Flags:
+ \\ -m,--myflag=ARG My flag with an argument
+ \\ -t,--twoflag=ARG My flag with an argument and a default (Default: twoflagdefault)
+ \\ -tf,--threeflag My flag with no argument
+ \\ -h,--help Show this help
+ \\
+ ;
+ try std.testing.expectEqualStrings(expected, out.items);
+}
+
+test "helpwanted" {
+ var ap = ArgParse.init(std.testing.allocator, "foo", "foo");
+ defer ap.deinit();
+ var ta: TestArgs = .{ .args = &.{ "prog", "--help" } };
+ try std.testing.expectError(error.HelpWanted, ap.parseInternal(&ta));
+ var ta2: TestArgs = .{ .args = &.{ "prog", "-h" } };
+ try std.testing.expectError(error.HelpWanted, ap.parseInternal(&ta2));
+}
diff --git a/src/inifile.zig b/src/inifile.zig
@@ -0,0 +1,88 @@
+const std = @import("std");
+
+_aa: std.heap.ArenaAllocator,
+_hmValues: std.StringArrayHashMapUnmanaged([]const u8),
+
+const IniFile = @This();
+// TODO There are a lot more intriciacies of parsing ini files, including:
+// - comments at the end of lines
+// - quoted values
+// - escaped special characters
+// This implementation does not handle any of them.
+pub fn parse(ca: std.mem.Allocator, content: []const u8) !IniFile {
+ var self: IniFile = .{
+ ._aa = std.heap.ArenaAllocator.init(ca),
+ ._hmValues = .{},
+ };
+ errdefer self.deinit();
+ const a = self._aa.allocator();
+ var section: []const u8 = ""; // Sections are not nested, thankfully
+ var it = std.mem.splitScalar(u8, content, '\n');
+ lp: while (it.next()) |nxtraw| {
+ const nxt = std.mem.trimLeft(u8, nxtraw, " ");
+ if (nxt.len == 0) continue :lp;
+ switch (nxt[0]) {
+ '#', ';' => continue :lp,
+ '[' => {
+ section = std.mem.trim(u8, nxt, " []");
+ continue :lp;
+ },
+ else => {
+ var it2 = std.mem.splitScalar(u8, nxt, '=');
+ const dot = if (section.len > 0) "." else "";
+ const key = try std.fmt.allocPrint(a, "{s}{s}{s}", .{ section, dot, std.mem.trim(u8, it2.first(), " ") });
+ const value = try a.dupe(u8, std.mem.trim(u8, it2.rest(), " "));
+ try self._hmValues.put(a, key, value);
+ },
+ }
+ }
+ return self;
+}
+
+pub fn get(self: IniFile, key: []const u8) ?[]const u8 {
+ return self._hmValues.get(key);
+}
+
+pub fn deinit(self: *IniFile) void {
+ self._hmValues.deinit(self._aa.allocator());
+ self._aa.deinit();
+}
+
+test "basic ini file" {
+ const content =
+ \\#
+ \\# This is the config file, and
+ \\# a '#' or ';' character indicates
+ \\# a comment
+ \\#
+ \\
+ \\ somesetting = somevalue
+ \\
+ \\; core variables
+ \\[core]
+ \\ ; Don't trust file modes
+ \\ filemode = false
+ \\
+ \\; Our diff algorithm
+ \\[diff]
+ \\ external = /usr/local/bin/diff-wrapper
+ \\ renames = true
+ \\
+ \\; Proxy settings
+ \\[core]
+ \\ gitproxy=proxy-command for kernel.org
+ \\ gitproxy=default-proxy ; for all the rest
+ \\
+ \\; HTTP
+ \\[http]
+ \\ sslVerify
+ \\[http "https://weak.example.com"]
+ \\ sslVerify = false
+ \\ cookieFile = /tmp/cookie.txt
+ ;
+ var ini = try IniFile.parse(std.testing.allocator, content);
+ defer ini.deinit();
+ try std.testing.expectEqualStrings("somevalue", ini.get("somesetting").?);
+ try std.testing.expectEqualStrings("true", ini.get("diff.renames").?);
+ // try std.testing.expectEqualStrings("default-proxy", ini.get("core.gitproxy").?); // FIXME
+}
diff --git a/src/main.zig b/src/main.zig
@@ -0,0 +1,6 @@
+const std = @import("std");
+const root = @import("root.zig");
+
+pub fn main() !void {
+ try root.doMain();
+}
diff --git a/src/root.zig b/src/root.zig
@@ -0,0 +1,61 @@
+const std = @import("std");
+const argparse = @import("argparse.zig");
+
+pub fn doMain() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ defer _ = gpa.deinit();
+ const a = gpa.allocator();
+ var ap = argparse.init(a, "wyag", "Write Yourself A Git: a bad version of git for educational purposes");
+ defer ap.deinit();
+ var command: argparse.Positional = .{ .description = "The command to run", .name = "command" };
+ try ap.addPositional(&command);
+ if (!try ap.parseOrHelp()) {
+ return;
+ }
+ const cmd = command.value orelse return error.InvalidArgs;
+ if (std.mem.eql(u8, cmd, "init")) {
+ // So how do we do a git init?
+
+ } else {
+ std.log.err("Unsupported sub-command {s}, have you tried implementing it yourself?", .{cmd});
+ }
+}
+
+const GitConfig = struct {};
+
+pub const GitRepository = struct {
+ worktree: []const u8,
+ gitdir: []const u8,
+ conf: GitConfig,
+ _aa: std.heap.ArenaAllocator,
+
+ pub const InitError = error{
+ OutOfMemory,
+ NotAGitRepo,
+ FsError,
+ };
+
+ pub fn init(ca: std.mem.Allocator, path: []const u8, force: bool) InitError!GitRepository {
+ const aa = std.heap.ArenaAllocator.init(ca);
+ errdefer aa.deinit();
+ const a = aa.allocator();
+
+ const gitdirpath = try std.fs.path.join(a, &.{ path, ".git" });
+ if (!force) {
+ std.fs.cwd().openDir(gitdirpath, .{}) catch |e| switch (e) {
+ error.FileNotFound, error.NotDir => return error.NotAGitRepo,
+ else => error.FsError,
+ };
+ }
+
+ return .{
+ ._aa = aa,
+ .worktree = path,
+ .gitdir = gitdirpath,
+ };
+ }
+
+ pub fn deinit(self: *GitRepository) void {
+ self._aa.deinit();
+ }
+};