Skip to content

Instantly share code, notes, and snippets.

@LucasArruda
Forked from alganet/git_reauthor.sh
Created July 15, 2026 23:04
Show Gist options
  • Select an option

  • Save LucasArruda/3cb19da1df6c46401ca5840b77660c33 to your computer and use it in GitHub Desktop.

Select an option

Save LucasArruda/3cb19da1df6c46401ca5840b77660c33 to your computer and use it in GitHub Desktop.
Small git tools to take authorship from LLM work
#!/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)"
#!/usr/bin/env bash
#
# git_redate.sh <back_until_sha_inclusive> <hour_spread>
#
# Rewrite the author and committer dates of every commit from
# <back_until_sha_inclusive> up to HEAD so they are spread evenly across
# the last <hour_spread> hours, ending at "now". The oldest commit lands
# at (now - hour_spread), the newest at now. Each commit keeps its
# original timezone offset.
#
# 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 [ "$#" -ne 2 ]; then
echo "usage: $(basename "$0") <back_until_sha_inclusive> <hour_spread>" >&2
exit 2
fi
SINCE="$1"
HOURS="$2"
# --- validate arguments ---------------------------------------------------
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
if ! [[ "$HOURS" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
echo "error: hour_spread must be a positive number, got '$HOURS'" >&2
exit 1
fi
# Convert an "HH" or "HH.dddd" hours value to whole seconds using only shell
# integer arithmetic (no bc / no floating point, so it is portable).
hours_to_seconds() {
local h="$1" int frac secs len denom i
int="${h%%.*}"
[ -z "$int" ] && int=0
secs=$(( int * 3600 ))
case "$h" in
*.*)
frac="${h#*.}"
len=${#frac}
denom=1; i=0
while [ "$i" -lt "$len" ]; do denom=$(( denom * 10 )); i=$(( i + 1 )); done
secs=$(( secs + frac * 3600 / denom ))
;;
esac
printf '%s' "$secs"
}
# Format an epoch as local wall-clock time, working on both GNU date
# (-d @epoch) and BSD/macOS date (-r epoch).
epoch_to_human() {
if date -r 0 >/dev/null 2>&1; then
date -r "$1" '+%Y-%m-%d %H:%M:%S' # BSD / macOS
else
date -d "@$1" '+%Y-%m-%d %H:%M:%S' # GNU
fi
}
# --- compute window -------------------------------------------------------
NOW=$(date +%s)
SPAN=$(hours_to_seconds "$HOURS")
START=$((NOW - SPAN))
# 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
# commits from SINCE (inclusive) to HEAD, oldest first, each with its
# original timezone offset. We read the offset from the ISO author date
# ("... ±hhmm") rather than a %z placeholder, which some git builds don't
# support. Separator is a tab so the offset lands in its own field.
MAPPING=$(git log --reverse --format='%H%x09%ai' $RANGE)
N=$(printf '%s\n' "$MAPPING" | grep -c .)
if [ "$N" -eq 0 ]; then
echo "error: no commits in ${SINCE}~1..HEAD" >&2
exit 1
fi
# --- build hash -> raw-date case body ------------------------------------
# raw git date format is: "<unix-timestamp> <±hhmm>"
CASES=""
i=0
while IFS=$'\t' read -r h ai; do
[ -z "$h" ] && continue
tz="${ai##* }" # trailing ±hhmm from "YYYY-MM-DD HH:MM:SS ±hhmm"
if [ "$N" -eq 1 ]; then
ts="$NOW"
else
# integer interpolation: START + i * SPAN / (N-1)
ts=$((START + i * SPAN / (N - 1)))
fi
CASES="${CASES} $h) D='$ts $tz' ;;
"
i=$((i + 1))
done <<< "$MAPPING"
echo "Redating $N commit(s) from $SINCE (inclusive) across the last $HOURS hour(s):"
echo " window: $(epoch_to_human "$START") -> $(epoch_to_human "$NOW")"
# --- rewrite --------------------------------------------------------------
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f --env-filter '
D=""
case "$GIT_COMMIT" in
'"$CASES"'
esac
if [ -n "$D" ]; then
export GIT_AUTHOR_DATE="$D"
export GIT_COMMITTER_DATE="$D"
fi
' $RANGE
echo
echo "Done. New dates (oldest first):"
git log --reverse --format='%h %ai %s' $RANGE
echo
echo "Backup ref kept at refs/original/. Undo with:"
echo " git reset --hard refs/original/refs/heads/$(git rev-parse --abbrev-ref HEAD)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment