Skip to content
shenxianpengPublic

About

Fast, safe, pure-Rust Git for Python — bindings to the gitoxide (gix) engine and a modern alternative to GitPython.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

gitoxide-python

PyPI Version CI

Fast, safe, pure-Rust Git for Python — bindings to the gitoxide engine, and a modern alternative to GitPython.

gitoxide (the gix crate) is a next-generation, pure-Rust implementation of Git. This project exposes that engine to Python through PyO3 and ships as pre-built wheels via maturin, so you get:

GitPython pygit2 gitoxide-python
Backend shells out to the git CLI C libgit2 pure-Rust gix
Walk 10,000 commits 323 ms (a git subprocess, then parsing) 389 ms 129 ms
Install needs git on PATH needs a C toolchain / system libgit2 pip install, self-contained wheels
Memory safety n/a (subprocess) C Rust

Nothing here shells out to the git CLI. A repository that configures a clean/smudge filter (Git LFS, say) will still have that filter program executed when blame reads such a file — exactly as git itself would.

Status: alpha. The binding surface is small but real. It currently covers read-oriented workflows (open/discover, HEAD, history walk, refs, branches, tags, blob reads) — the operations DevOps tooling reaches for most. See Scope for what is and isn't wrapped yet.

Installation

pip install gitoxide

Pre-built wheels are published for Linux (manylinux, x86_64 + aarch64), macOS (x86_64 + Apple Silicon), and Windows — no Rust toolchain or system libgit2 required.

Quick start

import gitoxide

repo = gitoxide.open(".")            # or gitoxide.discover(".")

print(repo.git_dir)                  # .../my-project/.git
print(repo.workdir)                  # .../my-project  (None if bare)
print(repo.is_bare)                  # False
print(repo.head_name)                # 'main'  (None if detached)

# The commit at HEAD
head = repo.head_commit()
print(head.short_id, head.summary)
print(head.author.name, head.author.email, head.author.time)

# Walk history (reverse-chronological), like `git log`
for commit in repo.commits(max_count=10):
    print(commit.short_id, commit.author.name, commit.summary)

# Resolve a revspec to an object id
print(repo.rev_parse("HEAD~2"))
print(repo.rev_parse("main"))

# Branches, tags, and all references
print(repo.branches())               # ['main', 'dev', ...]
print(repo.tags())                   # ['v1.0.0', ...]
for ref in repo.references():
    print(ref.name, "->", ref.target)

# Read a file's content at a revision
readme = repo.read_blob("HEAD:README.md")
print(readme.decode())

# Blame a file: which commit last touched each line
for hunk in repo.blame("README.md"):
    print(f"{hunk.start_line}-{hunk.end_line}\t{hunk.short_id}")

Performance

Median time per operation on a full clone of python/cpython — 130,654 commits, 531 references — on an Apple M2:

Seven operations benchmarked across GitPython, pygit2 and gitoxide-python on the cpython repository. gitoxide-python is fastest at walking 10,000 commits (129 ms vs 323 ms and 389 ms), listing 531 references (3.45 ms vs 74.4 ms and 1.31 ms), reading a blob (48.8 µs vs 179 µs and 188 µs), resolving HEAD~100 (602 µs vs 3.02 ms and 28.2 µs) and reading the HEAD commit (34.2 µs vs 87.3 µs and 22.6 µs). Opening a repository is a three-way tie at roughly 0.1 ms, and blaming a 475-line file takes 1.85 s against GitPython's 1.07 s and pygit2's 28.4 s.
Operation GitPython pygit2 gitoxide-python
Walk 10,000 commits 323 ms 389 ms 129 ms
List 531 references 74.4 ms 1.31 ms 3.45 ms
Read a 8.7 KB blob 179 µs 188 µs 48.8 µs
Resolve HEAD~100 3.02 ms 28.2 µs 602 µs
Read the HEAD commit 87.3 µs 22.6 µs 34.2 µs
Open the repository 129 µs 111 µs 146 µs
Blame a 475-line file 1.07 s 28.4 s 1.85 s

Faster than GitPython on five of these seven, a tie on open, and a loss on blame — where GitPython is really git's own C implementation in a subprocess, and gix's younger blame has not caught it yet. Against pygit2 the split runs the other way: history walking and blame are several times faster here, while pygit2 still wins the single-object lookups it has spent fifteen years tuning.

The three libraries are checked against each other on every operation — same commit ids, same blob bytes, same line count — so a fast wrong answer can't win. benchmarks/ has the exact call made against each library, the caveats, and a one-command way to reproduce it on your own machine.

Migrating from GitPython

Common operations, side by side:

GitPython gitoxide-python
from git import Repo import gitoxide
repo = Repo(path) repo = gitoxide.open(path)
Repo(path, search_parent_directories=True) gitoxide.discover(path)
repo.head.commit repo.head_commit()
repo.active_branch.name repo.head_name
repo.iter_commits("main", max_count=10) repo.commits("main", max_count=10)
repo.commit("HEAD~2") repo.commit("HEAD~2")
repo.rev_parse("main").hexsha repo.rev_parse("main")
[b.name for b in repo.branches] repo.branches()
[t.name for t in repo.tags] repo.tags()
c.hexsha, c.summary, c.author.name c.id, c.summary, c.author.name

The key difference is what happens underneath: GitPython's iter_commits spawns a git rev-list subprocess; gitoxide-python walks the object database in-process in Rust.

API

Module functions

  • gitoxide.open(path) -> Repository
  • gitoxide.discover(path) -> Repository — search path and its parents
  • gitoxide.init(path, bare=False) -> Repository
  • gitoxide.gix_version() -> str

Repository

Properties: git_dir, workdir, is_bare, is_shallow, head_id, head_name, head_is_detached.

Note: head_name and head_is_detached access the HEAD reference and may raise GitoxideError if it is inaccessible (e.g., corrupted repository). Other properties never raise.

Methods: head_commit(), rev_parse(spec), commit(rev), commits(rev=None, max_count=None), references(), branches(), tags(), read_blob(rev), blame(path, rev=None).

Commit

id, short_id, tree_id, message, summary, author, committer, parents.

Signature

name, email, time (Unix seconds), offset (UTC offset seconds).

Reference

name, shorthand, target.

BlameHunk

start_line, end_line, line_count, orig_start_line, commit_id, short_id. Line numbers are 1-based and inclusive, matching git blame. Consecutive lines from the same commit are grouped into one hunk.

All errors (from any function or method) are raised as gitoxide.GitoxideError.

Scope

This is a binding, not a reimplementation: it exposes a slice of what the gix engine already does. Today that slice is:

  • Wrapped: open / discover / init, HEAD, history walk, rev-parse, references, branches, tags, blob reads, and blame.
  • Not wrapped yet: diff & status, tree listing, writing commits and the index, and clone / remote operations.

The engine supports much more; these are simply the parts this binding hasn't surfaced yet. Issues and pull requests that expose more of gix are welcome — see Contributing.

Contributing

The binding layer lives in a single src/lib.rs and maps closely onto the gix API, so adding a method is usually a small, self-contained change.

Building from source requires a Rust toolchain (this is also the path used when installing the sdist on a platform without a pre-built wheel):

python -m venv .venv && source .venv/bin/activate
pip install maturin pytest

maturin develop          # build the extension into the venv
pytest -q                # run the test suite

cargo fmt --all          # format Rust
cargo clippy --all-targets -- -D warnings

The tests generate a throwaway git repository on the fly (see tests/conftest.py), so they need git on PATH but touch nothing outside a temp directory.

License

Licensed under either of Apache License 2.0 or MIT license at your option, to match the upstream gitoxide project.

About

Fast, safe, pure-Rust Git for Python — bindings to the gitoxide (gix) engine and a modern alternative to GitPython.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages