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.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `zig-git rev-parse`: the questions `git rev-parse` answers about where//! a repository is, answered from the library's discovery, and//! revisions, answered by its `Resolver`, and sets of commits.//!//! zig-git rev-parse [--path-format=(absolute|relative)]//! (--git-dir | --absolute-git-dir | --git-common-dir |//! --show-toplevel | --show-prefix | --show-cdup |//! --is-inside-git-dir | --is-inside-work-tree |//! --is-bare-repository | --is-shallow-repository |//! --show-object-format[=(storage|input|output|compat)] |//! --show-ref-format | --local-env-vars)...//! zig-git rev-parse [--verify] [-q | --quiet] [--short[=<n>]]//! [--symbolic | --symbolic-full-name | --abbrev-ref[=(strict|loose)]]//! [--default <rev>] [--disambiguate=<prefix>]//! [--all | --branches[=<pattern>] | --tags[=<pattern>] |//! --remotes[=<pattern>] | --glob=<pattern> | --exclude=<pattern>]...//! [--end-of-options] [<rev>...] [--] [<path>...]//!//! Each prints what git prints, in the order given. As in git, the//! repository is looked for only when an option needs one, so//! `--local-env-vars` answers anywhere.//!//! A revision is anything gitrevisions(7) names a single object by.//! Options git does not know are passed through, printed, as git passes//! them to rev-list; an argument that is not a revision is a path, printed//! and then checked as git checks it — the first must be there or be a//! pathspec with wildcards or magic, or git explains why it is neither,//! and each after it must be there too. Ranges and the other sets of//! commits (`A..B`, `A...B`, `^A`, `A^@`, `A^!`, `A^-<n>`) are printed as//! the revisions they stand for, each excluded one with `^`, as git//! prints them (`history.zig` takes them apart). `--not` turns the `^`//! of everything printed after it about, and `--revs-only` prints only//! revisions and the options git keeps for `rev-list` (`revListFlag`),//! passing over anything else, paths and what is neither path nor//! revision included, without a word. `-n` takes the next argument with//! it. The options git has and this does not (`--sq`, `--no-revs` and//! the like) are refused by name before anything is printed. Only//! a command that reads objects stops at a bad `core.abbrev` or//! `core.disambiguate`; git stops at one for the discovery queries too.//!//! git resolves a revision quietly and then again to explain why it//! failed, so what it says on the way — warnings, and errors such as a//! peel to the wrong kind — it says twice when a revision fails without//! `--verify`, and this does the same.//!//! How git prints each path, found by asking it from every kind of//! directory (`discover.Location` keeps the forms these start from)://!//! - `--git-dir` prints the git directory as git keeps it, except that a//! relative one is printed resolved when git has moved to the top of//! the working tree. `--git-common-dir` prints the common directory as//! git keeps it, with `../` for each directory git moved up.//! - `--show-cdup` prints nothing at all without a working tree, and the//! working tree's path when the start is outside it.//! - `--path-format=absolute` resolves symbolic links where the path//! exists. `--path-format=relative` makes a path relative to the start://! resolved first if git keeps it relative, as written if it keeps it//! absolute; except `--git-dir` after git has moved, which is the//! relative git directory with `../` before it.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const discover = git.discover;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 pathspecs = @import("pathspecs.zig");const history = @import("history.zig");const FullName = git.Resolver.FullName;const Diagnostics = git.Diagnostics;
const local_env_vars = [_][]const u8{ "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CONFIG", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT", "GIT_OBJECT_DIRECTORY", "GIT_DIR", "GIT_WORK_TREE", "GIT_IMPLICIT_WORK_TREE", "GIT_GRAFT_FILE", "GIT_INDEX_FILE", "GIT_NO_REPLACE_OBJECTS", "GIT_REPLACE_REF_BASE", "GIT_PREFIX", "GIT_SHALLOW_FILE", "GIT_COMMON_DIR",};
const Query = enum { @"--git-dir", @"--absolute-git-dir", @"--git-common-dir", @"--show-toplevel", @"--show-prefix", @"--show-cdup", @"--is-inside-git-dir", @"--is-inside-work-tree", @"--is-bare-repository", @"--is-shallow-repository", @"--show-object-format", @"--show-ref-format", @"--local-env-vars", @"--path-format",};
const PathFormat = enum { default, absolute, relative };
/// The options about revisions and object IDs.const IdOption = enum { @"--verify", @"-q", @"--quiet", @"--short", @"--disambiguate", @"--symbolic", @"--symbolic-full-name", @"--abbrev-ref", @"--default", @"--all", @"--branches", @"--tags", @"--remotes", @"--glob", @"--exclude",};
fn idOption(arg: []const u8) ?IdOption { const eq = std.mem.findScalar(u8, arg, '='); const o = std.meta.stringToEnum(IdOption, arg[0 .. eq orelse arg.len]) orelse return null; switch (o) { .@"--short", .@"--abbrev-ref", .@"--branches", .@"--tags", .@"--remotes" => {}, .@"--disambiguate", .@"--glob", .@"--exclude" => if (eq == null) return null, else => if (eq != null) return null, } return o;}
/// The options git's rev-parse has and this does not, refused by name.const unsupported_options = [_][]const u8{ "--parseopt", "--sq-quote", "--keep-dashdash", "--stop-at-non-option", "--stuck-long", "--no-revs", "--flags", "--no-flags", "--prefix", "--sq", "--since", "--after", "--until", "--before", "--bisect", "--resolve-git-dir", "--git-path", "--show-superproject-working-tree", "--shared-index-path", "--output-object-format", "--exclude-hidden",};
fn isUnsupported(arg: []const u8) bool { const eq = std.mem.findScalar(u8, arg, '='); const name = arg[0 .. eq orelse arg.len]; for (unsupported_options) |u| if (std.mem.eql(u8, u, name)) return true; return false;}
/// Whether `--revs-only` prints the flag `arg`: one of those `rev-list`/// takes that git was seen to keep, among every option git-rev-list(1)/// and git-log(1) name. The others are dropped, `--first-parent` and/// `--reverse` among them.fn revListFlag(arg: []const u8) bool { const exact = [_][]const u8{ "--date-order", "--topo-order", "--dense", "--sparse", "--header", "--parents", "--pretty", "--no-merges", "--no-min-parents", "--no-max-parents", "--objects", "--objects-edge", "--unpacked", "--ignore-missing", }; for (exact) |e| if (std.mem.eql(u8, arg, e)) return true; const prefixes = [_][]const u8{ "--max-count=", "--max-age=", "--min-age=", "--max-parents=", "--min-parents=", "-n" }; for (prefixes) |p| if (std.mem.startsWith(u8, arg, p)) return true; // `-<n>`, as git counts it: any argument a digit begins after the // dash. return arg.len >= 2 and arg[0] == '-' and std.ascii.isDigit(arg[1]);}
/// Runs `rev-parse` with `args`, the arguments after its name.pub fn run(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { // What this does not answer is refused before anything is said. for (args) |arg| { if (std.mem.eql(u8, arg, "--") or std.mem.eql(u8, arg, "--end-of-options")) break; if (isUnsupported(arg)) return tool.fatalMessage(stderr, "zig-git rev-parse: '{s}' is not supported yet", .{arg}); if (idOption(arg)) |o| if (o == .@"--disambiguate") { if (arg.len - "--disambiguate=".len > git.object_max_hex_length) return tool.fatalMessage(stderr, "zig-git rev-parse: '{s}' is longer than any object ID", .{arg}); // git lists some of the objects a single digit begins, by no // rule found yet; its documentation asks for at least four. if (arg.len - "--disambiguate=".len == 1 and git.abbrev.isHex(arg["--disambiguate=".len..])) return tool.fatalMessage(stderr, "zig-git rev-parse: '{s}' is a single digit, which is not supported", .{arg}); }; } var state: State = .{ .ctx = ctx, .stdout = stdout, .stderr = stderr }; defer state.deinit(); var i: usize = 0; while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, arg, "--default") and !state.options_ended and !state.paths_only and !state.path_phase) { if (i + 1 == args.len) return tool.fatalMessage(stderr, "--default requires an argument", .{}); i += 1; state.default = args[i]; continue; } // `-n` takes the next argument with it, whatever it is, and both // are printed as they are. if (std.mem.eql(u8, arg, "-n") and !state.options_ended and !state.paths_only and !state.path_phase) { if (i + 1 == args.len) return tool.fatalMessage(stderr, "-n requires an argument", .{}); i += 1; if (!state.verify) try stdout.print("-n\n{s}\n", .{args[i]}); continue; } try state.answer(arg); } try state.finish();}
fn query(arg: []const u8) ?Query { const eq = std.mem.findScalar(u8, arg, '='); const name = arg[0 .. eq orelse arg.len]; const q = std.meta.stringToEnum(Query, name) orelse return null; if (eq != null and q != .@"--show-object-format" and q != .@"--path-format") return null; return q;}
/// How a revision is printed.const Output = enum { /// Its object ID. id, /// As written: `--symbolic`. symbolic, /// The full name of the ref: `--symbolic-full-name`. full_name, /// The ref's shortest unambiguous name: `--abbrev-ref`. abbrev_strict, abbrev_loose,};
const State = struct { ctx: Context, stdout: *Io.Writer, stderr: *Io.Writer, location: ?discover.Location = null, repo: ?git.Repository = null, names: ?object_names.Names = null, revs: ?revisions.Revisions = null, path_format: PathFormat = .default, /// `--verify`, or `--short`, which implies it. verify: bool = false, quiet: bool = false, /// `--short`: how many digits to shorten to, at least; 0 for /// core.abbrev's length. short: ?i64 = null, output: Output = .id, /// The revision `--verify` is to print, once it is known to be the /// only one, and how it was written. verified: ?git.ObjectId = null, verified_text: []const u8 = "", /// How many revisions have been given. revisions: usize = 0, /// `--default`'s revision, used if no other is given. default: ?[]const u8 = null, default_done: bool = false, /// After `--end-of-options`, nothing is an option. options_ended: bool = false, /// After `--`, everything is a path. paths_only: bool = false, /// After the first path, everything is one. path_phase: bool = false, /// `--revs-only`: only revisions and the options `rev-list` is /// given are printed. revs_only: bool = false, /// `--not`, an odd number of times: each revision printed after it is /// printed with `^` if it would be without, and without if with. not: bool = false, /// `--exclude` patterns waiting for the next listing option. excludes: std.ArrayList([]const u8) = .empty, /// The commits, for the sets of them that need commits read. hist: ?history.History = null, arena: ?std.heap.ArenaAllocator = null, /// Whether what `--verify` verified was `^<rev>`, printed with its /// `^`. verified_caret: bool = false,
fn deinit(s: *State) void { s.excludes.deinit(s.ctx.gpa); if (s.hist) |*h| h.close(); if (s.arena) |*a| a.deinit(); if (s.revs) |*r| r.deinit(); if (s.names) |*n| n.deinit(); if (s.repo) |*r| r.close(s.ctx.io); if (s.location) |*l| l.deinit(s.ctx.gpa); }
/// Where the repository is, for the discovery queries, which open /// nothing in it. fn repository(s: *State) !*const discover.Location { if (s.location == null) s.location = try discovery.locate(s.ctx, s.stderr); return &s.location.?; }
/// The repository opened, for its objects. fn objectNames(s: *State) !*object_names.Names { if (s.names == null) { s.repo = try discovery.open(s.ctx, s.stderr); s.names = .init(s.ctx, &s.repo.?, try object_names.settings(&s.repo.?.config, s.stderr)); } return &s.names.?; }
/// The resolver, for revisions. fn resolver(s: *State) !*revisions.Revisions { const names = try s.objectNames(); if (s.revs == null) { s.revs = try revisions.Revisions.init(s.ctx, &s.repo.?, names.settings, .rev_parse, s.stderr); // Sets of commits are taken apart before a text is resolved, so // a text that reaches the resolver is a single revision. s.revs.?.resolver.options.sets = false; } return &s.revs.?; }
fn answer(s: *State, arg: []const u8) !void { if (s.paths_only) { if (!s.verify and !s.revs_only) try s.stdout.print("{s}\n", .{arg}); return; } const is_option = !s.options_ended and !s.path_phase and arg.len > 0 and arg[0] == '-'; if (is_option) { if (std.mem.eql(u8, arg, "--")) { try s.applyDefault(); s.paths_only = true; if (!s.verify and !s.revs_only) try s.stdout.writeAll("--\n"); return; } if (std.mem.eql(u8, arg, "--end-of-options")) { s.options_ended = true; if (!s.verify and !s.revs_only) try s.stdout.print("{s}\n", .{arg}); return; } if (std.mem.eql(u8, arg, "--revs-only")) { s.revs_only = true; return; } if (std.mem.eql(u8, arg, "--not")) { s.not = !s.not; return; } if (query(arg) != null) return s.answerQuery(arg); if (idOption(arg)) |o| return s.answerOption(o, arg); // Anything else is a flag for rev-list, which git passes on; // under --revs-only, only those git knows rev-list takes. if (!s.verify and (!s.revs_only or revListFlag(arg))) try s.stdout.print("{s}\n", .{arg}); return; } if (s.path_phase) return s.asPath(arg, null, false); return s.revision(arg); }
fn answerQuery(s: *State, arg: []const u8) !void { const gpa = s.ctx.gpa; const out = s.stdout; const q = query(arg).?; if (q == .@"--local-env-vars") { for (local_env_vars) |v| try out.print("{s}\n", .{v}); return; } const l = try s.repository(); switch (q) { .@"--local-env-vars" => unreachable, .@"--path-format" => { const eq = std.mem.findScalar(u8, arg, '=') orelse return tool.fatalMessage(s.stderr, "--path-format requires an argument", .{}); const v = arg[eq + 1 ..]; s.path_format = if (std.mem.eql(u8, v, "absolute")) .absolute else if (std.mem.eql(u8, v, "relative")) .relative else return tool.fatalMessage(s.stderr, "unknown argument to --path-format: {s}", .{v}); }, .@"--git-dir" => { const relative_moved = !std.fs.path.isAbsolute(l.git_dir) and l.prefix != null and l.prefix.?.len != 0; switch (s.path_format) { .default => try out.print("{s}\n", .{if (relative_moved) l.git_dir_real else l.git_dir}), .absolute => try s.printAbsolute(l, l.git_dir), .relative => if (relative_moved) { try s.printCdup(l); try out.print("{s}\n", .{l.git_dir}); } else try s.printRelative(l, l.git_dir), } }, .@"--absolute-git-dir" => try out.print("{s}\n", .{l.git_dir_real}), .@"--git-common-dir" => switch (s.path_format) { .default => { if (!std.fs.path.isAbsolute(l.common_dir)) try s.printCdup(l); try out.print("{s}\n", .{l.common_dir}); }, .absolute => try s.printAbsolute(l, l.common_dir), .relative => try s.printRelative(l, l.common_dir), }, .@"--show-toplevel" => { const top = l.work_tree orelse return tool.fatalMessage(s.stderr, "this operation must be run in a work tree", .{}); if (s.path_format == .relative) { const rel = try discover.relativePath(gpa, top, l.start); defer gpa.free(rel); try out.print("{s}\n", .{rel}); } else try out.print("{s}\n", .{top}); }, .@"--show-prefix" => try out.print("{s}\n", .{l.prefix orelse ""}), .@"--show-cdup" => if (l.prefix != null) { try s.printCdup(l); try out.writeAll("\n"); } else if (l.work_tree) |top| try out.print("{s}\n", .{top}), .@"--is-inside-git-dir" => try out.print("{}\n", .{l.inside_git_dir}), .@"--is-inside-work-tree" => try out.print("{}\n", .{l.prefix != null}), .@"--is-bare-repository" => try out.print("{}\n", .{l.bare}), .@"--is-shallow-repository" => try out.print("{}\n", .{try isShallow(s.ctx, l, s.stderr)}), .@"--show-object-format" => { const mode = if (std.mem.findScalar(u8, arg, '=')) |eq| arg[eq + 1 ..] else "storage"; if (std.mem.eql(u8, mode, "compat")) { try out.writeAll("\n"); } else if (std.mem.eql(u8, mode, "storage") or std.mem.eql(u8, mode, "input") or std.mem.eql(u8, mode, "output")) { try out.print("{s}\n", .{l.format.object_format.name()}); } else return tool.fatalMessage(s.stderr, "unknown mode for --show-object-format: {s}", .{mode}); }, .@"--show-ref-format" => try out.print("{s}\n", .{l.format.ref_storage.name()}), } }
fn answerOption(s: *State, o: IdOption, arg: []const u8) !void { const value: ?[]const u8 = if (std.mem.findScalar(u8, arg, '=')) |eq| arg[eq + 1 ..] else null; switch (o) { .@"--verify" => s.verify = true, .@"-q", .@"--quiet" => s.quiet = true, .@"--short" => { s.verify = true; s.short = if (value) |v| @max(git.abbrev.min_length, object_names.leadingNumber(v)) else 0; }, .@"--symbolic" => s.output = .symbolic, .@"--symbolic-full-name" => s.output = .full_name, .@"--abbrev-ref" => { const v = value orelse ""; if (value == null) { const names = try s.objectNames(); _ = names; var diag: Diagnostics = .{}; const warn = s.repo.?.boolSetting("core.warnAmbiguousRefs", &diag) catch true; s.output = if (warn orelse true) .abbrev_strict else .abbrev_loose; } else if (std.mem.eql(u8, v, "strict")) { s.output = .abbrev_strict; } else if (std.mem.eql(u8, v, "loose")) { s.output = .abbrev_loose; } else return tool.fatalMessage(s.stderr, "unknown mode for --abbrev-ref: {s}", .{v}); }, .@"--disambiguate" => { const names = try s.objectNames(); const p = value.?; const prefix = git.abbrev.Prefix.parse(names.repo.objectFormat(), p) catch return; var ids: std.ArrayList(git.ObjectId) = .empty; defer ids.deinit(s.ctx.gpa); names.lookup.findAll(s.ctx.io, prefix, &ids) catch |err| return tool.fatalMessage(s.stderr, "zig-git: cannot list the objects '{s}' names: {t}", .{ p, err }); for (ids.items) |id| try s.stdout.print("{f}\n", .{id}); }, .@"--default" => unreachable, .@"--exclude" => try s.excludes.append(s.ctx.gpa, value.?), .@"--all", .@"--branches", .@"--tags", .@"--remotes", .@"--glob" => try s.list(o, value), } }
/// `--all`, `--branches`, `--tags`, `--remotes` and `--glob`: the refs /// they choose, less the `--exclude`s before them. fn list(s: *State, o: IdOption, value: ?[]const u8) !void { const gpa = s.ctx.gpa; defer s.excludes.clearRetainingCapacity(); const names = try s.objectNames(); const store = s.repo.?.refs(); const strip: []const u8 = switch (o) { .@"--branches" => "refs/heads/", .@"--tags" => "refs/tags/", .@"--remotes" => "refs/remotes/", else => "", }; var pattern: ?[]u8 = null; defer if (pattern) |p| gpa.free(p); if (o == .@"--glob" or value != null) { const v = value.?; const base = if (o == .@"--glob") (if (std.mem.startsWith(u8, v, "refs/")) "" else "refs/") else strip; const globbing = std.mem.findAny(u8, v, "*?[") != null; pattern = try std.mem.concat(gpa, u8, &.{ base, v, if (globbing) "" else "/*" }); } var diag: Diagnostics = .{}; var refs = store.list(s.ctx.io, gpa, if (strip.len > 0) strip else "refs/", &diag) catch |err| return tool.fatalMessage(s.stderr, "zig-git: cannot list refs: {t}", .{err}); defer refs.deinit(); for (refs.entries) |e| { if (pattern) |p| if (!git.wildmatch.match(p, e.name, .{})) continue; const excluded = for (s.excludes.items) |x| { if (git.wildmatch.match(x, e.name[strip.len..], .{})) break true; } else false; if (excluded) continue; var res = store.resolve(s.ctx.io, gpa, e.name, &diag) catch continue; defer res.deinit(gpa); const id = switch (res.outcome) { .direct => |d| d.id, else => continue, }; if (s.not) try s.stdout.writeByte('^'); switch (s.output) { .id => { const length: ?usize = if (s.short) |n| s.shortLength(names, n, id) else null; try names.write(s.stdout, id, length); try s.stdout.writeByte('\n'); }, // --symbolic names the ref as the option chose it. .symbolic => try s.stdout.print("{s}\n", .{e.name[strip.len..]}), // --symbolic-full-name and --abbrev-ref name where a // symbolic ref leads. .full_name => try s.stdout.print("{s}\n", .{res.last()}), .abbrev_strict, .abbrev_loose => { const short = store.shorten(s.ctx.io, gpa, res.last(), s.output == .abbrev_strict, &diag) catch res.last(); try s.stdout.print("{s}\n", .{short}); }, } } }
fn shortLength(s: *State, names: *object_names.Names, n: i64, id: git.ObjectId) usize { _ = s; if (n == 0) return names.defaultLength(); return @min(@as(usize, @intCast(@min(n, git.object_max_hex_length))), id.object_format.hexLength()); }
/// Resolves `--default`'s revision, if no other was given. fn applyDefault(s: *State) !void { if (s.default_done) return; s.default_done = true; const d = s.default orelse return; if (s.revisions > 0) return; const revs = try s.resolver(); var diag: Diagnostics = .{}; const found = revs.resolver.resolve(d, &diag) catch { revs.resolver.clearMessages(); return; }; try revs.flushMessages(s.stderr, 1, s.quiet and s.verify); try s.emit(d, found.id); }
fn history_(s: *State) !*history.History { if (s.hist == null) s.hist = try history.History.open(s.ctx, &s.repo.?, s.stderr); return &s.hist.?; }
/// A set of commits, `A..B` and the like, printed as the revisions it /// stands for, each excluded one with `^`, as git prints it; false if /// git takes `arg` for a single revision. fn set(s: *State, arg: []const u8) !bool { const kind = git.revision.parseSet(arg) orelse return false; const revs = try s.resolver(); const h = try s.history_(); if (s.arena == null) s.arena = .init(s.ctx.gpa); var parts: std.ArrayList(history.Part) = .empty; defer parts.deinit(s.ctx.gpa); var diag: Diagnostics = .{}; _ = try history.expandSet(revs, h, s.arena.?.allocator(), arg, &parts, &diag, s.stderr) orelse return false; try h.report(s.stderr); // `^<rev>` is one revision to --verify; the other sets are not. if (s.verify and kind == .exclude) { s.verified = parts.items[0].id; s.verified_text = parts.items[0].symbolic; s.verified_caret = true; return true; } // git prints the right side of a symmetric difference first. for (parts.items) |p| if (p.side == .right) try s.printPart(p); for (parts.items) |p| if (p.side != .right) try s.printPart(p); // `^<rev>` is one revision to --verify; the other sets are not. if (s.verify) return s.needOne(); return true; }
fn printPart(s: *State, p: history.Part) !void { const names = try s.objectNames(); const caret: []const u8 = if (p.excluded != s.not) "^" else ""; switch (s.output) { .id => { try s.stdout.writeAll(caret); const length: ?usize = if (s.short) |n| s.shortLength(names, n, p.id) else null; try names.write(s.stdout, p.id, length); try s.stdout.writeByte('\n'); }, .symbolic => try s.stdout.print("{s}{s}\n", .{ caret, p.symbolic }), .full_name, .abbrev_strict, .abbrev_loose => { if (p.merge_base) return s.stdout.print("{s}{f}\n", .{ caret, p.id }); const revs = try s.resolver(); var diag: Diagnostics = .{}; const full = revs.resolver.fullName(p.symbolic, &diag) catch FullName.none; revs.resolver.clearMessages(); switch (full) { .name => |n| if (s.output == .full_name) { try s.stdout.print("{s}{s}\n", .{ caret, n }); } else { const short = s.repo.?.refs().shorten(s.ctx.io, s.ctx.gpa, n, s.output == .abbrev_strict, &diag) catch n; try s.stdout.print("{s}{s}\n", .{ caret, short }); }, // A parent that is not a ref, --abbrev-ref names by // object ID; anything else that is not one goes unnamed. else => if (s.output != .full_name and p.derived) try s.stdout.print("{s}{f}\n", .{ caret, p.id }), } }, } }
/// A revision, or, if it is not one, a path. fn revision(s: *State, arg: []const u8) !void { const names = try s.objectNames(); if (s.verify and s.revisions > 0) return s.needOne(); if (try s.set(arg)) { s.revisions += 1; return; } const revs = try s.resolver(); var diag: Diagnostics = .{}; const found = revs.resolver.resolve(arg, &diag) catch |err| { const sev = git.Resolver.severity(err); if (sev != .not_found and sev != .diagnosed) { try revs.flushMessages(s.stderr, 1, s.quiet and s.verify); try revisions.stopAt(err, &diag, s.quiet and s.verify, revs.resolver.loud, s.stderr); unreachable; } if (s.verify) { try revs.flushMessages(s.stderr, 1, s.quiet and s.verify); if (err == error.AmbiguousObjectName and !s.quiet) try revs.complain(names, s.stderr); return s.needOne(); } // git tries once quietly and again to explain, saying what it // says on the way twice. try revs.flushMessages(s.stderr, 2, false); if (err == error.AmbiguousObjectName and !s.quiet) try revs.complain(names, s.stderr); return s.asPath(arg, if (sev == .diagnosed) diag.detail() else null, true); }; try revs.flushMessages(s.stderr, 1, s.quiet and s.verify); s.revisions += 1; try s.emit(arg, found.id); }
/// Prints a revision found, or keeps it for `--verify`. fn emit(s: *State, arg: []const u8, id: git.ObjectId) !void { if (s.verify) { s.verified = id; s.verified_text = arg; return; } try s.print(arg, id); }
fn print(s: *State, arg: []const u8, id: git.ObjectId) !void { const names = try s.objectNames(); const caret: []const u8 = if (s.not) "^" else ""; switch (s.output) { .id => { try s.stdout.writeAll(caret); const length: ?usize = if (s.short) |n| s.shortLength(names, n, id) else null; try names.write(s.stdout, id, length); try s.stdout.writeByte('\n'); }, .symbolic => try s.stdout.print("{s}{s}\n", .{ caret, arg }), .full_name, .abbrev_strict, .abbrev_loose => { const revs = try s.resolver(); var diag: Diagnostics = .{}; const full = revs.resolver.fullName(arg, &diag) catch |err| { try revisions.stopAt(err, &diag, false, false, s.stderr); return; }; switch (full) { .none => {}, .ambiguous => |t| try s.stderr.print("error: refname '{s}' is ambiguous\n", .{t}), .name => |n| if (s.output == .full_name) { try s.stdout.print("{s}{s}\n", .{ caret, n }); } else { const short = s.repo.?.refs().shorten(s.ctx.io, s.ctx.gpa, n, s.output == .abbrev_strict, &diag) catch n; try s.stdout.print("{s}{s}\n", .{ caret, short }); }, } }, } }
/// An argument taken for a path: printed, then checked as git checks /// it. `explained` is what git says of the revision it was not, and /// `first` whether it is the first argument taken for a path. fn asPath(s: *State, arg: []const u8, explained: ?[]const u8, first: bool) !void { if (first) try s.applyDefault(); // Under --revs-only an argument that is no revision is passed // over, checked or not, and so is everything after it. if (s.revs_only) { s.path_phase = true; return; } try s.stdout.print("{s}\n", .{arg}); if (try s.pathExists(arg)) { s.path_phase = true; return; } if (arg.len > 0 and arg[0] == '-') return tool.fatalMessage(s.stderr, "option '{s}' must come before non-option arguments", .{arg}); if (!first) { try s.stderr.print("fatal: {s}: no such path in the working tree.\nUse 'git <command> -- <path>...' to specify paths that do not exist locally.\n", .{arg}); return error.Fatal; } if (explained) |e| return tool.fatalMessage(s.stderr, "{s}", .{e}); try s.stderr.print( \\fatal: ambiguous argument '{s}': unknown revision or path not in the working tree. \\Use '--' to separate paths from revisions, like this: \\'git <command> [<revision>...] -- [<file>...]' \\ , .{arg}); return error.Fatal; }
/// Whether git takes `arg` for a path: one that is there, or a /// pathspec with wildcards or magic that need not be. fn pathExists(s: *State, arg: []const u8) !bool { return pathspecs.takenForPath(s.ctx, &s.repo.?, arg); }
/// `--verify` without exactly one revision it can name. fn needOne(s: *State) anyerror { if (s.quiet) return error.No; return tool.fatalMessage(s.stderr, "Needed a single revision", .{}); }
/// Prints what `--verify` verified. fn finish(s: *State) !void { try s.applyDefault(); if (!s.verify) return; const id = s.verified orelse return s.needOne(); // `^<rev>` keeps its `^`, which --not takes away. const saved = s.not; defer s.not = saved; s.not = s.verified_caret != s.not; try s.print(s.verified_text, id); }
/// `../` for each directory git moved up from the start. fn printCdup(s: *State, l: *const discover.Location) !void { const prefix = l.prefix orelse return; var it = std.mem.tokenizeScalar(u8, prefix, '/'); while (it.next()) |_| try s.stdout.writeAll("../"); }
/// `path`, kept relative to the directory git works from or absolute, /// resolved where it exists. fn absolute(s: *State, l: *const discover.Location, path: []const u8) ![]u8 { const gpa = s.ctx.gpa; const joined = try discover.normalize(gpa, l.work_dir, path); var buffer: [std.fs.max_path_bytes]u8 = undefined; const n = Io.Dir.cwd().realPathFile(s.ctx.io, joined, &buffer) catch return joined; gpa.free(joined); return gpa.dupe(u8, buffer[0..n]); }
fn printAbsolute(s: *State, l: *const discover.Location, path: []const u8) !void { const abs = try s.absolute(l, path); defer s.ctx.gpa.free(abs); try s.stdout.print("{s}\n", .{abs}); }
fn printRelative(s: *State, l: *const discover.Location, path: []const u8) !void { const gpa = s.ctx.gpa; const abs = if (std.fs.path.isAbsolute(path)) try discover.normalize(gpa, "/", path) else try s.absolute(l, path); defer gpa.free(abs); const rel = try discover.relativePath(gpa, abs, l.start); defer gpa.free(rel); try s.stdout.print("{s}\n", .{rel}); }};
/// Whether the repository is shallow: `GIT_SHALLOW_FILE`, or `shallow` in/// the common directory, is a file; which git reads, and stops at a bad/// line of.fn isShallow(ctx: Context, l: *const discover.Location, stderr: *Io.Writer) !bool { const gpa = ctx.gpa; const path = if (ctx.env.get("GIT_SHALLOW_FILE")) |f| try discover.normalize(gpa, l.work_dir, f) else try std.fs.path.join(gpa, &.{ l.common_dir_real, "shallow" }); defer gpa.free(path); const st = Io.Dir.cwd().statFile(ctx.io, path, .{}) catch return false; if (st.kind != .file) return false; const bytes = Io.Dir.cwd().readFileAlloc(ctx.io, path, gpa, .unlimited) catch return true; defer gpa.free(bytes); var g: git.Grafts = .init(gpa, l.format.object_format); defer g.deinit(); var diag: git.Diagnostics = .{}; g.parseShallow(bytes, &diag) catch |err| switch (err) { error.BadShallowLine => return tool.fatalMessage(stderr, "bad shallow line: {s}", .{diag.detail()}), else => |e| return e, }; return true;}
// -- tests -----------------------------------------------------------------------
const testing = std.testing;const Repo = tool.Repo;
/// Every query, each asked on its own so that one failing does not hide/// the rest, then all together under each path format.fn expectAllSame(repo: *Repo, cwd: []const u8) !void { for (std.enums.values(Query)) |q| { if (q == .@"--path-format") continue; try tool.expectSameAsGitHow(repo, &.{ "rev-parse", @tagName(q) }, .{ .cwd = cwd }); } for ([_][]const u8{ "--show-object-format=storage", "--show-object-format=input", "--show-object-format=output", "--show-object-format=compat", "--show-object-format=bogus", "--path-format=bogus", "--path-format" }) |a| { try tool.expectSameAsGitHow(repo, &.{ "rev-parse", a }, .{ .cwd = cwd }); } for ([_][]const u8{ "--path-format=absolute", "--path-format=relative" }) |f| { try tool.expectSameAsGitHow(repo, &.{ "rev-parse", f, "--git-dir", "--git-common-dir", "--show-toplevel" }, .{ .cwd = cwd }); try tool.expectSameAsGitHow(repo, &.{ "rev-parse", f, "--git-dir", "--git-common-dir" }, .{ .cwd = cwd }); }}
/// The answers to the queries that hold whatever the working tree.fn expectSame(repo: *Repo, cwd: []const u8, args: []const []const u8) !void { try tool.expectSameAsGitHow(repo, args, .{ .cwd = cwd });}
fn mkdir(repo: *Repo, path: []const u8) !void { try repo.tmp.dir.createDirPath(repo.io, path);}
fn write(repo: *Repo, path: []const u8, contents: []const u8) !void { if (std.fs.path.dirname(path)) |d| try mkdir(repo, d); try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = contents });}
/// An object ID, in the repository's format, of no object.fn missingId(repo: *const Repo) []const u8 { const zeros = "0000000000000000000000000000000000000000000000000000000000000000"; return zeros[0..repo.object_format.hexLength()];}
fn inRepo(repo: *Repo, rest: []const u8) ![]u8 { return std.fs.path.join(repo.gpa, &.{ repo.path, rest });}
const discovery_queries: []const []const u8 = &.{ "rev-parse", "--git-dir", "--git-common-dir", "--is-inside-work-tree", "--show-prefix", "--show-cdup", "--is-bare-repository", "--show-object-format" };
test "rev-parse: the working tree, its subdirectories and its git directory" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try mkdir(&repo, "a/b"); try repo.commitFile("a/b/f", "x\n", "first"); for ([_][]const u8{ ".", "a", "a/b", ".git", ".git/objects", ".git/refs/heads" }) |cwd| try expectAllSame(&repo, cwd); // Another repository inside it is its own. try repo.git(&.{ "init", "-q", "a/inner" }); try mkdir(&repo, "a/inner/x"); try expectAllSame(&repo, "a/inner/x"); // As are the bare repositories inside it. try repo.git(&.{ "init", "-q", "--bare", "a/bare.git" }); try repo.git(&.{ "init", "-q", "--bare", "a/plain" }); for ([_][]const u8{ "a/bare.git", "a/bare.git/refs", "a/plain" }) |cwd| try expectAllSame(&repo, cwd); // Shallow is a file in the common directory. try write(&repo, ".git/shallow", ""); try expectSame(&repo, "a", &.{ "rev-parse", "--is-shallow-repository" }); // Revisions and the discovery queries together, in order. try expectSame(&repo, "a", &.{ "rev-parse", "--git-dir", "HEAD", "--show-prefix", "HEAD:./b/f" }); // An option this does not have is refused by name, before anything // is said. const ours = try tool.runOurs(&repo, &.{ "rev-parse", "--git-dir", "--sq", "HEAD" }, .{}); defer ours.deinit(testing.allocator); try testing.expectEqualStrings("", ours.stdout); try testing.expectEqual(128, ours.code); try testing.expect(std.mem.find(u8, ours.stderr, "'--sq'") != null); // --local-env-vars needs no repository, and the rest wait for one. try expectSame(&repo, ".", &.{ "rev-parse", "--local-env-vars", "--git-dir", "--local-env-vars" });}
test "rev-parse: .git files, linked worktrees and submodules" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try repo.commitFile("f", "x\n", "first"); try repo.git(&.{ "worktree", "add", "-q", "linked" }); try mkdir(&repo, "linked/a"); for ([_][]const u8{ "linked", "linked/a", ".git/worktrees/linked" }) |cwd| try expectAllSame(&repo, cwd); // A linked worktree's common configuration does not make it bare... try repo.git(&.{ "config", "core.bare", "true" }); try expectAllSame(&repo, "linked/a"); // ...unless it is shared with extensions.worktreeConfig. try repo.git(&.{ "config", "extensions.worktreeConfig", "true" }); try expectAllSame(&repo, "linked/a"); try repo.git(&.{ "config", "core.bare", "false" }); try write(&repo, ".git/worktrees/linked/config.worktree", "[core]\n\tbare = true\n"); try expectAllSame(&repo, "linked/a"); try repo.tmp.dir.deleteFile(repo.io, ".git/worktrees/linked/config.worktree");
// A submodule's .git file names its git directory relatively. const source = try inRepo(&repo, "subsource"); defer testing.allocator.free(source); try repo.git(&.{ "init", "-q", "subsource" }); try repo.git(&.{ "-C", "subsource", "commit", "-q", "--allow-empty", "-m", "sub" }); try repo.git(&.{ "-c", "protocol.file.allow=always", "submodule", "add", "-q", source, "sub" }); try mkdir(&repo, "sub/x"); for ([_][]const u8{ "sub", "sub/x", ".git/modules/sub", ".git/modules/sub/refs" }) |cwd| try expectAllSame(&repo, cwd);
// .git files written by hand: relative, absolute, and wrong. try repo.git(&.{ "init", "-q", "--bare", "store.git" }); const store = try inRepo(&repo, "store.git"); defer testing.allocator.free(store); const absolute = try std.fmt.allocPrint(testing.allocator, "gitdir: {s}\n", .{store}); defer testing.allocator.free(absolute); const cases = [_][]const u8{ "gitdir: ../store.git\n", "gitdir: ../store.git\r\n", absolute, "gitdir:../store.git\n", "gitdir: ../store.git \n", "gitdir: \n", "junk\n", "", "gitdir: ../nowhere\n", "gitdir: ../g1\n" }; for (cases, 0..) |contents, i| { var name_buffer: [32]u8 = undefined; const dir = try std.fmt.bufPrint(&name_buffer, "g{d}", .{i}); var path_buffer: [64]u8 = undefined; try write(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/.git", .{dir}), contents); try mkdir(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/sub", .{dir})); try expectSame(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/sub", .{dir}), discovery_queries); try expectSame(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/sub", .{dir}), &.{ "rev-parse", "--show-toplevel" }); }}
test "rev-parse: GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR and core.worktree" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try mkdir(&repo, "a/b"); try repo.commitFile("a/b/f", "x\n", "first"); try repo.git(&.{ "worktree", "add", "-q", "linked" }); const top = try inRepo(&repo, "."); defer testing.allocator.free(top); const abs_git = try inRepo(&repo, ".git"); defer testing.allocator.free(abs_git); const abs_b = try inRepo(&repo, "a/b"); defer testing.allocator.free(abs_b); const Case = struct { cwd: []const u8, git_dir: ?[]const u8 = null, work_tree: ?[]const u8 = null, common_dir: ?[]const u8 = null }; const cases = [_]Case{ .{ .cwd = ".", .git_dir = ".git" }, .{ .cwd = "a", .git_dir = "../.git" }, .{ .cwd = "a", .git_dir = abs_git }, .{ .cwd = "a", .git_dir = "../.git", .work_tree = ".." }, .{ .cwd = "a/b", .git_dir = "../../.git", .work_tree = ".." }, .{ .cwd = ".", .git_dir = ".git", .work_tree = "a" }, .{ .cwd = "a", .git_dir = abs_git, .work_tree = abs_b }, .{ .cwd = "a", .work_tree = ".." }, .{ .cwd = "a", .work_tree = "." }, .{ .cwd = "a", .work_tree = "nowhere" }, .{ .cwd = "a", .work_tree = "" }, .{ .cwd = "a", .git_dir = "../.git/worktrees/linked", .common_dir = "../.git" }, .{ .cwd = "a", .common_dir = "../.git" }, .{ .cwd = "a", .git_dir = "nowhere" }, .{ .cwd = "a", .git_dir = "" }, .{ .cwd = "a", .git_dir = "b" }, .{ .cwd = ".", .git_dir = ".git/" }, .{ .cwd = ".", .git_dir = "./.git" }, }; for (cases) |c| { errdefer std.debug.print("GIT_DIR={?s} GIT_WORK_TREE={?s} GIT_COMMON_DIR={?s}\n", .{ c.git_dir, c.work_tree, c.common_dir }); if (c.git_dir) |v| try repo.env.put("GIT_DIR", v); if (c.work_tree) |v| try repo.env.put("GIT_WORK_TREE", v); if (c.common_dir) |v| try repo.env.put("GIT_COMMON_DIR", v); defer { _ = repo.env.orderedRemove("GIT_DIR"); _ = repo.env.orderedRemove("GIT_WORK_TREE"); _ = repo.env.orderedRemove("GIT_COMMON_DIR"); } try expectAllSame(&repo, c.cwd); } // The global options stand for the variables. try expectSame(&repo, "a", &.{ "--git-dir=../.git", "--work-tree", "..", "rev-parse", "--show-prefix", "--git-dir" }); try expectSame(&repo, ".", &.{ "-C", "a", "-C", "b", "rev-parse", "--show-prefix", "--show-cdup" }); try tool.expectSameAsGitHow(&repo, &.{ "-C", "nowhere", "rev-parse" }, .{});
// core.worktree, relative to the git directory. try mkdir(&repo, "elsewhere/x"); try repo.git(&.{ "config", "core.worktree", "../elsewhere" }); for ([_][]const u8{ ".", "a", ".git", "elsewhere/x" }) |cwd| try expectAllSame(&repo, cwd); try repo.env.put("GIT_DIR", "../../.git"); try expectAllSame(&repo, "elsewhere/x"); _ = repo.env.orderedRemove("GIT_DIR"); // With core.bare too, git warns and has no working tree. try repo.git(&.{ "config", "core.bare", "true" }); try expectAllSame(&repo, "a"); try repo.git(&.{ "config", "core.worktree", "../nowhere" }); try repo.git(&.{ "config", "core.bare", "false" }); try expectAllSame(&repo, "a");}
test "rev-parse: ceilings and symbolic links" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try mkdir(&repo, "a/b/c"); try mkdir(&repo, "ab/c"); try repo.tmp.dir.symLink(repo.io, "a", "link", .{ .is_directory = true }); const at = struct { fn f(r: *Repo, p: []const u8) ![]u8 { return std.fs.path.join(r.gpa, &.{ r.path, p }); } }.f; const paths = [_][]const u8{ "a", "a/", "a//", "a/.", "a/b/..", "link", "link/", "nowhere/../a", "a/b", "a/b/c", "ab", "" }; for (paths) |p| for ([_][]const u8{ "", ":", "x:" }) |before| { const abs = try at(&repo, p); defer testing.allocator.free(abs); const value = try std.mem.concat(testing.allocator, u8, &.{ before, abs }); defer testing.allocator.free(value); try repo.env.put("GIT_CEILING_DIRECTORIES", value); errdefer std.debug.print("GIT_CEILING_DIRECTORIES={s}\n", .{value}); try expectSame(&repo, "a/b/c", &.{ "rev-parse", "--show-toplevel" }); try expectSame(&repo, "ab/c", &.{ "rev-parse", "--show-toplevel" }); }; for ([_][]const u8{ "a", "..", "relative:also" }) |v| { try repo.env.put("GIT_CEILING_DIRECTORIES", v); try expectSame(&repo, "a/b/c", &.{ "rev-parse", "--show-toplevel" }); } _ = repo.env.orderedRemove("GIT_CEILING_DIRECTORIES"); try repo.env.put("GIT_DISCOVERY_ACROSS_FILESYSTEM", "bogus"); try expectSame(&repo, "a", &.{ "rev-parse", "--git-dir" }); try repo.env.put("GIT_DISCOVERY_ACROSS_FILESYSTEM", "yes"); try expectSame(&repo, "a", &.{ "rev-parse", "--git-dir" }); _ = repo.env.orderedRemove("GIT_DISCOVERY_ACROSS_FILESYSTEM");
// Started through a symbolic link, git sees where it leads. for ([_][]const u8{ "link", "link/b" }) |cwd| try expectAllSame(&repo, cwd); // A .git that is a symbolic link to a git directory. try repo.git(&.{ "init", "-q", "--bare", "store.git" }); try repo.git(&.{ "-C", "store.git", "config", "core.bare", "false" }); try mkdir(&repo, "wt/x"); try repo.tmp.dir.symLink(repo.io, "../store.git", "wt/.git", .{ .is_directory = true }); for ([_][]const u8{ "wt", "wt/x", "store.git", "wt/.git" }) |cwd| try expectAllSame(&repo, cwd); try repo.env.put("GIT_DIR", "../link/../.git"); try expectAllSame(&repo, "wt"); _ = repo.env.orderedRemove("GIT_DIR");
// Outside any repository, every command says so as git does: the // ceiling stops the walk before the fixture's own repository. try mkdir(&repo, "a/plain/x"); const ceiling = try at(&repo, "a"); defer testing.allocator.free(ceiling); try repo.env.put("GIT_CEILING_DIRECTORIES", ceiling); for ([_][]const []const u8{ &.{ "rev-parse", "--git-dir" }, &.{ "rev-parse", "--local-env-vars" }, &.{ "cat-file", "-t", missingId(&repo) }, &.{ "config", "--local", "--list" }, }) |args| try expectSame(&repo, "a/plain/x", args);}
test "rev-parse: what makes a git directory, and the formats git accepts" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try repo.commitFile("f", "x\n", "first"); // Directories that look like git directories, each missing a piece: // the walk passes over them to the fixture's own. const Fake = struct { name: []const u8, head: ?[]const u8 = "ref: refs/heads/main\n", objects: bool = true, refs: bool = true }; const zeros = "0000000000000000000000000000000000000000"; const fakes = [_]Fake{ .{ .name = "complete" }, .{ .name = "no-head", .head = null }, .{ .name = "no-objects", .objects = false }, .{ .name = "no-refs", .refs = false }, .{ .name = "head-junk", .head = "junk\n" }, .{ .name = "head-ref-foo", .head = "ref: foo\n" }, .{ .name = "head-ref-refs", .head = "ref: refs" }, .{ .name = "head-ref-tab", .head = "ref:\trefs/x" }, .{ .name = "head-ref-vt", .head = "ref:\x0brefs/x" }, .{ .name = "head-id", .head = zeros ++ "\n" }, .{ .name = "head-id-short", .head = zeros[1..] ++ "\n" }, .{ .name = "head-id-junk", .head = zeros ++ "junk" }, .{ .name = "head-empty", .head = "" }, }; for (fakes) |f| { var buffer: [128]u8 = undefined; try mkdir(&repo, try std.fmt.bufPrint(&buffer, "{s}/sub", .{f.name})); if (f.head) |h| try write(&repo, try std.fmt.bufPrint(&buffer, "{s}/.git/HEAD", .{f.name}), h); if (f.objects) try mkdir(&repo, try std.fmt.bufPrint(&buffer, "{s}/.git/objects", .{f.name})); if (f.refs) try mkdir(&repo, try std.fmt.bufPrint(&buffer, "{s}/.git/refs", .{f.name})); errdefer std.debug.print("{s}\n", .{f.name}); try expectSame(&repo, try std.fmt.bufPrint(&buffer, "{s}/sub", .{f.name}), &.{ "rev-parse", "--show-toplevel" }); } try mkdir(&repo, "link-head/.git/objects"); try mkdir(&repo, "link-head/.git/refs"); try mkdir(&repo, "link-head/sub"); try repo.tmp.dir.symLink(repo.io, "refs/heads/main", "link-head/.git/HEAD", .{}); try expectSame(&repo, "link-head/sub", &.{ "rev-parse", "--show-toplevel" }); try mkdir(&repo, "objdir"); try repo.env.put("GIT_OBJECT_DIRECTORY", "../../objdir"); try expectSame(&repo, "no-objects/sub", &.{ "rev-parse", "--show-toplevel" }); _ = repo.env.orderedRemove("GIT_OBJECT_DIRECTORY");
// Formats, each in a repository of its own below the fixture's. const object_format = repo.object_format.name(); const formats = [_][]const u8{ "[core]\n\trepositoryformatversion = 0\n[extensions]\n\tunknown = x\n\tnoop\n\tpreciousObjects\n\tworktreeConfig = false\n\tpartialClone = origin\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tunknown = x\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tunknown = x\n\tSecond\n[extensions \"Sub\"]\n\tthird\n", "[core]\n\trepositoryformatversion = 0\n[extensions]\n\tobjectFormat = sha1\n", "[core]\n\trepositoryformatversion = 0\n[extensions]\n\tnoop-v1\n\trefStorage = files\n", "[core]\n\trepositoryformatversion = 2\n", "[core]\n\trepositoryformatversion = junk\n", "[core]\n\trepositoryformatversion = 99999999999\n", "[core]\n\trepositoryformatversion = -1\n[extensions]\n\tunknown\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tnoop\n\tnoop-v1\n\tpreciousObjects = true\n\tpartialClone = origin\n\tworktreeConfig\n\trelativeWorktrees\n\tsubmodulePathConfig = false\n\trefStorage = files\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectFormat = sha256\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectFormat = md5\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectFormat\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = bogus\n", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tpreciousObjects = maybe\n", "[core]\n\trepositoryformatversion = 1\n\tbare = maybe\n", "[core]\n\trepositoryformatversion = 1\n\tworktree\n", "[core]\n\trepositoryformatversion = 1\n[bad\n", }; _ = object_format; for (formats, 0..) |config, i| { var buffer: [128]u8 = undefined; const dir = try std.fmt.bufPrint(&buffer, "format{d}", .{i}); try repo.git(&.{ "init", "-q", dir }); var path_buffer: [128]u8 = undefined; try write(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/.git/config", .{dir}), config); try mkdir(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/sub", .{dir})); errdefer std.debug.print("{s}\n", .{config}); try expectSame(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/sub", .{dir}), &.{ "rev-parse", "--show-toplevel", "--show-object-format", "--show-ref-format", "--is-bare-repository" }); try expectSame(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/sub", .{dir}), &.{ "config", "--list", "--local" }); try expectSame(&repo, try std.fmt.bufPrint(&path_buffer, "{s}/.git", .{dir}), &.{ "rev-parse", "--git-dir" }); } // compatObjectFormat, which git accepts, is refused by name. try repo.git(&.{ "init", "-q", "compat" }); try write(&repo, "compat/.git/config", "[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectFormat = sha256\n\tcompatObjectFormat = sha1\n"); const ours = try tool.runOurs(&repo, &.{ "rev-parse", "--git-dir" }, .{ .cwd = "compat" }); defer ours.deinit(testing.allocator); try testing.expectEqual(128, ours.code); try testing.expect(std.mem.find(u8, ours.stderr, "compatObjectFormat") != null);}
test "rev-parse: safe.bareRepository" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); for ([_][]const u8{ "x/.git/foo", "y/.git/modules/a/b", "z/worktrees/q", "k/.git/worktrees/q", "n.git/modules/o", "p/.git/modules" }) |p| { try repo.git(&.{ "init", "-q", "--bare", p }); } for ([_][]const u8{ "explicit", "all", "bogus" }) |v| { var buffer: [64]u8 = undefined; try repo.env.put("GIT_CONFIG_PARAMETERS", try std.fmt.bufPrint(&buffer, "'safe.bareRepository'='{s}'", .{v})); for ([_][]const u8{ "x/.git/foo", "y/.git/modules/a/b", "z/worktrees/q", "k/.git/worktrees/q", "n.git/modules/o", "p/.git/modules", ".git" }) |p| { try expectSame(&repo, p, &.{ "rev-parse", "--git-dir" }); try expectSame(&repo, p, &.{ "cat-file", "-t", missingId(&repo) }); } } _ = repo.env.orderedRemove("GIT_CONFIG_PARAMETERS"); try write(&repo, "global", "[safe]\n\tbareRepository = bogus\n"); const global = try inRepo(&repo, "global"); defer testing.allocator.free(global); try repo.env.put("GIT_CONFIG_GLOBAL", global); try expectSame(&repo, "x/.git/foo", &.{ "rev-parse", "--git-dir" }); // Only protected configuration counts. try repo.env.put("GIT_CONFIG_GLOBAL", "/dev/null"); try repo.git(&.{ "-C", "x/.git/foo", "config", "safe.bareRepository", "explicit" }); try expectSame(&repo, "x/.git/foo", &.{ "rev-parse", "--git-dir" });}
/// Runs git as root in a user namespace of its own, where the files the/// test made belong to someone else: the only way a test run by the/// files' owner can see git refuse them. Null if the namespace cannot be/// made here.fn gitAsRoot(repo: *Repo, cwd: []const u8, args: []const []const u8) !?Repo.Outcome { const gpa = repo.gpa; var argv: std.ArrayList([]const u8) = .empty; defer argv.deinit(gpa); try argv.appendSlice(gpa, &.{ "unshare", "--map-auto", "--map-current-user", "--setuid", "0", "--setgid", "0", "git" }); try argv.appendSlice(gpa, args); const dir = try std.fs.path.join(gpa, &.{ repo.path, cwd }); defer gpa.free(dir); const result = std.process.run(gpa, repo.io, .{ .argv = argv.items, .cwd = .{ .path = dir }, .environ_map = &repo.env }) catch return null; const code: u8 = switch (result.term) { .exited => |c| c, else => 255, }; if (std.mem.startsWith(u8, result.stderr, "unshare:")) { gpa.free(result.stdout); gpa.free(result.stderr); return null; } return .{ .stdout = result.stdout, .stderr = result.stderr, .code = code };}
test "rev-parse: repositories owned by someone else" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try mkdir(&repo, "a"); try repo.commitFile("f", "x\n", "first"); try repo.git(&.{ "worktree", "add", "-q", "linked" }); try repo.git(&.{ "init", "-q", "--bare", "bare.git" }); try repo.git(&.{ "init", "-q", "odd 'name' here!" }); // git, as root in a namespace, sees the test's files as another // user's; zig-git is told that it is root, and sees the same. const probe = try gitAsRoot(&repo, "a", &.{ "rev-parse", "--git-dir" }) orelse return error.SkipZigTest; probe.deinit(testing.allocator); const Case = struct { cwd: []const u8, args: []const []const u8 }; const cases = [_]Case{ .{ .cwd = "a", .args = &.{ "rev-parse", "--show-toplevel" } }, .{ .cwd = "a", .args = &.{ "rev-parse", "--local-env-vars" } }, .{ .cwd = "a", .args = &.{ "cat-file", "-t", missingId(&repo) } }, .{ .cwd = "a", .args = &.{ "config", "--list", "--local" } }, .{ .cwd = ".git", .args = &.{ "rev-parse", "--git-dir" } }, .{ .cwd = "linked", .args = &.{ "rev-parse", "--git-dir" } }, .{ .cwd = "bare.git", .args = &.{ "rev-parse", "--git-dir" } }, .{ .cwd = "odd 'name' here!", .args = &.{ "rev-parse", "--git-dir" } }, .{ .cwd = "a", .args = &.{ "--git-dir=../.git", "rev-parse", "--git-dir" } }, }; const top = try inRepo(&repo, "."); defer testing.allocator.free(top); const top_star = try std.fmt.allocPrint(testing.allocator, "{s}/*", .{std.mem.trimEnd(u8, top, "/.")}); defer testing.allocator.free(top_star); const settings = [_]?[]const u8{ null, "*", top, top_star, "relative", "" }; for (settings) |setting| { if (setting) |v| { try repo.env.put("GIT_CONFIG_COUNT", "3"); try repo.env.put("GIT_CONFIG_KEY_2", "safe.directory"); try repo.env.put("GIT_CONFIG_VALUE_2", v); } for (cases) |c| { const theirs = (try gitAsRoot(&repo, c.cwd, c.args)).?; defer theirs.deinit(testing.allocator); errdefer std.debug.print("safe.directory={?s}\n", .{setting}); try tool.expectSameAs(&repo, theirs, c.args, .{ .cwd = c.cwd, .uid = 0 }); } } try repo.env.put("GIT_CONFIG_COUNT", "2"); // Root may use what sudo's user owns. const uid = git.discover.effectiveUid() orelse return; var buffer: [16]u8 = undefined; try repo.env.put("SUDO_UID", try std.fmt.bufPrint(&buffer, "{d}", .{uid})); const theirs = (try gitAsRoot(&repo, "a", &.{ "rev-parse", "--show-toplevel" })).?; defer theirs.deinit(testing.allocator); try tool.expectSameAs(&repo, theirs, &.{ "rev-parse", "--show-toplevel" }, .{ .cwd = "a", .uid = 0 });}