Add read-only git workbench panels
This commit is contained in:
+208
-1
@@ -12,6 +12,7 @@ pub const Error = error{
|
||||
InvalidPattern,
|
||||
InvalidQuery,
|
||||
InvalidSearchRow,
|
||||
InvalidStatusRow,
|
||||
FileNotFound,
|
||||
};
|
||||
|
||||
@@ -199,6 +200,129 @@ fn byteOffsetForLineColumn(content_bytes: []const u8, line_no: usize, column: us
|
||||
return Error.InvalidSearchRow;
|
||||
}
|
||||
|
||||
pub fn gitStatusRowsAlloc(allocator: std.mem.Allocator, io: std.Io, cwd: []const u8) ![][]const u8 {
|
||||
const argv = [_][]const u8{ "git", "-C", cwd, "status", "--short" };
|
||||
const result = std.process.run(allocator, io, .{
|
||||
.argv = &argv,
|
||||
.stdout_limit = .limited(64 * 1024),
|
||||
.stderr_limit = .limited(16 * 1024),
|
||||
}) catch |err| return singleOwnedRow(allocator, try std.fmt.allocPrint(allocator, "git_error:{s}", .{@errorName(err)}));
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
if (!termSucceeded(result.term)) return singleOwnedRow(allocator, try gitErrorRowAlloc(allocator, result.stderr));
|
||||
return parseStatusRowsAlloc(allocator, result.stdout);
|
||||
}
|
||||
|
||||
pub fn gitDiffRowsAlloc(allocator: std.mem.Allocator, io: std.Io, cwd: []const u8, path: []const u8) ![][]const u8 {
|
||||
try validatePath(path);
|
||||
const argv = [_][]const u8{ "git", "-C", cwd, "diff", "--unified=0", "--", path };
|
||||
const result = std.process.run(allocator, io, .{
|
||||
.argv = &argv,
|
||||
.stdout_limit = .limited(256 * 1024),
|
||||
.stderr_limit = .limited(16 * 1024),
|
||||
}) catch |err| return singleOwnedRow(allocator, try std.fmt.allocPrint(allocator, "git_error:{s}", .{@errorName(err)}));
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
if (!termSucceeded(result.term)) return singleOwnedRow(allocator, try gitErrorRowAlloc(allocator, result.stderr));
|
||||
return parseDiffRowsAlloc(allocator, result.stdout);
|
||||
}
|
||||
|
||||
pub fn gitPathFromStatusRow(row: []const u8) ![]const u8 {
|
||||
const colon = std.mem.indexOfScalar(u8, row, ':') orelse return Error.InvalidStatusRow;
|
||||
const path = row[colon + 1 ..];
|
||||
if (path.len == 0 or std.mem.startsWith(u8, row, "git_error:") or std.mem.eql(u8, row, "clean")) return Error.InvalidStatusRow;
|
||||
try validatePath(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
pub fn readRepoFileAlloc(allocator: std.mem.Allocator, io: std.Io, cwd: []const u8, path: []const u8) ![]u8 {
|
||||
try validatePath(path);
|
||||
const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ cwd, path });
|
||||
defer allocator.free(full_path);
|
||||
return std.Io.Dir.cwd().readFileAlloc(io, full_path, allocator, .limited(1024 * 1024));
|
||||
}
|
||||
|
||||
fn termSucceeded(term: std.process.Child.Term) bool {
|
||||
return switch (term) {
|
||||
.exited => |code| code == 0,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseStatusRowsAlloc(allocator: std.mem.Allocator, stdout: []const u8) ![][]const u8 {
|
||||
var rows = std.ArrayList([]const u8).empty;
|
||||
errdefer {
|
||||
for (rows.items) |row| allocator.free(row);
|
||||
rows.deinit(allocator);
|
||||
}
|
||||
var lines = std.mem.splitScalar(u8, stdout, '\n');
|
||||
while (lines.next()) |raw_line| {
|
||||
const line = std.mem.trim(u8, raw_line, "\r");
|
||||
if (line.len == 0) continue;
|
||||
if (line.len < 4) continue;
|
||||
const code = line[0..2];
|
||||
var path = std.mem.trim(u8, line[3..], " ");
|
||||
if (std.mem.indexOf(u8, path, " -> ")) |rename_arrow| path = path[rename_arrow + 4 ..];
|
||||
try validatePath(path);
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "{s}:{s}", .{ statusName(code), path }));
|
||||
}
|
||||
if (rows.items.len == 0) try rows.append(allocator, try allocator.dupe(u8, "clean"));
|
||||
return rows.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn statusName(code: []const u8) []const u8 {
|
||||
if (std.mem.indexOfScalar(u8, code, '?') != null) return "untracked";
|
||||
if (std.mem.indexOfScalar(u8, code, 'D') != null) return "deleted";
|
||||
if (std.mem.indexOfScalar(u8, code, 'A') != null) return "added";
|
||||
if (std.mem.indexOfScalar(u8, code, 'M') != null) return "modified";
|
||||
if (std.mem.indexOfScalar(u8, code, 'R') != null) return "renamed";
|
||||
return "changed";
|
||||
}
|
||||
|
||||
fn parseDiffRowsAlloc(allocator: std.mem.Allocator, stdout: []const u8) ![][]const u8 {
|
||||
var rows = std.ArrayList([]const u8).empty;
|
||||
errdefer {
|
||||
for (rows.items) |row| allocator.free(row);
|
||||
rows.deinit(allocator);
|
||||
}
|
||||
var lines = std.mem.splitScalar(u8, stdout, '\n');
|
||||
while (lines.next()) |raw_line| {
|
||||
const line = std.mem.trim(u8, raw_line, "\r");
|
||||
if (line.len == 0) continue;
|
||||
if (std.mem.startsWith(u8, line, "@@")) {
|
||||
const preview = try previewAlloc(allocator, line);
|
||||
defer allocator.free(preview);
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "hunk:{s}", .{preview}));
|
||||
} else if (std.mem.startsWith(u8, line, "+") and !std.mem.startsWith(u8, line, "+++")) {
|
||||
const preview = try previewAlloc(allocator, line[1..]);
|
||||
defer allocator.free(preview);
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "add:{s}", .{preview}));
|
||||
} else if (std.mem.startsWith(u8, line, "-") and !std.mem.startsWith(u8, line, "---")) {
|
||||
const preview = try previewAlloc(allocator, line[1..]);
|
||||
defer allocator.free(preview);
|
||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "del:{s}", .{preview}));
|
||||
}
|
||||
}
|
||||
if (rows.items.len == 0) try rows.append(allocator, try allocator.dupe(u8, "no diff"));
|
||||
return rows.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn gitErrorRowAlloc(allocator: std.mem.Allocator, stderr: []const u8) ![]u8 {
|
||||
const trimmed = std.mem.trim(u8, stderr, " \t\r\n");
|
||||
if (trimmed.len == 0) return allocator.dupe(u8, "git_error:command_failed");
|
||||
const limit = @min(trimmed.len, 120);
|
||||
const preview = try previewAlloc(allocator, trimmed[0..limit]);
|
||||
defer allocator.free(preview);
|
||||
return std.fmt.allocPrint(allocator, "git_error:{s}", .{preview});
|
||||
}
|
||||
|
||||
fn singleOwnedRow(allocator: std.mem.Allocator, row: []u8) ![][]const u8 {
|
||||
errdefer allocator.free(row);
|
||||
const rows = try allocator.alloc([]const u8, 1);
|
||||
rows[0] = row;
|
||||
return rows;
|
||||
}
|
||||
|
||||
fn matchesPattern(pattern: []const u8, path: []const u8) bool {
|
||||
if (std.mem.endsWith(u8, pattern, "/")) return std.mem.startsWith(u8, path, pattern);
|
||||
if (std.mem.startsWith(u8, pattern, "*.")) return std.mem.endsWith(u8, path, pattern[1..]);
|
||||
@@ -214,7 +338,7 @@ fn freeItems(allocator: std.mem.Allocator, items: [][]const u8) void {
|
||||
allocator.free(items);
|
||||
}
|
||||
|
||||
fn freeOwnedItems(allocator: std.mem.Allocator, items: [][]const u8) void {
|
||||
fn freeOwnedItems(allocator: std.mem.Allocator, items: []const []const u8) void {
|
||||
for (items) |item| allocator.free(item);
|
||||
allocator.free(items);
|
||||
}
|
||||
@@ -324,3 +448,86 @@ test "adversarial: project search rejects bad queries and rows" {
|
||||
try std.testing.expectError(Error.InvalidSearchRow, repo.searchOffsetFromRow("bad-row"));
|
||||
try std.testing.expectError(Error.FileNotFound, repo.searchOffsetFromRow("missing.zig:1:1:needle"));
|
||||
}
|
||||
|
||||
fn makeGitFixture(allocator: std.mem.Allocator) !struct { tmp: std.testing.TmpDir, cwd: []u8 } {
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
errdefer tmp.cleanup();
|
||||
var src = try tmp.dir.createDirPathOpen(std.testing.io, "src", .{});
|
||||
src.close(std.testing.io);
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "src/main.zig", .data = "const old = 1;\n" });
|
||||
const cwd = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{&tmp.sub_path});
|
||||
errdefer allocator.free(cwd);
|
||||
try runGitTest(allocator, cwd, &.{ "init", "--quiet" });
|
||||
try runGitTest(allocator, cwd, &.{ "add", "src/main.zig" });
|
||||
try runGitTest(allocator, cwd, &.{ "-c", "user.email=test@example.invalid", "-c", "user.name=Test", "commit", "--quiet", "-m", "init" });
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "src/main.zig", .data = "const new = 2;\n" });
|
||||
return .{ .tmp = tmp, .cwd = cwd };
|
||||
}
|
||||
|
||||
fn runGitTest(allocator: std.mem.Allocator, cwd: []const u8, args: []const []const u8) !void {
|
||||
var argv = try allocator.alloc([]const u8, args.len + 3);
|
||||
defer allocator.free(argv);
|
||||
argv[0] = "git";
|
||||
argv[1] = "-C";
|
||||
argv[2] = cwd;
|
||||
@memcpy(argv[3..], args);
|
||||
const result = try std.process.run(allocator, std.testing.io, .{
|
||||
.argv = argv,
|
||||
.stdout_limit = .limited(64 * 1024),
|
||||
.stderr_limit = .limited(64 * 1024),
|
||||
});
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
try std.testing.expect(termSucceeded(result.term));
|
||||
}
|
||||
|
||||
test "regular: git status rows expose changed files" {
|
||||
var fixture = try makeGitFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.cwd);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
|
||||
const rows = try gitStatusRowsAlloc(std.testing.allocator, std.testing.io, fixture.cwd);
|
||||
defer freeOwnedItems(std.testing.allocator, rows);
|
||||
try std.testing.expectEqual(@as(usize, 1), rows.len);
|
||||
try std.testing.expectEqualStrings("modified:src/main.zig", rows[0]);
|
||||
try std.testing.expectEqualStrings("src/main.zig", try gitPathFromStatusRow(rows[0]));
|
||||
}
|
||||
|
||||
test "regular: git diff rows expose hunks and changed lines" {
|
||||
var fixture = try makeGitFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.cwd);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
|
||||
const rows = try gitDiffRowsAlloc(std.testing.allocator, std.testing.io, fixture.cwd, "src/main.zig");
|
||||
defer freeOwnedItems(std.testing.allocator, rows);
|
||||
try std.testing.expect(rows.len >= 3);
|
||||
try std.testing.expect(std.mem.startsWith(u8, rows[0], "hunk:@@"));
|
||||
try std.testing.expect(std.mem.indexOf(u8, rows[1], "const_old_=_1;") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, rows[2], "const_new_=_2;") != null);
|
||||
}
|
||||
|
||||
test "regular: changed git file can be read for editor navigation" {
|
||||
var fixture = try makeGitFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.cwd);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
|
||||
const content = try readRepoFileAlloc(std.testing.allocator, std.testing.io, fixture.cwd, "src/main.zig");
|
||||
defer std.testing.allocator.free(content);
|
||||
try std.testing.expectEqualStrings("const new = 2;\n", content);
|
||||
}
|
||||
|
||||
test "adversarial: git errors are visible rows and status rows are validated" {
|
||||
const rows = try gitStatusRowsAlloc(std.testing.allocator, std.testing.io, "/definitely/not/a/mim/repo");
|
||||
defer freeOwnedItems(std.testing.allocator, rows);
|
||||
try std.testing.expectEqual(@as(usize, 1), rows.len);
|
||||
try std.testing.expect(std.mem.startsWith(u8, rows[0], "git_error:"));
|
||||
try std.testing.expectError(Error.InvalidStatusRow, gitPathFromStatusRow(rows[0]));
|
||||
try std.testing.expectError(Error.InvalidStatusRow, gitPathFromStatusRow("clean"));
|
||||
try std.testing.expectError(Error.InvalidPath, gitPathFromStatusRow("modified:../secret"));
|
||||
}
|
||||
|
||||
+136
@@ -85,18 +85,24 @@ pub const Client = struct {
|
||||
session: session_mod.Session,
|
||||
leader: leader_mod.Leader,
|
||||
repo: repo_mod.Index,
|
||||
io: ?std.Io,
|
||||
viewport: Viewport,
|
||||
saved_bytes: ?[]u8 = null,
|
||||
message: ?[]const u8 = null,
|
||||
quit: bool = false,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, viewport: Viewport) !Client {
|
||||
return initWithIo(allocator, viewport, null);
|
||||
}
|
||||
|
||||
pub fn initWithIo(allocator: std.mem.Allocator, viewport: Viewport, io: ?std.Io) !Client {
|
||||
try viewport.validate();
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.session = session_mod.Session.init(allocator),
|
||||
.leader = leader_mod.Leader.init(allocator),
|
||||
.repo = repo_mod.Index.init(allocator),
|
||||
.io = io,
|
||||
.viewport = viewport,
|
||||
};
|
||||
}
|
||||
@@ -128,6 +134,9 @@ pub const Client = struct {
|
||||
if (std.mem.startsWith(u8, line, "search_text all ")) return self.openSearchList(line[16..], true);
|
||||
if (std.mem.startsWith(u8, line, "search_text ")) return self.openSearchList(line[12..], false);
|
||||
if (std.mem.eql(u8, line, "search_open_selected")) return self.openSelectedSearchResult();
|
||||
if (std.mem.startsWith(u8, line, "git_status ")) return self.openGitStatus(line[11..]);
|
||||
if (std.mem.startsWith(u8, line, "git_diff_selected ")) return self.openSelectedGitDiff(line[18..]);
|
||||
if (std.mem.startsWith(u8, line, "git_open_changed_selected ")) return self.openSelectedGitFile(line[26..]);
|
||||
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
|
||||
@@ -329,6 +338,35 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openGitStatus(self: *Client, cwd: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const rows = repo_mod.gitStatusRowsAlloc(self.allocator, io, cwd) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("git-status", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openSelectedGitDiff(self: *Client, cwd: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
|
||||
const path = repo_mod.gitPathFromStatusRow(selected) catch return Error.ProtocolRejected;
|
||||
const rows = repo_mod.gitDiffRowsAlloc(self.allocator, io, cwd, path) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("git-diff", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openSelectedGitFile(self: *Client, cwd: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
|
||||
const path = repo_mod.gitPathFromStatusRow(selected) catch return Error.ProtocolRejected;
|
||||
const content = repo_mod.readRepoFileAlloc(self.allocator, io, cwd, path) catch return Error.ProtocolRejected;
|
||||
defer self.allocator.free(content);
|
||||
try self.session.openFixture(content);
|
||||
try self.session.closePanel();
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn handleTraceKey(self: *Client, key_name: []const u8) !void {
|
||||
if (std.mem.eql(u8, key_name, "space")) return self.handleInput(" ");
|
||||
if (std.mem.eql(u8, key_name, "enter")) return self.handleInput("\r");
|
||||
@@ -552,6 +590,11 @@ fn assertLinesFit(frame: []const u8, width: usize) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn freeOwnedRows(allocator: std.mem.Allocator, rows: []const []const u8) void {
|
||||
for (rows) |row| allocator.free(row);
|
||||
allocator.free(rows);
|
||||
}
|
||||
|
||||
test "regular: scripted narrow terminal trace edits saves exits and replays saved bytes" {
|
||||
const trace =
|
||||
\\open abc
|
||||
@@ -1118,3 +1161,96 @@ test "adversarial: project text search failures do not corrupt current buffer" {
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
fn makeTuiGitFixture(allocator: std.mem.Allocator) !struct { tmp: std.testing.TmpDir, cwd: []u8 } {
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
errdefer tmp.cleanup();
|
||||
var src = try tmp.dir.createDirPathOpen(std.testing.io, "src", .{});
|
||||
src.close(std.testing.io);
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "src/main.zig", .data = "const old = 1;\n" });
|
||||
const cwd = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{&tmp.sub_path});
|
||||
errdefer allocator.free(cwd);
|
||||
try runGitTuiTest(allocator, cwd, &.{ "init", "--quiet" });
|
||||
try runGitTuiTest(allocator, cwd, &.{ "add", "src/main.zig" });
|
||||
try runGitTuiTest(allocator, cwd, &.{ "-c", "user.email=test@example.invalid", "-c", "user.name=Test", "commit", "--quiet", "-m", "init" });
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "src/main.zig", .data = "const new = 2;\n" });
|
||||
return .{ .tmp = tmp, .cwd = cwd };
|
||||
}
|
||||
|
||||
fn runGitTuiTest(allocator: std.mem.Allocator, cwd: []const u8, args: []const []const u8) !void {
|
||||
var argv = try allocator.alloc([]const u8, args.len + 3);
|
||||
defer allocator.free(argv);
|
||||
argv[0] = "git";
|
||||
argv[1] = "-C";
|
||||
argv[2] = cwd;
|
||||
@memcpy(argv[3..], args);
|
||||
const result = try std.process.run(allocator, std.testing.io, .{
|
||||
.argv = argv,
|
||||
.stdout_limit = .limited(64 * 1024),
|
||||
.stderr_limit = .limited(64 * 1024),
|
||||
});
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
try std.testing.expect(switch (result.term) {
|
||||
.exited => |code| code == 0,
|
||||
else => false,
|
||||
});
|
||||
}
|
||||
|
||||
test "regular: git status panel opens changed file into editor" {
|
||||
var fixture = try makeTuiGitFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.cwd);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 64, .height = 6 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
const status = try std.fmt.allocPrint(std.testing.allocator, "git_status {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(status);
|
||||
try client.handleTraceLine(status);
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "git-status") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "modified:src/main.zig") != null);
|
||||
}
|
||||
|
||||
const open_changed = try std.fmt.allocPrint(std.testing.allocator, "git_open_changed_selected {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(open_changed);
|
||||
try client.handleTraceLine(open_changed);
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("const new = 2;\n", snap.bytes);
|
||||
}
|
||||
|
||||
test "regular: git diff panel renders hunks for selected changed file" {
|
||||
var fixture = try makeTuiGitFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.cwd);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 72, .height = 7 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
const status = try std.fmt.allocPrint(std.testing.allocator, "git_status {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(status);
|
||||
try client.handleTraceLine(status);
|
||||
const diff = try std.fmt.allocPrint(std.testing.allocator, "git_diff_selected {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(diff);
|
||||
try client.handleTraceLine(diff);
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "git-diff") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "hunk:@@") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "add:const_new_=_2;") != null);
|
||||
}
|
||||
|
||||
test "adversarial: git status errors are visible in panel" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 64, .height = 5 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("git_status /definitely/not/a/mim/repo");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "git_error:") != null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user