547 lines
23 KiB
Zig
547 lines
23 KiB
Zig
const std = @import("std");
|
|
const diagnostics = @import("diagnostics.zig");
|
|
|
|
// Minimal repo index for file picker/tree behavior.
|
|
// req: repo/001, ui/002, testing/001, testing/002, testing/003, testing/004
|
|
|
|
test {
|
|
_ = Index;
|
|
}
|
|
|
|
pub const Error = error{
|
|
InvalidPath,
|
|
InvalidPattern,
|
|
InvalidQuery,
|
|
InvalidSearchRow,
|
|
InvalidStatusRow,
|
|
FileNotFound,
|
|
};
|
|
|
|
pub const Entry = struct {
|
|
path: []u8,
|
|
content: []u8,
|
|
};
|
|
|
|
pub const Index = struct {
|
|
allocator: std.mem.Allocator,
|
|
entries: std.ArrayList(Entry) = .empty,
|
|
ignore_patterns: std.ArrayList([]u8) = .empty,
|
|
|
|
pub fn init(allocator: std.mem.Allocator) Index {
|
|
return .{ .allocator = allocator };
|
|
}
|
|
|
|
pub fn deinit(self: *Index) void {
|
|
for (self.entries.items) |entry| {
|
|
self.allocator.free(entry.path);
|
|
self.allocator.free(entry.content);
|
|
}
|
|
self.entries.deinit(self.allocator);
|
|
for (self.ignore_patterns.items) |pattern| self.allocator.free(pattern);
|
|
self.ignore_patterns.deinit(self.allocator);
|
|
self.* = undefined;
|
|
}
|
|
|
|
pub fn clear(self: *Index) void {
|
|
for (self.entries.items) |entry| {
|
|
self.allocator.free(entry.path);
|
|
self.allocator.free(entry.content);
|
|
}
|
|
self.entries.clearRetainingCapacity();
|
|
for (self.ignore_patterns.items) |pattern| self.allocator.free(pattern);
|
|
self.ignore_patterns.clearRetainingCapacity();
|
|
}
|
|
|
|
pub fn addIgnorePattern(self: *Index, pattern: []const u8) !void {
|
|
try validatePattern(pattern);
|
|
try self.ignore_patterns.append(self.allocator, try self.allocator.dupe(u8, pattern));
|
|
}
|
|
|
|
pub fn addFile(self: *Index, path: []const u8, file_content: []const u8) !void {
|
|
try validatePath(path);
|
|
if (!std.unicode.utf8ValidateSlice(file_content)) return Error.InvalidPath;
|
|
try self.entries.append(self.allocator, .{
|
|
.path = try self.allocator.dupe(u8, path),
|
|
.content = try self.allocator.dupe(u8, file_content),
|
|
});
|
|
}
|
|
|
|
pub fn pickerItemsAlloc(self: *const Index, allocator: std.mem.Allocator, include_ignored: bool) ![][]const u8 {
|
|
var items = std.ArrayList([]const u8).empty;
|
|
errdefer items.deinit(allocator);
|
|
for (self.entries.items) |entry| {
|
|
if (!include_ignored and self.isIgnored(entry.path)) continue;
|
|
try items.append(allocator, entry.path);
|
|
}
|
|
return items.toOwnedSlice(allocator);
|
|
}
|
|
|
|
pub fn treeItemsAlloc(self: *const Index, allocator: std.mem.Allocator, include_ignored: bool) ![][]const u8 {
|
|
// Tree rows are path-shaped for now; rendering is still through the shared list primitive.
|
|
return self.pickerItemsAlloc(allocator, include_ignored);
|
|
}
|
|
|
|
pub fn content(self: *const Index, path: []const u8) ![]const u8 {
|
|
for (self.entries.items) |entry| {
|
|
if (std.mem.eql(u8, entry.path, path)) return entry.content;
|
|
}
|
|
return Error.FileNotFound;
|
|
}
|
|
|
|
pub fn searchRowsAlloc(self: *const Index, allocator: std.mem.Allocator, query: []const u8, include_ignored: bool) ![][]const u8 {
|
|
return self.searchRowsLimitedAlloc(allocator, query, include_ignored, diagnostics.max_search_rows);
|
|
}
|
|
|
|
pub fn searchRowsLimitedAlloc(self: *const Index, allocator: std.mem.Allocator, query: []const u8, include_ignored: bool, limit: usize) ![][]const u8 {
|
|
try validateQuery(query);
|
|
if (limit == 0) return Error.InvalidQuery;
|
|
var rows = std.ArrayList([]const u8).empty;
|
|
errdefer {
|
|
for (rows.items) |row| allocator.free(row);
|
|
rows.deinit(allocator);
|
|
}
|
|
var truncated = false;
|
|
for (self.entries.items) |entry| {
|
|
if (!include_ignored and self.isIgnored(entry.path)) continue;
|
|
try appendSearchRowsForEntry(allocator, &rows, entry, query, limit, &truncated);
|
|
if (truncated) break;
|
|
}
|
|
if (truncated) try rows.append(allocator, try diagnostics.searchTruncatedRowAlloc(allocator, rows.items.len, limit));
|
|
return rows.toOwnedSlice(allocator);
|
|
}
|
|
|
|
pub fn searchOffsetFromRow(self: *const Index, row: []const u8) !struct { path: []const u8, offset: usize } {
|
|
const first = std.mem.indexOfScalar(u8, row, ':') orelse return Error.InvalidSearchRow;
|
|
const rest = row[first + 1 ..];
|
|
const second_rel = std.mem.indexOfScalar(u8, rest, ':') orelse return Error.InvalidSearchRow;
|
|
const third_rel = std.mem.indexOfScalar(u8, rest[second_rel + 1 ..], ':') orelse return Error.InvalidSearchRow;
|
|
const path = row[0..first];
|
|
const line_no = std.fmt.parseUnsigned(usize, rest[0..second_rel], 10) catch return Error.InvalidSearchRow;
|
|
const column = std.fmt.parseUnsigned(usize, rest[second_rel + 1 .. second_rel + 1 + third_rel], 10) catch return Error.InvalidSearchRow;
|
|
if (line_no == 0 or column == 0) return Error.InvalidSearchRow;
|
|
const file_content = try self.content(path);
|
|
return .{ .path = path, .offset = try byteOffsetForLineColumn(file_content, line_no, column) };
|
|
}
|
|
|
|
pub fn isIgnored(self: *const Index, path: []const u8) bool {
|
|
for (self.ignore_patterns.items) |pattern| {
|
|
if (matchesPattern(pattern, path)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
fn validatePath(path: []const u8) !void {
|
|
if (path.len == 0) return Error.InvalidPath;
|
|
if (!std.unicode.utf8ValidateSlice(path)) return Error.InvalidPath;
|
|
if (std.mem.startsWith(u8, path, "/") or std.mem.indexOf(u8, path, "..") != null) return Error.InvalidPath;
|
|
for (path) |byte| {
|
|
if (byte <= 0x20 or byte == '|') return Error.InvalidPath;
|
|
}
|
|
}
|
|
|
|
fn validatePattern(pattern: []const u8) !void {
|
|
if (pattern.len == 0) return Error.InvalidPattern;
|
|
if (!std.unicode.utf8ValidateSlice(pattern)) return Error.InvalidPattern;
|
|
for (pattern) |byte| {
|
|
if (byte <= 0x20 or byte == '|') return Error.InvalidPattern;
|
|
}
|
|
}
|
|
|
|
fn validateQuery(query: []const u8) !void {
|
|
if (query.len == 0) return Error.InvalidQuery;
|
|
if (!std.unicode.utf8ValidateSlice(query)) return Error.InvalidQuery;
|
|
for (query) |byte| {
|
|
if (byte <= 0x20 or byte == '|') return Error.InvalidQuery;
|
|
}
|
|
}
|
|
|
|
fn appendSearchRowsForEntry(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), entry: Entry, query: []const u8, limit: usize, truncated: *bool) !void {
|
|
var line_no: usize = 1;
|
|
var lines = std.mem.splitScalar(u8, entry.content, '\n');
|
|
while (lines.next()) |line| : (line_no += 1) {
|
|
if (std.mem.indexOf(u8, line, query)) |column_zero| {
|
|
if (rows.items.len >= limit) {
|
|
truncated.* = true;
|
|
return;
|
|
}
|
|
const preview = try previewAlloc(allocator, line);
|
|
defer allocator.free(preview);
|
|
try rows.append(allocator, try std.fmt.allocPrint(
|
|
allocator,
|
|
"{s}:{d}:{d}:{s}",
|
|
.{ entry.path, line_no, column_zero + 1, preview },
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn previewAlloc(allocator: std.mem.Allocator, line: []const u8) ![]u8 {
|
|
var out = std.ArrayList(u8).empty;
|
|
errdefer out.deinit(allocator);
|
|
var i: usize = 0;
|
|
while (i < line.len and out.items.len < 48) {
|
|
const len = std.unicode.utf8ByteSequenceLength(line[i]) catch 1;
|
|
const end = @min(line.len, i + len);
|
|
const slice = line[i..end];
|
|
if (slice.len == 1 and (slice[0] <= 0x20 or slice[0] == ':' or slice[0] == '|')) {
|
|
try out.append(allocator, '_');
|
|
} else {
|
|
try out.appendSlice(allocator, slice);
|
|
}
|
|
i = end;
|
|
}
|
|
if (out.items.len == 0) try out.append(allocator, '_');
|
|
return out.toOwnedSlice(allocator);
|
|
}
|
|
|
|
fn byteOffsetForLineColumn(content_bytes: []const u8, line_no: usize, column: usize) !usize {
|
|
var current_line: usize = 1;
|
|
var line_start: usize = 0;
|
|
var i: usize = 0;
|
|
while (i <= content_bytes.len) : (i += 1) {
|
|
if (i == content_bytes.len or content_bytes[i] == '\n') {
|
|
if (current_line == line_no) {
|
|
const line = content_bytes[line_start..i];
|
|
if (column - 1 > line.len) return Error.InvalidSearchRow;
|
|
return line_start + column - 1;
|
|
}
|
|
current_line += 1;
|
|
line_start = i + 1;
|
|
}
|
|
}
|
|
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..]);
|
|
if (std.mem.indexOfScalar(u8, pattern, '/') != null) return std.mem.eql(u8, pattern, path) or std.mem.startsWith(u8, path, pattern);
|
|
var parts = std.mem.splitScalar(u8, path, '/');
|
|
while (parts.next()) |part| {
|
|
if (std.mem.eql(u8, part, pattern)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn freeItems(allocator: std.mem.Allocator, items: [][]const u8) void {
|
|
allocator.free(items);
|
|
}
|
|
|
|
fn freeOwnedItems(allocator: std.mem.Allocator, items: []const []const u8) void {
|
|
for (items) |item| allocator.free(item);
|
|
allocator.free(items);
|
|
}
|
|
|
|
test "regular: picker respects gitignore by default and can include ignored" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
try repo.addIgnorePattern("zig-out/");
|
|
try repo.addIgnorePattern("*.o");
|
|
try repo.addIgnorePattern("secret.txt");
|
|
try repo.addFile("src/main.zig", "pub fn main() void {}");
|
|
try repo.addFile("zig-out/bin/mim", "binary");
|
|
try repo.addFile("build.o", "object");
|
|
try repo.addFile("secret.txt", "hidden");
|
|
|
|
{
|
|
const items = try repo.pickerItemsAlloc(std.testing.allocator, false);
|
|
defer freeItems(std.testing.allocator, items);
|
|
try std.testing.expectEqual(@as(usize, 1), items.len);
|
|
try std.testing.expectEqualStrings("src/main.zig", items[0]);
|
|
}
|
|
|
|
{
|
|
const items = try repo.pickerItemsAlloc(std.testing.allocator, true);
|
|
defer freeItems(std.testing.allocator, items);
|
|
try std.testing.expectEqual(@as(usize, 4), items.len);
|
|
}
|
|
}
|
|
|
|
test "regular: tree rows use the same gitignore boundary as picker rows" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
try repo.addIgnorePattern("tmp/");
|
|
try repo.addFile("src/main.zig", "main");
|
|
try repo.addFile("tmp/log.txt", "log");
|
|
|
|
const rows = try repo.treeItemsAlloc(std.testing.allocator, false);
|
|
defer freeItems(std.testing.allocator, rows);
|
|
try std.testing.expectEqual(@as(usize, 1), rows.len);
|
|
try std.testing.expectEqualStrings("src/main.zig", rows[0]);
|
|
}
|
|
|
|
test "regular: selected file content is retrievable" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
try repo.addFile("src/main.zig", "const std = @import(\"std\");");
|
|
|
|
try std.testing.expectEqualStrings("const std = @import(\"std\");", try repo.content("src/main.zig"));
|
|
}
|
|
|
|
test "adversarial: invalid paths and patterns are rejected" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
|
|
try std.testing.expectError(Error.InvalidPath, repo.addFile("../secret", "x"));
|
|
try std.testing.expectError(Error.InvalidPath, repo.addFile("two words", "x"));
|
|
try std.testing.expectError(Error.InvalidPattern, repo.addIgnorePattern("bad pattern"));
|
|
try std.testing.expectError(Error.FileNotFound, repo.content("missing.zig"));
|
|
}
|
|
|
|
test "regular: project search returns file line column and preview rows" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
try repo.addIgnorePattern("zig-out/");
|
|
try repo.addFile("src/main.zig", "pub fn main() void {}\nconst needle = 1;");
|
|
try repo.addFile("src/lib.zig", "needle here");
|
|
try repo.addFile("zig-out/log.txt", "needle ignored");
|
|
|
|
const rows = try repo.searchRowsAlloc(std.testing.allocator, "needle", false);
|
|
defer freeOwnedItems(std.testing.allocator, rows);
|
|
try std.testing.expectEqual(@as(usize, 2), rows.len);
|
|
try std.testing.expectEqualStrings("src/main.zig:2:7:const_needle_=_1;", rows[0]);
|
|
try std.testing.expectEqualStrings("src/lib.zig:1:1:needle_here", rows[1]);
|
|
}
|
|
|
|
test "regular: project search can include ignored files explicitly" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
try repo.addIgnorePattern("zig-out/");
|
|
try repo.addFile("src/main.zig", "needle");
|
|
try repo.addFile("zig-out/log.txt", "needle");
|
|
|
|
const rows = try repo.searchRowsAlloc(std.testing.allocator, "needle", true);
|
|
defer freeOwnedItems(std.testing.allocator, rows);
|
|
try std.testing.expectEqual(@as(usize, 2), rows.len);
|
|
}
|
|
|
|
test "regular: search row resolves back to file and byte offset" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
try repo.addFile("src/main.zig", "abc\nlet needle = 1;\n");
|
|
|
|
const rows = try repo.searchRowsAlloc(std.testing.allocator, "needle", false);
|
|
defer freeOwnedItems(std.testing.allocator, rows);
|
|
const resolved = try repo.searchOffsetFromRow(rows[0]);
|
|
try std.testing.expectEqualStrings("src/main.zig", resolved.path);
|
|
try std.testing.expectEqual(@as(usize, 8), resolved.offset);
|
|
}
|
|
|
|
test "adversarial: project search rejects bad queries and rows" {
|
|
var repo = Index.init(std.testing.allocator);
|
|
defer repo.deinit();
|
|
try repo.addFile("src/main.zig", "needle");
|
|
|
|
try std.testing.expectError(Error.InvalidQuery, repo.searchRowsAlloc(std.testing.allocator, "", false));
|
|
try std.testing.expectError(Error.InvalidQuery, repo.searchRowsAlloc(std.testing.allocator, "two words", false));
|
|
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"));
|
|
}
|