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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `zig-git`: some of git's commands answered by the library rather than//! by git, printing what git prints — its output, its error messages and//! its exit status — so that the two can be compared on any repository.//! It is for checking the library against git and for seeing how to use//! it, not for use in place of git.//!//! zig-git [-C <dir>] [-c <name>=<value>] [--git-dir=<path>] [--work-tree=<path>] [--no-replace-objects] <command> ...//! zig-git cat-file (-t | -s | -e | -p) <revision>//! zig-git cat-file <kind> <object>//! zig-git show-index [--object-format=<format>] < <pack-index>//! zig-git verify-pack [-v] [-s] [--] <pack>.idx...//! zig-git config [<option>...] (--get | --get-all | --get-regexp | --list) ...//! zig-git config (get | list) [<option>...] ...//! zig-git rev-parse [--path-format=<format>] <discovery-option>...//! zig-git rev-parse [<option>...] [<revision>...] [--] [<path>...]//! zig-git check-ref-format [--normalize] [--[no-]allow-onelevel] [--refspec-pattern] <refname>//! zig-git show-ref [<option>...] [--] [<pattern>...]//! zig-git show-ref --verify [<option>...] [--] <ref>...//! zig-git show-ref --exists <ref>//! zig-git for-each-ref [--format=<format>] [--sort=[-]refname] [--count=<n>] [<pattern>...]//! zig-git symbolic-ref [-q] [--short] [--[no-]recurse] <name>//! zig-git ls-files [<option>...] [--] [<path>...]//! zig-git rev-list [<option>...] <commit>... [--] [<path>...]//! zig-git merge-base [<option>...] <commit>...//! zig-git log [<option>...] [<revision-range>] [[--] <path>...]//! zig-git show (-s | --no-patch) [<option>...] [<object>...]//! zig-git check-mailmap [<option>...] <contact>...//! zig-git describe [<option>...] [<commit-ish>...]//! zig-git branch [<option>...] [--list] [<pattern>...]//! zig-git tag [<option>...] [--list] [<pattern>...]//! zig-git diff --no-index [<option>...] <path> <path>//! zig-git check-attr [<option>...] (-a | <attr>...) [--] <pathname>...//! zig-git diff-tree [<option>...] <tree-ish> [<tree-ish>] [[--] <path>...]//! zig-git diff-index [<option>...] <tree-ish> [[--] <path>...]//! zig-git diff-files [<option>...] [[--] <path>...]//!//! `config` reads configuration as `git config` does; see//! `config_command.zig`. `rev-parse` answers the questions about where a//! repository is; see `rev_parse.zig`. The ref commands are in//! `refs_command.zig`, and `ls-files` in `ls_files.zig`. `rev-list` and//! `log` are `rev_list.zig`, describing commits as `describe.zig` says;//! `show` and `check-mailmap` are `log.zig`; `describe` is//! `name_commits.zig`, and `branch` and `tag` are `branch_command.zig`. An object is named//! by any single revision (`revisions.zig`), and read through the whole//! object database://! loose objects, packs, the multi-pack-index and alternates,//! `GIT_ALTERNATE_OBJECT_DIRECTORIES` included. Every command finds its//! repository as git does, through the library's discovery//! (`discovery.zig`), honoring `-C`, `-c`, `--git-dir` and `--work-tree` before//! the command's name, and says what git says when there is none.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const git = @import("git");const ObjectId = git.ObjectId;const ObjectKind = git.ObjectKind;const ObjectFormat = git.ObjectFormat;const Diagnostics = git.Diagnostics;const config_command = @import("config_command.zig");const discovery = @import("discovery.zig");const rev_parse = @import("rev_parse.zig");const refs_command = @import("refs_command.zig");const ls_files = @import("ls_files.zig");const object_names = @import("object_names.zig");const revisions = @import("revisions.zig");const rev_list = @import("rev_list.zig");const merge_base = @import("merge_base.zig");const log = @import("log.zig");const name_commits = @import("name_commits.zig");const branch_command = @import("branch_command.zig");const diff_command = @import("diff_command.zig");const check_attr = @import("check_attr.zig");const diff_tree = @import("diff_tree.zig");const diff_index = @import("diff_index.zig");
pub fn main(init: std.process.Init) !void { const io = init.io; const args = try init.minimal.args.toSlice(init.arena.allocator()); var stdin_buffer: [64 * 1024]u8 = undefined; var stdin_reader: Io.File.Reader = .init(.stdin(), io, &stdin_buffer); var stdout_buffer: [64 * 1024]u8 = undefined; var stdout_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); var stderr_buffer: [1024]u8 = undefined; var stderr_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buffer); const code = run( .{ .io = io, .gpa = init.gpa, .env = init.environ_map, .cwd = "." }, args[1..], &stdin_reader.interface, &stdout_writer.interface, &stderr_writer.interface, ); stdout_writer.interface.flush() catch {}; stderr_writer.interface.flush() catch {}; std.process.exit(code);}
/// What a command runs with, passed in rather than reached for.pub const Context = struct { io: Io, gpa: Allocator, env: *const std.process.Environ.Map, /// The working directory the repository is found from. cwd: []const u8, /// `--git-dir`, which stands for `GIT_DIR`. git_dir: ?[]const u8 = null, /// `--work-tree`, which stands for `GIT_WORK_TREE`. work_tree: ?[]const u8 = null, /// The user whose repositories are trusted; the process's own if null. uid: ?u32 = null, /// `-c` settings. config_parameters: []const []const u8 = &.{}, /// False for `--no-replace-objects`. replace_objects: bool = true,};
const usage = \\usage: zig-git [-C <dir>] [-c <name>=<value>] [--git-dir=<path>] [--work-tree=<path>] [--no-replace-objects] <command> ... \\ zig-git cat-file (-t | -s | -e | -p) <object> \\ zig-git cat-file <kind> <object> \\ zig-git show-index [--object-format=<format>] < <pack-index> \\ zig-git verify-pack [-v] [-s] [--] <pack>.idx... \\ zig-git config [<option>...] (--get | --get-all | --get-regexp | --list) ... \\ zig-git config (get | list) [<option>...] ... \\ zig-git rev-parse [--path-format=<format>] <discovery-option>... \\ zig-git rev-parse [<option>...] [<revision>...] [--] [<path>...] \\ zig-git rev-list [<option>...] <commit>... [--] [<path>...] \\ zig-git merge-base [<option>...] <commit>... \\ zig-git log [<option>...] [<revision-range>] [[--] <path>...] \\ zig-git show (-s | --no-patch) [<option>...] [<object>...] \\ zig-git check-mailmap [<option>...] <contact>... \\ zig-git describe [<option>...] [<commit-ish>...] \\ zig-git branch [<option>...] [--list] [<pattern>...] \\ zig-git tag [<option>...] [--list] [<pattern>...] \\ zig-git diff --no-index [<option>...] <path> <path> \\ zig-git check-attr [<option>...] (-a | <attr>...) [--] <pathname>... \\ zig-git diff-tree [<option>...] <tree-ish> [<tree-ish>] [[--] <path>...] \\ zig-git diff-index [<option>...] <tree-ish> [[--] <path>...] \\ zig-git diff-files [<option>...] [[--] <path>...] \\;
/// Runs the command in `args`, as `main` would, and returns the status it/// would exit with: 0, 1 for a plain "no", 128 for a failure, and 129 for a/// command used wrongly, as git's are.pub fn run(ctx: Context, args: []const []const u8, stdin: *Io.Reader, stdout: *Io.Writer, stderr: *Io.Writer) u8 { runOrFail(ctx, args, stdin, stdout, stderr) catch |err| return switch (err) { error.Usage => usage: { stderr.writeAll(usage) catch {}; break :usage 129; }, error.UsageQuiet => 129, error.Fatal => 128, error.BadPattern => 6, error.No => 1, error.Missing => 2, error.Status255 => 255, error.CheckProblems => 2, error.CheckProblemsDiffer => 3, else => other: { stderr.print("fatal: {t}\n", .{err}) catch {}; break :other 128; }, }; return 0;}
const Failure = error{ /// The command was used wrongly. Usage, /// The command was used wrongly, and has said how. UsageQuiet, /// A regular expression would not compile, and it has said so. BadPattern, /// It failed, and has said why. Fatal, /// The answer is no, as `cat-file -e` says it, or a pack is bad, as /// `verify-pack` says it. No, /// There is no such thing, as `show-ref --exists` says it. Missing, /// It failed, has said why, and exits with 255, as git does where it /// returns -1 from a command. Status255, /// `--check` found problems (2), and `--exit-code` differences too (3). CheckProblems, CheckProblemsDiffer,};
fn fatal(stderr: *Io.Writer, comptime fmt: []const u8, args: anytype) Failure { stderr.print("fatal: " ++ fmt ++ "\n", args) catch {}; return error.Fatal;}
/// Says `fatal: ` and the message, and fails, as git's `die` does.pub fn fatalMessage(stderr: *Io.Writer, comptime fmt: []const u8, args: anytype) error{Fatal} { stderr.print("fatal: " ++ fmt ++ "\n", args) catch {}; return error.Fatal;}
fn runOrFail(given: Context, all_args: []const []const u8, stdin: *Io.Reader, stdout: *Io.Writer, stderr: *Io.Writer) !void { var ctx = given; var cwd: std.ArrayList(u8) = .empty; defer cwd.deinit(ctx.gpa); try cwd.appendSlice(ctx.gpa, ctx.cwd); var args = all_args; var parameters: std.ArrayList([]const u8) = .empty; defer parameters.deinit(ctx.gpa); try parameters.appendSlice(ctx.gpa, ctx.config_parameters); // git's own options, before the command. while (args.len != 0 and args[0].len > 1 and args[0][0] == '-') { const arg = args[0]; if (std.mem.eql(u8, arg, "--no-replace-objects")) { ctx.replace_objects = false; args = args[1..]; continue; } const Global = enum { @"-C", @"-c", @"--git-dir", @"--work-tree" }; const eq = std.mem.findScalar(u8, arg, '='); const g = std.meta.stringToEnum(Global, arg[0 .. eq orelse arg.len]) orelse return error.Usage; if ((g == .@"-C" or g == .@"-c") and eq != null) return error.Usage; const v = if (eq) |e| arg[e + 1 ..] else if (args.len > 1) v: { args = args[1..]; break :v args[0]; } else { stderr.print("no directory given for '{s}' option\n", .{arg}) catch {}; return error.Usage; }; args = args[1..]; switch (g) { .@"-C" => if (v.len != 0) { if (std.fs.path.isAbsolute(v)) cwd.clearRetainingCapacity() else try cwd.append(ctx.gpa, '/'); try cwd.appendSlice(ctx.gpa, v); var dir = Io.Dir.cwd().openDir(ctx.io, cwd.items, .{}) catch |err| return fatal(stderr, "cannot change to '{s}': {s}", .{ v, git.Config.reason(err) }); dir.close(ctx.io); }, .@"-c" => try parameters.append(ctx.gpa, v), .@"--git-dir" => ctx.git_dir = v, .@"--work-tree" => ctx.work_tree = v, } } ctx.cwd = cwd.items; ctx.config_parameters = parameters.items; if (args.len == 0) return error.Usage; const Name = enum { @"cat-file", @"show-index", @"verify-pack", config, @"rev-parse", @"check-ref-format", @"show-ref", @"for-each-ref", @"symbolic-ref", @"ls-files", @"rev-list", @"merge-base", log, whatchanged, show, @"check-mailmap", describe, @"name-rev", branch, tag, diff, @"check-attr", @"diff-tree", @"diff-index", @"diff-files" }; const name = std.meta.stringToEnum(Name, args[0]) orelse return error.Usage; switch (name) { .config => try config_command.run(ctx, args[1..], stdin, stdout, stderr), .@"cat-file" => try catFile(ctx, args[1..], stdout, stderr), .@"show-index" => try showIndex(ctx, args[1..], stdin, stdout, stderr), .@"verify-pack" => try verifyPack(ctx, args[1..], stdout, stderr), .@"rev-parse" => try rev_parse.run(ctx, args[1..], stdout, stderr), .@"check-ref-format" => try refs_command.checkRefFormat(ctx, args[1..], stdout, stderr), .@"show-ref" => try refs_command.showRef(ctx, args[1..], stdout, stderr), .@"for-each-ref" => try refs_command.forEachRef(ctx, args[1..], stdout, stderr), .@"symbolic-ref" => try refs_command.symbolicRef(ctx, args[1..], stdout, stderr), .@"ls-files" => try ls_files.run(ctx, args[1..], stdout, stderr), .@"rev-list" => try rev_list.run(ctx, args[1..], stdout, stderr), .@"merge-base" => try merge_base.run(ctx, args[1..], stdout, stderr), .log => try rev_list.runCommand(ctx, args[1..], stdout, stderr, .log), .whatchanged => try rev_list.whatchanged(ctx, args[1..], stdout, stderr), .show => try log.show(ctx, args[1..], stdout, stderr), .@"check-mailmap" => try log.checkMailmap(ctx, args[1..], stdin, stdout, stderr), .describe => try name_commits.describe(ctx, args[1..], stdout, stderr), // git's choice among the names that reach a commit was not // matched by any rule tried. .@"name-rev" => return fatal(stderr, "zig-git: 'name-rev' is not supported", .{}), .branch => try branch_command.branch(ctx, args[1..], stdout, stderr), .tag => try branch_command.tag(ctx, args[1..], stdout, stderr), .diff => try diff_command.run(ctx, args[1..], stdout, stderr), .@"check-attr" => try check_attr.run(ctx, args[1..], stdin, stdout, stderr), .@"diff-tree" => try diff_tree.run(ctx, args[1..], stdin, stdout, stderr), .@"diff-index" => try diff_index.runIndex(ctx, args[1..], stdout, stderr), .@"diff-files" => try diff_index.runFiles(ctx, args[1..], stdout, stderr), }}
/// The repository, found and opened as git would.const Repository = struct { repo: git.Repository,
fn open(ctx: Context, stderr: *Io.Writer) !Repository { return .{ .repo = try discovery.open(ctx, stderr) }; }
fn close(r: *Repository, ctx: Context) void { r.repo.close(ctx.io); }
/// Says what git says of what was wrong with the object database but /// worked around, when it first looks at it; with `midx_only`, what it /// says again each time it looks again for an object it did not find, /// which rereads the multi-pack-indexes but not the alternates. fn reportProblems(r: *const Repository, stderr: *Io.Writer, midx_only: bool) !void { for (r.repo.database.problems) |p| switch (p.err) { error.UnnormalizableAlternate, error.MissingAlternate, error.AlternatesTooDeep => if (midx_only) continue else switch (p.err) { error.UnnormalizableAlternate => try stderr.print("error: unable to normalize alternate object path: {s}\n", .{p.path}), error.MissingAlternate => try stderr.print("error: object directory {s} does not exist; check .git/objects/info/alternates\n", .{p.path}), else => try stderr.print("error: {s}: ignoring alternate object stores, nesting too deep\n", .{p.path}), }, error.MultiPackIndexTooSmall => try stderr.print("error: multi-pack-index file {s} is too small\n", .{p.path}), error.BadMultiPackIndexChunkOffsets => try stderr.print("error: improper chunk offset(s) {s}\n", .{p.detail}), error.MultiPackIndexHashMismatch => try stderr.print("error: multi-pack-index hash version {s}\n", .{p.detail}), else => |e| try stderr.print("error: {s}: {t}\n", .{ p.path, e }), }; }};
// -- cat-file ----------------------------------------------------------------
const CatFileMode = union(enum) { type, size, exists, pretty, kind: ObjectKind,};
fn catFile(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { var option: ?CatFileMode = null; var operands: [2][]const u8 = undefined; var n: usize = 0; var only_operands = false; for (args) |arg| { if (!only_operands and arg.len > 1 and arg[0] == '-') { if (std.mem.eql(u8, arg, "--")) { only_operands = true; continue; } const m: CatFileMode = if (std.mem.eql(u8, arg, "-t")) .type else if (std.mem.eql(u8, arg, "-s")) .size else if (std.mem.eql(u8, arg, "-e")) .exists else if (std.mem.eql(u8, arg, "-p")) .pretty else return error.Usage; if (option != null) return error.Usage; option = m; continue; } if (n == operands.len) return error.Usage; operands[n] = arg; n += 1; } const mode: CatFileMode, const name = if (option) |m| m: { if (n != 1) return error.Usage; break :m .{ m, operands[0] }; } else k: { if (n != 2) return error.Usage; const kind = ObjectKind.fromName(operands[0]) orelse return fatal(stderr, "invalid object type \"{s}\"", .{operands[0]}); break :k .{ .{ .kind = kind }, operands[1] }; };
var repo = try Repository.open(ctx, stderr); defer repo.close(ctx); const id = id: { var names: object_names.Names = .init(ctx, &repo.repo, try object_names.settings(&repo.repo.config, stderr)); defer names.deinit(); var revs = try revisions.Revisions.init(ctx, &repo.repo, names.settings, .cat_file, stderr); defer revs.deinit(); var diag: Diagnostics = .{}; const found = revs.resolver.resolve(name, &diag) catch |err| { try revs.flushMessages(stderr, 1, false); try revisions.stopAt(err, &diag, false, false, stderr); if (err == error.AmbiguousObjectName) try revs.complain(&names, stderr); if (git.Resolver.severity(err) == .diagnosed) return fatal(stderr, "{s}", .{diag.detail()}); return fatal(stderr, "Not a valid object name {s}", .{name}); }; try revs.flushMessages(stderr, 1, false); break :id found.id; }; try repo.reportProblems(stderr, false); if (mode != .exists) try @import("history.zig").reportReplacements(&repo.repo, stderr); const c: CatFile = .{ .ctx = ctx, .repo = &repo, .db = repo.repo.objects(), .stdout = stdout, .stderr = stderr, .name = name, .id = id }; switch (mode) { .exists => { var diag: Diagnostics = .{}; const there = c.db.contains(ctx.io, id, &diag) catch |err| switch (err) { error.ObjectUnreadable => return error.Fatal, else => |e| { try c.notFound(e, &diag); return error.No; }, }; if (!there) { try c.notFound(error.ObjectNotFound, &diag); return error.No; } }, .type, .size => { var diag: Diagnostics = .{}; const header = c.db.readHeader(ctx.io, id, &diag) catch |err| { if (try c.missing(err, &diag) or try describe(stderr, err, &diag) == .err) return fatal(stderr, "git cat-file: could not get object info", .{}); return error.Fatal; }; if (mode == .type) try stdout.print("{s}\n", .{header.kind.name()}) else try stdout.print("{d}\n", .{header.size}); }, .pretty => try c.pretty(), .kind => |want| try c.ofKind(want), }}
const CatFile = struct { ctx: Context, repo: *const Repository, db: git.ObjectDatabase, stdout: *Io.Writer, stderr: *Io.Writer, /// The object as it was named. name: []const u8, id: ObjectId,
/// Says what git says on not finding an object, or on finding no pack /// it could open that might have held it: it looks again, rereading /// the multi-pack-indexes, and finds the same. fn notFound(c: CatFile, err: anyerror, diag: *const Diagnostics) !void { if (isPackOpenError(err)) try describePackOpen(c.stderr, err, diag); try c.repo.reportProblems(c.stderr, true); if (isPackOpenError(err)) try describePackOpen(c.stderr, err, diag); }
/// Whether `err` means the object was not found, having said what git /// says of that. fn missing(c: CatFile, err: anyerror, diag: *const Diagnostics) !bool { // git does not look outside a pack for a REF_DELTA's base, and // finds an object whose base is elsewhere no more than one whose // base is nowhere. if (err != error.ObjectNotFound and err != error.DeltaBaseMissing and err != error.ReplacementNotFound and !isPackOpenError(err)) return false; try c.notFound(err, diag); return true; }
fn pretty(c: CatFile) !void { var diag: Diagnostics = .{}; const header = c.db.readHeader(c.ctx.io, c.id, &diag) catch |err| { if (try c.missing(err, &diag) or try describe(c.stderr, err, &diag) == .err) return fatal(c.stderr, "Not a valid object name {s}", .{c.name}); return error.Fatal; }; switch (header.kind) { .blob => return c.streamBlob(c.id), .tree => { var obj = try c.readWhole(c.id) orelse return fatal(c.stderr, "Not a valid object name {s}", .{c.name}); defer obj.deinit(c.ctx.gpa); // `-p` is the one place `cat-file` checks an object's ID: // the ID of what it read, a replacement if it was replaced. const read_id = c.repo.repo.replacements.resolve(c.id, null) catch c.id; if (!ObjectId.eql(read_id, git.hashObject(c.id.object_format, obj.kind, obj.data))) { try c.stderr.print("error: hash mismatch {f}\n", .{read_id}); return fatal(c.stderr, "not a tree object", .{}); } return c.printTree(obj.data); }, .commit, .tag => { var obj = try c.readWhole(c.id) orelse return fatal(c.stderr, "Not a valid object name {s}", .{c.name}); defer obj.deinit(c.ctx.gpa); try c.stdout.writeAll(obj.data); }, } }
/// `cat-file <kind> <object>`: the object, or what it leads to by way /// of tags, or from a commit to its tree. fn ofKind(c: CatFile, want: ObjectKind) !void { if (want == .blob) { // git asks a blob's kind before reading it, so a header it // cannot read is reported more than once. var diag: Diagnostics = .{}; if (c.db.readHeader(c.ctx.io, c.id, &diag)) |header| { if (header.kind == .blob) return c.streamBlob(c.id); } else |err| switch (err) { else => { // Its kind unknown, git reads it whole, and then the // pack entry it found is corrupt. if (err == error.DeltaBaseMissing) return c.corrupt(&diag); if (err == error.ObjectNotFound or isPackOpenError(err)) { // git looks three times. for (0..3) |_| try c.notFound(err, &diag); return fatal(c.stderr, "git cat-file {s}: bad file", .{c.name}); } for (0..3) |_| if (try describe(c.stderr, err, &diag) == .fatal) return error.Fatal; return c.corrupt(&diag); }, } } var id = c.id; while (true) { var obj = try c.readWhole(id) orelse return fatal(c.stderr, "git cat-file {s}: bad file", .{c.name}); defer obj.deinit(c.ctx.gpa); if (obj.kind == want) return c.stdout.writeAll(obj.data); id = switch (obj.kind) { .tag => git.Tag.targetOf(id.object_format, obj.data) orelse { if (want == .blob) return fatal(c.stderr, "{f} not a valid tag", .{id}); return fatal(c.stderr, "git cat-file {s}: bad file", .{c.name}); }, // Whatever the commit's tree line names, even if it is // not a tree: git follows it as far as it leads. .commit => git.Commit.treeOf(id.object_format, obj.data) orelse return fatal(c.stderr, "git cat-file {s}: bad file", .{c.name}), else => return fatal(c.stderr, "git cat-file {s}: bad file", .{c.name}), }; } }
/// The whole object, or null if there is none; any other failure has /// been reported. fn readWhole(c: CatFile, id: ObjectId) !?git.ObjectDatabase.Object { var diag: Diagnostics = .{}; return c.db.read(c.ctx.io, c.ctx.gpa, id, &diag) catch |err| switch (err) { error.OutOfMemory => err, error.DeltaBaseMissing => { try c.stderr.print("error: failed to validate delta base reference at offset {d} from {s}\n", .{ diag.offset orelse 0, diag.path() orelse "" }); return c.corrupt(&diag); }, else => { if (try c.missing(err, &diag)) return null; if (try describe(c.stderr, err, &diag) == .fatal) return error.Fatal; return c.corrupt(&diag); }, }; }
fn corrupt(c: CatFile, diag: *const Diagnostics) Failure { const where = if (isPacked(diag)) "packed" else "loose"; return fatal(c.stderr, "{s} object {f} (stored in {s}) is corrupt", .{ where, diag.object_id.?, diag.path() orelse "" }); }
/// Prints a blob as git does, as it is inflated. git writes it 16 KiB /// at a time, so a blob found corrupt part way prints the whole 16 KiB /// pieces that came before the corruption, and nothing of the rest. fn streamBlob(c: CatFile, id: ObjectId) !void { var aw: Io.Writer.Allocating = .init(c.ctx.gpa); defer aw.deinit(); var diag: Diagnostics = .{}; _ = c.db.stream(c.ctx.io, c.ctx.gpa, id, &aw.writer, &diag) catch |err| { if (!isPacked(&diag)) { const piece = 16 * 1024; const whole = aw.written().len / piece * piece; try c.stdout.writeAll(aw.written()[0..whole]); } return c.streamFailed(id, err, &diag); }; try c.stdout.writeAll(aw.written()); }
fn streamFailed(c: CatFile, id: ObjectId, err: git.ObjectDatabase.StreamError, diag: *const Diagnostics) !void { switch (err) { error.WriteFailed, error.OutOfMemory => return error.OutOfMemory, else => |e| if (isPacked(diag)) { // git reads a packed blob whole rather than streaming it. if (try describe(c.stderr, e, diag) == .fatal) return error.Fatal; return c.corrupt(diag); }, } switch (err) { error.ContentInflateFailed => { try c.stderr.print("error: inflate: {s}\n", .{diag.detail()}); return fatal(c.stderr, "unable to stream {f} to stdout", .{id}); }, error.TruncatedObject => return fatal(c.stderr, "unable to stream {f} to stdout", .{id}), error.WriteFailed, error.OutOfMemory => unreachable, else => |e| { if (try describe(c.stderr, e, diag) == .fatal) return error.Fatal; return fatal(c.stderr, "unable to stream {f} to stdout", .{id}); }, } }
fn printTree(c: CatFile, contents: []const u8) !void { var aw: Io.Writer.Allocating = .init(c.ctx.gpa); defer aw.deinit(); const w = &aw.writer; var it = git.tree.iterate(c.id.object_format, contents); while (it.next() catch |err| return fatal(c.stderr, "{s}", .{switch (err) { error.TruncatedTree => "too-short tree object", error.MalformedMode => "malformed mode in tree entry", error.EmptyName => "empty filename in tree entry", }})) |entry| { const mode = entry.mode(); try w.print("{o:0>6} {s} {f}\t", .{ @backingInt(mode), mode.kind().name(), entry.id }); try git.quote.writePath(w, entry.name); try w.writeByte('\n'); } try c.stdout.writeAll(aw.written()); }};
/// Whether the failure was in a pack rather than a loose object.fn isPacked(diag: *const Diagnostics) bool { return std.mem.endsWith(u8, diag.path() orelse "", ".pack");}
fn isPackOpenError(err: anyerror) bool { inline for (@typeInfo(git.ObjectDatabase.PackOpenError).error_set.error_names.?) |name| { if (err == @field(anyerror, name)) return true; } return false;}
const Severity = enum { err, fatal };
/// Writes what git says of a failed read: the "error: " lines it goes on/// from, or the "fatal: " line it stops at, which `Severity` tells apart.fn describe(w: *Io.Writer, err: git.ObjectDatabase.ReadError, diag: *const Diagnostics) !Severity { const id = diag.object_id orelse ObjectId.zero(.sha1); const path = diag.path() orelse ""; const packed_object = isPacked(diag); switch (err) { error.EmptyObjectFile => try w.print("error: object file {s} is empty\n", .{path}), error.HeaderInflateFailed => try w.print("error: inflate: {s}\nerror: unable to unpack {f} header\n", .{ diag.detail(), id }), error.HeaderTooLong => try w.print("error: header for {f} too long, exceeds 32 bytes\n", .{id}), error.MalformedHeader => try w.print("error: unable to parse {f} header\n", .{id}), error.ObjectSizeOverflow => { try w.print("fatal: size_t overflow: {s}\n", .{diag.detail()}); return .fatal; }, error.UnknownObjectKind => { try w.writeAll("fatal: invalid object type\n"); return .fatal; }, error.ContentInflateFailed => if (packed_object) try w.print("error: inflate: {s}\n", .{diag.detail()}) else try w.print("error: inflate: {s}\nerror: corrupt loose object '{f}'\n", .{ diag.detail(), id }), error.TruncatedObject, error.ContentTooLong, error.ContentTooShort => if (!packed_object) try w.print("error: corrupt loose object '{f}'\n", .{id}), error.TrailingGarbage => try w.print("error: garbage at end of loose object '{f}'\n", .{id}), error.ObjectUnreadable => try w.print("error: unable to read {s}: {s}\n", .{ path, diag.detail() }), error.ObjectNotFound => try w.print("error: {f}: no such object\n", .{id}), error.OutOfMemory => { try w.writeAll("fatal: out of memory\n"); return .fatal; }, error.ReplaceDepthTooHigh => { try w.print("fatal: replace depth too high for object {f}\n", .{id}); return .fatal; }, // A pack git could not open: it says so, looks again, and says so // again. error.PackIndexTooSmall, error.UnsupportedPackIndexVersion, error.NonMonotonicPackIndex, error.WrongPackIndexSize, error.PackIndexMissing, error.PackTooSmall, error.NotAPackfile, error.UnsupportedPackVersion, error.PackDoesNotMatchIndex, error.PackObjectCountMismatch, => |e| try describePackOpen(w, e, diag), // What git says of these has not been matched; the name of the // error stands in for it. else => |e| try w.print("error: {s}: {t} {s}\n", .{ path, e, diag.detail() }), } return .err;}
/// What git says of a pack it could not open.fn describePackOpen(w: *Io.Writer, err: anyerror, diag: *const Diagnostics) !void { const path = diag.path() orelse ""; switch (err) { error.PackIndexTooSmall => try w.print("error: index file {s} is too small\n", .{path}), error.UnsupportedPackIndexVersion => try w.print("error: index file {s} is version {s} and is not supported by this binary (try upgrading GIT to a newer version)\n", .{ path, diag.detail() }), error.NonMonotonicPackIndex => try w.print("error: non-monotonic index {s}\n", .{path}), error.WrongPackIndexSize => try w.print("error: wrong index v{s} file size in {s}\n", .{ diag.detail(), path }), error.PackIndexMissing => try w.print("error: packfile {s} index unavailable\n", .{path}), error.NotAPackfile => try w.print("error: file {s} is not a GIT packfile\n", .{path}), error.UnsupportedPackVersion => try w.print("error: packfile {s} is version {s} and not supported (try upgrading GIT to a newer version)\n", .{ path, diag.detail() }), error.PackDoesNotMatchIndex => try w.print("error: packfile {s} does not match index\n", .{path}), else => try w.print("error: packfile {s}: {t}\n", .{ path, err }), }}
// -- show-index --------------------------------------------------------------
/// `show-index`: the pack index on standard input, an object a line, read/// as git reads it: a piece at a time, so that a truncated index prints/// what came before the point it ends, and says where that was.fn showIndex(ctx: Context, args: []const []const u8, stdin: *Io.Reader, stdout: *Io.Writer, stderr: *Io.Writer) !void { var format: ?ObjectFormat = null; var a: usize = 0; while (a < args.len) : (a += 1) { const arg = args[a]; const value = if (std.mem.startsWith(u8, arg, "--object-format=")) arg["--object-format=".len..] else if (std.mem.eql(u8, arg, "--object-format") and a + 1 < args.len) v: { a += 1; break :v args[a]; } else return error.Usage; format = ObjectFormat.fromName(value) orelse return fatal(stderr, "unknown hash algorithm '{s}'", .{value}); } if (format == null) format = discovery.formatIfAny(ctx) orelse f: { try stderr.writeAll("warning: assuming SHA-1; use --object-format to override\n"); break :f .sha1; }; const f = format.?; const h = f.byteLength(); var aw: Io.Writer.Allocating = .init(ctx.gpa); defer aw.deinit(); _ = stdin.streamRemaining(&aw.writer) catch return fatal(stderr, "unable to read header", .{}); const bytes = aw.written();
if (bytes.len < 8) return fatal(stderr, "unable to read header", .{}); var fanout: [256]u32 = undefined; var version: u32 = 1; var at: usize = 0; if (std.mem.eql(u8, bytes[0..4], git.PackIndex.magic)) { version = std.mem.readInt(u32, bytes[4..8], .big); if (version != 2) return fatal(stderr, "unknown index version", .{}); at = 8; } if (bytes.len < at + 1024) return fatal(stderr, "unable to read index", .{}); for (&fanout, 0..) |*e, i| e.* = std.mem.readInt(u32, bytes[at + 4 * i ..][0..4], .big); at += 1024; for (1..256) |i| if (fanout[i] < fanout[i - 1]) return fatal(stderr, "corrupt index file", .{}); const n: u64 = fanout[255]; const rest = bytes[at..];
if (version == 1) { var k: u64 = 0; while (k < n) : (k += 1) { const e = (4 + h) * k; if (rest.len < e + 4 + h) return fatal(stderr, "unable to read entry {d}/{d}", .{ k, n }); const offset = std.mem.readInt(u32, rest[@intCast(e)..][0..4], .big); const id: ObjectId = .fromBytes(f, rest[@intCast(e + 4)..][0..h]); try stdout.print("{d} {f}\n", .{ offset, id }); } return; } // Version 2: every table read whole before anything is printed. const names_len = n * h; if (rest.len < names_len) return fatal(stderr, "unable to read sha1 {d}/{d}", .{ rest.len / h, n }); if (rest.len - names_len < n * 4) return fatal(stderr, "unable to read crc {d}/{d}", .{ (rest.len - names_len) / 4, n }); const crcs_end = names_len + n * 4; if (rest.len - crcs_end < n * 4) return fatal(stderr, "unable to read 32b offset {d}/{d}", .{ (rest.len - crcs_end) / 4, n }); var large_at: u64 = crcs_end + n * 4; var next_large: u32 = 0; var k: u64 = 0; while (k < n) : (k += 1) { const id: ObjectId = .fromBytes(f, rest[@intCast(h * k)..][0..h]); const crc = std.mem.readInt(u32, rest[@intCast(names_len + 4 * k)..][0..4], .big); const raw = std.mem.readInt(u32, rest[@intCast(crcs_end + 4 * k)..][0..4], .big); var offset: u64 = raw; if (raw & 0x8000_0000 != 0) { if (raw & 0x7fff_ffff != next_large) return fatal(stderr, "inconsistent 64b offset index", .{}); if (rest.len < large_at + 8) return fatal(stderr, "unable to read 64b offset {d}", .{next_large}); offset = std.mem.readInt(u64, rest[@intCast(large_at)..][0..8], .big); large_at += 8; next_large += 1; } try stdout.print("{d} {f} ({x:0>8})\n", .{ offset, id, crc }); }}
// -- verify-pack -------------------------------------------------------------
/// `verify-pack`: checks each pack and its index, and with `-v` lists the/// pack's objects and the lengths of its delta chains.fn verifyPack(ctx: Context, args: []const []const u8, stdout: *Io.Writer, stderr: *Io.Writer) !void { var verbose = false; var stat_only = false; var paths: std.ArrayList([]const u8) = .empty; defer paths.deinit(ctx.gpa); var only_operands = false; for (args) |arg| { if (!only_operands and arg.len > 1 and arg[0] == '-') { if (std.mem.eql(u8, arg, "--")) { only_operands = true; } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--verbose")) { verbose = true; } else if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--stat-only")) { stat_only = true; } else return error.Usage; continue; } try paths.append(ctx.gpa, arg); } if (paths.items.len == 0) return error.Usage; const format = discovery.formatIfAny(ctx) orelse .sha1; var cwd = try Io.Dir.cwd().openDir(ctx.io, ctx.cwd, .{}); defer cwd.close(ctx.io); var all_ok = true; for (paths.items) |path| { const ok = try verifyOne(ctx, cwd, format, path, verbose, stat_only, stdout, stderr); if (!ok) all_ok = false; } if (!all_ok) return error.No;}
fn verifyOne(ctx: Context, cwd: Io.Dir, format: ObjectFormat, path: []const u8, verbose: bool, stat_only: bool, stdout: *Io.Writer, stderr: *Io.Writer) !bool { _ = stderr; const gpa = ctx.gpa; const base = if (std.mem.endsWith(u8, path, ".idx")) path[0 .. path.len - 4] else if (std.mem.endsWith(u8, path, ".pack")) path[0 .. path.len - 5] else path; const idx_path = try std.fmt.allocPrint(gpa, "{s}.idx", .{base}); defer gpa.free(idx_path); const pack_path = try std.fmt.allocPrint(gpa, "{s}.pack", .{base}); defer gpa.free(pack_path);
const verdict = check: { const idx_bytes = cwd.readFileAlloc(ctx.io, idx_path, gpa, .unlimited) catch break :check false; defer gpa.free(idx_bytes); const pack_bytes = cwd.readFileAlloc(ctx.io, pack_path, gpa, .unlimited) catch break :check false; defer gpa.free(pack_bytes); const index = git.PackIndex.parse(format, idx_bytes, null) catch break :check false; const p = git.pack.Pack.init(format, pack_bytes, index, pack_path, null) catch break :check false; if (!stat_only) { if (!git.PackIndex.checksumMatches(format, pack_bytes)) break :check false; if (!try verifyObjects(ctx, &p)) break :check false; } if (verbose or stat_only) if (!try listObjects(ctx, &p, verbose and !stat_only, stdout)) break :check false; if (stat_only) return true; // git checks the index against the pack only after reading the // whole pack, and so after printing what it found there. break :check index.verifyChecksum() and try crcsMatch(ctx, &p); }; // Only `-v` says how it went; otherwise the exit status does. if (verbose) try stdout.print("{s}: {s}\n", .{ pack_path, if (verdict) "ok" else "bad" }); return verdict;}
/// Where each object's entry ends: the next entry's offset, or the pack's/// checksum. Indexed like the pack index.fn entryEnds(gpa: Allocator, p: *const git.pack.Pack) !struct { order: []u32, offsets: []u64 } { const n = p.index.count; const order = try gpa.alloc(u32, n); errdefer gpa.free(order); const offsets = try gpa.alloc(u64, n); errdefer gpa.free(offsets); for (order, offsets, 0..) |*o, *off, i| { o.* = @intCast(i); off.* = try p.index.offsetAt(@intCast(i), null); } std.mem.sort(u32, order, offsets, struct { fn lt(offs: []u64, a: u32, b: u32) bool { return offs[a] < offs[b]; } }.lt); return .{ .order = order, .offsets = offsets };}
/// Whether every object's entry has the CRC the index records for it.fn crcsMatch(ctx: Context, p: *const git.pack.Pack) !bool { const ends = entryEnds(ctx.gpa, p) catch return false; defer ctx.gpa.free(ends.order); defer ctx.gpa.free(ends.offsets); const data = p.data(); for (ends.order, 0..) |i, k| { const offset = ends.offsets[i]; const end = if (k + 1 < ends.order.len) ends.offsets[ends.order[k + 1]] else data.len; if (p.index.crcAt(i)) |crc| { if (std.hash.Crc32.hash(data[@intCast(offset)..@intCast(end)]) != crc) return false; } } return true;}
/// Whether every object in the pack reads and hashes to the ID its index/// gives it.fn verifyObjects(ctx: Context, p: *const git.pack.Pack) !bool { const gpa = ctx.gpa; const ends = entryEnds(gpa, p) catch return false; defer gpa.free(ends.order); defer gpa.free(ends.offsets); const data_end = p.data().len; for (ends.order, 0..) |i, k| { const offset = ends.offsets[i]; const end = if (k + 1 < ends.order.len) ends.offsets[ends.order[k + 1]] else data_end; if (offset >= end or end > data_end) return false; var obj = p.read(ctx.io, gpa, offset, null, .{}, null) catch return false; defer obj.deinit(gpa); if (!ObjectId.eql(p.index.idAt(i), git.hashObject(p.format, obj.kind, obj.data))) return false; } return true;}
/// Prints the objects in pack order, if `verbose`, and the histogram of/// delta chain lengths. Returns false if an entry cannot be read.fn listObjects(ctx: Context, p: *const git.pack.Pack, verbose: bool, stdout: *Io.Writer) !bool { const gpa = ctx.gpa; const ends = entryEnds(gpa, p) catch return false; defer gpa.free(ends.order); defer gpa.free(ends.offsets); var chains: std.ArrayList(u64) = .empty; defer chains.deinit(gpa); var aw: Io.Writer.Allocating = .init(gpa); defer aw.deinit(); const w = &aw.writer; const data_end = p.data().len; for (ends.order, 0..) |i, k| { const offset = ends.offsets[i]; const end = if (k + 1 < ends.order.len) ends.offsets[ends.order[k + 1]] else data_end; const entry = p.entryHeader(offset, null) catch return false; const header = p.readHeader(ctx.io, offset, null, .{}, null) catch return false; try w.print("{f} {s: <6} {d} {d} {d}", .{ p.index.idAt(i), header.kind.name(), entry.size, end - offset, offset }); // How deep the chain under it goes, and its immediate base. var depth: u64 = 0; var e = entry; var base_id: ?ObjectId = null; while (true) { const next: u64 = switch (e.base) { .none => break, .offset => |o| o, .id => |id| (p.find(id, null) catch return false) orelse return false, }; if (base_id == null) base_id = switch (e.base) { .id => |id| id, else => idAtOffset(p, ends.order, ends.offsets, next) orelse return false, }; depth += 1; if (depth > git.pack.max_delta_depth) return false; e = p.entryHeader(next, null) catch return false; } if (base_id) |b| try w.print(" {d} {f}", .{ depth, b }); try w.writeByte('\n'); if (depth >= chains.items.len) try chains.appendNTimes(gpa, 0, @intCast(depth + 1 - chains.items.len)); chains.items[@intCast(depth)] += 1; } if (verbose) try stdout.writeAll(aw.written()); for (chains.items, 0..) |count, depth| { if (count == 0) continue; const noun = if (count == 1) "object" else "objects"; if (depth == 0) { try stdout.print("non delta: {d} {s}\n", .{ count, noun }); } else { try stdout.print("chain length = {d}: {d} {s}\n", .{ depth, count, noun }); } } return true;}
fn idAtOffset(p: *const git.pack.Pack, order: []const u32, offsets: []const u64, offset: u64) ?ObjectId { var lo: usize = 0; var hi: usize = order.len; while (lo < hi) { const mid = lo + (hi - lo) / 2; const o = offsets[order[mid]]; if (o == offset) return p.index.idAt(order[mid]); if (o < offset) lo = mid + 1 else hi = mid; } return null;}
// -- tests -------------------------------------------------------------------
const testing = std.testing;pub const Repo = git.test_fixture.Repo;const zlibStored = git.test_fixture.zlibStored;
const Outcome = Repo.Outcome;
/// Where and how to run a command for comparison.pub const Run = struct { /// The directory to run in, relative to the repository. cwd: []const u8 = ".", /// What to give it on standard input. input: []const u8 = "", compare: Compare = .everything, /// The user zig-git is to trust repositories of, in place of the /// process's own. uid: ?u32 = null,};
/// What `zig-git <args>` prints and exits with in `repo`.pub fn runOurs(repo: *Repo, args: []const []const u8, how: Run) !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(how.input); const cwd = try std.fs.path.join(gpa, &.{ repo.path, how.cwd }); defer gpa.free(cwd); const code = run(.{ .io = repo.io, .gpa = gpa, .env = &repo.env, .cwd = cwd, .uid = how.uid }, args, &stdin, &out.writer, &err.writer); return .{ .stdout = try out.toOwnedSlice(), .stderr = try err.toOwnedSlice(), .code = code };}
pub const Compare = enum { /// Standard output, standard error and the exit status. everything, /// Standard output and the exit status: for usage errors, whose /// messages are git's own, and for failures whose messages this does /// not match. output,};
/// Checks that `zig-git <args>` prints what `git <args>` does, and exits/// as it does.fn expectSameAsGit(repo: *Repo, args: []const []const u8, compare: Compare) !void { return expectSameAsGitHow(repo, args, .{ .compare = compare });}
/// `expectSameAsGit`, run as `how` says.pub fn expectSameAsGitHow(repo: *Repo, args: []const []const u8, how: Run) !void { const theirs = try repo.runIn(how.cwd, args, how.input); defer theirs.deinit(repo.gpa); return expectSameAs(repo, theirs, args, how);}
/// Checks that `zig-git <args>` prints what git printed, `theirs`, and/// exits as it did.pub fn expectSameAs(repo: *Repo, theirs: Outcome, args: []const []const u8, how: Run) !void { const ours = try runOurs(repo, args, how); defer ours.deinit(repo.gpa); errdefer { std.debug.print("zig-git", .{}); for (args) |a| std.debug.print(" {s}", .{a}); std.debug.print(" (in {s})\n", .{how.cwd}); } try testing.expectEqualStrings(theirs.stdout, ours.stdout); if (how.compare == .everything) 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 });}
fn revParse(repo: *Repo, rev: []const u8) ![]u8 { const out = try repo.output(&.{ "rev-parse", rev }); defer repo.gpa.free(out); return repo.gpa.dupe(u8, std.mem.trimEnd(u8, out, "\n"));}
/// Every way of asking `cat-file` about one object.fn expectCatFileSameAsGit(repo: *Repo, id: []const u8) !void { for ([_][]const u8{ "-t", "-s", "-e", "-p", "blob", "tree", "commit", "tag" }) |mode| { try expectSameAsGit(repo, &.{ "cat-file", mode, id }, .everything); }}
test "zig-git cat-file prints what git prints" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try writeFile(&repo, "README", "hello\n"); try writeFile(&repo, "src/main.c", "int main(void) { return 0; }\n"); try writeFile(&repo, "src/lib/util.c", "void util(void) {}\n"); try writeFile(&repo, "tab\there", "tab\n"); try writeFile(&repo, "caf\xc3\xa9", "unicode\n"); try writeFile(&repo, "quote\"d", "quote\n"); try writeFile(&repo, "run.sh", "#!/bin/sh\n"); try writeFile(&repo, "empty", ""); try repo.tmp.dir.symLink(repo.io, "README", "link", .{}); try repo.git(&.{ "add", "." }); try repo.git(&.{ "update-index", "--chmod=+x", "run.sh" }); try repo.commit("first"); const first = try revParse(&repo, "HEAD"); defer gpa.free(first); const info = try std.fmt.allocPrint(gpa, "160000,{s},vendor/sub", .{first}); defer gpa.free(info); try repo.git(&.{ "update-index", "--add", "--cacheinfo", info }); try repo.commit("a submodule"); try repo.git(&.{ "tag", "-a", "-m", "version one", "v1" }); try repo.git(&.{ "tag", "-a", "-m", "a tag of a tag", "v1-again", "v1" }); try repo.git(&.{ "tag", "-a", "-m", "a tag of a tree", "tree-tag", "HEAD^{tree}" }); try repo.git(&.{ "tag", "-a", "-m", "a tag of a blob", "blob-tag", "HEAD:README" }); try writeFile(&repo, "README", "second\n"); try repo.git(&.{ "commit", "-q", "-a", "-m", "second\n\nwith a body" });
for ([_][]const u8{ "HEAD", "HEAD~", "HEAD^{tree}", "HEAD:src", "HEAD:README", "HEAD:empty", "HEAD:link", "v1", "v1-again", "tree-tag", "blob-tag" }) |rev| { const id = try revParse(&repo, rev); defer gpa.free(id); try expectCatFileSameAsGit(&repo, id); }
// An object ID in capitals, and one with no object. const head = try revParse(&repo, "HEAD"); defer gpa.free(head); var upper: [git.object_max_hex_length]u8 = undefined; try expectCatFileSameAsGit(&repo, std.ascii.upperString(upper[0..head.len], head)); @memset(upper[0..head.len], '1'); try expectCatFileSameAsGit(&repo, upper[0..head.len]);}
test "zig-git cat-file is used as git's is" { var repo = try Repo.init(testing.allocator, testing.io); defer repo.deinit(); try repo.commit("first"); const head = try revParse(&repo, "HEAD"); defer testing.allocator.free(head); for ([_][]const []const u8{ &.{"cat-file"}, &.{ "cat-file", "-t" }, &.{ "cat-file", "blob" }, &.{ "cat-file", "-x", head }, &.{ "cat-file", "-t", head, head }, &.{ "cat-file", "-t", "-s", head }, &.{ "cat-file", "-e", "-p", head }, }) |args| try expectSameAsGit(&repo, args, .output); try expectSameAsGit(&repo, &.{ "cat-file", "foo", head }, .everything); try expectSameAsGit(&repo, &.{ "cat-file", "Blob", head }, .everything); try expectSameAsGit(&repo, &.{ "cat-file", "-t", "--", head }, .everything);}
/// The loose object `raw` would be, stored as `file`, under the ID `raw`/// hashes to unless `name` gives another: for objects git refuses to write.fn putLoose(repo: *Repo, raw: []const u8, file: []const u8, name: ?[]const u8) ![]u8 { const gpa = repo.gpa; const hex = if (name) |n| try gpa.dupe(u8, n) else hex: { var h: git.ObjectHasher = .init(repo.object_format); h.update(raw); break :hex try std.fmt.allocPrint(gpa, "{f}", .{h.final()}); }; errdefer gpa.free(hex); try repo.writeLooseFile(hex, file); return hex;}
/// An object ID no object has, made from `seed`.fn madeUpName(repo: *Repo, seed: []const u8) ![]u8 { var h: git.ObjectHasher = .init(repo.object_format); h.update("made up: "); h.update(seed); return std.fmt.allocPrint(repo.gpa, "{f}", .{h.final()});}
test "zig-git cat-file refuses corrupt loose objects as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit();
// Headers, each in an otherwise well-formed loose object. const headers = [_][]const u8{ "blob 3\x00abc", "blob 3\x00abc", "blob 03\x00abc", "blob 0\x00", "blob +3\x00abc", "blob 3 \x00abc", "Blob 3\x00abc", "blob\x00abc", "blob \x00abc", "blob 3abc", "blob 2\x00abc", "foo 3\x00abc", "blob -1\x00abc", " blob 3\x00abc", "", "blob\t3\x00abc", "blob 0x3\x00abc", "ofs-delta 3\x00abc", "foo 03\x00abc", "foo\x00abc", "blob\x00", "blob 3", " 3\x00abc", "tag 3\x00abc", "commit 1\x00ab", "tree 0\x00x", "blob 99999999999999999999\x00abc", "blob 18446744073709551616\x00abc", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 3\x00abc", "blob 0000000000000000000000000000000000000000000003\x00abc", "aaaaaaaaaa" ++ "aaaaaaaaaa" ++ "aaaaaaaaa" ++ " 3\x00abc", "aaaaaaaaaa" ++ "aaaaaaaaaa" ++ "aaaaaaaaaa" ++ " 3\x00abc", "blob 11111111111111111111111111\x00abc", "blob 111111111111111111111111111\x00abc", "blob 30\x00yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", "commit 25\x00yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", }; for (headers) |raw| { const file = try zlibStored(gpa, raw); defer gpa.free(file); const hex = try putLoose(&repo, raw, file, null); defer gpa.free(hex); errdefer std.debug.print("object: \"{f}\"\n", .{std.zig.fmtString(raw)}); try expectCatFileSameAsGit(&repo, hex); }
// Objects holding less than their headers say. Reading one whole, git // makes up the missing bytes from whatever its buffer held, and the // library refuses instead; so only what reads less agrees. for ([_][]const u8{ "blob 4\x00abc", "blob 4294967299\x00abc", "blob 18446744073709551615\x00abc", "commit 3\x00ab", "blob 50\x00yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", "commit 50\x00yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", }) |raw| { const file = try zlibStored(gpa, raw); defer gpa.free(file); const hex = try putLoose(&repo, raw, file, null); defer gpa.free(hex); errdefer std.debug.print("object: \"{f}\"\n", .{std.zig.fmtString(raw)}); for ([_][]const u8{ "-t", "-s", "-e" }) |mode| try expectSameAsGit(&repo, &.{ "cat-file", mode, hex }, .everything); if (std.mem.startsWith(u8, raw, "blob")) { try expectSameAsGit(&repo, &.{ "cat-file", "-p", hex }, .everything); try expectSameAsGit(&repo, &.{ "cat-file", "blob", hex }, .everything); } }
// Corrupt streams, of objects short and long, around the 32 bytes git // inflates to read a header. for ([_]usize{ 10, 30, 31, 32, 33, 34, 40, 100, 5000, 40000 }) |total| { for ([_][]const u8{ "blob", "commit" }) |kind| { var raw: std.ArrayList(u8) = .empty; defer raw.deinit(gpa); try raw.print(gpa, "{s} ", .{kind}); const digits = std.fmt.count("{d}", .{total}); const body_len = total - raw.items.len - digits - 1; try raw.print(gpa, "{d}\x00", .{body_len}); for (0..body_len) |i| try raw.append(gpa, "abcdefghij"[i % 10]); const good = try zlibStored(gpa, raw.items); defer gpa.free(good);
const Damage = enum { checksum, truncated, garbage, empty, not_zlib }; for (std.enums.values(Damage)) |damage| { const file = try gpa.dupe(u8, good); defer gpa.free(file); const bytes: []const u8 = switch (damage) { .checksum => b: { file[file.len - 1] ^= 1; break :b file; }, .truncated => file[0 .. file.len - 6], .garbage => try std.mem.concat(gpa, u8, &.{ good, "garbage" }), .empty => file[0..0], .not_zlib => "this is not zlib at all", }; defer if (damage == .garbage) gpa.free(bytes); const seed = try std.fmt.allocPrint(gpa, "{s} {d} {t}", .{ kind, total, damage }); defer gpa.free(seed); const name = try madeUpName(&repo, seed); defer gpa.free(name); const hex = try putLoose(&repo, raw.items, bytes, name); defer gpa.free(hex); errdefer std.debug.print("{s}\n", .{seed}); try expectCatFileSameAsGit(&repo, hex); } } }
// zlib headers zlib refuses. const raw = "blob 3\x00abc"; const good = try zlibStored(gpa, raw); defer gpa.free(good); const ZlibHeader = struct { cmf: u8, flg: u8, fix_check: bool }; for ([_]ZlibHeader{ .{ .cmf = 0x78, .flg = 0x9d, .fix_check = false }, // a bad check .{ .cmf = 0x77, .flg = 0x00, .fix_check = true }, // not DEFLATE .{ .cmf = 0x88, .flg = 0x00, .fix_check = true }, // too big a window .{ .cmf = 0x78, .flg = 0x20, .fix_check = true }, // a preset dictionary }) |h| { const file = try gpa.dupe(u8, good); defer gpa.free(file); file[0] = h.cmf; file[1] = h.flg; // The check bits made right, so that each fails on its own account. if (h.fix_check) while ((@as(u16, file[0]) << 8 | file[1]) % 31 != 0) { file[1] += 1; }; const name = try madeUpName(&repo, file[0..2]); defer gpa.free(name); const hex = try putLoose(&repo, raw, file, name); defer gpa.free(hex); try expectCatFileSameAsGit(&repo, hex); } for ([_][]const u8{ "\x78", good[0..2], good[0..2] ++ "\x07\x00\x00\x00\x00\x00\x00\x00\x00", good[0..2] ++ "\x01\x03\x00\x00\x00abc" }) |file| { const name = try madeUpName(&repo, file); defer gpa.free(name); const hex = try putLoose(&repo, raw, file, name); defer gpa.free(hex); try expectCatFileSameAsGit(&repo, hex); }
// A well-formed object under another object's name. for ([_][]const u8{ "blob 3\x00abc", "tree 0\x00" }) |r| { const file = try zlibStored(gpa, r); defer gpa.free(file); const name = try madeUpName(&repo, r); defer gpa.free(name); const hex = try putLoose(&repo, r, file, name); defer gpa.free(hex); try expectCatFileSameAsGit(&repo, hex); }}
test "zig-git cat-file reads malformed trees, commits and tags as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try repo.commitFile("f", "hello\n", "first"); const blob = try revParse(&repo, "HEAD:f"); defer gpa.free(blob); const tree = try revParse(&repo, "HEAD^{tree}"); defer gpa.free(tree); const commit = try revParse(&repo, "HEAD"); defer gpa.free(commit); const missing = try madeUpName(&repo, "missing"); defer gpa.free(missing); var raw_blob: [git.object_max_byte_length]u8 = undefined; const B = (try ObjectId.parseHex(repo.object_format, blob)).slice(); @memcpy(raw_blob[0..B.len], B);
// Trees. const entry = struct { fn f(a: Allocator, mode: []const u8, name: []const u8, id: []const u8) ![]u8 { return std.mem.concat(a, u8, &.{ mode, " ", name, "\x00", id }); } }.f; var trees: std.ArrayList([]u8) = .empty; defer { for (trees.items) |t| gpa.free(t); trees.deinit(gpa); } for ([_][]const u8{ "100644", "040000", "40000", "100664", "100600", "777", "0", "120000", "160000", "100755", "100648", "abc", "", "1000000000000644", "37777777777", "140000", "+100644" }) |mode| { try trees.append(gpa, try entry(gpa, mode, "a", B)); } for ([_][]const u8{ "", "a/b", ".", "..", ".git", " a" }) |name| { try trees.append(gpa, try entry(gpa, "100644", name, B)); } { const a = try entry(gpa, "100644", "a", B); defer gpa.free(a); const b = try entry(gpa, "100644", "b", B); defer gpa.free(b); const bad = try entry(gpa, "100648", "c", B); defer gpa.free(bad); try trees.append(gpa, try std.mem.concat(gpa, u8, &.{ b, a })); try trees.append(gpa, try std.mem.concat(gpa, u8, &.{ a, a })); try trees.append(gpa, try std.mem.concat(gpa, u8, &.{ a, "x" })); try trees.append(gpa, try std.mem.concat(gpa, u8, &.{ a, bad })); try trees.append(gpa, try gpa.dupe(u8, a[0 .. a.len - 1])); try trees.append(gpa, try gpa.dupe(u8, "100644 a")); try trees.append(gpa, try gpa.dupe(u8, "abc a\x00x")); } { // A name for every byte but NUL and `/`, to see every quoting. var all: std.ArrayList(u8) = .empty; defer all.deinit(gpa); var c: u16 = 1; while (c < 256) : (c += 1) { if (c == '/') continue; const name = [_]u8{ @intCast(c), 'x' }; const e = try entry(gpa, "100644", &name, B); defer gpa.free(e); try all.appendSlice(gpa, e); } try trees.append(gpa, try all.toOwnedSlice(gpa)); } for (trees.items) |contents| { const hex = try repo.hashObject("tree", contents); defer gpa.free(hex); errdefer std.debug.print("tree: \"{f}\"\n", .{std.zig.fmtString(contents)}); try expectCatFileSameAsGit(&repo, hex); }
// Commits and tags, which `cat-file <kind>` follows to their trees and // targets. const objects = [_]struct { kind: []const u8, parts: []const []const u8 }{ .{ .kind = "commit", .parts = &.{ "tree ", tree, "\n\nm\n" } }, .{ .kind = "commit", .parts = &.{ "tree ", tree, "\n" } }, .{ .kind = "commit", .parts = &.{ "tree ", tree } }, .{ .kind = "commit", .parts = &.{ "tree ", tree, "\nparent zzz\n\nm\n" } }, .{ .kind = "commit", .parts = &.{"author A <a> 1 +0000\n\nm\n"} }, .{ .kind = "commit", .parts = &.{ "tree ", missing, "\n\nm\n" } }, .{ .kind = "commit", .parts = &.{ "tree ", blob, "\n\nm\n" } }, .{ .kind = "commit", .parts = &.{ "tree ", tree[0 .. tree.len - 1], "\n\nm\n" } }, .{ .kind = "commit", .parts = &.{""} }, .{ .kind = "tag", .parts = &.{ "object ", commit, "\ntype commit\ntag t\n\nm\n" } }, .{ .kind = "tag", .parts = &.{ "object ", blob, "\ntype commit\ntag t\n\nm\n" } }, .{ .kind = "tag", .parts = &.{ "object ", commit, "\ntype blob\ntag t\n\nm\n" } }, .{ .kind = "tag", .parts = &.{ "object ", tree, "\ntype frob\n" } }, .{ .kind = "tag", .parts = &.{ "object ", missing, "\ntype commit\ntag t\n" } }, .{ .kind = "tag", .parts = &.{"type commit\ntag t\n\nm\n"} }, .{ .kind = "tag", .parts = &.{ "object ", commit[0 .. commit.len - 1], "\ntype commit\n" } }, .{ .kind = "tag", .parts = &.{ "object ", commit } }, .{ .kind = "tag", .parts = &.{ "object ", commit, "\n" } }, .{ .kind = "tag", .parts = &.{ "tree ", tree, "\nauthor A <a> 1 +0000\n\nm\n" } }, }; for (objects) |o| { const contents = try std.mem.concat(gpa, u8, o.parts); defer gpa.free(contents); const hex = try repo.hashObject(o.kind, contents); defer gpa.free(hex); errdefer std.debug.print("{s}: \"{f}\"\n", .{ o.kind, std.zig.fmtString(contents) }); try expectCatFileSameAsGit(&repo, hex); }}
test "zig-git outside a repository fails" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); const gpa = testing.allocator; const path = try tmp.dir.realPathFileAlloc(testing.io, ".", gpa); defer gpa.free(path); var env: std.process.Environ.Map = .init(gpa); defer env.deinit(); var out: Io.Writer.Allocating = .init(gpa); defer out.deinit(); const ctx: Context = .{ .io = testing.io, .gpa = gpa, .env = &env, .cwd = path }; var stdin: Io.Reader = .fixed(""); try testing.expectEqual(128, run(ctx, &.{ "cat-file", "-t", "4b825dc642cb6eb9a060e54bf8d69288fbee4904" }, &stdin, &out.writer, &out.writer)); try testing.expectEqual(129, run(ctx, &.{"frobnicate"}, &stdin, &out.writer, &out.writer));}
// -- packs, the multi-pack-index and alternates -------------------------------
const packs = git.test_packs;
/// A history with small changes to a big file, so that git stores deltas,/// and deltas of deltas when asked to, and a tag pointing at its tip.fn makeHistory(repo: *Repo, commits: usize) !void { const gpa = repo.gpa; var text: std.ArrayList(u8) = .empty; defer text.deinit(gpa); for (0..300) |i| try text.print(gpa, "line {d}\n", .{i}); for (0..commits) |i| { for (0..repo.count + 1) |k| { const at = (k * 53 + i * 7) % (text.items.len - 10); text.items[at] = 'a' + @as(u8, @intCast((k + i) % 26)); } try repo.commitFile("f", text.items, "c"); var buf: [32]u8 = undefined; try repo.commitFile("g", try std.fmt.bufPrint(&buf, "{d}\n", .{repo.count}), "g"); } try repo.git(&.{ "tag", "-f", "-a", "-m", "a tag", "t" });}
/// The object IDs of every object in the repository, the tag included;/// the caller frees them with `freeIds`.fn allIds(repo: *Repo) ![][]u8 { const gpa = repo.gpa; const listing = try repo.output(&.{ "rev-list", "--objects", "--all" }); defer gpa.free(listing); var list: std.ArrayList([]u8) = .empty; errdefer freeIds(gpa, list.items); var lines = std.mem.tokenizeScalar(u8, listing, '\n'); while (lines.next()) |line| try list.append(gpa, try gpa.dupe(u8, line[0..repo.object_format.hexLength()])); try list.append(gpa, try revParse(repo, "t")); return list.toOwnedSlice(gpa);}
fn freeIds(gpa: Allocator, ids: []const []u8) void { for (ids) |id| gpa.free(id); gpa.free(ids);}
/// Every way of asking `cat-file` about one object, from `cwd`.fn expectCatFileSameAsGitIn(repo: *Repo, cwd: []const u8, id: []const u8, compare: Compare) !void { for ([_][]const u8{ "-t", "-s", "-e", "-p", "blob", "tree", "commit", "tag" }) |mode| { try expectSameAsGitHow(repo, &.{ "cat-file", mode, id }, .{ .cwd = cwd, .compare = compare }); }}
/// Every way of asking `cat-file` about every object in the repository.fn expectAllObjectsSameAsGit(repo: *Repo) !void { const ids = try allIds(repo); defer freeIds(repo.gpa, ids); for (ids) |id| try expectCatFileSameAsGit(repo, id);}
/// Replaces a pack with `pack_bytes`, its checksum made right, fixes its/// index's copy of the checksum and the index's own, and removes the/// reverse index, which records the checksum too.fn rewritePack(repo: *Repo, name: []const u8, pack_bytes: []u8, idx_bytes: []u8) !void { const h = repo.object_format.byteLength(); const sum = packs.digest(repo.object_format, pack_bytes[0 .. pack_bytes.len - h]); @memcpy(pack_bytes[pack_bytes.len - h ..], sum[0..h]); @memcpy(idx_bytes[idx_bytes.len - 2 * h ..][0..h], sum[0..h]); fixChecksum(repo.object_format, idx_bytes); try packs.writePackFile(repo, name, "pack", pack_bytes); try packs.writePackFile(repo, name, "idx", idx_bytes); removeRev(repo, name);}
fn fixChecksum(format: ObjectFormat, bytes: []u8) void { const h = format.byteLength(); const sum = packs.digest(format, bytes[0 .. bytes.len - h]); @memcpy(bytes[bytes.len - h ..], sum[0..h]);}
fn removeRev(repo: *Repo, name: []const u8) void { var buf: [256]u8 = undefined; const path = std.fmt.bufPrint(&buf, ".git/objects/pack/{s}.rev", .{name}) catch return; repo.tmp.dir.deleteFile(repo.io, path) catch {};}
/// The repository's packs as `.git/objects/pack/pack-<hex>`, without an/// extension; the caller frees them with `packs.freeNames`.fn packPaths(repo: *Repo) ![][]u8 { const names = try packs.names(repo); for (names) |*n| { const full = try std.fmt.allocPrint(repo.gpa, ".git/objects/pack/{s}", .{n.*}); repo.gpa.free(n.*); n.* = full; } return names;}
test "zig-git cat-file reads packed objects as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo, 8); try repo.git(&.{ "gc", "-q" }); try expectAllObjectsSameAsGit(&repo); // Long chains of deltas of deltas. try repo.git(&.{ "repack", "-adfq", "--depth=50", "--window=50" }); try expectAllObjectsSameAsGit(&repo); // REF_DELTAs rather than OFS_DELTAs. try repo.git(&.{ "-c", "repack.useDeltaBaseOffset=false", "repack", "-adfq" }); try expectAllObjectsSameAsGit(&repo); // Version 1 pack indexes. try repo.git(&.{ "-c", "pack.indexVersion=1", "repack", "-adfq" }); try expectAllObjectsSameAsGit(&repo); // A pack git reads but no longer writes: version 3. const names = try packs.names(&repo); defer packs.freeNames(gpa, names); const pack_bytes = try packs.readPackFile(&repo, names[0], "pack"); defer gpa.free(pack_bytes); const idx_bytes = try packs.readPackFile(&repo, names[0], "idx"); defer gpa.free(idx_bytes); pack_bytes[7] = 3; try rewritePack(&repo, names[0], pack_bytes, idx_bytes); try expectAllObjectsSameAsGit(&repo);}
test "zig-git cat-file reads through a multi-pack-index as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); for (0..3) |_| { try makeHistory(&repo, 2); try repo.git(&.{ "repack", "-dq" }); } try repo.git(&.{ "multi-pack-index", "write" }); try makeHistory(&repo, 2); try repo.git(&.{ "repack", "-dq" }); // a pack it does not name try makeHistory(&repo, 1); // and loose objects try expectAllObjectsSameAsGit(&repo); // An incremental chain, which git now writes. try repo.git(&.{ "multi-pack-index", "write", "--incremental" }); try makeHistory(&repo, 2); try repo.git(&.{ "repack", "-dq" }); try repo.git(&.{ "multi-pack-index", "write", "--incremental" }); try expectAllObjectsSameAsGit(&repo);}
test "zig-git cat-file reads alternates as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo, 4); try repo.git(&.{ "gc", "-q" }); try makeHistory(&repo, 1); const ids = try allIds(&repo); defer freeIds(gpa, ids); const missing = try madeUpName(&repo, "missing"); defer gpa.free(missing);
// A clone borrowing everything: an absolute path in its alternates. try repo.git(&.{ "clone", "-q", "--shared", ".", "shared" }); for (ids) |id| try expectCatFileSameAsGitIn(&repo, "shared", id, .everything); try expectCatFileSameAsGitIn(&repo, "shared", missing, .everything);
// A chain of alternates of alternates, by relative paths, each from // the objects directory naming it: git follows five levels and says // so when it stops. var name_buf: [32]u8 = undefined; for (1..8) |i| { const dir = try std.fmt.bufPrint(&name_buf, "d{d}", .{i}); try repo.git(&.{ "init", "-q", dir }); var path_buf: [64]u8 = undefined; var line_buf: [64]u8 = undefined; const line = if (i == 1) "../../../.git/objects\n" else try std.fmt.bufPrint(&line_buf, "../../../d{d}/.git/objects\n", .{i - 1}); try writeFile(&repo, try std.fmt.bufPrint(&path_buf, "d{d}/.git/objects/info/alternates", .{i}), line); } for ([_][]const u8{ "d1", "d6", "d7" }) |dir| { for (ids[0..3]) |id| try expectCatFileSameAsGitIn(&repo, dir, id, .everything); try expectCatFileSameAsGitIn(&repo, dir, missing, .everything); }
// Comments, blank lines, directories that are not there, a quoted // path, a cycle, and the repository's own directory. try repo.git(&.{ "init", "-q", "odd" }); try writeFile(&repo, "odd/.git/objects/info/alternates", \\# a comment \\ \\../../../nope/.git/objects \\/nonexistent/objects \\../../../d1/.git/HEAD \\../objects \\"../../../d\061/.git/objects" \\../../../shared/.git/objects/ \\ ); try writeFile(&repo, "d1/.git/objects/info/alternates", "../../../.git/objects\n../../../odd/.git/objects\n"); for (ids[0..3]) |id| try expectCatFileSameAsGitIn(&repo, "odd", id, .everything); try expectCatFileSameAsGitIn(&repo, "odd", missing, .everything);
// GIT_ALTERNATE_OBJECT_DIRECTORIES, relative to the top of the // working tree, with an empty entry and one that is not there. try repo.git(&.{ "init", "-q", "env" }); try repo.env.put("GIT_ALTERNATE_OBJECT_DIRECTORIES", "::../nope:../.git/objects"); defer _ = repo.env.swapRemove("GIT_ALTERNATE_OBJECT_DIRECTORIES"); for (ids[0..3]) |id| try expectCatFileSameAsGitIn(&repo, "env", id, .everything); try expectCatFileSameAsGitIn(&repo, "env", missing, .everything);}
test "zig-git cat-file resolves deltas against bases outside their pack as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo, 3); try repo.git(&.{ "branch", "old", "HEAD~2" });
// A thin pack, its bases left out, completed by index-pack // --fix-thin in a repository that has them: the bases are appended // after the REF_DELTAs needing them. const thin = try repo.outputWithInput(&.{ "pack-objects", "-q", "--thin", "--stdout", "--revs" }, "HEAD\n^old\n"); defer gpa.free(thin); try repo.git(&.{ "init", "-q", "thin" }); try repo.git(&.{ "-C", "thin", "fetch", "-q", "..", "old" }); const fixed = try repo.runIn("thin", &.{ "index-pack", "--fix-thin", "--stdin" }, thin); defer fixed.deinit(gpa); try testing.expectEqual(0, fixed.code); const ids = try allIds(&repo); defer freeIds(gpa, ids); for (ids[0 .. ids.len - 1]) |id| try expectCatFileSameAsGitIn(&repo, "thin", id, .everything);
// REF_DELTAs written byte by byte, each in a pack of its own: against // a base in another pack, a loose base, a base that is itself such a // delta, and a base that is nowhere. const f = repo.object_format; const base = "the base of several deltas\n" ++ "and of a delta of a delta\n"; const base_id = git.hashObject(f, .blob, base); const loose_base = "a loose base\n"; const loose_hex = try repo.hashObject("blob", loose_base); defer gpa.free(loose_hex); const loose_id = try ObjectId.parseHex(f, loose_hex); const missing_id = git.hashObject(f, .blob, "never written"); const Case = struct { base_id: ObjectId, base_len: u32, suffix: []const u8, base_text: []const u8 }; const in_pack = base ++ "from another pack\n"; const cases = [_]Case{ .{ .base_id = base_id, .base_len = base.len, .suffix = "from another pack\n", .base_text = base }, .{ .base_id = loose_id, .base_len = loose_base.len, .suffix = "from a loose base\n", .base_text = loose_base }, .{ .base_id = git.hashObject(f, .blob, in_pack), .base_len = in_pack.len, .suffix = "a delta of a delta\n", .base_text = in_pack }, .{ .base_id = missing_id, .base_len = 13, .suffix = "from nowhere\n", .base_text = "never written" }, }; { var w: packs.PackWriter = try .init(gpa, f); defer w.deinit(); _ = try w.whole(.blob, base); const out = try w.finish(); defer gpa.free(out.pack); defer gpa.free(out.idx); gpa.free(try packs.install(&repo, out.pack, out.idx)); } var hexes: std.ArrayList([]u8) = .empty; defer { for (hexes.items) |x| gpa.free(x); hexes.deinit(gpa); } for (cases) |case| { var w: packs.PackWriter = try .init(gpa, f); defer w.deinit(); const d = try packs.deltaAppend(gpa, case.base_len, case.suffix); defer gpa.free(d); const result = try std.mem.concat(gpa, u8, &.{ case.base_text, case.suffix }); defer gpa.free(result); const id = git.hashObject(f, .blob, result); _ = try w.refDelta(case.base_id, d, id); const out = try w.finish(); defer gpa.free(out.pack); defer gpa.free(out.idx); gpa.free(try packs.install(&repo, out.pack, out.idx)); try hexes.append(gpa, try std.fmt.allocPrint(gpa, "{f}", .{id})); } // git resolves none of them: it finds no object whose base is outside // its pack, wherever the base is, and says nothing more of it. for (hexes.items) |hex| try expectCatFileSameAsGit(&repo, hex);}
/// Sets up the test repository with one pack, and remembers its files so/// that each corruption can start from them.const OnePack = struct { repo: *Repo, name: []u8, pack: []u8, idx: []u8,
fn init(repo: *Repo) !OnePack { const names = try packs.names(repo); defer { for (names[1..]) |n| repo.gpa.free(n); repo.gpa.free(names); } const pack_bytes = try packs.readPackFile(repo, names[0], "pack"); errdefer repo.gpa.free(pack_bytes); return .{ .repo = repo, .name = names[0], .pack = pack_bytes, .idx = try packs.readPackFile(repo, names[0], "idx") }; }
fn deinit(o: *OnePack) void { o.repo.gpa.free(o.name); o.repo.gpa.free(o.pack); o.repo.gpa.free(o.idx); }
/// Fresh copies of the files, for a test to damage. fn copies(o: *OnePack) !struct { pack: []u8, idx: []u8 } { const p = try o.repo.gpa.dupe(u8, o.pack); errdefer o.repo.gpa.free(p); return .{ .pack = p, .idx = try o.repo.gpa.dupe(u8, o.idx) }; }
/// Puts `pack_bytes` and `idx_bytes` in place of the pack's files, or /// removes one if it is null. fn put(o: *OnePack, pack_bytes: ?[]const u8, idx_bytes: ?[]const u8) !void { var buf: [256]u8 = undefined; if (pack_bytes) |b| try packs.writePackFile(o.repo, o.name, "pack", b) else o.repo.tmp.dir.deleteFile(o.repo.io, try std.fmt.bufPrint(&buf, ".git/objects/pack/{s}.pack", .{o.name})) catch {}; if (idx_bytes) |b| try packs.writePackFile(o.repo, o.name, "idx", b) else o.repo.tmp.dir.deleteFile(o.repo.io, try std.fmt.bufPrint(&buf, ".git/objects/pack/{s}.idx", .{o.name})) catch {}; removeRev(o.repo, o.name); }};
/// The offsets of a pack's entries, sorted, as `git show-index` gives/// them, with the object at each.const EntryAt = struct { offset: u64, id: ObjectId };
fn entryOffsets(repo: *Repo, idx: []const u8) ![]EntryAt { const gpa = repo.gpa; const listing = try repo.outputWithInput(&.{"show-index"}, idx); defer gpa.free(listing); const Entry = EntryAt; var list: std.ArrayList(Entry) = .empty; errdefer list.deinit(gpa); var lines = std.mem.tokenizeScalar(u8, listing, '\n'); while (lines.next()) |line| { var fields = std.mem.splitScalar(u8, line, ' '); const offset = try std.fmt.parseInt(u64, fields.next().?, 10); try list.append(gpa, .{ .offset = offset, .id = try ObjectId.parseHex(repo.object_format, fields.next().?) }); } std.mem.sort(Entry, list.items, {}, struct { fn lt(_: void, a: Entry, b: Entry) bool { return a.offset < b.offset; } }.lt); return list.toOwnedSlice(gpa);}
test "zig-git cat-file refuses corrupt packs and pack indexes as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo, 3); try repo.git(&.{ "repack", "-adq" }); var one = try OnePack.init(&repo); defer one.deinit(); const ids = try allIds(&repo); defer freeIds(gpa, ids); const missing = try madeUpName(&repo, "missing"); defer gpa.free(missing); const h = repo.object_format.byteLength(); const n: usize = std.mem.readInt(u32, one.idx[8 + 255 * 4 ..][0..4], .big);
const Damage = enum { idx_truncated, idx_version_3, idx_not_magic, idx_too_long, idx_one_large_offset_longer, idx_checksum, idx_crc, idx_pack_checksum, pack_signature, pack_version_4, pack_checksum, pack_truncated, pack_missing, idx_missing, }; for (std.enums.values(Damage)) |damage| { const c = try one.copies(); defer gpa.free(c.pack); defer gpa.free(c.idx); var extra: ?[]u8 = null; defer if (extra) |e| gpa.free(e); switch (damage) { .idx_truncated => try one.put(c.pack, c.idx[0..1000]), .idx_version_3 => { c.idx[7] = 3; try one.put(c.pack, c.idx); }, .idx_not_magic => { c.idx[0] ^= 1; try one.put(c.pack, c.idx); }, .idx_too_long, .idx_one_large_offset_longer => { // Room for large offsets the index does not use, its // trailer moved to the end. const more: usize = if (damage == .idx_too_long) 8 * n else 8; const e = try gpa.alloc(u8, c.idx.len + more); extra = e; @memset(e, 0); @memcpy(e[0 .. c.idx.len - 2 * h], c.idx[0 .. c.idx.len - 2 * h]); @memcpy(e[e.len - 2 * h ..], c.idx[c.idx.len - 2 * h ..]); try one.put(c.pack, e); }, .idx_checksum => { c.idx[c.idx.len - 1] ^= 1; try one.put(c.pack, c.idx); }, .idx_crc => { c.idx[8 + 1024 + n * h] ^= 0xff; try one.put(c.pack, c.idx); }, .idx_pack_checksum => { c.idx[c.idx.len - h - 1] ^= 1; try one.put(c.pack, c.idx); }, .pack_signature => { c.pack[0] ^= 1; try one.put(c.pack, c.idx); }, .pack_version_4 => { c.pack[7] = 4; try rewritePack(&repo, one.name, c.pack, c.idx); }, .pack_checksum => { c.pack[c.pack.len - 1] ^= 1; try one.put(c.pack, c.idx); }, .pack_truncated => try one.put(c.pack[0 .. c.pack.len - 10], c.idx), .pack_missing => try one.put(null, c.idx), .idx_missing => try one.put(c.pack, null), } errdefer std.debug.print("damage: {t}\n", .{damage}); for ([_][]const u8{ ids[0], ids[ids.len / 2], ids[ids.len - 2], ids[ids.len - 1], missing }) |id| try expectCatFileSameAsGit(&repo, id); }
// A corrupt zlib stream in an entry: the last byte of its checksum. try one.put(one.pack, one.idx); const entries = try entryOffsets(&repo, one.idx); defer gpa.free(entries); for (entries, 0..) |e, i| { if (i % 3 != 0) continue; const end = if (i + 1 < entries.len) entries[i + 1].offset else one.pack.len - h; const c = try one.copies(); defer gpa.free(c.pack); defer gpa.free(c.idx); c.pack[@intCast(end - 1)] ^= 0x10; try one.put(c.pack, c.idx); var hex: [git.object_max_hex_length]u8 = undefined; const id_hex = e.id.toHex(&hex); // What git says of a corrupt object stored whole is matched; of a // corrupt delta, only that it fails. const entry = try git.pack.parseEntryHeader(repo.object_format, one.pack[0 .. one.pack.len - h], e.offset); errdefer std.debug.print("corrupt entry at {d}\n", .{e.offset}); try expectCatFileSameAsGitIn(&repo, ".", id_hex, if (entry.type.kind() != null) .everything else .output); }}
test "zig-git cat-file refuses corrupt multi-pack-indexes as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); for (0..3) |_| { try makeHistory(&repo, 2); try repo.git(&.{ "repack", "-dq" }); } try repo.git(&.{ "multi-pack-index", "write" }); const path = ".git/objects/pack/multi-pack-index"; const good = try repo.tmp.dir.readFileAlloc(repo.io, path, gpa, .unlimited); defer gpa.free(good); const ids = try allIds(&repo); defer freeIds(gpa, ids); const missing = try madeUpName(&repo, "missing"); defer gpa.free(missing);
const Damage = enum { checksum, truncated, tiny, empty, signature, version, hash_version, no_pack_names, pack_name }; for (std.enums.values(Damage)) |damage| { const bad = try gpa.dupe(u8, good); defer gpa.free(bad); const bytes: []const u8 = switch (damage) { .checksum => b: { bad[bad.len - 1] ^= 1; break :b bad; }, .truncated => bad[0..1000], .tiny => bad[0..10], .empty => bad[0..0], .signature => b: { bad[0] ^= 1; break :b bad; }, .version => b: { bad[4] = 6; break :b bad; }, .hash_version => b: { bad[5] ^= 3; break :b bad; }, .no_pack_names => b: { bad[12] ^= 1; break :b bad; }, .pack_name => b: { // One digit of the first pack's name, so that it names a // pack that is not there and the one that is goes // unnamed. const pnam: usize = @intCast(std.mem.readInt(u64, bad[16..24], .big)); bad[pnam + 10] = if (bad[pnam + 10] == '0') '1' else '0'; break :b bad; }, }; try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = path, .data = bytes }); errdefer std.debug.print("damage: {t}\n", .{damage}); for ([_][]const u8{ ids[0], ids[ids.len / 2], ids[ids.len - 1], missing }) |id| try expectCatFileSameAsGit(&repo, id); }}
// -- show-index and verify-pack ------------------------------------------------
test "zig-git show-index prints what git prints" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo, 3); const f = repo.object_format; for ([_][]const u8{ "pack.indexVersion=2", "pack.indexVersion=1" }) |config| { try repo.git(&.{ "-c", config, "repack", "-adq" }); const idx = try packs.readOnlyIndex(&repo); defer gpa.free(idx); try expectSameAsGitHow(&repo, &.{"show-index"}, .{ .input = idx }); try expectSameAsGitHow(&repo, &.{ "show-index", "--object-format", f.name() }, .{ .input = idx, .compare = .output }); var arg_buf: [64]u8 = undefined; try expectSameAsGitHow(&repo, &.{ "show-index", try std.fmt.bufPrint(&arg_buf, "--object-format={s}", .{f.name()}) }, .{ .input = idx }); // Cut short everywhere a table begins or ends, and in between. const h = f.byteLength(); const n: usize = if (config[config.len - 1] == '2') std.mem.readInt(u32, idx[8 + 255 * 4 ..][0..4], .big) else std.mem.readInt(u32, idx[255 * 4 ..][0..4], .big); const cuts = [_]usize{ 0, 5, 8, 100, 1024, 1031, 1032, 1033, 1032 + h * 3 + 1, 1032 + n * h, 1032 + n * h + 6, 1032 + n * (h + 4), 1032 + n * (h + 4) + 9, 1032 + n * (h + 8), idx.len - 1 }; for (cuts) |cut| { if (cut > idx.len) continue; errdefer std.debug.print("cut at {d}\n", .{cut}); try expectSameAsGitHow(&repo, &.{"show-index"}, .{ .input = idx[0..cut] }); } const bad = try gpa.dupe(u8, idx); defer gpa.free(bad); bad[8 + 4 * 10 + 3] = 0xff; // a fanout entry too big for the next try expectSameAsGitHow(&repo, &.{"show-index"}, .{ .input = bad }); if (config[config.len - 1] == '2') { bad[7] = 3; try expectSameAsGitHow(&repo, &.{"show-index"}, .{ .input = bad }); } } // Large offsets, which only a pack past 2 GiB would have: crafted. const Crafted = struct { offsets: []const u32, large: []const u64, cut: usize = 0 }; for ([_]Crafted{ .{ .offsets = &.{ 12, 0x8000_0000 }, .large = &.{0x1_0000_0000} }, .{ .offsets = &.{ 0x8000_0000, 12, 0x8000_0001 }, .large = &.{ 0x1_0000_0000, 0xffff_ffff_ffff } }, .{ .offsets = &.{0x8000_0000}, .large = &.{0x10} }, .{ .offsets = &.{ 0x8000_0001, 0x8000_0000 }, .large = &.{ 0x1_0000_0000, 0x2_0000_0000 } }, .{ .offsets = &.{ 12, 0x8000_0000, 0x8000_0001 }, .large = &.{0x1_0000_0000}, .cut = 1 }, }) |c| { const idx = try packs.craftIndex(gpa, f, c.offsets, c.large); defer gpa.free(idx); // Cut off the trailer and then some, so that a large offset runs // out of file. const len = if (c.cut > 0) idx.len - 2 * f.byteLength() else idx.len; try expectSameAsGitHow(&repo, &.{"show-index"}, .{ .input = idx[0..len] }); }}
test "zig-git verify-pack -v prints what git prints" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo, 6); try repo.git(&.{ "gc", "-q" }); try makeHistory(&repo, 4); try repo.git(&.{ "repack", "-dq" }); { const paths = try packPaths(&repo); defer packs.freeNames(gpa, paths); var arg_bufs: [3][256]u8 = undefined; const a = try std.fmt.bufPrint(&arg_bufs[0], "{s}.idx", .{paths[0]}); const b = try std.fmt.bufPrint(&arg_bufs[1], "{s}.pack", .{paths[1]}); try expectSameAsGit(&repo, &.{ "verify-pack", "-v", a }, .everything); try expectSameAsGit(&repo, &.{ "verify-pack", a, b }, .everything); try expectSameAsGit(&repo, &.{ "verify-pack", "-v", "--", a, b, paths[0] }, .everything); try expectSameAsGit(&repo, &.{ "verify-pack", "-s", a }, .everything); try expectSameAsGit(&repo, &.{ "verify-pack", "-v", "-s", a }, .everything); try expectSameAsGit(&repo, &.{ "verify-pack", "-v", "nope.idx" }, .output); try expectSameAsGit(&repo, &.{"verify-pack"}, .output); } // Deep chains, then REF_DELTAs, then a version 1 index. for ([_][]const []const u8{ &.{ "repack", "-adfq", "--depth=50", "--window=50" }, &.{ "-c", "repack.useDeltaBaseOffset=false", "repack", "-adfq" }, &.{ "-c", "pack.indexVersion=1", "repack", "-adfq" }, }) |args| { try repo.git(args); const paths = try packPaths(&repo); defer packs.freeNames(gpa, paths); var arg_buf: [256]u8 = undefined; try expectSameAsGit(&repo, &.{ "verify-pack", "-v", try std.fmt.bufPrint(&arg_buf, "{s}.idx", .{paths[0]}) }, .everything); }
// Damage, of which only the verdict and the exit status are compared. try repo.git(&.{ "repack", "-adq" }); var one = try OnePack.init(&repo); defer one.deinit(); const h = repo.object_format.byteLength(); const n: usize = std.mem.readInt(u32, one.idx[8 + 255 * 4 ..][0..4], .big); var arg_buf: [256]u8 = undefined; const arg = try std.fmt.bufPrint(&arg_buf, ".git/objects/pack/{s}.idx", .{one.name}); const Damage = enum { idx_checksum, crc, crc_checksum_fixed, pack_data, pack_checksum, pack_missing, idx_truncated }; for (std.enums.values(Damage)) |damage| { const c = try one.copies(); defer gpa.free(c.pack); defer gpa.free(c.idx); switch (damage) { .idx_checksum => { c.idx[c.idx.len - 1] ^= 1; try one.put(c.pack, c.idx); }, .crc, .crc_checksum_fixed => { c.idx[8 + 1024 + n * h + 1] ^= 1; if (damage == .crc_checksum_fixed) fixChecksum(repo.object_format, c.idx); try one.put(c.pack, c.idx); }, .pack_data => { c.pack[c.pack.len / 2] ^= 0x10; try one.put(c.pack, c.idx); }, .pack_checksum => { c.pack[c.pack.len - 1] ^= 1; try one.put(c.pack, c.idx); }, .pack_missing => try one.put(null, c.idx), .idx_truncated => try one.put(c.pack, c.idx[0..1000]), } errdefer std.debug.print("damage: {t}\n", .{damage}); try expectSameAsGit(&repo, &.{ "verify-pack", "-v", arg }, .output); }}
test "zig-git cat-file chooses among copies of an object as git does" { const gpa = testing.allocator; var repo = try Repo.init(gpa, testing.io); defer repo.deinit(); try makeHistory(&repo, 2); try repo.git(&.{ "repack", "-adq" }); const commit = try revParse(&repo, "HEAD"); defer gpa.free(commit); const blob = try revParse(&repo, "HEAD:g"); defer gpa.free(blob);
// A loose copy of a packed object, saying something else: git reads // the pack first, except when it streams a blob, when it reads the // loose copy first. const other = try zlibStored(gpa, "blob 7\x00abcdefg"); defer gpa.free(other); try repo.writeLooseFile(commit, other); try repo.writeLooseFile(blob, other); try expectCatFileSameAsGit(&repo, commit); try expectCatFileSameAsGit(&repo, blob);
// A corrupt packed copy and a good loose one: git says what was wrong // with the first, which is not matched, and reads the second. const contents = try repo.output(&.{ "cat-file", "commit", commit }); defer gpa.free(contents); const header = try std.fmt.allocPrint(gpa, "commit {d}\x00", .{contents.len}); defer gpa.free(header); const raw = try std.mem.concat(gpa, u8, &.{ header, contents }); defer gpa.free(raw); const good = try zlibStored(gpa, raw); defer gpa.free(good); try repo.writeLooseFile(commit, good); var one = try OnePack.init(&repo); defer one.deinit(); const entries = try entryOffsets(&repo, one.idx); defer gpa.free(entries); const h = repo.object_format.byteLength(); const want = try ObjectId.parseHex(repo.object_format, commit); for (entries, 0..) |e, i| { if (!e.id.eql(want)) continue; const end = if (i + 1 < entries.len) entries[i + 1].offset else one.pack.len - h; const c = try one.copies(); defer gpa.free(c.pack); defer gpa.free(c.idx); c.pack[@intCast(end - 1)] ^= 0x10; try one.put(c.pack, c.idx); } try expectCatFileSameAsGitIn(&repo, ".", commit, .output);}
test { _ = @import("walk_tests.zig"); _ = @import("history_tests.zig"); _ = @import("decorate_tests.zig"); _ = @import("naming_tests.zig"); _ = @import("listing_tests.zig"); _ = @import("simplify_tests.zig"); _ = config_command; _ = refs_command; _ = ls_files; _ = rev_parse; _ = discovery; _ = object_names; _ = revisions; _ = @import("revision_tests.zig"); _ = @import("log_tests.zig"); _ = @import("diff_tests.zig"); _ = @import("attr_tests.zig"); _ = @import("diff_tree_tests.zig"); _ = @import("diff_index_tests.zig"); _ = @import("log_diff_tests.zig"); _ = @import("rename_tests.zig"); _ = @import("merge_diff_tests.zig"); _ = @import("funcname_tests.zig"); _ = @import("describe.zig"); _ = @import("log.zig"); _ = @import("line_prefix.zig"); _ = @import("diffcore_tests.zig");}