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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `zig-git config`: the reading half of `git config`, answered by the//! library's `Config`, printing what git prints.//!//! zig-git config [<file-option>] [<display-option>] [--type=<type>] [-z]//! (--get | --get-all) <name> [<value-pattern>]//! zig-git config ... --get-regexp <name-regex> [<value-pattern>]//! zig-git config ... (-l | --list)//! zig-git config ... <name>//! zig-git config get [<option>...] <name>//! zig-git config list [<option>...]//!//! The file options are `--file <path>` (`-` for standard input),//! `--global`, `--system`, `--local` and `--worktree`; the display//! options `--show-origin`, `--show-scope` and `--name-only`; the types//! `bool`, `int`, `bool-or-int` and `path`. `--includes` and//! `--no-includes` choose whether includes are followed. `get` also takes//! `--all`, `--regexp`, `--value=<pattern>`, `--fixed-value`,//! `--show-names` and `--default=<value>`.//!//! Writing is not done, nor `--blob`, `--url`, `--type=color` or//! `--type=expiry-date`; each is refused by name. The repository is found//! as the other commands find it, except that where git carries on//! without one — none found, one it does not trust, one whose format it//! refuses — so does this.
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 regex = @import("git").regex;const tool = @import("zig_git.zig");const discovery = @import("discovery.zig");const Context = tool.Context;
const Action = enum { get, get_all, get_regexp, list };
const Type = enum { none, bool, int, bool_or_int, path };
const Source = union(enum) { all, file: []const u8, global, system, local, worktree,};
const Options = struct { action: ?Action = null, type: Type = .none, null_terminated: bool = false, name_only: bool = false, show_origin: bool = false, show_scope: bool = false, /// `get`'s `--show-names`; the old actions decide for themselves. show_names: ?bool = null, source: Source = .all, includes: ?bool = null, /// `get`'s `--value`. value_pattern: ?[]const u8 = null, fixed_value: bool = false, default: ?[]const u8 = null, /// `get`'s `--all` and `--regexp`. all: bool = false, regexp: bool = false,};
/// Runs `config` with `args`, the arguments after its name.pub fn run(ctx: Context, args: []const []const u8, stdin: *Io.Reader, stdout: *Io.Writer, stderr: *Io.Writer) !void { var o: Options = .{}; var operands: std.ArrayList([]const u8) = .empty; defer operands.deinit(ctx.gpa); const subcommand: ?enum { get, list } = if (args.len > 0 and std.mem.eql(u8, args[0], "get")) .get else if (args.len > 0 and std.mem.eql(u8, args[0], "list")) .list else null; const rest = if (subcommand != null) args[1..] else args; var i: usize = 0; var only_operands = false; var source_options: usize = 0; while (i < rest.len) : (i += 1) { const arg = rest[i]; if (only_operands or arg.len < 2 or arg[0] != '-') { try operands.append(ctx.gpa, arg); continue; } if (std.mem.eql(u8, arg, "--")) { only_operands = true; continue; } // An option's argument, attached with `=` or as the next word. const Value = struct { fn of(a: []const []const u8, index: *usize, name: []const u8) ?[]const u8 { const opt = a[index.*]; if (opt.len > name.len and std.mem.startsWith(u8, opt, name) and opt[name.len] == '=') return opt[name.len + 1 ..]; if (std.mem.eql(u8, opt, name) and index.* + 1 < a.len) { index.* += 1; return a[index.*]; } return null; } }; if (std.mem.eql(u8, arg, "-z") or std.mem.eql(u8, arg, "--null")) { o.null_terminated = true; } else if (std.mem.eql(u8, arg, "--name-only")) { o.name_only = true; } else if (std.mem.eql(u8, arg, "--show-origin")) { o.show_origin = true; } else if (std.mem.eql(u8, arg, "--show-scope")) { o.show_scope = true; } else if (std.mem.eql(u8, arg, "--includes")) { o.includes = true; } else if (std.mem.eql(u8, arg, "--no-includes")) { o.includes = false; } else if (std.mem.eql(u8, arg, "--global")) { o.source = .global; source_options += 1; } else if (std.mem.eql(u8, arg, "--system")) { o.source = .system; source_options += 1; } else if (std.mem.eql(u8, arg, "--local")) { o.source = .local; source_options += 1; } else if (std.mem.eql(u8, arg, "--worktree")) { o.source = .worktree; source_options += 1; } else if (std.mem.eql(u8, arg, "-f")) { if (i + 1 == rest.len) return error.Usage; i += 1; o.source = .{ .file = rest[i] }; source_options += 1; } else if (Value.of(rest, &i, "--file")) |f| { o.source = .{ .file = f }; source_options += 1; } else if (Value.of(rest, &i, "--type")) |t| { o.type = std.meta.stringToEnum(Type, if (std.mem.eql(u8, t, "bool-or-int")) "bool_or_int" else t) orelse { if (std.mem.eql(u8, t, "color") or std.mem.eql(u8, t, "expiry-date")) return refuse(stderr, "--type={s}", .{t}); return tool.fatalMessage(stderr, "unrecognized --type argument, {s}", .{t}); }; if (o.type == .none) return tool.fatalMessage(stderr, "unrecognized --type argument, {s}", .{t}); } else if (std.mem.eql(u8, arg, "--bool")) { o.type = .bool; } else if (std.mem.eql(u8, arg, "--int")) { o.type = .int; } else if (std.mem.eql(u8, arg, "--bool-or-int")) { o.type = .bool_or_int; } else if (std.mem.eql(u8, arg, "--path")) { o.type = .path; } else if (std.mem.eql(u8, arg, "--no-type")) { o.type = .none; } else if (Value.of(rest, &i, "--default")) |d| { o.default = d; } else if (std.mem.eql(u8, arg, "--fixed-value")) { o.fixed_value = true; } else if (subcommand == null and std.mem.eql(u8, arg, "--get")) { o.action = .get; } else if (subcommand == null and std.mem.eql(u8, arg, "--get-all")) { o.action = .get_all; } else if (subcommand == null and std.mem.eql(u8, arg, "--get-regexp")) { o.action = .get_regexp; } else if (subcommand == null and (std.mem.eql(u8, arg, "-l") or std.mem.eql(u8, arg, "--list"))) { o.action = .list; } else if (subcommand == .get and std.mem.eql(u8, arg, "--all")) { o.all = true; } else if (subcommand == .get and std.mem.eql(u8, arg, "--regexp")) { o.regexp = true; } else if (subcommand == .get and std.mem.eql(u8, arg, "--show-names")) { o.show_names = true; } else if (subcommand == .get and std.mem.eql(u8, arg, "--no-show-names")) { o.show_names = false; } else if (subcommand == .get and std.mem.startsWith(u8, arg, "--value=")) { o.value_pattern = arg["--value=".len..]; } else if (std.mem.eql(u8, arg, "--blob") or std.mem.startsWith(u8, arg, "--blob=") or std.mem.startsWith(u8, arg, "--url") or std.mem.startsWith(u8, arg, "--get-url") or std.mem.startsWith(u8, arg, "--get-color")) { return refuse(stderr, "{s}", .{arg}); } else { return refuse(stderr, "{s}", .{arg}); } }
if (source_options > 1 or (source_options == 1 and ctx.env.get("GIT_CONFIG") != null)) { try stderr.writeAll("error: only one config file at a time\n"); return error.UsageQuiet; } const ops = operands.items; const name: ?[]const u8 = switch (subcommand orelse .get) { .list => list: { if (subcommand == .list) { if (ops.len != 0) return wrongArgs(stderr, "should be 0"); o.action = .list; } break :list null; }, .get => get: { if (subcommand == .get) { if (ops.len != 1) return wrongArgs(stderr, "should be 1"); o.action = if (o.regexp) .get_regexp else if (o.all) .get_all else .get; if (o.default != null and o.all) return tool.fatalMessage(stderr, "--default= cannot be used with --all or --url=", .{}); if (o.fixed_value and o.value_pattern == null) return tool.fatalMessage(stderr, "--fixed-value only applies with 'value-pattern'", .{}); break :get ops[0]; } const action = o.action orelse { // `git config <name>` is `--get`; anything more would set. if (ops.len == 1) { o.action = .get; break :get ops[0]; } if (ops.len == 0) return error.Usage; return refuse(stderr, "setting a variable", .{}); }; if (action == .list) { if (ops.len != 0) return wrongArgs(stderr, "should be 0"); break :get null; } if (ops.len < 1 or ops.len > 2) return wrongArgs(stderr, "should be from 1 to 2"); if (ops.len == 2) o.value_pattern = ops[1]; if (o.fixed_value and o.value_pattern == null) { try stderr.writeAll("error: --fixed-value only applies with 'value-pattern'\n"); return error.UsageQuiet; } // The old actions show names only for --get-regexp. o.show_names = action == .get_regexp; break :get ops[0]; }, }; if (o.name_only and o.action != .list and o.action != .get_regexp) { try stderr.writeAll("error: --name-only is only applicable to --list or --get-regexp\n"); return error.UsageQuiet; } const action = o.action.?;
// Patterns are checked before anything is read. var key_regex: ?regex.Regex = null; defer if (key_regex) |*r| r.deinit(); var canonical_name: ?[]u8 = null; defer if (canonical_name) |c| ctx.gpa.free(c); if (name) |n| { if (action == .get_regexp) { const lowered = try std.ascii.allocLowerString(ctx.gpa, n); defer ctx.gpa.free(lowered); key_regex = try compilePattern(ctx.gpa, lowered, "invalid key pattern", stderr); } else { const buffer = try ctx.gpa.alloc(u8, n.len); canonical_name = buffer; _ = git.Config.syntax.canonicalKey(n, buffer) catch |err| { try stderr.print("error: {s}: {s}\n", .{ keyErrorText(err), n }); return error.No; }; } } var value_regex: ?regex.Regex = null; defer if (value_regex) |*r| r.deinit(); var negate = false; if (o.value_pattern) |p| if (!o.fixed_value) { var pattern = p; if (pattern.len > 0 and pattern[0] == '!') { negate = true; pattern = pattern[1..]; } value_regex = try compilePattern(ctx.gpa, pattern, "invalid pattern", stderr); };
var loaded = try load(ctx, &o, stdin, stderr, stdout); defer loaded.deinit(ctx); const c = &loaded.config;
const out: Output = .{ .ctx = ctx, .o = &o, .stdout = stdout, .stderr = stderr, .home = ctx.env.get("HOME") }; if (action == .list) return; // printed as it was read
// The entries asked for, in order. var found: std.ArrayList(*const Config.Entry) = .empty; defer found.deinit(ctx.gpa); if (action == .get_regexp) { for (c.entries.items) |*e| { if (!try key_regex.?.matches(ctx.gpa, e.key)) continue; if (!try valueMatches(ctx.gpa, &o, value_regex, negate, e.value)) continue; try found.append(ctx.gpa, e); } } else { var it = c.all(canonical_name.?) catch unreachable; while (it.next()) |e| { if (!try valueMatches(ctx.gpa, &o, value_regex, negate, e.value)) continue; try found.append(ctx.gpa, e); } } // A `:(optional)` path with nothing there is as good as unset. if (o.type == .path) { var kept: usize = 0; for (found.items) |e| { if (try out.optionalAndMissing(e)) continue; found.items[kept] = e; kept += 1; } found.shrinkRetainingCapacity(kept); } const last_only = action == .get or (subcommand == .get and action == .get_regexp and !o.all); if (found.items.len == 0) { if (o.default) |d| { const e: Config.Entry = .{ .key = canonical_name orelse name.?, .value = d, .scope = .command, .origin = .command_line, .line = 0 }; return out.entry(&e, false); } return error.No; } const chosen = if (last_only) found.items[found.items.len - 1 ..] else found.items; // Every value is checked before any is printed, as git converts them // all first. var text: std.Io.Writer.Allocating = .init(ctx.gpa); defer text.deinit(); const buffered: Output = .{ .ctx = ctx, .o = &o, .stdout = &text.writer, .stderr = stderr, .home = out.home }; for (chosen) |e| try buffered.entry(e, o.show_names orelse false); try stdout.writeAll(text.written());}
fn refuse(stderr: *Io.Writer, comptime fmt: []const u8, args: anytype) error{Fatal} { stderr.print("zig-git: config: " ++ fmt ++ " is not supported\n", args) catch {}; return error.Fatal;}
fn wrongArgs(stderr: *Io.Writer, comptime what: []const u8) error{UsageQuiet} { stderr.writeAll("error: wrong number of arguments, " ++ what ++ "\n") catch {}; return error.UsageQuiet;}
fn compilePattern(gpa: Allocator, pattern: []const u8, comptime what: []const u8, stderr: *Io.Writer) !regex.Regex { var unsupported: []const u8 = ""; return regex.Regex.compile(gpa, pattern, &unsupported) catch |err| switch (err) { error.InvalidPattern => { try stderr.print("error: " ++ what ++ ": {s}\n", .{pattern}); return error.BadPattern; }, error.Unsupported => refuse(stderr, "the regular expression {s}", .{unsupported}), error.OutOfMemory => |e| e, };}
fn valueMatches(gpa: Allocator, o: *const Options, value_regex: ?regex.Regex, negate: bool, value: ?[]const u8) !bool { const pattern = o.value_pattern orelse return true; if (o.fixed_value) { const v = value orelse return false; return std.mem.eql(u8, v, pattern); } return (try value_regex.?.matches(gpa, value orelse "")) != negate;}
fn keyErrorText(err: git.Config.syntax.KeyError) []const u8 { return switch (err) { error.KeyHasNoSection => "key does not contain a section", error.KeyHasNoName => "key does not contain variable name", error.InvalidKey => "invalid key", error.InvalidKeyNewline => "invalid key (newline)", };}
/// The configuration as loaded, and what it was loaded with.const Loaded = struct { config: Config, sources: ?Config.Sources = null, repo: Config.Repository.Fixed = .{}, /// The directory git works from, when there is a repository. work_dir: ?[]const u8 = null, owned: std.ArrayList([]u8) = .empty,
fn deinit(l: *Loaded, ctx: Context) void { l.config.deinit(); if (l.sources) |*s| s.deinit(ctx.gpa); for (l.owned.items) |p| ctx.gpa.free(p); l.owned.deinit(ctx.gpa); }};
/// The repository the configuration is for, if there is one that git/// would use: its configuration files as git names them, relative to the/// directory git works from, and what its `includeIf` conditions ask.const Found = struct { /// The common directory's `config`. local: []const u8, /// `config.worktree` in the git directory. worktree: []const u8,};
/// Finds the repository as git does for `config`: where git would carry/// on without one, so does this, and says so where git does.fn findRepository(ctx: Context, o: *const Options, loaded: *Loaded, stderr: *Io.Writer) !?Found { const gpa = ctx.gpa; var diag: Diagnostics = .{}; var protected = try discovery.protectedConfig(ctx, stderr); defer protected.deinit(); var refused: ?[]u8 = null; defer if (refused) |r| gpa.free(r); var warnings: std.ArrayList(git.discover.Warning) = .empty; defer { for (warnings.items) |w| gpa.free(w.text); warnings.deinit(gpa); } var options = discovery.options(ctx, &protected, &warnings); options.refused_git_dir = &refused; const result = git.discover.discover(gpa, ctx.io, options, &diag); try discovery.reportWarnings(stderr, warnings.items); var location = result catch |err| switch (err) { error.NotARepository, error.FilesystemBoundary, error.GitDirInvalid, error.DubiousOwnership, error.BareRepositoryNotAllowed => return null, error.UnsupportedVersion, error.UnknownExtensions, error.V1OnlyExtensions => { var buffer: [Diagnostics.max_detail_len * 2]u8 = undefined; var w: Io.Writer = .fixed(&buffer); _ = discovery.fail(&w, err, &diag) catch {}; const text = std.mem.trimEnd(u8, w.buffered(), "\n"); const message = text["fatal: ".len..]; try stderr.print("warning: {s}\n", .{message}); // Said again when the whole configuration is read again to be // listed. const repository_only = o.source == .local or o.source == .worktree; if (o.action == .list and !repository_only) try stderr.print("warning: ignoring git dir '{s}': {s}\n", .{ refused orelse "", message }); return null; }, else => |e| return discovery.fail(stderr, e, &diag), }; defer location.deinit(gpa); const work_dir = try gpa.dupe(u8, location.work_dir); try loaded.owned.append(gpa, work_dir); loaded.work_dir = work_dir; // `gitdir:` matches the git directory made absolute, and failing that // its real path; `onbranch:` the branch HEAD names. const absolute = try git.discover.normalize(gpa, location.work_dir, location.git_dir); try loaded.owned.append(gpa, absolute); loaded.repo.git_dir = absolute; const real = try gpa.dupe(u8, location.git_dir_real); try loaded.owned.append(gpa, real); loaded.repo.real_git_dir = real; if (location.work_tree) |t| { const kept = try gpa.dupe(u8, t); try loaded.owned.append(gpa, kept); loaded.repo.work_tree = kept; } var head_buffer: [1024]u8 = undefined; const head_path = try std.fs.path.join(gpa, &.{ location.git_dir_real, "HEAD" }); defer gpa.free(head_path); if (Io.Dir.cwd().readFile(ctx.io, head_path, &head_buffer)) |head| { const prefix = "ref: refs/heads/"; const line = std.mem.trimEnd(u8, head, "\n"); if (std.mem.startsWith(u8, line, prefix)) { const branch = try gpa.dupe(u8, line[prefix.len..]); try loaded.owned.append(gpa, branch); loaded.repo.branch = branch; } } else |_| {} const local = try joinName(gpa, location.common_dir, "config"); try loaded.owned.append(gpa, local); const worktree = try joinName(gpa, location.git_dir, "config.worktree"); try loaded.owned.append(gpa, worktree); return .{ .local = local, .worktree = worktree };}
/// A file in `dir` named as git names it: `config`, not `./config`.fn joinName(gpa: Allocator, dir: []const u8, name: []const u8) ![]u8 { var d = dir; while (std.mem.startsWith(u8, d, "./")) d = std.mem.trimStart(u8, d[2..], "/"); if (d.len == 0 or std.mem.eql(u8, d, ".")) return gpa.dupe(u8, name); return std.mem.concat(gpa, u8, &.{ std.mem.trimEnd(u8, d, "/"), "/", name });}
fn load(ctx: Context, o: *const Options, stdin: *Io.Reader, stderr: *Io.Writer, stdout: *Io.Writer) !Loaded { const gpa = ctx.gpa; var cwd = Io.Dir.cwd().openDir(ctx.io, ctx.cwd, .{}) catch return tool.fatalMessage(stderr, "cannot open the working directory", .{}); defer cwd.close(ctx.io); var loaded: Loaded = .{ .config = .init(gpa) }; errdefer loaded.deinit(ctx);
const found = try findRepository(ctx, o, &loaded, stderr); // Repository files are named relative to where git works from, the // top of the working tree; files given on the command line relative // to where it was started. var work_dir = if (loaded.work_dir) |w| Io.Dir.cwd().openDir(ctx.io, w, .{}) catch return tool.fatalMessage(stderr, "cannot open the working directory", .{}) else null; defer if (work_dir) |*d| d.close(ctx.io); const repo_dir = work_dir orelse cwd; const options: Config.Options = .{ .includes = o.includes orelse (o.source == .all and ctx.env.get("GIT_CONFIG") == null), .path = .{ .home = ctx.env.get("HOME") }, .repository = if (found != null) loaded.repo.repository() else null, };
var diag: Diagnostics = .{}; // git reads the whole configuration before any command runs, and // stops there, before printing anything, if it cannot. { var sources = try repositorySources(ctx, stderr, found); defer sources.deinit(gpa); var startup: Config = .init(gpa); defer startup.deinit(); var startup_options = options; startup_options.includes = true; // Before the repository is set up, git knows its directory but not // its branch or working tree. const early: Config.Repository.Fixed = .{ .git_dir = loaded.repo.git_dir, .real_git_dir = loaded.repo.real_git_dir }; if (startup_options.repository != null) startup_options.repository = early.repository(); const startup_result = startup.addSources(ctx.io, repo_dir, sources, startup_options, &diag); try reportMessages(stderr, &startup); startup_result catch |err| return describeLoadError(stderr, err, &diag); } const result = addSource(ctx, &loaded, cwd, repo_dir, found, o, options, stdin, stderr, &diag); // `--list` prints what it read, even when reading failed partway. const out: Output = .{ .ctx = ctx, .o = o, .stdout = stdout, .stderr = stderr, .home = ctx.env.get("HOME") }; if (o.action == .list) { for (loaded.config.entries.items) |*e| { if (o.type == .path and try out.optionalAndMissing(e)) continue; out.entry(e, true) catch |err| switch (err) { error.Omit => {}, else => |x| return x, }; } } // Listing reads everything again, and says it all again; the other // actions answer from what was read at the start. if (o.action == .list) try reportMessages(stderr, &loaded.config); result catch |err| return describeLoadError(stderr, err, &diag); return loaded;}
fn reportMessages(stderr: *Io.Writer, c: *const Config) !void { for (c.messages.items) |m| switch (m.kind) { .relative_condition_outside_file => try stderr.writeAll("error: relative config include conditionals must come from files\n"), };}
/// The sources git reads in the repository `found`, or outside one.fn repositorySources(ctx: Context, stderr: *Io.Writer, found: ?Found) !Config.Sources { var sources = Config.Sources.fromEnvironment(ctx.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, }; if (found) |f| { sources.local = f.local; sources.worktree = f.worktree; } return sources;}
fn addSource(ctx: Context, loaded: *Loaded, cwd: Io.Dir, repo_dir: Io.Dir, found: ?Found, o: *const Options, options: Config.Options, stdin: *Io.Reader, stderr: *Io.Writer, diag: *Diagnostics) !void { const gpa = ctx.gpa; const c = &loaded.config; var source = o.source; if (source == .all) if (ctx.env.get("GIT_CONFIG")) |f| { source = .{ .file = f }; }; switch (source) { .all => { loaded.sources = try repositorySources(ctx, stderr, found); try c.addSources(ctx.io, repo_dir, loaded.sources.?, options, diag); }, .file => |f| { if (std.mem.eql(u8, f, "-")) { const contents = try stdin.allocRemaining(gpa, .unlimited); defer gpa.free(contents); try c.addContents(ctx.io, cwd, contents, .{ .kind = .standard_input }, .command, options, diag); } else try c.addFile(ctx.io, cwd, f, .command, options, diag); }, .system => try c.addFile(ctx.io, cwd, ctx.env.get("GIT_CONFIG_SYSTEM") orelse "/etc/gitconfig", .system, options, diag), .global => { const path = if (ctx.env.get("GIT_CONFIG_GLOBAL")) |g| g else global: { const home = ctx.env.get("HOME") orelse return tool.fatalMessage(stderr, "$HOME not set", .{}); const dot = try std.fs.path.join(gpa, &.{ home, ".gitconfig" }); try loaded.owned.append(gpa, dot); // The XDG file only when it exists and ~/.gitconfig does not. if (cwd.access(ctx.io, dot, .{})) |_| break :global dot else |_| {} const xdg = if (ctx.env.get("XDG_CONFIG_HOME")) |x| if (x.len != 0) try std.fs.path.join(gpa, &.{ x, "git", "config" }) else try std.fs.path.join(gpa, &.{ home, ".config", "git", "config" }) else try std.fs.path.join(gpa, &.{ home, ".config", "git", "config" }); try loaded.owned.append(gpa, xdg); if (cwd.access(ctx.io, xdg, .{})) |_| break :global xdg else |_| {} break :global dot; }; try c.addFile(ctx.io, cwd, path, .global, options, diag); }, .local, .worktree => { const f = found orelse return tool.fatalMessage(stderr, "--{s} can only be used inside a git repository", .{@tagName(source)}); if (source == .worktree) { const contents = repo_dir.readFileAlloc(ctx.io, f.local, gpa, .unlimited) catch null; defer if (contents) |b| gpa.free(b); if (contents != null and try Config.worktreeConfigEnabled(gpa, contents.?)) { // Asked for on its own, it is reported as local. return c.addFile(ctx.io, repo_dir, f.worktree, .local, options, diag); } } try c.addFile(ctx.io, repo_dir, f.local, .local, options, diag); }, }}
/// Says what git says when it cannot read the configuration, and fails.pub fn describeLoadError(stderr: *Io.Writer, err: anyerror, diag: *const Diagnostics) anyerror { const path = diag.path() orelse ""; const line = diag.line orelse 0; const detail = diag.detail(); const command_line = "fatal: unable to parse command-line config\n"; switch (err) { error.BadConfigLine => if (diag.path() == null) try stderr.print("fatal: bad config line {d} in standard input\n", .{line}) else try stderr.print("fatal: bad config line {d} in file {s}\n", .{ line, path }), error.ConfigFileUnreadable => { if (!std.mem.eql(u8, detail, "No such file or directory")) try stderr.print("warning: unable to access '{s}': {s}\n", .{ path, detail }); try stderr.print("fatal: unable to read config file '{s}': {s}\n", .{ path, detail }); }, error.IncludeUnreadable => try stderr.print("fatal: unable to access '{s}': {s}\n", .{ path, detail }), error.IncludeIsDirectory => try stderr.print("warning: unable to access '{s}': Is a directory\nfatal: bad config line {d} in file {s}\n", .{ detail, line, path }), error.IncludeDepthExceeded => try stderr.print( "fatal: exceeded maximum include depth (10) while including\n\t{s}\nfrom\n\t{s}\nThis might be due to circular includes.\n", .{ path, detail }, ), error.MissingValue => if (diag.path() == null) try stderr.print("error: missing value for '{s}'\n" ++ command_line, .{detail}) else try stderr.print("error: missing value for '{s}'\nfatal: bad config line {d} in file {s}\n", .{ detail, line, path }), error.RelativeIncludeOutsideFile => try stderr.writeAll("error: relative config includes must come from files\n" ++ command_line), error.RemoteUrlInHasconfigInclude => try stderr.writeAll("fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\n"), error.UserHomeUnknown => try stderr.print("fatal: failed to expand user dir in: '{s}'\n", .{detail}), error.BogusParameters => try stderr.writeAll("error: bogus format in GIT_CONFIG_PARAMETERS\n" ++ command_line), error.BogusCount => try stderr.writeAll("error: bogus count in GIT_CONFIG_COUNT\n" ++ command_line), error.TooManyEntries => try stderr.writeAll("error: too many entries in GIT_CONFIG_COUNT\n" ++ command_line), error.MissingCountKey => try stderr.print("error: missing config key {s}\n" ++ command_line, .{detail}), error.MissingCountValue => try stderr.print("error: missing config value {s}\n" ++ command_line, .{detail}), error.EmptyKey => try stderr.writeAll("error: empty config key\n" ++ command_line), error.KeyHasNoSection, error.KeyHasNoName, error.InvalidKey, error.InvalidKeyNewline => try stderr.print("error: {s}: {s}\n" ++ command_line, .{ keyErrorText(@errorCast(err)), detail }), else => return err, } return error.Fatal;}
/// Prints entries as git does in each mode.const Output = struct { ctx: Context, o: *const Options, stdout: *Io.Writer, stderr: *Io.Writer, home: ?[]const u8,
/// Whether `e` is a `:(optional)` path with nothing at it. fn optionalAndMissing(out: Output, e: *const Config.Entry) !bool { const v = e.value orelse return false; if (!std.mem.startsWith(u8, v, ":(optional)")) return false; const p = Config.value.expandPath(out.ctx.gpa, v, .{ .home = out.home }) catch return false; defer out.ctx.gpa.free(p.path); Io.Dir.cwd().access(out.ctx.io, p.path, .{}) catch return true; return false; }
/// Prints one entry, with its name if `with_name`. In list mode a /// value that is not of the type asked for is `Omit`ted; otherwise it /// is fatal. fn entry(out: Output, e: *const Config.Entry, with_name: bool) !void { const o = out.o; const w = out.stdout; const listing = o.action == .list; var converted: [64]u8 = undefined; var path_buffer: ?[]u8 = null; defer if (path_buffer) |p| out.ctx.gpa.free(p); // The value to print, or null to print the name alone. const shown: ?[]const u8 = switch (o.type) { .none => e.value, .bool => if (Config.value.parseBool(e.value)) |b| (if (b) "true" else "false") else { if (listing) return error.Omit; return tool.fatalMessage(out.stderr, "bad boolean config value '{s}' for '{s}'", .{ e.value.?, e.key }); }, .int => if (Config.value.parseInt(i64, e.value orelse "")) |n| std.fmt.bufPrint(&converted, "{d}", .{n}) catch unreachable else |err| { if (listing) return error.Omit; return out.badNumeric(e, err); }, .bool_or_int => if (Config.value.parseBoolOrInt(e.value)) |v| switch (v) { .boolean => |b| if (b) "true" else "false", .int => |n| std.fmt.bufPrint(&converted, "{d}", .{n}) catch unreachable, } else |err| { if (listing) return error.Omit; return out.badNumeric(e, err); }, .path => path: { const v = e.value orelse { try out.stderr.print("error: missing value for '{s}'\n", .{e.key}); // Listing, git says so and carries on without it. if (listing) return error.Omit; if (e.origin.kind == .command_line) return tool.fatalMessage(out.stderr, "unable to parse command-line config", .{}); return tool.fatalMessage(out.stderr, "bad config line {d} in {f}", .{ e.line, describe(e.origin) }); }; const p = Config.value.expandPath(out.ctx.gpa, v, .{ .home = out.home }) catch |err| switch (err) { error.OutOfMemory => |x| return x, else => return tool.fatalMessage(out.stderr, "failed to expand user dir in: '{s}'", .{v}), }; path_buffer = p.path; break :path p.path; }, }; const z = o.null_terminated; if (o.show_scope) { try w.writeAll(e.scope.name()); try w.writeByte(if (z) 0 else '\t'); } if (o.show_origin) { try w.print("{f}", .{e.origin}); try w.writeByte(if (z) 0 else '\t'); } if (with_name) { try w.writeAll(e.key); if (o.name_only) return w.writeByte(if (z) 0 else '\n'); if (shown) |v| { try w.writeByte(if (z) '\n' else if (listing) '=' else ' '); try w.writeAll(v); } return w.writeByte(if (z) 0 else '\n'); } try w.writeAll(shown orelse ""); try w.writeByte(if (z) 0 else '\n'); }
fn badNumeric(out: Output, e: *const Config.Entry, err: Config.value.IntError) error{Fatal} { const why = switch (err) { error.InvalidUnit => "invalid unit", error.OutOfRange => "out of range", }; if (e.origin.kind == .command_line) return tool.fatalMessage(out.stderr, "bad numeric config value '{s}' for '{s}': {s}", .{ e.value orelse "", e.key, why }); return tool.fatalMessage(out.stderr, "bad numeric config value '{s}' for '{s}' in {f}: {s}", .{ e.value orelse "", e.key, describe(e.origin), why }); }};
fn describe(origin: Config.Origin) std.fmt.Alt(Config.Origin, Config.Origin.describe) { return .{ .data = origin };}
// -- tests ---------------------------------------------------------------------
const testing = std.testing;const Repo = git.test_fixture.Repo;
/// What `zig-git <args>` prints and exits with in `repo`.fn runOurs(repo: *Repo, args: []const []const u8, input: []const u8) !Repo.Outcome { const gpa = repo.gpa; var out: Io.Writer.Allocating = .init(gpa); errdefer out.deinit(); var err: Io.Writer.Allocating = .init(gpa); errdefer err.deinit(); var stdin: Io.Reader = .fixed(input); const code = tool.run(.{ .io = repo.io, .gpa = gpa, .env = &repo.env, .cwd = repo.path }, args, &stdin, &out.writer, &err.writer); return .{ .stdout = try out.toOwnedSlice(), .stderr = try err.toOwnedSlice(), .code = code };}
/// Checks that `zig-git <args>` prints what `git <args>` does, on both/// streams, and exits as it does.fn expectSame(repo: *Repo, args: []const []const u8) !void { return expectSameWith(repo, args, "");}
fn expectSameWith(repo: *Repo, args: []const []const u8, input: []const u8) !void { const theirs = try repo.runIn(".", args, input); defer theirs.deinit(repo.gpa); const ours = try runOurs(repo, args, input); defer ours.deinit(repo.gpa); errdefer { std.debug.print("zig-git", .{}); for (args) |a| std.debug.print(" \"{f}\"", .{std.zig.fmtString(a)}); std.debug.print("\n", .{}); } try testing.expectEqualStrings(theirs.stdout, ours.stdout); try testing.expectEqualStrings(theirs.stderr, ours.stderr); try testing.expectEqual(theirs.code, ours.code);}
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 });}
/// Files whose every corner the experiments behind `Config.syntax` turned/// over: each is read with `--list` both ways.const syntax_cases = [_][]const u8{ "[core]\nk=v", "[Core]\nK=v", "[ core ]\nk=v", "[core ]\nk=v", "[ core]\nk=v", "[core\"sub\"]\nk=v", "[core \"sub\"]\nk=v", "[core\t\"sub\"]\nk=v", "[core \"sub\" ]\nk=v", "[core \"sub\"x]\nk=v", "[core.sub]\nk=v", "[core.Sub]\nk=v", "[core.sub.x]\nk=v", "[core.]\nk=v", "[.core]\nk=v", "[]\nk=v", "[core \"a\\\"b\\\\c\\td\"]\nk=v", "[core \"a\nk=v", "[co_re]\nk=v", "[co-re]\nk=v", "[1core]\nk=v", "[core]k=v", "[core] # c\nk=v", "[core]junk", "[core \"s\"] k = v", "[core \"s\" \"t\"]\nk=v", "[core \"\"]\nk=v", "[core \"]\nk=v", "[core \"a\"\nk=v", "[core\n]\nk=v", "[core \"a\\", "k=v", "[core", "[co\x00re]\nk=v", "[core \"a\x00b\"]\nk=v", "[core \"a]b\"]\nk=v", "[c.o.r.e]\nk=v", "[core.a \"b\"]\nk=v", "[core \"a\"]]\nk=v", " [core] \nk=v", "[core\n", "[core \"a\"x\n", "[core \"a\\\n", "[core]\nk=v\n[x", "[core]\nk=\"a\n", "[core]\nk=\"a", "[core]\nk=\\q\n", "[core]\nk=a\\", "[core]\nk=a\\\n\n", "[core]\n=v\n", "[core]\n_k=v\n", "[core]\nk v\n", "[core]\nk\n", "[core]\nk \n", "[core]\nk;c\n", "[core]\nk \\\n=v\n", "[", "[\n", "[core]\n1k=v", "[core]\n-k=v", "[core]\nk-1=v", "[core]\nk.x=v", "[core]\n\x00", "[core]\nk=a\x00b\n", "x\n[core", "\n\n[core \"a\"\n", "[core]\nk=\"a\\\nb\"", "[s]\nk = a b \n", "[s]\nk = \" a \" \n", "[s]\nk = a\"b\"c\n", "[s]\nk = a\" b \"c # x\n", "[s]\nk = a;b\n", "[s]\nk = \\n\\t\\b\\\"\\\\\n", "[s]\nk = \\a\n", "[s]\nk = \\0\n", "[s]\nk = a\\\n b\n", "[s]\nk = a \\\n b \n", "[s]\nk = a\r\n", "[s]\r\nk = a\r\nj=b\r\n", "[s]\nk = a\rb\n", "[s]\nk = \"a\r\"\n", "[s]\nk = a \r\n", "[s]\nk\r\n", "[s]\nk=a\x00b\nj=1\n", "\xef\xbb\xbf[s]\nk=v\n", "\xef\xbb[s]\nk=v\n", "\n\xef\xbb\xbf[s]\nk=v\n", "[s]\nk= \n", "[s]\nk= \"\"\n", "[s]\nk=\t\tv\t\n", "[s]\n\tk\t=\tv\n", "[s]\nk = \"a\" \"b\"\n", "[s]\nk = a\\\r\n b\n", "[s]\nK-Ey = v\n", "[s]\nk = \\\n", "[s]\nk=v\\\n# not a comment\n", "[s]\nk=\\ \n", "[s]\nk=a\\\t\n", "[s]\nk = \x0b a\n", "[s]\nk = a\x0c\n", "[s]\n\x0bk=v\n", "\x0c[s]\nk=v\n", "[s]\nk\x0b= v\n", "[s]\nk=\\\n b\n", "[s]\nk=\"\" b\n", "[s]\nk=a\\\nb\nj\\\n", "[core \n", "[core ", "\r[s]\nk=v\n", "[s]\nk=v\r", "[s]\nk=v\r\r\n", "[s]\nk=\"a\r\n\"\n", "[s]\nk\r=v\n", "[s]\r k=v\n", "[s]\nk=a\\\r", "[s]\nk=a\\\rb\n", "[s]\nk=a\n\x00\n", "[s]\n# \x00 comment\nk=v\n", "[s \"a\\\nb\"]\nk=v\n", "[s \"\\x\"]\nk=v\n", "[s..t]\nk=v\n", "[s]\nk=a\n[t]\nk=b\n[s]\nk=c\n", "[S \"Sub\"]\nK=1\n[s.Sub]\nk=3\n", "[s]k\n", "[s] \n=", "[s]\nk==v\n", "[s]\nk = \"a\\\\\"b\n", "[s]\nk = \"a\" # c \"d\n", "[s]\nk=a\r\rb\n", "[s]\nk=\"a\r\r\"\n", "[s]\nk=a\r \n", "[s]\nk=a\r\x0c\n", "[s]\nk=a\r;c\n", "[s]\nk= \r a\n", "[s]\nk=\ra\n", "[s]\nk\r \n", "[s\r]\n", "[s \"a\"\r]\n", "[s \"a\r\"]\nk=v\n", "[s]\r\r\nk=v\n", "[s]\nk = a \"\" b\n", "[s]\nk = a \" b\"\n", "[s]\nk = v ; c \\\nj=1\n", "[s]\nk\r", "[s]\nk=\"a\\", "[s]\nk=a\\\n",};
test "zig-git config reads every corner of the file syntax as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); for (syntax_cases) |contents| { errdefer std.debug.print("file: \"{f}\"\n", .{std.zig.fmtString(contents)}); try writeFile(&repo, "f", contents); try expectSame(&repo, &.{ "config", "-f", "f", "--list", "-z" }); try expectSame(&repo, &.{ "config", "-f", "f", "--list", "--show-origin" }); try expectSame(&repo, &.{ "config", "-f", "f", "--get-regexp", "." }); try expectSameWith(&repo, &.{ "config", "-f", "-", "--list", "--show-scope", "--show-origin" }, contents); }}
test "zig-git config reads randomly assembled files as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); // Pieces of files, good and bad, put together at random: a fixed seed, // so that a failure can be had again. const pieces = [_][]const u8{ "[s]", "[S.Sub]", "[s \"Q\\\"x\"]", "[s \"a b\"]", "[bad", "\n", "\r\n", "\r", " ", "\t", "k", "K-2", "=", "\"", "\\", "\\\n", "\\n", "\\t", "\\q", ";c", "#c", "v", "a b", "\x00", "]", "[", "\xef\xbb\xbf", "\"x y\"", "k = v\n", "[t]\n", "\\\r\n", "x\\\"y", "\x0b", "[s.]", }; var prng: std.Random.DefaultPrng = .init(0x9e3779b97f4a7c15); const random = prng.random(); var contents: std.ArrayList(u8) = .empty; defer contents.deinit(gpa); for (0..400) |_| { contents.clearRetainingCapacity(); // Most start with a section, so that more get past the first line. if (random.boolean()) try contents.appendSlice(gpa, "[s]\n"); const n = random.intRangeAtMost(usize, 1, 24); for (0..n) |_| try contents.appendSlice(gpa, pieces[random.uintLessThan(usize, pieces.len)]); errdefer std.debug.print("file: \"{f}\"\n", .{std.zig.fmtString(contents.items)}); try writeFile(&repo, "f", contents.items); try expectSame(&repo, &.{ "config", "-f", "f", "--list", "-z" }); }}
test "zig-git config reads typed values as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); const values = [_][]const u8{ "true", "TRUE", "Yes", "on", "1", "false", "No", "off", "0", "", "2", "-1", "10", "0x10", "010", " 1", "1 ", "1k", "1K", "1m", "1g", "1G", "1t", "1kb", "k", "-", "+1", "+", "-0", "9223372036854775807", "9223372036854775808", "-9223372036854775808", "-9223372036854775809", "8388608k", "8796093022208k", "9007199254740992m", "2147483647", "2147483648", "4294967296", "trueish", "y", "00", "1.5", "1e3", "0b1", "1\\t", "tRuE", "0k", "-1k", "08", "0X1F", "0x", "0b", "-8589934592g", "8589934592g", "-2097152k", "2097151k", "~", "~/", "~/x", "~nosuchuserhere/x", "x/~/y", "%(prefix)", "./%(prefix)/bin", ":(optional)/nonexistent", ":(optional)/", "~//x", ":(optional)", }; for (values) |v| { const contents = try std.fmt.allocPrint(gpa, "[s]\n\tk = \"{s}\"\n\tn\n", .{v}); defer gpa.free(contents); try writeFile(&repo, "f", contents); for ([_][]const u8{ "--type=bool", "--type=int", "--type=bool-or-int", "--type=path", "--bool", "--int", "--path", "--bool-or-int" }) |t| { try expectSame(&repo, &.{ "config", "-f", "f", t, "--get", "s.k" }); try expectSame(&repo, &.{ "config", "-f", "f", t, "--list" }); try expectSame(&repo, &.{ "config", "-f", "f", t, "--get-regexp", "s.k" }); } } try writeFile(&repo, "f", "[s]\n\tn\n\tk = 1\n\tk = :(optional)/nonexistent\n"); for ([_][]const u8{ "--type=bool", "--type=int", "--type=bool-or-int", "--type=path" }) |t| { try expectSame(&repo, &.{ "config", "-f", "f", t, "--get", "s.n" }); try expectSame(&repo, &.{ "config", "-f", "f", t, "--get", "s.k" }); try expectSame(&repo, &.{ "config", "-f", "f", t, "--get-all", "s.k" }); } try expectSame(&repo, &.{ "config", "-f", "f", "--type=foo", "--get", "s.k" }); // From the command line and standard input, whose errors say so. try repo.env.put("GIT_CONFIG_PARAMETERS", "'s.k'='x'"); try expectSame(&repo, &.{ "config", "--type=int", "--get", "s.k" }); try expectSame(&repo, &.{ "config", "--type=bool", "--get", "s.k" }); try repo.env.put("GIT_CONFIG_PARAMETERS", "'s.k'"); try expectSame(&repo, &.{ "config", "--type=path", "--get", "s.k" }); _ = repo.env.swapRemove("GIT_CONFIG_PARAMETERS"); try expectSameWith(&repo, &.{ "config", "-f", "-", "--type=int", "--get", "s.k" }, "[s]\nk=x\n");}
/// `repo.path` joined with `rest`, which the caller frees.fn inRepo(repo: *Repo, rest: []const u8) ![]u8 { return std.fs.path.join(repo.gpa, &.{ repo.path, rest });}
test "zig-git config reads every source in git's order" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); _ = repo.env.swapRemove("GIT_CONFIG_GLOBAL"); _ = repo.env.swapRemove("GIT_CONFIG_NOSYSTEM"); const xdg = try inRepo(&repo, "xdg"); defer gpa.free(xdg); const system = try inRepo(&repo, "system-config"); defer gpa.free(system); try repo.env.put("XDG_CONFIG_HOME", xdg); try repo.env.put("GIT_CONFIG_SYSTEM", system); try writeFile(&repo, "system-config", "[s]\n\tk = system\n"); try writeFile(&repo, "xdg/git/config", "[s]\n\tk = xdg\n"); try writeFile(&repo, ".gitconfig", "[s]\n\tk = global\n"); try writeFile(&repo, ".git/config.worktree", "[s]\n\tk = worktree\n"); try repo.git(&.{ "config", "s.k", "local" }); try repo.env.put("GIT_CONFIG_PARAMETERS", "'s.k'='parameters' 'S.Sub.K'");
const queries = [_][]const []const u8{ &.{ "config", "--list", "--show-origin", "--show-scope" }, &.{ "config", "--list", "--show-origin", "--show-scope", "-z" }, &.{ "config", "--get-all", "--show-origin", "--show-scope", "s.k" }, &.{ "config", "--get", "--show-scope", "s.k" }, &.{ "config", "--global", "--list", "--show-origin", "--show-scope" }, &.{ "config", "--system", "--list", "--show-origin", "--show-scope" }, &.{ "config", "--local", "--list", "--show-origin", "--show-scope" }, &.{ "config", "--worktree", "--list", "--show-origin", "--show-scope" }, &.{ "config", "list", "--show-scope" }, &.{ "config", "get", "--all", "--show-names", "--show-scope", "s.k" }, }; for (queries) |q| try expectSame(&repo, q); // config.worktree, once the repository's own file asks for it. try repo.git(&.{ "config", "extensions.worktreeConfig", "true" }); for (queries) |q| try expectSame(&repo, q); try repo.git(&.{ "config", "extensions.worktreeConfig", "false" }); try writeFile(&repo, "wt.inc", "[extensions]\n\tworktreeConfig\n"); try repo.git(&.{ "config", "include.path", "../wt.inc" }); try expectSame(&repo, &.{ "config", "--get-all", "--show-scope", "s.k" }); try repo.git(&.{ "config", "--unset", "include.path" });
// The environment's ways of choosing files. const Env = struct { []const u8, ?[]const u8 }; const variations = [_][]const Env{ &.{.{ "GIT_CONFIG_NOSYSTEM", "1" }}, &.{.{ "GIT_CONFIG_NOSYSTEM", "yes" }}, &.{.{ "GIT_CONFIG_NOSYSTEM", "0" }}, &.{.{ "GIT_CONFIG_NOSYSTEM", "" }}, &.{.{ "GIT_CONFIG_NOSYSTEM", "2" }}, &.{.{ "GIT_CONFIG_NOSYSTEM", "maybe" }}, &.{.{ "GIT_CONFIG_GLOBAL", "" }}, &.{.{ "GIT_CONFIG_GLOBAL", "system-config" }}, &.{.{ "GIT_CONFIG_GLOBAL", "/nonexistent" }}, &.{.{ "GIT_CONFIG_SYSTEM", "" }}, &.{.{ "GIT_CONFIG_SYSTEM", "/nonexistent" }}, &.{.{ "XDG_CONFIG_HOME", "" }}, &.{.{ "XDG_CONFIG_HOME", null }}, &.{ .{ "HOME", null }, .{ "XDG_CONFIG_HOME", null } }, &.{.{ "HOME", null }}, &.{.{ "GIT_CONFIG", "system-config" }}, &.{.{ "GIT_CONFIG_PARAMETERS", "" }}, &.{.{ "GIT_CONFIG_COUNT", "" }}, &.{.{ "GIT_CONFIG_COUNT", "0" }}, &.{.{ "GIT_CONFIG_COUNT", "3" }}, &.{.{ "GIT_CONFIG_COUNT", "x" }}, &.{.{ "GIT_CONFIG_COUNT", "-1" }}, &.{.{ "GIT_CONFIG_COUNT", "1 " }}, &.{.{ "GIT_CONFIG_COUNT", "2147483648" }}, &.{ .{ "GIT_CONFIG_COUNT", "1" }, .{ "GIT_CONFIG_KEY_0", "k" } }, &.{ .{ "GIT_CONFIG_COUNT", "1" }, .{ "GIT_CONFIG_KEY_0", "" } }, &.{ .{ "GIT_CONFIG_COUNT", "1" }, .{ "GIT_CONFIG_KEY_0", "s.1k" } }, &.{ .{ "GIT_CONFIG_COUNT", "1" }, .{ "GIT_CONFIG_KEY_0", "s." } }, &.{ .{ "GIT_CONFIG_COUNT", "1" }, .{ "GIT_CONFIG_KEY_0", "S.Sub.K" }, .{ "GIT_CONFIG_VALUE_0", "" } }, &.{ .{ "GIT_CONFIG_COUNT", "2" }, .{ "GIT_CONFIG_KEY_1", null } }, &.{ .{ "GIT_CONFIG_COUNT", "2" }, .{ "GIT_CONFIG_VALUE_1", null } }, }; const parameter_cases = [_][]const u8{ "'s.k'='v'", "'s.k=v'", "'s.k'", "'s.k'=", "'s.k'=''", "'s.k='", " 's.a'='1'", "'s.a'='1''s.b'='2'", "'s.k'='it'\\''s'", "'S.Sub.K'='v'", "s.k=v", "'s.k'='v", "'s.k", "'s.k'x", "'s.k'='v'x", "'s.k'=v", "'k'='v'", "'s..k'='v'", "'s.1k'='v'", "'s.sub.k=a=b'", "'s.k'='a b'", " ", "'s.k'\t'x'", "'s.k' 'x.y'", "'s.a'='1' 's.b'='2'", "'s.a'='1' ", "'s.a'= '1'", "'a.b'\\!'c'='1'", "'a.b'\\x'c'='1'", "'a.b'\\''='1'", "'s.k=v'='w'", "'s.k'=='w'", "'s.k\nx'='1'", "'s.a\nb.k'='1'", "''='v'", "'s.'='1'", "'.k'='1'", "'include.path'", "'include.path'='rel'", }; for (variations) |vars| { var saved: [4]?[]u8 = @splat(null); defer for (saved) |s| if (s) |v| gpa.free(v); for (vars, 0..) |v, i| { if (repo.env.get(v[0])) |old| saved[i] = try gpa.dupe(u8, old); if (v[1]) |new| try repo.env.put(v[0], new) else _ = repo.env.swapRemove(v[0]); } errdefer for (vars) |v| std.debug.print("with {s}={?s}\n", .{ v[0], v[1] }); try expectSame(&repo, &.{ "config", "--list", "--show-origin", "--show-scope" }); try expectSame(&repo, &.{ "config", "--get-all", "--show-scope", "s.k" }); try expectSame(&repo, &.{ "config", "--global", "--list" }); try expectSame(&repo, &.{ "config", "--system", "--list" }); for (vars, 0..) |v, i| { if (saved[i]) |old| try repo.env.put(v[0], old) else _ = repo.env.swapRemove(v[0]); } } for (parameter_cases) |p| { try repo.env.put("GIT_CONFIG_PARAMETERS", p); errdefer std.debug.print("with GIT_CONFIG_PARAMETERS={s}\n", .{p}); try expectSame(&repo, &.{ "config", "--list", "--show-origin", "--show-scope", "-z" }); }}
test "zig-git config follows includes as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); _ = repo.env.swapRemove("GIT_CONFIG_GLOBAL"); try repo.env.put("XDG_CONFIG_HOME", "/nonexistent"); try repo.commit("first"); const abs = try inRepo(&repo, "abs.inc"); defer gpa.free(abs); const git_dir = try inRepo(&repo, ".git"); defer gpa.free(git_dir); const parent = std.fs.path.dirname(repo.path).?; const leaf = std.fs.path.basename(repo.path);
try writeFile(&repo, "abs.inc", "[a]\n\tabs = yes\n"); try writeFile(&repo, "sub/inc1", "[b]\n\tx = inc1\n[include]\n\tpath = deeper/inc2\n\tpath = missing\n\tpath = ~/tilde\n"); try writeFile(&repo, "sub/deeper/inc2", "[c]\n\tx = inc2\n"); try writeFile(&repo, "tilde", "[d]\n\tx = tilde\n"); try writeFile(&repo, "self", "[e]\n\tx = 1\n[include]\n\tpath = self\n"); try writeFile(&repo, "ping", "[include]\n\tpath = sub/pong\n"); try writeFile(&repo, "sub/pong", "[include]\n\tpath = ../ping\n[x]\n\ty = 1\n"); try writeFile(&repo, "bad.inc", "[x]\n\ty = 1\n[bad\n"); try writeFile(&repo, "url.inc", "[t]\n\tv = hit\n[remote \"x\"]\n\turl = foo\n"); try writeFile(&repo, "hit.inc", "[t]\n\tv = hit\n"); try writeFile(&repo, "noval", "[include]\n\tpath\n"); try writeFile(&repo, "emptyval", "[include]\n\tpath =\n"); try writeFile(&repo, "dirval", "[include]\n\tpath = sub\n"); try writeFile(&repo, "badinc", "[include]\n\tpath = bad.inc\n"); try repo.git(&.{ "config", "remote.origin.url", "https://example.com/a/b.git" });
const globals = [_][]const u8{ "[a]\n\tx = 1\n[include]\n\tpath = sub/inc1\n[a]\n\ty = after\n", try std.fmt.allocPrint(gpa, "[include]\n\tpath = {s}\n", .{abs}), "[include]\n\tpath = self\n", "[include]\n\tpath = ping\n", "[include]\n\tpath = badinc\n", "[include]\n\tpath = noval\n", "[include]\n\tpath = emptyval\n", "[include]\n\tpath = dirval\n", "[Include]\n\tPath = tilde\n", "[include \"x\"]\n\tpath = tilde\n[includeIf]\n\tpath = tilde\n", "[includeIf \"hasconfig:remote.*.url:https://example.com/**\"]\n\tpath = hit.inc\n", "[includeIf \"hasconfig:remote.*.url:https://example.com/*\"]\n\tpath = hit.inc\n", "[includeIf \"hasconfig:remote.*.url:https://example.com/**\"]\n\tpath = url.inc\n", "[includeIf \"hasconfig:remote.*.url:foo\"]\n\tpath = hit.inc\n[include]\n\tpath = url2.inc\n", "[includeIf \"hasconfig:remote.*.urlx:foo\"]\n\tpath = hit.inc\n[includeIf \"hasconfig:foo\"]\n\tpath = hit.inc\n[includeIf \"nosuch:foo\"]\n\tpath = hit.inc\n[includeIf \"gitdir\"]\n\tpath = hit.inc\n", "[includeIf \"Onbranch:**\"]\n\tpath = hit.inc\n", "[includeIf \"onbranch:main\"]\n\tpath\n", "[includeIf \"onbranch:nomatch\"]\n\tpath\n", }; defer gpa.free(globals[1]); try writeFile(&repo, "url2.inc", "[remote \"y\"]\n\turl = foo\n"); for (globals) |g| { errdefer std.debug.print("~/.gitconfig: \"{f}\"\n", .{std.zig.fmtString(g)}); try writeFile(&repo, ".gitconfig", g); try expectSame(&repo, &.{ "config", "--list", "--show-origin", "--show-scope" }); try expectSame(&repo, &.{ "config", "--get-all", "t.v" }); try expectSame(&repo, &.{ "config", "--global", "--list" }); try expectSame(&repo, &.{ "config", "--global", "--includes", "--list" }); try expectSame(&repo, &.{ "config", "--no-includes", "--list" }); try expectSame(&repo, &.{ "config", "--file", ".gitconfig", "--includes", "--show-origin", "--list" }); } try expectSame(&repo, &.{ "config", "--file", "self", "--includes", "--list" }); try expectSame(&repo, &.{ "config", "--file", "ping", "--includes", "--list" }); try expectSame(&repo, &.{ "config", "--file", "sub/inc1", "--includes", "--list", "--show-origin" });
// Conditions on the git directory and the branch. var patterns: std.ArrayList([]const u8) = .empty; defer { for (patterns.items) |p| gpa.free(p); patterns.deinit(gpa); } const upper_leaf = try std.ascii.allocUpperString(gpa, leaf); defer gpa.free(upper_leaf); for ([_][]const u8{ "{0}", "{0}/", "{1}/", "{1}", "{2}/", "{2}/*/.git", "{3}/.git", "{3}/", "{4}/", "*/.git", ".git", "/.git", "~", "~/", "~/.git", "", "**/{3}/**", "{0}/**", "{0}/*", "{3}x/" }) |template| { try patterns.append(gpa, try substitute(gpa, template, &.{ git_dir, repo.path, parent, leaf, upper_leaf })); } for (patterns.items) |p| { for ([_][]const u8{ "gitdir", "gitdir/i", "worktree", "worktree/i" }) |kw| { const g = try std.fmt.allocPrint(gpa, "[includeIf \"{s}:{s}\"]\n\tpath = hit.inc\n", .{ kw, p }); defer gpa.free(g); errdefer std.debug.print("~/.gitconfig: \"{f}\"\n", .{std.zig.fmtString(g)}); try writeFile(&repo, ".gitconfig", g); try expectSame(&repo, &.{ "config", "--get-all", "t.v" }); } } for ([_][]const u8{ "main", "main/", "ma*", "*", "**", "Main", "m?in", "refs/heads/main", "[m]ain" }) |b| { const g = try std.fmt.allocPrint(gpa, "[includeIf \"onbranch:{s}\"]\n\tpath = hit.inc\n", .{b}); defer gpa.free(g); try writeFile(&repo, ".gitconfig", g); try expectSame(&repo, &.{ "config", "--get-all", "t.v" }); try expectSame(&repo, &.{ "config", "--file", ".gitconfig", "--includes", "--get-all", "t.v" }); }
// Includes from the command line. try writeFile(&repo, ".gitconfig", ""); const cmd_abs = try std.fmt.allocPrint(gpa, "'include.path'='{s}'", .{abs}); defer gpa.free(cmd_abs); const cmd_cond = try std.fmt.allocPrint(gpa, "'includeIf.gitdir:./.path'='{s}'", .{abs}); defer gpa.free(cmd_cond); for ([_][]const u8{ cmd_abs, cmd_cond, "'include.path'='rel'", "'include.path'", "'remote.z.url'='foo'" }) |p| { try repo.env.put("GIT_CONFIG_PARAMETERS", p); try expectSame(&repo, &.{ "config", "--list", "--show-origin", "--show-scope" }); }}
/// `template` with each `{N}` replaced by `subs[N]`, which the caller/// frees.fn substitute(gpa: Allocator, template: []const u8, subs: []const []const u8) ![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); var i: usize = 0; while (i < template.len) : (i += 1) { if (template[i] == '{' and i + 2 < template.len and template[i + 2] == '}') { try out.appendSlice(gpa, subs[template[i + 1] - '0']); i += 2; } else try out.append(gpa, template[i]); } return out.toOwnedSlice(gpa);}
test "zig-git config answers queries as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try writeFile(&repo, "f", "[a]\n\tb = 1\n\tb = 2\n\tc\n\td =\n\te = x{y\n\te = (z)\n\te = a*b\n\te = !x\n" ++ "[x \"Y\"]\n\tz = q\n[x \"y\"]\n\tZ = r\n[t \"Sub\"]\n\tx = y\n"); const queries = [_][]const []const u8{ &.{ "--get", "a.b" }, &.{ "--get-all", "a.b" }, &.{ "--get", "a.c" }, &.{ "--get", "a.d" }, &.{ "--get-all", "a.zz" }, &.{ "--get", "A.B" }, &.{ "--get", "x.Y.z" }, &.{ "--get", "X.y.Z" }, &.{ "--get", "x.Y.Z" }, &.{ "--get-regexp", "a" }, &.{ "--get-regexp", "^a\\.[bc]" }, &.{ "--get-regexp", "t.Sub" }, &.{ "--get-regexp", "Sub" }, &.{ "--get-regexp", "ub" }, &.{ "--get-regexp", "S.A" }, &.{ "--get-regexp", "[[:upper:]]" }, &.{ "--get-regexp", "x$" }, &.{ "--get-regexp", "\\.X" }, &.{ "--name-only", "--get-regexp", "a" }, &.{ "-z", "--get-regexp", "a" }, &.{ "-z", "--get-all", "a.b" }, &.{ "-z", "--list" }, &.{ "--name-only", "--list" }, &.{ "--name-only", "-z", "--list" }, &.{ "--show-origin", "--get", "a.b" }, &.{ "--show-scope", "--get", "a.b" }, &.{ "--get", "a.b", "2" }, &.{ "--get", "a.b", "^1" }, &.{ "--get-all", "a.b", "!2" }, &.{ "--get-regexp", "a", "." }, &.{ "--get", "a.b", "[" }, &.{ "--get-regexp", "[" }, &.{ "--get-regexp", "a.b", "1", "extra" }, &.{"--get"}, &.{"--get-all"}, &.{ "--list", "x" }, &.{ "--name-only", "--get", "a.b" }, &.{ "--get-all", "a.c", "^$" }, &.{ "--get-all", "a.c", "!x" }, &.{ "--get-all", "a.c", "." }, &.{ "--get-all", "a.c", "!." }, &.{ "--get-regexp", "a", "^$" }, &.{ "--get-all", "--fixed-value", "a.b", "1" }, &.{ "--get-all", "--fixed-value", "a.b", "!1" }, &.{ "--get-all", "--fixed-value", "a.c", "1" }, &.{ "--get-all", "--fixed-value", "a.e", "!x" }, &.{ "--get-all", "a.e", "x{" }, &.{ "--get-all", "a.e", "\\(z" }, &.{ "--get-all", "a.e", "^x\\{" }, &.{ "--get-all", "a.e", "a\\*" }, &.{ "--get-all", "a.e", "!" }, &.{ "--get-all", "a.e", "!!x" }, &.{ "--get-all", "a.e", ")" }, &.{ "--get-all", "a.e", "(|x)y" }, &.{ "--get-all", "a.e", "^.{3}$" }, &.{ "--get-all", "a.e", "[[:punct:]]" }, &.{ "--get-all", "a.e", "*a" }, &.{ "--get", "k" }, &.{ "--get", "s." }, &.{ "--get", ".k" }, &.{ "--get", "s.1k" }, &.{ "--get", "s.k x" }, &.{ "--get", "s_x.k" }, &.{ "--get", "a.b\nc.d" }, &.{ "--get", "" }, &.{ "--get", "s.-k" }, &.{ "--fixed-value", "--get", "a.b" }, &.{"a.b"}, &.{"zz"}, &.{ "--get", "--global", "--local", "a.b" }, }; for (queries) |q| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, &.{ "config", "-f", "f" }); try args.appendSlice(gpa, q); try expectSame(&repo, args.items); } const subcommands = [_][]const []const u8{ &.{ "get", "a.b" }, &.{ "get", "--all", "a.b" }, &.{ "get", "--regexp", "a" }, &.{ "get", "--all", "--regexp", "a" }, &.{ "get", "--show-names", "a.b" }, &.{ "get", "--all", "--show-names", "a.b" }, &.{ "get", "--regexp", "--show-names", "a" }, &.{ "get", "--value=1", "a.b" }, &.{ "get", "--value=!1", "a.b" }, &.{ "get", "--value=1", "--fixed-value", "a.b" }, &.{ "get", "--fixed-value", "a.b" }, &.{ "get", "--default=d", "a.zz" }, &.{ "get", "--default=d", "--type=bool", "a.zz" }, &.{ "get", "--default=d", "--all", "a.zz" }, &.{ "get", "a.c" }, &.{ "get", "--type=bool", "a.c" }, &.{ "get", "a.b", "x" }, &.{ "get", "zz" }, &.{ "get", "--all", "zz" }, &.{"list"}, &.{ "list", "-z" }, &.{ "list", "x" }, &.{ "list", "--show-origin", "--show-scope" }, &.{ "get", "--show-origin", "a.b" }, &.{ "get", "-z", "--all", "a.b" }, &.{ "get", "-z", "--regexp", "--show-names", "a" }, &.{ "get", "--regexp", "[" }, &.{ "get", "--value=[", "a.b" }, &.{ "get", "--regexp", "--all", "A" }, &.{ "get", "--no-show-names", "a.b" }, &.{ "get", "--default=1", "--type=int", "a.b" }, &.{ "get", "--type=int", "--default=x", "a.zz" }, &.{ "list", "--name-only" }, }; for (subcommands) |q| { var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, &.{ "config", q[0], "-f", "f" }); try args.appendSlice(gpa, q[1..]); try expectSame(&repo, args.items); } // In the repository, with nothing named. try expectSame(&repo, &.{ "config", "--list" }); try expectSame(&repo, &.{ "config", "--get", "core.bare" }); try expectSame(&repo, &.{ "config", "--type=bool", "--get", "core.bare" }); try expectSame(&repo, &.{ "config", "--get", "extensions.objectformat" });}