Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `zig-git describe`: commits named by the nearest tag they descend//! from, as `git describe` names them, by the library's `describe`.//!//! zig-git describe [--tags] [--all] [--long] [--abbrev=<n>]//! [--candidates=<n> | --exact-match] [--match <pattern>]//! [--exclude <pattern>] [--first-parent] [--always]//! [<commit-ish>...]//!//! `--contains` names commits by what contains them, as `git name-rev`//! does, whose choice among names has not been matched; `--dirty` and//! `--broken` compare the working tree with the index, and describing a//! blob searches every tree. Those are refused by name, as is `--debug`.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const ObjectId = git.ObjectId;const Diagnostics = git.Diagnostics;const tool = @import("zig_git.zig");const Context = tool.Context;const discovery = @import("discovery.zig");const object_names = @import("object_names.zig");const revisions = @import("revisions.zig");const history = @import("history.zig");
/// What is said where git prints its own usage; the exit status is git's.pub const describe_usage = \\usage: zig-git describe [<options>] [<commit-ish>...] \\;
/// The options of `describe`, as given.pub const DescribeOptions = struct { names: git.describe.Options = .{}, long: bool = false, abbrev: ?usize = null, candidates: usize = 10, first_parent: bool = false, always: bool = false, contains: bool = false,};
/// Runs `describe` with `args`, the arguments after its name.pub fn describe(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { const gpa = ctx.gpa; var o: DescribeOptions = .{}; var matches: std.ArrayList([]const u8) = .empty; defer matches.deinit(gpa); var excludes: std.ArrayList([]const u8) = .empty; defer excludes.deinit(gpa); var operands: std.ArrayList([]const u8) = .empty; defer operands.deinit(gpa); var i: usize = 0; var only_operands = false; while (i < args.len) : (i += 1) { const arg = args[i]; if (only_operands or arg.len < 2 or arg[0] != '-') { try operands.append(gpa, arg); continue; } if (std.mem.eql(u8, arg, "--")) { only_operands = true; continue; } const eq = std.mem.findScalar(u8, arg, '='); const name = arg[0 .. eq orelse arg.len]; const value: ?[]const u8 = if (eq) |e| arg[e + 1 ..] else null; const Option = enum { @"--tags", @"--no-tags", @"--all", @"--no-all", @"--long", @"--no-long", @"--abbrev", @"--no-abbrev", @"--candidates", @"--exact-match", @"--match", @"--no-match", @"--exclude", @"--no-exclude", @"--first-parent", @"--always", @"--no-always", @"--contains", @"--dirty", @"--broken", @"--debug", }; const opt = std.meta.stringToEnum(Option, name) orelse { try stderr.print("error: unknown option `{s}'\n", .{arg[2..]}); try stderr.writeAll(describe_usage); return error.UsageQuiet; }; switch (opt) { .@"--tags" => o.names.tags = true, .@"--no-tags" => o.names.tags = false, .@"--all" => o.names.all = true, .@"--no-all" => o.names.all = false, .@"--long" => o.long = true, .@"--no-long" => o.long = false, .@"--abbrev" => o.abbrev = if (value) |v| blk: { const n = object_names.optionNumber(v) orelse { try stderr.writeAll("error: option `abbrev' expects a numerical value\n"); try stderr.writeAll(describe_usage); return error.UsageQuiet; }; break :blk if (n <= 0) 0 else @intCast(@max(4, @min(n, git.object_max_hex_length))); } else null, .@"--no-abbrev" => o.abbrev = 0, .@"--candidates" => { const v = value orelse next: { i += 1; if (i == args.len) return error.Usage; break :next args[i]; }; const n = object_names.optionNumber(v) orelse { try stderr.writeAll("error: option `candidates' expects a numerical value\n"); try stderr.writeAll(describe_usage); return error.UsageQuiet; }; o.candidates = if (n < 0) 0 else @intCast(@min(n, 1000)); }, .@"--exact-match" => o.candidates = 0, .@"--match", .@"--exclude" => { const v = value orelse next: { i += 1; if (i == args.len) { try stderr.print("error: option `{s}' requires a value\n", .{name[2..]}); try stderr.writeAll(describe_usage); return error.UsageQuiet; } break :next args[i]; }; try (if (opt == .@"--match") &matches else &excludes).append(gpa, v); }, .@"--no-match" => matches.clearRetainingCapacity(), .@"--no-exclude" => excludes.clearRetainingCapacity(), .@"--first-parent" => o.first_parent = true, .@"--always" => o.always = true, .@"--no-always" => o.always = false, .@"--contains" => o.contains = true, .@"--dirty", .@"--broken", .@"--debug" => return tool.fatalMessage(stderr, "zig-git describe: '{s}' is not supported", .{arg}), } } o.names.match = matches.items; o.names.exclude = excludes.items; if (o.long and o.abbrev != null and o.abbrev.? == 0) return tool.fatalMessage(stderr, "options '--long' and '--abbrev=0' cannot be used together", .{}); // --contains names commits as name-rev does, whose choice of name // has not been matched. if (o.contains) return tool.fatalMessage(stderr, "zig-git describe: '--contains' is not supported", .{}); if (operands.items.len == 0) try operands.append(gpa, "HEAD");
var repo = try discovery.open(ctx, stderr); defer repo.close(ctx.io); const settings = try object_names.settings(&repo.config, stderr); var names_of: object_names.Names = .init(ctx, &repo, settings); defer names_of.deinit(); var revs = try revisions.Revisions.init(ctx, &repo, settings, .cat_file, stderr); defer revs.deinit(); var h = try history.History.open(ctx, &repo, stderr); defer h.close();
var diag: Diagnostics = .{}; var names = git.describe.loadNames(gpa, ctx.io, repo.refs(), repo.objects(), o.names, &diag) catch |err| return tool.fatalMessage(stderr, "zig-git: cannot read the refs: {t}", .{err}); defer names.deinit(); if (names.isEmpty() and names.light.count() == 0 and !o.always) return tool.fatalMessage(stderr, "No names found, cannot describe anything.", .{}); const length = o.abbrev orelse names_of.defaultLength();
for (operands.items) |text| { const found = revs.resolver.resolve(text, &diag) catch { try revs.flushMessages(stderr, 1, false); return tool.fatalMessage(stderr, "Not a valid object name {s}", .{text}); }; try revs.flushMessages(stderr, 1, false); const c = try revs.resolver.peelToCommit(found.id) orelse { revs.resolver.clearMessages(); if (found.kind == .blob) return tool.fatalMessage(stderr, "zig-git describe: describing a blob is not supported", .{}); return tool.fatalMessage(stderr, "{s} is neither a commit nor blob", .{text}); }; const ci = try h.store.name(c); const result = git.describe.search(gpa, &names, &h.store, ci, .{ .candidates = o.candidates, .first_parent = o.first_parent }, &diag) catch |err| { try h.report(stderr); return h.fail(err, &diag, null, stderr); }; try h.report(stderr); if (result == .found) { const r = result.found; try stdout.writeAll(r.name.shown); if ((r.depth != 0 or o.long) and length != 0) { try stdout.print("-{d}-g", .{r.depth}); try names_of.write(stdout, if (r.depth == 0) r.name.target else c, length); } try stdout.writeByte('\n'); continue; } if (result == .mixed) return tool.fatalMessage(stderr, "zig-git describe: choosing among annotated tags, lightweight tags and other refs is not supported", .{}); if (o.always) { try names_of.write(stdout, c, @max(length, 4)); try stdout.writeByte('\n'); continue; } if (o.candidates == 0) return tool.fatalMessage(stderr, "no tag exactly matches '{f}'", .{c}); if (result.none.unannotated) return tool.fatalMessage(stderr, "No annotated tags can describe '{f}'.\nHowever, there were unannotated tags: try --tags.", .{c}); return tool.fatalMessage(stderr, "No tags can describe '{f}'.\nTry --always, or create some tags.", .{c}); }}