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