commit de5486d0f6ed99e98ce4afb93c6917dc81cd93a1 Author: Ronniie Date: Thu Aug 21 01:38:00 2025 +0000 feat(scripts): add git-commit-update for rewriting commit identity - Rewrites author/committer names & emails via git-filter-repo - Flags: -u/--url, -e/--email, -n/--name, -c/--auto-clone, -d/--dir - Adds safety tag; pushes heads/tags only (force+prune) - Installs git-filter-repo on Arch if missing - Comments: first-person style; generic examples; NEW_NAME/NEW_EMAIL marked “REPLACE THESE” diff --git a/bin/git-commit-update b/bin/git-commit-update new file mode 100644 index 0000000..4939084 --- /dev/null +++ b/bin/git-commit-update @@ -0,0 +1,147 @@ +#!/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."