diff --git a/build.zig b/build.zig index a5f528b..478a7b5 100644 --- a/build.zig +++ b/build.zig @@ -87,32 +87,46 @@ pub fn build(b: *std.Build) !void { var test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_lib_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); - { - const helpgen_exe = b.addExecutable(.{ - .name = "helpgen", + + // Setup helpgen for generating help_strings module + const helpgen_exe = b.addExecutable(.{ + .name = "helpgen", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cli/helpgen.zig"), + .target = b.graph.host, + }), + }); + const help_run = b.addRunArtifact(helpgen_exe); + const help_strings_stdout = help_run.captureStdOut(); + // Workaround for Zig 0.15 regression: module files need .zig extension + // See: https://github.com/ziglang/zig/issues/24957 + const wf = b.addWriteFiles(); + const help_strings_output = wf.addCopyFile(help_strings_stdout, "help_strings.zig"); + exe_mod.addAnonymousImport( + "help_strings", + .{ + .root_source_file = help_strings_output, + }, + ); + var helpgen_step = b.step("helpgen", "Generate Help Text"); + helpgen_step.dependOn(&help_run.step); + + const integration_tests_step = step: { + const integration_tests = b.addExecutable(.{ + .name = "sparse-integration-tests", .root_module = b.createModule(.{ - .root_source_file = b.path("src/cli/helpgen.zig"), - .target = b.graph.host, + .root_source_file = b.path("test/integration.zig"), + .optimize = optimize, + .target = target, }), }); - const help_run = b.addRunArtifact(helpgen_exe); - const output = help_run.captureStdOut(); - exe.root_module.addAnonymousImport( + integration_tests.root_module.addImport("sparse", exe_mod); + integration_tests.root_module.addAnonymousImport( "help_strings", .{ - .root_source_file = output, + .root_source_file = help_strings_output, }, ); - } - - const integration_tests_step = step: { - const integration_tests = b.addExecutable(.{ - .name = "sparse-integration-tests", - .root_source_file = b.path("test/integration.zig"), - .optimize = optimize, - .target = target, - }); - integration_tests.root_module.addImport("sparse", exe_mod); const integration_unit_tests = b.addTest(.{ .root_module = integration_tests.root_module, diff --git a/build.zig.zon b/build.zig.zon index 7e924ee..3d72924 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,9 +2,12 @@ .name = .sparse, .version = "0.0.0", .fingerprint = 0x2883adb9d90cf982, // Changing this has security and trust implications. - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.15.2", .dependencies = .{ - .libgit2 = .{ .path = "vendored/libgit2" }, + .libgit2 = .{ + .url = "git+https://github.com/allyourcodebase/libgit2#58dfd002d47a8c9fbd99d4a939cb343172590e1b", + .hash = "libgit2-1.9.0-uizqTQnZAADyPOmBklxzj_lrnfJoRLRDBbbTvi28SrZX", + }, .apple_sdk = .{ .path = "vendored/apple-sdk" }, }, diff --git a/flake.lock b/flake.lock index 485c4cf..2b2d7af 100644 --- a/flake.lock +++ b/flake.lock @@ -41,17 +41,17 @@ }, "zig-nixpkgs": { "locked": { - "lastModified": 1744502386, - "narHash": "sha256-QAd1L37eU7ktL2WeLLLTmI6P9moz9+a/ONO8qNBYJgM=", + "lastModified": 1767151656, + "narHash": "sha256-ujL2AoYBnJBN262HD95yer7QYUmYp5kFZGYbyCCKxq8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f6db44a8daa59c40ae41ba6e5823ec77fe0d2124", + "rev": "f665af0cdb70ed27e1bd8f9fdfecaf451260fc55", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", - "rev": "f6db44a8daa59c40ae41ba6e5823ec77fe0d2124", + "rev": "f665af0cdb70ed27e1bd8f9fdfecaf451260fc55", "type": "github" } } diff --git a/flake.nix b/flake.nix index 54c8ed5..20fa3ba 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ flake-utils.url = "github:numtide/flake-utils"; # 0.14.0 - zig-nixpkgs.url = "github:NixOS/nixpkgs/f6db44a8daa59c40ae41ba6e5823ec77fe0d2124"; + zig-nixpkgs.url = "github:NixOS/nixpkgs/f665af0cdb70ed27e1bd8f9fdfecaf451260fc55"; # 1.9.0 # libgit2-nixpkgs.url = "github:NixOS/nixpkgs/f6db44a8daa59c40ae41ba6e5823ec77fe0d2124"; }; @@ -115,6 +115,7 @@ devShells.default = zig-nixpkgs.mkShell { packages = [ zig-nixpkgs.zig + zig-nixpkgs.zls zig-nixpkgs.openssl ]; diff --git a/src/cli.zig b/src/cli.zig index 63174b5..505324b 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -11,9 +11,15 @@ fn getCommandDescription(command_name: []const u8) []const u8 { return "Unknown command"; } -fn showHelp() void { - const writer = std.io.getStdOut().writer(); - +fn showHelp() !void { + //const writer = std.fs.File.stdout().writer(stdout) + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; + //TODO: add error log to flush + defer { + writer.flush() catch {}; + } writer.print("Sparse - A CLI tool for stacked pull request workflows\n\n", .{}) catch return; writer.print("USAGE:\n sparse [options]\n\n", .{}) catch return; writer.print("COMMANDS:\n", .{}) catch return; @@ -35,6 +41,13 @@ fn showHelp() void { } fn parse(args: [][:0]u8) !Command { + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; + // TODO: add err log to defer + defer { + writer.flush() catch {}; + } const my_commands = @typeInfo(Command).@"union".fields; if (args.len < 2) { @@ -43,7 +56,7 @@ fn parse(args: [][:0]u8) !Command { // Check for global --help flag if (std.mem.eql(u8, args[1], "--help")) { - showHelp(); + try showHelp(); std.process.exit(0); } @@ -56,25 +69,32 @@ fn parse(args: [][:0]u8) !Command { } pub fn run(alloc: Allocator) !void { + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + var writer = &stdout_writer.interface; + //TODO: add error log to flush + defer { + writer.flush() catch {}; + } const args = try std.process.argsAlloc(alloc); defer std.process.argsFree(alloc, args); const command = parse(args) catch |err| switch (err) { CommandError.UnknownCommand => { - const stdout = std.io.getStdOut().writer(); + //const stdout = std.io.getStdOut().writer(); if (args.len >= 2) { - stdout.print("'{s}' is not a sparse command.\n\n", .{args[1]}) catch {}; + writer.print("'{s}' is not a sparse command.\n\n", .{args[1]}) catch {}; } else { - stdout.print("No command specified.\n\n", .{}) catch {}; + writer.print("No command specified.\n\n", .{}) catch {}; } - stdout.print("Available commands: ", .{}) catch {}; + writer.print("Available commands: ", .{}) catch {}; const my_commands = @typeInfo(Command).@"union".fields; inline for (my_commands, 0..) |c, i| { - if (i > 0) stdout.print(", ", .{}) catch {}; - stdout.print("{s}", .{c.name}) catch {}; + if (i > 0) writer.print(", ", .{}) catch {}; + writer.print("{s}", .{c.name}) catch {}; } - stdout.print("\n\nFor more help: sparse --help\n", .{}) catch {}; + writer.print("\n\nFor more help: sparse --help\n", .{}) catch {}; std.process.exit(1); }, else => return err, @@ -82,10 +102,10 @@ pub fn run(alloc: Allocator) !void { const return_code = try command.run(alloc); std.process.exit(return_code); } - +// TODO: add tests about writer test "parse a non existent command" { const expectEqual = std.testing.expectEqual; const args: [2][:0]const u8 = .{ "sparse", "boo" }; - const command = parse(@constCast(@ptrCast(&args))) catch |e| e; + const command = parse(@ptrCast(@constCast(&args))) catch |e| e; try expectEqual(CommandError.UnknownCommand, command); } diff --git a/src/cli/command.zig b/src/cli/command.zig index bb2404a..5058acb 100644 --- a/src/cli/command.zig +++ b/src/cli/command.zig @@ -1,5 +1,6 @@ const std = @import("std"); const log = std.log.scoped(.command); +//const FileWriter = std.Io.Writer; const Allocator = std.mem.Allocator; diff --git a/src/cli/feature_command.zig b/src/cli/feature_command.zig index a4f90b3..79874bb 100644 --- a/src/cli/feature_command.zig +++ b/src/cli/feature_command.zig @@ -26,7 +26,13 @@ const Params = struct { @"-h": *const fn () void = Options.help, pub fn help() void { - std.io.getStdOut().writer().print(help_strings.sparse_feature, .{}) catch return; + var buffer: [4096]u8 = .{0} ** 4096; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + defer { + stdout.flush() catch {}; + } + stdout.print(help_strings.sparse_feature, .{}) catch {}; } } = .{}, }; @@ -40,7 +46,9 @@ pub const FeatureCommand = struct { var params = Params{ .feature_name = undefined }; const args = try std.process.argsAlloc(alloc); defer std.process.argsFree(alloc, args); - log.debug("got cli arguments: {s}", .{args}); + for (args) |arg| { + log.debug("got cli arguments: {s}", .{arg}); + } const cli_positionals = command.parseOptions( @TypeOf(params._options), diff --git a/src/cli/helpgen.zig b/src/cli/helpgen.zig index 0d6b476..6863240 100644 --- a/src/cli/helpgen.zig +++ b/src/cli/helpgen.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Writer = std.Io.Writer; const Tag = std.zig.Token.Tag; const Ast = std.zig.Ast; const Allocator = std.mem.Allocator; @@ -6,21 +7,29 @@ const Allocator = std.mem.Allocator; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const alloc = gpa.allocator(); - const stdout = std.io.getStdOut().writer(); + var buffer: [4096]u8 = .{0} ** 4096; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + defer { + stdout.flush() catch {}; + } try genCommands(alloc, stdout); } -fn genCommands(alloc: std.mem.Allocator, writer: anytype) !void { +fn genCommands(alloc: std.mem.Allocator, writer: *Writer) !void { try extractFileIntoHelp(alloc, writer, "feature_command.zig", "sparse_feature"); try extractFileIntoHelp(alloc, writer, "slice_command.zig", "sparse_slice"); try extractFileIntoHelp(alloc, writer, "update_command.zig", "sparse_update"); try extractFileIntoHelp(alloc, writer, "status_command.zig", "sparse_status"); } -fn extractFileIntoHelp(alloc: Allocator, writer: anytype, comptime zig_file: []const u8, comptime const_name: []const u8) !void { +fn extractFileIntoHelp(alloc: Allocator, writer: *Writer, comptime zig_file: []const u8, comptime const_name: []const u8) !void { var ast = try Ast.parse(alloc, @embedFile(zig_file), .zig); defer ast.deinit(alloc); + defer { + writer.flush() catch {}; + } const tokens = ast.tokens.items(.tag); const maybe_params_struct = findToken(ast, tokens, isParams); if (maybe_params_struct) |params_struct| { @@ -65,15 +74,15 @@ fn isParams(tt: []Tag, current_index: usize, a: Ast) bool { /// First token must be .l_brace fn extractNextStruct(alloc: Allocator, ast: Ast, start_idx: usize) ![]const u8 { - var stack = std.ArrayList(Tag).init(alloc); - defer stack.deinit(); - var lines = std.ArrayList([]const u8).init(alloc); - defer lines.deinit(); + var stack = std.ArrayList(Tag).empty; + defer stack.deinit(alloc); + var lines = std.ArrayList([]const u8).empty; + defer lines.deinit(alloc); const tokens = ast.tokens.items(.tag); for (tokens[start_idx..], start_idx..) |token, i| { //std.debug.print("Found {} name: {s}\n", .{ token, ast.tokenSlice(@intCast(i)) }); - if (token == .l_brace) _ = try stack.append(token); + if (token == .l_brace) _ = try stack.append(alloc, token); if (token == .r_brace) _ = stack.pop(); if (stack.items.len == 0) break; @@ -81,15 +90,15 @@ fn extractNextStruct(alloc: Allocator, ast: Ast, start_idx: usize) ![]const u8 { if (token != .identifier) continue; if (tokens[i - 2] != .doc_comment and tokens[i - 1] != .doc_comment) continue; const extracted = try extractDocComments(alloc, ast, @intCast(i), tokens); - try lines.append(extracted); + try lines.append(alloc, extracted); } - var buffer = std.ArrayList(u8).init(alloc); - defer buffer.deinit(); + var buffer = std.ArrayList(u8).empty; + defer buffer.deinit(alloc); for (lines.items) |line| { - try buffer.writer().print("{s}", .{line}); + try buffer.print(alloc, "{s}", .{line}); } - return buffer.toOwnedSlice(); + return buffer.toOwnedSlice(alloc); } fn extractDocComments( @@ -107,24 +116,22 @@ fn extractDocComments( } else unreachable; // Go through and build up the lines. - var lines = std.ArrayList([]const u8).init(alloc); - defer lines.deinit(); + var lines = std.ArrayList([]const u8).empty; + defer lines.deinit(alloc); for (start_idx..index + 1) |i| { const token = tokens[i]; if (token != .doc_comment) break; - try lines.append(ast.tokenSlice(@intCast(i))[3..]); + try lines.append(alloc, ast.tokenSlice(@intCast(i))[3..]); } // Convert the lines to a multiline string. - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); + var buffer = std.ArrayList(u8).empty; const prefix = findCommonPrefix(lines); for (lines.items) |line| { - try writer.writeAll(line[@min(prefix, line.len)..]); - try writer.writeAll("\n"); + try buffer.print(alloc, "{s}\n", .{line[@min(prefix, line.len)..]}); } - return buffer.toOwnedSlice(); + return buffer.toOwnedSlice(alloc); } fn findCommonPrefix(lines: std.ArrayList([]const u8)) usize { diff --git a/src/cli/slice_command.zig b/src/cli/slice_command.zig index 8763d20..23b4c85 100644 --- a/src/cli/slice_command.zig +++ b/src/cli/slice_command.zig @@ -20,7 +20,14 @@ const Params = struct { @"-h": *const fn () void = Options.help, pub fn help() void { - std.io.getStdOut().writer().print(help_strings.sparse_slice, .{}) catch return; + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + // TODO: add err log + defer { + stdout.flush() catch {}; + } + stdout.print(help_strings.sparse_slice, .{}) catch {}; } } = .{}, }; @@ -38,7 +45,10 @@ pub const SliceCommand = struct { var params = Params{ .slice_name = undefined }; const args = try std.process.argsAlloc(alloc); defer std.process.argsFree(alloc, args); - log.debug("run:: args: {s}", .{args}); + for (args) |arg| { + log.debug("got cli arguments: {s}", .{arg}); + } + //log.debug("run:: args: {any}", .{args}); const cli_positionals = command.parseOptions( @TypeOf(params._options), diff --git a/src/cli/status_command.zig b/src/cli/status_command.zig index 357af6f..ae570c3 100644 --- a/src/cli/status_command.zig +++ b/src/cli/status_command.zig @@ -16,7 +16,14 @@ const Params = struct { @"-h": *const fn () void = Options.help, pub fn help() void { - std.io.getStdOut().writer().print(help_strings.sparse_status, .{}) catch return; + //std.io.getStdOut().writer().print(help_strings.sparse_status, .{}) catch return; + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + defer { + stdout.flush() catch {}; + } + stdout.print(help_strings.sparse_status, .{}) catch {}; } } = .{}, }; @@ -30,7 +37,10 @@ pub const StatusCommand = struct { var params = Params{}; const args = try std.process.argsAlloc(alloc); defer std.process.argsFree(alloc, args); - log.debug("run:: args: {s}", .{args}); + //log.debug("run:: args: {any}", .{args}); + for (args) |arg| { + log.debug("got cli arguments: {s}", .{arg}); + } const cli_positionals = command.parseOptions( @TypeOf(params._options), diff --git a/src/cli/update_command.zig b/src/cli/update_command.zig index ede5aa4..929d902 100644 --- a/src/cli/update_command.zig +++ b/src/cli/update_command.zig @@ -23,7 +23,15 @@ const Params = struct { @"-h": *const fn () void = Options.help, pub fn help() void { - std.io.getStdOut().writer().print(help_strings.sparse_update, .{}) catch return; + //std.io.getStdOut().writer().print(help_strings.sparse_update, .{}) catch return; + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + stdout.print(help_strings.sparse_slice, .{}) catch {}; + defer { + stdout.flush() catch {}; + } + //try stdout.flush(); } } = .{}, }; @@ -37,7 +45,10 @@ pub const UpdateCommand = struct { var params = Params{}; const args = try std.process.argsAlloc(alloc); defer std.process.argsFree(alloc, args); - log.debug("run:: args: {s}", .{args}); + //log.debug("run:: args: {any}", .{args}); + for (args) |arg| { + log.debug("got cli arguments: {s}", .{arg}); + } const cli_positionals = command.parseOptions( @TypeOf(params._options), diff --git a/src/lib/Feature.zig b/src/lib/Feature.zig index c40a02e..616e1c4 100644 --- a/src/lib/Feature.zig +++ b/src/lib/Feature.zig @@ -22,10 +22,10 @@ pub fn new(o: struct { }; if (o.slices) |s| { if (f.slices) |*fs| { - try fs.appendSlice(s); + try fs.appendSlice(o.alloc, s); } else { f.slices = try std.ArrayList(Slice).initCapacity(o.alloc, s.len); - try f.slices.?.appendSlice(s); + try f.slices.?.appendSlice(o.alloc, s); } const orphan_count, const forked_count = try Slice.constructLinks( @@ -98,9 +98,10 @@ pub fn target(self: Feature, alloc: Allocator) !?GitReference { pub fn free(self: *Feature, allocator: Allocator) void { allocator.free(self.ref_name); - if (self.slices) |s| { + //TODO: look this line didn't understand + if (self.slices) |*s| { for (s.items) |*i| i.free(allocator); - s.deinit(); + s.deinit(allocator); } allocator.free(self.name); } @@ -368,14 +369,14 @@ test "asFeatureRefName" { const expectEqualStrings = std.testing.expectEqualStrings; const allocator = std.testing.allocator; { - const res = try asFeatureRefName(allocator, "refs/heads/sparse/talhaHavadar/test/slice/1"); + const res = try asFeatureRefName(allocator, "refs/heads/sparse/bahanurenis/test/slice/1"); defer allocator.free(res); - try expectEqualStrings("refs/heads/sparse/talhaHavadar/test", res); + try expectEqualStrings("refs/heads/sparse/bahanurenis/test", res); } { const res = try asFeatureRefName(allocator, "test"); defer allocator.free(res); - try expectEqualStrings("refs/heads/sparse/talhaHavadar/test", res); + try expectEqualStrings("refs/heads/sparse/bahanurenis/test", res); } } diff --git a/src/lib/slice.zig b/src/lib/slice.zig index ca77044..d61a4b6 100644 --- a/src/lib/slice.zig +++ b/src/lib/slice.zig @@ -1,6 +1,6 @@ const std = @import("std"); const log = std.log.scoped(.slice); -const ArrayListUnmanaged = std.ArrayListUnmanaged; +const ArrayList = std.ArrayList; const Allocator = std.mem.Allocator; const StringHashMap = std.StringHashMap; @@ -8,7 +8,7 @@ pub const Slice = struct { repo: GitRepository, ref: GitReference, target: ?*Slice = null, - children: ArrayListUnmanaged(*Slice) = ArrayListUnmanaged(*Slice).empty, + children: ArrayList(*Slice) = .empty, /// cache to hold already calculated isMerged calls _is_merge_into_map: StringHashMap(bool), @@ -110,7 +110,7 @@ pub const Slice = struct { alloc: Allocator, slice_pool: []Slice, }) ![]*Slice { - var leaves = ArrayListUnmanaged(*Slice).empty; + var leaves = ArrayList(*Slice).empty; defer leaves.deinit(o.alloc); for (o.slice_pool) |*s| { @@ -121,9 +121,14 @@ pub const Slice = struct { return try leaves.toOwnedSlice(o.alloc); } - pub fn printSliceGraph(writer: anytype, slice_pool: []Slice) !void { + pub fn printSliceGraph(writer: *std.Io.Writer, slice_pool: []Slice) !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer std.debug.assert(gpa.deinit() == .ok); + defer { + writer.flush() catch |e| { + log.err("Writer failed with {t}", .{e}); + }; + } const allocator = gpa.allocator(); // find leaf nodes @@ -136,13 +141,13 @@ pub const Slice = struct { defer allocator.free(leaves); for (leaves, 0..) |l, leaf_index| { - var slice_chain = std.ArrayList(*Slice).init(allocator); - defer slice_chain.deinit(); + var slice_chain: std.ArrayList(*Slice) = .empty; //std.ArrayList(*Slice).init(allocator); + defer slice_chain.deinit(allocator); // Build the chain from leaf to root var current_slice: ?*Slice = l; while (current_slice != null) { - try slice_chain.append(current_slice.?); + try slice_chain.append(allocator, current_slice.?); current_slice = current_slice.?.target; } diff --git a/src/lib/sparse.zig b/src/lib/sparse.zig index 4afa8ce..9cee193 100644 --- a/src/lib/sparse.zig +++ b/src/lib/sparse.zig @@ -30,7 +30,7 @@ pub fn feature( slice_name: ?[]const u8, target: []const u8, ) !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer std.debug.assert(gpa.deinit() == .ok); const allocator = gpa.allocator(); try LibGit.init(); @@ -44,7 +44,7 @@ pub fn feature( const _slice = if (slice_name) |s| s else constants.LAST_SLICE_NAME_POINTER; - // once sparse branchinde olup olmadigimizi kontrol edelim + // Check if we are at sparse slice branch // git show-ref --branches --head # butun branchleri ve suan ki HEAD i gormemizi // sagliyor var maybe_active_feature = try Feature.activeFeature(.{ @@ -222,16 +222,21 @@ pub fn update(o: struct { // and updated to reflect new commit IDs after the update try LibGit.init(); defer LibGit.shutdown() catch @panic("Oops: couldn't shutdown libgit2, something weird is cooking..."); + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + defer { + stdout.flush() catch {}; + } const repo = try LibGit.GitRepository.open(); defer repo.free(); - var state = try State.Update.load(o.alloc, repo); defer state.free(o.alloc); // Check if a rebase is in progress const is_rebase_in_progress = try Git.isRebaseInProgress(o.alloc, repo); if (is_rebase_in_progress) { log.err("update:: rebase in progress", .{}); - const stdout = std.io.getStdOut().writer(); + //const stdout = std.io.getStdOut().writer(); try stdout.print("⚠️ A rebase is currently in progress.\n", .{}); try stdout.print("Please resolve any conflicts and run:\n", .{}); try stdout.print(" git rebase --continue\n", .{}); @@ -250,14 +255,14 @@ pub fn update(o: struct { } else { if (!o.@"continue") { log.err("update:: not able to detect current branch", .{}); - const stdout = std.io.getStdOut().writer(); + //const stdout = std.io.getStdOut().writer(); try stdout.print("❌ Unable to detect current sparse feature.\n", .{}); try stdout.print("Make sure you're on a sparse feature branch before running update.\n", .{}); return Error.UNABLE_TO_DETECT_CURRENT_FEATURE; } if (!state.inProgress()) { try state.delete(); - const stdout = std.io.getStdOut().writer(); + // const stdout = std.io.getStdOut().writer(); try stdout.print("❌ No update in progress to continue.\n", .{}); try stdout.print("Run 'sparse update' without --continue to start a new update.\n", .{}); return Error.NO_UPDATE_IN_PROGRESS; @@ -312,7 +317,12 @@ pub fn status(o: struct { try LibGit.init(); defer LibGit.shutdown() catch @panic("Oops: couldn't shutdown libgit2, something weird is cooking..."); - const stdout = std.io.getStdOut().writer(); + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + defer { + stdout.flush() catch {}; + } log.debug("status:: checking for active feature", .{}); // Check if we are currently on an active feature @@ -321,6 +331,7 @@ pub fn status(o: struct { if (active_feature == null) { try stdout.print("\n🚫 \x1b[33mNo active sparse feature detected\x1b[0m\n", .{}); try stdout.print(" Use `sparse feature ` to create or switch to a feature\n\n", .{}); + //try stdout.flush(); return; } @@ -433,6 +444,7 @@ pub fn status(o: struct { try stdout.print("│ 📊 Total slices: \x1b[1m{d}\x1b[0m\n", .{slices.len}); try stdout.print("│\n", .{}); try stdout.print("└─ \x1b[2mℹ Note: Cannot check merge status without a target reference\x1b[0m\n\n", .{}); + //try stdout.flush(); return; } @@ -546,9 +558,15 @@ fn updateGoodWeather(o: struct { }) !void { const target = try o.feature.target(o.alloc); o.state.free(o.alloc); + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + defer { + stdout.flush() catch {}; + } // Print update information - const stdout = std.io.getStdOut().writer(); + //const stdout = std.io.getStdOut().writer(); try stdout.print("Updating feature '{s}' from target '{s}'\n", .{ o.feature.name, target.?.name() }); try fetchTarget(.{ .alloc = o.alloc, .target = target.? }); @@ -611,6 +629,7 @@ fn updateGoodWeather(o: struct { .args = &.{ "--oneline", "--decorate", "--graph", log_range }, }) catch |err| { try stdout.print("Unable to show commit log: {}\n", .{err}); + //try stdout.flush(); return err; }; defer o.alloc.free(log_result.stdout); @@ -637,8 +656,8 @@ fn updateGoodWeather(o: struct { // push all unmerged slices in remotes ss = leaves[0]; - var pushed_slices = std.ArrayList(*Slice).init(o.alloc); - defer pushed_slices.deinit(); + var pushed_slices: std.ArrayList(*Slice) = .empty; //std.ArrayList(*Slice).init(o.alloc); + defer pushed_slices.deinit(o.alloc); while (ss != null) : (ss = ss.?.target) { const is_merged = try ss.?.isMerged(.{ @@ -648,7 +667,7 @@ fn updateGoodWeather(o: struct { if (!is_merged) { try ss.?.activate(o.alloc); try ss.?.push(o.alloc); - try pushed_slices.append(ss.?); + try pushed_slices.append(o.alloc, ss.?); } else break; } @@ -687,7 +706,14 @@ fn updateGoodWeather(o: struct { } fn handleUpdateInProgress(alloc: std.mem.Allocator, state: *State.Update) !void { - const stdout = std.io.getStdOut().writer(); + // const stdout = std.io.getStdOut().writer(); + + var buffer: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + defer { + stdout.flush() catch {}; + } try stdout.print("🔄 Continuing update for feature '{s}'...\n", .{state._data.feature.?}); var feature_updated = try Feature.findFeatureByName(.{ @@ -709,6 +735,7 @@ fn handleUpdateInProgress(alloc: std.mem.Allocator, state: *State.Update) !void try updateGoodWeather(.{ .alloc = alloc, .feature = f, .state = state }); } else { try stdout.print("❌ Unable to find feature to continue update\n", .{}); + //try stdout.flush(); return Error.UNABLE_TO_DETECT_CURRENT_FEATURE; } return Error.UNABLE_TO_DETECT_CURRENT_FEATURE; @@ -728,8 +755,8 @@ fn handleUpdateInProgress(alloc: std.mem.Allocator, state: *State.Update) !void const upstream = try target.upstream(ss.?.repo); defer upstream.free(); - var pushed_slices = std.ArrayList(*Slice).init(alloc); - defer pushed_slices.deinit(); + var pushed_slices: std.ArrayList(*Slice) = .empty; //std.ArrayList(*Slice).init(alloc); + defer pushed_slices.deinit(alloc); while (ss != null) : (ss = ss.?.target) { const is_merged = try ss.?.isMerged(.{ @@ -739,7 +766,7 @@ fn handleUpdateInProgress(alloc: std.mem.Allocator, state: *State.Update) !void if (!is_merged) { try ss.?.activate(alloc); try ss.?.push(alloc); - try pushed_slices.append(ss.?); + try pushed_slices.append(alloc, ss.?); } else break; } @@ -759,11 +786,13 @@ fn handleUpdateInProgress(alloc: std.mem.Allocator, state: *State.Update) !void } else { try stdout.print("❌ Unable to find feature to continue update\n", .{}); } + //try stdout.flush(); }, .Complete => { log.debug("update:: failed when complete command is called before, no need to continue updating", .{}); try stdout.print("✓ Update already completed - nothing to continue\n", .{}); try state.delete(); + //try stdout.flush(); }, } } @@ -828,7 +857,7 @@ fn jump(o: struct { } // Helper function to display git notes information for slices -fn displayGitNotesInfo(alloc: std.mem.Allocator, writer: anytype, slices: []Slice) !void { +fn displayGitNotesInfo(alloc: std.mem.Allocator, stdout: *std.Io.Writer, slices: []Slice) !void { // Track if any notes were found var notes_found = false; @@ -851,7 +880,7 @@ fn displayGitNotesInfo(alloc: std.mem.Allocator, writer: anytype, slices: []Slic notes_with_parents += 1; const slice_name = slice_item.name(); - try writer.print("│ 📝 \x1b[1m{s}:\x1b[0m parent → \x1b[32m{s}\x1b[0m\n", .{ slice_name, parent_info }); + try stdout.print("│ 📝 \x1b[1m{s}:\x1b[0m parent → \x1b[32m{s}\x1b[0m\n", .{ slice_name, parent_info }); } else { // No note exists for this slice notes_without_parents += 1; @@ -864,19 +893,20 @@ fn displayGitNotesInfo(alloc: std.mem.Allocator, writer: anytype, slices: []Slic // Show summary of notes status if (notes_found) { - try writer.print("│ ✅ Slices with parent notes: \x1b[1;32m{d}\x1b[0m\n", .{notes_with_parents}); + try stdout.print("│ ✅ Slices with parent notes: \x1b[1;32m{d}\x1b[0m\n", .{notes_with_parents}); if (notes_without_parents > 0) { - try writer.print("│ ⚠️ Slices without parent notes: \x1b[1;33m{d}\x1b[0m\n", .{notes_without_parents}); + try stdout.print("│ ⚠️ Slices without parent notes: \x1b[1;33m{d}\x1b[0m\n", .{notes_without_parents}); } } else { - try writer.print("│ 📄 No git notes found for slice relationships\n", .{}); - try writer.print("│ 💡 \x1b[2mTip: Use git notes to preserve relationships after rebasing\x1b[0m\n", .{}); + try stdout.print("│ 📄 No git notes found for slice relationships\n", .{}); + try stdout.print("│ 💡 \x1b[2mTip: Use git notes to preserve relationships after rebasing\x1b[0m\n", .{}); } // Show instructions for team collaboration if notes exist if (notes_found) { - try writer.print("│ \x1b[2m💡 Team tip: Push notes with 'git push origin refs/notes/commits'\x1b[0m\n", .{}); + try stdout.print("│ \x1b[2m💡 Team tip: Push notes with 'git push origin refs/notes/commits'\x1b[0m\n", .{}); } + //try stdout.flush(); } test { diff --git a/src/lib/state.zig b/src/lib/state.zig index 5c79af6..d130de2 100644 --- a/src/lib/state.zig +++ b/src/lib/state.zig @@ -37,7 +37,9 @@ pub const Update = struct { const file = try state_dir.createFile(file_name, .{}); defer file.close(); - try std.zon.stringify.serialize(self._data, .{}, file.writer()); + var buffer: [4096]u8 = .{0} ** 4096; + var fw = file.writer(&buffer); + try std.zon.stringify.serialize(self._data, .{}, &fw.interface); } pub fn delete(self: Update) !void { diff --git a/src/lib/system/Git.zig b/src/lib/system/Git.zig index bef8281..9d5c2ea 100644 --- a/src/lib/system/Git.zig +++ b/src/lib/system/Git.zig @@ -19,7 +19,7 @@ pub const Ref = struct { oname: []const u8, rname: []const u8, }) !Ref { - logger.debug("Ref::new:: objectname:{s} refname:{s}", .{ o.oname, o.rname }); + logger.debug("Ref::new:: objectname:{any} refname:{any}", .{ o.oname, o.rname }); // duping strings since we got them from RunResult which we free before returning const oname = try o.alloc.dupe(u8, o.oname); @@ -68,7 +68,7 @@ pub fn isRebaseInProgress(alloc: std.mem.Allocator, repo: GitRepository) !bool { &.{ git_dir, "rebase-merge" }, ); defer alloc.free(rebase_merge_path); - logger.debug("isRebaseInProgress:: rebase_merge_path: {s}", .{rebase_merge_path}); + logger.debug("isRebaseInProgress:: rebase_merge_path: {any}", .{rebase_merge_path}); // We wont open the file afterwards so it is ok to check the existence of the directory std.fs.accessAbsolute(rebase_merge_path, .{ .mode = .read_only }) catch |err| { @@ -81,7 +81,7 @@ pub fn isRebaseInProgress(alloc: std.mem.Allocator, repo: GitRepository) !bool { &.{ git_dir, "rebase-apply" }, ); defer alloc.free(rebase_apply_path); - logger.debug("isRebaseInProgress:: rebase_apply_path: {s}", .{rebase_apply_path}); + logger.debug("isRebaseInProgress:: rebase_apply_path: {any}", .{rebase_apply_path}); std.fs.accessAbsolute( rebase_apply_path, @@ -231,7 +231,7 @@ fn @"for-each-ref"(o: struct { allocator: std.mem.Allocator, args: []const []const u8 = &.{}, }) !RunResult { - logger.debug("for-each-ref:: args:{s}", .{o.args}); + logger.debug("for-each-ref:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "for-each-ref", @@ -251,7 +251,7 @@ fn @"show-ref"(options: struct { allocator: std.mem.Allocator, args: []const []const u8 = &.{}, }) !RunResult { - logger.debug("show-ref:: args:{s}", .{options.args}); + logger.debug("show-ref:: args:{any}", .{options.args}); const command: []const []const u8 = &.{ "git", "show-ref", @@ -271,7 +271,7 @@ pub fn @"rev-parse"(o: struct { allocator: std.mem.Allocator, args: []const []const u8, }) !RunResult { - logger.debug("rev-parse:: args:{s}", .{o.args}); + logger.debug("rev-parse:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "rev-parse", @@ -289,7 +289,7 @@ pub fn @"switch"(o: struct { allocator: std.mem.Allocator, args: []const []const u8, }) !RunResult { - logger.debug("switch:: args:{s}", .{o.args}); + logger.debug("switch:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "switch", @@ -307,7 +307,7 @@ pub fn log(o: struct { allocator: std.mem.Allocator, args: []const []const u8, }) !RunResult { - logger.debug("log:: args:{s}", .{o.args}); + logger.debug("log:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "log", @@ -325,7 +325,7 @@ pub fn rebase(o: struct { allocator: std.mem.Allocator, args: []const []const u8, }) !RunResult { - logger.debug("rebase:: args:{s}", .{o.args}); + logger.debug("rebase:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "rebase", @@ -343,7 +343,7 @@ pub fn push(o: struct { allocator: std.mem.Allocator, args: []const []const u8, }) !RunResult { - logger.debug("push:: args:{s}", .{o.args}); + logger.debug("push:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "push", @@ -361,7 +361,7 @@ pub fn @"merge-base"(o: struct { allocator: std.mem.Allocator, args: []const []const u8, }) !RunResult { - logger.debug("merge-base:: args:{s}", .{o.args}); + logger.debug("merge-base:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "merge-base", @@ -379,7 +379,7 @@ pub fn fetch(o: struct { allocator: std.mem.Allocator, args: []const []const u8, }) !RunResult { - logger.debug("fetch:: args:{s}", .{o.args}); + logger.debug("fetch:: args:{any}", .{o.args}); const command: []const []const u8 = &.{ "git", "fetch", diff --git a/src/main.zig b/src/main.zig index a5729c7..0d10225 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,7 @@ const std = @import("std"); // Global log level based on environment variable -var runtime_log_level: ?std.log.Level = null; // Default to no logs +var runtime_log_level: ?std.log.Level = .debug; // Default to no logs pub const std_options: std.Options = .{ .logFn = logFn, @@ -25,7 +25,7 @@ pub fn logFn( } pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer std.debug.assert(gpa.deinit() == .ok); const allocator = gpa.allocator(); diff --git a/test/integration.zig b/test/integration.zig index a4c96b7..72abdee 100644 --- a/test/integration.zig +++ b/test/integration.zig @@ -1,68 +1,133 @@ const builtin = @import("builtin"); const std = @import("std"); -const assert = @import("std").debug.assert; +const RunResult = std.process.Child.RunResult; +const Allocator = std.mem.Allocator; const log = std.log.scoped(.integration); -const sparse = @import("sparse"); const build_options = @import("build_options"); +pub const IntegrationTestError = error{ + TERM_EXIT_FAILED, + UNEXPECTED_ERROR, + SPARSE_FEATURE_EMPTY_REF, + SPARSE_FEATURE_NOT_FOUND, + SPARSE_FEATURE_TARGET_MISMATCH, +}; + +pub const IntegrationTestResult = union(enum) { + feature: SparseFeatureTestResult, + pub fn status(self: IntegrationTestResult) bool { + switch (self) { + inline else => |test_result| return test_result.status(), + } + } +}; + +pub const IntegrationTest = union(enum) { + feature: SparseFeatureTest, + pub fn setup( + self: IntegrationTest, + alloc: Allocator, + comptime T: anytype, + ) !T { + switch (self) { + inline else => |integration_test| return try integration_test.setup( + alloc, + ), + } + } + + pub fn teardown( + self: IntegrationTest, + alloc: Allocator, + data: anytype, + ) !void { + switch (self) { + inline else => |integration_test| return integration_test.teardown( + alloc, + data, + ), + } + } + + pub fn run( + self: IntegrationTest, + alloc: Allocator, + comptime T: anytype, + data: T, + comptime func: fn (Allocator, T) IntegrationTestResult, + ) IntegrationTestResult { + switch (self) { + inline else => |integration_test| return try integration_test.run( + alloc, + T, + data, + func, + ), + } + } +}; + pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer std.debug.assert(gpa.deinit() == .ok); const allocator = gpa.allocator(); const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); + std.testing.log_level = .debug; + // maybe we should use it this approach in build.zig? + const repo_dir = try std.fs.path.join(allocator, &.{ build_options.output_dir, "sparse_test_repo" }); + log.debug("test::main:: repo dir {s}", .{repo_dir}); + defer allocator.free(repo_dir); +} + +test "Create Sparse Feature with only feature name" { + const test_allocator = std.testing.allocator; + const args = try std.process.argsAlloc(test_allocator); + for (args) |arg| { + log.debug("benis:::Create Sparse Feature with only name:: arg - {s}", .{arg}); + } + defer std.process.argsFree(test_allocator, args); + + const integration: IntegrationTest = undefined; + const feature_integration = @field( + integration, + "feature", + ); + var data: SparseFeatureTestData = try feature_integration.setup( + test_allocator, + SparseFeatureTestData, + ); + defer data.free(test_allocator); + // set a feature name + data.feature_name = "hellofeature"; log.debug( - "main:: args={s} build_options={s} output_dir={s}", + "test::data feature_name: {s}, repodir: {s} ,", .{ - args, - build_options.sparse_exe_path, - build_options.output_dir, + data.feature_name.?, + data.repo_dir.?, }, ); - const repo_dir = try std.fs.path.join(allocator, &.{ build_options.output_dir, "sparse_test_repo" }); - defer allocator.free(repo_dir); - { - const rr = try system.system(.{ - .allocator = allocator, - .args = &.{ - "mkdir", - "-p", - repo_dir, - }, - }); - defer allocator.free(rr.stdout); - defer allocator.free(rr.stderr); - } - { - const rr = try system.git(.{ - .allocator = allocator, - .args = &.{ "init", "." }, - .cwd = repo_dir, - }); - defer allocator.free(rr.stdout); - defer allocator.free(rr.stderr); - } - { - const rr = try system.system(.{ - .allocator = allocator, - .args = &.{ - "rm", - "-r", - repo_dir, - }, + const rr_feature_step = feature_integration.run( + test_allocator, + SparseFeatureTestData, + data, + sparse_feature_test.createFeatureStep, + ); + + if (!rr_feature_step.feature.status()) { + log.err("Test Failed with exit_code {d} {any}", .{ + rr_feature_step.feature.exit_code, + rr_feature_step.feature.error_context.?.err, }); - defer allocator.free(rr.stdout); - defer allocator.free(rr.stderr); } + try feature_integration.teardown(test_allocator, data); + try std.testing.expect(rr_feature_step.feature.exit_code == 0); } -test "Hello Integration" { - try std.testing.expect(false); -} - -test "Hello Integration2" { - try std.testing.expect(true); -} - +// TODO: remove this comment please const system = @import("system.zig"); +const SparseFeatureTest = @import("sparse_feature_test.zig").SparseFeatureTest; +const sparse_feature_test = @import("sparse_feature_test.zig"); +const SparseFeatureTestData = @import("sparse_feature_test.zig").TestData; +const SparseFeatureTestResult = @import("sparse_feature_test.zig").TestResult; diff --git a/test/sparse_feature_test.zig b/test/sparse_feature_test.zig new file mode 100644 index 0000000..ef2bb76 --- /dev/null +++ b/test/sparse_feature_test.zig @@ -0,0 +1,274 @@ +const std = @import("std"); +const build_options = @import("build_options"); +const log = std.log.scoped(.sparse_feature_test); +const Allocator = std.mem.Allocator; +const RunResult = std.process.Child.RunResult; +const TEST_SPARSE_USER_ID: []const u8 = "exampleUser"; + +pub const TestData = struct { + repo_dir: ?[]const u8 = null, + feature_name: ?[]const u8 = null, + feature_to: ?[]const u8 = null, + pub fn free(self: TestData, alloc: Allocator) void { + if (self.repo_dir) |repo_dir| { + alloc.free(repo_dir); + } + } +}; +pub const TestResult = struct { + error_context: ?struct { + err: ?IntegrationTestError = null, + err_msg: ?[]const u8 = "", + } = null, + // output: ?struct { + // feature_name: ?[]const u8 = null, + // feature_prefix: []const u8 = "refs/heads/sparse/", + // target: ?[]const u8 = null, + // user_config: ?[]const u8 = null, + // slice_prefix: []const u8 = "/slice/", + // slice_name: ?[]const u8 = null, + // } = null, + exit_code: u8 = 1, + + pub fn status(self: TestResult) bool { + return self.exit_code == 0; + } +}; + +pub const SparseFeatureTest = struct { + pub fn setup( + self: SparseFeatureTest, + alloc: Allocator, + comptime T: anytype, + ) !T { + _ = self; + var data: TestData = .{}; + std.testing.log_level = .debug; + const rr_temp_dir = try system.system(.{ + .allocator = alloc, + .args = &.{ "mktemp", "-d", "-p", ".zig-cache/tmp" }, + }); + defer alloc.free(rr_temp_dir.stdout); + defer alloc.free(rr_temp_dir.stderr); + + try std.testing.expect(rr_temp_dir.term.Exited == 0); + try std.testing.expect(std.mem.eql(u8, rr_temp_dir.stderr, "")); + try std.testing.expect(!std.mem.eql(u8, rr_temp_dir.stdout, "")); + + data.repo_dir = try alloc.dupe(u8, std.mem.trim(u8, rr_temp_dir.stdout, "\n\t \r")); + { + const rr = try system.git(.{ + .allocator = alloc, + .args = &.{ "init", "." }, + .cwd = data.repo_dir.?, + }); + defer alloc.free(rr.stdout); + defer alloc.free(rr.stderr); + try std.testing.expect(rr.term.Exited == 0); + } + { + const rr = try system.git(.{ + .allocator = alloc, + .args = &.{ "config", "sparse.user.id", TEST_SPARSE_USER_ID }, + .cwd = data.repo_dir.?, + }); + defer alloc.free(rr.stdout); + defer alloc.free(rr.stderr); + } + + log.debug( + "sparse::feature::test:: repo_dir {s}", + .{data.repo_dir.?}, + ); + + return @as(T, data); + } + + pub fn teardown( + self: SparseFeatureTest, + alloc: Allocator, + data: TestData, + ) !void { + _ = self; + _ = alloc; + + std.testing.log_level = .debug; + log.info("repo_dir {s}\n", .{data.repo_dir.?}); + // const rr_temp_dir = try system.system(.{ + // .allocator = alloc, + // .args = &.{ + // "rm", + // "-r", + // data.repo_dir.?, + // }, + // }); + // log.info("stdout {s}\n", .{rr_temp_dir.stdout}); + // defer alloc.free(rr_temp_dir.stdout); + // defer alloc.free(rr_temp_dir.stderr); + // + // try std.testing.expect(rr_temp_dir.term.Exited == 0); + // try std.testing.expect(std.mem.eql(u8, rr_temp_dir.stderr, "")); + // try std.testing.expect(std.mem.eql(u8, rr_temp_dir.stdout, "")); + } + pub fn run( + self: SparseFeatureTest, + alloc: Allocator, + comptime T: anytype, + data: T, + comptime func: fn (Allocator, T) IntegrationTestResult, + ) IntegrationTestResult { + _ = self; + return func(alloc, data); + } +}; + +pub fn createFeatureStep(alloc: Allocator, data: TestData) IntegrationTestResult { + std.testing.log_level = .debug; + var test_result: IntegrationTestResult = .{ + .feature = .{ + .exit_code = 1, + .error_context = .{ + .err = null, + .err_msg = null, + }, + }, + }; + // Resolve sparse exe path to absolute path (needed because test runs in temp dir) + const sparse_exe_abs = std.fs.cwd().realpathAlloc(alloc, build_options.sparse_exe_path) catch { + test_result.feature.error_context.?.err = IntegrationTestError.TERM_EXIT_FAILED; + return test_result; + }; + defer alloc.free(sparse_exe_abs); + + createCommitOnTarget(alloc, data) catch { + test_result.feature.error_context.?.err = IntegrationTestError.TERM_EXIT_FAILED; + return test_result; + }; + const rr_sparse_feature = system.system(.{ + .allocator = alloc, + .args = &.{ + sparse_exe_abs, + "feature", + data.feature_name.?, + }, + .cwd = data.repo_dir.?, + }) catch |e| { + log.debug("createFeatureStep::: sparse exe path:{s} ", .{sparse_exe_abs}); + log.debug("createFeatureStep::: error:{any} ", .{e}); + test_result.feature.error_context.?.err = IntegrationTestError.TERM_EXIT_FAILED; + return test_result; + }; + defer alloc.free(rr_sparse_feature.stdout); + defer alloc.free(rr_sparse_feature.stderr); + const rr_git_show_ref = system.git( + .{ + .allocator = alloc, + .args = &.{"show-ref"}, + .cwd = data.repo_dir.?, + }, + ) catch { + test_result.feature.error_context.?.err = IntegrationTestError.TERM_EXIT_FAILED; + test_result.feature.error_context.?.err_msg = "git show-ref command failed"; + return test_result; + }; + log.debug( + "sparse::feature::test:: git show ref stdout:{s}, stderr: {s}\n", + .{ rr_git_show_ref.stdout, rr_git_show_ref.stderr }, + ); + defer alloc.free(rr_git_show_ref.stdout); + defer alloc.free(rr_git_show_ref.stderr); + + //Parsing git-show-ref + _ = parseGitShowRefResult( + alloc, + rr_git_show_ref.stdout, + data.feature_name.?, + TEST_SPARSE_USER_ID, + ) catch |res| { + switch (res) { + IntegrationTestError.SPARSE_FEATURE_NOT_FOUND => test_result.feature.error_context.?.err = IntegrationTestError.SPARSE_FEATURE_NOT_FOUND, + IntegrationTestError.SPARSE_FEATURE_EMPTY_REF => test_result.feature.error_context.?.err = IntegrationTestError.SPARSE_FEATURE_EMPTY_REF, + else => test_result.feature.error_context.?.err = IntegrationTestError.UNEXPECTED_ERROR, + } + + return test_result; + }; + + if (test_result.feature.error_context.?.err == null) { + test_result.feature.exit_code = 0; + } + return test_result; +} + +//test facility functions (TODO: move another module later maybe?) +fn createCommitOnTarget(alloc: Allocator, data: TestData) !void { + std.testing.log_level = .debug; + const rr_new_file = try system.system(.{ + .allocator = alloc, + .args = &.{ + "touch", + "test.txt", + }, + .cwd = data.repo_dir.?, + }); + + defer alloc.free(rr_new_file.stdout); + defer alloc.free(rr_new_file.stderr); + + const rr_git_add = try system.git( + .{ + .allocator = alloc, + .args = &.{ "add", "." }, + .cwd = data.repo_dir.?, + }, + ); + defer alloc.free(rr_git_add.stdout); + defer alloc.free(rr_git_add.stderr); + + const rr_git_commit = try system.git(.{ + .allocator = alloc, + .args = &.{ "commit", "-m", "first commit" }, + .cwd = data.repo_dir.?, + }); + defer alloc.free(rr_git_commit.stdout); + defer alloc.free(rr_git_commit.stderr); +} + +/// Attention!: this function will not be responsible to free stdout, +/// it should be done in caller side +fn parseGitShowRefResult( + alloc: Allocator, + stdout: []u8, + feature_name: []const u8, + user_config: []const u8, +) IntegrationTestError![]const u8 { + std.testing.log_level = .debug; + const ref_result = std.mem.trim(u8, stdout, "\n\t \r"); + + const expected_sparse_slice = std.fmt.allocPrint( + alloc, + "refs/heads/sparse/{s}/{s}/slice/", + .{ user_config, feature_name }, + ) catch return IntegrationTestError.SPARSE_FEATURE_NOT_FOUND; + defer alloc.free(expected_sparse_slice); + if (ref_result.len != 0) { + var split_ref_result = std.mem.tokenizeAny( + u8, + stdout, + " \n", + ); + while (split_ref_result.next()) |iter| { + log.debug("ref iter {s}\n ", .{iter}); + if (std.mem.startsWith(u8, iter, expected_sparse_slice)) { + return iter; + } + } + } + return IntegrationTestError.SPARSE_FEATURE_NOT_FOUND; +} + +const sparse = @import("sparse"); +const system = @import("system.zig"); +const integration_test = @import("integration.zig"); +const IntegrationTestResult = integration_test.IntegrationTestResult; +const IntegrationTestError = integration_test.IntegrationTestError; diff --git a/test/system.zig b/test/system.zig index 5b485bb..c2d531c 100644 --- a/test/system.zig +++ b/test/system.zig @@ -18,13 +18,19 @@ pub fn git(o: struct { }; const argv = try combine([]const u8, o.allocator, command, o.args); defer o.allocator.free(argv); - log.debug( - "git:: args:{s} cwd:{s}", - .{ - argv, + for (argv) |arg| { + log.debug("git:: args:{s} cwd:{s}", .{ + arg, if (o.cwd) |c| c else "null", - }, - ); + }); + } + // log.debug( + // "git:: args:{any} cwd:{s}", + // .{ + // argv, + // if (o.cwd) |c| c else "null", + // }, + // ); return try std.process.Child.run(.{ .allocator = o.allocator, @@ -38,13 +44,19 @@ pub fn system(o: struct { args: []const []const u8, cwd: ?[]const u8 = null, }) !RunResult { - log.debug( - "system:: args:{s} cwd:{s}", - .{ - o.args, + for (o.args) |arg| { + log.debug("system:: args:{s} cwd:{s}", .{ + arg, if (o.cwd) |c| c else "null", - }, - ); + }); + } + // log.debug( + // "system:: args:{any} cwd:{s}", + // .{ + // o.args, + // if (o.cwd) |c| c else "null", + // }, + // ); return try std.process.Child.run(.{ .allocator = o.allocator, @@ -59,7 +71,8 @@ pub fn combine( arr1: []const T, arr2: []const T, ) ![]T { - var arr_list: std.ArrayListUnmanaged(T) = try std.ArrayListUnmanaged(T).initCapacity(allocator, arr1.len + arr2.len); + var arr_list: std.ArrayList(T) = try std.ArrayList(T).initCapacity(allocator, arr1.len + arr2.len); + for (arr1) |item| { try arr_list.append(allocator, item); } diff --git a/vendored/apple-sdk/build.zig b/vendored/apple-sdk/build.zig index 18a6c09..96ad8fd 100644 --- a/vendored/apple-sdk/build.zig +++ b/vendored/apple-sdk/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const AllocatingWriter = @import("std").Io.Writer.Allocating; pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); @@ -46,20 +47,22 @@ pub fn addPaths( // find the SDK path. const libc = try std.zig.LibCInstallation.findNative(.{ .allocator = b.allocator, - .target = step.rootModuleTarget(), + .target = &step.rootModuleTarget(), .verbose = false, }); // Render the file compatible with the `--libc` Zig flag. - var list: std.ArrayList(u8) = .init(b.allocator); - defer list.deinit(); - try libc.render(list.writer()); - + var buffer: std.ArrayList(u8) = .empty; + //defer buffer.deinit(b.allocator); + var writer_allocating: AllocatingWriter = AllocatingWriter.fromArrayList(b.allocator, &buffer); + defer writer_allocating.deinit(); + try libc.render(&writer_allocating.writer); // Create a temporary file to store the libc path because // `--libc` expects a file path. const wf = b.addWriteFiles(); - const path = wf.add("libc.txt", list.items); - + const path = wf.add("libc.txt", writer_allocating.written()); + //try writer.flush(); + //list.deinit(); // Determine our framework path. Zig has a bug where it doesn't // parse this from the libc txt file for `-framework` flags: // https://github.com/ziglang/zig/issues/24024 diff --git a/vendored/libgit2/ClarTestStep.zig b/vendored/libgit2/ClarTestStep.zig index 3679cf5..91cc013 100644 --- a/vendored/libgit2/ClarTestStep.zig +++ b/vendored/libgit2/ClarTestStep.zig @@ -41,15 +41,15 @@ fn make(step: *Step, options: Step.MakeOptions) !void { var man = b.graph.cache.obtain(); defer man.deinit(); - var argv_list: std.ArrayList([]const u8) = .init(arena); + var argv_list: std.ArrayList([]const u8) = .empty; { const file_path = clar.runner.installed_path orelse clar.runner.generated_bin.?.path.?; - try argv_list.append(file_path); + try argv_list.append(arena, file_path); _ = try man.addFile(file_path, null); } - try argv_list.append("-t"); // force TAP output + try argv_list.append(arena, "-t"); // force TAP output for (clar.args.items) |arg| { - try argv_list.append(arg); + try argv_list.append(arena, arg); man.hash.addBytes(arg); } @@ -67,33 +67,16 @@ fn make(step: *Step, options: Step.MakeOptions) !void { try child.spawn(); - var poller = std.io.poll( - b.allocator, - enum { stdout }, - .{ .stdout = child.stdout.? }, - ); - defer poller.deinit(); - - const fifo = poller.fifo(.stdout); - const r = fifo.reader(); - - var buf: std.BoundedArray(u8, 1024) = .{}; - const w = buf.writer(); + var reader_buf: [1024]u8 = undefined; + var file_reader = child.stdout.?.readerStreaming(&reader_buf); + const r = &file_reader.interface; var parser: TapParser = .default; var node: ?std.Progress.Node = null; defer if (node) |n| n.end(); - while (true) { - r.streamUntilDelimiter(w, '\n', null) catch |err| switch (err) { - error.EndOfStream => if (try poller.poll()) continue else break, - else => return err, - }; - - const line = buf.constSlice(); - defer buf.resize(0) catch unreachable; - - switch (try parser.parseLine(arena, line)) { + while (r.takeDelimiter('\n')) |line| { + switch (try parser.parseLine(arena, line orelse break)) { .start_suite => |suite| { if (node) |n| n.end(); node = options.progress_node.start(suite, 0); @@ -111,6 +94,9 @@ fn make(step: *Step, options: Step.MakeOptions) !void { }, .feed_line => {}, } + } else |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + error.StreamTooLong => return error.TapLineTooLong, } const term = try child.wait(); @@ -131,8 +117,8 @@ const TapParser = struct { feed_line, const Failure = struct { - description: std.ArrayListUnmanaged(u8), - reasons: std.ArrayListUnmanaged([]const u8), + description: std.ArrayList(u8), + reasons: std.ArrayList([]const u8), }; }; diff --git a/vendored/libgit2/build.zig b/vendored/libgit2/build.zig index 4634252..4c4f3f8 100644 --- a/vendored/libgit2/build.zig +++ b/vendored/libgit2/build.zig @@ -445,34 +445,19 @@ pub fn build(b: *std.Build) !void { const test_step = b.step("test", "Run core unit tests (requires python)"); { - if (builtin.os.tag != .windows) { - // Fix the test fixture file permissions. This is necessary because Zig does - // not respect the execute permission on arbitrary files it extracts from dependencies. - // Since we need those files to have the execute permission set for tests to - // run successfully, we need to patch them before we bake them into the - // test executable. While modifying the global cache is hacky, it wont break - // hashes for the same reason above. -blurrycat 3/31/25 - for ([_]std.Build.LazyPath{ - libgit_root.path(b, "tests/resources/filemodes/exec_on"), - libgit_root.path(b, "tests/resources/filemodes/exec_off2on_staged"), - libgit_root.path(b, "tests/resources/filemodes/exec_off2on_workdir"), - libgit_root.path(b, "tests/resources/filemodes/exec_on_untracked"), - }) |lazy| { - const path = lazy.getPath2(b, null); - const file = try std.fs.cwd().openFile(path, .{ - .mode = .read_write, - }); - defer file.close(); - try file.setPermissions(.{ .inner = .{ .mode = 0o755 } }); - } - } - const gen_cmd = b.addSystemCommand(&.{"python3"}); gen_cmd.addFileArg(libgit_src.path("tests/clar/generate.py")); const clar_suite = gen_cmd.addPrefixedOutputDirectoryArg("-o", "clar_suite"); gen_cmd.addArgs(&.{ "-f", "-xonline", "-xstress", "-xperf" }); gen_cmd.addDirectoryArg(libgit_src.path("tests/libgit2")); + // Copy the clar source so it can be modified below. + const clar_src = b.addWriteFiles().addCopyDirectory( + libgit_src.path("tests/clar"), + "clar_src", + .{}, + ); + const runner = b.addExecutable(.{ .name = "libgit2_tests", .root_module = b.createModule(.{ @@ -482,7 +467,7 @@ pub fn build(b: *std.Build) !void { }), }); runner.addIncludePath(clar_suite); - runner.addIncludePath(libgit_src.path("tests/clar")); + runner.addIncludePath(clar_src); runner.addIncludePath(libgit_src.path("tests/libgit2")); runner.addConfigHeader(features); @@ -496,22 +481,73 @@ pub fn build(b: *std.Build) !void { runner.linkLibrary(lib); + const runner_flags = &.{ + "-DCLAR_FIXTURE_PATH", // See clar_fix step below + "-DCLAR_TMPDIR=\"libgit2_tests\"", + "-DCLAR_WIN32_LONGPATHS", + "-DGIT_DEPRECATE_HARD", + }; runner.addCSourceFiles(.{ - .root = libgit_src.path("tests/"), - .files = &(clar_sources ++ libgit2_test_sources), - .flags = &.{ - b.fmt( - "-DCLAR_FIXTURE_PATH=\"{s}\"", - // clar expects the fixture path to only have posix seperators or else some tests will break on windows - .{try getNormalizedPath(libgit_src.path("tests/resources"), b, &runner.step)}, - ), - "-DCLAR_TMPDIR=\"libgit2_tests\"", - "-DCLAR_WIN32_LONGPATHS", - "-D_FILE_OFFSET_BITS=64", - "-DGIT_DEPRECATE_HARD", - }, + .root = libgit_src.path("tests/libgit2/"), + .files = &libgit2_test_sources, + .flags = runner_flags, + }); + runner.addCSourceFiles(.{ + .root = clar_src, + .files = &clar_sources, + .flags = runner_flags, }); + const resources_dir = switch (@import("builtin").os.tag) { + .windows => libgit_src.path("tests/resources/"), + else => dir: { + // Fix the test fixture file permissions. This is necessary because Zig does + // not respect the execute permission on arbitrary files it extracts from dependencies. + // Since we need those files to have the execute permission set for tests to + // run successfully, we need to patch them before we bake them into the + // test executable. + const resources_dir = b.addWriteFiles().addCopyDirectory( + libgit_root.path(b, "tests/resources/"), + "test_resources", + .{}, + ); + const chmod = b.addExecutable(.{ + .name = "chmod", + .root_module = b.createModule(.{ + .root_source_file = b.path("chmod.zig"), + .target = b.graph.host, + }), + }); + const run_chmod = b.addRunArtifact(chmod); + run_chmod.addFileArg(resources_dir.path(b, "filemodes/exec_on")); + run_chmod.addFileArg(resources_dir.path(b, "filemodes/exec_off2on_staged")); + run_chmod.addFileArg(resources_dir.path(b, "filemodes/exec_off2on_workdir")); + run_chmod.addFileArg(resources_dir.path(b, "filemodes/exec_on_untracked")); + runner.step.dependOn(&run_chmod.step); + + break :dir resources_dir; + }, + }; + { + // Clar hardcodes the path to resources_dir via the `-DCLAR_FIXTURE_PATH="..."` flag. + // This path isn't known at configure-time, so we have to create a dedicated build step. + // This step replaces *reads* of the `CLAR_FIXTURE_PATH` macro in a local-cache copy of the source code + // (see clar_src). Thankfully the macro is only read by `tests/clar/clar/fixture.h` once. + const clar_fix = b.addExecutable(.{ + .name = "clar_fix", + .root_module = b.createModule(.{ + .root_source_file = b.path("clar_fix.zig"), + .target = b.graph.host, + }), + }); + + const run_fix = b.addRunArtifact(clar_fix); + // run_fix.has_side_effects = true; // @Todo is this necessary? What are the rules for cache invalidation with Run steps? + run_fix.addFileArg(clar_src.path(b, "clar/fixtures.h")); + run_fix.addDirectoryArg(resources_dir); + runner.step.dependOn(&run_fix.step); + } + const TestHelper = struct { b: *std.Build, top_level_step: *std.Build.Step, diff --git a/vendored/libgit2/build.zig.zon b/vendored/libgit2/build.zig.zon index 4821e82..9090aa6 100644 --- a/vendored/libgit2/build.zig.zon +++ b/vendored/libgit2/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .libgit2, .version = "1.9.0", - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.15.2", .fingerprint = 0x7f0051374dea2cba, .dependencies = .{ .libgit2 = .{ diff --git a/vendored/libgit2/chmod.zig b/vendored/libgit2/chmod.zig new file mode 100644 index 0000000..7a07480 --- /dev/null +++ b/vendored/libgit2/chmod.zig @@ -0,0 +1,22 @@ +//! Usage: chmod [file-path...] +//! Accepts a list of file paths as input and changes their permission bits to `0o755`. +//! POSIX only. + +pub fn main() !void { + var args = std.process.args(); + _ = args.skip(); + while (args.next()) |path| { + const file = std.fs.cwd().openFile(path, .{ .mode = .read_write }) catch |err| + fatal("unable to open file '{s}': {t}", .{ path, err }); + defer file.close(); + file.setPermissions(.{ .inner = .{ .mode = 0o755 } }) catch |err| + fatal("unable to set permissions on file '{s}': {t}", .{ path, err }); + } +} + +fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + std.log.err(fmt, args); + std.process.exit(1); +} + +const std = @import("std"); diff --git a/vendored/libgit2/clar_fix.zig b/vendored/libgit2/clar_fix.zig new file mode 100644 index 0000000..8126e67 --- /dev/null +++ b/vendored/libgit2/clar_fix.zig @@ -0,0 +1,67 @@ +//! Usage: clar_fix +//! Replaces *reads* of the CLAR_FIXTURE_PATH macro definition in `src-file` with +//! the absolute path of `fixtures-dir`. `#ifdef`s are not affected. + +const std = @import("std"); + +const fixture_var_name = "CLAR_FIXTURE_PATH"; + +pub fn main() !void { + var arena_inst: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + defer arena_inst.deinit(); + const arena = arena_inst.allocator(); + + var args = try std.process.argsWithAllocator(arena); + _ = args.skip(); + + const clar_fixture_h = args.next() orelse fatal("expected path to 'clar/fixtures.h' file", .{}); + const fixture_path: []const u8 = blk: { + const path_arg = args.next() orelse fatal("expected path to test resources directory", .{}); + + var cleaned_path: std.ArrayList(u8) = try .initCapacity(arena, std.fs.max_path_bytes + 2); + cleaned_path.appendAssumeCapacity('"'); // add string quotes + const abs_path = try std.fs.cwd().realpath(path_arg, cleaned_path.unusedCapacitySlice()); + cleaned_path.items.len += abs_path.len; + cleaned_path.appendAssumeCapacity('"'); + + // clar expects the fixture path to only have posix seperators or else some tests will break + for (cleaned_path.items) |*c| { + if (c.* == '\\') c.* = '/'; + } + break :blk cleaned_path.items; + }; + + const file = try std.fs.cwd().openFile(clar_fixture_h, .{ .mode = .read_write }); + defer file.close(); + + var buf: [1024]u8 = undefined; + var src: std.ArrayList(u8) = src: { + var file_reader = file.reader(&buf); + + const file_size = try file_reader.getSize(); + const to_add = fixture_path.len -| fixture_var_name.len; + var src: std.Io.Writer.Allocating = try .initCapacity(arena, file_size + to_add); + + _ = try file_reader.interface.streamRemaining(&src.writer); + break :src src.toArrayList(); + }; + + const i = std.mem.indexOf( + u8, + src.items, + "return fixture_path(CLAR_FIXTURE_PATH, fixture_name);", + ) orelse return; + + const start = i + "return fixture_path(".len; + src.replaceRangeAssumeCapacity(start, fixture_var_name.len, fixture_path); + + try file.seekTo(0); + var writer = file.writer(&buf); // buf is safe to reuse since file_reader is out of scope + try writer.interface.writeAll(src.items); + try writer.interface.flush(); +} + +fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + std.log.err(fmt, args); + std.process.exit(1); +}