Skip to content

feat: expose git blame via Repository.blame() - #19

Merged
shenxianpeng merged 1 commit into
mainfrom
feature/blame-support
Sep 1, 2026
Merged

feat: expose git blame via Repository.blame()#19
shenxianpeng merged 1 commit into
mainfrom
feature/blame-support

Conversation

@shenxianpeng

Copy link
Copy Markdown
Owner

Summary

Wraps gix's blame engine as Repository.blame(path, rev=None), the first
piece of the feature-gated gix surface this binding has reached for.

>>> repo = gitoxide.open(".")
>>> for hunk in repo.blame("README.md")[:3]:
...     print(f"{hunk.start_line}-{hunk.end_line}\t{hunk.short_id}")
1-2     b55a5e0
3-3     f6a6ddd
4-5     295428e

What's here

  • Cargo.toml — enables the blame feature of gix (which pulls in
    blob-diffattributes).
  • Repository.blame(path, rev=None)rev defaults to HEAD.
  • BlameHunkstart_line, end_line, line_count, orig_start_line,
    commit_id, short_id, plus a __repr__.
  • Line numbers converted to 1-based inclusive, matching git blame. gix
    counts from 0, so passing its values straight through would have been off by
    one in every hunk.
  • 4 tests against the existing sample_repo fixture, including the exact
    commit attribution for a two-line file, blame at an older revision, and the
    error path for a missing file.
  • README — quick-start example, API reference, and blame moved from
    "not wrapped yet" to "wrapped".

Cost

Measured on macOS arm64, release + thin LTO:

before after
wheel 1.51 MB 1.99 MB (+31.7%)
cold build 26s ~38s
transitive crates 113 125

The twelve new crates (gix-blame, gix-imara-diff, gix-filter,
gix-worktree, gix-attributes, gix-dir, …) are all pure Rust — wheels
stay self-contained and still need no C toolchain.

Worth knowing: enabling the feature without calling it only costs ~1%,
because LTO strips the unreachable code. The +31.7% above is the real,
measured cost with the binding actually wired up.

One caveat, reflected in the README

blob-diff brings in gix-filter, which will execute a configured
clean/smudge filter
(Git LFS, for example) when blaming such a file — the
same thing git does. Nothing shells out to the git CLI, so the comparison
against GitPython still holds, but the blanket "no subprocess" line in the
README now carries a footnote.

Verification

pytest -q20 passed (16 existing + 4 new). cargo fmt --check and
cargo clippy --all-targets -- -D warnings both clean. The README example is
copied from a real run against this repository.

Not included

repo.is_dirty() was floated alongside this, but it lives behind gix's
status feature, not blame — and status drags in dirwalk + index
on top of blob-diff. Separate change, separate cost.

Enables the `blame` feature of gix (which pulls in `blob-diff`) and wraps
`Repository::blame_file()` as `Repository.blame(path, rev=None)`, returning
a list of `BlameHunk` objects with 1-based, inclusive line numbers to match
`git blame` — gix itself counts from 0.

Consecutive lines sharing a commit arrive already grouped into a hunk, so
the result is typically far shorter than the file.

Cost on macOS arm64: the wheel grows 1.51 MB -> 1.99 MB (+31.7%) and a cold
release build goes from 26s to ~38s. The twelve new transitive crates
(gix-blame, gix-imara-diff, gix-filter, gix-worktree, ...) are all pure
Rust, so wheels stay self-contained and still need no C toolchain.

Note that `blob-diff` brings in gix-filter, which executes a configured
clean/smudge filter (Git LFS, for instance) when blaming such a file, just
as git does. README now qualifies the "no subprocess" claim accordingly.
@shenxianpeng shenxianpeng added the enhancement New feature or request label Sep 1, 2026
@shenxianpeng
shenxianpeng merged commit 73b86fe into main Sep 1, 2026
13 checks passed
@shenxianpeng
shenxianpeng deleted the feature/blame-support branch September 1, 2026 06:21
shenxianpeng added a commit that referenced this pull request Sep 1, 2026
v0.3.0 can never be released from this repository. The tag name was
consumed by an immutable release that was published and then deleted,
and GitHub reserves such names permanently ("tag name was used by an
immutable release, published releases must have a valid tag"). Turning
the setting off does not free an already-consumed name.

0.4.0 rather than 0.3.1: main now carries the blame binding (#19), which
is a feature and warrants a minor bump, and 0.3.0 never reached PyPI —
publishing a 0.3.1 with no 0.3.0 preceding it would only confuse.

Only Cargo.toml (and the lockfile) change here. Since #18 the Python
`__version__` is read from installed package metadata, so it follows
along on its own.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant