libgit2.cr
libgit2.cr
Raw Crystal bindings to the libgit2 C API.
LibGit2 is a single lib that mirrors git2.h one-to-one: structs, constants and fun declarations keep their exact C names, so any libgit2 documentation or example translates directly. There are no wrappers, no automatic memory management and no error handling — this is deliberately the thinnest maintainable layer, intended as a base for higher-level code (editors, tooling, git UIs).
Checked against libgit2 1.9 (standard SHA-1 build). Every bound symbol is verified against the exported symbols of the actual shared library, and the specs exercise all of it against real repositories.
Installation
-
Add the dependency to your
shard.yml:dependencies: libgit2: github: OrelSokolov/libgit2.cr -
Install libgit2 itself, e.g.
sudo apt install libgit2-dev,brew install libgit2, etc. -
require "libgit2"
No dev package installed?
If only the runtime library is present (e.g. libgit2.so.1.9 without the -dev package), link against a local symlink instead:
mkdir -p ext
ln -s /lib/x86_64-linux-gnu/libgit2.so.1.9 ext/libgit2.so
LIBRARY_PATH=$PWD/ext crystal spec
Usage
require "libgit2"
LibGit2.git_libgit2_init
begin
repo = uninitialized LibGit2::Repository*
LibGit2.git_repository_open(pointerof(repo), ".")
# Current branch
head = uninitialized LibGit2::Reference*
LibGit2.git_repository_head(pointerof(head), repo)
puts String.new(LibGit2.git_reference_shorthand(head)) # => "main"
LibGit2.git_reference_free(head)
# Blame: who wrote this line?
blame = uninitialized LibGit2::Blame*
opts = uninitialized LibGit2::BlameOptions
LibGit2.git_blame_options_init(pointerof(opts), LibGit2::BLAME_OPTIONS_VERSION)
LibGit2.git_blame_file(pointerof(blame), repo, "src/app.cr", pointerof(opts))
hunk = LibGit2.git_blame_hunk_byline(blame, 42).not_nil!.value
sig = hunk.final_signature.value
puts "#{String.new(sig.name)} — #{String.new(hunk.summary)}"
LibGit2.git_blame_free(blame)
ensure
LibGit2.git_repository_free(repo)
LibGit2.git_libgit2_shutdown
end
Line-level diff — the basis for highlighting modified lines in an editor:
patch = uninitialized LibGit2::Patch*
LibGit2.git_patch_from_buffers(
pointerof(patch),
old_text, old_text.bytesize, "file.cr",
new_text, new_text.bytesize, "file.cr", nil)
added = [] of Int32
0.times do |hunk_idx|
break if hunk_idx >= LibGit2.git_patch_num_hunks(patch)
LibGit2.git_patch_num_lines_in_hunk(patch, hunk_idx).times do |i|
line = uninitialized LibGit2::DiffLine*
LibGit2.git_patch_get_line_in_hunk(pointerof(line), patch, hunk_idx, i)
l = line.value
added << l.new_lineno if l.origin == LibGit2::GIT_DIFF_LINE_ADDITION.to_i8!
end
end
LibGit2.git_patch_free(patch)
API coverage
Bound so far, grouped by the git2/*.h header each section follows:
| Header | Bound |
|---|---|
global.h, common.h, errors.h |
init/shutdown, version, git_error_last, all error codes |
oid.h |
hex↔raw conversion, compare, is_zero |
strarray.h, buffer.h |
git_strarray, git_buf + dispose |
signature.h |
git_signature struct, dup, free |
config.h |
open/snapshot/free, get_string/int32/bool |
index.h |
entrycount, get_byindex, get_bypath, git_index_entry |
repository.h |
open/open_ext, head, workdir/path/state, config, index |
object.h, revparse.h |
lookup, id, type, owner, peel, git_revparse_single |
revwalk.h |
push/hide/next/sorting — full history walk |
commit.h |
lookup, author/committer, message, time, tree, parents |
refs.h, branch.h |
lookup, name/shorthand/target, resolve, iterators, create/rename/delete; branch lookup/name/upstream/is_head |
tag.h |
annotated tags: name, message, tagger, target, list |
tree.h, blob.h |
tree walk by name/index, blob raw and filtered content |
remote.h |
lookup, list, name/url (read-only, no network ops) |
blame.h |
file blame, hunks by line/index, buffer re-blame |
diff.h |
all diff generators, deltas, stats, find_similar (renames) |
patch.h |
from diff/blobs/buffers, hunks, lines, line_stats, to_buf |
status.h |
status list, single-file status, all flags |
Not bound (yet): network operations (fetch/push/clone), worktree mutations (checkout/merge/rebase/stash), ODB internals, mailmap, submodules, packfiles. Adding a missing function is a one-line fun copied from the header — contributions welcome.
libgit2 gotchas (found the hard way, covered by specs)
git_*_options_initfills onlyversion; every other field is zero. Opt into what you need, e.g.opts.flags = LibGit2::GIT_STATUS_OPT_DEFAULTSto see untracked files.- In
git_diff_tree_to_workdir_with_index,old_tree = NULLmeans the empty tree, not HEAD. Resolve HEAD explicitly:HEAD→git_commit_tree_id→git_tree_lookup. git_config_get_stringrejects a live config: callgit_config_snapshotfirst.git_oid_tostr_sreturns a pointer into a static thread-local buffer — wrap it inString.newimmediately.git_bufmust be zero-initialized (LibGit2::Buf.new) before use.- Strings and structs returned by blame/commit/tag belong to their parent object and die with it.
Development
make test # LIBRARY_PATH=$PWD/ext crystal spec
The specs build a throwaway git repository with the git CLI (no fixtures committed) and run every bound feature against it: blame, status, diff, patches and line parsing, rename detection, revwalk, tags, remotes, config, index, references.
License
MIT — see LICENSE. libgit2 itself is GPLv2 with a linking exception; using it from a closed-source Crystal application is permitted by that exception.
libgit2.cr
- 0
- 0
- 0
- 0
- 0
- about 2 hours ago
- October 14, 2026
MIT License
Wed, 14 Oct 2026 19:12:18 GMT