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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The commands that read refs, answered by the library's ref store and//! printing what git prints://!//! zig-git check-ref-format [--normalize] [--[no-]allow-onelevel] [--refspec-pattern] <refname>//! zig-git show-ref [--head] [-d] [-s | --hash[=<n>]] [--abbrev[=<n>]] [--branches | --heads] [--tags] [-q] [--] [<pattern>...]//! zig-git show-ref --verify [-q] [-d] [-s | --hash[=<n>]] [--abbrev[=<n>]] [--] <ref>...//! zig-git show-ref --exists <ref>//! zig-git for-each-ref [--format=<format>] [--sort=[-]refname] [--count=<n>]//! [--exclude=<pattern>] [<pattern>...]//! zig-git symbolic-ref [-q] [--short] [--[no-]recurse] <name>//!//! `for-each-ref` knows the atoms that need nothing but the refs and the//! objects they name: `refname` (with `:short`, `:lstrip=`, `:rstrip=`//! and `:strip=`), `objectname` and `*objectname` (with `:short` and//! `:short=<n>`), `objecttype`, `symref` (with the same modifiers as//! `refname`), `*objecttype` and `HEAD`. Object IDs are shortened as git//! shortens them (see `object_names.zig`): `show-ref --abbrev` and//! `--hash=<n>`, and `%(objectname:short)`. Every other atom is refused//! by name; so are `show-ref --exclude-existing`, `check-ref-format//! --branch`, the writing forms of `symbolic-ref`, and `for-each-ref`'s//! filtering, quoting and sorting options other than those above.//!//! What git does that is not matched: a reftable stack git cannot read,//! it reads as empty without a word (for-each-ref lists nothing and//! succeeds), where this says what is wrong and fails; and a malformed//! `packed-refs` is refused before anything is printed, where `show-ref`//! prints the refs it reads before the bad line.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const ObjectId = git.ObjectId;const RefStore = git.RefStore;const refname = git.refname;const Diagnostics = git.Diagnostics;const tool = @import("zig_git.zig");const discovery = @import("discovery.zig");const object_names = @import("object_names.zig");const revisions = @import("revisions.zig");const history = @import("history.zig");const ref_filters = @import("ref_filters.zig");const Context = tool.Context;
fn refuse(stderr: *Io.Writer, comptime command: []const u8, comptime fmt: []const u8, args: anytype) error{Fatal} { stderr.print("zig-git: " ++ command ++ ": " ++ fmt ++ " is not supported\n", args) catch {}; return error.Fatal;}
/// Writes `name` as git writes a ref name in a warning: control/// characters as `?`.fn writeSanitized(w: *Io.Writer, name: []const u8) !void { for (name) |c| try w.writeByte(if (c < 0x20 or c == 0x7f) '?' else c);}
/// Says what git says of refs it could not read, and fails.fn describe(stderr: *Io.Writer, err: anyerror, diag: *const Diagnostics) anyerror { const path = diag.path() orelse ""; switch (err) { // git shows a line of 80 bytes or more as its first 75 and `...`. error.UnexpectedPackedRefsLine, error.UnterminatedPackedRefsLine => { const line = diag.detail(); const what = if (err == error.UnexpectedPackedRefsLine) "unexpected" else "unterminated"; if (line.len >= 80) try stderr.print("fatal: {s} line in {s}: {s}...\n", .{ what, path, line[0..75] }) else try stderr.print("fatal: {s} line in {s}: {s}\n", .{ what, path, line }); }, error.MalformedReftable => try stderr.print("fatal: zig-git: corrupt reftable {s} at offset {d}: {s}\n", .{ path, diag.offset orelse 0, diag.detail() }), error.ReftableChecksumMismatch => try stderr.print("fatal: zig-git: corrupt reftable {s}: footer checksum mismatch\n", .{path}), error.UnsupportedReftableVersion => try stderr.print("fatal: zig-git: reftable {s} has unsupported version {s}\n", .{ path, diag.detail() }), error.ReftableObjectFormatMismatch => try stderr.print("fatal: zig-git: reftable {s} is for {s} object IDs\n", .{ path, diag.detail() }), error.MissingReftable => try stderr.print("fatal: zig-git: reftable {s} is missing\n", .{path}), error.RefUnreadable => try stderr.print("fatal: zig-git: cannot read {s}: {s}\n", .{ path, diag.detail() }), else => return err, } return error.Fatal;}
// -- check-ref-format ------------------------------------------------------------
const check_ref_format_usage = \\usage: git check-ref-format [--normalize] [<options>] <refname> \\ or: git check-ref-format --branch <branchname-shorthand> \\;
/// `check-ref-format`, which needs no repository.pub fn checkRefFormat(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { var options: refname.CheckOptions = .{}; var normalize = false; var name: ?[]const u8 = null; for (args) |a| { if (name == null and a.len > 0 and a[0] == '-') { if (std.mem.eql(u8, a, "--normalize") or std.mem.eql(u8, a, "--print")) { normalize = true; } else if (std.mem.eql(u8, a, "--allow-onelevel")) { options.allow_onelevel = true; } else if (std.mem.eql(u8, a, "--no-allow-onelevel")) { options.allow_onelevel = false; } else if (std.mem.eql(u8, a, "--refspec-pattern")) { options.refspec_pattern = true; } else if (std.mem.eql(u8, a, "-h")) { try stdout.writeAll(check_ref_format_usage); return; } else if (std.mem.eql(u8, a, "--branch")) { return refuse(stderr, "check-ref-format", "--branch", .{}); } else { try stderr.writeAll(check_ref_format_usage); return error.UsageQuiet; } continue; } if (name != null) { try stderr.writeAll(check_ref_format_usage); return error.UsageQuiet; } name = a; } const n = name orelse { try stderr.writeAll(check_ref_format_usage); return error.UsageQuiet; }; var buffer: std.ArrayList(u8) = .empty; defer buffer.deinit(ctx.gpa); const checked = if (normalize) blk: { try buffer.resize(ctx.gpa, n.len); break :blk refname.normalize(n, buffer.items); } else n; if (!refname.isValid(checked, options)) return error.No; if (normalize) try stdout.print("{s}\n", .{checked});}
// -- show-ref --------------------------------------------------------------------
const show_ref_usage = \\usage: git show-ref [--head] [-d | --dereference] \\ [-s | --hash[=<n>]] [--abbrev[=<n>]] [--branches] [--tags] \\ [--] [<pattern>...] \\ or: git show-ref --verify [-q | --quiet] [-d | --dereference] \\ [-s | --hash[=<n>]] [--abbrev[=<n>]] \\ [--] [<ref>...] \\ or: git show-ref --exclude-existing[=<pattern>] \\ or: git show-ref --exists <ref> \\ \\ --[no-]tags only show tags (can be combined with --branches) \\ --[no-]branches only show branches (can be combined with --tags) \\ --[no-]exists check for reference existence without resolving \\ --[no-]verify stricter reference checking, requires exact ref path \\ --[no-]head show the HEAD reference, even if it would be filtered out \\ -d, --[no-]dereference \\ dereference tags into object IDs \\ -s, --[no-]hash[=<n>] only show SHA1 hash using <n> digits \\ --[no-]abbrev[=<n>] use <n> digits to display object names \\ -q, --[no-]quiet do not print results to stdout (useful with --verify) \\ --exclude-existing[=<pattern>] \\ show refs from stdin that aren't in local repository \\ \\;
const ShowRef = struct { ctx: Context, repo: *git.Repository, store: RefStore, stdout: *Io.Writer, stderr: *Io.Writer, names: *object_names.Names, /// How many digits object IDs are shortened to, or null for whole. id_length: ?usize, dereference: bool = false, hash_only: bool = false, quiet: bool = false,
/// Prints one ref as show-ref does, after checking that its object is /// there; `peeled` is what its storage says it peels to. fn show(s: *ShowRef, name: []const u8, direct: RefStore.Direct) !void { const io = s.ctx.io; var diag: Diagnostics = .{}; if (direct.id.eql(ObjectId.zero(direct.id.object_format)) or !(s.repo.database.contains(io, direct.id, &diag) catch false)) { try s.stderr.writeAll("fatal: git show-ref: bad ref "); try writeSanitized(s.stderr, name); try s.stderr.print(" ({f})\n", .{direct.id}); return error.Fatal; } if (s.quiet) return; try s.names.write(s.stdout, direct.id, s.id_length); if (s.hash_only) try s.stdout.writeByte('\n') else try s.stdout.print(" {s}\n", .{name}); if (!s.dereference) return; const peeled = direct.peeled orelse blk: { const header = s.repo.objects().readHeader(io, direct.id, &diag) catch return; if (header.kind != .tag) return; const p = RefStore.peel(io, s.ctx.gpa, s.repo.objects(), direct.id, &diag) catch return; break :blk p.id; }; try s.names.write(s.stdout, peeled, s.id_length); try s.stdout.print(" {s}^{{}}\n", .{name}); }
fn badRef(s: *ShowRef, name: []const u8, id: ObjectId) error{ Fatal, WriteFailed } { try s.stderr.writeAll("fatal: git show-ref: bad ref "); try writeSanitized(s.stderr, name); try s.stderr.print(" ({f})\n", .{id}); return error.Fatal; }};
/// `show-ref`.pub fn showRef(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { var head = false; var heads = false; var tags = false; var verify = false; var exists = false; var dereference = false; var hash_only = false; var quiet = false; // `--abbrev`: null for whole object IDs, 0 for core.abbrev's length, // or the number given. var abbrev: ?i64 = null; var patterns: std.ArrayList([]const u8) = .empty; defer patterns.deinit(ctx.gpa); var options_done = false; for (args) |a| { if (!options_done and a.len > 1 and a[0] == '-') { if (std.mem.eql(u8, a, "--")) { options_done = true; } else if (std.mem.eql(u8, a, "--head")) { head = true; } else if (std.mem.eql(u8, a, "--no-head")) { head = false; } else if (std.mem.eql(u8, a, "--branches") or std.mem.eql(u8, a, "--heads")) { heads = true; } else if (std.mem.eql(u8, a, "--no-branches") or std.mem.eql(u8, a, "--no-heads")) { heads = false; } else if (std.mem.eql(u8, a, "--tags")) { tags = true; } else if (std.mem.eql(u8, a, "--no-tags")) { tags = false; } else if (std.mem.eql(u8, a, "--verify")) { verify = true; } else if (std.mem.eql(u8, a, "--no-verify")) { verify = false; } else if (std.mem.eql(u8, a, "--exists")) { exists = true; } else if (std.mem.eql(u8, a, "--no-exists")) { exists = false; } else if (std.mem.eql(u8, a, "-d") or std.mem.eql(u8, a, "--dereference")) { dereference = true; } else if (std.mem.eql(u8, a, "--no-dereference")) { dereference = false; } else if (std.mem.eql(u8, a, "-s") or std.mem.eql(u8, a, "--hash")) { hash_only = true; } else if (std.mem.eql(u8, a, "--no-hash")) { hash_only = false; } else if (std.mem.eql(u8, a, "-q") or std.mem.eql(u8, a, "--quiet")) { quiet = true; } else if (std.mem.eql(u8, a, "--no-quiet")) { quiet = false; } else if (std.mem.eql(u8, a, "--abbrev")) { abbrev = 0; } else if (std.mem.eql(u8, a, "--no-abbrev")) { abbrev = null; } else if (std.mem.cutPrefix(u8, a, "--abbrev=") orelse std.mem.cutPrefix(u8, a, "--hash=")) |v| { const n = object_names.optionNumber(v) orelse { try stderr.print("error: option `{s}' expects a numerical value\n", .{if (a[2] == 'a') "abbrev" else "hash"}); try stderr.writeAll(show_ref_usage); return error.UsageQuiet; }; if (a[2] == 'h') hash_only = true; abbrev = if (n == 0) null else n; } else if (std.mem.startsWith(u8, a, "--exclude-existing")) { return refuse(stderr, "show-ref", "{s}", .{a}); } else { if (std.mem.startsWith(u8, a, "--")) try stderr.print("error: unknown option `{s}'\n", .{a[2..]}) else try stderr.print("error: unknown switch `{c}'\n", .{a[1]}); try stderr.writeAll(show_ref_usage); return error.UsageQuiet; } continue; } try patterns.append(ctx.gpa, a); } if (verify and exists) return tool.fatalMessage(stderr, "options '--verify' and '--exists' cannot be used together", .{});
var repo = try discovery.open(ctx, stderr); defer repo.close(ctx.io); const store = repo.refs(); var names: object_names.Names = .init(ctx, &repo, try object_names.settings(&repo.config, stderr)); defer names.deinit(); var s: ShowRef = .{ .ctx = ctx, .repo = &repo, .names = &names, .id_length = if (abbrev) |n| (if (n == 0) names.defaultLength() else git.abbrev.optionLength(repo.objectFormat(), n)) else null, .store = store, .stdout = stdout, .stderr = stderr, .dereference = dereference, .hash_only = hash_only, .quiet = quiet, }; const gpa = ctx.gpa; var diag: Diagnostics = .{};
if (exists) { if (patterns.items.len == 0) return tool.fatalMessage(stderr, "--exists requires a reference", .{}); if (patterns.items.len > 1) return tool.fatalMessage(stderr, "--exists requires exactly one reference", .{}); var target: std.ArrayList(u8) = .empty; defer target.deinit(gpa); const v = store.read(ctx.io, gpa, patterns.items[0], &target, &diag) catch |err| switch (err) { error.MalformedRef => { try stderr.writeAll("error: failed to look up reference: Invalid argument\n"); return error.No; }, else => |e| return describe(stderr, e, &diag), }; if (v == null) { try stderr.writeAll("error: reference does not exist\n"); return error.Missing; } return; }
if (verify) { if (patterns.items.len == 0) return tool.fatalMessage(stderr, "--verify requires a reference", .{}); for (patterns.items) |n| { const ok = std.mem.startsWith(u8, n, "refs/") or refname.hasRootRefSyntax(n); var resolved: ?RefStore.Resolved = if (ok) store.resolve(ctx.io, gpa, n, &diag) catch |err| return describe(stderr, err, &diag) else null; defer if (resolved) |*r| r.deinit(gpa); const direct = if (resolved) |r| switch (r.outcome) { .direct => |d| d, else => null, } else null; if (direct) |d| { try s.show(n, d); } else { if (quiet) return error.No; return tool.fatalMessage(stderr, "'{s}' - not a valid ref", .{n}); } } return; }
var shown = false; if (head) { var resolved = store.resolve(ctx.io, gpa, "HEAD", &diag) catch |err| return describe(stderr, err, &diag); defer resolved.deinit(gpa); if (resolved.outcome == .direct) { try s.show("HEAD", resolved.outcome.direct); shown = true; } } var list = store.list(ctx.io, gpa, "", &diag) catch |err| return describe(stderr, err, &diag); defer list.deinit(); for (list.entries) |e| { if (heads or tags) { const keep = (heads and std.mem.startsWith(u8, e.name, "refs/heads/")) or (tags and std.mem.startsWith(u8, e.name, "refs/tags/")); if (!keep) continue; } if (patterns.items.len != 0) { for (patterns.items) |p| { if (refname.matchesShowRefPattern(p, e.name)) break; } else continue; } const direct: RefStore.Direct = switch (e.value) { .direct => |d| d, .symbolic => blk: { var resolved = store.resolve(ctx.io, gpa, e.name, &diag) catch |err| return describe(stderr, err, &diag); defer resolved.deinit(gpa); switch (resolved.outcome) { .direct => |d| break :blk d, else => continue, } }, .broken => |b| switch (b) { .dangerous_name => { try stderr.writeAll("fatal: packed refname is dangerous: "); try writeSanitized(stderr, e.name); try stderr.writeAll("\n"); return error.Fatal; }, else => return s.badRef(e.name, ObjectId.zero(repo.objectFormat())), }, }; try s.show(e.name, direct); shown = true; } if (!shown) return error.No;}
// -- for-each-ref ----------------------------------------------------------------
const for_each_ref_usage = \\usage: git for-each-ref [--count=<count>] [--shell|--perl|--python|--tcl] \\ [(--sort=<key>)...] [--format=<format>] \\ [--include-root-refs] [--points-at=<object>] \\ [--merged[=<object>]] [--no-merged[=<object>]] \\ [--contains[=<object>]] [--no-contains[=<object>]] \\ [(--exclude=<pattern>)...] [--start-after=<marker>] \\ [ --stdin | (<pattern>...)] \\ \\ -s, --[no-]shell quote placeholders suitably for shells \\ -p, --[no-]perl quote placeholders suitably for perl \\ --[no-]python quote placeholders suitably for python \\ --[no-]tcl quote placeholders suitably for Tcl \\ --[no-]omit-empty do not output a newline after empty formatted refs \\ \\ --[no-]count <n> show only <n> matched refs \\ --[no-]format <format> \\ format to use for the output \\ --[no-]start-after <marker> \\ start iteration after the provided marker \\ --[no-]color[=<when>] respect format colors \\ --[no-]exclude <pattern> \\ exclude refs which match pattern \\ --[no-]sort <key> field name to sort on \\ --[no-]points-at <object> \\ print only refs which points at the given object \\ --merged <commit> print only refs that are merged \\ --no-merged <commit> print only refs that are not merged \\ --contains <commit> print only refs which contain the commit \\ --no-contains <commit> \\ print only refs which don't contain the commit \\ --[no-]ignore-case sorting and filtering are case insensitive \\ --[no-]stdin read reference patterns from stdin \\ --[no-]include-root-refs \\ also include HEAD ref and pseudorefs \\ \\;
/// One piece of a `--format`.const Piece = union(enum) { literal: []const u8, atom: Atom,};
const Atom = struct { field: Field, /// For `refname` and `symref`. strip: Strip = .none, /// `*`: of the object a tag peels to. deref: bool = false, /// For `objectname`: how far to shorten it. length: Length = .whole,
const Length = union(enum) { whole, default, digits: i64 }; const Field = enum { refname, objectname, objecttype, symref, head }; const Strip = union(enum) { none, short, lstrip: i64, rstrip: i64 };};
/// Parses a format into pieces, refusing what is not supported. Literal/// pieces are slices of `format` or of `arena`.fn parseFormat(arena: Allocator, format: []const u8, stderr: *Io.Writer) ![]Piece { var pieces: std.ArrayList(Piece) = .empty; var literal: std.ArrayList(u8) = .empty; var i: usize = 0; while (i < format.len) { const c = format[i]; if (c != '%') { try literal.append(arena, c); i += 1; continue; } if (i + 1 < format.len and format[i + 1] == '%') { try literal.append(arena, '%'); i += 2; continue; } if (i + 2 < format.len and std.ascii.isHex(format[i + 1]) and std.ascii.isHex(format[i + 2])) { try literal.append(arena, std.fmt.parseInt(u8, format[i + 1 .. i + 3], 16) catch unreachable); i += 3; continue; } if (i + 1 < format.len and format[i + 1] == '(') { const close = std.mem.findScalarPos(u8, format, i, ')') orelse { try stderr.print("error: malformed format string {s}\n", .{format[i..]}); try stderr.writeAll(for_each_ref_usage); return error.UsageQuiet; }; if (literal.items.len != 0) { try pieces.append(arena, .{ .literal = try literal.toOwnedSlice(arena) }); } try pieces.append(arena, .{ .atom = try parseAtom(format[i + 2 .. close], stderr) }); i = close + 1; continue; } try literal.append(arena, '%'); i += 1; } if (literal.items.len != 0) try pieces.append(arena, .{ .literal = try literal.toOwnedSlice(arena) }); return pieces.toOwnedSlice(arena);}
fn parseAtom(text: []const u8, stderr: *Io.Writer) !Atom { var a: Atom = .{ .field = .refname }; var rest = text; if (std.mem.cutPrefix(u8, rest, "*")) |r| { a.deref = true; rest = r; } const colon = std.mem.findScalar(u8, rest, ':'); const field_name = rest[0 .. colon orelse rest.len]; const modifier: ?[]const u8 = if (colon) |cpos| rest[cpos + 1 ..] else null; const known = std.meta.stringToEnum(enum { refname, objectname, objecttype, symref, HEAD }, field_name) orelse return refuse(stderr, "for-each-ref", "%({s})", .{text}); a.field = switch (known) { .refname => .refname, .objectname => .objectname, .objecttype => .objecttype, .symref => .symref, .HEAD => .head, }; // Of the dereferenced object only its name and type are known here. if (a.deref and a.field != .objectname and a.field != .objecttype) return refuse(stderr, "for-each-ref", "%({s})", .{text}); if (modifier) |m| if (a.field == .objectname) { if (std.mem.eql(u8, m, "short")) { a.length = .default; } else if (std.mem.cutPrefix(u8, m, "short=")) |v| { const n = object_names.optionNumber(v) orelse 0; if (n <= 0) return tool.fatalMessage(stderr, "positive value expected '{s}' in %({s})", .{ v, text }); a.length = .{ .digits = n }; } else return tool.fatalMessage(stderr, "unrecognized %(objectname) argument: {s}", .{m}); return a; }; if (modifier) |m| { if (a.field != .refname and a.field != .symref) return refuse(stderr, "for-each-ref", "%({s})", .{text}); if (std.mem.eql(u8, m, "short")) { a.strip = .short; } else if (std.mem.cutPrefix(u8, m, "lstrip=") orelse std.mem.cutPrefix(u8, m, "strip=")) |n| { a.strip = .{ .lstrip = std.fmt.parseInt(i64, n, 10) catch return refuse(stderr, "for-each-ref", "%({s})", .{text}) }; } else if (std.mem.cutPrefix(u8, m, "rstrip=")) |n| { a.strip = .{ .rstrip = std.fmt.parseInt(i64, n, 10) catch return refuse(stderr, "for-each-ref", "%({s})", .{text}) }; } else return refuse(stderr, "for-each-ref", "%({s})", .{text}); } return a;}
/// `for-each-ref`.pub fn forEachRef(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { const gpa = ctx.gpa; var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); var format: []const u8 = "%(objectname) %(objecttype)\t%(refname)"; var reverse = false; var count: usize = 0; var patterns: std.ArrayList([]const u8) = .empty; var excludes: std.ArrayList([]const u8) = .empty; var filters: ref_filters.Filters = .{}; var i: usize = 0; var options_done = false; while (i < args.len) : (i += 1) { const a = args[i]; if (options_done or a.len < 2 or a[0] != '-') { try patterns.append(arena, a); continue; } if (std.mem.eql(u8, a, "--")) { options_done = true; continue; } if (filters.parse(arena, a, if (i + 1 < args.len) args[i + 1] else null) catch { try stderr.writeAll("error: option `points-at' requires a value\n"); try stderr.writeAll(for_each_ref_usage); return error.UsageQuiet; }) |took| { i += took - 1; continue; } const Takes = enum { format, sort, count, exclude }; const takes: ?Takes, const inline_value: ?[]const u8 = inline for (.{ "format", "sort", "count", "exclude" }) |opt| { if (std.mem.eql(u8, a, "--" ++ opt)) break .{ @field(Takes, opt), null }; if (std.mem.cutPrefix(u8, a, "--" ++ opt ++ "=")) |v| break .{ @field(Takes, opt), v }; } else .{ null, null }; const t = takes orelse { const name = a[0 .. std.mem.findScalar(u8, a, '=') orelse a.len]; return refuse(stderr, "for-each-ref", "{s}", .{name}); }; const value = inline_value orelse if (i + 1 < args.len) blk: { i += 1; break :blk args[i]; } else { try stderr.print("error: option `{s}' requires a value\n", .{@tagName(t)}); try stderr.writeAll(for_each_ref_usage); return error.UsageQuiet; }; switch (t) { .format => format = value, .sort => { if (std.mem.eql(u8, value, "refname")) { reverse = false; } else if (std.mem.eql(u8, value, "-refname")) { reverse = true; } else return refuse(stderr, "for-each-ref", "--sort={s}", .{value}); }, .count => count = std.fmt.parseInt(usize, value, 10) catch return refuse(stderr, "for-each-ref", "--count={s}", .{value}), .exclude => try excludes.append(arena, value), } } const pieces = try parseFormat(arena, format, stderr);
var repo = try discovery.open(ctx, stderr); defer repo.close(ctx.io); const store = repo.refs(); const db = repo.objects(); var diag: Diagnostics = .{};
// `--contains` and the rest, which read commits. var settings_for_filters: ?object_names.Settings = null; var revs: ?revisions.Revisions = null; defer if (revs) |*r| r.deinit(); var h: ?history.History = null; defer if (h) |*x| x.close(); var prepared: ?ref_filters.Prepared = null; defer if (prepared) |*p| p.deinit(); if (filters.active()) { settings_for_filters = try object_names.settings(&repo.config, stderr); revs = try revisions.Revisions.init(ctx, &repo, settings_for_filters.?, .rev_parse, stderr); h = try history.History.open(ctx, &repo, stderr); prepared = try ref_filters.prepare(&filters, ctx, &repo, &revs.?, &h.?, stderr); }
// What HEAD names, for %(HEAD). var head_resolved = store.resolve(ctx.io, gpa, "HEAD", &diag) catch |err| return describe(stderr, err, &diag); defer head_resolved.deinit(gpa); const head_target: ?[]const u8 = if (head_resolved.isSymbolic() and head_resolved.outcome == .direct) head_resolved.last() else null;
var list = store.list(ctx.io, gpa, "", &diag) catch |err| return describe(stderr, err, &diag); defer list.deinit();
// With refname order, which is the order the refs are listed in, git // prints each ref as it comes to it; sorted otherwise, it reads them // all first. Either way it stops where something is wrong, with what // it printed before then printed. const Chosen = struct { entry: RefStore.Entry, direct: RefStore.Direct, symref: ?[]const u8 }; var chosen: std.ArrayList(Chosen) = .empty; var printed: usize = 0; var names: object_names.Names = .init(ctx, &repo, try object_names.settings(&repo.config, stderr)); defer names.deinit(); var formatter: Formatter = .{ .ctx = ctx, .names = &names, .store = store, .db = db, .pieces = pieces, .head_target = head_target, .stdout = stdout, .stderr = stderr }; for (list.entries) |e| { if (refname.isRootRef(e.name)) continue; if (patterns.items.len != 0) { for (patterns.items) |p| { if (refname.matchesForEachRefPattern(p, e.name)) break; } else continue; } const excluded = for (excludes.items) |p| { if (refname.matchesForEachRefPattern(p, e.name)) break true; } else false; if (excluded) continue; var symref: ?[]const u8 = null; const direct: RefStore.Direct = switch (e.value) { .direct => |d| d, .symbolic => blk: { var resolved = store.resolve(ctx.io, gpa, e.name, &diag) catch |err| return describe(stderr, err, &diag); defer resolved.deinit(gpa); switch (resolved.outcome) { .direct => |d| { symref = try arena.dupe(u8, resolved.last()); break :blk d; }, else => continue, } }, .broken => |b| { switch (b) { .malformed => try stderr.writeAll("warning: ignoring broken ref "), .bad_name => try stderr.writeAll("warning: ignoring ref with broken name "), .dangerous_name => { try stderr.writeAll("fatal: packed refname is dangerous: "); try writeSanitized(stderr, e.name); try stderr.writeAll("\n"); return error.Fatal; }, } try writeSanitized(stderr, e.name); try stderr.writeAll("\n"); continue; }, }; if (direct.id.eql(ObjectId.zero(direct.id.object_format))) { try stderr.writeAll("warning: ignoring broken ref "); try writeSanitized(stderr, e.name); try stderr.writeAll("\n"); continue; } if (prepared) |*p| if (!try p.keeps(direct.id, stderr)) continue; if (reverse) { try chosen.append(arena, .{ .entry = e, .direct = direct, .symref = symref }); } else if (count == 0 or printed < count) { try formatter.write(e.name, direct, symref); printed += 1; } } if (reverse) { std.mem.reverse(Chosen, chosen.items); const n = if (count == 0) chosen.items.len else @min(count, chosen.items.len); for (chosen.items[0..n]) |c| try formatter.write(c.entry.name, c.direct, c.symref); }}
/// Writes one ref as a `--format` says.const Formatter = struct { ctx: Context, names: *object_names.Names, store: RefStore, db: git.ObjectDatabase, pieces: []const Piece, head_target: ?[]const u8, stdout: *Io.Writer, stderr: *Io.Writer,
fn header(f: *const Formatter, id: ObjectId, name: []const u8) !git.ObjectKind { var diag: Diagnostics = .{}; const h = f.db.readHeader(f.ctx.io, id, &diag) catch |err| switch (err) { error.ObjectNotFound => return tool.fatalMessage(f.stderr, "missing object {f} for {s}", .{ id, name }), else => |x| return x, }; return h.kind; }
fn writeId(f: *const Formatter, w: *Io.Writer, id: ObjectId, length: Atom.Length) !void { const n: ?usize = switch (length) { .whole => null, .default => f.names.defaultLength(), .digits => |d| git.abbrev.optionLength(id.object_format, d), }; try f.names.write(w, id, n); }
/// Formats the ref whole before writing any of it, so that a ref git /// stops at leaves nothing of itself behind. fn write(f: *const Formatter, name: []const u8, direct: RefStore.Direct, symref: ?[]const u8) !void { var line: Io.Writer.Allocating = .init(f.ctx.gpa); defer line.deinit(); try f.format(&line.writer, name, direct, symref); try f.stdout.writeAll(line.written()); }
fn format(f: *const Formatter, w: *Io.Writer, name: []const u8, direct: RefStore.Direct, symref: ?[]const u8) !void { var kind: ?git.ObjectKind = null; var peeled: ?RefStore.Peeled = null; var peel_done = false; for (f.pieces) |piece| switch (piece) { .literal => |l| try w.writeAll(l), .atom => |atom| { if ((atom.deref or atom.field == .objecttype) and kind == null) kind = try f.header(direct.id, name); if (atom.deref and !peel_done) { peel_done = true; if (kind.? == .tag) { if (direct.peeled) |p| { peeled = .{ .id = p, .kind = try f.header(p, name) }; } else { var diag: Diagnostics = .{}; peeled = RefStore.peel(f.ctx.io, f.ctx.gpa, f.db, direct.id, &diag) catch |err| switch (err) { error.ObjectNotFound => return tool.fatalMessage(f.stderr, "missing object {f} for {s}", .{ diag.object_id orelse direct.id, name }), else => |x| return x, }; } } } switch (atom.field) { .refname => try writeName(w, f.ctx, f.store, name, atom.strip), .symref => if (symref) |t| try writeName(w, f.ctx, f.store, t, atom.strip), .objectname => if (atom.deref) { if (peeled) |p| try f.writeId(w, p.id, atom.length); } else try f.writeId(w, direct.id, atom.length), .objecttype => if (atom.deref) { if (peeled) |p| try w.writeAll(p.kind.name()); } else try w.writeAll(kind.?.name()), .head => try w.writeAll(if (f.head_target != null and std.mem.eql(u8, f.head_target.?, name)) "*" else " "), } }, }; try w.writeByte('\n'); }};
fn writeName(w: *Io.Writer, ctx: Context, store: RefStore, name: []const u8, strip: Atom.Strip) !void { switch (strip) { .none => try w.writeAll(name), .short => { var diag: Diagnostics = .{}; try w.writeAll(try store.shorten(ctx.io, ctx.gpa, name, true, &diag)); }, .lstrip => |n| try w.writeAll(refname.lstrip(name, n)), .rstrip => |n| try w.writeAll(refname.rstrip(name, n)), }}
// -- symbolic-ref ----------------------------------------------------------------
const symbolic_ref_usage = \\usage: git symbolic-ref [-m <reason>] <name> <ref> \\ or: git symbolic-ref [-q] [--short] [--no-recurse] <name> \\ or: git symbolic-ref --delete [-q] <name> \\ \\ -q, --[no-]quiet suppress error message for non-symbolic (detached) refs \\ -d, --[no-]delete delete symbolic ref \\ --[no-]short shorten ref output \\ --[no-]recurse recursively dereference (default) \\ -m <reason> reason of the update \\ \\;
/// `symbolic-ref`, reading.pub fn symbolicRef(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { var quiet = false; var short = false; var recurse = true; var names: std.ArrayList([]const u8) = .empty; defer names.deinit(ctx.gpa); var options_done = false; for (args) |a| { if (!options_done and a.len > 1 and a[0] == '-') { if (std.mem.eql(u8, a, "--")) { options_done = true; } else if (std.mem.eql(u8, a, "-q") or std.mem.eql(u8, a, "--quiet")) { quiet = true; } else if (std.mem.eql(u8, a, "--no-quiet")) { quiet = false; } else if (std.mem.eql(u8, a, "--short")) { short = true; } else if (std.mem.eql(u8, a, "--no-short")) { short = false; } else if (std.mem.eql(u8, a, "--recurse")) { recurse = true; } else if (std.mem.eql(u8, a, "--no-recurse")) { recurse = false; } else if (std.mem.eql(u8, a, "-d") or std.mem.eql(u8, a, "--delete") or std.mem.startsWith(u8, a, "-m")) { return refuse(stderr, "symbolic-ref", "{s}", .{a}); } else { try stderr.writeAll(symbolic_ref_usage); return error.UsageQuiet; } continue; } try names.append(ctx.gpa, a); } if (names.items.len == 2) return refuse(stderr, "symbolic-ref", "setting a symbolic ref", .{}); if (names.items.len != 1) { try stderr.writeAll(symbolic_ref_usage); return error.UsageQuiet; } const name = names.items[0]; var repo = try discovery.open(ctx, stderr); defer repo.close(ctx.io); const store = repo.refs(); const gpa = ctx.gpa; var diag: Diagnostics = .{}; var resolved = store.resolve(ctx.io, gpa, name, &diag) catch |err| return describe(stderr, err, &diag); defer resolved.deinit(gpa); if (resolved.outcome == .malformed and !resolved.isSymbolic()) return tool.fatalMessage(stderr, "No such ref: {s}", .{name}); if (!resolved.isSymbolic()) { if (quiet) return error.No; return tool.fatalMessage(stderr, "ref {s} is not a symbolic ref", .{name}); } const target = if (!recurse) resolved.chain[1] else switch (resolved.outcome) { .direct, .missing => resolved.last(), else => return tool.fatalMessage(stderr, "No such ref: {s}", .{name}), }; if (!recurse and resolved.outcome == .bad_target and resolved.chain.len == 2) return tool.fatalMessage(stderr, "No such ref: {s}", .{name}); const shown = if (short) try store.shorten(ctx.io, gpa, target, true, &diag) else target; try stdout.print("{s}\n", .{shown});}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;const Repo = tool.Repo;
fn expectAllSameAsGit(repo: *Repo, cwd: []const u8, commands: []const []const []const u8) !void { for (commands) |args| try tool.expectSameAsGitHow(repo, args, .{ .cwd = cwd });}
fn writeFile(repo: *Repo, path: []const u8, contents: []const u8) !void { if (std.fs.path.dirname(path)) |d| try repo.tmp.dir.createDirPath(repo.io, d); try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = contents });}
fn revParse(repo: *Repo, rev: []const u8) ![]u8 { const out = try repo.output(&.{ "rev-parse", rev }); defer repo.gpa.free(out); return repo.gpa.dupe(u8, std.mem.trimEnd(u8, out, "\n"));}
test "zig-git check-ref-format answers as git does" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); const names = [_][]const u8{ "refs/heads/a", "a", "@", "refs/@", "a/@", "foo//bar", "/foo", "//foo/bar", "foo/", "foo/.", "foo/.bar", "foo/bar.", "foo/bar.lock", "foo/bar.lock/x", "foo/a..b", "foo/a@{b", "foo/a@b", "foo/a{b", "foo/a b", "foo/a~b", "foo/a\\b", "foo/a*b", "foo/*", "foo/*/*", "foo/a?b", "foo/a[b", "foo/a]b", "foo/a\x7fb", "foo/\xc3\xa9", "foo/\xff", "", "foo/-bar", "HEAD", ".foo/bar", "foo.lock", "foo/.lock", "foo/...", "./foo", "*", "/", "foo/*bar*", "*/foo", "@/x", "x/@{", "a/b\tc", "a/b:c", "a/b^c", "refs/heads/x.y", }; const option_sets = [_][]const []const u8{ &.{}, &.{"--normalize"}, &.{"--allow-onelevel"}, &.{"--refspec-pattern"}, &.{ "--normalize", "--allow-onelevel", "--refspec-pattern" }, &.{ "--allow-onelevel", "--no-allow-onelevel" }, &.{"--print"}, }; var args: std.ArrayList([]const u8) = .empty; defer args.deinit(testing.allocator); for (names) |n| for (option_sets) |o| { args.clearRetainingCapacity(); try args.append(testing.allocator, "check-ref-format"); try args.appendSlice(testing.allocator, o); try args.append(testing.allocator, n); try tool.expectSameAsGitHow(&repo, args.items, .{}); }; try expectAllSameAsGit(&repo, ".", &.{ &.{"check-ref-format"}, &.{ "check-ref-format", "a/b", "c/d" }, &.{ "check-ref-format", "--bogus", "a/b" }, &.{ "check-ref-format", "-h" }, });}
/// Branches, annotated, nested and tree tags, a remote's symbolic HEAD,/// some refs packed and some loose.fn makeRefs(repo: *Repo) !void { try repo.commit("one"); try repo.git(&.{ "tag", "-a", "-m", "version one", "v1" }); try repo.git(&.{ "tag", "-a", "-m", "nested", "v1-nested", "v1" }); try repo.git(&.{ "tag", "-a", "-m", "a tree", "tree-tag", "HEAD^{tree}" }); try repo.git(&.{ "tag", "light" }); try repo.git(&.{ "branch", "side" }); try repo.git(&.{ "branch", "x" }); try repo.git(&.{ "tag", "x" }); try repo.git(&.{ "update-ref", "refs/remotes/origin/main", "HEAD" }); try repo.git(&.{ "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main" }); try repo.git(&.{ "update-ref", "refs/notes/commits", "HEAD" }); try repo.git(&.{ "pack-refs", "--all" }); try repo.commit("two"); try repo.git(&.{ "branch", "a/b" }); try repo.git(&.{ "branch", "a-b" }); try repo.git(&.{ "branch", "heads/x" }); try repo.git(&.{ "tag", "-a", "-m", "loose", "v2" }); try repo.git(&.{ "symbolic-ref", "refs/heads/current", "refs/heads/main" });}
const listing_commands = [_][]const []const u8{ &.{"show-ref"}, &.{ "show-ref", "--head" }, &.{ "show-ref", "-d" }, &.{ "show-ref", "--dereference", "--head", "--tags" }, &.{ "show-ref", "-s", "-d", "--tags" }, &.{ "show-ref", "--hash", "--branches" }, &.{ "show-ref", "--heads", "--tags", "x" }, &.{ "show-ref", "x" }, &.{ "show-ref", "main", "v1" }, &.{ "show-ref", "heads/x" }, &.{ "show-ref", "HEAD" }, &.{ "show-ref", "--head", "nomatch" }, &.{ "show-ref", "nomatch" }, &.{ "show-ref", "-q", "x" }, &.{ "show-ref", "--", "x" }, &.{ "show-ref", "--verify", "refs/heads/main" }, &.{ "show-ref", "--verify", "-d", "refs/tags/v1-nested", "refs/tags/tree-tag" }, &.{ "show-ref", "--verify", "-s", "HEAD" }, &.{ "show-ref", "--verify", "refs/heads/current" }, &.{ "show-ref", "--verify", "main" }, &.{ "show-ref", "--verify", "refs/tags/v1", "refs/heads/nope", "refs/heads/x" }, &.{ "show-ref", "--verify", "-q", "refs/heads/nope" }, &.{ "show-ref", "--verify", "-q", "refs/heads/main" }, &.{ "show-ref", "--verify" }, &.{ "show-ref", "--exists", "refs/heads/main" }, &.{ "show-ref", "--exists", "refs/heads/nope" }, &.{ "show-ref", "--exists", "HEAD" }, &.{ "show-ref", "--exists" }, &.{ "show-ref", "--exists", "a", "b" }, &.{ "show-ref", "--verify", "--exists", "refs/heads/main" }, &.{"for-each-ref"}, &.{ "for-each-ref", "refs/tags" }, &.{ "for-each-ref", "refs/remotes/", "refs/heads/side" }, &.{ "for-each-ref", "refs/tags/v*" }, &.{ "for-each-ref", "refs/hea" }, &.{ "for-each-ref", "refs/heads/*" }, &.{ "for-each-ref", "refs/heads/?/b" }, &.{ "for-each-ref", "refs/heads/a?b" }, &.{ "for-each-ref", "refs/**/x" }, &.{ "for-each-ref", "--exclude=refs/tags/v*", "refs/tags" }, &.{ "for-each-ref", "--sort=-refname" }, &.{ "for-each-ref", "--count=3", "--sort=refname" }, &.{ "for-each-ref", "--format=%(refname) %(objectname) %(objecttype) %(symref)" }, &.{ "for-each-ref", "--format=%(refname:short)|%(refname:lstrip=2)|%(refname:rstrip=1)|%(refname:strip=-1)|%(refname:lstrip=9)" }, &.{ "for-each-ref", "--format=%(*objectname) %(*objecttype) %(refname)", "refs/tags" }, &.{ "for-each-ref", "--format=%(HEAD) %(refname) %(symref:short) %(symref:lstrip=1)" }, &.{ "for-each-ref", "--format", "%%%0a%41%(refname)%x41%" }, &.{ "for-each-ref", "--format=%(refname" }, &.{ "symbolic-ref", "HEAD" }, &.{ "symbolic-ref", "--short", "HEAD" }, &.{ "symbolic-ref", "-q", "refs/heads/main" }, &.{ "symbolic-ref", "refs/heads/main" }, &.{ "symbolic-ref", "refs/heads/nope" }, &.{ "symbolic-ref", "--short", "refs/remotes/origin/HEAD" }, &.{ "symbolic-ref", "--no-recurse", "refs/heads/current" }, &.{ "symbolic-ref", "--short", "refs/heads/current" },};
test "zig-git lists refs as git does, packed and loose, in both ref formats" { for ([_][]const u8{ "files", "reftable" }) |ref_format| { var repo = try Repo.initWith(testing.allocator, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try makeRefs(&repo); try expectAllSameAsGit(&repo, ".", &listing_commands); // Usage errors, whose messages are git's, compared on output. for ([_][]const []const u8{ &.{ "show-ref", "--bogus" }, &.{ "show-ref", "-z" }, &.{"symbolic-ref"}, &.{ "symbolic-ref", "a", "b", "c" }, &.{ "for-each-ref", "--format" }, }) |args| try tool.expectSameAsGitHow(&repo, args, .{ .compare = .output }); // A detached HEAD. try repo.git(&.{ "checkout", "-q", "--detach" }); try expectAllSameAsGit(&repo, ".", &.{ &.{ "symbolic-ref", "HEAD" }, &.{ "symbolic-ref", "-q", "HEAD" }, &.{ "show-ref", "--head" }, &.{ "for-each-ref", "--format=%(HEAD)%(refname)" }, }); }}
test "zig-git lists refs after git refs migrate as git does" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try makeRefs(&repo); try repo.git(&.{ "refs", "migrate", "--ref-format=reftable" }); try expectAllSameAsGit(&repo, ".", &listing_commands); try repo.git(&.{ "refs", "migrate", "--ref-format=files" }); try expectAllSameAsGit(&repo, ".", &listing_commands);}
test "zig-git refuses what it does not do, by name" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try repo.commit("one"); for ([_][]const []const u8{ &.{ "check-ref-format", "--branch", "main" }, &.{ "show-ref", "--exclude-existing" }, &.{ "for-each-ref", "--format=%(subject)" }, &.{ "for-each-ref", "--format=%(*refname)" }, &.{ "for-each-ref", "--sort=objectname" }, &.{ "for-each-ref", "--include-root-refs" }, &.{ "symbolic-ref", "HEAD", "refs/heads/other" }, &.{ "symbolic-ref", "-d", "HEAD" }, }) |args| { const ours = try tool.runOurs(&repo, args, .{}); defer ours.deinit(testing.allocator); try testing.expectEqual(128, ours.code); try testing.expect(std.mem.indexOf(u8, ours.stderr, "is not supported") != null); try testing.expectEqualStrings("", ours.stdout); }}
test "zig-git passes over and stops at broken refs as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "tag", "-a", "-m", "t", "v1" }); const head = try revParse(&repo, "HEAD"); defer gpa.free(head); const missing = try gpa.alloc(u8, head.len); defer gpa.free(missing); @memset(missing, '1'); const zero = try gpa.alloc(u8, head.len); defer gpa.free(zero); @memset(zero, '0'); const cases = [_][]const u8{ "", "garbage\n", "ref: refs/heads/nope\n", "ref: bad..name\n", "ref: refs/heads/x\n", "ref: refs/heads/broken-target\n" }; const commands = [_][]const []const u8{ &.{"for-each-ref"}, &.{ "for-each-ref", "--format=%(refname)" }, &.{"show-ref"}, &.{ "show-ref", "-d" }, &.{ "show-ref", "--verify", "refs/heads/x" }, &.{ "show-ref", "--exists", "refs/heads/x" }, &.{ "symbolic-ref", "refs/heads/x" }, }; try writeFile(&repo, ".git/refs/heads/broken-target", "garbage\n"); for (cases) |contents| { try writeFile(&repo, ".git/refs/heads/x", contents); try expectAllSameAsGit(&repo, ".", &commands); } try repo.tmp.dir.deleteFile(repo.io, ".git/refs/heads/broken-target"); for ([_][]const u8{ head[0 .. head.len - 1], missing, zero }) |id| { const contents = try std.fmt.allocPrint(gpa, "{s}\n", .{id}); defer gpa.free(contents); try writeFile(&repo, ".git/refs/heads/x", contents); try expectAllSameAsGit(&repo, ".", &commands); } // Contents git reads despite their oddities. for ([_][]const u8{ "\r\n", " trailing garbage\n", "\tx", "" }) |tail| { const contents = try std.fmt.allocPrint(gpa, "{s}{s}", .{ head, tail }); defer gpa.free(contents); try writeFile(&repo, ".git/refs/heads/x", contents); try expectAllSameAsGit(&repo, ".", &commands); } try repo.tmp.dir.deleteFile(repo.io, ".git/refs/heads/x"); // Names git will not make, and files it passes over. const line = try std.fmt.allocPrint(gpa, "{s}\n", .{head}); defer gpa.free(line); for ([_][]const u8{ "a..b", "a b", "x.", "a~b", ".hidden", "x.lock" }) |n| { const path = try std.fmt.allocPrint(gpa, ".git/refs/heads/{s}", .{n}); defer gpa.free(path); try writeFile(&repo, path, line); try expectAllSameAsGit(&repo, ".", commands[0..4]); try repo.tmp.dir.deleteFile(repo.io, path); }}
test "zig-git reads packed-refs as git does, its oddities and corruptions included" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "tag", "-a", "-m", "t", "v1" }); try repo.git(&.{ "pack-refs", "--all" }); const h = try revParse(&repo, "HEAD"); defer gpa.free(h); const t = try revParse(&repo, "v1"); defer gpa.free(t); const tree = try revParse(&repo, "HEAD^{tree}"); defer gpa.free(tree); const header = "# pack-refs with: peeled fully-peeled sorted \n"; const files = [_][]const u8{ try std.fmt.allocPrint(gpa, "{s} refs/heads/main\n{s} refs/tags/v1\n", .{ h, t }), try std.fmt.allocPrint(gpa, "{s} refs/tags/v1\n^{s}\n{s} refs/heads/main\n", .{ t, h, h }), try std.fmt.allocPrint(gpa, header ++ "{s} refs/tags/v1\n{s} refs/heads/main\n", .{ t, h }), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main\r\n{s} refs/tags/v1\r\n", .{ h, t }), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main\n{s} refs/tags/v1\n^{s}\n", .{ h, t, tree }), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main\n^{s}\n", .{ h, tree }), try std.fmt.allocPrint(gpa, header ++ "{s}\trefs/heads/main\n", .{h}), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main\n{s} refs/heads/main\n", .{ h, h }), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/a..b\n{s} refs/heads/main\n", .{ h, h }), try std.fmt.allocPrint(gpa, header ++ "{s} foo\n{s} FOO\n{s} HEAD\n{s} ORIG_HEAD\n{s} x/y\n{s} refs/heads/main\n", .{ h, h, h, h, h, h }), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main\n{s} refs/heads/x/\n", .{ h, h }), try std.fmt.allocPrint(gpa, header ++ "{s} a b\n{s} refs/heads/main\n", .{ h, h }), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main\ngarbage\n", .{h}), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main", .{h}), try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/main\n", .{h}), "", }; defer for (files[0 .. files.len - 1]) |f| gpa.free(f); for (files) |f| { try writeFile(&repo, ".git/packed-refs", f); try expectAllSameAsGit(&repo, ".", &.{ &.{"for-each-ref"}, &.{ "for-each-ref", "--format=%(refname) %(*objectname) %(*objecttype)" }, }); // show-ref prints what it read before a bad line, which this does // not; its answer otherwise is git's. try tool.expectSameAsGitHow(&repo, &.{ "show-ref", "-d" }, .{ .compare = if (std.mem.indexOf(u8, f, "garbage") != null or (f.len > 0 and f[f.len - 1] != '\n')) .output else .everything }); } // A loose ref overrides a packed one, and a packed ref with a loose // one under its name as a directory is still there. try repo.git(&.{ "pack-refs", "--all" }); try repo.commit("two"); const with_d = try std.fmt.allocPrint(gpa, header ++ "{s} refs/heads/d\n{s} refs/heads/main\n", .{ h, h }); defer gpa.free(with_d); try writeFile(&repo, ".git/packed-refs", with_d); // git will not make it, with refs/heads/d packed, so it is written. const line = try std.fmt.allocPrint(gpa, "{s}\n", .{h}); defer gpa.free(line); try writeFile(&repo, ".git/refs/heads/d/e", line); try expectAllSameAsGit(&repo, ".", &.{ &.{"for-each-ref"}, &.{ "show-ref", "-d" }, &.{ "show-ref", "--verify", "refs/heads/d", "refs/heads/d/e" }, });}
test "zig-git follows symbolic refs as far as git does, cycles included" { for ([_][]const u8{ "files", "reftable" }) |ref_format| { var repo = try Repo.initWith(testing.allocator, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "symbolic-ref", "refs/heads/s0", "refs/heads/main" }); for (1..6) |i| { var from: [32]u8 = undefined; var to: [32]u8 = undefined; try repo.git(&.{ "symbolic-ref", try std.fmt.bufPrint(&from, "refs/heads/s{d}", .{i}), try std.fmt.bufPrint(&to, "refs/heads/s{d}", .{i - 1}) }); } try repo.git(&.{ "symbolic-ref", "refs/heads/loop-a", "refs/heads/loop-b" }); try repo.git(&.{ "symbolic-ref", "refs/heads/loop-b", "refs/heads/loop-a" }); try repo.git(&.{ "symbolic-ref", "refs/heads/unborn", "refs/heads/nothing" }); try expectAllSameAsGit(&repo, ".", &.{ &.{"for-each-ref"}, &.{ "for-each-ref", "--format=%(refname) %(symref) %(symref:short)" }, &.{"show-ref"}, &.{ "show-ref", "--verify", "refs/heads/s3" }, &.{ "show-ref", "--verify", "refs/heads/s4" }, &.{ "show-ref", "--verify", "refs/heads/loop-a" }, &.{ "show-ref", "--exists", "refs/heads/loop-a" }, &.{ "symbolic-ref", "refs/heads/s3" }, &.{ "symbolic-ref", "refs/heads/s4" }, &.{ "symbolic-ref", "--no-recurse", "refs/heads/s4" }, &.{ "symbolic-ref", "refs/heads/loop-a" }, &.{ "symbolic-ref", "refs/heads/unborn" }, &.{ "symbolic-ref", "--short", "refs/heads/unborn" }, }); for ([_][]const u8{ "refs/heads/s2", "refs/heads/s3", "refs/heads/loop-a" }) |target| { try repo.git(&.{ "symbolic-ref", "HEAD", target }); try expectAllSameAsGit(&repo, ".", &.{ &.{ "show-ref", "--head" }, &.{ "symbolic-ref", "HEAD" }, &.{ "for-each-ref", "--format=%(HEAD) %(refname)" }, }); } }}
test "zig-git reads refnames with every awkward character git allows" { const gpa = testing.allocator; for ([_][]const u8{ "files", "reftable" }) |ref_format| { var repo = try Repo.initWith(gpa, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try repo.commit("one"); const names = [_][]const u8{ "a", "a-b", "a.b", "a+b", "a_b", "a,b", "a=b", "a!b", "a#b", "a$b", "a%b", "a&b", "a'b", "a\"b", "a(b)", "a;b", "a<b>", "a|b", "a`b", "a{b}", "a]b", "a@b", "@", "-x", "\xc3\xa9t\xc3\xa9", "\xff\xfe", "d/b/c", "a0", "A", "Z", "zz/top", }; var input: Io.Writer.Allocating = .init(gpa); defer input.deinit(); for (names) |n| try input.writer.print("create refs/heads/{s} HEAD\ncreate refs/tags/t{s} HEAD\n", .{ n, n }); const out = try repo.outputWithInput(&.{ "update-ref", "--stdin" }, input.written()); gpa.free(out); try repo.git(&.{ "update-ref", "refs/x", "HEAD" }); for ([_]bool{ false, true }) |pack| { if (pack) try repo.git(&.{ "pack-refs", "--all" }); try expectAllSameAsGit(&repo, ".", &.{ &.{"for-each-ref"}, &.{ "for-each-ref", "--format=%(refname:short)" }, &.{ "for-each-ref", "refs/heads/a*" }, &.{"show-ref"}, &.{ "show-ref", "b" }, &.{ "show-ref", "--verify", "refs/heads/a<b>" }, &.{ "show-ref", "--verify", "refs/heads/\xff\xfe" }, }); } }}
test "zig-git reads a linked worktree's per-worktree refs as git does" { for ([_][]const u8{ "files", "reftable" }) |ref_format| { var repo = try Repo.initWith(testing.allocator, testing.io, .{ .ref_format = ref_format }); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "update-ref", "refs/bisect/main-good", "HEAD" }); try repo.git(&.{ "update-ref", "refs/worktree/main-own", "HEAD" }); try repo.git(&.{ "worktree", "add", "-q", "wt", "-b", "wt-branch" }); try repo.commit("two"); try repo.git(&.{ "-C", "wt", "update-ref", "refs/bisect/wt-good", "HEAD" }); try repo.git(&.{ "-C", "wt", "update-ref", "refs/rewritten/wt-own", "HEAD" }); for ([_]bool{ false, true }) |pack| { if (pack) try repo.git(&.{ "pack-refs", "--all" }); for ([_][]const u8{ ".", "wt" }) |where| { try expectAllSameAsGit(&repo, where, &.{ &.{"for-each-ref"}, &.{ "for-each-ref", "--format=%(HEAD) %(refname)" }, &.{ "show-ref", "--head" }, &.{ "show-ref", "--verify", "refs/bisect/wt-good" }, &.{ "show-ref", "--verify", "refs/bisect/main-good" }, &.{ "show-ref", "--exists", "main-worktree/HEAD" }, &.{ "show-ref", "--exists", "worktrees/wt/refs/bisect/wt-good" }, &.{ "symbolic-ref", "HEAD" }, &.{ "symbolic-ref", "main-worktree/HEAD" }, &.{ "symbolic-ref", "worktrees/wt/HEAD" }, }); } } }}
test "zig-git reads a reftable stack after many updates, deletions and compaction as git does" { const gpa = testing.allocator; var repo = try Repo.initWith(gpa, testing.io, .{ .ref_format = "reftable" }); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "config", "reftable.blockSize", "512" }); try repo.git(&.{ "tag", "-a", "-m", "t", "v1" }); for (0..30) |i| { var buf: [64]u8 = undefined; try repo.git(&.{ "branch", try std.fmt.bufPrint(&buf, "b{d}", .{i}) }); if (i % 3 == 0) try repo.git(&.{ "branch", "-D", "-q", try std.fmt.bufPrint(&buf, "b{d}", .{i / 2}) }); if (i % 5 == 0) try repo.commit("more"); } try repo.git(&.{ "symbolic-ref", "refs/heads/link", "refs/heads/b29" }); for ([_]bool{ false, true }) |compact| { if (compact) try repo.git(&.{"pack-refs"}); try expectAllSameAsGit(&repo, ".", &.{ &.{"for-each-ref"}, &.{ "for-each-ref", "--format=%(refname) %(*objectname) %(symref)" }, &.{ "show-ref", "-d", "--head" }, &.{ "show-ref", "--verify", "refs/heads/b0" }, &.{ "show-ref", "--verify", "refs/heads/b29" }, &.{ "symbolic-ref", "refs/heads/link" }, }); }}
test "zig-git refuses a corrupt reftable, where git reads it as empty" { const gpa = testing.allocator; var repo = try Repo.initWith(gpa, testing.io, .{ .ref_format = "reftable" }); defer repo.deinit(); try repo.commit("one"); try repo.git(&.{ "branch", "side" }); const list = try repo.tmp.dir.readFileAlloc(repo.io, ".git/reftable/tables.list", gpa, .unlimited); defer gpa.free(list); var lines = std.mem.tokenizeScalar(u8, list, '\n'); var last: []const u8 = ""; while (lines.next()) |l| last = l; const path = try std.fmt.allocPrint(gpa, ".git/reftable/{s}", .{last}); defer gpa.free(path); const good = try repo.tmp.dir.readFileAlloc(repo.io, path, gpa, .unlimited); defer gpa.free(good); const bad = try gpa.dupe(u8, good); defer gpa.free(bad); for ([_]usize{ 0, 4, bad.len - 1 }) |at| { @memcpy(bad, good); bad[at] ^= 0x5a; try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = bad }); for ([_][]const []const u8{ &.{"for-each-ref"}, &.{"show-ref"} }) |args| { const theirs = try repo.runIn(".", args, ""); defer theirs.deinit(gpa); const ours = try tool.runOurs(&repo, args, .{}); defer ours.deinit(gpa); // git prints nothing, and lists nothing; so does this, but it // says why and fails. try testing.expectEqualStrings(theirs.stdout, ours.stdout); try testing.expectEqualStrings("", ours.stdout); try testing.expectEqual(128, ours.code); try testing.expect(std.mem.indexOf(u8, ours.stderr, "reftable") != null); } } try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = good }); try tool.expectSameAsGitHow(&repo, &.{"for-each-ref"}, .{});}