Something went wrong. Try again.
Reading and writing git repositories in Zig, without running git.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! An object database: where a repository's objects are found by object//! ID, whatever the storage behind it.//!//! This is the seam the rest of the library reads objects through. Loose//! objects (`loose.Store`) are one implementation; packs, the//! multi-pack-index and alternates sit behind the same interface, and a//! caller may supply its own. It is a runtime interface rather than a//! closed union so that adding a kind of storage does not mean editing//! everything that reads objects.//!//! Every read takes an optional `*Diagnostics`, filled in with the object,//! the file and a detail in git's words when the read fails on the//! repository's account.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const object = @import("object.zig");const Id = object.Id;const Kind = object.Kind;const Header = object.Header;const Diagnostics = @import("Diagnostics.zig");
const ObjectDatabase = @This();
ptr: *anyopaque,vtable: *const VTable,
/// What an implementation provides.pub const VTable = struct { contains: *const fn (ptr: *anyopaque, io: Io, id: Id, diag: ?*Diagnostics) ContainsError!bool, readHeader: *const fn (ptr: *anyopaque, io: Io, id: Id, diag: ?*Diagnostics) ReadError!Header, read: *const fn (ptr: *anyopaque, io: Io, gpa: Allocator, id: Id, diag: ?*Diagnostics) ReadError!Object, stream: *const fn (ptr: *anyopaque, io: Io, gpa: Allocator, id: Id, out: *Io.Writer, diag: ?*Diagnostics) StreamError!Header,};
/// An object read whole: its kind and its contents, which the caller owns.pub const Object = struct { kind: Kind, /// The contents, without the header. data: []const u8, /// What to free: `data` may be only part of it. allocation: []u8,
/// Frees the contents. pub fn deinit(o: *Object, gpa: Allocator) void { gpa.free(o.allocation); o.* = undefined; }};
/// Why an object could not be read. Each is a corruption git reports in/// words of its own, and the names follow what it found wrong:////// - `ObjectNotFound`: there is no such object./// - `ObjectUnreadable`: the storage could not be read; the diagnostic's/// detail names the system's error./// - `EmptyObjectFile`: "object file … is empty"./// - `HeaderInflateFailed`: the zlib stream was corrupt before the end of/// the object's header ("unable to unpack … header"); the detail is/// zlib's description, such as `data stream error (incorrect header/// check)`./// - `HeaderTooLong`, `MalformedHeader`, `ObjectSizeOverflow`,/// `UnknownObjectKind`: as `Header.parse` says./// - `ContentInflateFailed`: the zlib stream was corrupt after the header;/// the detail is zlib's description./// - `TruncatedObject`: the stored bytes ran out before the zlib stream/// ended./// - `ContentTooLong`: the object holds more than its header says ("corrupt/// loose object")./// - `ContentTooShort`: the object holds less than its header says. git/// does not notice, and shows whatever its buffer held past the end;/// this library refuses rather than invent the missing bytes./// - `TrailingGarbage`: bytes after the end of the zlib stream ("garbage/// at end of loose object")./// - `ReplaceDepthTooHigh`: read through replace refs, a chain of more/// replacements than git follows ("replace depth too high for object/// …"); the diagnostic's object ID is the original./// - `ReplacementNotFound`: read through replace refs, the replacement is/// not there ("replacement … not found for …"); the diagnostic's object/// ID is the original and its detail the replacement.////// Reading a packed object can also fail with the errors `PackError`/// lists, and finding one with those `PackOpenError` does: see there.pub const ReadError = PackError || PackOpenError || error{ ObjectNotFound, ObjectUnreadable, OutOfMemory, EmptyObjectFile, HeaderInflateFailed, HeaderTooLong, MalformedHeader, ObjectSizeOverflow, UnknownObjectKind, ContentInflateFailed, TruncatedObject, ContentTooLong, ContentTooShort, TrailingGarbage, ReplaceDepthTooHigh, ReplacementNotFound,};
/// Why a packed object could not be read, beyond the errors a loose one/// shares (`ContentInflateFailed`, `TruncatedObject`, `ContentTooLong` and/// `ContentTooShort` mean the same of a pack entry's zlib stream):////// - `TruncatedPackEntry`: an entry's header, or an OFS_DELTA's offset or/// a REF_DELTA's base, runs into the pack's checksum./// - `BadPackEntryType`: an entry of type 0 or 5, which mean nothing./// - `PackEntrySizeOverflow`: an entry's size too big for 64 bits./// - `BadDeltaBaseOffset`: an OFS_DELTA whose base would be at or before/// the start of the pack, or not before the delta itself./// - `BadLargeOffset`: a pack index or multi-pack-index entry pointing/// past the end of its large-offset table./// - `BadMultiPackIndexEntry`: a multi-pack-index entry naming a pack it/// does not list./// - `DeltaBaseMissing`: a REF_DELTA whose base is nowhere in the object/// database; the diagnostic's detail is the base's object ID./// - `DeltaChainTooDeep`: a chain of more than `pack.max_delta_depth`/// deltas, which a chain looping back on itself always is./// - The errors of `delta.Error`: a delta that cannot be applied to its/// base.pub const PackError = error{ TruncatedPackEntry, BadPackEntryType, PackEntrySizeOverflow, BadDeltaBaseOffset, BadLargeOffset, BadMultiPackIndexEntry, DeltaBaseMissing, DeltaChainTooDeep, TruncatedDelta, DeltaSizeOverflow, DeltaBaseSizeMismatch, DeltaReservedInstruction, DeltaCopyOutOfRange, DeltaResultTooLong, DeltaResultTooShort,};
/// Why a pack could not be used, each in git's words. git meets these/// when it first needs the pack, says so, and goes on looking elsewhere;/// so an object database returns one only for an object it found nowhere/// else, in place of `ObjectNotFound`, since the broken pack might have/// held it. The diagnostic's path names the file.////// - The errors of `PackIndex.Error`, for its pack index./// - `PackIndexMissing`: a pack the multi-pack-index names has no index/// ("packfile … index unavailable")./// - `PackTooSmall`: a pack too short to hold its header and checksum./// - `NotAPackfile`: "file … is not a GIT packfile"./// - `UnsupportedPackVersion`: "packfile … is version N and not/// supported"; git reads versions 2 and 3. The detail is the version./// - `PackDoesNotMatchIndex`: "packfile … does not match index": the/// checksum the pack ends with is not the one its index copied./// - `PackObjectCountMismatch`: the pack's header and its index disagree/// about how many objects it holds.pub const PackOpenError = error{ PackIndexTooSmall, UnsupportedPackIndexVersion, NonMonotonicPackIndex, WrongPackIndexSize, PackIndexMissing, PackTooSmall, NotAPackfile, UnsupportedPackVersion, PackDoesNotMatchIndex, PackObjectCountMismatch,};
/// Why an object could not be streamed: a read error, or the writer's.pub const StreamError = ReadError || error{WriteFailed};
/// Why the database could not say whether it holds an object: the/// storage could not be read, or the object is in none of the places that/// could be read while a pack that might hold it could not be/// (`PackOpenError`).pub const ContainsError = PackOpenError || error{ObjectUnreadable};
/// Whether the database holds the object, without reading it: an object/// that is there but corrupt is held, as `git cat-file -e` says. `diag`/// says what a failure was about.pub fn contains(db: ObjectDatabase, io: Io, id: Id, diag: ?*Diagnostics) ContainsError!bool { return db.vtable.contains(db.ptr, io, id, diag);}
/// The object's kind and size, reading no more of it than that takes, as/// `git cat-file -t` and `-s` do; so an object whose contents are corrupt/// may still have a header.pub fn readHeader(db: ObjectDatabase, io: Io, id: Id, diag: ?*Diagnostics) ReadError!Header { return db.vtable.readHeader(db.ptr, io, id, diag);}
/// The whole object, checked as git checks an object it reads whole: the/// contents must be exactly as long as the header says, and nothing may/// follow them. The object ID is not recomputed; `object.hash` does that/// for a caller that wants it, as `git cat-file -p` does for a tree.pub fn read(db: ObjectDatabase, io: Io, gpa: Allocator, id: Id, diag: ?*Diagnostics) ReadError!Object { return db.vtable.read(db.ptr, io, gpa, id, diag);}
/// Writes the object's contents to `out` as they are inflated, in memory/// bounded whatever the object's size, and returns its header. This is/// how git prints a blob, and it checks what git checks then: the stream/// must be whole, but its length is not compared with the header's size/// and anything after it is ignored. If the stream turns out to be corrupt/// part way, everything inflated before that has been written by the time/// the error is returned.pub fn stream(db: ObjectDatabase, io: Io, gpa: Allocator, id: Id, out: *Io.Writer, diag: ?*Diagnostics) StreamError!Header { return db.vtable.stream(db.ptr, io, gpa, id, out, diag);}