- Zig 99.6%
- Nix 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
date.zig ports git's date.c: parse_date_basic for the forms git writes,
and approxidate_str for the ones people do, with every rule of each,
down to which of two readings of 01/02 wins and what "tea" means.
Both read a date without a zone in local time, through localtime_r and
mktime, and how a time of day comes out on a day of the other daylight
saving state depends on glibc's mktime in detail. TimeZone ports what
git relies on: tzset_internal's reading of TZ, __tzfile_read for TZif
files and their rule string, __tzset_parse_tz and compute_change for
POSIX rules, __offtime, and __mktime_internal with its remembered offset
and its search for a neighbouring time in the daylight saving state
asked for. Zones with leap seconds, and rule strings that need glibc's
posixrules file, are refused.
--since, --after, --until, --before and --since-as-filter are read with
a clock in the revision context, @{<date>} chooses a reflog entry by
date, and --max-age, --min-age, --min-parents and --max-parents are read
as strictly as 2.56 reads them, failing on anything but a number.
zig-git's rev-parse prints --max-age and --min-age as git's does. The
tests compare twenty-seven dates under eleven zones, from UTC to Chatham
Islands and three kinds of POSIX rule, at six moments either side of
daylight saving's changes in both hemispheres; skipping mktime's search
for the daylight saving state fails them. The devshell and the package's
check set TZDIR, so that git and the library read the same zones.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P9FTtTrkEbuvbZnRyJCBir
|
||
| .forgejo/workflows | ||
| docs/adr | ||
| LICENSES | ||
| src | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| CONTRIBUTING.md | ||
| flake.lock | ||
| flake.nix | ||
| GLOSSARY.md | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
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 today is the core layer, for reading, and the start of the revision layer. The core layer reads:
- object IDs in both object formats, SHA-1 and SHA-256, chosen per repository at run time;
- object headers, and hashing an object to its ID;
- commits, trees, annotated tags and the signatures in them, read from their contents without allocating, as forgivingly as git reads them;
- loose objects, read whole or just their header, with every check git makes when it reads one;
- packs and both versions of the pack index, 64-bit offsets included, with offset and reference deltas resolved, read whole or just their header;
- the multi-pack-index;
- an object database over a repository's objects directory and its alternates, found as git finds them, behind an interface that something other than a directory can implement too;
- abbreviated object IDs, resolved and made as git resolves and makes them;
- replace refs, applied to every object read unless git would leave them off;
- configuration, from every file git reads and the command line, in git's
order, with
include.pathand all four kinds ofincludeIffollowed, and values read as git reads booleans and integers; - git's glob matcher,
wildmatch; - finding the repository a directory is in, as git's setup finds it:
.gitfiles, linked worktrees, bare repositories,GIT_DIRandGIT_WORK_TREE,core.worktree, ceiling directories,safe.directoryandsafe.bareRepository, and the repository format check, which refuses an extension git does not know and names it; - refs, behind an interface: loose refs and
packed-refs, or a stack of reftables, whichever the repository uses, with each worktree's own refs kept apart from the shared ones as git keeps them; symbolic refs followed as git follows them; reflogs; and ref names checked by git's rules; - the index, in all three versions, with its cache tree and resolve-undo records, a split index merged with its shared index, and a sparse index's directory entries.
The revision layer so far:
- resolves revisions, the names gitrevisions(7) gives objects, as git
resolves them: object IDs and abbreviations chosen between by the kind of
object wanted, ref names as a person writes them,
git describeoutput,~nand^n,^{type}and^{/text},@{n},@{-n}and@{upstream}, and paths in a tree or the index; - reads revision arguments as
git rev-listreads them — ranges,--not,--alland the other ways of naming refs, orderings, counts and the parent and age limits — into a revision query that a caller can also build directly; - walks the history a query chooses in git's order, ties and clock skew included: newest first, or topologically by graph, commit date or author date, reversed, with boundary commits;
- limits history to paths as git does, simplifying merges that took the
paths unchanged from a parent, or keeping the full history, or
simplifying merges afterwards (
--simplify-merges), with parents rewritten to join up what is shown, to the ancestry path between commits, and to the commits refs decorate (--simplify-by-decoration); - chooses commits by what they say —
--grep,--authorand--committer, with--all-matchand--invert-grep— combining the patterns as git does, with names seen through the repository's mailmap asgit logsees them; - finds merge bases, and the independent commits among several, as
git merge-basedoes, and which commits reach which, for--mergedand--contains; - walks history with the parents that grafts and a shallow clone's boundary give commits, as git does;
- reads dates as git does for
--since,--untiland@{<date>}, strictly and then approximately (2 weeks ago,noon last friday), in local time as glibc computes it from the zoneTZnames, daylight saving included.
What it does not support yet — pathspec magic such as :(exclude),
@{push}, time zones with leap seconds, any option outside the list — it
refuses by name. Patterns to search messages with go through an interface,
so the regular expression engine is the caller's choice; the one built in
finds fixed strings.
The API documentation is generated from the doc comments and published at https://jeff.jcollie.page/zig-git/.
zig-git, built alongside the library, answers a few of git's plumbing
commands — cat-file, rev-parse, for-each-ref, ls-files,
ls-tree, rev-list, log --format=%H and merge-base — with the
library rather than with git, printing what git
prints, so that the two can be compared on any repository:
zig build run -- ls-tree -r --abbrev HEAD
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.
- 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. - Revision — revision arguments in git's own syntax, and walking the history they choose: ranges, path limiting, the three orderings, mailmap, replace refs, grafts.
- Diff — between trees, the index and the working tree, producing patches byte-identical to git's.
- Write and transport, later: ref transactions, writing objects,
signing, and git's protocol v2 over
file://,ssh://andhttps://.
Two 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
gitand 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
gitrather than receive an answer that is nearly right.
The words used for the things in a repository — object ID, object format,
object database, ref store, git directory — are defined in
GLOSSARY.md.
Using it
Add it to build.zig.zon:
zig fetch --save git+https://git.jcollie.dev/jeff/zig-git.git
and import the git module:
const git = b.dependency("zig_git", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("git", git.module("git"));
Reading the commit HEAD points at, in whatever repository the current
directory is in:
const git = @import("git");
// Find the repository as git would, from the current directory and the
// environment, check that its format is one this library can read, and open
// its objects (packed or loose, alternates included) and its refs (files or
// reftables).
var repo = try git.Repository.discover(io, gpa, .{ .cwd = ".", .env = env }, null);
defer repo.close(io, gpa);
const head = try repo.refStore().resolve(io, gpa, "HEAD", null) orelse
return error.NoCommitsYet;
defer gpa.free(head.name);
var diag: git.Diagnostics = .{};
var obj = repo.objectDatabase().read(io, gpa, head.id, &diag) catch |err| {
std.log.err("{t}: {f}", .{ err, diag });
return err;
};
defer obj.deinit(gpa);
const commit = try git.Commit.parse(repo.objectFormat(), obj.data);
std.debug.print("{s} is at {f}, by {s}\n", .{ head.name, head.id, commit.author.?.name });
The library takes its std.Io and its allocators from the caller and never
reaches for either itself. What it reads into memory belongs to the caller;
the parsed views of commits, trees and tags borrow from it.
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
- Forgejo: https://git.jcollie.dev/jeff/zig-git
- Tangled: https://tangled.org/jcollie.dev/zig-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
- The Git Project. gitglossary(7): A Git Glossary.
https://git-scm.com/docs/gitglossary — the vocabulary
GLOSSARY.mdfollows. - The Git Project. gitrepository-layout(5): Git Repository Layout. https://git-scm.com/docs/gitrepository-layout — where loose objects live and how they are named.
- The Git Project. hash-function-transition: Transitioning from SHA-1 to
SHA-256. https://git-scm.com/docs/hash-function-transition — the
SHA-256 object format and
extensions.objectFormat. - The Git Project. gitformat-pack(5): Git pack format. https://git-scm.com/docs/gitformat-pack — packs, both versions of the pack index, and the delta encoding.
- The Git Project. reftable: the reftable format. https://git-scm.com/docs/reftable — reftables: blocks, records, indexes and the stack.
- The Git Project. gitformat-index(5): Git index format. https://git-scm.com/docs/gitformat-index — the index file, its versions and its extensions.
- The Git Project. gitrevisions(7): Specifying revisions and ranges for Git. https://git-scm.com/docs/gitrevisions — the syntax of revisions and ranges.
- The Git Project. gitmailmap(5): Map author/committer names and/or E-Mail addresses. https://git-scm.com/docs/gitmailmap — the mailmap format.
- The Git Project. git-replace(1): Create, list, delete refs to replace objects. https://git-scm.com/docs/git-replace — replace refs, and grafts.
- Murchison, A., Kling, J., and Thompson, A. The Time Zone Information Format (TZif). RFC 8536, Internet Engineering Task Force, 2019. https://www.rfc-editor.org/info/rfc8536 — the time zone files local time is read from.
- The GNU C Library, v2.44. https://sourceware.org/glibc/ —
time/mktime.c(__mktime_internal),time/tzfile.c(__tzfile_read,__tzfile_compute),time/tzset.c(tzset_internal,__tzset_parse_tz,compute_change) andtime/offtime.c, whose local time git's dates are read in. - The Git Project. git-check-ref-format(1): Ensures that a reference name is well formed. https://git-scm.com/docs/git-check-ref-format — the rules for ref names.
- Chacon, Scott, and Ben Straub. "Git Objects", in Pro Git, 2nd ed., §10.2. Apress, 2014. https://git-scm.com/book/en/v2/Git-Internals-Git-Objects — the object header and the zlib framing of a loose object.
- The Git Project. git source code, v2.56.0.
https://github.com/git/git/tree/v2.56.0 —
ident.c(split_ident_line),commit.c(parse_commit_buffer),tag.c(parse_tag_buffer),tree-walk.c(decode_tree_entry) andobject-file.c(parse_loose_header),packfile.c(check_packed_git_idx,unpack_object_header_buffer,get_delta_base),patch-delta.c(patch_delta),chunk-format.c(read_table_of_contents),midx.c(load_multi_pack_index_one),odb.c(parse_alternates,odb_add_alternate_recursively),quote.c(unquote_c_style,sq_dequote_step),config.c(git_parse_source,git_config_include,do_git_config_sequence,git_config_from_parameters),parse.c(git_parse_signed,git_parse_maybe_bool),path.c(interpolate_path),setup.c(setup_git_directory_gentlyand the functions it calls),wildmatch.c,refs.c(check_refname_format,parse_worktree_ref,refs_resolve_ref_unsafe),refs/files-backend.c,refs/packed-backend.c,refs/reftable-backend.candreftable/(block.c,record.c,table.c,merged.c,stack.c),read-cache.c(do_read_index,create_from_disk,read_index_extension,check_ce_order),cache-tree.c(cache_tree_read),resolve-undo.c(resolve_undo_read),split-index.c(read_link_extension,merge_base_index),varint.c,ewah/(ewah_read_mmap,ewah_each_bit),object-name.c(get_short_oid,get_oid_with_context_1,get_oid_1,get_oid_basic,peel_onion,get_oid_oneline,repo_interpret_branch_name),refs.c(read_ref_at,repo_dwim_reflog),remote.c(branch_get_upstream,query_refspecs),refspec.c,path.c(normalize_path_copy_len),builtin/rev-parse.c,revision.c(setup_revisions,handle_revision_arg,prepare_revision_walk,get_revision,limit_list,process_parents,try_to_simplify_commit,rewrite_one,rev_compare_tree,simplify_merges,limit_to_ancestry),builtin/rev-list.c(finish_commit),tree-diff.c,tree-walk.c(tree_entry_interesting),pathspec.c,commit.c(sort_in_topological_order,parse_commit_date),commit-reach.c(paint_down_to_common,remove_redundant,reduce_heads),prio-queue.c,grep.c(compile_grep_patterns,match_expr_eval,grep_source),mailmap.c(read_mailmap,add_mapping,map_user),ident.c(apply_mailmap_to_header),replace-object.c(prepare_replace_object,do_lookup_replace_object),commit.c(read_graft_line,parse_commit_buffer),shallow.c(is_repository_shallow),log-tree.c(load_ref_decorations,add_ref_decoration),builtin/log.c(set_default_decoration_filter),ref-filter.c(reach_filter),date.c(approxidate_careful,parse_date_basic,approxidate_strand the functions they call),odb.c(odb_find_abbrev_len),packfile.c(find_abbrev_len_for_pack) andodb/source-loose.c, whose behavior the parsers reproduce, andt/t3070-wildmatch.sh, whose cases the matcher is tested with. - 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.