// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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)); }