-
-
Save LucasArruda/3cb19da1df6c46401ca5840b77660c33 to your computer and use it in GitHub Desktop.
Small git tools to take authorship from LLM work
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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