flow/src/git.zig

273 lines
11 KiB
Zig

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: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
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: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><sep><origPath>
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: <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
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: <path>
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: <path>
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());