ExGix

Hex DocsGitHub

This project provides high-level git operations through rustler bindings to the gitoxide library, which is a pure Rust implementation of Git. The goal of this project is to provide a high-performance, native Git library for Elixir applications.

Installation

If available in Hex, the package can be installed by adding ex_gix to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_gix, "~> 0.1.0"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/ex_gix.

Usage Guide

ExGix provides a high-level API for interacting with Git repositories. Here are some examples of what you can do:

Opening and Initializing Repositories

# Open an existing repository
{:ok, repo} = ExGix.open(".")

# Or discover a repository from a subdirectory
{:ok, repo} = ExGix.discover("test/some/nested/dir")

# Initialize a new repository
{:ok, repo} = ExGix.init("/path/to/new/repo")

# Get repository properties
ExGix.is_bare(repo) #=> false
ExGix.path(repo) #=> "/path/to/repo/.git"
{:ok, head_name} = ExGix.head_name(repo) #=> {:ok, "refs/heads/main"}

Reading Files and Objects

You can read the contents of files from the repository without needing a working tree:

{:ok, repo} = ExGix.open(".")

# Read a file directly from HEAD
{:ok, content} = ExGix.cat_file(repo, "HEAD:README.md")

# Resolve references to object IDs
{:ok, commit_id} = ExGix.rev_parse(repo, "HEAD")
{:ok, blob_id} = ExGix.rev_parse(repo, "HEAD:README.md")

# Read using an object ID
{:ok, content} = ExGix.cat_file(repo, blob_id)

Working with Object IDs (ExGix.ObjectId):

# Parse from hex
{:ok, oid} = ExGix.ObjectId.from_hex("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")

# Convert to hex
hex_string = ExGix.ObjectId.to_hex(oid)

# Check object properties
ExGix.ObjectId.is_empty_tree(oid)
ExGix.ObjectId.is_empty_blob(oid)

Checking Repository Status

You can check the status of files in the working directory compared to the index and HEAD:

{:ok, repo} = ExGix.Repository.open(".")
{:ok, status_items} = ExGix.Repository.status(repo)

# Example output item:
# %ExGix.StatusItem{
#   location: :index_worktree, # or :tree_index
#   path: "lib/ex_gix.ex",
#   status: :modified          # :modified, :untracked, :added, etc.
# }

Listing Tree Contents

You can list the contents of a tree (directory) at a specific revision:

{:ok, repo} = ExGix.Repository.open(".")

# List root directory of HEAD
{:ok, items} = ExGix.Repository.ls_tree(repo, "HEAD")

# List a specific subdirectory
{:ok, items} = ExGix.Repository.ls_tree(repo, "HEAD:lib")

# List recursively
{:ok, items} = ExGix.Repository.ls_tree(repo, "HEAD", recursive: true)

# Example output item:
# %ExGix.TreeItem{
#   filename: "README.md",
#   kind: :blob,       # :blob, :tree, :exe, etc.
#   mode: "100644",
#   oid: <reference>   # Object reference
# }

Committing Changes

You can create new commits programmatically:

{:ok, repo} = ExGix.Repository.open(".")

# Define the author/committer signature
sig = %ExGix.Signature{
  name: "Committer Name",
  email: "committer@example.com",
  time_seconds: System.os_time(:second),
  time_offset: 0
}

# Commit changes (uses the current index/staged changes)
{:ok, commit_id} = ExGix.Repository.commit_as(
  repo,
  sig,                   # Author
  sig,                   # Committer
  "Test commit message"  # Message
)

Why ExGix?

The Elixir ecosystem has historically relied on wrappers around the C library libgit2 (such as egit) or pure Elixir implementations like xgit. While valuable, these approaches have limitations—C bindings can introduce memory safety risks, and pure Elixir implementations may struggle with the performance required for large repositories.

ExGix bridges this gap by leveraging gitoxide, a high-performance pure Rust implementation of Git.

Architecture & Design

Opening a Git repository in ExGix creates a RepoResource that contains a gix::ThreadSafeRepository. This ThreadSafeRepository is a lightweight, thread-safe wrapper around the underlying Git repository data structures. In every native function call, ExGix creates a lightweight, thread-local Repository instance from the shared ThreadSafeRepository using .to_thread_local(), ensuring that operations run concurrently without bottlenecking the Erlang schedulers.

Local Development

If you want to contribute to ex_gix or build the Rust NIF locally instead of using precompiled binaries, you need to set the EX_GIX_BUILD environment variable to 1:

export EX_GIX_BUILD=1
mix deps.get
mix compile
mix test