Reading and writing git repositories in Zig, without running git.

README.md

zig-git #

A library for reading git repositories from Zig 0.17 without running git, written in Zig alone: no libgit2, no C zlib, nothing to link. It is meant to answer the questions a history browser asks git — what a commit holds, what a revision range means, what a diff says — with the same answers git gives, and to grow into writing to repositories and talking to remote ones.

It is early. What works so far is the core layer, the first parts of the revision layer — revisions, walking commits, history limited to paths, describing commits as git log does, and naming them by refs — and the first two parts of the diff layer, comparing two texts as git diff --no-index does, and comparing trees, the index and the working tree as git diff, git diff-tree, git diff-index and git diff-files do:

  • object formats and object IDs, SHA-1 and SHA-256 alike, chosen per repository at run time, and hashing an object to its ID;
  • object headers, read exactly as strictly as git reads them;
  • commits, trees, annotated tags and their author, committer and tagger lines, read without allocating and as forgivingly as git reads them, their oddities included;
  • loose objects: finding, inflating and checking them, whole, header only or streamed, refusing each corruption git refuses and in the same circumstances;
  • packs and their indexes (versions 1 and 2 of the index, versions 2 and 3 of the pack, offsets past 2 GiB), with OFS_DELTA and REF_DELTA chains resolved without recursion and bounded in depth, so that a chain that loops is an error rather than a hang; reading an object's kind and size applies no delta;
  • the multi-pack-index, and incremental chains of them, as a faster way of finding objects in the packs they name;
  • alternates, from objects/info/alternates and from a GIT_ALTERNATE_OBJECT_DIRECTORIES value the caller passes in, nested as deep as git nests them;
  • abbreviated object IDs, both ways: a prefix expanded to the objects it begins, found by binary search in every pack index and multi-pack-index layer and by listing one fan-out directory of loose objects, in every objects directory and alternate; an ambiguous one told apart by kind as core.disambiguate and ^{commit} tell it, and described as git's ambiguity hint describes each candidate; and object IDs shortened to the fewest digits no other object shares, at least core.abbrev's, whose auto grows with the number of packed objects as git's does;
  • replace refs, as git-replace(1) describes them: every object read through the refs under refs/replace/ (or GIT_REPLACE_REF_BASE) as its replacement, commits, trees, blobs and tags alike, keeping its own object ID, chains followed four deep with git's errors for a deeper one and a missing replacement, and its warning for a name it passes over; not when core.useReplaceRefs is false, GIT_NO_REPLACE_OBJECTS is set or --no-replace-objects given;
  • grafts and the shallow boundary: info/grafts (or GIT_GRAFT_FILE) read as git reads it, with its deprecation hint and its errors for bad and duplicate lines, and the shallow file (or GIT_SHALLOW_FILE) with its fatal bad line, giving commits the parents the walk, <rev>^, <rev>~<n>, %P and merge-base follow; and, as git was seen to, the commit-graph left unread when anything is replaced or grafted or the repository is shallow;
  • all of that as one object database, which asks its objects directories, packs and loose objects in the order git can be seen to ask them, passes over a broken copy for the next as git does, and names each corruption it meets;
  • configuration: the file syntax read on bytes, every corner git accepts and every malformed line it refuses at the line it names; the system, global, local and worktree files, GIT_CONFIG_COUNT, GIT_CONFIG_PARAMETERS and -c, in git's order and with every path and environment value passed in; include.path and includeIf with gitdir:, gitdir/i:, onbranch:, worktree: and hasconfig:remote.*.url:, its conditions answered by the caller; booleans, integers with their units, and paths with ~ and %(prefix); and lookups that spell keys as git does;
  • wildmatch, git's glob dialect, as a module of its own for refs, pathspecs and attributes to use, with the corners git check-attr and git tag -l show;
  • discovering a repository as git does: walking up from a starting directory, .git directories and .git files (linked worktrees and submodules, commondir included), bare repositories, GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY, GIT_CEILING_DIRECTORIES with its empty-entry and symbolic link rules, filesystem boundaries and GIT_DISCOVERY_ACROSS_FILESYSTEM, core.worktree and core.bare, safe.directory and ownership, safe.bareRepository, and what git requires of a git directory before it trusts one; the environment, the starting directory and the user are all passed in;
  • the repository format: versions 0 and 1 and every extension git 2.56 knows, refused with git's own message where git refuses them, and by name where this library cannot read what git can (extensions.compatObjectFormat, a refStorage payload);
  • refs: ref names checked by git-check-ref-format(1)'s rules, root refs, pseudorefs and per-worktree refs told apart, and main-worktree/ and worktrees/<name>/ followed; loose refs (symbolic refs, symbolic links, CRLF and trailing garbage read as git reads them), packed-refs with its traits and peel lines, sorted or not; reftables of both versions, SHA-1 and SHA-256, their blocks, prefix compression, ref, obj, log and index blocks and checksums, and stacks of them with their updates and deletions; reflogs from both; all of it behind one ref store that follows symbolic refs as far as git does (five refs, cycles caught), lists refs in git's order, peels tags from the storage or the object database, and shortens names as %(refname:short) does;
  • dates: every --date= mode git-log(1) documents, their -local variants, auto:, and format: and format-local: with strftime — flags, field widths, modifiers and the conversions glibc copies as text included — shown as git shows them; TZ read as a glibc-linked git reads it, a POSIX rule as leniently as glibc reads one; and git's strict and approximate date parsers — the calendar, the time zone database, strftime and the parsers all from zig-datetime;
  • the index: versions 2, 3 and 4 (version 4's prefix-compressed paths included), stat data, modes, stages, the assume-valid, skip-worktree and intent-to-add flags, padding and the trailing checksum, all zeros under index.skipHash; the cache tree with its invalidated nodes, resolve-undo, the untracked cache, the fsmonitor cache, the end of index entries and the index entry offset table; the split index, its EWAH bitmaps and its shared index merged into one list of entries; the sparse index, its sparse directories expanded through their trees when asked; unknown extensions passed over or refused by git's rule, and every complaint git makes of a malformed one in git's words;
  • Repository, the library's way in: a discovered repository opened with its configuration from every source, config.worktree included, its includeIf conditions answered by the repository itself (onbranch: from its refs, in either storage), its object database and its ref store, and its index, from GIT_INDEX_FILE when the caller passes it;
  • revisions, the first of the revision layer: every way gitrevisions(7) names a single object, read into a structured form with no I/O and then resolved — object IDs and their abbreviations, ref names by git's ordered rules with its warnings for names that are more than one thing, git describe output, @, ^<n>, ~<n>, every ^{...}, :/<text> and ^{/<text>} with POSIX extended regular expressions, <rev>:<path> with ./ and ../, :[<n>:]<path> in the index, reflog entries by count and by date (the current time passed in), @{-<n>}, and @{upstream} and @{push} from branches, remotes and push.default in every mode — each failure named and carrying git's message, and ranges and other sets of commits parsed for the walk to come and refused here;
  • refspecs, fetch and push, read with git's rules and mapped both ways;
  • the commit-graph: its file and split chains of them, every chunk — fanout, object IDs, commit data, extra edges, corrected commit dates and their overflow, changed-path Bloom filters of both hash versions, base graphs — and git's rules for passing over a file, dropping a chunk or cutting a chain short, each found by corrupting files git wrote and said in git's words; commits read from it, with the object as the fallback, and generation numbers as corrected commit dates or topological levels;
  • walking commits as git rev-list walks them: starting points and exclusions, --not, --all and the ref listings, the default order with git's handling of equal dates and of clocks that run backwards, --date-order, --author-date-order, --topo-order, --reverse, --max-count, --skip, the parent counts, --first-parent, --ancestry-path, --since, --until, --since-as-filter, --boundary, the sides of a symmetric difference and children — every rule found by comparing a model with git over thousands of generated histories, merges, octopus merges and criss-cross merges included;
  • merge bases as git merge-base finds them, --all in git's order for criss-cross histories, --octopus, --independent, --is-ancestor and --fork-point from the reflog.
  • pathspecs as gitglossary(7) describes them and git ls-files and git log match them: literal paths and directories, wildcards matched against whole paths, the top, literal, glob, icase and exclude magic in their long and short forms, paths relative to the current directory with git's errors for one outside the working tree, and the GIT_LITERAL_PATHSPECS, GIT_GLOB_PATHSPECS, GIT_NOGLOB_PATHSPECS and GIT_ICASE_PATHSPECS settings, all read with no I/O;
  • two trees compared in step under a pathspec, passing over every subtree that is the same on both sides or that the pathspec cannot reach, and yielding each file, symbolic link or gitlink added, deleted or modified, as git diff-tree -r lists them;
  • history limited to paths, simplified as git-rev-list(1)'s "History Simplification" describes and as git was seen to do where it does not: the default mode, --full-history, --dense, --sparse, --simplify-merges, --show-pulls and --remove-empty, merges' parents counted only when they are relevant, parents rewritten for --parents and --children with git's reading ahead, and the commit-graph's changed-path Bloom filters asked before trees are compared, in the cases git asks them and with git's own count of what they answered;
  • describing commits as git log --pretty does, as a library that writes to an Io.Writer and nothing else: the built-in formats oneline, short, medium, full, fuller, reference, email (with its RFC 2047 encoded words and folded headers), mboxrd and raw, and format:, tformat: and bare % strings with every placeholder that needs no decorations, notes, signatures or reflogs — object IDs whole and shortened, identities in every date mode and through the mailmap, the subject, body and sanitized subject, %x00, colors with %C(auto), padding and truncation in all eight forms, %w(...) wrapping, the +, - and space magic, and %(trailers) with every option — down to what git does with a NUL, a control character or a placeholder it cannot answer, found by comparing with git over thousands of generated formats and commits; and pretty.<name> aliases;
  • trailers found as git-interpret-trailers(1) finds them, with trailer.separators, configured trailers and the comment string;
  • mailmaps as gitmailmap(5) describes them, every line form, with git's rules for repeated and conflicting lines found by experiment, read from the work tree, mailmap.file, mailmap.blob and a bare repository's HEAD:.mailmap;
  • choosing commits by their messages and identities as --grep, --author and --committer do, with --all-match, --invert-grep, -i, and glibc's basic and extended regular expressions with GNU's escapes, or fixed strings, and the mailmap applied to identities when git applies it;
  • decorations as git log --decorate shows them: the refs, HEAD -> <branch>, tag: , grafted and replaced, in git's order, short or whole, chosen by --decorate-refs, --decorate-refs-exclude, --clear-decorations, log.excludeDecoration and log.initialDecorationSet with git's rules for its patterns, colored by color.decorate.*; %d, %D and %(decorate:...) with every option; --source and %S; and --simplify-by-decoration, simplified as git simplifies it;
  • naming commits by the nearest tag, as git describe does: annotated tags, lightweight ones or every ref, --match and --exclude, git's choice among the names of one commit, and its depth, which is counted as git counts it rather than as git-describe(1) says, all found by comparing with git over every commit of generated histories; and %(describe:...) with every option;
  • the reachability filters --contains, --no-contains, --merged, --no-merged and --points-at, as git branch, git tag and git for-each-ref answer them;
  • two texts compared line by line in the edit script git chooses, not merely a shortest one: the linear-space refinement of Myers' algorithm as his paper gives it, with what git was seen to add — lines that occur only in one text taken out first, lines that occur very often taken out where the lines around them say so, the search for a middle snake given up at git's cost limit on large, much-changed texts (and refused where it would search further, see below), runs of changes slid and merged as git slides them, git's indent heuristic (on by default) placing each run by the indentation and blank lines around it, and the common tail git sets aside for a patch without context — and --minimal, all found by comparing a model with git over many thousands of generated texts; the whitespace options -w, -b, --ignore-space-at-eol and --ignore-cr-at-eol; hunks with -U<n>, --inter-hunk-context, --ignore-blank-lines and -I<regex>, merged and left out as git does, git's own notion of a blank line included; function names after @@ by git's default rule or a diff driver's, cut as git cuts them; -W, each hunk reaching over the whole of the functions its changes are in as git's hunks were seen to reach, the lines just above a function line and git's odd ends of the text included; and the patch for a pair of files — the diff --git header, mode lines, new file mode and deleted file mode, the index line in SHA-1 or SHA-256, --- and +++ with prefixes and quoted paths, \ No newline at end of file, and binary files told apart as git tells them;
  • attributes, as gitattributes(5) gives them: the system and global files, every .gitattributes from the top down to a path's directory and info/attributes, read lazily and in git's order, from the working tree with the index behind it, the index alone or a tree (attr.tree, GIT_ATTR_SOURCE); patterns matched as gitignore(5) reads them, macros (binary built in) expanded where they are set, and git's warnings about bad lines, said as often as git says them;
  • diff drivers' function lines: diff.<driver>.xfuncname and funcname, whichever was set last, as several patterns one to a line with ! for those that rule a line out, the text git shows being the first group of the match where it took part, and git's errors for a malformed or wrongly negated pattern, said when git says them; the regular expressions match as glibc's do, the leftmost-longest match and glibc's choice of groups within it (which is not POSIX's), found by comparing with git over thousands of random patterns; and all 28 of git's built-in drivers (ada, bash, bibtex, cpp, csharp, css, dts, elixir, fortran, fountain, golang, html, ini, java, kotlin, markdown, matlab, objc, pascal, perl, php, python, r, ruby, rust, scheme, swift and tex), as patterns of zig-git's own fitted to which lines git takes and what it shows of them, each over at least 3,000 random lines (18,000 and more for most) with no difference left; their word patterns are not done;
  • the index compared with a tree and with the working tree from the stat data it holds, without refreshing or writing it: modification and change times in whole seconds, inode, owner, group, size and mode, as experiments showed git compares them, racily clean files hashed, and intent-to-add, assume-unchanged, skip-worktree and unmerged entries as git treats them; or, as git diff does after refreshing, a file whose stat data alone changed taken to be unchanged by its contents;
  • every format git writes for a list of changed paths: the raw format (--abbrev, -z), --name-only, --name-status, --diff-filter, --numstat, --stat with git's layout (fitted to over a thousand of git's outputs: the widths, COLUMNS, diff.statNameWidth, diff.statGraphWidth, --stat-count, and the bars' scaling), --shortstat, --summary and the patch, in git's order, with -R, binary files told by attributes, contents and core.bigFileThreshold, gitlinks as Subproject commit lines, and what whitespace options and -I drop from each format;
  • the pickaxe, as gitdiffcore(7) describes it and as git was seen to count: -S<string> (occurrences counted without overlapping, NUL bytes and binary files searched), --pickaxe-regex (matches counted as git resumes its search, the rest of the text not beginning a line, empty matches included), -G<regex> (each line a diff adds or deletes matched alone, with its newline, binary files passed over without --text), --find-object (either side; not in log), --pickaxe-all, and -i, in diff, diff-tree, diff-index, diff-files, log (choosing the commits shown) and show, with git's errors for patterns and for options it refuses together; matched over 2,400 random cases;
  • orderfiles (-O<orderfile>, diff.orderFile for the porcelain), --skip-to and --rotate-to (in log and show, from the first path at or after the one named), --relative[=<path>] and diff.relative (the prefix matched as a string and cut off every path but --summary's; the orderfile and the rotation still match the whole paths, so --relative=dir/ --rotate-to=dir/x starts at what is written as x, and of the pairs taking one deleted source the last in the new order is the rename), --line-prefix (before every line log and show write, the porcelain's --stat narrowed by its width) and -D (--irreversible-delete, a rewrite's hunk counting its old lines as -?,?);
  • --check: trailing whitespace, a space before a tab in the indent, an indent of spaces (tabwidth), a tab in the indent, new blank lines at the end of a file, a last line without a newline and leftover conflict markers (conflict-marker-size), as core.whitespace and the whitespace attribute choose, each found by probing git line by line, with git's exit statuses 2 and 3; and -B breaking a type change between a file and a symbolic link, shown as T100;
  • --dirstat (-X, --dirstat-by-file, --cumulative, diff.dirstat with git's warning) in its changes, lines and files modes, the damage of each path measured as git was seen to measure it (the kept chunks rename detection counts, lines, 64-byte pieces of a binary file), directories folded and shown as git shows them; and zig-git whatchanged, with --i-still-use-this as git 2.56 asks, log with the raw format and commits whose diff is empty, and merges unless -m asks for them, left out (paths, with which git was seen to walk otherwise, are refused).

zig-git diff --no-index compares two files, or two directories file by file, and prints what git diff --no-index prints, with its exit status and its errors: every option above, --diff-algorithm, -R, --quiet, --exit-code, --output, --text, --full-index, --abbrev, --no-prefix, --src-prefix, --dst-prefix, --default-prefix, and diff.algorithm, diff.context, diff.interHunkContext, diff.indentHeuristic, diff.noprefix, diff.mnemonicPrefix (1/ and 2/ without an index), diff.suppressBlankEmpty, core.quotePath, diff.renames and diff.autoRefreshIndex (each stopping, as git stops, at a value that is not a boolean, without naming the file); symbolic links compare as their targets (a link against a file as a deletion and an addition), executable files by mode, and directories by name, a file on one side and a directory on the other shown as git shows them. --patience, --histogram, --anchored, colors (where git would color), --binary, --stat and the other summaries, word diffs, rename detection, pickaxe and - for standard input are refused there by name, and so are two directories with a file only on each side that git could take for a rename (the same contents, or two files at least half alike) unless diff.renames is false. zig-git diff in a repository compares the index with the working tree, a commit with the index (--cached, --staged) or the working tree, and two commits or trees (A B, A..B), with pathspecs, every format and option above, and the configuration git diff reads (diff.context, diff.interHunkContext, diff.algorithm, diff.noprefix, diff.mnemonicPrefix, diff.srcPrefix, diff.dstPrefix, the stat widths, diff.renames); zig-git diff-tree compares two trees or a commit with its parents (-r, -t, --root, -m, --always, --no-commit-id, --stdin), zig-git diff-index a tree with the index or the working tree (--cached, -m), and zig-git diff-files the index with the working tree (-0, -1, -2, -3, --base, --ours, --theirs, -q), each reading only the configuration git's plumbing reads. Every one of them finds renames and copies as git does: -M[<n>], -C[<n>], --find-copies-harder, -l<n> and diff.renameLimit with git's warnings when the limit is reached, --no-renames, --rename-empty, and diff.renames (on by default) for diff, log and show, the pairs scored on the chunks git counts them by (src/diff/similarity.zig) and chosen as git chooses them (src/diff/renames.zig), and written in every format: R096 and C100 in the raw and name formats, similarity index, rename from and copy to in the patch, a/{b => c} in --stat, --numstat and --summary. -B[<n>][/<m>] breaks rewrites into a deletion and an addition for renames to take, and shows those nothing took as rewrites (M100, dissimilarity index, the whole file's lines, rewrite in --summary, B for --diff-filter). zig-git log --follow <path> (and log.follow with one path) follows a file back through its renames and copies, as git does: commits chosen by the pathspec they followed, and where the path it names was added, its source looked for among every path, unchanged ones included. zig-git log and zig-git show write each commit's diff in any of these formats after its message (--full-diff, --first-parent, log.showRoot), with the separators git puts there in each pretty format, show defaulting to the patch, and show merges as -m, --diff-merges=off|on|first-parent|separate (each merge once per parent, with (from <parent>) on its first line), --dd, --no-diff-merges and log.diffMerges ask, or as combined diffs (-c, --diff-merges=combined) and dense combined diffs (--cc, --dd's merges aside, --diff-merges=dense-combined, and git show's default for a merge): the raw format with a colon per parent, the names with a status per parent, and the combined patch with its column per parent (src/diff/combined.zig), for merges of any number of parents, paths renamed or copied against a parent compared with their source there, its function names found as git finds them there (by the default rule whatever the diff driver, from no further back than the hunk before, cut short at 40 bytes and then by one more) and binary files, as attributes and drivers decide them, said to differ. The dense form leaves out each stretch of changes where the parents have only two versions and the result takes one of them as it is, as git-diff-tree(1) describes it; which changes make one stretch, and how a hunk is counted around a change left out, were found by experiment and matched over thousands of generated merges of two to four parents. Without context (-U0) only the changes are written, with git's counts, even the one less it gives a hunk reaching past the end of the file. -W and --inter-hunk-context leave a combined patch as it is, as they do git's, and -I stops at the first merge as git does. zig-git check-attr reports attributes as git check-attr does (-a, --cached, --source, --stdin, -z, builtin_objectmode). zig-git cat-file (-t, -s, -e, -p and <kind> <object>) answers from the library for objects named by any single revision (cat-file -p HEAD~2:src/x.zig), wherever they are stored, printing what git cat-file prints, its error messages, explanations of paths not found and ambiguity hints included; like git's, it takes no range and searches no message. zig-git show-index and zig-git verify-pack [-v] [-s] print what git's do, the delta chain histogram included. zig-git config reads configuration as git config does — --get, --get-all, --get-regexp, --list, the get and list subcommands, with --show-origin, --show-scope, -z, --name-only, the types bool, int, bool-or-int and path, and every file option — its output, errors and exit statuses included. It writes nothing, and refuses by name what it does not do: --blob, --url, the color and expiry-date types, and GNU's regular expression extensions and back references. zig-git rev-parse answers the questions about where a repository is — --git-dir, --absolute-git-dir, --git-common-dir, --show-toplevel, --show-prefix, --show-cdup, the --is-* questions, --show-object-format, --show-ref-format, --local-env-vars and --path-format — as git rev-parse does, and answers single revisions with --verify, -q, --short[=<n>], --symbolic, --symbolic-full-name, --abbrev-ref[=(strict|loose)], --default, --end-of-options, -- and --disambiguate=<prefix>, telling revisions from paths as git does, and lists refs with --all, --branches, --tags, --remotes, --glob and --exclude. It prints ranges and the other sets of commits as the revisions they stand for (A..B as B and ^A), in each of its ways of naming them, and refuses by name the options it does not have (--not, --sq, --revs-only and the like). zig-git rev-list walks commits as git rev-list does, with every option above and --parents, --children, --timestamp, --left-right, --count, --abbrev-commit, the searches by message and identity, and every format log has; it takes paths, after -- or, where they are there, without it, with git's complaints about an argument that is both a revision and a file, a later one that is neither, and an option after a path, and simplifies the history they limit in every mode above. It refuses by name --follow, --objects, the patch-equivalence options (--cherry-pick, --cherry-mark and --cherry, which need diffs), reflog walks and the rest. zig-git merge-base answers in all five of git's modes, and zig-git describe names commits with every option above, --long, --abbrev, --candidates, --exact-match, --first-parent and --always included. zig-git log walks as rev-list does, every option above included, and describes each commit in any format above, with --oneline, --abbrev-commit, --date, --expand-tabs, --use-mailmap, --color, the searches, decorations and --source, and format.pretty, pretty.<name>, log.date, log.abbrevCommit, log.mailmap, log.decorate, color.ui, color.diff, color.diff.commit and color.decorate.*; zig-git rev-list takes the same formats, --header and --no-commit-header, in rev-list's framing. zig-git show -s shows commits, annotated tags, trees and blobs as git show does, and zig-git check-mailmap maps contacts. They refuse by name what needs what the library does not have yet: notes (--notes, %N), signatures (--show-signature, %G? and the rest), reflog walks and %g*, --graph, Perl's regular expressions (-P and grep.patternType=perl), and output in any encoding but UTF-8. Both read through the commit-graph when git would, say what git says of a corrupt one, and walk the history replace refs, grafts and a shallow boundary make. zig-git check-ref-format (--normalize, --allow-onelevel, --refspec-pattern), zig-git show-ref (--head, --branches, --tags, -d, -s, --hash[=<n>], --abbrev[=<n>], --verify, --exists, -q, patterns), zig-git for-each-ref (patterns, --exclude, --sort=[-]refname, --count, the reachability filters, and the --format atoms refname with its :short, :lstrip and :rstrip, objectname and *objectname with their :short[=<n>], objecttype, symref, *objecttype and HEAD) and zig-git symbolic-ref (-q, --short, --no-recurse) read refs as git's do, warnings about broken refs included, and refuse by name the atoms and options they do not have. zig-git branch and zig-git tag list branches (-a, -r, --show-current, a detached HEAD described from its reflog) and tags, with patterns and the reachability filters; they create, rename and delete nothing, and refuse by name every option that would, and verbose, sorted, formatted, columned and colored output. zig-git ls-files lists the index as git ls-files does — --cached, --stage, --unmerged, --resolve-undo, -z, -t, -v, -f, --debug with its stat data and flags, --deduplicate, --sparse, --full-name, --abbrev[=<n>], and --format with the atoms objectmode, objecttype, objectname, objectsize, stage and path — from any directory, with pathspecs, a sparse index expanded with git's hint, and every message git gives a corrupt index. It refuses by name what needs the working tree (--others, --modified, --deleted, --ignored, --killed, the exclude options, --eol), the attr pathspec magic, --error-unmatch, --with-tree and --recurse-submodules. Every command stops at a bad core.abbrev or core.disambiguate as git does. Every command finds its repository the way git does, takes -C, -c, --git-dir, --work-tree and --no-replace-objects before its name, and says what git says when there is none, or one it will not use. The differences are deliberate or not yet matched:

  • an object holding fewer bytes than its header says is refused, where git shows whatever its buffer held in their place;
  • a REF_DELTA chain that loops is DeltaChainTooDeep, where git never returns;
  • git never resolves a REF_DELTA whose base is outside its own pack, and nor does the library unless asked to (resolve_external_bases);
  • what git says on standard error of a corrupt delta, of a corrupt pack found by verify-pack, and of a broken pack met on the way to a copy that could be read, is not matched; the answers and exit statuses are;
  • a format: string is refused where git would write it differently from strftime: where git's own pass over %s, %z and %Z pairs % signs differently (%E%s, %5%%s), %s with a flag (the C library's mktime), %z and %Z with a flag in format: (written at UTC's offset), git's own %s and %z where they are not the timestamp and the offset strftime writes (a local time from 2100 on, or a recorded zone such as +0099), a local abbreviation longer than six bytes, and a field width past 255; strftime is the C locale's only;
  • a date whose year does not fit in 32 bits is refused in every mode that shows a calendar date, where git wraps the year;
  • a TZ rule naming daylight saving time without its dates while the database has a posixrules file to supply them, or with a month of 0 or past 12, is refused rather than guessed at;
  • relative and human are matched for a current time before 2038, the latest GIT_TEST_DATE_NOW can set;
  • zig-git config expands %(prefix)/ only when given a prefix, since it is not git and has none of its own;
  • a repository with extensions.compatObjectFormat is refused, where git reads it (and crashes when it names the object format itself);
  • safe.bareRepository with no value is refused as a bad setting, where git crashes;
  • a commondir that begins with blank space names no repository, where git names a path part-way through resolving it;
  • a bad core.useReplaceRefs stops zig-git only once it opens the repository, where git stops even to answer rev-parse --git-dir;
  • GIT_REPLACE_REF_BASE not ending in / is refused, where git stops on a bug of its own; a replace ref that is a broken or dangling symbolic ref replaces nothing, where git cannot read the object; a replaced empty tree is read as its replacement, where git's cat-file -p reports a hash mismatch; and a commit replaced by an object of another kind is not matched;
  • the warning for a replace ref's bad name is written when the command first reads commits or, in cat-file, an object, which may come after other warnings git writes after it;
  • grafts or replacements that make a cycle are walked, where git's --topo-order lists nothing;
  • zig-git name-rev, and describe --contains, which names commits as it does, are refused: no rule tried matched git's choice among the names that reach a commit, which sometimes prefers a name through a merge to a nearer one along first parents;
  • zig-git describe refuses to choose among candidates of more than one kind — annotated tags, lightweight tags, other refs — where git was seen not to take the nearest, nor one kind over another; with candidates of one kind it matches git's choice and depth; --dirty, --broken, --debug and describing a blob are refused;
  • --simplify-by-decoration is refused with paths, exclusions, a commit-graph, --sparse, --first-parent, --boundary, --show-pulls, --remove-empty, --children or a parent count, the combinations where --simplify-merges, whose rules it shares, was seen to differ or was not compared;
  • ownership and filesystem boundaries are found with statx, so only on Linux; elsewhere a caller supplies its own answers or discovery says it cannot check;
  • a reftable stack git cannot read, git treats as empty without a word; the library names what is wrong, and the commands fail;
  • a malformed packed-refs is refused whole, where git's single-ref lookups in a file that says it is sorted may pass over the bad line, and show-ref prints the refs before it;
  • a malformed reflog line is passed over as git passes it over, but the identity and message are read as the identity line and everything after the tab, where git reads some malformed lines differently;
  • an index entry or extension running past the end of the file, or a path whose length disagrees with its flags, is refused, where git shows whatever lies past it or crashes; so is an entry with an empty path, which git shows as ./, and a link extension of the object ID alone, which crashes git;
  • a sparse directory whose tree cannot be read is refused, where git crashes, and an fsmonitor bitmap longer than the index is dropped with a warning, where git stops on a bug of its own;
  • like git, the index's checksum is not checked unless asked for;
  • a revision whose search pattern uses GNU's regular expression extensions (\w, \b and the rest) is refused, and so is @{upstream} or @{push} in a repository with remotes in the files of remotes/ or branches/, which git still reads;
  • @{-<n>} reads only HEAD's checkout: moving from entries, and @{push} takes no account of remote.<name>.mirror;
  • among commits of one committer date, a message search answers the one put in its queue last, as git was seen to; git's order may differ in histories where that rule was not tested;
  • zig-git cat-file finds nothing by a range, as git does, rather than refusing it;
  • zig-git rev-parse --disambiguate with a single digit is refused: git then lists some of the objects the digit begins and not others;
  • zig-git rev-parse stops at a bad core.abbrev, core.disambiguate or push.default only when it reads objects, where git stops even to answer the discovery queries;
  • a tag whose tagger's timestamp is negative is refused in an ambiguity hint, where git stops with "Timestamp too large for this system";
  • zig-git ls-files --debug and -f are refused when core.fsmonitor is set, since git asks the monitor before it answers them, and long options cannot be abbreviated as git allows;
  • with a commit-graph, git orders --topo-order, --date-order and --author-date-order walks by generation number, and with exclusions, --first-parent, --since, --until or --since-as-filter it lists something other than the same walk without a commit-graph — in one split chain whose dates ran out of order, nothing at all for a range of dozens of commits — so those combinations are refused there; without them, and in the default order, the walk is the same either way;
  • --ancestry-path=<commit> is refused, and so is --children with --boundary for a symmetric difference, whose boundary commits' children depend on which commits git's merge-base search happened to read;
  • given a commit-graph whose parent positions are corrupt in a commit that is only an ancestor of the merge bases, git may find the bases without reading it, where zig-git merge-base reads it and stops with git's own complaint;
  • gitformat-commit-graph(5) gives the second Bloom filter seed as 0x7e646e2; the filters git writes use 0x7e646e2c, and so does the library.
  • the attr pathspec magic is refused, where git reads attributes from the working tree, and so is the internal prefix magic;
  • with paths, zig-git rev-list refuses what was seen to list commits git's rules did not account for: --ancestry-path; --first-parent with --parents, --children, --boundary, an ordering or --remove-empty; --parents with --full-history and --reverse or --skip in a walk without exclusions or an ordering; --remove-empty with --boundary, with --reverse and --parents, or in a repository with changed-path Bloom filters, where git was seen to list other commits with the filters than without them; and --simplify-merges with excluded commits, --remove-empty, --sparse, --first-parent, --boundary, --show-pulls, a parent count or a commit-graph;
  • --follow is refused across a merge (at the first one the walk reaches, after the commits before it are written) and with --reverse;
  • a commit whose encoding header names anything but UTF-8 is refused wherever its message or identities are shown, as is i18n.logOutputEncoding or --encoding naming anything else, since messages are not re-encoded;
  • colors that git turns on only for a terminal (--color=auto, color.ui=auto) stay off, as zig-git takes its output never to be one;
  • a few corners of padding are not matched: %>>(<n>) taking back spaces written before a %w(...) that changes nothing, padding followed by a color and then %%, %>>(<n>) with the space magic on a placeholder too wide for it, and padding followed by a color and a %w(...); nor is a NUL written before a %w(...) region, which git's wrapping loses;
  • the display widths %<(<n>) and %w(...) count are Unicode 17.0.0's, which match git 2.56 at every code point except U+FFFE and U+FFFF (git does not take those for UTF-8 and counts a column a byte), and may differ from a git built with another Unicode version for the characters added or changed since;
  • a malformed --grep or --author pattern is refused in glibc's words, where git names it before refusing an unknown option, and a back reference is refused by name;
  • git's indent heuristic is matched by a model fitted to git's choices (src/diff/indent.zig), not derived from a description of it: its structure was pinned by probes, its weights fitted to several hundred thousand generated slides. It disagreed with git on 3 of 40,000 generated code-like comparisons, and on none of 14,050 others; where it disagrees, a run is placed a line from where git puts it. --no-indent-heuristic and diff.indentHeuristic=false turn it off;
  • a line diff whose search for a middle snake would go on past 256 steps is refused (LineDiff.Error.LongSearchUnsupported). Only a comparison of more than 65,532 lines, once the common head and tail and the lines found in one text only are set aside, can search that long, and past that step git sometimes splits the comparison early at a run of matching lines it has found. Probes found most of the rule (from step 257, a run of at least 21 lines ending more than twice as many lines from where that search began, in each text, as the steps taken), but git was also seen to split at runs of exactly 20 lines and at runs found before step 257, by a rule not yet found. In 640 generated comparisons of 5,000 to 100,000 lines, 145 were refused and the rest matched git; --minimal never searches that way and is not refused;
  • --patience and --histogram are refused, their choices not yet matched;
  • long options cannot be abbreviated in zig-git diff, as git allows, and short ones cannot be bundled;
  • --find-copies-harder against the working tree or the index under -R is refused, and so is --diff-filter for a merge in show;
  • combined diffs (-c, --cc) with --ignore-blank-lines, where git leaves changes out by another rule, or of a rewrite -B broke, are refused, and so are diff-tree -c and --cc, git diff-files -p with unmerged paths (unless -1, -2 or -3 chooses a stage), --combined-all-paths, --remerge-diff and --diff-merges=remerge;
  • a function pattern with a back reference, which git takes, is refused, and so is one whose first group lies inside a repetition that may iterate more than once, where glibc's choice of iteration was matched only 98 times in 100 over 3,440 random cases (against none missed in 13,660 with the group anywhere else); so is -W together with --ignore-blank-lines or -I, whose meeting has not been matched; drivers that run programs (diff.<driver>.command, textconv) and diff.external are refused too. --binary, colors, word diffs, --color-moved, --submodule=log|diff and --ignore-submodules (other than none) are refused by name;
  • the pickaxe is refused with --quiet, which stops at the first difference before the pickaxe has seen the rest; with -i and --find-object together, where git stops on a bug; with --find-object naming a tree (git then lists every diff's trees), or against the working tree; with unmerged paths, a merge's non-empty combined diff and --follow; and log --find-object is refused, since git was seen to list the commits it chooses out of the walk's order;
  • --relative is refused with paths, which git matches against the shortened paths, with the working tree, and with --summary, whose paths git leaves whole; --line-prefix is refused with -z;
  • --check is refused where a file of blank lines alone, before or after, ends in blank lines, where git was seen to choose the line it reports by a rule not found; and so is a whitespace attribute with both tab-in-indent and indent-with-non-tab, and a conflict-marker-size that is not a number;
  • log with -m, --dd or --diff-merges (other than off and combined) and paths is refused: git then walks as --full-history walks, and shows a merge against its rewritten parents;
  • contents git would convert on their way in from the working tree are refused: core.autocrlf, and the text, eol, crlf, filter, ident and working-tree-encoding attributes on a file read there; so are a checked-out submodule's HEAD, core.fsmonitor and a sparse index;
  • git diff refreshes the index and writes it when it can; zig-git diff never writes it, comparing contents where git would refresh, so that the output is the same; the device number in the stat data is not compared, nor nanoseconds, which git was seen not to compare either;
  • zig-git diff outside a repository without --no-index, --merge-base, A...B, blobs and more than two commits are refused;

The API documentation is generated from the doc comments and published at https://jeff.jcollie.page/zig-git/.

What it is for #

grebe, a history browser for git, reads repositories by running git log and git show. zig-git is being written so that it can stop: the plan is a library in layers, each depending only on the ones before it.

  1. Core — the object database (loose objects, packs, the multi-pack index, alternates), the ref store (loose refs, packed-refs, reftable, reflogs), configuration with includes, discovering a repository the way git does, and the index file.
  2. Revision — revision arguments in git's own syntax, and walking the history they choose: ranges, path limiting, the three orderings, mailmap, replace refs, grafts.
  3. Diff — between two texts first, then between trees, the index and the working tree, producing patches byte-identical to git's.
  4. Write and transport, later: ref transactions, writing objects, signing, and git's protocol v2 over file://, ssh:// and https://.

Three choices run through all of it, and the decision records in docs/adr/ give the reasons:

  • Agreeing with git is the definition of correct. The tests build their repositories with git and compare answers with it, and patches are to match git's byte for byte.
  • What it does not support, it says so. A revision argument outside the supported set is an error naming it, and a repository using an extension it does not implement is refused, so a caller can fall back to git rather than receive an answer that is nearly right.
  • It is written from git's documentation and behavior, never its source, so that it can be MIT licensed.

The words used for the things in a repository — object ID, object format, object database, ref store, git directory — are defined in GLOSSARY.md.

Building and testing #

Everything is pinned in the Nix flake, Zig and the git the tests compare against included:

nix develop
zig build test --summary all   # every repository test runs with SHA-1 and SHA-256
zig build docs-serve           # the API documentation on http://localhost:8000

CONTRIBUTING.md says more.

Where this lives #

The canonical repository is on my Forgejo instance, and Tangled carries a mirror of it:

git clone https://git.jcollie.dev/jeff/zig-git.git

Report issues and send pull requests to the Tangled mirror. CI and the published documentation come from the Forgejo repository.

License #

MIT; see LICENSES/MIT.txt. The project follows REUSE.

References cited #

  • Deutsch, P. and J-L. Gailly. ZLIB Compressed Data Format Specification version 3.3. RFC 1950, 1996. https://www.rfc-editor.org/info/rfc1950 — the container loose objects are stored in, and its checksum.
  • Deutsch, P. DEFLATE Compressed Data Format Specification version 1.3. RFC 1951, 1996. https://www.rfc-editor.org/info/rfc1951 — the stored blocks the tests write corrupt loose objects with.
  • The Git Project. gitrepository-layout(5): Git Repository Layout. https://git-scm.com/docs/gitrepository-layout — the objects directory, objects/info/alternates, .git files, commondir, a worktree's gitdir, the repository format versions, and info/grafts and shallow.
  • The Git Project. git(1). https://git-scm.com/docs/git — GIT_ALTERNATE_OBJECT_DIRECTORIES and its quoting, -c, the GIT_CONFIG_* environment variables, -C, --git-dir, --work-tree, GIT_INDEX_FILE, --no-replace-objects and GIT_NO_REPLACE_OBJECTS, and the variables discovery reads: GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY, GIT_CEILING_DIRECTORIES and GIT_DISCOVERY_ACROSS_FILESYSTEM; and GIT_LITERAL_PATHSPECS, GIT_GLOB_PATHSPECS, GIT_NOGLOB_PATHSPECS and GIT_ICASE_PATHSPECS; GIT_ATTR_NOSYSTEM, GIT_ATTR_SOURCE and GIT_EXTERNAL_DIFF.
  • The Git Project. hash-function-transition: Transitioning Git to SHA-256. https://git-scm.com/docs/hash-function-transition — SHA-256 object formats alongside SHA-1, and extensions.compatObjectFormat.
  • The Git Project. git-rev-parse(1). https://git-scm.com/docs/git-rev-parse — what zig-git rev-parse answers, --path-format, --verify, --quiet, --short, --disambiguate, --symbolic, --symbolic-full-name, --abbrev-ref, --default, --all, --branches, --tags, --remotes, --glob and --exclude.
  • The Git Project. git-worktree(1). https://git-scm.com/docs/git-worktree — linked worktrees, their git directories, extensions.worktreeConfig and the core.bare and core.worktree it moves, and which refs are per-worktree and how main-worktree/ and worktrees/ reach them.
  • The Git Project. gitsubmodules(7). https://git-scm.com/docs/gitsubmodules — a submodule's .git file and its git directory under the superproject's.
  • The Git Project. Upcoming breaking changes. https://git-scm.com/docs/BreakingChanges — why safe.bareRepository exists, and its default to come.
  • The Linux man-pages project. statx(2). https://man7.org/linux/man-pages/man2/statx.2.html — a file's owner and device, for ownership and filesystem boundaries.
  • The util-linux project. unshare(1). https://man7.org/linux/man-pages/man1/unshare.1.html — the user namespace the tests run git as root in, to see a repository someone else owns.
  • The Git Project. reftable. https://git-scm.com/docs/reftable — the reftable format: header, footer, blocks, records, varints, the tables.list stack and its layout in a repository.
  • The Git Project. git-check-ref-format(1). https://git-scm.com/docs/git-check-ref-format — the rules for ref names, and what zig-git check-ref-format answers.
  • The Git Project. git-show-ref(1). https://git-scm.com/docs/git-show-ref — what zig-git show-ref answers, and its patterns.
  • The Git Project. git-for-each-ref(1). https://git-scm.com/docs/git-for-each-ref — what zig-git for-each-ref answers: patterns, field names, %(refname:short), and --contains, --no-contains, --merged, --no-merged and --points-at.
  • The Git Project. git-symbolic-ref(1). https://git-scm.com/docs/git-symbolic-ref — what zig-git symbolic-ref answers.
  • The Git Project. git-pack-refs(1). https://git-scm.com/docs/git-pack-refs — packed-refs, and the compaction the tests run.
  • The Git Project. git-update-ref(1). https://git-scm.com/docs/git-update-ref — the reflog line format.
  • The Git Project. git-refs(1). https://git-scm.com/docs/git-refs — git refs migrate, which the tests convert repositories with.
  • The Git Project. git-reflog(1). https://git-scm.com/docs/git-reflog — the oracle for reading reflogs.
  • The Git Project. gitrevisions(7). https://git-scm.com/docs/gitrevisions — the rules short ref names are tried by, which shortening reverses; abbreviated object IDs, and the ^{commit} and ^{tree} suffixes that tell an ambiguous one apart; every form of a single revision, the ranges and sets of commits refused for now, and git describe output.
  • The Git Project. gitcli(7). https://git-scm.com/docs/gitcli — how git tells a revision from a path, behind rev-parse's and rev-list's complaints about an argument that is neither, or both, and --.
  • The Git Project. git-fetch(1). https://git-scm.com/docs/git-fetch — the refspec format, pattern and negative refspecs, and how a remote's fetch refspecs say where its branches are tracked.
  • The Git Project. git-push(1). https://git-scm.com/docs/git-push — push refspecs, : and deleting with an empty source.
  • The Git Project. git-fast-import(1). https://git-scm.com/docs/git-fast-import — the stream the tests build long histories with merges and tags from.
  • The Git Project. gitdatamodel(7). https://git-scm.com/docs/gitdatamodel — references, symbolic references and HEAD.
  • The Git Project. git-cat-file(1). https://git-scm.com/docs/git-cat-file — what zig-git cat-file answers.
  • The Git Project. git-hash-object(1). https://git-scm.com/docs/git-hash-object — how an object ID is computed, and --literally, which the tests make malformed objects with.
  • The Git Project. gitformat-pack(5): Git Pack Format. https://git-scm.com/docs/gitformat-pack — packs, pack indexes, the delta instruction format and the multi-pack-index.
  • The Git Project. gitformat-chunk(5): Chunk-based File Formats. https://git-scm.com/docs/gitformat-chunk — the table of contents the multi-pack-index begins with.
  • The Git Project. Multi-Pack-Index (MIDX) Design Notes. https://git-scm.com/docs/multi-pack-index — incremental multi-pack-index chains and their layout.
  • The Git Project. git-multi-pack-index(1). https://git-scm.com/docs/git-multi-pack-index — writing one, and the incremental chains the tests make.
  • The Git Project. git-show-index(1). https://git-scm.com/docs/git-show-index — what zig-git show-index answers.
  • The Git Project. git-verify-pack(1). https://git-scm.com/docs/git-verify-pack — what zig-git verify-pack answers.
  • The Git Project. git-index-pack(1). https://git-scm.com/docs/git-index-pack — version 1 pack indexes and completing thin packs with --fix-thin.
  • The Git Project. git-pack-objects(1). https://git-scm.com/docs/git-pack-objects — thin packs, and the deepest delta chain git writes.
  • The Git Project. git-repack(1). https://git-scm.com/docs/git-repack — the packs the tests make.
  • The Git Project. git-clone(1). https://git-scm.com/docs/git-clone — --shared, which borrows objects through alternates, and --depth, --shallow-since and --shallow-exclude, with which the tests make shallow clones.
  • The Git Project. git-config(1). https://git-scm.com/docs/git-config — the configuration file syntax, its value types, its sources and scopes, includes and their conditions, what zig-git config answers, protected configuration, core.bare, core.worktree, safe.directory, safe.bareRepository, every extensions.* git knows, and the index.*, core.splitIndex, core.sparseCheckout, core.fsmonitor and core.quotePath settings the index is read by, core.abbrev, core.warnAmbiguousRefs, advice.objectNameWarning, branch.<name>.remote, .merge and .pushRemote, format.pretty, pretty.<name>, log.date, log.abbrevCommit, log.mailmap, log.decorate, log.excludeDecoration, log.initialDecorationSet, color.decorate.<slot>, core.useReplaceRefs, color.ui, color.diff, color.diff.commit, the "color" value type, grep.patternType, grep.extendedRegexp, core.commentChar, core.commentString, mailmap.file, mailmap.blob and i18n.logOutputEncoding, diff.algorithm, diff.context, diff.interHunkContext, core.whitespace, diff.orderFile, diff.relative, diff.dirstat, diff.indentHeuristic, diff.noprefix, diff.mnemonicPrefix, diff.suppressBlankEmpty, diff.external, diff.srcPrefix, diff.dstPrefix, diff.statNameWidth, diff.statGraphWidth, diff.renames, diff.renameLimit, diff.<driver>.binary, diff.<driver>.xfuncname, diff.<driver>.funcname and the driver settings zig-git refuses, core.attributesFile, attr.tree, core.bigFileThreshold, core.fileMode, core.trustctime, core.checkStat, core.symlinks, core.autocrlf, log.showRoot, remote.pushDefault, remote.<name>.url, .pushurl, .fetch and .push, push.default, and commitGraph.readChangedPaths and commitGraph.changedPathsVersion.
  • The Git Project. gitignore(5): Specifies intentionally untracked files to ignore. https://git-scm.com/docs/gitignore — the glob patterns wildmatch implements, ** among them.
  • The Git Project. gitattributes(5): Defining attributes per path. https://git-scm.com/docs/gitattributes — attribute patterns, through which git check-attr was asked how globs match paths; the files, their precedence, the four states, macros and binary; the diff attribute, diff drivers and diff.<driver>.binary; the conversion attributes zig-git refuses; the whitespace and conflict-marker-size attributes --check reads.
  • The Git Project. git-check-attr(1). https://git-scm.com/docs/git-check-attr — the oracle for the wildmatch tests, and what zig-git check-attr answers.
  • The Git Project. git-tag(1). https://git-scm.com/docs/git-tag — git tag -l and --ignore-case, the oracle for patterns matched without the pathname rule, and what zig-git tag lists.
  • The Open Group. Regular Expressions. The Open Group Base Specifications Issue 8, chapter 9, 2024. https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap09.html — their syntax, basic and extended, bracket expressions and character classes, for zig-git config, message searches, --grep and diff drivers' function patterns, and the leftmost-longest rule for where a match lies (from which glibc's groups were seen to depart).
  • The Open Group. strtol: convert a string to a long integer. The Open Group Base Specifications Issue 8, 2024. https://pubs.opengroup.org/onlinepubs/9799919799/functions/strtol.html — how git reads an integer value, base prefixes and all.
  • Cox, Russ. Glob Matching Can Be Simple And Fast Too. 2017. https://research.swtch.com/glob — matching globs by resuming from the latest star, which wildmatch extends to ** to keep its work bounded.
  • Cox, Russ. Regular Expression Matching Can Be Simple And Fast. 2007. https://swtch.com/~rsc/regexp/regexp1.html — running every state of an automaton at once, as zig-git config's regular expressions do.
  • The Git Project. gitglossary(7): A Git Glossary. https://git-scm.com/docs/gitglossary — the vocabulary GLOSSARY.md follows, root refs, pseudorefs and per-worktree refs, and pathspecs: their syntax, the long and short magic forms, and the top, literal, icase, glob, attr and exclude magic.
  • The Git Project. git-log(1). https://git-scm.com/docs/git-log — the --date= modes, auto:, format: and human; its "PRETTY FORMATS": the built-in formats and every placeholder; and what zig-git log answers: --pretty, --format, --oneline, --abbrev-commit, --expand-tabs, --use-mailmap, --grep, --author, --committer, --all-match, --invert-grep, -i, -E and -F; and --decorate, --decorate-refs, --decorate-refs-exclude, --clear-decorations, --source, and the %d, %D, %(decorate), %S and %(describe) placeholders; its diff options, --full-diff and --first-parent.
  • The Git Project. git-show(1). https://git-scm.com/docs/git-show — what zig-git show answers, and how it shows tags, trees and blobs; its default patch and combined diff for merges.
  • The Git Project. git-log(1): Diff Formatting. https://git-scm.com/docs/git-log#_diff_formatting — -m, -c, --cc, --dd, --no-diff-merges, --diff-merges and its formats, --follow, and log.diffMerges and log.follow.
  • The Git Project. git-interpret-trailers(1). https://git-scm.com/docs/git-interpret-trailers — trailer blocks, their keys, separators, folded values and the share of trailers a block needs, and trailer.separators and trailer.<key-alias>.key.
  • The Git Project. gitmailmap(5). https://git-scm.com/docs/gitmailmap — the mailmap's line forms and matching.
  • The Git Project. git-check-mailmap(1). https://git-scm.com/docs/git-check-mailmap — what zig-git check-mailmap answers.
  • Moore, K. MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text. RFC 2047, 1996. https://www.rfc-editor.org/info/rfc2047 — the encoded words the email format writes a header past ASCII in.
  • Resnick, P., Ed. Internet Message Format. RFC 5322, 2008. https://www.rfc-editor.org/info/rfc5322 — the specials that make the email format quote a name.
  • The Unicode Consortium. Unicode Standard Annex #11: East Asian Width. https://www.unicode.org/reports/tr11/ — which characters take two columns when padding and wrapping.
  • The Unicode Consortium. Unicode Character Database, version 17.0.0. https://www.unicode.org/Public/17.0.0/ucd/ — DerivedEastAsianWidth.txt and DerivedGeneralCategory.txt, from which tools/gen_width.zig generates the tables of characters taking no columns and two.
  • The Unicode Consortium. Unicode Standard Annex #44: Unicode Character Database, revision 36 (Unicode 17.0.0). https://www.unicode.org/reports/tr44/tr44-36.html — the format of the UCD's data files, and the @missing lines that give the default East Asian width of unassigned code points.
  • The Git Project. git-rev-list(1). https://git-scm.com/docs/git-rev-list — --since and --until, through which the approximate parser was checked; and what zig-git rev-list answers: commit limiting, the orderings, --boundary, --ancestry-path, --parents, --children, --left-right and --count; and its "History Simplification": TREESAME, --simplify-by-decoration, the default mode, --full-history with and without parent rewriting, --dense, --sparse, --simplify-merges, --show-pulls and --remove-empty, and its two example histories, which the tests build.
  • The Git Project. git-diff-tree(1). https://git-scm.com/docs/git-diff-tree — -r and its raw output, the oracle for comparing trees; -t, --root, -m, --always, --no-commit-id, --stdin and the raw output format, what zig-git diff-tree answers; --cc, whose description of the uninteresting hunks it leaves out is the rule dense combined diffs were matched from, and the raw format for merges.
  • The Git Project. git-diff-index(1). https://git-scm.com/docs/git-diff-index — --cached, -m, and comparing a tree with the index or the working tree, what zig-git diff-index answers.
  • The Git Project. git-diff-files(1). https://git-scm.com/docs/git-diff-files — the stages of unmerged paths, -q, what zig-git diff-files answers.
  • The Git Project. git-mktree(1). https://git-scm.com/docs/git-mktree — trees with gitlinks and with a file and a directory of one name, made for the experiments.
  • The Git Project. api-trace2: Trace2 API. https://git-scm.com/docs/api-trace2 — GIT_TRACE2_PERF, through whose bloom statistics the tests see when git asks its changed-path filters.
  • The Git Project. gitformat-commit-graph(5): Git commit-graph format. https://git-scm.com/docs/gitformat-commit-graph — the commit-graph file, its chunks, generation numbers, the changed-path Bloom filters and the base graphs of a chain.
  • The Git Project. commit-graph: Git Commit-Graph Design Notes. https://git-scm.com/docs/commit-graph — corrected commit dates and topological levels, split chains and their file layout, mixed generation chains, and chains across alternates.
  • The Git Project. git-commit-graph(1). https://git-scm.com/docs/git-commit-graph — writing commit-graphs and split chains for the tests, and commitGraph.generationVersion and commitGraph.changedPathsVersion.
  • The Git Project. git-merge-base(1). https://git-scm.com/docs/git-merge-base — what zig-git merge-base answers: merge bases, --octopus, --independent, --is-ancestor and --fork-point.
  • The Git Project. git-replace(1). https://git-scm.com/docs/git-replace — replace refs, what they replace and how they are turned off, and --graft, with which the tests make them.
  • The Git Project. git-describe(1). https://git-scm.com/docs/git-describe — what zig-git describe answers, and its "Search Strategy".
  • The Git Project. git-name-rev(1). https://git-scm.com/docs/git-name-rev — the naming the experiments could not match, for which name-rev is refused.
  • The Git Project. git-branch(1). https://git-scm.com/docs/git-branch — what zig-git branch lists, --show-current and the reachability filters.
  • The Git Project. git-commit(1). https://git-scm.com/docs/git-commit — its "Date formats" section, the forms GIT_COMMITTER_DATE accepts.
  • The Git Project. git-var(1). https://git-scm.com/docs/git-var — GIT_COMMITTER_IDENT, through which the strict parser was checked.
  • The Open Group. strftime: convert date and time to a string. The Open Group Base Specifications Issue 8, 2024. https://pubs.opengroup.org/onlinepubs/9799919799/functions/strftime.html — the conversions, and the E and O modifiers.
  • The Git Project. gitformat-index(5): Git index format. https://git-scm.com/docs/gitformat-index — the index file: its header, entries, versions, padding, checksum and every extension; the stat data compared with the working tree.
  • The Git Project. bitmap-format: GIT bitmap v1 format. https://git-scm.com/docs/bitmap-format — its appendix on the serialization of an EWAH bitmap, as the split index, untracked cache and fsmonitor extensions hold them.
  • The Git Project. sparse-index: Git Sparse-Index Design Document. https://git-scm.com/docs/sparse-index — sparse directories, the sdir extension, and expanding a sparse index to a full one.
  • The Git Project. git-ls-files(1). https://git-scm.com/docs/git-ls-files — what zig-git ls-files answers: its options, tags and field names.
  • The Git Project. git-update-index(1). https://git-scm.com/docs/git-update-index — index versions, the split index, the untracked cache, skip-worktree, assume-unchanged and the fsmonitor bits, through which the tests make their indexes; "Using index info" and racy git, which the working tree comparison follows.
  • The Git Project. git-sparse-checkout(1). https://git-scm.com/docs/git-sparse-checkout — cone mode and the sparse index, and the settings that allow it.
  • The Git Project. git-status(1). https://git-scm.com/docs/git-status — the oracle for what the untracked cache holds.
  • Myers, Eugene W. An O(ND) Difference Algorithm and Its Variations. Algorithmica 1, 251–266, 1986. https://doi.org/10.1007/BF01840446 — the greedy algorithm, the middle snake and the linear-space refinement the line diff follows.
  • The Git Project. git-diff(1). https://git-scm.com/docs/git-diff — --no-index, the patch format, the combined diff format and its diff --cc header, the combined options, --minimal, the whitespace options, --ignore-blank-lines, -I, --inter-hunk-context, -W (--function-context), the prefixes, --output and the exit status.
  • The Git Project. diff-options, as included in git-diff(1), git-diff-files(1), git-diff-index(1) and git-diff-tree(1) — --indent-heuristic, --diff-algorithm, --full-index, --abbrev and --text; the output formats, --stat and its widths, --diff-filter, -R, --exit-code, and the raw output format; -M, -C, --find-copies-harder, -l, --no-renames and --rename-empty, and the rename and copy lines of the patch and --summary; -S, -G, --find-object, --pickaxe-all, --pickaxe-regex, -O, --skip-to, --rotate-to, --relative, --line-prefix, -D, --check and --dirstat with its parameters.
  • The Git Project. git-whatchanged(1). https://git-scm.com/docs/git-whatchanged — whatchanged as log with the raw format and without merges.
  • The Git Project. gitdiffcore(7): Tweaking diff output. https://git-scm.com/docs/gitdiffcore — the order of the diffcore stages, rename and copy detection as "the amount of material kept", the sources -C and --find-copies-harder add, the preliminary step that pairs files of the same name, and -B's breaking of rewrites and putting them back together; the pickaxe (-S, -G, --pickaxe-all), the orderfile and the rotation, and where each comes in the chain.
  • The Git Project. Git 2.11 Release Notes. https://github.com/git/git/blob/master/Documentation/RelNotes/2.11.0.adoc — the indent heuristic's introduction, as --indent-heuristic and diff.indentHeuristic, chosen by the lines before and after a change.
  • The Git Project. gitattributes(5): Defining a custom hunk-header. https://git-scm.com/docs/gitattributes#_defining_a_custom_hunk_header — git's default rule for the function name after @@, diff.<driver>.xfuncname and the list of built-in drivers; the rest of what git does with a pattern (several to a value, !, funcname, the group shown) was found by experiment.
  • The Open Group. diff: compare two files. The Open Group Base Specifications Issue 8, 2024. https://pubs.opengroup.org/onlinepubs/9799919799/utilities/diff.html — the unified format: --- and +++, @@ ranges and the line marks.
  • Ollie, Jeffrey C. zig-datetime: dates, times and time zones for Zig. 2026. https://git.jcollie.dev/jeff/zig-datetime — the calendar, the time zone database, glibc's lenient reading of TZ, strftime with format strings read at run time, and git's two date parsers.
  • Ollie, Jeffrey C. grebe: a history browser for git. 2026. https://git.jcollie.dev/jeff/grebe — the first consumer, and the source of the test fixture helper.