const std = @import("std"); const tp = @import("thespian"); const shell = @import("shell"); const bin_path = @import("bin_path"); pub const Error = error{ OutOfMemory, GitNotFound, GitCallFailed }; const log_execute = false; pub fn workspace_path(context_: usize) Error!void { const fn_name = @src().fn_name; try git(context_, .{ "rev-parse", "--show-toplevel" }, struct { fn result(context: usize, parent: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); while (it.next()) |value| if (value.len > 0) parent.send(.{ module_name, context, fn_name, value }) catch {}; } }.result, exit_null_on_error(fn_name)); } pub fn current_branch(context_: usize) Error!void { const fn_name = @src().fn_name; try git(context_, .{ "rev-parse", "--abbrev-ref", "HEAD" }, struct { fn result(context: usize, parent: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); while (it.next()) |value| if (value.len > 0) { parent.send(.{ module_name, context, fn_name, value }) catch {}; return; }; } }.result, exit_null_on_error(fn_name)); } pub fn workspace_files(context: usize) Error!void { return git_line_output( context, @src().fn_name, .{ "ls-files", "--cached", "--others", "--exclude-standard" }, ); } pub fn workspace_ignored_files(context: usize) Error!void { return git_line_output( context, @src().fn_name, .{ "ls-files", "--cached", "--others", "--exclude-standard", "--ignored" }, ); } const StatusRecordType = enum { @"#", // header @"1", // ordinary file @"2", // rename or copy u, // unmerged file @"?", // untracked file @"!", // ignored file }; pub fn status(context_: usize) Error!void { const tag = @src().fn_name; try git_err(context_, .{ "--no-optional-locks", "status", "--porcelain=v2", "--branch", "--show-stash", // "--untracked-files=no", "--null", }, struct { fn result(context: usize, parent: tp.pid_ref, output: []const u8) void { var it_ = std.mem.splitScalar(u8, output, 0); while (it_.next()) |line| { var it = std.mem.splitScalar(u8, line, ' '); const rec_type = if (it.next()) |type_tag| std.meta.stringToEnum(StatusRecordType, type_tag) orelse return else return; switch (rec_type) { .@"#" => { // header const name = it.next() orelse return; const value1 = it.next() orelse return; if (it.next()) |value2| parent.send(.{ module_name, context, tag, "#", name, value1, value2 }) catch {} else parent.send(.{ module_name, context, tag, "#", name, value1 }) catch {}; }, .@"1" => { // ordinary file: const XY = it.next() orelse return; const sub = it.next() orelse return; const mH = it.next() orelse return; const mI = it.next() orelse return; const mW = it.next() orelse return; const hH = it.next() orelse return; const hI = it.next() orelse return; var path: std.ArrayListUnmanaged(u8) = .empty; defer path.deinit(allocator); while (it.next()) |path_part| { if (path.items.len > 0) path.append(allocator, ' ') catch return; path.appendSlice(allocator, path_part) catch return; } parent.send(.{ module_name, context, tag, "1", XY, sub, mH, mI, mW, hH, hI, path.items }) catch {}; }, .@"2" => { // rename or copy: const XY = it.next() orelse return; const sub = it.next() orelse return; const mH = it.next() orelse return; const mI = it.next() orelse return; const mW = it.next() orelse return; const hH = it.next() orelse return; const hI = it.next() orelse return; const Xscore = it.next() orelse return; var path: std.ArrayListUnmanaged(u8) = .empty; defer path.deinit(allocator); while (it.next()) |path_part| { if (path.items.len > 0) path.append(allocator, ' ') catch return; path.appendSlice(allocator, path_part) catch return; } const origPath = it_.next() orelse return; // NOTE: this is the next zero terminated part parent.send(.{ module_name, context, tag, "2", XY, sub, mH, mI, mW, hH, hI, Xscore, path.items, origPath }) catch {}; }, .u => { // unmerged file:

const XY = it.next() orelse return; const sub = it.next() orelse return; const m1 = it.next() orelse return; const m2 = it.next() orelse return; const m3 = it.next() orelse return; const mW = it.next() orelse return; const h1 = it.next() orelse return; const h2 = it.next() orelse return; const h3 = it.next() orelse return; var path: std.ArrayListUnmanaged(u8) = .empty; defer path.deinit(allocator); while (it.next()) |path_part| { if (path.items.len > 0) path.append(allocator, ' ') catch return; path.appendSlice(allocator, path_part) catch return; } parent.send(.{ module_name, context, tag, "u", XY, sub, m1, m2, m3, mW, h1, h2, h3, path.items }) catch {}; }, .@"?" => { // untracked file: var path: std.ArrayListUnmanaged(u8) = .empty; defer path.deinit(allocator); while (it.next()) |path_part| { if (path.items.len > 0) path.append(allocator, ' ') catch return; path.appendSlice(allocator, path_part) catch return; } parent.send(.{ module_name, context, tag, "?", path.items }) catch {}; }, .@"!" => { // ignored file: var path: std.ArrayListUnmanaged(u8) = .empty; defer path.deinit(allocator); while (it.next()) |path_part| { if (path.items.len > 0) path.append(allocator, ' ') catch return; path.appendSlice(allocator, path_part) catch return; } parent.send(.{ module_name, context, tag, "!", path.items }) catch {}; }, } // parent.send(.{ module_name, context, tag, value }) catch {}; } } }.result, struct { fn result(_: usize, _: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); while (it.next()) |line| std.log.err("{s}: {s}", .{ module_name, line }); } }.result, exit_null(tag)); } fn git_line_output(context_: usize, comptime tag: []const u8, cmd: anytype) Error!void { try git_err(context_, cmd, struct { fn result(context: usize, parent: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); while (it.next()) |value| if (value.len > 0) parent.send(.{ module_name, context, tag, value }) catch {}; } }.result, struct { fn result(_: usize, _: tp.pid_ref, output: []const u8) void { var it = std.mem.splitScalar(u8, output, '\n'); while (it.next()) |line| std.log.err("{s}: {s}", .{ module_name, line }); } }.result, exit_null(tag)); } fn git( context: usize, cmd: anytype, out: OutputHandler, exit: ExitHandler, ) Error!void { return git_err(context, cmd, out, noop, exit); } fn git_err( context: usize, cmd: anytype, out: OutputHandler, err: OutputHandler, exit: ExitHandler, ) Error!void { const cbor = @import("cbor"); const git_binary = get_git() orelse return error.GitNotFound; var buf: std.ArrayListUnmanaged(u8) = .empty; const writer = buf.writer(allocator); switch (@typeInfo(@TypeOf(cmd))) { .@"struct" => |info| if (info.is_tuple) { try cbor.writeArrayHeader(writer, info.fields.len + 1); try cbor.writeValue(writer, git_binary); inline for (info.fields) |f| try cbor.writeValue(writer, @field(cmd, f.name)); return shell.execute(allocator, .{ .buf = buf.items }, .{ .context = context, .out = to_shell_output_handler(out), .err = to_shell_output_handler(err), .exit = exit, .log_execute = log_execute, }) catch error.GitCallFailed; }, else => {}, } @compileError("git command should be a tuple: " ++ @typeName(@TypeOf(cmd))); } fn exit_null(comptime tag: []const u8) shell.ExitHandler { return struct { fn exit(context: usize, parent: tp.pid_ref, _: []const u8, _: []const u8, _: i64) void { parent.send(.{ module_name, context, tag, null }) catch {}; } }.exit; } fn exit_null_on_error(comptime tag: []const u8) shell.ExitHandler { return struct { fn exit(context: usize, parent: tp.pid_ref, _: []const u8, _: []const u8, exit_code: i64) void { if (exit_code > 0) parent.send(.{ module_name, context, tag, null }) catch {}; } }.exit; } const OutputHandler = fn (context: usize, parent: tp.pid_ref, output: []const u8) void; const ExitHandler = shell.ExitHandler; fn to_shell_output_handler(handler: anytype) shell.OutputHandler { return struct { fn out(context: usize, parent: tp.pid_ref, _: []const u8, output: []const u8) void { handler(context, parent, output); } }.out; } fn noop(_: usize, _: tp.pid_ref, _: []const u8) void {} var git_path: ?struct { path: ?[:0]const u8 = null, } = null; const allocator = std.heap.c_allocator; fn get_git() ?[]const u8 { if (git_path) |p| return p.path; const path = bin_path.find_binary_in_path(allocator, module_name) catch null; git_path = .{ .path = path }; return path; } const module_name = @typeName(@This());