Scripts/bin/git-commit-update
Ronniie de5486d0f6 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”
2025-08-21 01:38:00 +00:00

147 lines
4.5 KiB
Bash

#!/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 <<EOF
Usage:
git-commit-update --url <URL> [--email <old1,old2,...>] [--name <old1,old2,...>] [--auto-clone] [--dir <path>]
git-commit-update -u <URL> [-e <old1,old2,...>] [-n <old1,old2,...>] [-c] [-d <path>]
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."