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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The two ways a repository names its alternates, read as git reads//! them (gitrepository-layout(5), git(1)); what was found by experiment//! is noted where the documentation is silent.//!//! - `objects/info/alternates`: one path per line. A line beginning with//! `#` is a comment and an empty line is skipped; nothing else is//! trimmed, so a line of spaces, or one ending in a carriage return or//! a space, names a directory with those characters in its name. A//! relative path is relative to the objects directory holding the file.//! - `GIT_ALTERNATE_OBJECT_DIRECTORIES`: paths separated by `:` (`;` on//! Windows), empty ones skipped and, as git skips them, those beginning//! with `#`. A relative path is relative to the top of the working tree,//! where git is running by the time it reads the variable.//!//! In both, a path beginning with `"` is C-quoted, and is taken literally//! if the quoting is malformed. Turning the paths into directories, and//! following alternates of alternates, is `DiskDatabase`'s business; this//! only reads the text, and does no I/O.
const std = @import("std");const Allocator = std.mem.Allocator;const builtin = @import("builtin");
const quote = @import("quote.zig");
/// The character separating paths in `GIT_ALTERNATE_OBJECT_DIRECTORIES`.pub const list_separator: u8 = if (builtin.os.tag == .windows) ';' else ':';
/// Steps through the paths named by the contents of an/// `objects/info/alternates` file.pub const FileIterator = struct { lines: std.mem.SplitIterator(u8, .scalar), buffer: std.ArrayList(u8) = .empty,
/// Frees the buffer quoted paths are read into. pub fn deinit(it: *FileIterator, gpa: Allocator) void { it.buffer.deinit(gpa); }
/// The next path, valid until the next call, or null after the last. pub fn next(it: *FileIterator, gpa: Allocator) Allocator.Error!?[]const u8 { while (it.lines.next()) |line| { if (line.len == 0 or line[0] == '#') continue; it.buffer.clearRetainingCapacity(); if (try quote.unquote(gpa, line, &it.buffer)) |q| { if (q.len == line.len) return it.buffer.items; } return line; } return null; }};
/// The paths an `objects/info/alternates` file names.pub fn iterateFile(contents: []const u8) FileIterator { return .{ .lines = std.mem.splitScalar(u8, contents, '\n') };}
/// Steps through the paths a `GIT_ALTERNATE_OBJECT_DIRECTORIES` value/// names.pub const ListIterator = struct { rest: []const u8, separator: u8, buffer: std.ArrayList(u8) = .empty,
/// Frees the buffer quoted paths are read into. pub fn deinit(it: *ListIterator, gpa: Allocator) void { it.buffer.deinit(gpa); }
/// The next path, valid until the next call, or null after the last. pub fn next(it: *ListIterator, gpa: Allocator) Allocator.Error!?[]const u8 { while (it.rest.len > 0) { it.buffer.clearRetainingCapacity(); if (try quote.unquote(gpa, it.rest, &it.buffer)) |q| { const after = it.rest[q.len..]; if (after.len == 0 or after[0] == it.separator) { it.rest = if (after.len == 0) after else after[1..]; if (it.buffer.items.len == 0) continue; return it.buffer.items; } it.buffer.clearRetainingCapacity(); } const end = std.mem.findScalar(u8, it.rest, it.separator) orelse it.rest.len; const path = it.rest[0..end]; it.rest = if (end == it.rest.len) it.rest[end..] else it.rest[end + 1 ..]; if (path.len == 0 or path[0] == '#') continue; return path; } return null; }};
/// The paths a `GIT_ALTERNATE_OBJECT_DIRECTORIES` value names, separated/// by `separator` (`list_separator` on this system).pub fn iterateList(value: []const u8, separator: u8) ListIterator { return .{ .rest = value, .separator = separator };}
const testing = std.testing;
fn collect(gpa: Allocator, it: anytype) ![][]u8 { var list: std.ArrayList([]u8) = .empty; errdefer { for (list.items) |p| gpa.free(p); list.deinit(gpa); } while (try it.next(gpa)) |p| try list.append(gpa, try gpa.dupe(u8, p)); return list.toOwnedSlice(gpa);}
fn expectPaths(want: []const []const u8, got: [][]u8) !void { defer { for (got) |p| testing.allocator.free(p); testing.allocator.free(got); } try testing.expectEqual(want.len, got.len); for (want, got) |w, g| try testing.expectEqualStrings(w, g);}
test "an alternates file is read a line at a time, comments and blank lines skipped" { const gpa = testing.allocator; var it = iterateFile("# comment\n\n../a/objects\n \n\t# not a comment\n\"q\\\"uoted\"\n\"bad\n/abs/objects \r\nlast"); defer it.deinit(gpa); try expectPaths(&.{ "../a/objects", " ", "\t# not a comment", "q\"uoted", "\"bad", "/abs/objects \r", "last" }, try collect(gpa, &it));}
test "an alternates list is split on its separator, with quoted paths kept whole" { const gpa = testing.allocator; var it = iterateList("::a:#comment:\"with-\\\"-and-:-in-it\":vanilla:\"bad:x", ':'); defer it.deinit(gpa); try expectPaths(&.{ "a", "with-\"-and-:-in-it", "vanilla", "\"bad", "x" }, try collect(gpa, &it));}