// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Revisions on `zig-git`'s command lines: a `Resolver` set up as git //! would set one up — the current time from `GIT_TEST_DATE_NOW` when it //! is set, the local zone from `TZ`, the index from `GIT_INDEX_FILE` — //! and git's words for what it finds wrong. const std = @import("std"); const Allocator = std.mem.Allocator; const Io = std.Io; const git = @import("git"); const Resolver = git.Resolver; const Diagnostics = git.Diagnostics; const tool = @import("zig_git.zig"); const Context = tool.Context; const object_names = @import("object_names.zig"); /// A resolver for one repository, and what it needs kept alive. pub const Revisions = struct { ctx: Context, zone: *git.date.Zone, resolver: Resolver, /// A resolver for `repo`, which must outlive it, with the settings /// already read, as `command` uses one: `cat-file` checks no path /// under the caller's directory, takes no range and searches no /// message. pub fn init(ctx: Context, repo: *git.Repository, settings: object_names.Settings, command: enum { rev_parse, cat_file }, stderr: *Io.Writer) !Revisions { const gpa = ctx.gpa; const zone = try gpa.create(git.date.Zone); errdefer gpa.destroy(zone); zone.* = git.date.loadZone(ctx.io, gpa, ctx.env.get("TZ"), ctx.env.get("TZDIR")) catch |err| switch (err) { error.UnsupportedTimeZone => return tool.fatalMessage(stderr, "zig-git: TZ='{s}' is read by rules that are not supported", .{ctx.env.get("TZ") orelse ""}), else => |e| return e, }; return .{ .ctx = ctx, .zone = zone, .resolver = .init(gpa, ctx.io, repo, .{ .now = now(ctx), .zone = zone, .hint = settings.hint, .index = .{ .index_file = ctx.env.get("GIT_INDEX_FILE") }, .explain = if (command == .rev_parse) .rev_parse else .cat_file, .sets = command == .rev_parse, .search_refs = command == .rev_parse, }), }; } pub fn deinit(r: *Revisions) void { r.resolver.deinit(); r.zone.deinit(r.ctx.gpa); r.ctx.gpa.destroy(r.zone); } /// Writes what the resolver has said, `times` times over, and forgets /// it. With `quiet`, as `rev-parse -q --verify` is, what git leaves /// out is left out. pub fn flushMessages(r: *Revisions, stderr: *Io.Writer, times: usize, quiet: bool) !void { for (0..times) |t| for (r.resolver.messages.items) |m| { if (quiet and m.hidden_by_quiet) continue; // What git says as it reads the grafts it says once. if (m.level == .raw and t != 0) continue; try m.write(stderr); }; r.resolver.clearMessages(); } /// Writes git's hint about the abbreviation that was ambiguous. pub fn complain(r: *Revisions, names: *object_names.Names, stderr: *Io.Writer) !void { const a = r.resolver.ambiguity orelse return; var diag: Diagnostics = .{}; names.lookup.writeAmbiguity(r.ctx.io, stderr, a.prefix, a.ambiguity, names.defaultLength(), &diag) catch |err| switch (err) { error.WriteFailed => return err, else => return tool.fatalMessage(stderr, "zig-git: cannot describe the objects '{s}' names: {f}", .{ "", tool.errorWords(err) }), }; } }; /// The current time as git takes it: `GIT_TEST_DATE_NOW` when it is set. pub fn now(ctx: Context) i64 { if (ctx.env.get("GIT_TEST_DATE_NOW")) |v| return object_names.leadingNumber(v); return Io.Clock.real.now(ctx.io).toSeconds(); } /// Says what git says of a failure it stops at, or refuses what is not /// supported; returns for the others, which each command words its own /// way. /// `quiet` is `rev-parse -q --verify`'s, and `loud` the resolver's word /// on whether the revision makes git loud anyway. pub fn stopAt(err: anyerror, diag: *const Diagnostics, quiet: bool, loud: bool, stderr: *Io.Writer) !void { switch (Resolver.severity(err)) { .not_found, .diagnosed => return, .fatal => { // git keeps quiet about a short reflog under -q, and not about // a path outside the repository. if (quiet and (err == error.ReflogTooShort or err == error.ReflogEmpty)) return error.Fatal; if (err == error.BadPushDefault) return badPushDefault(diag, stderr); return tool.fatalMessage(stderr, "{s}", .{diag.detail()}); }, .fatal_unless_quiet => { if (quiet and !loud) return error.No; return tool.fatalMessage(stderr, "{s}", .{diag.detail()}); }, .unsupported => return tool.fatalMessage(stderr, "zig-git: {s}, which is not supported", .{diag.detail()}), .failure => return tool.fatalMessage(stderr, "zig-git: cannot resolve the revision: {f}", .{tool.errorWords(err)}), } } fn badPushDefault(diag: *const Diagnostics, stderr: *Io.Writer) error{Fatal} { stderr.print("error: malformed value for push.default: {s}\nerror: must be one of nothing, matching, simple, upstream or current\n", .{diag.detail()}) catch {}; if (diag.path()) |p| return tool.fatalMessage(stderr, "bad config variable 'push.default' in file '{s}' at line {d}", .{ p, diag.line orelse 0 }); return tool.fatalMessage(stderr, "unable to parse 'push.default' from command-line config", .{}); }