From 8a3fcb441fa06d351daa97c8252c583bd95493f6 Mon Sep 17 00:00:00 2001 From: Laurynas Keturakis Date: Fri, 17 Apr 2026 11:07:04 +0200 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20bump=20to=20zig=200.16=20=E2=80=94?= =?UTF-8?q?=20mise=20pin,=20clap=20master,=20min=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mise.toml | 2 ++ build.zig.zon | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .mise.toml diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..ef36926 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +zig = "0.16.0" diff --git a/build.zig.zon b/build.zig.zon index cf78a16..37ac9c3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .drift, .version = "0.1.0", .fingerprint = 0x486fc72c7a030a48, - .minimum_zig_version = "0.15.0", + .minimum_zig_version = "0.16.0", .paths = .{ "build.zig", "build.zig.zon", @@ -13,10 +13,10 @@ "vendor", }, .dependencies = .{ - // CLI argument parsing (0.11.0, compatible with zig 0.15+) + // CLI argument parsing (master @ fc1e5cc, first commit with zig 0.16 support). .clap = .{ - .url = "git+https://github.com/Hejsil/zig-clap#5289e0753cd274d65344bef1c114284c633536ea", - .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", + .url = "git+https://github.com/Hejsil/zig-clap#fc1e5cc3f6d9d3001112385ee6256d694e959d2f", + .hash = "clap-0.11.0-oBajB7foAQC3Iyn4IVCkUdYaOVVng5IZkSncySTjNig1", }, // Grammars — lazy loaded, compiled only when needed From 874ce662149bfa3557023e2b1ec77da1e5e07ceb Mon Sep 17 00:00:00 2001 From: Laurynas Keturakis Date: Fri, 17 Apr 2026 11:08:21 +0200 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20port=20drift=20to=20zig=200.16=20?= =?UTF-8?q?=E2=80=94=20plumbing=20(juicy=20main,=20io=20threading,=20write?= =?UTF-8?q?r/path=20renames)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/link.zig | 80 ++++++------- src/commands/lint.zig | 77 +++++++------ src/commands/refs.zig | 14 +-- src/commands/status.zig | 10 +- src/commands/unlink.zig | 18 +-- src/context.zig | 1 + src/lockfile.zig | 112 +++++++++--------- src/main.zig | 73 ++++++------ src/markdown.zig | 6 +- src/payload/drift_check_v1.zig | 2 +- src/vcs.zig | 204 +++++++++++++++++++-------------- test/helpers.zig | 53 +++++---- 12 files changed, 341 insertions(+), 309 deletions(-) diff --git a/src/commands/link.zig b/src/commands/link.zig index e9ceb7e..e9f4a7a 100644 --- a/src/commands/link.zig +++ b/src/commands/link.zig @@ -11,20 +11,20 @@ pub const RunError = error{ DocReadFailed, NoBindingsForDoc, CannotComputeFinger pub fn run( ctx: CommandContext, - stdout_w: *std.io.Writer, - stderr_w: *std.io.Writer, + stdout_w: *std.Io.Writer, + stderr_w: *std.Io.Writer, doc_path: []const u8, optional_anchor: ?[]const u8, doc_is_still_accurate: bool, ) !void { - const cwd_path = try std.fs.cwd().realpathAlloc(ctx.run_arena, "."); + const cwd_path = try std.Io.Dir.cwd().realPathFileAlloc(ctx.io, ".", ctx.run_arena); const abs_doc_path = try std.fs.path.resolve(ctx.run_arena, &.{ cwd_path, doc_path }); const doc_dir = std.fs.path.dirname(abs_doc_path) orelse cwd_path; - var lf = try lockfile.discover(ctx.run_arena, ctx.scratch(), doc_dir); + var lf = try lockfile.discover(ctx.io, ctx.run_arena, ctx.scratch(), doc_dir); ctx.resetScratch(); - const doc_content = std.fs.cwd().readFileAlloc(ctx.run_arena, doc_path, 1024 * 1024) catch |err| { + const doc_content = std.Io.Dir.cwd().readFileAlloc(ctx.io, doc_path, ctx.run_arena, .limited(1024 * 1024)) catch |err| { stderr_w.print("error: cannot read '{s}': {s}\n", .{ doc_path, @errorName(err) }) catch {}; return error.DocReadFailed; }; @@ -54,11 +54,11 @@ pub fn run( const binding = findBinding(lf.bindings.items, normalized_doc_path, normalized_target).?; if (isDocGateBlocked(binding, old_sig, doc_is_still_accurate)) { printStaleContext(ctx, stderr_w, lf.root_path, cwd_path, doc_path, doc_content, binding.target); - if (!promptDocAccurate(stderr_w)) return error.DocUnchanged; + if (!promptDocAccurate(ctx.io, stderr_w)) return error.DocUnchanged; } binding.removeField("doc"); - try lockfile.writeFile(&lf, ctx.scratch()); + try lockfile.writeFile(ctx.io, &lf, ctx.scratch()); stdout_w.print("added {s} -> {s}", .{ normalized_doc_path, binding.target }) catch {}; if (binding.fieldValue("sig")) |sig| { @@ -90,27 +90,28 @@ pub fn run( if (refused_count > 0) { printBlanketRefusal(ctx, stderr_w, lf.root_path, cwd_path, doc_path, doc_content, lf.bindings.items, normalized_doc_path, refused_count); - if (!promptDocAccurate(stderr_w)) return error.DocUnchanged; + if (!promptDocAccurate(ctx.io, stderr_w)) return error.DocUnchanged; } - try lockfile.writeFile(&lf, ctx.scratch()); + try lockfile.writeFile(ctx.io, &lf, ctx.scratch()); stdout_w.print("relinked all anchors in {s}\n", .{normalized_doc_path}) catch {}; } /// In TTY mode, prompt the user to confirm the doc is still accurate. /// In non-TTY mode, print the refusal message and return false. -fn promptDocAccurate(stderr_w: *std.io.Writer) bool { - const stdin = std.fs.File.stdin(); - if (!stdin.isTty()) { +fn promptDocAccurate(io: std.Io, stderr_w: *std.Io.Writer) bool { + const stdin = std.Io.File.stdin(); + const is_tty = stdin.isTty(io) catch false; + if (!is_tty) { stderr_w.print("refused: target changed since last link.\nReview the doc, then relink with --doc-is-still-accurate.\n", .{}) catch {}; return false; } stderr_w.print("Doc is still accurate? [y/N] ", .{}) catch {}; stderr_w.flush() catch {}; var buf: [16]u8 = undefined; - const n = stdin.read(&buf) catch return false; - if (n == 0) return false; - const answer = std.mem.trimRight(u8, buf[0..n], "\r\n \t"); + var stdin_reader = stdin.readerStreaming(io, &buf); + const slice = stdin_reader.interface.takeDelimiterExclusive('\n') catch return false; + const answer = std.mem.trimEnd(u8, slice, "\r\n \t"); return answer.len > 0 and (answer[0] == 'y' or answer[0] == 'Y'); } @@ -130,7 +131,7 @@ fn isDocGateBlocked( /// each refused binding with its target context. fn printBlanketRefusal( ctx: CommandContext, - stderr_w: *std.io.Writer, + stderr_w: *std.Io.Writer, root_path: []const u8, cwd_path: []const u8, doc_path: []const u8, @@ -184,7 +185,7 @@ fn upsertBinding( var binding = lockfile.Binding{ .doc_path = try ctx.run_arena.dupe(u8, normalized_doc_path), .target = try ctx.run_arena.dupe(u8, normalized_target), - .metadata = .{}, + .metadata = .empty, }; try refreshBindingSig(ctx, cwd_path, lf.root_path, &binding); try lf.bindings.append(ctx.run_arena, binding); @@ -225,7 +226,7 @@ fn normalizeDocPath( doc_path: []const u8, ) ![]const u8 { const absolute = try resolveInputPath(ctx, root_path, cwd_path, doc_path); - const relative = try std.fs.path.relative(ctx.run_arena, root_path, absolute); + const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); return relative; } @@ -238,12 +239,12 @@ fn normalizeTargetPath( ) ![]const u8 { const parsed = target.parse(raw_target); const absolute = try resolveInputPath(ctx, root_path, cwd_path, parsed.file_path); - if (!pathExists(absolute)) { + if (!pathExists(ctx.io, absolute)) { ctx.resetScratch(); return error.TargetNotFound; } - const relative = try std.fs.path.relative(ctx.run_arena, root_path, absolute); + const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); if (parsed.symbol_name) |symbol| { if (parsed.isHeading()) { @@ -278,23 +279,24 @@ fn resolveInputPath( } const cwd_candidate = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, path }); - if (pathExists(cwd_candidate)) return cwd_candidate; + if (pathExists(ctx.io, cwd_candidate)) return cwd_candidate; return try std.fs.path.resolve(ctx.scratch(), &.{ root_path, path }); } -fn pathExists(path: []const u8) bool { - std.fs.accessAbsolute(path, .{}) catch return false; +fn pathExists(io: std.Io, path: []const u8) bool { + std.Io.Dir.accessAbsolute(io, path, .{}) catch return false; return true; } fn readResolvedFile(ctx: CommandContext, path: []const u8) ![]const u8 { - if (std.fs.path.isAbsolute(path)) { - const file = try std.fs.openFileAbsolute(path, .{}); - defer file.close(); - return try file.readToEndAlloc(ctx.scratch(), 1024 * 1024); - } - return try std.fs.cwd().readFileAlloc(ctx.scratch(), path, 1024 * 1024); + const file = if (std.fs.path.isAbsolute(path)) + try std.Io.Dir.openFileAbsolute(ctx.io, path, .{}) + else + try std.Io.Dir.cwd().openFile(ctx.io, path, .{}); + defer file.close(ctx.io); + var file_reader = file.reader(ctx.io, &.{}); + return try file_reader.interface.allocRemaining(ctx.scratch(), .limited(1024 * 1024)); } fn findBinding(bindings: []lockfile.Binding, doc_path: []const u8, normalized_target: []const u8) ?*lockfile.Binding { @@ -311,7 +313,7 @@ fn findBinding(bindings: []lockfile.Binding, doc_path: []const u8, normalized_ta /// silently ignored so the refusal message always follows. fn printStaleContext( ctx: CommandContext, - stderr_w: *std.io.Writer, + stderr_w: *std.Io.Writer, root_path: []const u8, cwd_path: []const u8, doc_path: []const u8, @@ -337,7 +339,7 @@ fn printStaleContext( } fn printDocSection( - stderr_w: *std.io.Writer, + stderr_w: *std.Io.Writer, doc_path: []const u8, doc_content: []const u8, binding_target: []const u8, @@ -402,7 +404,7 @@ fn extractSectionAroundOffset(content: []const u8, offset: usize) []const u8 { var line_iter = std.mem.splitScalar(u8, content, '\n'); while (line_iter.next()) |line| { - const trimmed = std.mem.trimLeft(u8, line, " \t"); + const trimmed = std.mem.trimStart(u8, line, " \t"); if (trimmed.len > 0 and trimmed[0] == '#') { const level = countLeadingChar(trimmed, '#'); if (level > 0 and level <= 6 and pos <= offset) { @@ -429,7 +431,7 @@ fn extractSectionAroundOffset(content: []const u8, offset: usize) []const u8 { while (iter2.next()) |line| { if (past_heading) { - const trimmed = std.mem.trimLeft(u8, line, " \t"); + const trimmed = std.mem.trimStart(u8, line, " \t"); if (trimmed.len > 0 and trimmed[0] == '#') { const level = countLeadingChar(trimmed, '#'); if (level > 0 and level <= heading_level) { @@ -444,7 +446,7 @@ fn extractSectionAroundOffset(content: []const u8, offset: usize) []const u8 { } const section = content[heading_start..@min(section_end, content.len)]; - return std.mem.trimRight(u8, section, "\n\r "); + return std.mem.trimEnd(u8, section, "\n\r "); } fn countLeadingChar(s: []const u8, c: u8) usize { @@ -467,7 +469,7 @@ fn findNearestHeadingAbove(doc_content: []const u8, section_text: []const u8) ?[ var last_heading: ?[]const u8 = null; var lines = std.mem.splitScalar(u8, prefix, '\n'); while (lines.next()) |line| { - const trimmed = std.mem.trimLeft(u8, line, " \t"); + const trimmed = std.mem.trimStart(u8, line, " \t"); if (trimmed.len > 0 and trimmed[0] == '#') { const hashes = countLeadingChar(trimmed, '#'); if (hashes > 0 and hashes <= 6 and trimmed.len > hashes) { @@ -478,7 +480,7 @@ fn findNearestHeadingAbove(doc_content: []const u8, section_text: []const u8) ?[ // Also check if section_text itself starts with a heading const first_line_end = std.mem.indexOfScalar(u8, section_text, '\n') orelse section_text.len; - const first_line = std.mem.trimLeft(u8, section_text[0..first_line_end], " \t"); + const first_line = std.mem.trimStart(u8, section_text[0..first_line_end], " \t"); if (first_line.len > 0 and first_line[0] == '#') { const hashes = countLeadingChar(first_line, '#'); if (hashes > 0 and hashes <= 6 and first_line.len > hashes) { @@ -491,7 +493,7 @@ fn findNearestHeadingAbove(doc_content: []const u8, section_text: []const u8) ?[ fn printHeadingTarget( ctx: CommandContext, - stderr_w: *std.io.Writer, + stderr_w: *std.Io.Writer, root_path: []const u8, cwd_path: []const u8, parsed: target.ParsedTarget, @@ -511,7 +513,7 @@ fn printHeadingTarget( fn printSymbolTarget( ctx: CommandContext, - stderr_w: *std.io.Writer, + stderr_w: *std.Io.Writer, root_path: []const u8, cwd_path: []const u8, parsed: target.ParsedTarget, @@ -531,7 +533,7 @@ fn printSymbolTarget( printCappedLines(stderr_w, source); } -fn printCappedLines(stderr_w: *std.io.Writer, text: []const u8) void { +fn printCappedLines(stderr_w: *std.Io.Writer, text: []const u8) void { var lines = std.mem.splitScalar(u8, text, '\n'); var printed: usize = 0; diff --git a/src/commands/lint.zig b/src/commands/lint.zig index 6b7ec87..57a368a 100644 --- a/src/commands/lint.zig +++ b/src/commands/lint.zig @@ -13,10 +13,12 @@ pub const RunStatus = enum { pass, fail }; pub const RunError = error{LintCheckFailed}; const FileCache = struct { + io: std.Io, arena: std.heap.ArenaAllocator, current: std.StringHashMap([]const u8), - fn init(self: *FileCache, parent: std.mem.Allocator) void { + fn init(self: *FileCache, io: std.Io, parent: std.mem.Allocator) void { + self.io = io; self.arena = std.heap.ArenaAllocator.init(parent); self.current = std.StringHashMap([]const u8).init(self.arena.allocator()); } @@ -29,14 +31,15 @@ const FileCache = struct { fn getCurrent(self: *FileCache, absolute_path: []const u8) !?[]const u8 { if (self.current.get(absolute_path)) |content| return content; - const file = std.fs.openFileAbsolute(absolute_path, .{}) catch |err| switch (err) { + const file = std.Io.Dir.openFileAbsolute(self.io, absolute_path, .{}) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; - defer file.close(); + defer file.close(self.io); const a = self.arena.allocator(); - const content = try file.readToEndAlloc(a, 1024 * 1024); + var file_reader = file.reader(self.io, &.{}); + const content = try file_reader.interface.allocRemaining(a, .limited(1024 * 1024)); const key = try a.dupe(u8, absolute_path); try self.current.put(key, content); return content; @@ -156,27 +159,27 @@ const DocGroup = struct { pub fn run( ctx: CommandContext, - stdout_w: *std.io.Writer, - stderr_w: *std.io.Writer, + stdout_w: *std.Io.Writer, + stderr_w: *std.Io.Writer, format: Format, changed_path: ?[]const u8, ) !RunStatus { - const cwd_path = try std.fs.cwd().realpathAlloc(ctx.run_arena, "."); + const cwd_path = try std.Io.Dir.cwd().realPathFileAlloc(ctx.io, ".", ctx.run_arena); - const lf = try lockfile.discover(ctx.run_arena, ctx.scratch(), cwd_path); + const lf = try lockfile.discover(ctx.io, ctx.run_arena, ctx.scratch(), cwd_path); ctx.resetScratch(); - var doc_groups = try discoverDocGroups(ctx.run_arena, lf.root_path, lf.bindings.items); + var doc_groups = try discoverDocGroups(ctx.io, ctx.run_arena, lf.root_path, lf.bindings.items); defer { for (doc_groups.items) |*doc| doc.bindings.deinit(ctx.run_arena); doc_groups.deinit(ctx.run_arena); } const detected_vcs = vcs.detectVcs(); - const repo_identity = vcs.getRepoIdentity(ctx.run_arena, ctx.scratch(), cwd_path); + const repo_identity = vcs.getRepoIdentity(ctx.io, ctx.run_arena, ctx.scratch(), cwd_path); var file_cache: FileCache = undefined; - file_cache.init(ctx.run_arena); + file_cache.init(ctx.io, ctx.run_arena); defer file_cache.deinit(); const normalized_changed = if (changed_path) |raw| @@ -186,8 +189,8 @@ pub fn run( var result: CheckResult = .{ .repo = repo_identity, - .checked_at_ms = std.time.milliTimestamp(), - .docs = .{}, + .checked_at_ms = std.Io.Timestamp.now(ctx.io, .real).toMilliseconds(), + .docs = .empty, .failed = false, .docs_total = 0, .docs_fresh = 0, @@ -212,8 +215,8 @@ pub fn run( .path = doc.path, .origin = commonOrigin(doc.bindings.items), .result = .fresh, - .anchors = .{}, - .links = .{}, + .anchors = .empty, + .links = .empty, }; var fresh_count: usize = 0; @@ -290,21 +293,21 @@ pub fn run( } fn discoverDocGroups( + io: std.Io, allocator: std.mem.Allocator, root_path: []const u8, bindings: []lockfile.Binding, ) !std.ArrayList(DocGroup) { - var docs: std.ArrayList(DocGroup) = .{}; + var docs: std.ArrayList(DocGroup) = .empty; errdefer { for (docs.items) |*doc| doc.bindings.deinit(allocator); docs.deinit(allocator); } - const result = try std.process.Child.run(.{ - .allocator = allocator, + const result = try std.process.run(allocator, io, .{ .argv = &.{ "git", "ls-files", "-z", "--cached", "--others", "--exclude-standard" }, - .cwd = root_path, - .max_output_bytes = 10 * 1024 * 1024, + .cwd = .{ .path = root_path }, + .stdout_limit = .limited(10 * 1024 * 1024), }); defer allocator.free(result.stdout); defer allocator.free(result.stderr); @@ -317,7 +320,7 @@ fn discoverDocGroups( offset += rel_end + 1; if (!std.mem.endsWith(u8, line, ".md")) continue; - if (hasNestedLockfile(root_path, line, allocator)) continue; + if (hasNestedLockfile(io, root_path, line, allocator)) continue; _ = try ensureDocGroup(allocator, &docs, line); } @@ -345,12 +348,12 @@ fn discoverDocGroups( /// Check if a relative path has a closer drift.lock than root_path. /// Returns true if there's an intermediate drift.lock (the file belongs to a nested scope). -fn hasNestedLockfile(root_path: []const u8, rel_path: []const u8, allocator: std.mem.Allocator) bool { +fn hasNestedLockfile(io: std.Io, root_path: []const u8, rel_path: []const u8, allocator: std.mem.Allocator) bool { var dir: []const u8 = std.fs.path.dirname(rel_path) orelse return false; while (dir.len > 0) { const candidate = std.fs.path.join(allocator, &.{ root_path, dir, "drift.lock" }) catch return false; - if (pathExists(candidate)) return true; + if (pathExists(io, candidate)) return true; dir = std.fs.path.dirname(dir) orelse break; } return false; @@ -367,7 +370,7 @@ fn ensureDocGroup( try docs.append(allocator, .{ .path = try allocator.dupe(u8, path), - .bindings = .{}, + .bindings = .empty, }); return &docs.items[docs.items.len - 1]; } @@ -431,11 +434,11 @@ fn classifyRelativeLink( // Resolve symlinks on the doc path so relative links are computed from the real location. const raw_absolute_doc = try std.fs.path.resolve(ctx.scratch(), &.{ root_path, doc_path }); - const real_doc_path = std.fs.cwd().realpathAlloc(ctx.scratch(), raw_absolute_doc) catch raw_absolute_doc; + const real_doc_path = std.Io.Dir.cwd().realPathFileAlloc(ctx.io, raw_absolute_doc, ctx.scratch()) catch raw_absolute_doc; const doc_dir = std.fs.path.dirname(real_doc_path) orelse root_path; const absolute = try std.fs.path.resolve(ctx.scratch(), &.{ doc_dir, path_part }); - const relative = try std.fs.path.relative(ctx.run_arena, root_path, absolute); - const exists = pathExists(absolute); + const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const exists = pathExists(ctx.io, absolute); ctx.resetScratch(); return .{ .display_target = relative, .exists = exists }; @@ -474,11 +477,11 @@ fn normalizeChangedPrefix( raw_path: []const u8, ) ![]const u8 { if (std.fs.path.isAbsolute(raw_path)) { - return try std.fs.path.relative(ctx.run_arena, root_path, raw_path); + return try std.fs.path.relative(ctx.run_arena, "", null, root_path, raw_path); } const absolute = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, raw_path }); - const relative = try std.fs.path.relative(ctx.run_arena, root_path, absolute); + const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); return relative; } @@ -525,7 +528,7 @@ fn checkBinding( return .{ .result = .fresh, .reason_code = .none }; } - const blame = try vcs.getLatestBlameInfo(ctx.run_arena, ctx.scratch(), root_path, parsed.file_path, detected_vcs); + const blame = try vcs.getLatestBlameInfo(ctx.io, ctx.run_arena, ctx.scratch(), root_path, parsed.file_path, detected_vcs); return .{ .result = .stale, .reason_code = .changed_after_baseline, .blame = blame }; } @@ -564,7 +567,7 @@ fn jsonAnchorFromOutcome(raw_target: []const u8, sig: ?[]const u8, parsed: targe }; } -fn writeResultsText(w: *std.io.Writer, result: *const CheckResult, checked_any: bool) !void { +fn writeResultsText(w: *std.Io.Writer, result: *const CheckResult, checked_any: bool) !void { if (!checked_any) { try w.writeAll("ok\n"); return; @@ -596,7 +599,7 @@ fn writeResultsText(w: *std.io.Writer, result: *const CheckResult, checked_any: } } -fn textEmitAnchor(w: *std.io.Writer, origin: ?[]const u8, row: drift_check_v1.Anchor, blame_storage: ?vcs.BlameInfo) void { +fn textEmitAnchor(w: *std.Io.Writer, origin: ?[]const u8, row: drift_check_v1.Anchor, blame_storage: ?vcs.BlameInfo) void { if (std.mem.eql(u8, row.result, "stale")) { const msg = row.reason.?.message; if (msg.len > 0) { @@ -620,12 +623,12 @@ fn textEmitAnchor(w: *std.io.Writer, origin: ?[]const u8, row: drift_check_v1.An } } -fn textEmitLink(w: *std.io.Writer, display_target: []const u8, row: drift_check_v1.Link) void { +fn textEmitLink(w: *std.Io.Writer, display_target: []const u8, row: drift_check_v1.Link) void { if (!std.mem.eql(u8, row.result, "broken")) return; w.print(" BROKEN {s} ({s})\n", .{ display_target, row.reason.?.message }) catch {}; } -fn writeSummaryText(w: *std.io.Writer, result: *const CheckResult) !void { +fn writeSummaryText(w: *std.Io.Writer, result: *const CheckResult) !void { var wrote_any = false; if (result.docs_stale > 0) { @@ -643,7 +646,7 @@ fn writeSummaryText(w: *std.io.Writer, result: *const CheckResult) !void { } } -fn writeResultsJson(run_alloc: std.mem.Allocator, w: *std.io.Writer, result: *const CheckResult) !void { +fn writeResultsJson(run_alloc: std.mem.Allocator, w: *std.Io.Writer, result: *const CheckResult) !void { const doc = try checkResultToDriftCheckV1(run_alloc, result); try drift_check_v1.writeJson(w, doc); } @@ -696,7 +699,7 @@ fn checkResultSummaryWire(result: *const CheckResult) drift_check_v1.Summary { }; } -fn pathExists(path: []const u8) bool { - std.fs.accessAbsolute(path, .{}) catch return false; +fn pathExists(io: std.Io, path: []const u8) bool { + std.Io.Dir.accessAbsolute(io, path, .{}) catch return false; return true; } diff --git a/src/commands/refs.zig b/src/commands/refs.zig index 2943075..1e475dc 100644 --- a/src/commands/refs.zig +++ b/src/commands/refs.zig @@ -3,12 +3,12 @@ const CommandContext = @import("../context.zig").CommandContext; const lockfile = @import("../lockfile.zig"); const target = @import("../target.zig"); -pub fn run(ctx: CommandContext, stdout_w: *std.io.Writer, stderr_w: *std.io.Writer, raw_target: []const u8) !void { +pub fn run(ctx: CommandContext, stdout_w: *std.Io.Writer, stderr_w: *std.Io.Writer, raw_target: []const u8) !void { _ = stderr_w; - const cwd_path = try std.fs.cwd().realpathAlloc(ctx.run_arena, "."); + const cwd_path = try std.Io.Dir.cwd().realPathFileAlloc(ctx.io, ".", ctx.run_arena); - const lf = try lockfile.discover(ctx.run_arena, ctx.scratch(), cwd_path); + const lf = try lockfile.discover(ctx.io, ctx.run_arena, ctx.scratch(), cwd_path); ctx.resetScratch(); if (!lf.exists) return; @@ -38,7 +38,7 @@ fn normalizeTargetPath( const symbol_name = parsed.symbol_name; const absolute = try resolveInputPath(ctx, root_path, cwd_path, file_part); - const relative = try std.fs.path.relative(ctx.run_arena, root_path, absolute); + const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); if (symbol_name) |symbol| { @@ -58,12 +58,12 @@ fn resolveInputPath( } const cwd_candidate = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, path }); - if (pathExists(cwd_candidate)) return cwd_candidate; + if (pathExists(ctx.io, cwd_candidate)) return cwd_candidate; return try std.fs.path.resolve(ctx.scratch(), &.{ root_path, path }); } -fn pathExists(path: []const u8) bool { - std.fs.accessAbsolute(path, .{}) catch return false; +fn pathExists(io: std.Io, path: []const u8) bool { + std.Io.Dir.accessAbsolute(io, path, .{}) catch return false; return true; } diff --git a/src/commands/status.zig b/src/commands/status.zig index e91ac49..4dbad50 100644 --- a/src/commands/status.zig +++ b/src/commands/status.zig @@ -5,12 +5,12 @@ const lockfile = @import("../lockfile.zig"); pub const Format = lint.Format; -pub fn run(ctx: CommandContext, stdout_w: *std.io.Writer, stderr_w: *std.io.Writer, format: Format) !void { +pub fn run(ctx: CommandContext, stdout_w: *std.Io.Writer, stderr_w: *std.Io.Writer, format: Format) !void { _ = stderr_w; - const cwd_path = try std.fs.cwd().realpathAlloc(ctx.run_arena, "."); + const cwd_path = try std.Io.Dir.cwd().realPathFileAlloc(ctx.io, ".", ctx.run_arena); - const lf = try lockfile.discover(ctx.run_arena, ctx.scratch(), cwd_path); + const lf = try lockfile.discover(ctx.io, ctx.run_arena, ctx.scratch(), cwd_path); ctx.resetScratch(); var docs = try lockfile.groupByDoc(ctx.run_arena, lf.bindings.items); @@ -25,7 +25,7 @@ pub fn run(ctx: CommandContext, stdout_w: *std.io.Writer, stderr_w: *std.io.Writ } } -fn writeDocsText(w: *std.io.Writer, docs: []const lockfile.DocBindings) void { +fn writeDocsText(w: *std.Io.Writer, docs: []const lockfile.DocBindings) void { if (docs.len == 0) return; for (docs, 0..) |doc, idx| { @@ -48,7 +48,7 @@ fn writeDocsText(w: *std.io.Writer, docs: []const lockfile.DocBindings) void { } } -fn writeDocsJson(w: *std.io.Writer, docs: []const lockfile.DocBindings) !void { +fn writeDocsJson(w: *std.Io.Writer, docs: []const lockfile.DocBindings) !void { var json_w: std.json.Stringify = .{ .writer = w, .options = .{} }; try json_w.beginArray(); diff --git a/src/commands/unlink.zig b/src/commands/unlink.zig index 6481a61..f1838c6 100644 --- a/src/commands/unlink.zig +++ b/src/commands/unlink.zig @@ -3,14 +3,14 @@ const CommandContext = @import("../context.zig").CommandContext; const lockfile = @import("../lockfile.zig"); const target = @import("../target.zig"); -pub fn run(ctx: CommandContext, stdout_w: *std.io.Writer, stderr_w: *std.io.Writer, doc_path: []const u8, anchor: []const u8) !void { +pub fn run(ctx: CommandContext, stdout_w: *std.Io.Writer, stderr_w: *std.Io.Writer, doc_path: []const u8, anchor: []const u8) !void { _ = stderr_w; - const cwd_path = try std.fs.cwd().realpathAlloc(ctx.run_arena, "."); + const cwd_path = try std.Io.Dir.cwd().realPathFileAlloc(ctx.io, ".", ctx.run_arena); const abs_doc_path = try std.fs.path.resolve(ctx.run_arena, &.{ cwd_path, doc_path }); const doc_dir = std.fs.path.dirname(abs_doc_path) orelse cwd_path; - var lf = try lockfile.discover(ctx.run_arena, ctx.scratch(), doc_dir); + var lf = try lockfile.discover(ctx.io, ctx.run_arena, ctx.scratch(), doc_dir); ctx.resetScratch(); if (!lf.exists) return; @@ -37,7 +37,7 @@ pub fn run(ctx: CommandContext, stdout_w: *std.io.Writer, stderr_w: *std.io.Writ if (!removed) return; - try lockfile.writeFile(&lf, ctx.scratch()); + try lockfile.writeFile(ctx.io, &lf, ctx.scratch()); stdout_w.print("removed {s} -> {s} from drift.lock\n", .{ normalized_doc_path, normalized_target }) catch {}; } @@ -48,7 +48,7 @@ fn normalizeSpecPath( doc_path: []const u8, ) ![]const u8 { const absolute = try resolveInputPath(ctx, root_path, cwd_path, doc_path); - const relative = try std.fs.path.relative(ctx.run_arena, root_path, absolute); + const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); return relative; } @@ -64,7 +64,7 @@ fn normalizeTargetPath( const symbol_name = parsed.symbol_name; const absolute = try resolveInputPath(ctx, root_path, cwd_path, file_part); - const relative = try std.fs.path.relative(ctx.run_arena, root_path, absolute); + const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); if (symbol_name) |symbol| { @@ -84,12 +84,12 @@ fn resolveInputPath( } const cwd_candidate = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, path }); - if (pathExists(cwd_candidate)) return cwd_candidate; + if (pathExists(ctx.io, cwd_candidate)) return cwd_candidate; return try std.fs.path.resolve(ctx.scratch(), &.{ root_path, path }); } -fn pathExists(path: []const u8) bool { - std.fs.accessAbsolute(path, .{}) catch return false; +fn pathExists(io: std.Io, path: []const u8) bool { + std.Io.Dir.accessAbsolute(io, path, .{}) catch return false; return true; } diff --git a/src/context.zig b/src/context.zig index ff5380a..6aeaad6 100644 --- a/src/context.zig +++ b/src/context.zig @@ -3,6 +3,7 @@ const std = @import("std"); /// Per-command memory scope: `run` owns command-lifetime data; `scratch` holds /// loop-local temporaries and is reset between iterations (see docs/DECISIONS.md). pub const CommandContext = struct { + io: std.Io, run_arena: std.mem.Allocator, scratch_arena: *std.heap.ArenaAllocator, diff --git a/src/lockfile.zig b/src/lockfile.zig index 860b4c4..0b111fe 100644 --- a/src/lockfile.zig +++ b/src/lockfile.zig @@ -63,11 +63,11 @@ pub const ParseError = error{ }; /// `run` holds durable lockfile state; `scratch` holds walk temporaries and the lockfile file buffer (reset by caller). -pub fn discover(run: std.mem.Allocator, scratch: std.mem.Allocator, start_path: []const u8) !Lockfile { +pub fn discover(io: std.Io, run: std.mem.Allocator, scratch: std.mem.Allocator, start_path: []const u8) !Lockfile { const resolved_run = if (std.fs.path.isAbsolute(start_path)) try run.dupe(u8, start_path) else - try std.fs.cwd().realpathAlloc(run, start_path); + try std.Io.Dir.cwd().realPathFileAlloc(io, start_path, run); defer run.free(resolved_run); var current = try scratch.dupe(u8, resolved_run); @@ -75,19 +75,19 @@ pub fn discover(run: std.mem.Allocator, scratch: std.mem.Allocator, start_path: while (true) { const candidate = try std.fs.path.join(scratch, &.{ current, "drift.lock" }); - if (fileExists(candidate)) { - return try readAtPath(run, scratch, current, candidate, true); + if (fileExists(io, candidate)) { + return try readAtPath(io, run, scratch, current, candidate, true); } // Stop at VCS root — don't climb out of the current repository. - const has_git = fileExists(try std.fs.path.join(scratch, &.{ current, ".git" })); - const has_jj = fileExists(try std.fs.path.join(scratch, &.{ current, ".jj" })); + const has_git = fileExists(io, try std.fs.path.join(scratch, &.{ current, ".git" })); + const has_jj = fileExists(io, try std.fs.path.join(scratch, &.{ current, ".jj" })); if (has_git or has_jj) { return .{ .root_path = try run.dupe(u8, current), .lockfile_path = try std.fs.path.join(run, &.{ current, "drift.lock" }), .exists = false, - .bindings = .{}, + .bindings = .empty, }; } @@ -96,15 +96,15 @@ pub fn discover(run: std.mem.Allocator, scratch: std.mem.Allocator, start_path: .root_path = try run.dupe(u8, resolved_run), .lockfile_path = try std.fs.path.join(run, &.{ resolved_run, "drift.lock" }), .exists = false, - .bindings = .{}, + .bindings = .empty, }; }; current = try scratch.dupe(u8, parent); } } -pub fn readAtPath(run: std.mem.Allocator, scratch: std.mem.Allocator, root_path: []const u8, lockfile_path: []const u8, exists: bool) !Lockfile { - var bindings: std.ArrayList(Binding) = .{}; +pub fn readAtPath(io: std.Io, run: std.mem.Allocator, scratch: std.mem.Allocator, root_path: []const u8, lockfile_path: []const u8, exists: bool) !Lockfile { + var bindings: std.ArrayList(Binding) = .empty; errdefer { for (bindings.items) |*binding| { run.free(binding.doc_path); @@ -119,9 +119,7 @@ pub fn readAtPath(run: std.mem.Allocator, scratch: std.mem.Allocator, root_path: } if (exists) { - const file = try openPath(lockfile_path); - defer file.close(); - const content = try file.readToEndAlloc(scratch, 1024 * 1024); + const content = try readFileAt(io, scratch, lockfile_path, 1024 * 1024); defer scratch.free(content); try parseInto(run, content, &bindings); } @@ -144,7 +142,7 @@ pub fn parseInto(allocator: std.mem.Allocator, content: []const u8, bindings: *s } pub fn groupByDoc(allocator: std.mem.Allocator, bindings: []Binding) !std.ArrayList(DocBindings) { - var docs: std.ArrayList(DocBindings) = .{}; + var docs: std.ArrayList(DocBindings) = .empty; errdefer { for (docs.items) |*doc| doc.bindings.deinit(allocator); docs.deinit(allocator); @@ -164,7 +162,7 @@ pub fn groupByDoc(allocator: std.mem.Allocator, bindings: []Binding) !std.ArrayL } else { var doc = DocBindings{ .path = binding.doc_path, - .bindings = .{}, + .bindings = .empty, }; errdefer doc.bindings.deinit(allocator); try doc.bindings.append(allocator, binding); @@ -189,7 +187,7 @@ pub fn groupByDoc(allocator: std.mem.Allocator, bindings: []Binding) !std.ArrayL return docs; } -fn renderLineToWriter(writer: anytype, binding: Binding) !void { +fn renderLineToWriter(writer: *std.Io.Writer, binding: Binding) !void { try writer.print("{s} -> {s}", .{ binding.doc_path, binding.target }); for (binding.metadata.items) |field| { try writer.print(" {s}:{s}", .{ field.key, field.value }); @@ -197,18 +195,18 @@ fn renderLineToWriter(writer: anytype, binding: Binding) !void { } /// Writes sorted lockfile lines to `writer`. Uses `scratch` for sort temporaries. -pub fn serializeToWriter(scratch: std.mem.Allocator, writer: anytype, bindings: []const Binding) !void { - var lines: std.ArrayList([]const u8) = .{}; +pub fn serializeToWriter(scratch: std.mem.Allocator, writer: *std.Io.Writer, bindings: []const Binding) !void { + var lines: std.ArrayList([]const u8) = .empty; defer { for (lines.items) |line| scratch.free(line); lines.deinit(scratch); } for (bindings) |binding| { - var row: std.ArrayList(u8) = .{}; - errdefer row.deinit(scratch); - try renderLineToWriter(row.writer(scratch), binding); - try lines.append(scratch, try row.toOwnedSlice(scratch)); + var row: std.Io.Writer.Allocating = .init(scratch); + errdefer row.deinit(); + try renderLineToWriter(&row.writer, binding); + try lines.append(scratch, try row.toOwnedSlice()); } std.mem.sort([]const u8, lines.items, {}, struct { @@ -224,17 +222,17 @@ pub fn serializeToWriter(scratch: std.mem.Allocator, writer: anytype, bindings: } pub fn serialize(allocator: std.mem.Allocator, bindings: []const Binding) ![]u8 { - var output: std.ArrayList(u8) = .{}; - errdefer output.deinit(allocator); - try serializeToWriter(allocator, output.writer(allocator), bindings); - return try output.toOwnedSlice(allocator); + var output: std.Io.Writer.Allocating = .init(allocator); + errdefer output.deinit(); + try serializeToWriter(allocator, &output.writer, bindings); + return try output.toOwnedSlice(); } -pub fn writeFile(lockfile: *const Lockfile, scratch: std.mem.Allocator) !void { - const file = try createPath(lockfile.lockfile_path); - defer file.close(); +pub fn writeFile(io: std.Io, lockfile: *const Lockfile, scratch: std.mem.Allocator) !void { + const file = try std.Io.Dir.createFileAbsolute(io, lockfile.lockfile_path, .{ .truncate = true }); + defer file.close(io); var buf: [4096]u8 = undefined; - var fw = file.writer(&buf); + var fw = file.writer(io, &buf); defer fw.interface.flush() catch {}; try serializeToWriter(scratch, &fw.interface, lockfile.bindings.items); } @@ -248,7 +246,7 @@ fn parseLine(allocator: std.mem.Allocator, line: []const u8) !Binding { var tokens = std.mem.tokenizeScalar(u8, rest, ' '); const target = tokens.next() orelse return error.InvalidBindingLine; - var metadata: std.ArrayList(MetadataField) = .{}; + var metadata: std.ArrayList(MetadataField) = .empty; errdefer metadata.deinit(allocator); while (tokens.next()) |token| { @@ -267,24 +265,24 @@ fn parseLine(allocator: std.mem.Allocator, line: []const u8) !Binding { }; } -fn fileExists(path: []const u8) bool { - const file = openPath(path) catch return false; - file.close(); - return true; -} - -fn openPath(path: []const u8) !std.fs.File { +fn fileExists(io: std.Io, path: []const u8) bool { if (std.fs.path.isAbsolute(path)) { - return std.fs.openFileAbsolute(path, .{}); + std.Io.Dir.accessAbsolute(io, path, .{}) catch return false; + } else { + std.Io.Dir.cwd().access(io, path, .{}) catch return false; } - return std.fs.cwd().openFile(path, .{}); + return true; } -fn createPath(path: []const u8) !std.fs.File { - if (std.fs.path.isAbsolute(path)) { - return std.fs.createFileAbsolute(path, .{ .truncate = true }); - } - return std.fs.cwd().createFile(path, .{ .truncate = true }); +/// Read a file at `path` (absolute or cwd-relative) fully into memory via `allocator`. +fn readFileAt(io: std.Io, allocator: std.mem.Allocator, path: []const u8, max_bytes: usize) ![]u8 { + const file = if (std.fs.path.isAbsolute(path)) + try std.Io.Dir.openFileAbsolute(io, path, .{}) + else + try std.Io.Dir.cwd().openFile(io, path, .{}); + defer file.close(io); + var file_reader = file.reader(io, &.{}); + return try file_reader.interface.allocRemaining(allocator, .limited(max_bytes)); } fn parentPath(path: []const u8) ?[]const u8 { @@ -302,7 +300,7 @@ test "parseInto reads bindings and metadata" { "docs/auth.md -> src/auth/login.ts sig:a1b2c3d4e5f6a7b8\n" ++ "docs/auth.md -> src/auth/provider.ts#AuthConfig sig:1a2b3c4d5e6f7890 origin:github:fiberplane/drift\n"; - var bindings: std.ArrayList(Binding) = .{}; + var bindings: std.ArrayList(Binding) = .empty; defer { for (bindings.items) |*binding| { allocator.free(binding.doc_path); @@ -327,7 +325,7 @@ test "parseInto reads bindings and metadata" { test "serialize sorts lines and appends trailing newline" { const allocator = std.testing.allocator; - var bindings: std.ArrayList(Binding) = .{}; + var bindings: std.ArrayList(Binding) = .empty; defer { for (bindings.items) |*binding| { allocator.free(binding.doc_path); @@ -344,12 +342,12 @@ test "serialize sorts lines and appends trailing newline" { try bindings.append(allocator, .{ .doc_path = try allocator.dupe(u8, "docs/z.md"), .target = try allocator.dupe(u8, "src/z.ts"), - .metadata = .{}, + .metadata = .empty, }); try bindings.append(allocator, .{ .doc_path = try allocator.dupe(u8, "docs/a.md"), .target = try allocator.dupe(u8, "src/a.ts"), - .metadata = .{}, + .metadata = .empty, }); const content = try serialize(allocator, bindings.items); @@ -363,22 +361,23 @@ test "serialize sorts lines and appends trailing newline" { test "discover walks up to find drift.lock" { const allocator = std.testing.allocator; + const io = std.testing.io; var scratch_arena = std.heap.ArenaAllocator.init(allocator); defer scratch_arena.deinit(); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.makePath("repo/nested/work"); - try tmp.dir.writeFile(.{ + try tmp.dir.createDirPath(io, "repo/nested/work"); + try tmp.dir.writeFile(io, .{ .sub_path = "repo/drift.lock", .data = "docs/doc.md -> src/main.ts sig:abc123\n", }); - const start_path = try tmp.dir.realpathAlloc(allocator, "repo/nested/work"); + const start_path = try tmp.dir.realPathFileAlloc(io, "repo/nested/work", allocator); defer allocator.free(start_path); - var discovered = try discover(allocator, scratch_arena.allocator(), start_path); + var discovered = try discover(io, allocator, scratch_arena.allocator(), start_path); defer { for (discovered.bindings.items) |*b| { allocator.free(b.doc_path); @@ -402,18 +401,19 @@ test "discover walks up to find drift.lock" { test "discover returns empty lockfile rooted at start path when missing" { const allocator = std.testing.allocator; + const io = std.testing.io; var scratch_arena = std.heap.ArenaAllocator.init(allocator); defer scratch_arena.deinit(); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.makePath("repo/.git"); + try tmp.dir.createDirPath(io, "repo/.git"); - const start_path = try tmp.dir.realpathAlloc(allocator, "repo"); + const start_path = try tmp.dir.realPathFileAlloc(io, "repo", allocator); defer allocator.free(start_path); - var discovered = try discover(allocator, scratch_arena.allocator(), start_path); + var discovered = try discover(io, allocator, scratch_arena.allocator(), start_path); defer { discovered.bindings.deinit(allocator); allocator.free(discovered.root_path); diff --git a/src/main.zig b/src/main.zig index 6f259f9..b63f994 100644 --- a/src/main.zig +++ b/src/main.zig @@ -91,8 +91,9 @@ fn parseExOrReport( comptime value_parsers: anytype, allocator: std.mem.Allocator, diag: *clap.Diagnostic, + io: std.Io, stderr_w: *std.Io.Writer, - iter: *std.process.ArgIterator, + iter: *std.process.Args.Iterator, terminating_positional: usize, ) clap.ResultEx(clap.Help, params, value_parsers) { return clap.parseEx(clap.Help, params, value_parsers, iter, .{ @@ -100,31 +101,30 @@ fn parseExOrReport( .allocator = allocator, .terminating_positional = terminating_positional, }) catch |err| { - diag.report(stderr_w, err) catch {}; + diag.reportToFile(io, .stderr(), err) catch {}; fatal(stderr_w, "", .{}); }; } -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const io = init.io; var stdout_buf: [4096]u8 = undefined; var stderr_buf: [4096]u8 = undefined; - var stdout_w = std.fs.File.stdout().writer(&stdout_buf); - var stderr_w = std.fs.File.stderr().writer(&stderr_buf); + var stdout_w = std.Io.File.stdout().writer(io, &stdout_buf); + var stderr_w = std.Io.File.stderr().writer(io, &stderr_buf); defer stdout_w.interface.flush() catch {}; defer stderr_w.interface.flush() catch {}; - var iter = try std.process.ArgIterator.initWithAllocator(allocator); + var iter = try init.minimal.args.iterateAllocator(gpa); defer iter.deinit(); _ = iter.next(); // skip executable name var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, &main_params, main_parsers, &iter, .{ .diagnostic = &diag, - .allocator = allocator, + .allocator = gpa, .terminating_positional = 0, }) catch |err| switch (err) { error.NameNotPartOfEnum => { @@ -133,7 +133,7 @@ pub fn main() !void { fatal(&stderr_w.interface, "", .{}); }, else => { - diag.report(&stderr_w.interface, err) catch {}; + diag.reportToFile(io, .stderr(), err) catch {}; fatal(&stderr_w.interface, "", .{}); }, }; @@ -158,7 +158,7 @@ pub fn main() !void { switch (command) { .check, .lint => { - var sub = parseExOrReport(&check_params, clap.parsers.default, allocator, &diag, &stderr_w.interface, &iter, clap_parse_all); + var sub = parseExOrReport(&check_params, clap.parsers.default, gpa, &diag, io, &stderr_w.interface, &iter, clap_parse_all); defer sub.deinit(); if (iter.next()) |_| { fatal(&stderr_w.interface, "usage: drift check [--format text|json] [--changed ] [--silent]\n", .{}); @@ -166,16 +166,13 @@ pub fn main() !void { const format = parseFormat(sub.args.format, &stderr_w.interface); const silent = sub.args.silent != 0; var null_buf: [1]u8 = undefined; - var null_file = std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }) catch - fatal(&stderr_w.interface, "error: cannot open /dev/null\n", .{}); - defer null_file.close(); - var null_w = null_file.writer(&null_buf); - var run_arena = std.heap.ArenaAllocator.init(allocator); + var discarding = std.Io.Writer.Discarding.init(&null_buf); + var run_arena = std.heap.ArenaAllocator.init(gpa); defer run_arena.deinit(); - var scratch_arena = std.heap.ArenaAllocator.init(allocator); + var scratch_arena = std.heap.ArenaAllocator.init(gpa); defer scratch_arena.deinit(); - const ctx = CommandContext{ .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; - const out_w = if (silent) &null_w.interface else &stdout_w.interface; + const ctx = CommandContext{ .io = io, .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; + const out_w = if (silent) &discarding.writer else &stdout_w.interface; const run_status = lint.run(ctx, out_w, &stderr_w.interface, format, sub.args.changed) catch |err| switch (err) { error.LintCheckFailed => { stdout_w.interface.flush() catch {}; @@ -194,23 +191,23 @@ pub fn main() !void { } }, .status => { - var sub = parseExOrReport(&format_params, clap.parsers.default, allocator, &diag, &stderr_w.interface, &iter, clap_parse_all); + var sub = parseExOrReport(&format_params, clap.parsers.default, gpa, &diag, io, &stderr_w.interface, &iter, clap_parse_all); defer sub.deinit(); if (iter.next()) |_| { fatal(&stderr_w.interface, "usage: drift status [--format text|json]\n", .{}); } const format = parseFormat(sub.args.format, &stderr_w.interface); - var run_arena = std.heap.ArenaAllocator.init(allocator); + var run_arena = std.heap.ArenaAllocator.init(gpa); defer run_arena.deinit(); - var scratch_arena = std.heap.ArenaAllocator.init(allocator); + var scratch_arena = std.heap.ArenaAllocator.init(gpa); defer scratch_arena.deinit(); - const ctx = CommandContext{ .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; + const ctx = CommandContext{ .io = io, .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; status.run(ctx, &stdout_w.interface, &stderr_w.interface, format) catch |err| { exitWithError(&stderr_w.interface, err); }; }, .link => { - var sub = parseExOrReport(&link_params, link_parsers, allocator, &diag, &stderr_w.interface, &iter, 0); + var sub = parseExOrReport(&link_params, link_parsers, gpa, &diag, io, &stderr_w.interface, &iter, 0); defer sub.deinit(); var doc_is_still_accurate = sub.args.@"doc-is-still-accurate" != 0; const doc_path = sub.positionals[0] orelse { @@ -231,11 +228,11 @@ pub fn main() !void { if (has_extra_args) { fatal(&stderr_w.interface, "usage: drift link [anchor] [--doc-is-still-accurate]\n", .{}); } - var run_arena = std.heap.ArenaAllocator.init(allocator); + var run_arena = std.heap.ArenaAllocator.init(gpa); defer run_arena.deinit(); - var scratch_arena = std.heap.ArenaAllocator.init(allocator); + var scratch_arena = std.heap.ArenaAllocator.init(gpa); defer scratch_arena.deinit(); - const ctx = CommandContext{ .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; + const ctx = CommandContext{ .io = io, .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; link.run(ctx, &stdout_w.interface, &stderr_w.interface, doc_path, optional_anchor, doc_is_still_accurate) catch |err| switch (err) { error.DocReadFailed, error.NoBindingsForDoc => { fatal(&stderr_w.interface, "", .{}); @@ -253,7 +250,7 @@ pub fn main() !void { }; }, .unlink => { - var sub = parseExOrReport(&unlink_params, unlink_parsers, allocator, &diag, &stderr_w.interface, &iter, clap_parse_all); + var sub = parseExOrReport(&unlink_params, unlink_parsers, gpa, &diag, io, &stderr_w.interface, &iter, clap_parse_all); defer sub.deinit(); const doc_path = sub.positionals[0] orelse { fatal(&stderr_w.interface, "usage: drift unlink \n", .{}); @@ -264,11 +261,11 @@ pub fn main() !void { if (iter.next()) |_| { fatal(&stderr_w.interface, "usage: drift unlink \n", .{}); } - var run_arena = std.heap.ArenaAllocator.init(allocator); + var run_arena = std.heap.ArenaAllocator.init(gpa); defer run_arena.deinit(); - var scratch_arena = std.heap.ArenaAllocator.init(allocator); + var scratch_arena = std.heap.ArenaAllocator.init(gpa); defer scratch_arena.deinit(); - const ctx = CommandContext{ .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; + const ctx = CommandContext{ .io = io, .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; unlink.run(ctx, &stdout_w.interface, &stderr_w.interface, doc_path, anchor) catch |err| { exitWithError(&stderr_w.interface, err); }; @@ -280,11 +277,11 @@ pub fn main() !void { if (iter.next()) |_| { fatal(&stderr_w.interface, "usage: drift refs \n", .{}); } - var run_arena = std.heap.ArenaAllocator.init(allocator); + var run_arena = std.heap.ArenaAllocator.init(gpa); defer run_arena.deinit(); - var scratch_arena = std.heap.ArenaAllocator.init(allocator); + var scratch_arena = std.heap.ArenaAllocator.init(gpa); defer scratch_arena.deinit(); - const ctx = CommandContext{ .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; + const ctx = CommandContext{ .io = io, .run_arena = run_arena.allocator(), .scratch_arena = &scratch_arena }; refs.run(ctx, &stdout_w.interface, &stderr_w.interface, target) catch |err| { exitWithError(&stderr_w.interface, err); }; @@ -293,7 +290,7 @@ pub fn main() !void { } } -fn printUsage(w: *std.io.Writer) void { +fn printUsage(w: *std.Io.Writer) void { w.print( \\drift — bind docs to code, lint for drift \\ @@ -315,13 +312,13 @@ fn printUsage(w: *std.io.Writer) void { , .{}) catch {}; } -fn fatal(stderr_w: *std.io.Writer, comptime fmt: []const u8, args: anytype) noreturn { +fn fatal(stderr_w: *std.Io.Writer, comptime fmt: []const u8, args: anytype) noreturn { stderr_w.print(fmt, args) catch {}; stderr_w.flush() catch {}; std.process.exit(1); } -fn exitWithError(stderr_w: *std.io.Writer, err: anyerror) noreturn { +fn exitWithError(stderr_w: *std.Io.Writer, err: anyerror) noreturn { const message: []const u8 = switch (err) { error.InvalidBindingLine => "malformed binding in drift.lock", error.InvalidMetadataField => "malformed metadata field in drift.lock", diff --git a/src/markdown.zig b/src/markdown.zig index 82e3717..70853db 100644 --- a/src/markdown.zig +++ b/src/markdown.zig @@ -26,12 +26,12 @@ pub fn parseDocument(allocator: Allocator, source: []const u8) !?ParsedDoc { const block_tree = parseBlockTree(source) orelse return null; defer block_tree.destroy(); - var ranges: std.ArrayList(ts.Range) = .{}; + var ranges: std.ArrayList(ts.Range) = .empty; defer ranges.deinit(allocator); try collectInlineRangesForNode(allocator, block_tree.rootNode(), &ranges); if (ranges.items.len == 0) { - return .{ .links = .{}, .allocator = allocator }; + return .{ .links = .empty, .allocator = allocator }; } var inline_parser = ts.Parser.create(); @@ -42,7 +42,7 @@ pub fn parseDocument(allocator: Allocator, source: []const u8) !?ParsedDoc { const inline_tree = inline_parser.parseString(source, null) orelse return null; defer inline_tree.destroy(); - var links: std.ArrayList(Link) = .{}; + var links: std.ArrayList(Link) = .empty; errdefer links.deinit(allocator); try collectInlineLinks(allocator, source, inline_tree.rootNode(), &links); diff --git a/src/payload/drift_check_v1.zig b/src/payload/drift_check_v1.zig index 445082e..e4c231c 100644 --- a/src/payload/drift_check_v1.zig +++ b/src/payload/drift_check_v1.zig @@ -73,7 +73,7 @@ pub const Blame = struct { subject: []const u8, }; -pub fn writeJson(w: *std.io.Writer, doc: DriftCheckV1) !void { +pub fn writeJson(w: *std.Io.Writer, doc: DriftCheckV1) !void { try std.json.Stringify.value(doc, .{ .whitespace = .indent_2 }, w); try w.writeByte('\n'); } diff --git a/src/vcs.zig b/src/vcs.zig index d79e22e..c9a4dbf 100644 --- a/src/vcs.zig +++ b/src/vcs.zig @@ -15,24 +15,41 @@ pub fn detectVcs() VcsKind { return .git; } +fn runGit( + io: std.Io, + allocator: std.mem.Allocator, + argv: []const []const u8, + cwd_path: []const u8, + stdout_limit: std.Io.Limit, +) !std.process.RunResult { + return std.process.run(allocator, io, .{ + .argv = argv, + .cwd = .{ .path = cwd_path }, + .stdout_limit = stdout_limit, + .stderr_limit = stdout_limit, + }); +} + /// Get the last commit/change ID that touched a given file path. -pub fn getLastCommit(allocator: std.mem.Allocator, cwd_path: []const u8, file_path: []const u8, vcs: VcsKind) !?[]const u8 { +pub fn getLastCommit(io: std.Io, allocator: std.mem.Allocator, cwd_path: []const u8, file_path: []const u8, vcs: VcsKind) !?[]const u8 { const result = switch (vcs) { - .git => try std.process.Child.run(.{ - .allocator = allocator, - .argv = &.{ "git", "log", "-1", "--format=%H", "--", file_path }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }), + .git => try runGit( + io, + allocator, + &.{ "git", "log", "-1", "--format=%H", "--", file_path }, + cwd_path, + .limited(256 * 1024), + ), .jj => blk: { const revset = try std.fmt.allocPrint(allocator, "latest(::@ & file(\"{s}\"))", .{file_path}); defer allocator.free(revset); - break :blk try std.process.Child.run(.{ - .allocator = allocator, - .argv = &.{ "jj", "log", "-r", revset, "--no-graph", "-T", "change_id ++ \"\\n\"", "--color=never" }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }); + break :blk try runGit( + io, + allocator, + &.{ "jj", "log", "-r", revset, "--no-graph", "-T", "change_id ++ \"\\n\"", "--color=never" }, + cwd_path, + .limited(256 * 1024), + ); }, }; defer allocator.free(result.stderr); @@ -44,7 +61,7 @@ pub fn getLastCommit(allocator: std.mem.Allocator, cwd_path: []const u8, file_pa } // Trim trailing newline - const trimmed = std.mem.trimRight(u8, stdout, "\n\r "); + const trimmed = std.mem.trimEnd(u8, stdout, "\n\r "); if (trimmed.len == 0) { allocator.free(stdout); return null; @@ -57,6 +74,7 @@ pub fn getLastCommit(allocator: std.mem.Allocator, cwd_path: []const u8, file_pa /// Check if a bound file was modified after the given commit/change. pub fn checkStaleness( + io: std.Io, allocator: std.mem.Allocator, cwd_path: []const u8, doc_commit: []const u8, @@ -67,34 +85,37 @@ pub fn checkStaleness( .git => blk: { const range = try std.fmt.allocPrint(allocator, "{s}..HEAD", .{doc_commit}); defer allocator.free(range); - break :blk try std.process.Child.run(.{ - .allocator = allocator, - .argv = &.{ "git", "log", "--oneline", range, "--", bound_file }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }); + break :blk try runGit( + io, + allocator, + &.{ "git", "log", "--oneline", range, "--", bound_file }, + cwd_path, + .limited(256 * 1024), + ); }, .jj => blk: { const revset = try std.fmt.allocPrint(allocator, "{s}..@ & file(\"{s}\")", .{ doc_commit, bound_file }); defer allocator.free(revset); - break :blk try std.process.Child.run(.{ - .allocator = allocator, - .argv = &.{ "jj", "log", "-r", revset, "--no-graph", "-T", "change_id ++ \"\\n\"", "--color=never" }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }); + break :blk try runGit( + io, + allocator, + &.{ "jj", "log", "-r", revset, "--no-graph", "-T", "change_id ++ \"\\n\"", "--color=never" }, + cwd_path, + .limited(256 * 1024), + ); }, }; defer allocator.free(result.stdout); defer allocator.free(result.stderr); - const trimmed = std.mem.trimRight(u8, result.stdout, "\n\r "); + const trimmed = std.mem.trimEnd(u8, result.stdout, "\n\r "); return trimmed.len > 0; } /// Get file content at a specific revision. Returns null if the file didn't exist at that revision. /// Caller owns returned memory. pub fn getFileAtRevision( + io: std.Io, allocator: std.mem.Allocator, cwd_path: []const u8, revision: []const u8, @@ -112,17 +133,12 @@ pub fn getFileAtRevision( .jj => &.{ "jj", "file", "show", "-r", revision, file_path }, }; - const result = std.process.Child.run(.{ - .allocator = allocator, - .argv = argv, - .cwd = cwd_path, - .max_output_bytes = 1024 * 1024, - }) catch return null; + const result = runGit(io, allocator, argv, cwd_path, .limited(1024 * 1024)) catch return null; defer allocator.free(result.stderr); // Non-zero exit means the file didn't exist at that revision switch (result.term) { - .Exited => |code| { + .exited => |code| { if (code != 0) { allocator.free(result.stdout); return null; @@ -155,7 +171,7 @@ pub const BlameInfo = struct { }; fn parseBlameInfoOutput(allocator: std.mem.Allocator, stdout: []const u8) !?BlameInfo { - const trimmed = std.mem.trimRight(u8, stdout, "\n\r "); + const trimmed = std.mem.trimEnd(u8, stdout, "\n\r "); if (trimmed.len == 0) return null; var lines = std.mem.splitScalar(u8, trimmed, '\n'); @@ -185,6 +201,7 @@ fn parseBlameInfoOutput(allocator: std.mem.Allocator, stdout: []const u8) !?Blam /// Returns null if no commits changed the file after the revision. /// String fields are allocated with `result_allocator`. Subprocess I/O buffers use `subprocess_allocator`. pub fn getBlameInfo( + io: std.Io, result_allocator: std.mem.Allocator, subprocess_allocator: std.mem.Allocator, cwd_path: []const u8, @@ -197,12 +214,13 @@ pub fn getBlameInfo( const range = try std.fmt.allocPrint(subprocess_allocator, "{s}..HEAD", .{after_revision}); defer subprocess_allocator.free(range); - const result = std.process.Child.run(.{ - .allocator = subprocess_allocator, - .argv = &.{ "git", "log", "-1", "--format=%an%n%H%n%cd%n%s", "--date=iso-strict", range, "--", file_path }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }) catch return null; + const result = runGit( + io, + subprocess_allocator, + &.{ "git", "log", "-1", "--format=%an%n%H%n%cd%n%s", "--date=iso-strict", range, "--", file_path }, + cwd_path, + .limited(256 * 1024), + ) catch return null; defer subprocess_allocator.free(result.stderr); defer subprocess_allocator.free(result.stdout); @@ -215,6 +233,7 @@ pub fn getBlameInfo( /// Get blame info for the most recent commit that touched a file, regardless of baseline. /// String fields are allocated with `result_allocator`. Subprocess I/O buffers use `subprocess_allocator`. pub fn getLatestBlameInfo( + io: std.Io, result_allocator: std.mem.Allocator, subprocess_allocator: std.mem.Allocator, cwd_path: []const u8, @@ -223,12 +242,13 @@ pub fn getLatestBlameInfo( ) !?BlameInfo { switch (vcs_kind) { .git => { - const result = std.process.Child.run(.{ - .allocator = subprocess_allocator, - .argv = &.{ "git", "log", "-1", "--format=%an%n%H%n%cd%n%s", "--date=iso-strict", "--", file_path }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }) catch return null; + const result = runGit( + io, + subprocess_allocator, + &.{ "git", "log", "-1", "--format=%an%n%H%n%cd%n%s", "--date=iso-strict", "--", file_path }, + cwd_path, + .limited(256 * 1024), + ) catch return null; defer subprocess_allocator.free(result.stderr); defer subprocess_allocator.free(result.stdout); @@ -243,7 +263,7 @@ pub fn getLatestBlameInfo( /// and SSH URL (`ssh://git@github.com/owner/repo`) formats. /// Strips `.git` suffix and trailing slashes. Returns null for non-GitHub URLs. pub fn normalizeGitHubUrl(allocator: std.mem.Allocator, url: []const u8) ?[]const u8 { - const trimmed = std.mem.trimRight(u8, url, " \t\n\r/"); + const trimmed = std.mem.trimEnd(u8, url, " \t\n\r/"); // Extract the owner/repo path from the URL const path = blk: { @@ -276,42 +296,45 @@ pub fn normalizeGitHubUrl(allocator: std.mem.Allocator, url: []const u8) ?[]cons /// Get the normalized repo identity by querying `git remote get-url origin`. /// Returns `github:owner/repo` or null if not a GitHub remote. /// Result string is allocated with `result_allocator`; subprocess buffers use `subprocess_allocator`. -pub fn getRepoIdentity(result_allocator: std.mem.Allocator, subprocess_allocator: std.mem.Allocator, cwd_path: []const u8) ?[]const u8 { - const result = std.process.Child.run(.{ - .allocator = subprocess_allocator, - .argv = &.{ "git", "remote", "get-url", "origin" }, - .cwd = cwd_path, - .max_output_bytes = 4096, - }) catch return null; +pub fn getRepoIdentity(io: std.Io, result_allocator: std.mem.Allocator, subprocess_allocator: std.mem.Allocator, cwd_path: []const u8) ?[]const u8 { + const result = runGit( + io, + subprocess_allocator, + &.{ "git", "remote", "get-url", "origin" }, + cwd_path, + .limited(4096), + ) catch return null; defer subprocess_allocator.free(result.stderr); defer subprocess_allocator.free(result.stdout); switch (result.term) { - .Exited => |code| if (code != 0) return null, + .exited => |code| if (code != 0) return null, else => return null, } - const trimmed = std.mem.trimRight(u8, result.stdout, "\n\r "); + const trimmed = std.mem.trimEnd(u8, result.stdout, "\n\r "); if (trimmed.len == 0) return null; return normalizeGitHubUrl(result_allocator, trimmed); } /// Get the current change/commit ID (short form) for auto-provenance. -pub fn getCurrentChangeId(allocator: std.mem.Allocator, cwd_path: []const u8, vcs: VcsKind) !?[]const u8 { +pub fn getCurrentChangeId(io: std.Io, allocator: std.mem.Allocator, cwd_path: []const u8, vcs: VcsKind) !?[]const u8 { const result = switch (vcs) { - .git => try std.process.Child.run(.{ - .allocator = allocator, - .argv = &.{ "git", "rev-parse", "--short", "HEAD" }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }), - .jj => try std.process.Child.run(.{ - .allocator = allocator, - .argv = &.{ "jj", "log", "-r", "@", "--no-graph", "-T", "change_id.shortest(8)", "--color=never" }, - .cwd = cwd_path, - .max_output_bytes = 256 * 1024, - }), + .git => try runGit( + io, + allocator, + &.{ "git", "rev-parse", "--short", "HEAD" }, + cwd_path, + .limited(256 * 1024), + ), + .jj => try runGit( + io, + allocator, + &.{ "jj", "log", "-r", "@", "--no-graph", "-T", "change_id.shortest(8)", "--color=never" }, + cwd_path, + .limited(256 * 1024), + ), }; defer allocator.free(result.stderr); @@ -321,7 +344,7 @@ pub fn getCurrentChangeId(allocator: std.mem.Allocator, cwd_path: []const u8, vc return null; } - const trimmed = std.mem.trimRight(u8, stdout, "\n\r "); + const trimmed = std.mem.trimEnd(u8, stdout, "\n\r "); if (trimmed.len == 0) { allocator.free(stdout); return null; @@ -335,34 +358,37 @@ pub fn getCurrentChangeId(allocator: std.mem.Allocator, cwd_path: []const u8, vc /// Persistent `git cat-file --batch` process for fetching historical file /// content without spawning a new process per query. pub const GitCatFile = struct { + io: std.Io, child: std.process.Child, read_buf: []u8, - stdout_reader: std.fs.File.Reader, + stdout_reader: std.Io.File.Reader, allocator: std.mem.Allocator, const read_buf_size = 8192; - pub fn init(allocator: std.mem.Allocator, cwd_path: []const u8) !GitCatFile { + pub fn init(io: std.Io, allocator: std.mem.Allocator, cwd_path: []const u8) !GitCatFile { const read_buf = try allocator.alloc(u8, read_buf_size); errdefer allocator.free(read_buf); - var child = std.process.Child.init( - &.{ "git", "cat-file", "--batch" }, - allocator, - ); - child.cwd = cwd_path; - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Ignore; - try child.spawn(); + var child = try std.process.spawn(io, .{ + .argv = &.{ "git", "cat-file", "--batch" }, + .cwd = .{ .path = cwd_path }, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .ignore, + }); + errdefer { + child.kill(io); + } var result: GitCatFile = .{ + .io = io, .child = child, .read_buf = read_buf, .stdout_reader = undefined, .allocator = allocator, }; - result.stdout_reader = result.child.stdout.?.readerStreaming(result.read_buf); + result.stdout_reader = result.child.stdout.?.readerStreaming(io, result.read_buf); return result; } @@ -371,10 +397,10 @@ pub const GitCatFile = struct { pub fn getContent(self: *GitCatFile, allocator: std.mem.Allocator, revision: []const u8, file_path: []const u8) !?[]const u8 { const stdin = self.child.stdin orelse return error.BrokenPipe; - stdin.writeAll(revision) catch return null; - stdin.writeAll(":") catch return null; - stdin.writeAll(file_path) catch return null; - stdin.writeAll("\n") catch return null; + stdin.writeStreamingAll(self.io, revision) catch return null; + stdin.writeStreamingAll(self.io, ":") catch return null; + stdin.writeStreamingAll(self.io, file_path) catch return null; + stdin.writeStreamingAll(self.io, "\n") catch return null; const header = self.stdout_reader.interface.takeDelimiterExclusive('\n') catch return null; @@ -401,10 +427,10 @@ pub const GitCatFile = struct { pub fn deinit(self: *GitCatFile) void { if (self.child.stdin) |stdin| { - stdin.close(); + stdin.close(self.io); self.child.stdin = null; } - _ = self.child.wait() catch {}; + _ = self.child.wait(self.io) catch {}; self.allocator.free(self.read_buf); } }; diff --git a/test/helpers.zig b/test/helpers.zig index d780b6f..2fb219b 100644 --- a/test/helpers.zig +++ b/test/helpers.zig @@ -10,7 +10,7 @@ pub const ExecResult = struct { pub fn exitCode(self: ExecResult) u8 { return switch (self.term) { - .Exited => |code| code, + .exited => |code| code, else => 255, }; } @@ -23,33 +23,36 @@ pub const ExecResult = struct { /// A temporary git repository for integration tests. pub const TempRepo = struct { + io: std.Io, tmp: std.testing.TmpDir, - abs_path: []const u8, + abs_path: [:0]u8, allocator: std.mem.Allocator, /// Create a temp dir and run `git init` inside it. pub fn init(allocator: std.mem.Allocator) !TempRepo { + const io = std.testing.io; const tmp = std.testing.tmpDir(.{}); // Resolve the absolute path of the temp dir so we can pass it as cwd to child processes. - const abs_path = try std.fs.cwd().realpathAlloc(allocator, ".zig-cache/tmp/" ++ &tmp.sub_path); + const abs_path = try std.Io.Dir.cwd().realPathFileAlloc(io, ".zig-cache/tmp/" ++ &tmp.sub_path, allocator); // git init - const git_init = try runProcess(allocator, &.{ "git", "init" }, abs_path); + const git_init = try runProcess(allocator, io, &.{ "git", "init" }, abs_path); defer git_init.deinit(allocator); // Configure git user for commits - const git_email = try runProcess(allocator, &.{ "git", "config", "user.email", "test@drift.dev" }, abs_path); + const git_email = try runProcess(allocator, io, &.{ "git", "config", "user.email", "test@drift.dev" }, abs_path); defer git_email.deinit(allocator); - const git_name = try runProcess(allocator, &.{ "git", "config", "user.name", "Test" }, abs_path); + const git_name = try runProcess(allocator, io, &.{ "git", "config", "user.name", "Test" }, abs_path); defer git_name.deinit(allocator); // Create initial commit so HEAD exists - const allow_empty = try runProcess(allocator, &.{ "git", "commit", "--allow-empty", "-m", "initial" }, abs_path); + const allow_empty = try runProcess(allocator, io, &.{ "git", "commit", "--allow-empty", "-m", "initial" }, abs_path); defer allow_empty.deinit(allocator); return .{ + .io = io, .tmp = tmp, .abs_path = abs_path, .allocator = allocator, @@ -59,9 +62,9 @@ pub const TempRepo = struct { /// Write a file at the given relative path, creating parent directories as needed. pub fn writeFile(self: *TempRepo, path: []const u8, content: []const u8) !void { if (std.fs.path.dirname(path)) |parent| { - try self.tmp.dir.makePath(parent); + try self.tmp.dir.createDirPath(self.io, parent); } - try self.tmp.dir.writeFile(.{ + try self.tmp.dir.writeFile(self.io, .{ .sub_path = path, .data = content, }); @@ -71,10 +74,10 @@ pub const TempRepo = struct { /// `frontmatter_files` is the list of file anchors for the drift: files: section. /// `body` is the markdown body after the frontmatter. pub fn writeDoc(self: *TempRepo, path: []const u8, frontmatter_files: []const []const u8, body: []const u8) !void { - var buf: std.ArrayList(u8) = .{}; - defer buf.deinit(self.allocator); + var buf: std.Io.Writer.Allocating = .init(self.allocator); + defer buf.deinit(); - const writer = buf.writer(self.allocator); + const writer = &buf.writer; try writer.writeAll("---\n"); try writer.writeAll("drift:\n"); try writer.writeAll(" files:\n"); @@ -89,15 +92,15 @@ pub const TempRepo = struct { } } - try self.writeFile(path, buf.items); + try self.writeFile(path, buf.written()); } /// Stage all files and create a git commit. pub fn commit(self: *TempRepo, message: []const u8) !void { - const add_result = try runProcess(self.allocator, &.{ "git", "add", "-A" }, self.abs_path); + const add_result = try runProcess(self.allocator, self.io, &.{ "git", "add", "-A" }, self.abs_path); defer add_result.deinit(self.allocator); - const commit_result = try runProcess(self.allocator, &.{ "git", "commit", "-m", message }, self.abs_path); + const commit_result = try runProcess(self.allocator, self.io, &.{ "git", "commit", "-m", message }, self.abs_path); defer commit_result.deinit(self.allocator); } @@ -113,7 +116,7 @@ pub const TempRepo = struct { } const argv = argv_buf[0 .. args.len + 1]; - return runProcess(self.allocator, argv, self.abs_path); + return runProcess(self.allocator, self.io, argv, self.abs_path); } /// Run the drift binary with given arguments, cwd set to a subdirectory of the temp repo. @@ -130,12 +133,12 @@ pub const TempRepo = struct { const sub_path = try std.fs.path.join(self.allocator, &.{ self.abs_path, subdir }); defer self.allocator.free(sub_path); - return runProcess(self.allocator, argv, sub_path); + return runProcess(self.allocator, self.io, argv, sub_path); } /// Get the short commit hash of HEAD. Caller owns returned memory. pub fn getHeadRevision(self: *TempRepo, allocator: std.mem.Allocator) ![]const u8 { - const result = try runProcess(allocator, &.{ "git", "rev-parse", "--short", "HEAD" }, self.abs_path); + const result = try runProcess(allocator, self.io, &.{ "git", "rev-parse", "--short", "HEAD" }, self.abs_path); defer allocator.free(result.stderr); const stdout = result.stdout; if (stdout.len > 0 and stdout[stdout.len - 1] == '\n') { @@ -148,7 +151,7 @@ pub const TempRepo = struct { /// Read a file from the temp repo. Caller owns the returned memory. pub fn readFile(self: *TempRepo, path: []const u8) ![]const u8 { - return self.tmp.dir.readFileAlloc(self.allocator, path, 1024 * 1024); + return self.tmp.dir.readFileAlloc(self.io, path, self.allocator, .limited(1024 * 1024)); } /// Clean up the temp directory. @@ -159,12 +162,12 @@ pub const TempRepo = struct { }; /// Run a process and collect stdout/stderr. Caller owns result memory. -fn runProcess(allocator: std.mem.Allocator, argv: []const []const u8, cwd: []const u8) !ExecResult { - const result = try std.process.Child.run(.{ - .allocator = allocator, +fn runProcess(allocator: std.mem.Allocator, io: std.Io, argv: []const []const u8, cwd: []const u8) !ExecResult { + const result = try std.process.run(allocator, io, .{ .argv = argv, - .cwd = cwd, - .max_output_bytes = 256 * 1024, + .cwd = .{ .path = cwd }, + .stdout_limit = .limited(256 * 1024), + .stderr_limit = .limited(256 * 1024), }); return .{ @@ -179,7 +182,7 @@ fn runProcess(allocator: std.mem.Allocator, argv: []const []const u8, cwd: []con /// Assert that the process exited with the expected code. pub fn expectExitCode(term: std.process.Child.Term, expected: u8) !void { switch (term) { - .Exited => |code| { + .exited => |code| { if (code != expected) { std.debug.print("\nExpected exit code {d}, got {d}\n", .{ expected, code }); return error.TestUnexpectedResult; From bb761ec9417119242a02e65db88f728d87a5474a Mon Sep 17 00:00:00 2001 From: Laurynas Keturakis Date: Fri, 17 Apr 2026 11:26:51 +0200 Subject: [PATCH 3/6] chore: rename to non-deprecated std.Io.Dir.path and std.mem.find* --- src/commands/link.zig | 22 ++++++++++----------- src/commands/lint.zig | 36 +++++++++++++++++----------------- src/commands/refs.zig | 8 ++++---- src/commands/unlink.zig | 14 ++++++------- src/lockfile.zig | 22 ++++++++++----------- src/symbols.zig | 2 +- src/target.zig | 4 ++-- src/vcs.zig | 2 +- test/helpers.zig | 8 ++++---- test/integration/lint_test.zig | 2 +- 10 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/commands/link.zig b/src/commands/link.zig index e9f4a7a..a0d909d 100644 --- a/src/commands/link.zig +++ b/src/commands/link.zig @@ -19,8 +19,8 @@ pub fn run( ) !void { const cwd_path = try std.Io.Dir.cwd().realPathFileAlloc(ctx.io, ".", ctx.run_arena); - const abs_doc_path = try std.fs.path.resolve(ctx.run_arena, &.{ cwd_path, doc_path }); - const doc_dir = std.fs.path.dirname(abs_doc_path) orelse cwd_path; + const abs_doc_path = try std.Io.Dir.path.resolve(ctx.run_arena, &.{ cwd_path, doc_path }); + const doc_dir = std.Io.Dir.path.dirname(abs_doc_path) orelse cwd_path; var lf = try lockfile.discover(ctx.io, ctx.run_arena, ctx.scratch(), doc_dir); ctx.resetScratch(); @@ -226,7 +226,7 @@ fn normalizeDocPath( doc_path: []const u8, ) ![]const u8 { const absolute = try resolveInputPath(ctx, root_path, cwd_path, doc_path); - const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const relative = try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); return relative; } @@ -244,7 +244,7 @@ fn normalizeTargetPath( return error.TargetNotFound; } - const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const relative = try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, absolute); if (parsed.symbol_name) |symbol| { if (parsed.isHeading()) { @@ -274,14 +274,14 @@ fn resolveInputPath( cwd_path: []const u8, path: []const u8, ) ![]const u8 { - if (std.fs.path.isAbsolute(path)) { + if (std.Io.Dir.path.isAbsolute(path)) { return try ctx.scratch().dupe(u8, path); } - const cwd_candidate = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, path }); + const cwd_candidate = try std.Io.Dir.path.resolve(ctx.scratch(), &.{ cwd_path, path }); if (pathExists(ctx.io, cwd_candidate)) return cwd_candidate; - return try std.fs.path.resolve(ctx.scratch(), &.{ root_path, path }); + return try std.Io.Dir.path.resolve(ctx.scratch(), &.{ root_path, path }); } fn pathExists(io: std.Io, path: []const u8) bool { @@ -290,7 +290,7 @@ fn pathExists(io: std.Io, path: []const u8) bool { } fn readResolvedFile(ctx: CommandContext, path: []const u8) ![]const u8 { - const file = if (std.fs.path.isAbsolute(path)) + const file = if (std.Io.Dir.path.isAbsolute(path)) try std.Io.Dir.openFileAbsolute(ctx.io, path, .{}) else try std.Io.Dir.cwd().openFile(ctx.io, path, .{}); @@ -377,7 +377,7 @@ fn findDocSectionForTarget( while (lines.next()) |line| { for (search_terms) |term| { - if (term.len > 0 and std.mem.indexOf(u8, line, term) != null) { + if (term.len > 0 and std.mem.find(u8, line, term) != null) { match_offset = line_start; break; } @@ -479,7 +479,7 @@ fn findNearestHeadingAbove(doc_content: []const u8, section_text: []const u8) ?[ } // Also check if section_text itself starts with a heading - const first_line_end = std.mem.indexOfScalar(u8, section_text, '\n') orelse section_text.len; + const first_line_end = std.mem.findScalar(u8, section_text, '\n') orelse section_text.len; const first_line = std.mem.trimStart(u8, section_text[0..first_line_end], " \t"); if (first_line.len > 0 and first_line[0] == '#') { const hashes = countLeadingChar(first_line, '#'); @@ -523,7 +523,7 @@ fn printSymbolTarget( defer ctx.resetScratch(); const symbol = parsed.symbol_name orelse return; - const ext = std.fs.path.extension(parsed.file_path); + const ext = std.Io.Dir.path.extension(parsed.file_path); const lang_query = symbols.languageForExtension(ext) orelse return; const range = symbols.extractSymbolContent(content, lang_query, symbol) orelse return; const source = content[range[0]..range[1]]; diff --git a/src/commands/lint.zig b/src/commands/lint.zig index 57a368a..eb9c6b5 100644 --- a/src/commands/lint.zig +++ b/src/commands/lint.zig @@ -315,7 +315,7 @@ fn discoverDocGroups( var offset: usize = 0; while (offset < result.stdout.len) { const rest = result.stdout[offset..]; - const rel_end = std.mem.indexOfScalar(u8, rest, 0) orelse break; + const rel_end = std.mem.findScalar(u8, rest, 0) orelse break; const line = rest[0..rel_end]; offset += rel_end + 1; @@ -349,12 +349,12 @@ fn discoverDocGroups( /// Check if a relative path has a closer drift.lock than root_path. /// Returns true if there's an intermediate drift.lock (the file belongs to a nested scope). fn hasNestedLockfile(io: std.Io, root_path: []const u8, rel_path: []const u8, allocator: std.mem.Allocator) bool { - var dir: []const u8 = std.fs.path.dirname(rel_path) orelse return false; + var dir: []const u8 = std.Io.Dir.path.dirname(rel_path) orelse return false; while (dir.len > 0) { - const candidate = std.fs.path.join(allocator, &.{ root_path, dir, "drift.lock" }) catch return false; + const candidate = std.Io.Dir.path.join(allocator, &.{ root_path, dir, "drift.lock" }) catch return false; if (pathExists(io, candidate)) return true; - dir = std.fs.path.dirname(dir) orelse break; + dir = std.Io.Dir.path.dirname(dir) orelse break; } return false; } @@ -392,7 +392,7 @@ fn checkDocLinks( file_cache: *FileCache, out: *std.ArrayList(JsonLinkRow), ) !void { - const absolute_doc_path = try std.fs.path.join(ctx.scratch(), &.{ root_path, doc_path }); + const absolute_doc_path = try std.Io.Dir.path.join(ctx.scratch(), &.{ root_path, doc_path }); const content = file_cache.getCurrent(absolute_doc_path) catch return orelse return; var parsed = (try markdown.parseDocument(ctx.run_arena, content)) orelse return; @@ -426,18 +426,18 @@ fn classifyRelativeLink( const trimmed = std.mem.trim(u8, raw_target, " \t\r\n"); if (trimmed.len == 0) return null; if (trimmed[0] == '#') return null; - if (std.fs.path.isAbsolute(trimmed)) return null; + if (std.Io.Dir.path.isAbsolute(trimmed)) return null; if (hasUriScheme(trimmed)) return null; - const path_part = if (std.mem.indexOfScalar(u8, trimmed, '#')) |idx| trimmed[0..idx] else trimmed; + const path_part = if (std.mem.findScalar(u8, trimmed, '#')) |idx| trimmed[0..idx] else trimmed; if (path_part.len == 0) return null; // Resolve symlinks on the doc path so relative links are computed from the real location. - const raw_absolute_doc = try std.fs.path.resolve(ctx.scratch(), &.{ root_path, doc_path }); + const raw_absolute_doc = try std.Io.Dir.path.resolve(ctx.scratch(), &.{ root_path, doc_path }); const real_doc_path = std.Io.Dir.cwd().realPathFileAlloc(ctx.io, raw_absolute_doc, ctx.scratch()) catch raw_absolute_doc; - const doc_dir = std.fs.path.dirname(real_doc_path) orelse root_path; - const absolute = try std.fs.path.resolve(ctx.scratch(), &.{ doc_dir, path_part }); - const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const doc_dir = std.Io.Dir.path.dirname(real_doc_path) orelse root_path; + const absolute = try std.Io.Dir.path.resolve(ctx.scratch(), &.{ doc_dir, path_part }); + const relative = try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, absolute); const exists = pathExists(ctx.io, absolute); ctx.resetScratch(); @@ -458,7 +458,7 @@ fn filePathMatchesChangedPrefix(file_path: []const u8, prefix: []const u8) bool if (prefix.len == 0) return true; if (!std.mem.startsWith(u8, file_path, prefix)) return false; if (file_path.len == prefix.len) return true; - return std.fs.path.isSep(file_path[prefix.len]); + return std.Io.Dir.path.isSep(file_path[prefix.len]); } fn docMatchesChangedPath(doc: DocGroup, changed_prefix: []const u8) bool { @@ -476,12 +476,12 @@ fn normalizeChangedPrefix( cwd_path: []const u8, raw_path: []const u8, ) ![]const u8 { - if (std.fs.path.isAbsolute(raw_path)) { - return try std.fs.path.relative(ctx.run_arena, "", null, root_path, raw_path); + if (std.Io.Dir.path.isAbsolute(raw_path)) { + return try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, raw_path); } - const absolute = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, raw_path }); - const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const absolute = try std.Io.Dir.path.resolve(ctx.scratch(), &.{ cwd_path, raw_path }); + const relative = try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); return relative; } @@ -496,7 +496,7 @@ fn checkBinding( ) !AnchorOutcome { const sig_hex = binding.fieldValue("sig") orelse return .{ .result = .stale, .reason_code = .baseline_unavailable }; - const absolute_path = try std.fs.path.join(ctx.scratch(), &.{ root_path, parsed.file_path }); + const absolute_path = try std.Io.Dir.path.join(ctx.scratch(), &.{ root_path, parsed.file_path }); const current_content = file_cache.getCurrent(absolute_path) catch { return .{ .result = .stale, .reason_code = .file_not_readable }; } orelse { @@ -509,7 +509,7 @@ fn checkBinding( return .{ .result = .stale, .reason_code = .symbol_not_found }; } } else { - const ext = std.fs.path.extension(parsed.file_path); + const ext = std.Io.Dir.path.extension(parsed.file_path); if (symbols.languageForExtension(ext)) |lang_query| { if (!symbols.resolveSymbolWithTreeSitter(current_content, lang_query, sym)) { return .{ .result = .stale, .reason_code = .symbol_not_found }; diff --git a/src/commands/refs.zig b/src/commands/refs.zig index 1e475dc..e24ea83 100644 --- a/src/commands/refs.zig +++ b/src/commands/refs.zig @@ -38,7 +38,7 @@ fn normalizeTargetPath( const symbol_name = parsed.symbol_name; const absolute = try resolveInputPath(ctx, root_path, cwd_path, file_part); - const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const relative = try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); if (symbol_name) |symbol| { @@ -53,14 +53,14 @@ fn resolveInputPath( cwd_path: []const u8, path: []const u8, ) ![]const u8 { - if (std.fs.path.isAbsolute(path)) { + if (std.Io.Dir.path.isAbsolute(path)) { return try ctx.scratch().dupe(u8, path); } - const cwd_candidate = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, path }); + const cwd_candidate = try std.Io.Dir.path.resolve(ctx.scratch(), &.{ cwd_path, path }); if (pathExists(ctx.io, cwd_candidate)) return cwd_candidate; - return try std.fs.path.resolve(ctx.scratch(), &.{ root_path, path }); + return try std.Io.Dir.path.resolve(ctx.scratch(), &.{ root_path, path }); } fn pathExists(io: std.Io, path: []const u8) bool { diff --git a/src/commands/unlink.zig b/src/commands/unlink.zig index f1838c6..4d4ba68 100644 --- a/src/commands/unlink.zig +++ b/src/commands/unlink.zig @@ -8,8 +8,8 @@ pub fn run(ctx: CommandContext, stdout_w: *std.Io.Writer, stderr_w: *std.Io.Writ const cwd_path = try std.Io.Dir.cwd().realPathFileAlloc(ctx.io, ".", ctx.run_arena); - const abs_doc_path = try std.fs.path.resolve(ctx.run_arena, &.{ cwd_path, doc_path }); - const doc_dir = std.fs.path.dirname(abs_doc_path) orelse cwd_path; + const abs_doc_path = try std.Io.Dir.path.resolve(ctx.run_arena, &.{ cwd_path, doc_path }); + const doc_dir = std.Io.Dir.path.dirname(abs_doc_path) orelse cwd_path; var lf = try lockfile.discover(ctx.io, ctx.run_arena, ctx.scratch(), doc_dir); ctx.resetScratch(); @@ -48,7 +48,7 @@ fn normalizeSpecPath( doc_path: []const u8, ) ![]const u8 { const absolute = try resolveInputPath(ctx, root_path, cwd_path, doc_path); - const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const relative = try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); return relative; } @@ -64,7 +64,7 @@ fn normalizeTargetPath( const symbol_name = parsed.symbol_name; const absolute = try resolveInputPath(ctx, root_path, cwd_path, file_part); - const relative = try std.fs.path.relative(ctx.run_arena, "", null, root_path, absolute); + const relative = try std.Io.Dir.path.relative(ctx.run_arena, "", null, root_path, absolute); ctx.resetScratch(); if (symbol_name) |symbol| { @@ -79,14 +79,14 @@ fn resolveInputPath( cwd_path: []const u8, path: []const u8, ) ![]const u8 { - if (std.fs.path.isAbsolute(path)) { + if (std.Io.Dir.path.isAbsolute(path)) { return try ctx.scratch().dupe(u8, path); } - const cwd_candidate = try std.fs.path.resolve(ctx.scratch(), &.{ cwd_path, path }); + const cwd_candidate = try std.Io.Dir.path.resolve(ctx.scratch(), &.{ cwd_path, path }); if (pathExists(ctx.io, cwd_candidate)) return cwd_candidate; - return try std.fs.path.resolve(ctx.scratch(), &.{ root_path, path }); + return try std.Io.Dir.path.resolve(ctx.scratch(), &.{ root_path, path }); } fn pathExists(io: std.Io, path: []const u8) bool { diff --git a/src/lockfile.zig b/src/lockfile.zig index 0b111fe..45c9e91 100644 --- a/src/lockfile.zig +++ b/src/lockfile.zig @@ -64,7 +64,7 @@ pub const ParseError = error{ /// `run` holds durable lockfile state; `scratch` holds walk temporaries and the lockfile file buffer (reset by caller). pub fn discover(io: std.Io, run: std.mem.Allocator, scratch: std.mem.Allocator, start_path: []const u8) !Lockfile { - const resolved_run = if (std.fs.path.isAbsolute(start_path)) + const resolved_run = if (std.Io.Dir.path.isAbsolute(start_path)) try run.dupe(u8, start_path) else try std.Io.Dir.cwd().realPathFileAlloc(io, start_path, run); @@ -73,19 +73,19 @@ pub fn discover(io: std.Io, run: std.mem.Allocator, scratch: std.mem.Allocator, var current = try scratch.dupe(u8, resolved_run); while (true) { - const candidate = try std.fs.path.join(scratch, &.{ current, "drift.lock" }); + const candidate = try std.Io.Dir.path.join(scratch, &.{ current, "drift.lock" }); if (fileExists(io, candidate)) { return try readAtPath(io, run, scratch, current, candidate, true); } // Stop at VCS root — don't climb out of the current repository. - const has_git = fileExists(io, try std.fs.path.join(scratch, &.{ current, ".git" })); - const has_jj = fileExists(io, try std.fs.path.join(scratch, &.{ current, ".jj" })); + const has_git = fileExists(io, try std.Io.Dir.path.join(scratch, &.{ current, ".git" })); + const has_jj = fileExists(io, try std.Io.Dir.path.join(scratch, &.{ current, ".jj" })); if (has_git or has_jj) { return .{ .root_path = try run.dupe(u8, current), - .lockfile_path = try std.fs.path.join(run, &.{ current, "drift.lock" }), + .lockfile_path = try std.Io.Dir.path.join(run, &.{ current, "drift.lock" }), .exists = false, .bindings = .empty, }; @@ -94,7 +94,7 @@ pub fn discover(io: std.Io, run: std.mem.Allocator, scratch: std.mem.Allocator, const parent = parentPath(current) orelse { return .{ .root_path = try run.dupe(u8, resolved_run), - .lockfile_path = try std.fs.path.join(run, &.{ resolved_run, "drift.lock" }), + .lockfile_path = try std.Io.Dir.path.join(run, &.{ resolved_run, "drift.lock" }), .exists = false, .bindings = .empty, }; @@ -238,7 +238,7 @@ pub fn writeFile(io: std.Io, lockfile: *const Lockfile, scratch: std.mem.Allocat } fn parseLine(allocator: std.mem.Allocator, line: []const u8) !Binding { - const arrow = std.mem.indexOf(u8, line, " -> ") orelse return error.InvalidBindingLine; + const arrow = std.mem.find(u8, line, " -> ") orelse return error.InvalidBindingLine; const doc_path = std.mem.trim(u8, line[0..arrow], " \t"); const rest = std.mem.trim(u8, line[arrow + " -> ".len ..], " \t"); if (doc_path.len == 0 or rest.len == 0) return error.InvalidBindingLine; @@ -250,7 +250,7 @@ fn parseLine(allocator: std.mem.Allocator, line: []const u8) !Binding { errdefer metadata.deinit(allocator); while (tokens.next()) |token| { - const colon = std.mem.indexOfScalar(u8, token, ':') orelse return error.InvalidMetadataField; + const colon = std.mem.findScalar(u8, token, ':') orelse return error.InvalidMetadataField; if (colon == 0 or colon == token.len - 1) return error.InvalidMetadataField; try metadata.append(allocator, .{ .key = try allocator.dupe(u8, token[0..colon]), @@ -266,7 +266,7 @@ fn parseLine(allocator: std.mem.Allocator, line: []const u8) !Binding { } fn fileExists(io: std.Io, path: []const u8) bool { - if (std.fs.path.isAbsolute(path)) { + if (std.Io.Dir.path.isAbsolute(path)) { std.Io.Dir.accessAbsolute(io, path, .{}) catch return false; } else { std.Io.Dir.cwd().access(io, path, .{}) catch return false; @@ -276,7 +276,7 @@ fn fileExists(io: std.Io, path: []const u8) bool { /// Read a file at `path` (absolute or cwd-relative) fully into memory via `allocator`. fn readFileAt(io: std.Io, allocator: std.mem.Allocator, path: []const u8, max_bytes: usize) ![]u8 { - const file = if (std.fs.path.isAbsolute(path)) + const file = if (std.Io.Dir.path.isAbsolute(path)) try std.Io.Dir.openFileAbsolute(io, path, .{}) else try std.Io.Dir.cwd().openFile(io, path, .{}); @@ -287,7 +287,7 @@ fn readFileAt(io: std.Io, allocator: std.mem.Allocator, path: []const u8, max_by fn parentPath(path: []const u8) ?[]const u8 { if (std.mem.eql(u8, path, "/")) return null; - const parent = std.fs.path.dirname(path) orelse return null; + const parent = std.Io.Dir.path.dirname(path) orelse return null; if (parent.len == 0) return "/"; if (std.mem.eql(u8, parent, path)) return null; return parent; diff --git a/src/symbols.zig b/src/symbols.zig index 87513ba..0ba14db 100644 --- a/src/symbols.zig +++ b/src/symbols.zig @@ -178,7 +178,7 @@ pub fn resolveSymbolWithTreeSitter(source: []const u8, lang_query: LanguageQuery /// Compute a fingerprint for a file or symbol, dispatching to the appropriate /// tree-sitter language when available and falling back to raw XxHash3. pub fn computeFingerprint(content: []const u8, file_path: []const u8, symbol_name: ?[]const u8) ?u64 { - const ext = std.fs.path.extension(file_path); + const ext = std.Io.Dir.path.extension(file_path); if (symbol_name) |sym| { if (std.mem.eql(u8, ext, ".md")) { return markdown.fingerprintHeadingSection(content, sym); diff --git a/src/target.zig b/src/target.zig index 8f41e9e..3e2cfd6 100644 --- a/src/target.zig +++ b/src/target.zig @@ -6,7 +6,7 @@ pub const ParsedTarget = struct { symbol_name: ?[]const u8, pub fn isHeading(self: ParsedTarget) bool { - return self.symbol_name != null and std.mem.eql(u8, std.fs.path.extension(self.file_path), ".md"); + return self.symbol_name != null and std.mem.eql(u8, std.Io.Dir.path.extension(self.file_path), ".md"); } pub fn kind(self: ParsedTarget) []const u8 { @@ -17,7 +17,7 @@ pub const ParsedTarget = struct { }; pub fn parse(raw_target: []const u8) ParsedTarget { - const hash_pos = std.mem.indexOfScalar(u8, raw_target, '#'); + const hash_pos = std.mem.findScalar(u8, raw_target, '#'); return .{ .identity = raw_target, .file_path = if (hash_pos) |pos| raw_target[0..pos] else raw_target, diff --git a/src/vcs.zig b/src/vcs.zig index c9a4dbf..69f73cd 100644 --- a/src/vcs.zig +++ b/src/vcs.zig @@ -408,7 +408,7 @@ pub const GitCatFile = struct { return null; } - const last_space = std.mem.lastIndexOfScalar(u8, header, ' ') orelse return null; + const last_space = std.mem.findScalarLast(u8, header, ' ') orelse return null; const size = std.fmt.parseInt(usize, header[last_space + 1 ..], 10) catch return null; const content = try allocator.alloc(u8, size); diff --git a/test/helpers.zig b/test/helpers.zig index 2fb219b..f36a2b8 100644 --- a/test/helpers.zig +++ b/test/helpers.zig @@ -61,7 +61,7 @@ pub const TempRepo = struct { /// Write a file at the given relative path, creating parent directories as needed. pub fn writeFile(self: *TempRepo, path: []const u8, content: []const u8) !void { - if (std.fs.path.dirname(path)) |parent| { + if (std.Io.Dir.path.dirname(path)) |parent| { try self.tmp.dir.createDirPath(self.io, parent); } try self.tmp.dir.writeFile(self.io, .{ @@ -130,7 +130,7 @@ pub const TempRepo = struct { } const argv = argv_buf[0 .. args.len + 1]; - const sub_path = try std.fs.path.join(self.allocator, &.{ self.abs_path, subdir }); + const sub_path = try std.Io.Dir.path.join(self.allocator, &.{ self.abs_path, subdir }); defer self.allocator.free(sub_path); return runProcess(self.allocator, self.io, argv, sub_path); @@ -197,7 +197,7 @@ pub fn expectExitCode(term: std.process.Child.Term, expected: u8) !void { /// Assert that `haystack` contains `needle`. pub fn expectContains(haystack: []const u8, needle: []const u8) !void { - if (std.mem.indexOf(u8, haystack, needle) == null) { + if (std.mem.find(u8, haystack, needle) == null) { std.debug.print("\n--- Expected to find ---\n{s}\n--- in output ---\n{s}\n--- end ---\n", .{ needle, haystack }); return error.TestUnexpectedResult; } @@ -205,7 +205,7 @@ pub fn expectContains(haystack: []const u8, needle: []const u8) !void { /// Assert that `haystack` does NOT contain `needle`. pub fn expectNotContains(haystack: []const u8, needle: []const u8) !void { - if (std.mem.indexOf(u8, haystack, needle) != null) { + if (std.mem.find(u8, haystack, needle) != null) { std.debug.print("\n--- Expected NOT to find ---\n{s}\n--- in output ---\n{s}\n--- end ---\n", .{ needle, haystack }); return error.TestUnexpectedResult; } diff --git a/test/integration/lint_test.zig b/test/integration/lint_test.zig index 73c7cd9..03e0c8b 100644 --- a/test/integration/lint_test.zig +++ b/test/integration/lint_test.zig @@ -383,7 +383,7 @@ test "check --format json reports stale anchors with blame" { const blame = parsed.value.docs[0].anchors[0].blame orelse return error.MissingBlame; try std.testing.expect(blame.author.len > 0); try std.testing.expect(blame.commit.len >= 40); - try std.testing.expect(std.mem.indexOfScalar(u8, blame.date, 'T') != null); + try std.testing.expect(std.mem.findScalar(u8, blame.date, 'T') != null); try helpers.expectContains(blame.subject, "refactor: tweak main return value"); } From 15157337bbc086a81891d3e79ee0bb4425bb8c75 Mon Sep 17 00:00:00 2001 From: Laurynas Keturakis Date: Fri, 17 Apr 2026 11:51:00 +0200 Subject: [PATCH 4/6] chore: update CI, docs, README to zig 0.16 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- CLAUDE.md | 2 +- README.md | 4 ++-- docs/RELEASING.md | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88d8e23..d5964d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Build and test run: zig build test -Doptimize=ReleaseSafe @@ -69,7 +69,7 @@ jobs: - name: Set up Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Build run: zig build -Doptimize=ReleaseSafe -Dtarget=${{ matrix.zig-target }} ${{ matrix.zig-cpu && format('-Dcpu={0}', matrix.zig-cpu) || '' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 507f866..2a47532 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,7 +76,7 @@ jobs: - name: Set up Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Build run: zig build -Doptimize=ReleaseSafe -Dversion=${{ github.ref_name }} -Dtarget=${{ matrix.zig-target }} ${{ matrix.zig-cpu && format('-Dcpu={0}', matrix.zig-cpu) || '' }} diff --git a/CLAUDE.md b/CLAUDE.md index 6905e67..085628a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ ## Stack -- Language: Zig 0.15.2 +- Language: Zig 0.16.0 - C interop: tree-sitter (vendor/tree-sitter + vendor/zig-tree-sitter, parsed on demand) - Grammars: lazy zig build deps (not vendored) - CLI: zig-clap 0.11.0 diff --git a/README.md b/README.md index f2c4e25..a267593 100644 --- a/README.md +++ b/README.md @@ -158,10 +158,10 @@ For faster CI runs, use `--changed` to scope checking to docs affected by the fi ## Development -Requires Zig 0.15.2. The repo includes a `.tool-versions` file for [mise](https://mise.jdx.dev/) (or asdf). If you haven't already, [activate mise](https://mise.jdx.dev/getting-started.html#activate-mise) in your shell, then: +Requires Zig 0.16.0. The repo includes a `.mise.toml` file for [mise](https://mise.jdx.dev/). If you haven't already, [activate mise](https://mise.jdx.dev/getting-started.html#activate-mise) in your shell, then: ```bash -mise install # installs zig 0.15.2 +mise install # installs zig 0.16.0 zig build test # run tests zig build -Doptimize=ReleaseSafe # build release binary ``` diff --git a/docs/RELEASING.md b/docs/RELEASING.md index d4493b1..155b6a4 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -7,7 +7,7 @@ Source of truth: `.github/workflows/release.yml`, `.github/workflows/ci.yml`, `c Every push to `main` and every pull request runs the CI workflow (`.github/workflows/ci.yml`). -The **lint** job: install Zig 0.15.2, build the project, run the full test suite (`zig build test -Doptimize=ReleaseSafe`), regenerate `docs/schemas/drift.check.v1.json` from the payload types and fail if that file differs from what is committed (`zig build gen-check-schema` plus `git diff --exit-code`), then run `./zig-out/bin/drift lint` so the repo’s own drift docs stay current. If any step fails, the job fails. +The **lint** job: install Zig 0.16.0, build the project, run the full test suite (`zig build test -Doptimize=ReleaseSafe`), regenerate `docs/schemas/drift.check.v1.json` from the payload types and fail if that file differs from what is committed (`zig build gen-check-schema` plus `git diff --exit-code`), then run `./zig-out/bin/drift lint` so the repo’s own drift docs stay current. If any step fails, the job fails. The **build** job runs after **lint** and cross-compiles release binaries for all four targets (aarch64-macos, x86_64-macos, x86_64-linux, aarch64-linux), packaging each as a tarball artifact. @@ -38,7 +38,7 @@ Types `chore`, `style`, and `ci` are excluded from changelogs. Merge commits are ``` 3. The tag push triggers `.github/workflows/release.yml`, which first verifies the tag points to a commit on `main` (tags on feature branches are rejected), then: - Generates release notes with git-cliff (grouped by Features, Bug Fixes, Documentation, Refactor) - - Cross-compiles for all 4 targets with Zig 0.15.2 + - Cross-compiles for all 4 targets with Zig 0.16.0 - Creates a GitHub release with the generated notes, all tarballs, and matching `.sha256` checksum files attached - Optionally dispatches `fiberplane/homebrew-tap` to open or refresh the Homebrew formula PR for that tag From 2efae33d9e291e26745409246d0e8adbf09406f9 Mon Sep 17 00:00:00 2001 From: Laurynas Keturakis Date: Fri, 17 Apr 2026 12:04:31 +0200 Subject: [PATCH 5/6] chore: fix tools/gen-check-schema for zig 0.16 --- src/payload/drift_check_schema_gen.zig | 446 ++++++++++++------------- tools/gen_drift_check_schema.zig | 26 +- 2 files changed, 236 insertions(+), 236 deletions(-) diff --git a/src/payload/drift_check_schema_gen.zig b/src/payload/drift_check_schema_gen.zig index 38543af..9380c70 100644 --- a/src/payload/drift_check_schema_gen.zig +++ b/src/payload/drift_check_schema_gen.zig @@ -16,49 +16,49 @@ pub fn writeJsonSchema(allocator: std.mem.Allocator, writer: anytype) !void { } fn buildDocument(a: std.mem.Allocator) !json.Value { - var root = json.ObjectMap.init(a); + var root = json.ObjectMap.empty; - try root.put("$schema", .{ .string = "https://json-schema.org/draft/2020-12/schema" }); - try root.put("$id", .{ .string = "drift.check.v1.json" }); - try root.put("title", .{ .string = "drift check / lint JSON output" }); - try root.put("description", .{ .string = + try root.put(a, "$schema", .{ .string = "https://json-schema.org/draft/2020-12/schema" }); + try root.put(a, "$id", .{ .string = "drift.check.v1.json" }); + try root.put(a, "title", .{ .string = "drift check / lint JSON output" }); + try root.put(a, "description", .{ .string = \\Wire format emitted by `drift check --format json` and `drift lint --format json`. Match top-level `schema_version` to "drift.check.v1". Unknown properties may appear in future drift versions; consumers should ignore them. See docs/check-json-schema.md. }); - try root.put("type", .{ .string = "object" }); - try root.put("additionalProperties", .{ .bool = true }); - try root.put("required", try requiredNamesArray(a, P.DriftCheckV1)); + try root.put(a, "type", .{ .string = "object" }); + try root.put(a, "additionalProperties", .{ .bool = true }); + try root.put(a, "required", try requiredNamesArray(a, P.DriftCheckV1)); - var props = json.ObjectMap.init(a); - try props.put("schema_version", try stringConst(a, "drift.check.v1")); - try props.put("tool", try stringConst(a, "drift")); + var props = json.ObjectMap.empty; + try props.put(a, "schema_version", try stringConst(a, "drift.check.v1")); + try props.put(a, "tool", try stringConst(a, "drift")); try putStringDesc(a, &props, "tool_version", "Drift binary version string."); - try props.put("repo", try nullableStringDesc(a, + try props.put(a, "repo", try nullableStringDesc(a, \\Repository identity when detectable (e.g. github:owner/name), else null. )); try putIntegerDesc(a, &props, "checked_at_ms", \\Wall-clock time of the run, milliseconds since Unix epoch. ); - try props.put("summary", try refObj(a, "#/$defs/summary")); - - var specs_map = json.ObjectMap.init(a); - try specs_map.put("type", .{ .string = "array" }); - try specs_map.put("description", .{ .string = "Discovered docs in scanner order." }); - var item_ref = json.ObjectMap.init(a); - try item_ref.put("$ref", .{ .string = "#/$defs/doc" }); - try specs_map.put("items", .{ .object = item_ref }); - try props.put("docs", .{ .object = specs_map }); - - try root.put("properties", .{ .object = props }); - - var defs = json.ObjectMap.init(a); - try defs.put("summary", .{ .object = try defSummary(a) }); - try defs.put("doc", .{ .object = try defDoc(a) }); - try defs.put("anchor", .{ .object = try defAnchor(a) }); - try defs.put("link", .{ .object = try defLink(a) }); - try defs.put("provenance", .{ .object = try defProvenance(a) }); - try defs.put("reason", .{ .object = try defReason(a) }); - try defs.put("blame", .{ .object = try defBlame(a) }); - try root.put("$defs", .{ .object = defs }); + try props.put(a, "summary", try refObj(a, "#/$defs/summary")); + + var specs_map = json.ObjectMap.empty; + try specs_map.put(a, "type", .{ .string = "array" }); + try specs_map.put(a, "description", .{ .string = "Discovered docs in scanner order." }); + var item_ref = json.ObjectMap.empty; + try item_ref.put(a, "$ref", .{ .string = "#/$defs/doc" }); + try specs_map.put(a, "items", .{ .object = item_ref }); + try props.put(a, "docs", .{ .object = specs_map }); + + try root.put(a, "properties", .{ .object = props }); + + var defs = json.ObjectMap.empty; + try defs.put(a, "summary", .{ .object = try defSummary(a) }); + try defs.put(a, "doc", .{ .object = try defDoc(a) }); + try defs.put(a, "anchor", .{ .object = try defAnchor(a) }); + try defs.put(a, "link", .{ .object = try defLink(a) }); + try defs.put(a, "provenance", .{ .object = try defProvenance(a) }); + try defs.put(a, "reason", .{ .object = try defReason(a) }); + try defs.put(a, "blame", .{ .object = try defBlame(a) }); + try root.put(a, "$defs", .{ .object = defs }); return .{ .object = root }; } @@ -81,276 +81,276 @@ fn stringArray(a: std.mem.Allocator, names: []const []const u8) !json.Value { } fn refObj(a: std.mem.Allocator, path: []const u8) !json.Value { - var m = json.ObjectMap.init(a); - try m.put("$ref", .{ .string = path }); + var m = json.ObjectMap.empty; + try m.put(a, "$ref", .{ .string = path }); return .{ .object = m }; } fn stringConst(a: std.mem.Allocator, comptime c: []const u8) !json.Value { - var m = json.ObjectMap.init(a); - try m.put("type", .{ .string = "string" }); - try m.put("const", .{ .string = c }); + var m = json.ObjectMap.empty; + try m.put(a, "type", .{ .string = "string" }); + try m.put(a, "const", .{ .string = c }); return .{ .object = m }; } fn nullableStringDesc(a: std.mem.Allocator, comptime desc: []const u8) !json.Value { - var m = json.ObjectMap.init(a); - try m.put("description", .{ .string = desc }); + var m = json.ObjectMap.empty; + try m.put(a, "description", .{ .string = desc }); var types = json.Array.init(a); try types.append(.{ .string = "string" }); try types.append(.{ .string = "null" }); - try m.put("type", .{ .array = types }); + try m.put(a, "type", .{ .array = types }); return .{ .object = m }; } fn putStringDesc(a: std.mem.Allocator, props: *json.ObjectMap, key: []const u8, comptime desc: []const u8) !void { - var m = json.ObjectMap.init(a); - try m.put("type", .{ .string = "string" }); - try m.put("description", .{ .string = desc }); - try props.put(key, .{ .object = m }); + var m = json.ObjectMap.empty; + try m.put(a, "type", .{ .string = "string" }); + try m.put(a, "description", .{ .string = desc }); + try props.put(a, key, .{ .object = m }); } fn putIntegerDesc(a: std.mem.Allocator, props: *json.ObjectMap, key: []const u8, comptime desc: []const u8) !void { - var m = json.ObjectMap.init(a); - try m.put("type", .{ .string = "integer" }); - try m.put("description", .{ .string = desc }); - try props.put(key, .{ .object = m }); + var m = json.ObjectMap.empty; + try m.put(a, "type", .{ .string = "integer" }); + try m.put(a, "description", .{ .string = desc }); + try props.put(a, key, .{ .object = m }); } fn uintSchema(a: std.mem.Allocator) !json.ObjectMap { - var m = json.ObjectMap.init(a); - try m.put("type", .{ .string = "integer" }); - try m.put("minimum", .{ .integer = 0 }); + var m = json.ObjectMap.empty; + try m.put(a, "type", .{ .string = "integer" }); + try m.put(a, "minimum", .{ .integer = 0 }); return m; } fn defSummary(a: std.mem.Allocator) !json.ObjectMap { - var o = json.ObjectMap.init(a); - try o.put("type", .{ .string = "object" }); - try o.put("additionalProperties", .{ .bool = true }); - try o.put("required", try requiredNamesArray(a, P.Summary)); - var props = json.ObjectMap.init(a); - - var result = json.ObjectMap.init(a); - try result.put("type", .{ .string = "string" }); - try result.put("enum", try stringArray(a, &.{ "pass", "fail" })); - try result.put("description", .{ .string = + var o = json.ObjectMap.empty; + try o.put(a, "type", .{ .string = "object" }); + try o.put(a, "additionalProperties", .{ .bool = true }); + try o.put(a, "required", try requiredNamesArray(a, P.Summary)); + var props = json.ObjectMap.empty; + + var result = json.ObjectMap.empty; + try result.put(a, "type", .{ .string = "string" }); + try result.put(a, "enum", try stringArray(a, &.{ "pass", "fail" })); + try result.put(a, "description", .{ .string = \\fail iff any anchor is stale or any link is broken; mirrors process exit code (0 pass, 1 fail). }); - try props.put("result", .{ .object = result }); + try props.put(a, "result", .{ .object = result }); - var vs = json.ObjectMap.init(a); - try vs.put("type", .{ .string = "string" }); - try vs.put("enum", try stringArray(a, &.{ "none", "partial", "full" })); - try vs.put("description", .{ .string = + var vs = json.ObjectMap.empty; + try vs.put(a, "type", .{ .string = "string" }); + try vs.put(a, "enum", try stringArray(a, &.{ "none", "partial", "full" })); + try vs.put(a, "description", .{ .string = \\Coverage of verification: none = all docs skipped; partial = mix; full = nothing skipped (including zero docs). }); - try props.put("verification_state", .{ .object = vs }); - - try props.put("docs_total", .{ .object = try uintSchema(a) }); - var sc = json.ObjectMap.init(a); - try sc.put("type", .{ .string = "integer" }); - try sc.put("minimum", .{ .integer = 0 }); - try sc.put("description", .{ .string = "docs_fresh + docs_stale (docs not skipped)." }); - try props.put("docs_checked", .{ .object = sc }); - try props.put("docs_skipped", .{ .object = try uintSchema(a) }); - try props.put("docs_fresh", .{ .object = try uintSchema(a) }); - try props.put("docs_stale", .{ .object = try uintSchema(a) }); - try props.put("anchors_total", .{ .object = try uintSchema(a) }); - try props.put("anchors_fresh", .{ .object = try uintSchema(a) }); - try props.put("anchors_stale", .{ .object = try uintSchema(a) }); - try props.put("anchors_skipped", .{ .object = try uintSchema(a) }); - try props.put("links_total", .{ .object = try uintSchema(a) }); - try props.put("links_broken", .{ .object = try uintSchema(a) }); - - try o.put("properties", .{ .object = props }); + try props.put(a, "verification_state", .{ .object = vs }); + + try props.put(a, "docs_total", .{ .object = try uintSchema(a) }); + var sc = json.ObjectMap.empty; + try sc.put(a, "type", .{ .string = "integer" }); + try sc.put(a, "minimum", .{ .integer = 0 }); + try sc.put(a, "description", .{ .string = "docs_fresh + docs_stale (docs not skipped)." }); + try props.put(a, "docs_checked", .{ .object = sc }); + try props.put(a, "docs_skipped", .{ .object = try uintSchema(a) }); + try props.put(a, "docs_fresh", .{ .object = try uintSchema(a) }); + try props.put(a, "docs_stale", .{ .object = try uintSchema(a) }); + try props.put(a, "anchors_total", .{ .object = try uintSchema(a) }); + try props.put(a, "anchors_fresh", .{ .object = try uintSchema(a) }); + try props.put(a, "anchors_stale", .{ .object = try uintSchema(a) }); + try props.put(a, "anchors_skipped", .{ .object = try uintSchema(a) }); + try props.put(a, "links_total", .{ .object = try uintSchema(a) }); + try props.put(a, "links_broken", .{ .object = try uintSchema(a) }); + + try o.put(a, "properties", .{ .object = props }); return o; } fn defDoc(a: std.mem.Allocator) !json.ObjectMap { - var o = json.ObjectMap.init(a); - try o.put("type", .{ .string = "object" }); - try o.put("additionalProperties", .{ .bool = true }); - try o.put("required", try requiredNamesArray(a, P.Doc)); - var props = json.ObjectMap.init(a); - try props.put("path", .{ .object = try stringType(a) }); - try props.put("origin", try nullableStringDesc(a, + var o = json.ObjectMap.empty; + try o.put(a, "type", .{ .string = "object" }); + try o.put(a, "additionalProperties", .{ .bool = true }); + try o.put(a, "required", try requiredNamesArray(a, P.Doc)); + var props = json.ObjectMap.empty; + try props.put(a, "path", .{ .object = try stringType(a) }); + try props.put(a, "origin", try nullableStringDesc(a, \\Common origin qualifier for the doc's bindings when present, else null. )); - var res = json.ObjectMap.init(a); - try res.put("type", .{ .string = "string" }); - try res.put("enum", try stringArray(a, &.{ "fresh", "stale", "skip", "broken" })); - try res.put("description", .{ .string = "Worst of child anchors and links; broken > stale > skip > fresh." }); - try props.put("result", .{ .object = res }); - var anchors = json.ObjectMap.init(a); - try anchors.put("type", .{ .string = "array" }); - var ar = json.ObjectMap.init(a); - try ar.put("$ref", .{ .string = "#/$defs/anchor" }); - try anchors.put("items", .{ .object = ar }); - try props.put("anchors", .{ .object = anchors }); - var links = json.ObjectMap.init(a); - try links.put("type", .{ .string = "array" }); - var lr = json.ObjectMap.init(a); - try lr.put("$ref", .{ .string = "#/$defs/link" }); - try links.put("items", .{ .object = lr }); - try props.put("links", .{ .object = links }); - try o.put("properties", .{ .object = props }); + var res = json.ObjectMap.empty; + try res.put(a, "type", .{ .string = "string" }); + try res.put(a, "enum", try stringArray(a, &.{ "fresh", "stale", "skip", "broken" })); + try res.put(a, "description", .{ .string = "Worst of child anchors and links; broken > stale > skip > fresh." }); + try props.put(a, "result", .{ .object = res }); + var anchors = json.ObjectMap.empty; + try anchors.put(a, "type", .{ .string = "array" }); + var ar = json.ObjectMap.empty; + try ar.put(a, "$ref", .{ .string = "#/$defs/anchor" }); + try anchors.put(a, "items", .{ .object = ar }); + try props.put(a, "anchors", .{ .object = anchors }); + var links = json.ObjectMap.empty; + try links.put(a, "type", .{ .string = "array" }); + var lr = json.ObjectMap.empty; + try lr.put(a, "$ref", .{ .string = "#/$defs/link" }); + try links.put(a, "items", .{ .object = lr }); + try props.put(a, "links", .{ .object = links }); + try o.put(a, "properties", .{ .object = props }); return o; } fn stringType(a: std.mem.Allocator) !json.ObjectMap { - var m = json.ObjectMap.init(a); - try m.put("type", .{ .string = "string" }); + var m = json.ObjectMap.empty; + try m.put(a, "type", .{ .string = "string" }); return m; } fn defAnchor(a: std.mem.Allocator) !json.ObjectMap { - var o = json.ObjectMap.init(a); - try o.put("type", .{ .string = "object" }); - try o.put("additionalProperties", .{ .bool = true }); - try o.put("required", try requiredNamesArray(a, P.Anchor)); - var props = json.ObjectMap.init(a); - - var id = json.ObjectMap.init(a); - try id.put("type", .{ .string = "string" }); - try id.put("description", .{ .string = "Anchor without @provenance suffix." }); - try props.put("identity", .{ .object = id }); - - var raw = json.ObjectMap.init(a); - try raw.put("type", .{ .string = "string" }); - try raw.put("description", .{ .string = "Full anchor string from the doc." }); - try props.put("raw", .{ .object = raw }); - - var kind = json.ObjectMap.init(a); - try kind.put("type", .{ .string = "string" }); - try kind.put("enum", try stringArray(a, &.{ "file", "symbol", "heading" })); - try props.put("kind", .{ .object = kind }); - - try props.put("path", .{ .object = try stringType(a) }); - - var sym = json.ObjectMap.init(a); - try sym.put("description", .{ .string = "Symbol segment when kind is symbol." }); + var o = json.ObjectMap.empty; + try o.put(a, "type", .{ .string = "object" }); + try o.put(a, "additionalProperties", .{ .bool = true }); + try o.put(a, "required", try requiredNamesArray(a, P.Anchor)); + var props = json.ObjectMap.empty; + + var id = json.ObjectMap.empty; + try id.put(a, "type", .{ .string = "string" }); + try id.put(a, "description", .{ .string = "Anchor without @provenance suffix." }); + try props.put(a, "identity", .{ .object = id }); + + var raw = json.ObjectMap.empty; + try raw.put(a, "type", .{ .string = "string" }); + try raw.put(a, "description", .{ .string = "Full anchor string from the doc." }); + try props.put(a, "raw", .{ .object = raw }); + + var kind = json.ObjectMap.empty; + try kind.put(a, "type", .{ .string = "string" }); + try kind.put(a, "enum", try stringArray(a, &.{ "file", "symbol", "heading" })); + try props.put(a, "kind", .{ .object = kind }); + + try props.put(a, "path", .{ .object = try stringType(a) }); + + var sym = json.ObjectMap.empty; + try sym.put(a, "description", .{ .string = "Symbol segment when kind is symbol." }); var st = json.Array.init(a); try st.append(.{ .string = "string" }); try st.append(.{ .string = "null" }); - try sym.put("type", .{ .array = st }); - try props.put("symbol", .{ .object = sym }); + try sym.put(a, "type", .{ .array = st }); + try props.put(a, "symbol", .{ .object = sym }); - var prov = json.ObjectMap.init(a); - try prov.put("description", .{ .string = "null when anchor has no provenance suffix." }); + var prov = json.ObjectMap.empty; + try prov.put(a, "description", .{ .string = "null when anchor has no provenance suffix." }); var ptypes = json.Array.init(a); try ptypes.append(.{ .string = "null" }); - var pref = json.ObjectMap.init(a); - try pref.put("$ref", .{ .string = "#/$defs/provenance" }); + var pref = json.ObjectMap.empty; + try pref.put(a, "$ref", .{ .string = "#/$defs/provenance" }); try ptypes.append(.{ .object = pref }); - try prov.put("type", .{ .array = ptypes }); - try props.put("provenance", .{ .object = prov }); + try prov.put(a, "type", .{ .array = ptypes }); + try props.put(a, "provenance", .{ .object = prov }); - var ares = json.ObjectMap.init(a); - try ares.put("type", .{ .string = "string" }); - try ares.put("enum", try stringArray(a, &.{ "fresh", "stale", "skip" })); - try props.put("result", .{ .object = ares }); + var ares = json.ObjectMap.empty; + try ares.put(a, "type", .{ .string = "string" }); + try ares.put(a, "enum", try stringArray(a, &.{ "fresh", "stale", "skip" })); + try props.put(a, "result", .{ .object = ares }); - var reason = json.ObjectMap.init(a); - try reason.put("description", .{ .string = "null when anchor is fresh." }); + var reason = json.ObjectMap.empty; + try reason.put(a, "description", .{ .string = "null when anchor is fresh." }); var rtypes = json.Array.init(a); try rtypes.append(.{ .string = "null" }); - var rref = json.ObjectMap.init(a); - try rref.put("$ref", .{ .string = "#/$defs/reason" }); + var rref = json.ObjectMap.empty; + try rref.put(a, "$ref", .{ .string = "#/$defs/reason" }); try rtypes.append(.{ .object = rref }); - try reason.put("type", .{ .array = rtypes }); - try props.put("reason", .{ .object = reason }); + try reason.put(a, "type", .{ .array = rtypes }); + try props.put(a, "reason", .{ .object = reason }); - var blame = json.ObjectMap.init(a); - try blame.put("description", .{ .string = "null when not applicable or unavailable." }); + var blame = json.ObjectMap.empty; + try blame.put(a, "description", .{ .string = "null when not applicable or unavailable." }); var btypes = json.Array.init(a); try btypes.append(.{ .string = "null" }); - var bref = json.ObjectMap.init(a); - try bref.put("$ref", .{ .string = "#/$defs/blame" }); + var bref = json.ObjectMap.empty; + try bref.put(a, "$ref", .{ .string = "#/$defs/blame" }); try btypes.append(.{ .object = bref }); - try blame.put("type", .{ .array = btypes }); - try props.put("blame", .{ .object = blame }); + try blame.put(a, "type", .{ .array = btypes }); + try props.put(a, "blame", .{ .object = blame }); - try o.put("properties", .{ .object = props }); + try o.put(a, "properties", .{ .object = props }); return o; } fn defLink(a: std.mem.Allocator) !json.ObjectMap { - var o = json.ObjectMap.init(a); - try o.put("type", .{ .string = "object" }); - try o.put("additionalProperties", .{ .bool = true }); - try o.put("required", try requiredNamesArray(a, P.Link)); - var props = json.ObjectMap.init(a); - try props.put("target", .{ .object = try stringType(a) }); - try props.put("line", .{ .object = try uintSchema(a) }); - var result = json.ObjectMap.init(a); - try result.put("type", .{ .string = "string" }); - try result.put("enum", try stringArray(a, &.{ "ok", "broken" })); - try props.put("result", .{ .object = result }); - - var reason = json.ObjectMap.init(a); - try reason.put("description", .{ .string = "null when link result is ok." }); + var o = json.ObjectMap.empty; + try o.put(a, "type", .{ .string = "object" }); + try o.put(a, "additionalProperties", .{ .bool = true }); + try o.put(a, "required", try requiredNamesArray(a, P.Link)); + var props = json.ObjectMap.empty; + try props.put(a, "target", .{ .object = try stringType(a) }); + try props.put(a, "line", .{ .object = try uintSchema(a) }); + var result = json.ObjectMap.empty; + try result.put(a, "type", .{ .string = "string" }); + try result.put(a, "enum", try stringArray(a, &.{ "ok", "broken" })); + try props.put(a, "result", .{ .object = result }); + + var reason = json.ObjectMap.empty; + try reason.put(a, "description", .{ .string = "null when link result is ok." }); var rtypes = json.Array.init(a); try rtypes.append(.{ .string = "null" }); - var rref = json.ObjectMap.init(a); - try rref.put("$ref", .{ .string = "#/$defs/reason" }); + var rref = json.ObjectMap.empty; + try rref.put(a, "$ref", .{ .string = "#/$defs/reason" }); try rtypes.append(.{ .object = rref }); - try reason.put("type", .{ .array = rtypes }); - try props.put("reason", .{ .object = reason }); + try reason.put(a, "type", .{ .array = rtypes }); + try props.put(a, "reason", .{ .object = reason }); - try o.put("properties", .{ .object = props }); + try o.put(a, "properties", .{ .object = props }); return o; } fn defProvenance(a: std.mem.Allocator) !json.ObjectMap { - var o = json.ObjectMap.init(a); - try o.put("type", .{ .string = "object" }); - try o.put("additionalProperties", .{ .bool = true }); - try o.put("required", try requiredNamesArray(a, P.Provenance)); - var props = json.ObjectMap.init(a); - var k = json.ObjectMap.init(a); - try k.put("type", .{ .string = "string" }); - try k.put("enum", try stringArray(a, &.{ "sig", "vcs" })); - try props.put("kind", .{ .object = k }); - try props.put("value", .{ .object = try stringType(a) }); - try o.put("properties", .{ .object = props }); + var o = json.ObjectMap.empty; + try o.put(a, "type", .{ .string = "object" }); + try o.put(a, "additionalProperties", .{ .bool = true }); + try o.put(a, "required", try requiredNamesArray(a, P.Provenance)); + var props = json.ObjectMap.empty; + var k = json.ObjectMap.empty; + try k.put(a, "type", .{ .string = "string" }); + try k.put(a, "enum", try stringArray(a, &.{ "sig", "vcs" })); + try props.put(a, "kind", .{ .object = k }); + try props.put(a, "value", .{ .object = try stringType(a) }); + try o.put(a, "properties", .{ .object = props }); return o; } fn defReason(a: std.mem.Allocator) !json.ObjectMap { - var o = json.ObjectMap.init(a); - try o.put("type", .{ .string = "object" }); - try o.put("additionalProperties", .{ .bool = true }); - try o.put("required", try requiredNamesArray(a, P.Reason)); - var props = json.ObjectMap.init(a); - var code = json.ObjectMap.init(a); - try code.put("type", .{ .string = "string" }); - try code.put("description", .{ .string = "Machine-stable reason; new values may be added over time." }); - try props.put("code", .{ .object = code }); - var msg = json.ObjectMap.init(a); - try msg.put("type", .{ .string = "string" }); - try msg.put("description", .{ .string = "Human-readable English; stable for a given code in drift.check.v1." }); - try props.put("message", .{ .object = msg }); - try o.put("properties", .{ .object = props }); + var o = json.ObjectMap.empty; + try o.put(a, "type", .{ .string = "object" }); + try o.put(a, "additionalProperties", .{ .bool = true }); + try o.put(a, "required", try requiredNamesArray(a, P.Reason)); + var props = json.ObjectMap.empty; + var code = json.ObjectMap.empty; + try code.put(a, "type", .{ .string = "string" }); + try code.put(a, "description", .{ .string = "Machine-stable reason; new values may be added over time." }); + try props.put(a, "code", .{ .object = code }); + var msg = json.ObjectMap.empty; + try msg.put(a, "type", .{ .string = "string" }); + try msg.put(a, "description", .{ .string = "Human-readable English; stable for a given code in drift.check.v1." }); + try props.put(a, "message", .{ .object = msg }); + try o.put(a, "properties", .{ .object = props }); return o; } fn defBlame(a: std.mem.Allocator) !json.ObjectMap { - var o = json.ObjectMap.init(a); - try o.put("type", .{ .string = "object" }); - try o.put("additionalProperties", .{ .bool = true }); - try o.put("required", try requiredNamesArray(a, P.Blame)); - var props = json.ObjectMap.init(a); - try props.put("author", .{ .object = try stringType(a) }); - var commit = json.ObjectMap.init(a); - try commit.put("type", .{ .string = "string" }); - try commit.put("description", .{ .string = "Full Git object id (40 hex chars for SHA-1)." }); - try props.put("commit", .{ .object = commit }); - var date = json.ObjectMap.init(a); - try date.put("type", .{ .string = "string" }); - try date.put("description", .{ .string = "Committer date, ISO 8601 strict (git --date=iso-strict)." }); - try props.put("date", .{ .object = date }); - try props.put("subject", .{ .object = try stringType(a) }); - try o.put("properties", .{ .object = props }); + var o = json.ObjectMap.empty; + try o.put(a, "type", .{ .string = "object" }); + try o.put(a, "additionalProperties", .{ .bool = true }); + try o.put(a, "required", try requiredNamesArray(a, P.Blame)); + var props = json.ObjectMap.empty; + try props.put(a, "author", .{ .object = try stringType(a) }); + var commit = json.ObjectMap.empty; + try commit.put(a, "type", .{ .string = "string" }); + try commit.put(a, "description", .{ .string = "Full Git object id (40 hex chars for SHA-1)." }); + try props.put(a, "commit", .{ .object = commit }); + var date = json.ObjectMap.empty; + try date.put(a, "type", .{ .string = "string" }); + try date.put(a, "description", .{ .string = "Committer date, ISO 8601 strict (git --date=iso-strict)." }); + try props.put(a, "date", .{ .object = date }); + try props.put(a, "subject", .{ .object = try stringType(a) }); + try o.put(a, "properties", .{ .object = props }); return o; } diff --git a/tools/gen_drift_check_schema.zig b/tools/gen_drift_check_schema.zig index 84d1b71..28de33d 100644 --- a/tools/gen_drift_check_schema.zig +++ b/tools/gen_drift_check_schema.zig @@ -1,24 +1,24 @@ const std = @import("std"); const schema_gen = @import("drift_check_schema_gen"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const io = init.io; - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + var iter = try init.minimal.args.iterateAllocator(gpa); + defer iter.deinit(); - if (args.len < 2) { - std.debug.print("usage: {s} \n", .{args[0]}); + _ = iter.next(); // exe + const out_path = iter.next() orelse { + std.debug.print("usage: gen-drift-check-schema \n", .{}); std.process.exit(2); - } + }; - var file = try std.fs.cwd().createFile(args[1], .{}); - defer file.close(); + var file = try std.Io.Dir.cwd().createFile(io, out_path, .{ .truncate = true }); + defer file.close(io); var buf: [512 * 1024]u8 = undefined; - var w = file.writer(&buf); + var w = file.writer(io, &buf); defer w.interface.flush() catch {}; - try schema_gen.writeJsonSchema(allocator, &w.interface); + try schema_gen.writeJsonSchema(gpa, &w.interface); } From 3f6df579722af1a459098b31de7061d6d2a31e2a Mon Sep 17 00:00:00 2001 From: Laurynas Keturakis Date: Fri, 17 Apr 2026 12:09:24 +0200 Subject: [PATCH 6/6] fix: refresh drift.lock sigs after zig 0.16 port --- drift.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/drift.lock b/drift.lock index c9dee7a..0a71dae 100644 --- a/drift.lock +++ b/drift.lock @@ -1,19 +1,19 @@ -.claude/skills/drift/SKILL.md -> src/main.zig sig:647a31274655a84d origin:github:fiberplane/drift -.claude/skills/drift/SKILL.md -> src/vcs.zig sig:2468937f00d5305a origin:github:fiberplane/drift +.claude/skills/drift/SKILL.md -> src/main.zig sig:f2735440986d2477 origin:github:fiberplane/drift +.claude/skills/drift/SKILL.md -> src/vcs.zig sig:84da70be235ca9d4 origin:github:fiberplane/drift CLAUDE.md -> build.zig sig:7194b38f39dbadba -CLAUDE.md -> src/main.zig sig:647a31274655a84d -docs/CLI.md -> src/commands/link.zig sig:70e52c01fb9022a8 -docs/CLI.md -> src/commands/lint.zig sig:0073bbe22ef7c5fa -docs/CLI.md -> src/commands/refs.zig sig:e3309a0d11c02bb0 -docs/CLI.md -> src/commands/status.zig sig:ab9cee37b4b22644 -docs/CLI.md -> src/commands/unlink.zig sig:6fc59e2a25f80fac -docs/DESIGN.md -> src/context.zig sig:70678dcc0872470d -docs/DESIGN.md -> src/lockfile.zig sig:23bc7256cff13942 -docs/DESIGN.md -> src/main.zig sig:647a31274655a84d -docs/DESIGN.md -> src/symbols.zig sig:8e4a403c6f0130c3 -docs/DESIGN.md -> src/vcs.zig sig:2468937f00d5305a -docs/RELEASING.md -> .github/workflows/ci.yml sig:e8440b1d7ee3e4ba -docs/RELEASING.md -> .github/workflows/release.yml sig:f74d66b6bd7f959c +CLAUDE.md -> src/main.zig sig:f2735440986d2477 +docs/CLI.md -> src/commands/link.zig sig:7bd7f824afc30e0b +docs/CLI.md -> src/commands/lint.zig sig:1fd2cb4096c65c64 +docs/CLI.md -> src/commands/refs.zig sig:f623b7774086094e +docs/CLI.md -> src/commands/status.zig sig:eade166d24a20b81 +docs/CLI.md -> src/commands/unlink.zig sig:0dbe1ee3315211b5 +docs/DESIGN.md -> src/context.zig sig:82d9da38ea486f36 +docs/DESIGN.md -> src/lockfile.zig sig:0e4b79237b6e4c8b +docs/DESIGN.md -> src/main.zig sig:f2735440986d2477 +docs/DESIGN.md -> src/symbols.zig sig:bbe250d43609daa1 +docs/DESIGN.md -> src/vcs.zig sig:84da70be235ca9d4 +docs/RELEASING.md -> .github/workflows/ci.yml sig:c14a23e6547d575f +docs/RELEASING.md -> .github/workflows/release.yml sig:19b334776bec1eda docs/RELEASING.md -> cliff.toml sig:d2a8e301fe4b788e docs/check-json-schema.md -> docs/schemas/drift.check.v1.json sig:4e6d23b9945aebb1 -docs/check-json-schema.md -> src/payload/drift_check_v1.zig sig:d446ad53565b6916 +docs/check-json-schema.md -> src/payload/drift_check_v1.zig sig:c80e398e38ece09d