Skip to main content

Git Reattribute

CI PyPI License: MIT

A small, cross-platform CLI for replacing one Git identity with another across a repository's history, safely — without requiring you to hand-write a git-filter-repo invocation. Also includes guard, a prevention companion that blocks a denied identity in CI or a local hook before it ever lands, so you don't need to run the remediation flow again.

Contents: Why? · Features · Installation · Quick Start · Example · Interactive Usage · Non-Interactive Usage · Dry Run · Branches · Authors vs Committers · Co-authored-by Trailers · History Rewriting Warning · Push Behavior · Recovery · guard: prevent denied identities · Limitations · Development · License

⚠️ This tool rewrites Git history. Rewriting history changes commit SHAs, invalidates existing signatures on rewritten commits, and requires a force push to update any already-published branch. A force push can disrupt collaborators and invalidate their existing clones. Only use this on repositories and history you are authorized to modify. This tool does not modify GitHub (or any other host's) account data or contributor database directly — hosts may independently recompute contributor attribution from the resulting commit metadata.

Why?

A Git "identity" isn't always a person — it can be a placeholder, a bot account, a shared machine account, or an AI agent that paired on the work. Correcting a mis-attributed one usually means hand-writing a git-filter-repo invocation. Git Reattribute turns that into one guided command.

Features

  • Interactive branch, contributor, and replacement-identity selection.
  • Non-interactive / scriptable usage via flags.
  • Replaces author, committer, or both (default: both).
  • Removes matching Co-authored-by: trailers for the replaced identity.
  • Dry-run mode — preview the impact with no changes made.
  • Creates a backup ref before every rewrite.
  • Mandatory post-rewrite verification.
  • --force-with-lease push only, never --force, never silent.
  • Warns about signed commits and shallow repositories before rewriting.
  • guard subcommand: prevent denied identities from ever being committed or merged, via a GitHub Action and/or a local pre-commit hook — see guard.

Requirements

  • Python 3.10+
  • Git 2.x
  • git-filter-repo — installed automatically as a dependency when you pip install git-reattribute; you don't need to install it separately.

Installation

python -m pip install git-reattribute

Quick Start

cd your-repo
git-reattribute --version
git-reattribute --help
git-reattribute

Example

$ git-reattribute

Git Reattribute

Repository: /home/alice/projects/example
Remote:    origin
Branch:    main

Contributors on main:

  1. Claude <claude@example.com>    47 commits
  2. Alice <alice@example.com>      18 commits
  3. Bob <bob@example.com>           6 commits

Select contributor to replace: Claude <claude@example.com>    47 commits

Replace with:
  Current Git identity
  Alice <alice@example.com>
  Bob <bob@example.com>
  Enter a custom identity

Select replacement: Alice <alice@example.com>

Summary
-------
Branch:       main
From:         Claude <claude@example.com>
To:           Alice <alice@example.com>
Author commits affected:    47
Committer commits affected: 47
Co-authored-by trailers to remove: 3

WARNING: This operation rewrites Git history.

Type REWRITE to continue: REWRITE

History rewrite completed.

Recovery reference:
  refs/backup/git-reattribute/2026-08-22T10-30-00

To restore the original branch:
  git reset --hard refs/backup/git-reattribute/2026-08-22T10-30-00

Keep this reference until you have verified the rewritten history.

Verification

  Old identity commits before: 94
  Remaining old identity commits: 0
  New identity commits: 94
  Remaining Co-authored-by trailers: 0

Verification: PASS

Push rewritten branch to origin with --force-with-lease? [y/N]

Interactive Usage

Running git-reattribute with no flags walks you through: select a branch, see its contributors, pick who to replace, pick the replacement, preview the change, then type REWRITE to confirm.

Non-Interactive Usage

git-reattribute \
  --branch main \
  --from-email claude@example.com \
  --to-name Alice \
  --to-email alice@example.com \
  --identity-type both \
  --yes --push

--yes does not imply --push — pass both explicitly when you want a scripted rewrite-and-push.

Dry Run

git-reattribute --branch main --from-email claude@example.com --to-current-user --dry-run

Shows affected author/committer commit counts and any Co-authored-by: trailers that would be removed. No history or remote is touched.

Branches

Local branches are listed and selectable. Remote-tracking branches are not rewritten directly — check out a local branch first. The rewrite scope is the entire reachable history of the selected branch — every commit an ancestor of that branch's tip, not just commits made while that branch was checked out.

Authors vs Committers

Every commit has an author and a committer, and they can differ. Use --identity-type author, --identity-type committer, or the default --identity-type both to control which field(s) get rewritten. The interactive UI shows both roles whenever they differ for a given contributor.

Co-authored-by Trailers

When an identity (for example an AI agent that paired with the human author) appears as a Co-authored-by: trailer in a commit message, that trailer is removed (not replaced) for the identity being replaced, by default. This matters because rewriting only the author field of a commit like:

Author: Claude <claude@example.com>

Co-authored-by: Claude <claude@example.com>

would otherwise leave the old identity behind in the message body even after the commit object's author is fixed.

This runs alongside the author/committer rewrite for the same source identity — there is no separate replacement target for trailers, and the trailer is deleted, not rewritten to a new co-author. Disable it with --no-strip-coauthor-trailers.

Matching rule: a trailer line matches when it starts with Co-authored-by: (case-insensitive) followed by the source identity's name and <email>, with any amount of whitespace tolerated around the colon, name, and angle brackets — but the name and email themselves must match the source identity's recorded name/email (case-insensitively), not a partial or fuzzy match. Only matching trailer lines are removed; other trailers (e.g. Signed-off-by:) and body text that merely mentions the same name are left untouched.

History Rewriting Warning

This tool rewrites Git history using git-filter-repo. Two different --force flags matter here, and they are not the same thing:

  • git-filter-repo --force (used internally, always) only permits the local rewrite to run on a non-fresh-clone repository. git-filter-repo normally refuses to touch anything but a fresh clone as a generic safety net; this tool creates its own backup ref (see Recovery below) before that rewrite ever runs, so that backup is the safety net instead, and the --force flag just lets the local rewrite proceed.
  • git push --force is never used by this tool, anywhere. Publishing a rewritten branch always uses git push --force-with-lease (see Push Behavior), and only after you explicitly confirm it.

Contributor discovery uses Git's raw %an/%ae/%cn/%ce fields, not mailmap-resolved fields — a repository's .mailmap is intentionally ignored so what you see and rewrite always matches the actual commit-object bytes.

Signed commits lose their signatures when rewritten — a warning is shown before you confirm. Merge commits are rewritten natively by git-filter-repo as part of a full-branch rewrite; this requires no special handling in v1 since there is no commit-range filtering yet.

Push Behavior

Pushing always uses git push --force-with-lease, never git push --force. Nothing is pushed unless you pass --push (scripted) or confirm the interactive prompt. If the remote branch changed since the rewrite began, the push is rejected and nothing is retried automatically.

Recovery

Before every rewrite — and before git-filter-repo is ever invoked — a backup ref is created pointing at the original branch tip:

refs/backup/git-reattribute/<timestamp>

The rewrite is scoped (via --refs <branch>) to only the selected branch, so the backup ref itself is never touched or rewritten by the same operation. It is never deleted automatically. After a rewrite, the tool prints the exact recovery command:

Recovery reference:
  refs/backup/git-reattribute/2026-08-22T10-30-00

To restore the original branch:
  git reset --hard refs/backup/git-reattribute/2026-08-22T10-30-00

Keep the backup ref until you've verified the rewritten history (and pushed, if applicable) — then it's safe to delete with git update-ref -d refs/backup/git-reattribute/<timestamp>.

guard: prevent denied identities before they land

Everything above is remediation — fixing history that already has a bad identity in it. git-reattribute guard is the prevention half: it never touches history, it only scans and reports (exit 0 clean, exit 1 on a match), so it's safe to run unattended in a hook or CI job.

Setup

git-reattribute guard init

Scaffolds everything in one shot: .git-reattribute-guard.yml (empty denylist template with commented examples, or pass --deny-name/ --deny-email to populate a real entry immediately) and .github/workflows/guard.yml (wired to the Action below). It also prints the pre-commit snippet to paste in yourself — that one's never auto-written, since safely merging into an existing .pre-commit-config.yaml needs a human, not a blind overwrite. Existing files are left untouched unless you pass --force.

Or write .git-reattribute-guard.yml by hand at your repo root:

deny:
  - name: Claude
    email: claude@example.com
  - email: "*@bots.example.com"   # glob supported on email only
check_coauthors: true              # also check Co-authored-by trailers (default: true)

CI (the actual enforcement point)

name: Guard commit identities
on: [pull_request]

jobs:
  guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: drk1rd/git-reattribute/.github/actions/guard@v1

This runs git-reattribute guard check against every commit in the PR's range, server-side — regardless of what a contributor did locally. This is the layer that actually matters for keeping history clean.

Local hook (fast feedback, skippable)

git-reattribute guard check-local checks the identity Git is about to use right now — your user.name/user.email, plus any Co-authored-by: trailer in the message you're writing. Wire it up via the pre-commit framework's local hook type (no separate hook-repo needed — it just runs the already-installed git-reattribute command):

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: git-reattribute-guard
        name: git-reattribute-guard
        entry: git-reattribute guard check-local
        language: system
        stages: [commit-msg]
pre-commit install --hook-type commit-msg

This is a convenience for the person committing — it's skippable with git commit --no-verify, so it's not a substitute for the CI check above.

Direct CLI use

git-reattribute guard check --base origin/main --head HEAD
git-reattribute guard: found 1 violation(s) in origin/main..HEAD:

  a1b2c3d4  author: Claude <claude@example.com>

Fix with:
  git-reattribute --branch main --from-email claude@example.com --to-current-user

Every violation names the exact git-reattribute command to fix it — the two halves of this tool are meant to be used together: guard stops it from happening again, git-reattribute cleans up what's already there.

Limitations

  • Shallow clones may show an incomplete contributor list and rewrite scope; run git fetch --unshallow first.
  • Submodules, Git LFS objects, and annotated tag signatures are not specially handled in v1.
  • Branch-protection or remote-policy push rejections are surfaced as-is; this tool never attempts to bypass them.
  • One source identity → one target identity per run.

Development

python -m pip install -e ".[dev]"

Testing

pytest

Tests build temporary Git repositories per-test; nothing is ever run against a real/shared repository.

Release Process

See CHANGELOG.md. Versioning follows SemVer (MAJOR.MINOR.PATCH).

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

git_reattribute-0.3.0.tar.gz (28.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

git_reattribute-0.3.0-py3-none-any.whl (26.8 kB view details)

Uploaded Python 3

File details

Details for the file git_reattribute-0.3.0.tar.gz.

File metadata

  • Download URL: git_reattribute-0.3.0.tar.gz
  • Upload date:
  • Size: 28.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for git_reattribute-0.3.0.tar.gz
Algorithm Hash digest
SHA256 0657c7b9f4624ed43223aa93324d5f84f5544afec54ff7fd80c08b0d7b614ec1
MD5 60a65967d81cad21c40c2357ec8bffaf
BLAKE2b-256 8611dcd4e818dc6ca733e8344b37e3b459491a1490aae6d7b7d035d49b913ac0

See more details on using hashes here.

Provenance

The following attestation bundles were made for git_reattribute-0.3.0.tar.gz:

Publisher: publish.yml on drk1rd/git-reattribute

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file git_reattribute-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: git_reattribute-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 26.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for git_reattribute-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1fda7d20209f0e2d33441f9579fd8407695ffaa7858cf3b87d0910b8dc146207
MD5 26977bab8a9f3922277e2a049ac4b99e
BLAKE2b-256 dde5ac937b24d34f794845f99aa2702e1cc549c679de8913b65889b1ca8c1982

See more details on using hashes here.

Provenance

The following attestation bundles were made for git_reattribute-0.3.0-py3-none-any.whl:

Publisher: publish.yml on drk1rd/git-reattribute

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page