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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! How every `zig-git` command finds its repository: the library's//! discovery, given what git would take from the environment and the//! global options, and git's own words when it fails.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const Config = git.Config;const Diagnostics = git.Diagnostics;const discover = git.discover;const tool = @import("zig_git.zig");const Context = tool.Context;const config_command = @import("config_command.zig");const object_names = @import("object_names.zig");
/// The discovery options for `ctx`, its global options applied.pub fn options(ctx: Context, protected: ?*const Config, warnings: ?*std.ArrayList(discover.Warning)) discover.Options { var env: discover.Environment = .fromMap(ctx.env); if (ctx.git_dir) |g| env.git_dir = g; if (ctx.work_tree) |w| env.work_tree = w; const uid = ctx.uid orelse discover.effectiveUid(); return .{ .start = ctx.cwd, .environment = env, .protected_config = protected, .path = .{ .home = ctx.env.get("HOME") }, .ownership = if (uid) |u| .{ .uid = u } else null, .warnings = warnings, };}
/// The system, global and command configuration, which decides/// `safe.directory` and `safe.bareRepository`; git reads it first, and/// stops if it cannot.pub fn protectedConfig(ctx: Context, stderr: *Io.Writer) !Config { const gpa = ctx.gpa; var sources = Config.Sources.fromEnvironment(gpa, ctx.env, .{}) catch |err| switch (err) { error.BadNoSystem => return tool.fatalMessage(stderr, "bad boolean environment value '{s}' for 'GIT_CONFIG_NOSYSTEM'", .{ctx.env.get("GIT_CONFIG_NOSYSTEM").?}), else => |e| return e, }; defer sources.deinit(gpa); sources.command_line = ctx.config_parameters; var diag: Diagnostics = .{}; var c: Config = .init(gpa); errdefer c.deinit(); c.addSources(ctx.io, Io.Dir.cwd(), sources, .{ .path = .{ .home = ctx.env.get("HOME") } }, &diag) catch |err| return config_command.describeLoadError(stderr, err, &diag); return c;}
/// The outcome of looking for a repository.pub const Found = struct { protected: Config, location: ?discover.Location,
pub fn deinit(f: *Found, gpa: Allocator) void { f.protected.deinit(); if (f.location) |*l| l.deinit(gpa); }};
/// Looks for the repository and says what git would of what it found/// wrong but carried on after. On failure, `diag` and the error say why,/// for `fail` to put into git's words.pub fn find(ctx: Context, stderr: *Io.Writer, diag: *Diagnostics) !struct { Config, discover.Error!discover.Location } { const gpa = ctx.gpa; var protected = try protectedConfig(ctx, stderr); errdefer protected.deinit(); var warnings: std.ArrayList(discover.Warning) = .empty; defer { for (warnings.items) |w| gpa.free(w.text); warnings.deinit(gpa); } const result = discover.discover(gpa, ctx.io, options(ctx, &protected, &warnings), diag); try reportWarnings(stderr, warnings.items); return .{ protected, result };}
/// Says what git warns of during discovery.pub fn reportWarnings(stderr: *Io.Writer, warnings: []const discover.Warning) !void { for (warnings) |w| switch (w.kind) { .relative_safe_directory => try stderr.print("warning: safe.directory '{s}' not absolute\n", .{w.text}), .bare_and_worktree => try stderr.writeAll("warning: core.bare and core.worktree do not make sense\n"), };}
/// Finds the repository, or says why not as git says it and fails.pub fn locate(ctx: Context, stderr: *Io.Writer) !discover.Location { var diag: Diagnostics = .{}; var protected, const result = try find(ctx, stderr, &diag); defer protected.deinit(); return result catch |err| fail(stderr, err, &diag);}
/// Finds and opens the repository, or says why not and fails.pub fn open(ctx: Context, stderr: *Io.Writer) !git.Repository { const location = try locate(ctx, stderr); var diag: Diagnostics = .{}; var repo = try openLocated(ctx, location, stderr, &diag); errdefer repo.close(ctx.io); // git reads these whenever it reads the configuration, and stops at a // bad one whatever the command. _ = try object_names.settings(&repo.config, stderr); return repo;}
fn openLocated(ctx: Context, location: discover.Location, stderr: *Io.Writer, diag: *Diagnostics) !git.Repository { return git.Repository.open(ctx.gpa, ctx.io, location, .{ .environment = ctx.env, .command_line = ctx.config_parameters, .path = .{ .home = ctx.env.get("HOME") }, .replace_objects = ctx.replace_objects, // git reads an object of any size, and zig-git is for comparing // with it. .max_object_size = null, }, diag) catch |err| switch (err) { error.BadReplaceRefsSetting => tool.fatalMessage(stderr, "bad boolean config value '{s}' for 'core.usereplacerefs'", .{diag.detail()}), error.UnsupportedReplaceRefBase => tool.fatalMessage(stderr, "zig-git: GIT_REPLACE_REF_BASE '{s}' must end in '/'", .{diag.detail()}), error.BadMultiPackIndexSignature => tool.fatalMessage(stderr, "multi-pack-index signature {s} does not match signature 0x4d494458", .{diag.detail()}), error.UnsupportedMultiPackIndexVersion => tool.fatalMessage(stderr, "multi-pack-index version {s} not recognized", .{diag.detail()}), error.MissingMultiPackIndexChunk => tool.fatalMessage(stderr, "multi-pack-index required {s} chunk missing or corrupted", .{diag.detail()}), error.ObjectsDirectoryUnreadable => tool.fatalMessage(stderr, "not a git repository (or any of the parent directories): .git", .{}), error.BadMultiPackIndexSetting => tool.fatalMessage(stderr, "bad boolean config value '{s}' for 'core.multipackindex'", .{diag.detail()}), error.RefStorageNotReadable => tool.fatalMessage(stderr, "zig-git: an onbranch: condition could not read the refs", .{}), else => |e| config_command.describeLoadError(stderr, e, diag), };}
/// The object format of the repository there is, if there is one: for/// the commands that work with or without one.pub fn formatIfAny(ctx: Context) ?git.ObjectFormat { var buffer: [256]u8 = undefined; var discard: Io.Writer.Discarding = .init(&buffer); var diag: Diagnostics = .{}; var protected, const result = find(ctx, &discard.writer, &diag) catch return null; defer protected.deinit(); var location = result catch return null; defer location.deinit(ctx.gpa); return location.format.object_format;}
/// Says what git says when it cannot use the repository, and fails.pub fn fail(stderr: *Io.Writer, err: anyerror, diag: *const Diagnostics) anyerror { const path = diag.path() orelse ""; const detail = diag.detail(); const line = diag.line orelse 0; switch (err) { error.StartUnreadable => try stderr.print("fatal: cannot change to '{s}': {s}\n", .{ path, detail }), error.NotARepository => try stderr.writeAll("fatal: not a git repository (or any of the parent directories): .git\n"), error.FilesystemBoundary => try stderr.print("fatal: not a git repository (or any parent up to mount point {s})\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\n", .{path}), error.BadDiscoveryAcrossFilesystem => try stderr.print("fatal: bad boolean environment value '{s}' for 'GIT_DISCOVERY_ACROSS_FILESYSTEM'\n", .{detail}), error.GitDirInvalid => try stderr.print("fatal: not a git repository: '{s}'\n", .{path}), error.InvalidGitFile => try stderr.print("fatal: invalid gitfile format: {s}\n", .{path}), error.GitFileNoPath => try stderr.print("fatal: no path in gitfile: {s}\n", .{path}), error.GitFileNotARepository => try stderr.print("fatal: gitfile does not point to a valid repository: {s}\n", .{path}), error.CommonDirEmpty => try stderr.print("fatal: failed to read {s}: Success\n", .{path}), error.DubiousOwnership => { try stderr.print("fatal: detected dubious ownership in repository at '{s}'\nTo add an exception for this directory, call:\n\n\tgit config --global --add safe.directory ", .{path}); try git.quote.writeShellQuoted(stderr, path); try stderr.writeAll("\n"); }, error.BareRepositoryNotAllowed => try stderr.print("fatal: cannot use bare repository '{s}' (safe.bareRepository is 'explicit')\n", .{path}), error.BadBareRepositorySetting => if (diag.path() == null) try stderr.writeAll("fatal: unable to parse 'safe.barerepository' from command-line config\n") else try stderr.print("fatal: bad config variable 'safe.barerepository' in file '{s}' at line {d}\n", .{ path, line }), error.WorkTreeUnreachable => try stderr.print("fatal: cannot chdir to '{s}': No such file or directory\n", .{detail}), error.EmptyWorkTree => try stderr.writeAll("fatal: The empty string is not a valid path\n"), error.BadConfigLine => try stderr.print("fatal: bad config line {d} in file {s}\n", .{ line, path }), error.MissingValue => try stderr.print("error: missing value for '{s}'\nfatal: bad config line {d} in file {s}\n", .{ detail, line, path }), error.InvalidExtensionValue => { const eq = std.mem.findScalar(u8, detail, '=') orelse detail.len; try stderr.print("error: invalid value for '{s}': '{s}'\nfatal: bad config line {d} in file {s}\n", .{ detail[0..eq], detail[@min(eq + 1, detail.len)..], line, path }); }, error.BadBoolean => { const eq = std.mem.findScalar(u8, detail, '=') orelse detail.len; try stderr.print("fatal: bad boolean config value '{s}' for '{s}'\n", .{ detail[@min(eq + 1, detail.len)..], detail[0..eq] }); }, error.VersionInvalidUnit, error.VersionOutOfRange => try stderr.print( "fatal: bad numeric config value '{s}' for 'core.repositoryformatversion' in file {s}: {s}\n", .{ detail, path, if (err == error.VersionInvalidUnit) "invalid unit" else "out of range" }, ), error.UnsupportedVersion => try stderr.print("fatal: Expected git repo version <= 1, found {s}\n", .{detail}), error.UnknownExtensions, error.V1OnlyExtensions => { const plural = std.mem.findScalar(u8, detail, '\n') != null; if (err == error.UnknownExtensions) try stderr.print("fatal: unknown repository extension{s} found:\n", .{if (plural) "s" else ""}) else try stderr.print("fatal: repo version is 0, but v1-only extension{s} found:\n", .{if (plural) "s" else ""}); var it = std.mem.splitScalar(u8, detail, '\n'); while (it.next()) |name| try stderr.print("\t{s}\n", .{name}); }, error.CompatObjectFormatUnsupported => try stderr.writeAll("fatal: zig-git: extensions.compatObjectFormat is not supported\n"), error.RefStoragePayloadUnsupported => try stderr.writeAll("fatal: zig-git: extensions.refStorage with a payload is not supported\n"), error.FileSystemUnsupported => try stderr.print("fatal: zig-git: cannot tell who owns '{s}' or what it is mounted on here\n", .{path}), error.Unreadable => try stderr.print("fatal: zig-git: cannot read '{s}': {s}\n", .{ path, detail }), else => return err, } return error.Fatal;}