commit 104c4784208a32b03cdc2bafaa61eeeb8e788f1c
parent 26a70bb0dd29c550f1f4e6f0c8e8a69ec565615e
Author: Martin Ashby <martin@ashbysoft.com>
Date: Sun, 11 Aug 2024 22:18:09 +0100
wyag 3.2 "init" command complete
Diffstat:
M | src/argparse.zig | | | 160 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- |
M | src/inifile.zig | | | 2 | +- |
M | src/root.zig | | | 120 | ++++++++++++++++++++++++++++++++++++++++--------------------------------------- |
3 files changed, 208 insertions(+), 74 deletions(-)
diff --git a/src/argparse.zig b/src/argparse.zig
@@ -1,8 +1,24 @@
const std = @import("std");
-const Self = @This();
+const ArgParse = @This();
-const ArgParse = Self;
+pub const Subcommand = struct {
+ parent: *ArgParse,
+ name: []const u8,
+ description: []const u8,
+ positionals: std.ArrayListUnmanaged(*Positional) = .{},
+ flags: std.ArrayListUnmanaged(*Flag) = .{},
+
+ wasExecuted: bool = false,
+
+ pub fn addPositional(self: *Subcommand, pos: *Positional) !void {
+ try self.positionals.append(self.parent.aa.allocator(), pos);
+ }
+
+ pub fn addFlag(self: *Subcommand, flag: *Flag) !void {
+ try self.flags.append(self.parent.aa.allocator(), flag);
+ }
+};
pub const Positional = struct {
name: []const u8,
@@ -26,11 +42,12 @@ pub const Flag = struct {
aa: std.heap.ArenaAllocator,
progname: []const u8,
description: []const u8,
+subcommands: std.ArrayListUnmanaged(*Subcommand) = .{},
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 {
+pub fn init(ca: std.mem.Allocator, progname: []const u8, description: []const u8) ArgParse {
return .{
.aa = std.heap.ArenaAllocator.init(ca),
.progname = progname,
@@ -38,20 +55,24 @@ pub fn init(ca: std.mem.Allocator, progname: []const u8, description: []const u8
};
}
-pub fn deinit(self: *Self) void {
+pub fn deinit(self: *ArgParse) void {
self.aa.deinit();
}
-pub fn addPositional(self: *Self, pos: *Positional) !void {
+pub fn addSubcommand(self: *ArgParse, subcommand: *Subcommand) !void {
+ try self.subcommands.append(self.aa.allocator(), subcommand);
+}
+
+pub fn addPositional(self: *ArgParse, pos: *Positional) !void {
try self.positionals.append(self.aa.allocator(), pos);
}
-pub fn addFlag(self: *Self, flag: *Flag) !void {
+pub fn addFlag(self: *ArgParse, 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 {
+pub fn parseOrHelp(self: *ArgParse) !bool {
self.parse() catch |e| {
switch (e) {
error.HelpWanted => {
@@ -68,13 +89,13 @@ pub fn parseOrHelp(self: *Self) !bool {
return true;
}
-pub fn parse(self: *Self) !void {
+pub fn parse(self: *ArgParse) !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 {
+pub fn help(self: *ArgParse, writer: anytype) !void {
// Add the default help argument
var helpflag: Flag = .{
.long = "help",
@@ -129,10 +150,11 @@ fn writeFlagsLongShort(flag: *Flag, writer: anytype) !void {
}
}
-fn parseInternal(self: *Self, it: anytype) !void {
+fn parseInternal(self: *ArgParse, it: anytype) !void {
var pos_i: usize = 0;
+ var subcommand: ?*Subcommand = null;
if (!it.skip()) return error.InvalidArgs;
- while (it.next()) |nxt| {
+ lp: while (it.next()) |nxt| {
if (std.mem.startsWith(u8, nxt, "-")) {
var flag = nxt[1..];
const longflag = std.mem.startsWith(u8, nxt, "--");
@@ -145,7 +167,8 @@ fn parseInternal(self: *Self, it: anytype) !void {
return error.HelpWanted;
}
- for (self.flags.items) |aflag| {
+ const flags: []*Flag = if (subcommand) |sc| sc.flags.items else self.flags.items;
+ for (flags) |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) {
@@ -170,8 +193,20 @@ fn parseInternal(self: *Self, it: anytype) !void {
return error.InvalidArgs;
}
} else {
- if (pos_i < self.positionals.items.len) {
- self.positionals.items[pos_i].value = try self.aa.allocator().dupe(u8, nxt);
+ // Check for a matching subcommand to step into
+ if (pos_i == 0 and subcommand == null) {
+ for (self.subcommands.items) |sc| {
+ if (std.mem.eql(u8, sc.name, nxt)) {
+ sc.wasExecuted = true;
+ subcommand = sc;
+ continue :lp;
+ }
+ }
+ }
+
+ const positionals: []*Positional = if (subcommand) |sc| sc.positionals.items else self.positionals.items;
+ if (pos_i < positionals.len) {
+ positionals[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));
@@ -190,6 +225,26 @@ fn parseInternal(self: *Self, it: anytype) !void {
}
}
+pub fn reset(self: *ArgParse) void {
+ for (self.flags.items) |flag| {
+ flag.waspresent = false;
+ flag.argvalue = null;
+ }
+ for (self.positionals.items) |pos| {
+ pos.value = null;
+ }
+ for (self.subcommands.items) |sc| {
+ sc.wasExecuted = false;
+ for (sc.flags.items) |flag| {
+ flag.waspresent = false;
+ flag.argvalue = null;
+ }
+ for (sc.positionals.items) |pos| {
+ pos.value = null;
+ }
+ }
+}
+
const TestArgs = struct {
args: []const []const u8,
pos: usize = 0,
@@ -506,3 +561,80 @@ test "helpwanted" {
var ta2: TestArgs = .{ .args = &.{ "prog", "-h" } };
try std.testing.expectError(error.HelpWanted, ap.parseInternal(&ta2));
}
+
+test "subcommand" {
+ var ap = ArgParse.init(std.testing.allocator, "foo", "foo");
+ defer ap.deinit();
+ var sc1: Subcommand = .{
+ .parent = &ap,
+ .name = "bar",
+ .description = "Do the bar thing",
+ };
+ try ap.addSubcommand(&sc1);
+ var ta: TestArgs = .{ .args = &.{ "prog", "bar" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(sc1.wasExecuted);
+}
+
+test "subcommand with flag and positional" {
+ var ap = ArgParse.init(std.testing.allocator, "foo", "foo");
+ defer ap.deinit();
+
+ var f1: Flag = .{
+ .long = "baz",
+ .hasarg = false,
+ .description = "baz flag",
+ };
+ try ap.addFlag(&f1);
+ var p1: Positional = .{
+ .name = "bam",
+ .description = "do things to bam",
+ };
+ try ap.addPositional(&p1);
+
+ var sc1: Subcommand = .{
+ .parent = &ap,
+ .name = "bar",
+ .description = "Do the bar thing",
+ };
+ var scf1: Flag = .{
+ .long = "barflag1",
+ .hasarg = false,
+ .description = "barflag1",
+ };
+ try sc1.addFlag(&scf1);
+ var scp1: Positional = .{
+ .name = "barpos1",
+ .description = "do bar to this thing",
+ };
+ try sc1.addPositional(&scp1);
+ try ap.addSubcommand(&sc1);
+
+ var ta: TestArgs = .{ .args = &.{"prog"} };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(!sc1.wasExecuted);
+ try std.testing.expect(!f1.waspresent);
+ try std.testing.expect(p1.value == null);
+ try std.testing.expect(!scf1.waspresent);
+ try std.testing.expect(scp1.value == null);
+
+ ap.reset();
+ ta = .{ .args = &.{ "prog", "--baz", "yoyoyo" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(!sc1.wasExecuted);
+ try std.testing.expect(f1.waspresent);
+ try std.testing.expectEqualStrings("yoyoyo", p1.value.?);
+ try std.testing.expect(!scf1.waspresent);
+ try std.testing.expect(scp1.value == null);
+
+ ap.reset();
+ ta = .{ .args = &.{ "prog", "bar", "yoyoyo", "--barflag1" } };
+ try ap.parseInternal(&ta);
+ try std.testing.expect(sc1.wasExecuted);
+ try std.testing.expect(!f1.waspresent);
+ try std.testing.expect(p1.value == null);
+ try std.testing.expect(scf1.waspresent);
+ try std.testing.expectEqualStrings("yoyoyo", scp1.value.?);
+}
+
+// TODO extend help with subcommands.
diff --git a/src/inifile.zig b/src/inifile.zig
@@ -31,7 +31,7 @@ pub fn parse(ca: std.mem.Allocator, content: []const u8) !IniFile {
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(), " "));
+ const value = std.mem.trim(u8, it2.rest(), " ");
try self._hmValues.put(a, key, value);
},
}
diff --git a/src/root.zig b/src/root.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const argparse = @import("argparse.zig");
const IniFile = @import("inifile.zig");
+const Dir = std.fs.Dir;
pub fn doMain() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@@ -8,90 +9,91 @@ pub fn doMain() !void {
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);
+ var init: argparse.Subcommand = .{ .parent = &ap, .description = "Initialize a new git repository", .name = "init" };
+ try ap.addSubcommand(&init);
+ var init_path: argparse.Positional = .{ .name = "path", .description = "The directory in which to make a git repository" };
+ try init.addPositional(&init_path);
+
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?
-
+ if (init.wasExecuted) {
+ if (init_path.value) |path| {
+ var repo = try repo_create(a, path);
+ defer repo.deinit();
+ } else {
+ std.log.err("No 'path' provided to init", .{});
+ return error.InvalidArgs;
+ }
} else {
- std.log.err("Unsupported sub-command {s}, have you tried implementing it yourself?", .{cmd});
+ if (ap.excess.items.len > 0) {
+ std.log.err("Unsupported sub-command {s}, have you tried implementing it yourself?", .{ap.excess.items[0]});
+ } else {
+ std.log.err("No sub-command requested", .{});
+ }
+ return error.InvalidArgs;
}
}
pub const GitRepository = struct {
- worktree: []const u8,
- gitdir: []const u8,
+ worktree: Dir,
+ gitdir: Dir,
conf: IniFile,
_aa: std.heap.ArenaAllocator,
- pub fn init(ca: std.mem.Allocator, path: []const u8, force: bool) !GitRepository {
+ pub fn init(ca: std.mem.Allocator, path: []const u8) !GitRepository {
var self: GitRepository = undefined;
self._aa = std.heap.ArenaAllocator.init(ca);
- errdefer self.deinit();
+ errdefer self._aa.deinit();
+ const a = self._aa.allocator();
- self.worktree = path;
+ self.worktree = try std.fs.cwd().openDir(path, .{ .iterate = true });
+ errdefer self.worktree.close();
+ self.gitdir = try self.worktree.openDir(".git", .{ .iterate = true });
+ errdefer self.gitdir.close();
- const a = self._aa.allocator();
- const cwd = std.fs.cwd();
- const gitdirpath = try std.fs.path.join(a, &.{ path, ".git" });
- if (!force) {
- var gitdir = cwd.openDir(gitdirpath, .{}) catch |e| switch (e) {
- error.FileNotFound, error.NotDir => return error.NotAGitRepo,
- else => return error.FsError,
- };
- defer gitdir.close();
- }
- self.gitdir = gitdirpath;
- const gitconfigpath = try std.fs.path.join(a, &.{ gitdirpath, "config" });
- const configcontent = try cwd.readFileAlloc(a, gitconfigpath, 10_000_000);
- defer a.free(configcontent);
+ const configcontent = try self.gitdir.readFileAlloc(a, "config", 10_000_000);
self.conf = try IniFile.parse(a, configcontent);
return self;
}
pub fn deinit(self: *GitRepository) void {
+ self.gitdir.close();
+ self.worktree.close();
self._aa.deinit();
}
-
- /// Compute path under repo's gitdir.
- fn repo_path(self: GitRepository, path: []const u8) ![]const u8 {
- return try std.fs.path.join(self._aa.allocator(), &.{ self.gitdir, path });
- }
-
- /// Same as repo_path, but create dirname(*path) if absent. For
- /// example, repo_file(r, \"refs\", \"remotes\", \"origin\", \"HEAD\") will create
- /// .git/refs/remotes/origin."""
- fn repo_file(self: GitRepository, path: []const u8, mkdir: bool) !?[]const u8 {
- const dirname = std.fs.path.dirname(path) orelse return error.EmptyPath;
- if (try self.repo_dir(dirname, mkdir)) |_| {
- return try repo_path(self, path);
- } else {
- return null;
- }
- }
-
- /// Same as repo_path, but mkdir *path if absent if mkdir.
- fn repo_dir(self: GitRepository, path: []const u8, mkdir: bool) !?[]const u8 {
- const p = try self.repo_path(path);
- if (mkdir) {
- try std.fs.cwd().makePath(p);
- return p;
- } else {
- const dir = std.fs.cwd().openDir(p, .{}) catch |e| switch (e) {
- error.FileNotFound => return null,
- else => return e,
- };
- dir.close();
- return p;
- }
- }
};
test "init repo" {
var gr = try GitRepository.init(std.testing.allocator, ".", false);
defer gr.deinit();
}
+
+fn repo_create(ca: std.mem.Allocator, path: []const u8) !GitRepository {
+ {
+ var aa = std.heap.ArenaAllocator.init(ca);
+ defer aa.deinit();
+ const a = aa.allocator();
+ const cwd = std.fs.cwd();
+ var worktree = try cwd.makeOpenPath(path, .{});
+ defer worktree.close();
+ var gitdir = try worktree.makeOpenPath(".git", .{});
+ defer gitdir.close();
+ try gitdir.makePath("branches");
+ try gitdir.makePath("objects");
+ try gitdir.makePath(try std.fs.path.join(a, &.{ "refs", "tags" }));
+ try gitdir.makePath(try std.fs.path.join(a, &.{ "refs", "heads" }));
+ var headfile = try gitdir.createFile("HEAD", .{});
+ defer headfile.close();
+ try headfile.writeAll("ref: refs/heads/main\n");
+ var configfile = try gitdir.createFile("config", .{});
+ defer configfile.close();
+ try configfile.writeAll(
+ \\[core]
+ \\ repositoryformatversion = 0
+ \\ filemode = false
+ \\ bare = false
+ );
+ }
+ return try GitRepository.init(ca, path);
+}