wyag

Write yourself a git
Log | Files | Refs | README

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:
Msrc/argparse.zig | 160++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/inifile.zig | 2+-
Msrc/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); +}