#!/usr/bin/env bash set -euo pipefail # REPLACE THESE: this is the identity I want in rewritten commits NEW_NAME="Your Name" NEW_EMAIL="you@example.com" URL="" OLD_EMAILS="" OLD_NAMES="" AUTO_CLONE=false CLONE_DIR="" usage() { cat < [--email ] [--name ] [--auto-clone] [--dir ] git-commit-update -u [-e ] [-n ] [-c] [-d ] What it does: - Rewrites commit author/committer identities across the repo history: * any email in --email becomes: \$NEW_EMAIL * any name in --name becomes: \$NEW_NAME - Pushes branches and tags (force + prune), no PR/internal refs. Generic examples: git-commit-update -u https://git.example.com/user/repo.git \\ -e "old-email@example.com,old-alias@example.org" \\ -n "Old Name,Another Old Name" git-commit-update -u "ssh://git@git.example.com:2222/user/repo.git" -c EOF } # Parse args while [[ $# -gt 0 ]]; do case "$1" in --url|-u) URL="${2:-}"; shift 2;; --email|-e) OLD_EMAILS="${2:-}"; shift 2;; --name|-n) OLD_NAMES="${2:-}"; shift 2;; --auto-clone|-c) AUTO_CLONE=true; shift 1;; --dir|-d) CLONE_DIR="${2:-}"; shift 2;; -h|--help) usage; exit 0;; *) echo "Unknown arg: $1"; usage; exit 1;; esac done [[ -z "$URL" ]] && { echo "ERROR: --url is required"; usage; exit 1; } # I need git-filter-repo; if missing on Arch, I install it, else ask the user to install. if ! command -v git-filter-repo >/dev/null 2>&1; then if command -v pacman >/dev/null 2>&1; then echo "Installing git-filter-repo via pacman..." sudo pacman -S --needed --noconfirm git-filter-repo else echo "ERROR: git-filter-repo not found. Install it and re-run." echo " https://github.com/newren/git-filter-repo" exit 1 fi fi cleanup() { :; } WORKDIR="" # If I pass -c/--auto-clone, I work in a mirror clone (safe for rewriting everything) if "$AUTO_CLONE"; then if [[ -n "$CLONE_DIR" ]]; then WORKDIR="$CLONE_DIR" mkdir -p "$WORKDIR" echo "Cloning (mirror) into: $WORKDIR" else WORKDIR="$(mktemp -d -t gitupd.XXXXXX)" echo "Cloning (mirror) into temp dir: $WORKDIR" cleanup() { rm -rf "$WORKDIR"; } trap cleanup EXIT fi git clone --mirror "$URL" "$WORKDIR" cd "$WORKDIR" else # Otherwise I expect to be inside an existing Git worktree if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "ERROR: Not inside a Git repository. Use --auto-clone or cd into a repo." exit 1 fi fi # Helper: turn "a,b,c" into a Python bytes set: {b"a",b"b",b"c"} to_py_set() { local csv="${1:-}" if [[ -z "$csv" ]]; then printf 'set()' else awk -v RS=',' ' { gsub(/^[ \t\r\n]+|[ \t\r\n]+$/,"",$0); if (length($0)) printf "b\"%s\",", $0 } ' <<<"$csv" | sed 's/,$//' | awk '{ printf "{%s}", $0 }' fi } EMAIL_SET=$(to_py_set "$OLD_EMAILS") NAME_SET=$(to_py_set "$OLD_NAMES") EMAIL_CB=() NAME_CB=() if [[ "$EMAIL_SET" != "set()" ]]; then EMAIL_CB=( --email-callback "return b\"${NEW_EMAIL}\" if email in ${EMAIL_SET} else email" ) echo "Rewriting these emails -> ${NEW_EMAIL}: $OLD_EMAILS" fi if [[ "$NAME_SET" != "set()" ]]; then NAME_CB=( --name-callback "return b\"${NEW_NAME}\" if name in ${NAME_SET} else name" ) echo "Rewriting these names -> ${NEW_NAME}: $OLD_NAMES" fi if [[ ${#EMAIL_CB[@]} -eq 0 && ${#NAME_CB[@]} -eq 0 ]]; then echo "Nothing to rewrite (no --email/--name provided). Exiting." exit 0 fi # I fetch before rewriting so leases/refs are current git fetch --all --tags --prune # I always drop a safety tag I can roll back to TAG="pre-rewrite-$(date +%Y%m%d-%H%M%S)" git tag "$TAG" || true echo "Created safety tag: $TAG" echo "Running git-filter-repo..." git filter-repo --force "${EMAIL_CB[@]}" "${NAME_CB[@]}" # git-filter-repo may remove 'origin'; I make sure it points to the requested URL if git remote get-url origin >/dev/null 2>&1; then git remote set-url origin "$URL" else git remote add origin "$URL" fi # I push only branches and tags (no PR/internal refs), with force + prune echo "Pushing branches and tags (force + prune)..." git push --force --prune origin 'refs/heads/*:refs/heads/*' git push --force --prune origin 'refs/tags/*:refs/tags/*' echo "Done. Current unique identities:" git log --all --pretty='%an <%ae>' | sort -u echo echo "Notes:" echo "- If the push is rejected, temporarily allow force-push on protected branches on the server, then re-protect." echo "- For HTTPS, most servers require a Personal Access Token as the password."