Git.cr
git
A native Crystal Git library for SHA-1 and SHA-256 repositories. It reads and writes repositories without a Git subprocess, a shell or libgit2, and has no dependency on a host application or permission model. It requires Crystal >= 1.13.0 and the standard library's Compress::Zlib (which calls native zlib).
Add the dependency to your shard.yml:
dependencies:
git:
github: immateria/Git.cr
Then install it:
shards install
require "git"
repository = Git::Repository.new("/path/to/repository")
repository.head.symref_target # => "refs/heads/main"
repository.rev_parse("main~2:src/app.cr") # => blob ID
repository.log(["origin/main..HEAD"], limit: 20).commits.each { |commit| puts commit.subject }
me = Git::Signature.new("Me", "me@example.com", Time.utc.to_unix, 0)
repository.switch("feature", create: true, from: "main", actor: me) # git switch -c
repository.stage # git add -A
print repository.diff(cached: true) # git diff --cached
repository.commit(author: me, committer: me, message: "Fix the app")
repository.amend(committer: me, message: "Fix the app properly")
repository.stash.push(actor: me) # nil when there is nothing to stash
Repository keeps Git's top-level commands as verbs (commit, switch, reset, restore, merge, rebase, cherry_pick, fetch, push, ...). Families of subcommands hang off handles named for what they manage: refs, branches, tags, remotes, stash and recovery. Without arguments, merge, rebase, cherry_pick, revert and sequencer give the operation in progress (repository.merge.abort, repository.rebase.continue(committer: me)).
Remote work goes through an explicit transport:
tls = OpenSSL::SSL::Context::Client.new
credentials = ->(url : String, attempt : Int32) do
attempt == 1 ? Git::Transport::Credential.new("me", ENV["TOKEN"]) : nil
end
transport = Git::Transport::HTTP.new("https://example.com/project.git",
credentials: credentials, tls: tls)
repository = Git::Repository.clone(transport, "/tmp/project",
url: "https://example.com/project.git")
repository.fetch("origin", transport: transport)
repository.push("origin", ["main"], transport: transport)
repository.push("origin", ["main"], transport: transport,
lease: Git::Push::Lease.tracking) # --force-with-lease
repository.fetch(transport, ["+refs/heads/*:refs/remotes/mirror/*"]) # no configured remote
Local paths (Git::Transport::Local, served in-process) and plain HTTP(S) URLs without credentials can be given as strings. ssh, git:// and other programs need Git::Transport::SSH, Git::Transport::Daemon or Git::Transport::Command, constructed with the program and environment the caller chooses.
The trusted caller handles permission checks, operation limits, authorization, secret masking, cancellation and any operating-system confinement. This library enforces bounded parsing and reports unsupported Git formats rather than approximating them. An optional adapter outside this shard uses it for read-only agent tools and controls every grant; see docs/CAPABILITY_BOUNDARY.md for the boundary.
What it does
-
Repository selection.
Repository.newfollows valid.gitpointer files (linked worktrees, separate Git directories) and opens bare repositories;follow_gitfile: falserequires an explicitly host-selectedlinked_worktree_admin_dir:.discover: truesearches ancestors, bounded bydiscovery_ceiling:. It does not read the process environment:Repository.from_environment(cwd, variables)takesGIT_DIR,GIT_COMMON_DIRandGIT_WORK_TREEfrom a hash the caller supplies. Ownership and bare-repository admission checks are available (open_with_ownership_check,open_with_bare_repository_check). -
Reading. Loose and packed objects (
read_object,object_header,read_commit, typed byGit::ObjectType), refs and packed refs, the HEAD tree (head_entry,head_directory,read_head_file), index versions 2 to 4, bounded status (staged_changes,working_changes) and per-file comparisons (staged_file,working_file), and effective configuration with provenance. Paths are byte-exact (Git::PathBytes), including invalid UTF-8 on Linux; callers must not normalize them for lookup or permission decisions.Git::RefName.valid?follows the byte-levelgit check-ref-formatgrammar.Git::Ignoreevaluates repository.gitignore, nested.gitignoreandinfo/exclude, parent before child, so an ignored parent cannot be re-included by negating a child. -
Writing objects.
initmakes a repository and probes its file system as Git's init does, writingfilemode,symlinks,ignorecaseand, on macOS,precomposeunicodeas Git would.write_blob,write_tree,write_commitandwrite_tagpublish loose objects, andbranches.createandtags.createcreate refs.Config::FileEditormakes explicit file-scoped configuration edits with an exclusive.lockand atomic replace. -
Index and refs.
stageisgit add -A(or-uwithtracked_only:) over the whole tree, files or directories, in one index write;removeisgit rmwith Git's safety checks (cached:,force:,recursive:).stage_blob,stage_file,remove_from_index,write_index_tree,commitandamend(git commit --amend) stage and commit with index versions 2 to 4.refs.updateandrefs.deleteare single-ref compare-and-swap updates andrefs.applytakes a mixed create/update/delete batch, with packed-refs rewriting, optional reflog appends and explicit partial-publication diagnostics.refs.list,refs.resolve?,refs.set_symbolic,refs.detach_headandrefs.packcover the rest offor-each-ref,symbolic-ref,update-refandpack-refs. -
Recovery.
recovery.apply(refs, or index edits and refs together) andrecovery.commitwrite a bounded redo journal and a checksummed finalization receipt;recovery.resume,recovery.pending,recovery.pending_finalizationsandrecovery.finish_finalizationfinish or clean up an interrupted run. Durable transactions share a per-common-directory advisory lock and refuse to start while a journal is pending. This is a recoverable intent record, not global crash atomicity or rollback. -
Workflows.
switch(to a branch;create:andfrom:as-c;detach:; and Git's guess of a remote branch, with an upstream set asbranch.autoSetupMergesays),restore,restore_staged,read_tree,reset(soft, mixed or hard),fast_forward,merge(thenmerge.continue,merge.abortandmerge.conflicts), the single-commit sequencer (cherry_pickandrevert, thencherry_pick.continueand so on), linearrebase(thenrebase.continue,rebase.skipandrebase.abort) andstash.push,stash.list,stash.show,stash.apply,stash.popandstash.drop.stash.pushtakesinclude_untracked,include_ignored,keep_index,staged_onlyand literal repository-relativepaths:; there are no globs or pathspec magic. Merge and replay conflicts keep Git-compatible index stages and worktree leaves, and rebase keeps commits that were empty in their source history unless the caller says otherwise. Repeated-line alignment and Git's fullortconflict layout are intentionally bounded. Given anactor:(or, formergeandpull, the committer),switch,reset,fast_forward,merge,pullandbranches.createwrite Git's reflog lines, andreset,merge,fast_forward,pullandrebaserecordORIG_HEAD. Commands that take a commit accept therev_parsegrammar. -
History.
rev_parseresolves Git's everyday revision grammar: full and abbreviated IDs, ref lookup rules,@,@{n},@{upstream},~,^,^{type},<rev>:<path>and:<path>.rev_listandlogtake^A,A..BandA...B, andlogorders by date or topologically (History::Order);ahead_behind,merge_basesandancestor?answer ancestry questions;reachable_objectsisrev-list --objects.diff_treesandcommit_changeslist added, deleted, modified, type-changed and (exactly) renamed paths between trees asTreeDiff::Changevalues, asgit diff-tree -rdoes. -
Diffs.
diffmirrorsgit diff's forms (index against worktree,cached:for HEAD or a revision against the index, a revision against the worktree, two trees) andcommit_patchis a commit against its first parent. Both return aGit::Patch, per-file sections whose text is byte for bytegit diff --no-color: xdiff's Myers diff with its heuristics and the indent heuristic, function-name hunk headers, exact renames, mode and type changes, binary files, symlinks, gitlinks and C-style quoted paths. -
Branches and tags.
branches.list(Git::Branch, with upstream and ahead/behind),branches.create,branches.delete,branches.rename,branches.upstreamandbranches.set_upstream;tags.list,tags.createandtags.delete. -
Packs. Pack reads cache verified indexes and follow delta chains of any Git depth.
Pack::Writer,Pack::DeltaandPack::Builder(pack-objects, with delta search and reuse of stored deltas) write packs;Pack::IndexerandRepository#index_packare index-pack, including thin-pack completion, and write.idxand.revfiles (Pack::IndexFile,Pack::ReverseIndexFile) byte-identical to Git's. -
Remotes.
remotes.add,remotes.remove,remotes.rename,remotes.set_urlandRemote::Refspec;clone,fetch(from a remote name or aTransport; tag following, prune, shallowdepth:, FETCH_HEAD),pull(fast-forward or merge),push(fast-forward rules, force,lease:for--force-with-lease, atomic, deletes) andls_remote, reportingFetch::Result,Pull::ResultandPush::Result. Fetch speaks protocol v2 (Protocol::UploadPack::Client); push speaks receive-pack v0/v1 (Protocol::ReceivePack::Client), and both services have native servers. EveryGit::Transportis one ofLocal(in-process),HTTP(smart HTTP with a credential callback, TLS context and CONNECT proxy),SSH,Daemon(git://) andCommand.require "git/server/http_handler"adds anHTTP::Handlerthat serves repositories to Git clients over smart HTTP. -
Maintenance.
repack(one pack of everything reachable, honouring.keep),prune_packed,refs.pack,gcandfsck(corrupt, missing and unreachable objects), reportingRepack::Result,Gc::ResultandFsck::Report.
Worktree, index and HEAD are published separately. Checkout::PartialPublication and related types report possibly incomplete results; they do not promise rollback. Pinned directory descriptors (src/git/descriptor_path.cr, with renameat2(RENAME_NOREPLACE) on Linux and renameatx_np(RENAME_EXCL) on macOS) handle index publication, single-ref updates, mixed refs, redo journals and receipts. They do not provide atomic conditional replacement of existing tracked files. The library runs on Linux and macOS; Windows is unverified.
Checks
crystal spec runs the specs. The specs that compare against upstream Git need Git 2.47.3, selected by GIT_ORACLE_BINARY (default: git on PATH); the oracle refuses any other version. Runtime Git operations never launch that binary. The remote specs also run git http-backend, git daemon, git upload-pack and git receive-pack from that installation on loopback addresses. docs/ORACLE_FIXTURES.md lists the fixtures and their profiles. In an umbrella workspace that vendors this shard, scripts/test-all git-differential runs a bounded routine oracle suite.
bench/realworld.cr checks the library against Git on a clone of any real repository and times both; docs/REAL_WORLD.md has the results for three public repositories and what they found.
Current limits
- Not implemented: worktree and submodule lifecycle; hooks, filters and signing; credential helpers; protocol v0/v1 fetch (servers must speak protocol v2 for fetching, as Git servers have by default since 2.26), partial clones,
rebasepulls, similarity-based rename detection and copies, globs and pathspec magic; and full attribute and ignore parity (global excludes, case folding and the whole of Git's wildmatch: Git's?and[...]match single bytes, the library's whole characters). Reftable ref storage, split and sparse indexes, replace refs, grafts and alternate object directories are unsupported. - Clone's checkout writes the tree into an empty worktree; it does not apply filters or attributes. Indexes the library writes (clone, checkout,
stage) carry no stat data, so the nextstatus, Git's or the library's, hashes every file; status trusts the stat data in indexes Git wrote. - Merges use a bounded text algorithm and handle UTF-8 text only. Diffs take any bytes, but apply no attributes or textconv; hunk headers use Git's default function-name rule, not the language drivers
.gitattributescan select (*.c diff=cpp), and IDs are abbreviated to at least 7 digits rather than scaling with the object count (abbrev:). - A detached HEAD can be reached with
switch(detach: true)and left withswitch, butcommit,amend,resetandmergeneed a checked-out branch. 0band0Bconfiguration integer prefixes follow a fixed binary-integer policy independent of the host libc.- Working-file status and diffs use a bounded regular-file reader and an ancestor symlink check. They reject observed redirection and concurrent change but are not kernel-atomic against hostile replacement.
Verification history
The recorded oracle runs used Git 2.47.3, Crystal 1.21.0 and Linux x86-64 in a ChatGPT Linux sandbox. On 2026-11-02 all 640 examples passed with Crystal 1.22.2 and Git 2.47.3 built from source, as a normal user, on macOS 15.4.1 arm64 and Linux 7.0 arm64. On 2026-11-03 all 640 examples passed with Crystal 1.13.0 and the same oracle Git on macOS 15.4.1 arm64. That sets the crystal: floor: 1.12.0 and earlier put system_fsync on the File layer, where a bare IO::FileDescriptor has no fsync.
On 2026-11-03 all 681 examples passed with Crystal 1.13.3, 1.21.1 and 1.22.2 and Git 2.47.3 built from source on Linux x86-64 (Ubuntu 24.04), including the pack, remote, transport, porcelain and maintenance specs. Crystal 1.13 resolves the oracle's git through the restricted PATH it is given, so set GIT_ORACLE_BINARY to the 2.47.3 binary's full path there. A native clone of github.com/crystal-lang/crystal (depth 1, 2,749 files) through an HTTPS CONNECT proxy passed git fsck and left a clean git status.
Apple's file systems refuse names that are not UTF-8. On those systems the specs put such names into the index and packed-refs without a file, as Git would write them, and skip the work-tree and loose-ref checks that need one (GitOracle::RAW_NAMES). The init probes were compared with Git's on FAT, exFAT and case-sensitive APFS on macOS and on vfat on Linux, by hand, not in a spec. Older and newer Git versions and Windows are unverified.
Further reading
Design notes: docs/CAPABILITY_BOUNDARY.md (what belongs in the library and what in an adapter) and docs/SEMANTIC_NAMESPACES.md (why the types are named as they are). The capability matrix and per-gap status are docs/CORE_FUNCTION_COVERAGE.md and docs/PARITY_LEDGER.md.
Git.cr
- 0
- 0
- 0
- 0
- 0
- 9 minutes ago
- November 3, 2026
MIT License
Wed, 04 Nov 2026 01:57:47 GMT