From 2642423b44e6205477a88ec1e2242c9b5ae07f16 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 19 Jul 2026 13:04:09 +0100 Subject: [PATCH 1/3] ADD: Configuration File Configuration File is now created. Currently is still not being used. Signed-off-by: Jim Fitzpatrick --- .../unreleased/Added-20260801-152458.yaml | 3 + .changie.yaml => .lchangie.yaml | 0 src/default_config.zon | 14 ++ src/defaults.zig | 5 + src/logging.zig | 32 ++++ src/main.zig | 69 +++------ src/root.zig | 138 +++++++++++++++++- 7 files changed, 210 insertions(+), 51 deletions(-) create mode 100644 .changes/unreleased/Added-20260801-152458.yaml rename .changie.yaml => .lchangie.yaml (100%) create mode 100644 src/default_config.zon create mode 100644 src/defaults.zig create mode 100644 src/logging.zig diff --git a/.changes/unreleased/Added-20260801-152458.yaml b/.changes/unreleased/Added-20260801-152458.yaml new file mode 100644 index 0000000..322e009 --- /dev/null +++ b/.changes/unreleased/Added-20260801-152458.yaml @@ -0,0 +1,3 @@ +kind: Added +body: A configuration file can now be configured to store a number of common setting. Create the configuration file using the `--init` flag. +time: 2026-08-01T15:24:58.621532877+01:00 diff --git a/.changie.yaml b/.lchangie.yaml similarity index 100% rename from .changie.yaml rename to .lchangie.yaml diff --git a/src/default_config.zon b/src/default_config.zon new file mode 100644 index 0000000..0c3e1e8 --- /dev/null +++ b/src/default_config.zon @@ -0,0 +1,14 @@ +.{ + //Default clone path + //Setting `GRAB_PATH` env var overrides + //.path = "path/to/source", + + //Default clone mode (.worktree or .standard) + .action = .worktree, + + //Create shallow clones by default + .shallow = false, + + //Default log level (.debug, .info, .warn, .error) + .log_level = .info, +} diff --git a/src/defaults.zig b/src/defaults.zig new file mode 100644 index 0000000..3634637 --- /dev/null +++ b/src/defaults.zig @@ -0,0 +1,5 @@ +const builtin = @import("builtin"); + +pub const log_level = if (builtin.mode == .Debug) .debug else .info; +pub const shallow = false; +pub const action = .worktree; diff --git a/src/logging.zig b/src/logging.zig new file mode 100644 index 0000000..f07e5db --- /dev/null +++ b/src/logging.zig @@ -0,0 +1,32 @@ +const std = @import("std"); + +const defaults = @import("defaults.zig"); + +pub const Level = enum { info, debug, @"error", warn }; +pub var log_level: std.log.Level = defaults.log_level; + +pub fn log( + comptime level: std.log.Level, + comptime scope: @EnumLiteral(), + comptime format: []const u8, + args: anytype, +) void { + const prefix = comptime blk: { + if (scope == .default) + break :blk "[" ++ level.asText() ++ "] "; + break :blk "[" ++ level.asText() ++ "][" ++ @tagName(scope) ++ "] "; + }; + + if (@intFromEnum(level) <= @intFromEnum(log_level)) { + std.debug.print(prefix ++ format ++ "\n", args); + } +} + +pub fn set_log_level(level: Level) void { + switch (level) { + .debug => log_level = .debug, + .@"error" => log_level = .err, + .info => log_level = .info, + .warn => log_level = .warn, + } +} diff --git a/src/main.zig b/src/main.zig index 33c74b3..2cff1e0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,32 +3,12 @@ const clap = @import("clap"); const grab = @import("grab"); const help = @import("help.zig"); -pub const Level = enum { info, debug, @"error", warn }; pub const std_options: std.Options = .{ // Keep compile-time logging permissive; runtime filter in `log`. .log_level = .debug, - .logFn = log, + .logFn = grab.logging.log, }; -pub var log_level: std.log.Level = .info; - -pub fn log( - comptime level: std.log.Level, - comptime scope: @EnumLiteral(), - comptime format: []const u8, - args: anytype, -) void { - const prefix = comptime blk: { - if (scope == .default) - break :blk "[" ++ level.asText() ++ "] "; - break :blk "[" ++ level.asText() ++ "][" ++ @tagName(scope) ++ "] "; - }; - - if (@intFromEnum(level) <= @intFromEnum(log_level)) { - std.debug.print(prefix ++ format ++ "\n", args); - } -} - pub fn main(init: std.process.Init) !void { const allocator = init.gpa; @@ -42,12 +22,13 @@ pub fn main(init: std.process.Init) !void { \\-r, --remote Add remote to existing repo. \\--log-level Set the log level. All logs are saved to file. Possible values are (debug, info, warn, error). Defualt level is info. \\--version Show program's version number and exit + \\--init Create a configuration file ); const parsers = comptime .{ .PATH = clap.parsers.string, .REPO = clap.parsers.string, - .LEVEL = clap.parsers.enumeration(Level), + .LEVEL = clap.parsers.enumeration(grab.logging.Level), }; var diag = clap.Diagnostic{}; @@ -61,6 +42,15 @@ pub fn main(init: std.process.Init) !void { }; defer res.deinit(); + // Load configuration first to ensure the correct log level is set. + var config = try grab.Configuration.init(init.io, init.gpa, init.minimal.environ); + defer config.deinit(allocator); + + // Set up logger + if (res.args.@"log-level") |l| { + grab.logging.set_log_level(l); + } + if (res.args.help != 0) return clap.helpToFile(init.io, .stderr(), clap.Help, ¶ms, .{}); if (res.args.version != 0) { @@ -69,20 +59,9 @@ pub fn main(init: std.process.Init) !void { std.process.exit(0); } - var config = grab.Configuration.init(); - defer config.deinit(allocator); - - // Set up logger - var level = Level.info; - if (res.args.@"log-level") |l| { - level = l; - } - - switch (level) { - .debug => log_level = std.log.Level.debug, - .@"error" => log_level = std.log.Level.err, - .info => log_level = std.log.Level.info, - .warn => log_level = std.log.Level.warn, + if (res.args.init != 0) { + try grab.init(init.io, init.gpa, init.minimal.environ); + return; } if (res.args.temp != 0 and res.args.path != null) { @@ -107,17 +86,17 @@ pub fn main(init: std.process.Init) !void { config.path = .{ .provided = path }; std.log.info("using {s} as path", .{path}); } else { - const path = init.minimal.environ.getPosix("GRAB_PATH") orelse { - std.log.err("unable to get GRAB_PATH, please set or use --temp or --path", .{}); - std.process.exit(1); - }; + const path = init.minimal.environ.getPosix("GRAB_PATH") orelse ""; - if (path.len == 0) { - std.log.err("unable to get GRAB_PATH, please set or use --temp or --path", .{}); - std.process.exit(1); + if (path.len != 0) { + config.path = .{ .provided = path }; + std.log.debug("Using path from env var", .{}); } - config.path = .{ .provided = path }; - std.log.debug("try to get path from env", .{}); + } + + if (config.getPath() == null) { + std.log.err("No path source path found, please set path in config file, see --init, set GRAB_PATH env var or use --temp or --path", .{}); + std.process.exit(1); } if (res.args.remote != 0) { config.action = .remote; diff --git a/src/root.zig b/src/root.zig index df1d636..55ae2b6 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,5 +1,11 @@ const std = @import("std"); +const defaults = @import("defaults.zig"); +const default_config_source = @embedFile("default_config.zon"); + +const _logging = @import("logging.zig"); +pub const logging = _logging; + pub const Project = struct { site: []const u8, owner: []const u8, @@ -59,22 +65,52 @@ pub const PathSource = union(enum) { none, }; +pub const ConfigurationFile = struct { + path: ?[]const u8 = null, + action: ?Action = null, + shallow: ?bool = null, + log_level: ?logging.Level = null, + + pub fn deinit(self: @This(), gpa: std.mem.Allocator) void { + std.zon.parse.free(gpa, self); + } +}; + pub const Configuration = struct { path: ?PathSource = .none, - action: Action = .worktree, - shallow: bool = false, + action: Action = defaults.action, + shallow: bool = defaults.shallow, + configFile: ?ConfigurationFile = null, + + pub fn init(io: std.Io, gpa: std.mem.Allocator, environ: std.process.Environ) !Configuration { + std.log.debug("setting up configuration", .{}); + const config_file = load_config_file(io, gpa, environ) catch |err| switch (err) { + error.NotFound => null, + else => return err, + }; + if (config_file) |config| { + if (config.log_level) |v| logging.set_log_level(v); + + return Configuration{ + .path = if (config.path) |p| .{ .provided = p } else .none, + .action = if (config.action) |v| v else defaults.action, + .shallow = if (config.shallow) |v| v else defaults.shallow, + .configFile = config, + }; + } - pub fn init() Configuration { - return Configuration{}; + return .{}; } - pub fn deinit(self: *Configuration, allocator: std.mem.Allocator) void { + pub fn deinit(self: *Configuration, gpa: std.mem.Allocator) void { if (self.path) |path| { switch (path) { - .allocated => |p| allocator.free(p), + .allocated => |p| gpa.free(p), .provided, .none => {}, } } + + if (self.configFile) |configFile| configFile.deinit(gpa); } pub fn getPath(self: *const Configuration) ?[]const u8 { @@ -87,6 +123,96 @@ pub const Configuration = struct { } }; +fn load_config_file(io: std.Io, gpa: std.mem.Allocator, environ: std.process.Environ) !ConfigurationFile { + const xdg_config_home = environ.getPosix("XDG_CONFIG_HOME") orelse ""; + const owns_path = xdg_config_home.len == 0; + const path: []const u8 = if (xdg_config_home.len > 0) xdg_config_home else try userPath(gpa, environ); + defer if (owns_path) gpa.free(path); + + const config_file_path = try std.fmt.allocPrint(gpa, "{s}/grab/config.zon", .{path}); + defer gpa.free(config_file_path); + if (!pathIsFile(io, config_file_path)) { + std.log.debug("No existing configuration file found at {s}", .{config_file_path}); + return error.NotFound; + } + + const cwd = std.Io.Dir.cwd(); + const file = try cwd.openFile(io, config_file_path, .{ .mode = .read_only }); + defer file.close(io); + + const size: usize = @intCast(try file.length(io)); + const buffer = try gpa.allocSentinel(u8, size, 0); + defer gpa.free(buffer); + + _ = try file.readPositionalAll(io, buffer[0..size], 0); + + var diag: std.zon.parse.Diagnostics = .{}; + defer diag.deinit(gpa); + + const config = std.zon.parse.fromSliceAlloc(ConfigurationFile, gpa, buffer, &diag, .{}) catch |err| { + std.log.err("Failed to parse {s}: {}", .{ config_file_path, err }); + std.log.err("{f}", .{diag}); + return err; + }; + + return config; +} + +pub fn init(io: std.Io, gpa: std.mem.Allocator, environ: std.process.Environ) !void { + // set up root configuration path + const xdg_config_home = environ.getPosix("XDG_CONFIG_HOME") orelse ""; + const owns_path = xdg_config_home.len == 0; + const path: []const u8 = if (xdg_config_home.len > 0) xdg_config_home else try userPath(gpa, environ); + defer if (owns_path) gpa.free(path); + + // Check if root configuration path exits + if (!pathIsDir(io, path)) { + std.log.err("Root configuration path does not exist. '{s}'", .{path}); + return error.PathNotFound; + } + std.log.debug("using root configuration path of '{s}'", .{path}); + + // Create path to configuration file + const config_path = try std.fmt.allocPrint(gpa, "{s}/grab", .{path}); + defer gpa.free(config_path); + + const config_file_path = try std.fmt.allocPrint(gpa, "{s}/config.zon", .{config_path}); + defer gpa.free(config_file_path); + // Check if configuration file exist + if (pathIsFile(io, config_file_path)) { + std.log.warn("Existing configuration file found: {s}", .{config_file_path}); + return; + } + + // Create the config directory + const cwd = std.Io.Dir.cwd(); + cwd.createDir(io, config_path, .default_dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + const file = try cwd.createFile(io, config_file_path, .{}); + defer file.close(io); + try file.writePositionalAll(io, default_config_source, 0); + + std.log.info("Configuration file created, see {s} for configuration options", .{config_file_path}); +} + +fn userPath(gpa: std.mem.Allocator, environ: std.process.Environ) ![]const u8 { + const home = environ.getPosix("HOME") orelse ""; + if (home.len > 0) return try std.fmt.allocPrint(gpa, "{s}/.config", .{home}) else return error.NoHomeFound; +} + +fn pathIsDir(io: std.Io, path: []const u8) bool { + const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch return false; + return stat.kind == .directory; +} + +fn pathIsFile(io: std.Io, path: []const u8) bool { + const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch return false; + return stat.kind == .file; +} + pub fn clone(allocator: std.mem.Allocator, io: std.Io, project: Project, opts: CloneOptions) !void { std.log.debug("cloning: {s}", .{project.name}); From ccd7878e77bde11640644c3821b5c4b25b7d5926 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 1 Aug 2026 15:17:13 +0100 Subject: [PATCH 2/3] REFACTOR: Mostly Did some clean up in the code base. Switched branch.autoSetupMerge to true. Signed-off-by: Jim Fitzpatrick --- .../unreleased/Changed-20260719-232001.yaml | 3 ++ src/main.zig | 49 +++++++++---------- src/root.zig | 31 ++++++------ 3 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 .changes/unreleased/Changed-20260719-232001.yaml diff --git a/.changes/unreleased/Changed-20260719-232001.yaml b/.changes/unreleased/Changed-20260719-232001.yaml new file mode 100644 index 0000000..97b08ef --- /dev/null +++ b/.changes/unreleased/Changed-20260719-232001.yaml @@ -0,0 +1,3 @@ +kind: Changed +body: branch.autoSetupMerge change to "true" from "always"." +time: 2026-07-19T23:20:01.678634875+01:00 diff --git a/src/main.zig b/src/main.zig index 2cff1e0..371a6e1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,8 @@ const std = @import("std"); const clap = @import("clap"); const grab = @import("grab"); +const build_options = @import("build_options"); + const help = @import("help.zig"); pub const std_options: std.Options = .{ @@ -47,22 +49,22 @@ pub fn main(init: std.process.Init) !void { defer config.deinit(allocator); // Set up logger - if (res.args.@"log-level") |l| { - grab.logging.set_log_level(l); - } + if (res.args.@"log-level") |l| grab.logging.set_log_level(l); + + if (res.args.help != 0) return clap.helpToFile( + init.io, + .stderr(), + clap.Help, + ¶ms, + .{}, + ); - if (res.args.help != 0) - return clap.helpToFile(init.io, .stderr(), clap.Help, ¶ms, .{}); - if (res.args.version != 0) { - const build_options = @import("build_options"); - std.log.info("{s}: {s}", .{ build_options.name, build_options.version }); - std.process.exit(0); - } + if (res.args.version != 0) return std.log.info( + "{s}: {s}", + .{ build_options.name, build_options.version }, + ); - if (res.args.init != 0) { - try grab.init(init.io, init.gpa, init.minimal.environ); - return; - } + if (res.args.init != 0) return try grab.init(init.io, init.gpa, init.minimal.environ); if (res.args.temp != 0 and res.args.path != null) { std.log.err("Cannot specify both --temp and --path", .{}); @@ -98,17 +100,10 @@ pub fn main(init: std.process.Init) !void { std.log.err("No path source path found, please set path in config file, see --init, set GRAB_PATH env var or use --temp or --path", .{}); std.process.exit(1); } - if (res.args.remote != 0) { - config.action = .remote; - } - if (res.args.standard != 0) { - config.action = .standard; - } - - if (res.args.shallow != 0) { - config.shallow = true; - } + config.action = if (res.args.remote != 0) .remote else config.action; + config.action = if (res.args.standard != 0) .standard else config.action; + config.shallow = if (res.args.shallow != 0) true else config.shallow; try grab.setLocation(init.io, config); @@ -139,7 +134,11 @@ pub fn main(init: std.process.Init) !void { } } std.log.info("Finished", .{}); - if (run_failure) std.process.exit(1); + if (run_failure) { + std.log.warn("Finished with errors", .{}); + std.process.exit(1); + } + std.log.info("Finished", .{}); } const gitOpts = struct { shallow: bool = false }; diff --git a/src/root.zig b/src/root.zig index 55ae2b6..352bfdc 100644 --- a/src/root.zig +++ b/src/root.zig @@ -116,8 +116,7 @@ pub const Configuration = struct { pub fn getPath(self: *const Configuration) ?[]const u8 { const path = self.path orelse return null; return switch (path) { - .provided => |p| p, - .allocated => |p| p, + .provided, .allocated => |p| p, .none => null, }; } @@ -238,12 +237,17 @@ pub fn clone(allocator: std.mem.Allocator, io: std.Io, project: Project, opts: C return err; }; - defer allocator.free(result.stdout); - defer allocator.free(result.stderr); + defer { + allocator.free(result.stdout); + allocator.free(result.stderr); + } if (std.mem.startsWith(u8, result.stderr, "fatal")) { - if (std.mem.endsWith(u8, result.stderr, "already exists and is not an empty directory.\n")) { - return error.exists; - } + if (std.mem.endsWith( + u8, + result.stderr, + "already exists and is not an empty directory.\n", + )) return error.exists; + std.log.err("{s}", .{result.stderr}); return error.unknown; } @@ -303,8 +307,7 @@ fn isGitRepo(io: std.Io, path: []const u8) !bool { for (subPaths) |p| { var isRepo = true; _ = cwd.openDir(io, p, .{}) catch |err| switch (err) { - error.NotDir => isRepo = false, - error.FileNotFound => isRepo = false, + error.NotDir, error.FileNotFound => isRepo = false, else => return err, }; if (isRepo) return isRepo; @@ -327,12 +330,10 @@ pub fn addRemote(allocator: std.mem.Allocator, io: std.Io, project: Project, pat } var output = std.mem.splitSequence(u8, checkResult.stdout, "\n"); - var value = output.first(); - while (true) { + while (output.next()) |value| { if (std.mem.eql(u8, value, project.owner)) { return error.RemoteExists; } - value = output.next() orelse break; } const addCmd = [_][]const u8{ "git", "-C", path, "remote", "add", project.owner, project.clone }; @@ -425,7 +426,7 @@ pub fn setLogAllRef(allocator: std.mem.Allocator, io: std.Io, path: std.Io.Dir) pub fn setAutoSetupMerge(allocator: std.mem.Allocator, io: std.Io, path: std.Io.Dir) !void { const _path = try path.realPathFileAlloc(io, ".", allocator); defer allocator.free(_path); - const cmd = [_][]const u8{ "git", "-C", _path, "config", "branch.autoSetupMerge", "always" }; + const cmd = [_][]const u8{ "git", "-C", _path, "config", "branch.autoSetupMerge", "true" }; std.log.debug("Configuring autoSetupMerge", .{}); const result = std.process.run(allocator, io, .{ .argv = &cmd, @@ -482,7 +483,7 @@ pub fn setLocalTracking(allocator: std.mem.Allocator, io: std.Io, path: std.Io.D const remote_cmd = [_][]const u8{ "git", "-C", _path, "config", remote, "origin" }; const merge_cmd = [_][]const u8{ "git", "-C", _path, "config", merge, head }; - std.log.debug("Setting up remotes for origin", .{}); + std.log.debug("[{s}] Setting up remotes for origin", .{remote}); const remote_result = std.process.run(allocator, io, .{ .argv = &remote_cmd }) catch |err| { std.log.err("Failed to run git config {s} origin", .{remote}); return err; @@ -496,7 +497,7 @@ pub fn setLocalTracking(allocator: std.mem.Allocator, io: std.Io, path: std.Io.D return error.runtime; } - std.log.debug("Setting up merge configuration", .{}); + std.log.debug("[{s}] Setting up merge configuration", .{remote}); const merge_result = std.process.run(allocator, io, .{ .argv = &merge_cmd }) catch |err| { std.log.err("Failed to run git config {s} {s}", .{ merge, head }); return err; From 49b50a1277df2d977a7c9023949ee1598ee30eed Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 1 Aug 2026 16:14:01 +0100 Subject: [PATCH 3/3] ADD: log colors Signed-off-by: Jim Fitzpatrick --- .changes/unreleased/Added-20260801-161340.yaml | 3 +++ .lchangie.yaml => .changie.yaml | 0 src/logging.zig | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Added-20260801-161340.yaml rename .lchangie.yaml => .changie.yaml (100%) diff --git a/.changes/unreleased/Added-20260801-161340.yaml b/.changes/unreleased/Added-20260801-161340.yaml new file mode 100644 index 0000000..17618dc --- /dev/null +++ b/.changes/unreleased/Added-20260801-161340.yaml @@ -0,0 +1,3 @@ +kind: Added +body: 'Logs now have some color. ' +time: 2026-08-01T16:13:40.454204347+01:00 diff --git a/.lchangie.yaml b/.changie.yaml similarity index 100% rename from .lchangie.yaml rename to .changie.yaml diff --git a/src/logging.zig b/src/logging.zig index f07e5db..aa67214 100644 --- a/src/logging.zig +++ b/src/logging.zig @@ -13,8 +13,8 @@ pub fn log( ) void { const prefix = comptime blk: { if (scope == .default) - break :blk "[" ++ level.asText() ++ "] "; - break :blk "[" ++ level.asText() ++ "][" ++ @tagName(scope) ++ "] "; + break :blk levelColor(level); + break :blk levelColor(level) ++ "[" ++ @tagName(scope) ++ "] "; }; if (@intFromEnum(level) <= @intFromEnum(log_level)) { @@ -30,3 +30,17 @@ pub fn set_log_level(level: Level) void { .warn => log_level = .warn, } } + +fn levelColor(level: std.log.Level) []const u8 { + const csi = "\x1b["; + const end = csi ++ "0m"; + const yellow = csi ++ "33m"; + const red = csi ++ "31m"; + const blue = csi ++ "34m"; + return switch (level) { + .debug => blue ++ "[" ++ level.asText() ++ "]" ++ end ++ " ", + .warn => yellow ++ "[" ++ level.asText() ++ "]" ++ end ++ " ", + .err => red ++ "[" ++ level.asText() ++ "]" ++ end ++ " ", + .info => "[" ++ level.asText() ++ "] ", + }; +}