#!/usr/bin/env python3 """ git-claim-authorship.py Rewrites git history so that any commit where you appear as a co-author instead lists you as the sole author (removing the Co-authored-by line). Usage: python git-claim-authorship.py --name "Your Name" --email "you@example.com" Run from inside the repository you want to rewrite. WARNING: This rewrites history. Force-push required afterwards. """ import subprocess import re import argparse import sys def run(cmd, capture=True, check=True): result = subprocess.run( cmd, shell=True, capture_output=capture, text=True, check=check ) return result.stdout.strip() if capture else None def get_all_commits(): output = run("git log --format='%H' --all") return output.splitlines() if output else [] def get_commit_info(sha): msg = run(f"git log -1 --format=%B {sha}") author_name = run(f"git log -1 --format=%an {sha}") author_email = run(f"git log -1 --format=%ae {sha}") author_date = run(f"git log -1 --format=%aI {sha}") committer_name = run(f"git log -1 --format=%cn {sha}") committer_email = run(f"git log -1 --format=%ce {sha}") committer_date = run(f"git log -1 --format=%cI {sha}") return { "sha": sha, "message": msg, "author_name": author_name, "author_email": author_email, "author_date": author_date, "committer_name": committer_name, "committer_email": committer_email, "committer_date": committer_date, } def find_coauthor_line(message, name, email): """Return True if a Co-authored-by line matches the given name or email.""" pattern = re.compile( r"^Co-authored-by:.*?" + re.escape(name) + r".*$|" r"^Co-authored-by:.*?" + re.escape(email) + r".*$", re.IGNORECASE | re.MULTILINE, ) return bool(pattern.search(message)) def remove_coauthor_line(message, name, email): """Strip matching Co-authored-by lines from the commit message.""" pattern = re.compile( r"^Co-authored-by:.*?" + re.escape(name) + r".*\n?|" r"^Co-authored-by:.*?" + re.escape(email) + r".*\n?", re.IGNORECASE | re.MULTILINE, ) cleaned = pattern.sub("", message) cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).rstrip() + "\n" return cleaned def main(): parser = argparse.ArgumentParser( description="Claim authorship of co-authored commits." ) parser.add_argument("--name", required=True, help="Your full name (as in git)") parser.add_argument("--email", required=True, help="Your email (as in git)") parser.add_argument( "--dry-run", action="store_true", help="Show what would change without modifying anything", ) args = parser.parse_args() # Make sure we're in a git repo try: run("git rev-parse --is-inside-work-tree") except subprocess.CalledProcessError: print("Error: not inside a git repository.", file=sys.stderr) sys.exit(1) print(f"Scanning commits for co-authorship by: {args.name} <{args.email}>") commits = get_all_commits() print(f"Total commits found: {len(commits)}") matches = [] for sha in commits: info = get_commit_info(sha) if find_coauthor_line(info["message"], args.name, args.email): matches.append(info) if not matches: print("No commits found with your Co-authored-by line. Nothing to do.") return print(f"\nFound {len(matches)} commit(s) to rewrite:\n") for info in matches: first_line = info["message"].split("\n")[0] print(f" {info['sha'][:10]} {first_line}") if args.dry_run: print("\n[Dry run] No changes made.") return confirm = input( "\n⚠️ This will rewrite history. Proceed? (yes/no): " ).strip().lower() if confirm != "yes": print("Aborted.") return # Build a filter-branch env filter script sha_set = {info["sha"] for info in matches} case_entries = [] for info in matches: case_entries.append( f" {info['sha']})\n" f" export GIT_AUTHOR_NAME='{args.name}'\n" f" export GIT_AUTHOR_EMAIL='{args.email}'\n" f" ;;" ) case_block = "\n".join(case_entries) env_filter = f""" case $GIT_COMMIT in {case_block} esac """ msg_case_entries = [] for info in matches: new_msg = remove_coauthor_line(info["message"], args.name, args.email) escaped_msg = new_msg.replace("'", "'\\''").replace("`", "\\`").replace("$", "\\$") msg_case_entries.append( f" {info['sha']})\n" f" printf '%s' '{escaped_msg}'\n" f" ;;" ) msg_case_block = "\n".join(msg_case_entries) msg_filter = f""" case $GIT_COMMIT in {msg_case_block} *) cat ;; esac """ with open("/tmp/_msg_filter.sh", "w") as f: f.write(msg_filter) print("\nRewriting history with git filter-branch...") try: result = subprocess.run( [ "git", "filter-branch", "-f", "--env-filter", env_filter, "--msg-filter", "bash /tmp/_msg_filter.sh", "--tag-name-filter", "cat", "--", "--all" ], capture_output=False, text=True, ) if result.returncode != 0: print("filter-branch failed.", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) print("\n✅ Done! History rewritten.") print("\nTo publish the changes, force-push all branches:") print(" git push --force --all") print(" git push --force --tags") print("\nNote: Anyone else with a clone of this repo will need to re-clone or rebase.") if __name__ == "__main__": main()