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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The two ways configuration reaches git from its environment rather than//! from a file: `GIT_CONFIG_PARAMETERS`, which is how `git -c` passes its//! settings on to the commands it runs, and `GIT_CONFIG_COUNT` with its//! numbered keys and values (git-config(1)). Neither is read from the//! environment here; the caller passes the values in.//!//! `GIT_CONFIG_PARAMETERS` is undocumented. It holds settings separated//! by spaces, each a key in single quotes followed either by `=` and a//! value in single quotes, by `=` and nothing (no value: true), or by//! nothing, in which case an `=` inside the quotes separates the key from//! its value, as older versions of git wrote it. Settings are separated//! by blank space of any kind, tabs and line feeds included. Inside quotes every byte//! stands for itself; a quote is written by closing the quotes, writing//! `\'`, and opening them again, as is `!`, written `\!`. Nothing may//! come before the first setting, not even blank space; anything out of//! place is git's "bogus format".
const std = @import("std");const Allocator = std.mem.Allocator;
/// One setting read from `GIT_CONFIG_PARAMETERS`.pub const Parameter = struct { /// The key as it was written, not yet checked or lowered. key: []const u8, /// The value, or null for none (true). value: ?[]const u8,};
/// Steps through the settings of a `GIT_CONFIG_PARAMETERS` value.pub const Iterator = struct { text: []const u8, pos: usize = 0, key: std.ArrayList(u8) = .empty, value: std.ArrayList(u8) = .empty,
/// Frees the buffers settings are read into. pub fn deinit(it: *Iterator, gpa: Allocator) void { it.key.deinit(gpa); it.value.deinit(gpa); }
/// The next setting, valid until the next call, or null after the /// last. `error.BogusFormat` is git's "bogus format in /// GIT_CONFIG_PARAMETERS". pub fn next(it: *Iterator, gpa: Allocator) (error{BogusFormat} || Allocator.Error)!?Parameter { const t = it.text; if (it.pos == t.len) return null; if (t[it.pos] != '\'') return error.BogusFormat; it.key.clearRetainingCapacity(); try it.dequote(gpa, &it.key); var key: []const u8 = it.key.items; var value: ?[]const u8 = null; if (it.pos < t.len and t[it.pos] == '=') { it.pos += 1; if (it.pos < t.len and t[it.pos] == '\'') { it.value.clearRetainingCapacity(); try it.dequote(gpa, &it.value); value = it.value.items; } } else if (std.mem.findScalar(u8, key, '=')) |eq| { value = key[eq + 1 ..]; key = key[0..eq]; } if (it.pos < t.len) { if (!isSpace(t[it.pos])) return error.BogusFormat; while (it.pos < t.len and isSpace(t[it.pos])) it.pos += 1; } return .{ .key = key, .value = value }; }
/// Reads the quoted string at `pos` into `out`. fn dequote(it: *Iterator, gpa: Allocator, out: *std.ArrayList(u8)) (error{BogusFormat} || Allocator.Error)!void { const t = it.text; std.debug.assert(t[it.pos] == '\''); it.pos += 1; while (true) { const close = std.mem.findScalarPos(u8, t, it.pos, '\'') orelse return error.BogusFormat; try out.appendSlice(gpa, t[it.pos..close]); it.pos = close + 1; // `'\''` and `'\!'` stand for a quote and an exclamation mark. if (it.pos + 2 < t.len and t[it.pos] == '\\' and (t[it.pos + 1] == '\'' or t[it.pos + 1] == '!') and t[it.pos + 2] == '\'') { try out.append(gpa, t[it.pos + 1]); it.pos += 3; continue; } return; } }};
fn isSpace(c: u8) bool { return c == ' ' or (c >= '\t' and c <= '\r');}
/// The settings in a `GIT_CONFIG_PARAMETERS` value.pub fn iterate(text: []const u8) Iterator { return .{ .text = text };}
/// Why a `GIT_CONFIG_COUNT` was refused, in git's words.pub const CountError = error{ /// "bogus count in GIT_CONFIG_COUNT": not a number. BogusCount, /// "too many entries in GIT_CONFIG_COUNT": negative, or more than a C /// `int` holds. TooManyEntries,};
/// Reads a `GIT_CONFIG_COUNT` value as git does, with C's `strtoul` in/// base 10 and nothing allowed after the digits. An empty value is no/// entries.pub fn parseCount(text: []const u8) CountError!u32 { if (text.len == 0) return 0; var i: usize = 0; while (i < text.len and (text[i] == ' ' or (text[i] >= '\t' and text[i] <= '\r'))) i += 1; var negative = false; if (i < text.len and (text[i] == '+' or text[i] == '-')) { negative = text[i] == '-'; i += 1; } const start = i; var n: u128 = 0; while (i < text.len and std.ascii.isDigit(text[i])) : (i += 1) { n = @min(n * 10 + (text[i] - '0'), std.math.maxInt(u64) + 1); } if (i == start or i != text.len) return error.BogusCount; // strtoul makes a negative number a huge one. if ((negative and n != 0) or n > std.math.maxInt(i32)) return error.TooManyEntries; return @intCast(n);}
const testing = std.testing;
fn expectParameters(text: []const u8, want: []const ?[]const u8) !void { const gpa = testing.allocator; var it = iterate(text); defer it.deinit(gpa); var i: usize = 0; while (try it.next(gpa)) |p| : (i += 2) { try testing.expectEqualStrings(want[i].?, p.key); if (want[i + 1]) |v| try testing.expectEqualStrings(v, p.value.?) else try testing.expectEqual(null, p.value); } try testing.expectEqual(want.len, i);}
fn expectBogus(text: []const u8) !void { const gpa = testing.allocator; var it = iterate(text); defer it.deinit(gpa); while (it.next(gpa) catch |err| { try testing.expectEqual(error.BogusFormat, err); return; }) |_| {} return error.TestExpectedError;}
test "GIT_CONFIG_PARAMETERS is read as git reads it" { try expectParameters("'s.k'='v'", &.{ "s.k", "v" }); try expectParameters("'s.k=v'", &.{ "s.k", "v" }); try expectParameters("'s.k'", &.{ "s.k", null }); try expectParameters("'s.k'=", &.{ "s.k", null }); try expectParameters("'s.k'=''", &.{ "s.k", "" }); try expectParameters("'s.sub.k=a=b'", &.{ "s.sub.k", "a=b" }); try expectParameters("'s.k'='it'\\''s'", &.{ "s.k", "it's" }); try expectParameters("'a.b'\\!'c'='1'", &.{ "a.b!c", "1" }); try expectParameters("'s.a'='1' 's.b'='2' ", &.{ "s.a", "1", "s.b", "2" }); try expectParameters("'s.a'= '1'", &.{ "s.a", null, "1", null }); try expectParameters("'s.k=v'='w'", &.{ "s.k=v", "w" }); try expectParameters("", &.{}); try expectParameters("'s.k'\t'x'", &.{ "s.k", null, "x", null }); for ([_][]const u8{ " ", " 's.a'='1'", "'s.a'='1''s.b'='2'", "s.k=v", "'s.k'='v", "'s.k'x", "'s.k'=v", "'a.b'\\x'c'", "'a.b'\\''='1'", "'s.k'=='w'" }) |bad| { try expectBogus(bad); }}
test "GIT_CONFIG_COUNT is read as git reads it" { try testing.expectEqual(0, try parseCount("")); try testing.expectEqual(1, try parseCount("01")); try testing.expectEqual(1, try parseCount("+1")); try testing.expectEqual(1, try parseCount(" 1")); try testing.expectEqual(std.math.maxInt(i32), try parseCount("2147483647")); try testing.expectError(error.BogusCount, parseCount("x")); try testing.expectError(error.BogusCount, parseCount("1k")); try testing.expectError(error.BogusCount, parseCount("0x1")); try testing.expectError(error.BogusCount, parseCount("1 ")); try testing.expectError(error.TooManyEntries, parseCount("-1")); try testing.expectError(error.TooManyEntries, parseCount("2147483648")); try testing.expectError(error.TooManyEntries, parseCount("99999999999999999999"));}
const fuzz_input = @import("../testing/fuzz.zig");
fn fuzzOne(_: void, smith: *testing.Smith) anyerror!void { const gpa = testing.allocator; var buffer: [1024]u8 = undefined; const input = fuzz_input.bytes(smith, &buffer); var it = iterate(input); defer it.deinit(gpa); var n: usize = 0; while (it.next(gpa) catch return) |_| { // Each setting takes at least its two quotes. n += 1; try testing.expect(n * 2 <= input.len); } _ = parseCount(input) catch {};}
test "fuzz: GIT_CONFIG_PARAMETERS" { try fuzz_input.fuzz({}, fuzzOne, .{ .corpus = &.{ @embedFile("../fuzz/config-parameters"), } });}