# Small git tools to take authorship from LLM work

> Source: <https://gist.github.com/alganet/2437625c4dc7c8f44ccf19aed2cfc996>
> Published: 2026-07-15 14:54:43+00:00

|
#!/usr/bin/env bash |
|
# |
|
# git_reauthor.sh <back_until_sha_inclusive> <committer_name_email> <coauthored_by_name_email> |
|
# |
|
# Rewrite the identity (author AND committer name/email) of every commit from |
|
# <back_until_sha_inclusive> up to HEAD, and fix up the Co-Authored-By trailer. |
|
# |
|
# Arguments 2 and 3 are optional: |
|
# * no <committer_name_email> -> use the current git config user.name/email |
|
# as the identity (and, since arg 3 is then also absent, strip co-authors). |
|
# * no <coauthored_by_name_email> -> strip every Co-Authored-By line from the |
|
# commit messages. |
|
# * <coauthored_by_name_email> present -> replace any existing Co-Authored-By |
|
# line(s) with a single "Co-Authored-By: <that value>" (appended if the |
|
# commit had none). |
|
# |
|
# Identity arguments are in the usual git form: Name Surname <email@host> |
|
# Commit dates are left untouched. |
|
# |
|
# WARNING: this rewrites history (commit SHAs change). filter-branch keeps a |
|
# backup at refs/original/... ; undo with: |
|
# git reset --hard refs/original/refs/heads/<branch> |
|
# |
|
set -euo pipefail |
|
|
|
if [ "$#" -lt 1 ] || [ "$#" -gt 3 ]; then |
|
echo "usage: $(basename "$0") <back_until_sha_inclusive> <committer_name_email> <coauthored_by_name_email>" >&2 |
|
echo " (args 2 and 3 optional; omit arg 3 to strip Co-Authored-By lines," >&2 |
|
echo " omit arg 2 to also fall back to the current git config identity)" >&2 |
|
exit 2 |
|
fi |
|
|
|
SINCE="$1" |
|
IDENT="${2:-}" |
|
COAUTHOR="${3:-}" |
|
|
|
# --- validate ------------------------------------------------------------- |
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
|
echo "error: not inside a git work tree" >&2 |
|
exit 1 |
|
fi |
|
|
|
if ! git rev-parse --verify --quiet "${SINCE}^{commit}" >/dev/null; then |
|
echo "error: '$SINCE' is not a valid commit" >&2 |
|
exit 1 |
|
fi |
|
|
|
# Resolve the target identity: an explicit "Name <email>" argument, or the |
|
# current git config user.name/user.email when arg 2 is omitted. |
|
if [ -n "$IDENT" ]; then |
|
case "$IDENT" in |
|
*"<"*">"*) : ;; |
|
*) echo "error: committer identity must look like 'Name <email>', got '$IDENT'" >&2; exit 1 ;; |
|
esac |
|
RA_NAME="$(printf '%s' "$IDENT" | sed -E 's/[[:space:]]*<[^>]*>[[:space:]]*$//')" |
|
RA_EMAIL="$(printf '%s' "$IDENT" | sed -E 's/^[^<]*<([^>]*)>.*$/\1/')" |
|
else |
|
RA_NAME="$(git config user.name || true)" |
|
RA_EMAIL="$(git config user.email || true)" |
|
if [ -z "$RA_NAME" ] || [ -z "$RA_EMAIL" ]; then |
|
echo "error: no identity given and git config user.name/user.email is unset" >&2 |
|
exit 1 |
|
fi |
|
fi |
|
|
|
if [ -n "$COAUTHOR" ]; then |
|
case "$COAUTHOR" in |
|
*"<"*">"*) : ;; |
|
*) echo "error: co-author must look like 'Name <email>', got '$COAUTHOR'" >&2; exit 1 ;; |
|
esac |
|
fi |
|
|
|
# Range that includes SINCE itself. If SINCE is the root commit it has no |
|
# parent, so fall back to the full history reachable from HEAD. |
|
if git rev-parse --verify --quiet "${SINCE}^{commit}^" >/dev/null; then |
|
RANGE="${SINCE}~1..HEAD" |
|
else |
|
RANGE="HEAD" |
|
fi |
|
|
|
# --- awk program that rewrites the commit message ------------------------- |
|
# Removes existing Co-Authored-By lines, trims trailing blank lines, and (when |
|
# ca is non-empty) appends a single Co-Authored-By trailer, keeping it inside |
|
# the trailing trailer block when one already exists. |
|
AWK_PROG="$(mktemp "${TMPDIR:-/tmp}/reauthor.XXXXXX")" |
|
trap 'rm -f "$AWK_PROG"' EXIT |
|
cat > "$AWK_PROG" <<'AWK' |
|
{ buf[NR] = $0 } |
|
END { |
|
m = 0 |
|
for (i = 1; i <= NR; i++) { |
|
if (tolower(buf[i]) ~ /^co-authored-by:/) continue |
|
out[++m] = buf[i] |
|
} |
|
while (m > 0 && out[m] ~ /^[ \t]*$/) m-- # trim trailing blanks |
|
for (i = 1; i <= m; i++) print out[i] |
|
if (ca != "") { |
|
if (m > 0 && out[m] ~ /^[A-Za-z0-9-]+:[ \t]/) { # last line already a trailer |
|
print "Co-Authored-By: " ca |
|
} else { |
|
if (m > 0) print "" # blank line before trailer |
|
print "Co-Authored-By: " ca |
|
} |
|
} |
|
} |
|
AWK |
|
|
|
# --- rewrite -------------------------------------------------------------- |
|
export RA_NAME RA_EMAIL COAUTHOR |
|
|
|
ENV_FILTER=' |
|
export GIT_AUTHOR_NAME="$RA_NAME" |
|
export GIT_AUTHOR_EMAIL="$RA_EMAIL" |
|
export GIT_COMMITTER_NAME="$RA_NAME" |
|
export GIT_COMMITTER_EMAIL="$RA_EMAIL" |
|
' |
|
MSG_FILTER="awk -v ca=\"\$COAUTHOR\" -f \"$AWK_PROG\"" |
|
|
|
if [ -n "$COAUTHOR" ]; then |
|
echo "Reauthoring $RANGE as '$RA_NAME <$RA_EMAIL>', co-author -> '$COAUTHOR'" |
|
else |
|
echo "Reauthoring $RANGE as '$RA_NAME <$RA_EMAIL>', stripping Co-Authored-By lines" |
|
fi |
|
|
|
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f \ |
|
--env-filter "$ENV_FILTER" \ |
|
--msg-filter "$MSG_FILTER" \ |
|
-- $RANGE |
|
|
|
echo |
|
echo "Done. Backup ref kept at refs/original/. Undo with:" |
|
echo " git reset --hard refs/original/refs/heads/$(git rev-parse --abbrev-ref HEAD)" |
