wyag

Write yourself a git
Log | Files | Refs | README

commit 06bbaa657da60958a0ac2629b36a72ad6beaed53
Author: Martin Ashby <martin@ashbysoft.com>
Date:   Sat, 10 Aug 2024 22:44:15 +0100

Initial

Diffstat:
A.gitignore | 2++
AREADME.md | 6++++++
Abuild.zig | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild.zig.zon | 13+++++++++++++
Asrc/argparse.zig | 508+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/inifile.zig | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.zig | 6++++++
Asrc/root.zig | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(); + } +};