WARNING: This code has been vibed. you probably don't want to depend upon it, but I'm a dork like that
A comprehensive Deno FFI binding for libgit2, providing a powerful and flexible way to interact with Git repositories directly from Deno.
This package offers a modern, type-safe, and object-oriented API for Git operations, built on top of the performance and reliability of libgit2.
- Comprehensive Git Operations: Full support for repositories, branches, commits, trees, blobs, diffs, merges, rebases, and more.
- High-Level API: Intuitive, class-based API (
Repository,Index,Commit,Diff,Blame, etc.) that simplifies complex libgit2 functions. - Type-Safe: Full TypeScript support with detailed type definitions for all libgit2 structures and enums.
- Cross-Platform: Automatic library resolution for Linux, macOS, Windows, and other platforms via @denosaurs/plug.
- Resource Management: Classes implement
Symbol.disposefor use withusingstatements for automatic cleanup. - No External Dependencies: Relies only on Deno runtime and a system installation of libgit2.
- Deno: Version 2.6 or later
- libgit2: A shared library installation of libgit2 (v1.1.0 or later recommended)
- Ubuntu/Debian:
sudo apt-get install libgit2-dev - macOS (Homebrew):
brew install libgit2 - Windows (vcpkg):
vcpkg install libgit2 - Fedora:
sudo dnf install libgit2-devel
Import the package from JSR:
import * as git from "jsr:@nullstyle/libgit2";Or add it to your deno.json:
{
"imports": {
"@nullstyle/libgit2": "jsr:@nullstyle/libgit2"
}
}import { initGit, Repository } from "jsr:@nullstyle/libgit2";
// Initialize libgit2 with automatic cleanup via `using`
using git = await initGit();
using repo = Repository.open(".");
console.log(`Repository: ${repo.path}`);
console.log(`HEAD: ${repo.headOid()}`);
// Library is automatically shut down when `git` goes out of scopeBoth the library and classes that wrap native resources implement
Symbol.dispose and can be used with using for automatic cleanup:
import { Index, initGit, Repository } from "jsr:@nullstyle/libgit2";
using git = await initGit();
using repo = Repository.open(".");
using index = Index.fromRepository(repo);
// Resources are automatically freed when they go out of scope
console.log(`Index entries: ${index.entryCount}`);import { initGit, Repository } from "jsr:@nullstyle/libgit2";
using git = await initGit();
using repo = Repository.open(".");
console.log("Branches:");
for (const branch of repo.listBranches()) {
const marker = branch.isHead ? "* " : " ";
console.log(`${marker}${branch.name}`);
}
console.log("\nRecent Commits:");
for (const commit of repo.walkCommits(undefined, 5)) {
const shortOid = commit.oid.slice(0, 7);
const summary = commit.message.split("\n")[0];
console.log(`${shortOid} ${summary}`);
}import {
createCommit,
Index,
initGit,
Repository,
} from "jsr:@nullstyle/libgit2";
using git = await initGit();
using repo = Repository.open("/path/to/repo");
using index = Index.fromRepository(repo);
// Stage files
index.add("README.md");
index.add("src/main.ts");
index.write();
// Create the commit
const oid = createCommit(repo, {
message: "Add initial files",
author: { name: "Your Name", email: "you@example.com" },
});
console.log(`Created commit: ${oid}`);Track the history of files that have been deleted from the repository:
import {
fileExistsAtHead,
findFileDeletion,
findFileHistory,
initGit,
Repository,
} from "jsr:@nullstyle/libgit2";
using git = await initGit();
using repo = Repository.open("/path/to/repo");
const filePath = "path/to/deleted-file.md";
// Check if file exists at HEAD
if (!fileExistsAtHead(repo, filePath)) {
// Find when and where the file was deleted
const deletion = findFileDeletion(repo, filePath, { includeContent: true });
if (deletion) {
console.log(`Deleted in: ${deletion.deletedInCommit.commitOid}`);
console.log(`Last existed in: ${deletion.lastExistedInCommit.commitOid}`);
console.log(`Content at deletion:\n${deletion.lastContent}`);
}
}
// Get full history of the file
const history = findFileHistory(repo, filePath);
console.log(`File appeared in ${history.commits.length} commits`);import { initGit, Repository } from "jsr:@nullstyle/libgit2";
using git = await initGit();
using repo = Repository.open(".");
// Diff between two commits
const headOid = repo.headOid();
using diff = repo.diffTreeToWorkdir(headOid);
console.log(`Changed files: ${diff.numDeltas}`);
for (const delta of diff.deltas()) {
console.log(`${delta.status}: ${delta.newFile.path}`);
}import { GitMergeAnalysis, initGit, Repository } from "jsr:@nullstyle/libgit2";
using git = await initGit();
using repo = Repository.open(".");
// Analyze merge possibility
const annotated = repo.annotatedCommitFromRevspec("feature-branch");
const analysis = repo.mergeAnalysis(annotated);
if (analysis.analysis & GitMergeAnalysis.FASTFORWARD) {
console.log("Fast-forward merge possible");
} else if (analysis.analysis & GitMergeAnalysis.NORMAL) {
console.log("Normal merge required");
}
// Perform merge
repo.merge(annotated);
// Check for conflicts
const conflicts = repo.getConflicts();
if (conflicts.length > 0) {
console.log("Conflicts detected:");
for (const conflict of conflicts) {
console.log(` ${conflict.ancestorPath}`);
}
}import { initGit, Repository } from "jsr:@nullstyle/libgit2";
using git = await initGit();
using repo = Repository.open(".");
// Save current changes to stash
const stashOid = repo.stashSave({
message: "Work in progress",
});
console.log(`Stashed: ${stashOid}`);
// List stashes
const stashes = repo.listStashes();
for (const stash of stashes) {
console.log(`${stash.index}: ${stash.message}`);
}
// Apply and drop the stash
repo.stashPop();| Function | Description |
|---|---|
initGit() |
Initialize libgit2, returns a GitLibrary handle for use with using |
version() |
Get libgit2 version as {major, minor, revision} |
versionString() |
Get libgit2 version as a string |
The GitLibrary object returned by initGit() implements Symbol.dispose, so
it can be used with the using statement for automatic cleanup:
using git = await initGit();
// Library is automatically shut down when `git` goes out of scopeYou can also access version info directly from the GitLibrary object:
git.version- Version as{major, minor, revision}git.versionString- Version as a stringgit.shutdown()- Manually shutdown (also called automatically on dispose)
Core repository operations:
| Method | Description |
|---|---|
Repository.open(path) |
Open an existing repository |
Repository.init(path) |
Create a new repository |
Repository.discover() |
Find a repository by walking up from a path |
close() |
Close the repository and free memory |
head() |
Get the current HEAD reference |
headOid() |
Get the OID of the commit HEAD points to |
setHead(ref) |
Set HEAD to a reference |
setHeadDetached(oid) |
Detach HEAD at a specific commit |
state |
Get repository state (normal, merging, etc.) |
stateCleanup() |
Clean up repository state after merge/revert |
Branch operations:
| Method | Description |
|---|---|
listBranches(type?) |
List local and/or remote branches |
createBranch(name, target) |
Create a new branch |
deleteBranch(name) |
Delete a branch |
Commit operations:
| Method | Description |
|---|---|
lookupCommit(oid) |
Find a commit by its OID |
walkCommits(start?, max?) |
Generator to walk commit history |
getCommits(start?, max?) |
Get commits as an array |
Status and references:
| Method | Description |
|---|---|
status() |
Get working directory status |
listReferences() |
List all references |
lookupReference() |
Look up a reference by name |
resolveReference() |
Resolve a symbolic reference |
| Method | Description |
|---|---|
Index.fromRepository(repo) |
Get the index for a repository |
add(path) |
Stage a file |
addAll(paths) |
Stage multiple files |
remove(path) |
Unstage a file |
write() |
Write index changes to disk |
writeTree() |
Create a tree object from the index |
entries() |
Get all entries in the index |
hasConflicts |
Check if index has conflicts |
- Repository: init, open, discover, clone, state management
- Commits: create, lookup, amend, walk history, parents
- Branches: create, delete, list, rename, upstream tracking
- Index: add, remove, write, read, conflict detection
- References: lookup, resolve, create, delete, list
- Trees & Blobs: lookup, traverse, read content
- Status: working directory and index status
- Merge: merge analysis, merge commits, merge base, conflict detection
- Rebase: init, open, next, commit, abort, finish
- Cherry-pick: cherry-pick commits with options
- Revert: revert commits with options
- Diff: tree-to-tree, tree-to-workdir, index-to-workdir
- Patch: create patches from diffs, line statistics
- Apply: apply diffs to index or tree
- Blame: file blame with options
- Stash: save, apply, pop, drop, list
- Tags: create (annotated and lightweight), delete, list, lookup
- Remotes: create, lookup, list, rename, delete, set URLs
- Worktrees: add, list, lookup, lock, unlock, prune
- Submodules: list, lookup, status
- Config: read and write configuration values
- Reflog: read, delete, rename
- Notes: create, read, remove, list
- Describe: describe commits and workdir
- Graph: ahead/behind, descendant checking
- Ignore: add rules, check paths
- Pathspec: pattern matching
- Mailmap: author/committer resolution
- Message: prettify, parse trailers
- ODB: object database operations
# Basic usage example (creates a temp repository)
deno task run-basic
# Inspect an existing repository
deno task run-inspect
# Or run directly:
deno run --allow-ffi --allow-read --allow-write examples/basic_usage.ts
deno run --allow-ffi --allow-read examples/inspect_repo.ts /path/to/repo
deno run --allow-ffi --allow-read examples/deleted_file_history.ts /path/to/repo path/to/file--allow-ffi: Required for native library calls--allow-read: Required for reading repository files--allow-write: Required for write operations (commits, index, etc.)--allow-env: Required for environment variable access (optional)
Contributions are welcome! Please feel free to open an issue or submit a pull request.
This project is licensed under the MIT License. See the LICENSE file for details.