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”
This commit is contained in:
commit
de5486d0f6
1 changed files with 147 additions and 0 deletions
147
bin/git-commit-update
Normal file
147
bin/git-commit-update
Normal file
|
@ -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 <<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."
|
Loading…
Add table
Add a link
Reference in a new issue