-
Notifications
You must be signed in to change notification settings - Fork 62
Refactor commit date modifier to support multiple modes #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,84 +1,317 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Script Name: commit_date_modifier.sh | ||
| # Description: Modifies the date of the latest Git commit to a user-provided date | ||
| # Usage: commit_date_modifier.sh DD-MM-YYYY | ||
| # The date is in the format day-month-year | ||
| # Example: commit_date_modifier.sh 25-12-2022 | ||
| # ----------------------------------------------------------------------------- | ||
| # commit_date_tools.sh | ||
| # | ||
| # Powerful Git commit-date tools: | ||
| # 1) amend-latest: set latest commit to a specific date/time | ||
| # 2) shift: shift ALL commits by hours/days | ||
| # 3) move: move ALL commits into day or night hours (randomized within window) | ||
| # | ||
| # Defaults: | ||
| # - Timezone offset: +0200 (UTC+2) | ||
| # - Day window: 09-18 (9am..6pm) | ||
| # - Night window: 20-23,00-05 (8pm..11pm and midnight..5am) | ||
| # | ||
| # Requirements: git, GNU date (Linux 'date' or macOS 'gdate' from coreutils) | ||
| # | ||
| # Examples: | ||
| # # 1) Set latest commit to specific day (random time) in UTC+2 | ||
| # ./commit_date_tools.sh amend-latest --date 25-12-2022 | ||
| # | ||
| # # 1b) Set latest commit to specific day & time with custom tz | ||
| # ./commit_date_tools.sh amend-latest --date 25-12-2022 --time 14:30 --tz +0530 | ||
| # | ||
| # # 2) Shift entire history forward 7 hours | ||
| # ./commit_date_tools.sh shift --hours 7 | ||
| # | ||
| # # 2b) Shift entire history back 2 days and 3 hours, timezone +0200 | ||
| # ./commit_date_tools.sh shift --days -2 --hours -3 --tz +0200 | ||
| # | ||
| # # 3) Move all commits into daytime (random hour in window) | ||
| # ./commit_date_tools.sh move --to day | ||
| # | ||
| # # 3b) Move to night hours with custom windows and timezone | ||
| # ./commit_date_tools.sh move --to night --night-window 21-23,00-04 --tz -0500 | ||
| # | ||
| # After rewriting history: | ||
| # git push --force-with-lease | ||
| # ----------------------------------------------------------------------------- | ||
|
|
||
| # Function: Returns absolute value of a number | ||
| abs() { | ||
| echo "${1#-}" | ||
| # ---------- Utilities ---------- | ||
|
|
||
| # Pick GNU date (Linux: date, macOS: gdate) | ||
| DATE_BIN="$(command -v gdate || command -v date)" | ||
| if ! "$DATE_BIN" -d "@0" +%s >/dev/null 2>&1; then | ||
| echo "Error: GNU 'date' required. On macOS: brew install coreutils (use gdate)." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| require_git_repo() { | ||
| git rev-parse --git-dir >/dev/null 2>&1 || { echo "Not a git repo."; exit 1; } | ||
| } | ||
|
|
||
| # Function: Convert the input date into a weekday string | ||
| day_string_converter() { | ||
| local day=$1 | ||
| local month=$2 | ||
| local year=$3 | ||
| # Parse "+HHMM" or "-HHMM" into seconds (supports half-hours like +0530) | ||
| tz_to_seconds() { | ||
| local tz="$1" sign hh mm secs | ||
| [[ "$tz" =~ ^[+-][0-9]{4}$ ]] || { echo "Invalid tz offset '$tz' (use +HHMM/-HHMM)"; exit 1; } | ||
| sign="${tz:0:1}" | ||
| hh=$((10#${tz:1:2})) | ||
| mm=$((10#${tz:3:2})) | ||
| secs=$((hh*3600 + mm*60)) | ||
| if [[ "$sign" == "-" ]]; then secs=$((-secs)); fi | ||
| echo "$secs" | ||
| } | ||
|
|
||
| # Zeller's Congruence algorithm for calculating the day of the week | ||
| local a=$(( (14 - month) / 12 )) | ||
| local y=$(( year - a )) | ||
| local m=$(( month + (12 * a) - 2 )) | ||
| local d=$(( (day + y + (y / 4) - (y / 100) + (y / 400) + ((31 * m) / 12)) % 7 )) | ||
| # Random int in [min,max] inclusive | ||
| rand_int() { | ||
| local min=$1 max=$2 | ||
| echo $(( min + RANDOM % (max - min + 1) )) | ||
| } | ||
|
|
||
| local days=("Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat") | ||
| # Build a space-separated list of allowed hours from a window spec like "09-18" or "20-23,00-05" | ||
| expand_windows_to_hours() { | ||
| local spec="$1" part start end h hours=() | ||
| IFS=',' read -ra PARTS <<< "$spec" | ||
| for part in "${PARTS[@]}"; do | ||
| [[ "$part" =~ ^([0-1][0-9]|2[0-3])\-([0-1][0-9]|2[0-3])$ ]] \ | ||
| || { echo "Invalid window '$part' (use HH-HH,HH-HH)"; exit 1; } | ||
| start="${part%-*}"; end="${part#*-}" | ||
| start=$((10#$start)); end=$((10#$end)) | ||
| if (( start <= end )); then | ||
| for ((h=start; h<=end; h++)); do hours+=("$h"); done | ||
| else | ||
| # Wrap around midnight (e.g., 20-05) | ||
| for ((h=start; h<=23; h++)); do hours+=("$h"); done | ||
| for ((h=0; h<=end; h++)); do hours+=("$h"); done | ||
| fi | ||
| done | ||
| echo "${hours[*]}" | ||
| } | ||
|
|
||
| echo "${days[d]}" | ||
| # Turn Y-m-d H:M:S in the *local tz* into epoch seconds: | ||
| ymd_hms_to_epoch_in_tz() { | ||
| local y="$1" m="$2" d="$3" H="$4" M="$5" S="$6" tz="$7" | ||
| "$DATE_BIN" -d "${y}-${m}-${d} ${H}:${M}:${S} ${tz}" +%s | ||
| } | ||
|
|
||
| # Function: Convert numeric month into a month string | ||
| month_string_converter() { | ||
| local month_strs=("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec") | ||
| # ---------- Modes ---------- | ||
|
|
||
| print_help() { | ||
| cat <<'EOF' | ||
| Usage: | ||
| commit_date_tools.sh <mode> [options] | ||
|
|
||
| Modes: | ||
| amend-latest Set the latest commit to a specific date/time. | ||
| shift Shift ALL commits by hours/days. | ||
| move Move ALL commits into day or night hours (randomized). | ||
|
|
||
| Common options: | ||
| --tz <+HHMM|-HHMM> Timezone offset to use when writing dates (default +0200). | ||
|
|
||
| amend-latest options: | ||
| --date DD-MM-YYYY Required (e.g., 25-12-2024) | ||
| --time HH:MM Optional (random if omitted) | ||
|
|
||
| echo "${month_strs[$1-1]}" | ||
| shift options: | ||
| --hours N Integer hours to shift (can be negative) | ||
| --days N Integer days to shift (can be negative) | ||
|
|
||
| move options: | ||
| --to day|night Required | ||
| --day-window HH-HH Default 09-18 | ||
| --night-window HH-HH,HH-HH Default 20-23,00-05 | ||
|
|
||
| Notes: | ||
| • This rewrites history (all branches & tags). Backup first. | ||
| • After running: git push --force-with-lease | ||
| EOF | ||
| } | ||
|
|
||
| # Function: Generate a random time string in the format HH:MM | ||
| random_time() { | ||
| printf -v time "%02d:%02d" $((RANDOM % 24)) $((RANDOM % 60)) | ||
| echo "$time" | ||
| # ---------- Argument parsing ---------- | ||
|
|
||
| MODE="${1:-}" | ||
| [[ -z "${MODE}" ]] && { print_help; exit 1; } | ||
| shift || true | ||
|
|
||
| TZ_OFFSET="+0200" | ||
| DATE_DDMMYYYY="" | ||
| TIME_HHMM="" | ||
| SHIFT_HOURS="0" | ||
| SHIFT_DAYS="0" | ||
| MOVE_TO="" | ||
| DAY_WINDOW="09-18" | ||
| NIGHT_WINDOW="20-23,00-05" | ||
|
|
||
| while (( "$#" )); do | ||
| case "$1" in | ||
| --tz) TZ_OFFSET="${2:?}"; shift 2;; | ||
| --date) DATE_DDMMYYYY="${2:?}"; shift 2;; | ||
| --time) TIME_HHMM="${2:?}"; shift 2;; | ||
| --hours) SHIFT_HOURS="${2:?}"; shift 2;; | ||
| --days) SHIFT_DAYS="${2:?}"; shift 2;; | ||
| --to) MOVE_TO="${2:?}"; shift 2;; | ||
| --day-window) DAY_WINDOW="${2:?}"; shift 2;; | ||
| --night-window) NIGHT_WINDOW="${2:?}"; shift 2;; | ||
| -h|--help) print_help; exit 0;; | ||
| *) echo "Unknown option: $1"; echo; print_help; exit 1;; | ||
| esac | ||
| done | ||
|
|
||
| # ---------- Validations ---------- | ||
|
|
||
| require_git_repo | ||
| TZ_SECS="$(tz_to_seconds "$TZ_OFFSET")" | ||
|
|
||
| # ---------- Implementations ---------- | ||
|
|
||
| amend_latest() { | ||
| [[ "$DATE_DDMMYYYY" =~ ^([0-2][0-9]|3[0-1])-([0][1-9]|1[0-2])-[0-9]{4}$ ]] \ | ||
| || { echo "Invalid --date. Use DD-MM-YYYY"; exit 1; } | ||
| IFS='-' read -r DD MM YYYY <<< "$DATE_DDMMYYYY" | ||
|
|
||
| if [[ -z "$TIME_HHMM" ]]; then | ||
| HH=$(rand_int 0 23) | ||
| MMm=$(rand_int 0 59) | ||
| printf -v TIME_HHMM "%02d:%02d" "$HH" "$MMm" | ||
| else | ||
| [[ "$TIME_HHMM" =~ ^([0-1][0-9]|2[0-3]):([0-5][0-9])$ ]] \ | ||
| || { echo "Invalid --time. Use HH:MM"; exit 1; } | ||
| fi | ||
|
|
||
| HH="${TIME_HHMM%:*}" | ||
| MI="${TIME_HHMM#*:}" | ||
| SS="00" | ||
|
|
||
| EPOCH="$("$DATE_BIN" -d "${YYYY}-${MM}-${DD} ${HH}:${MI}:${SS} ${TZ_OFFSET}" +%s)" | ||
|
|
||
| GIT_AUTHOR_DATE="${EPOCH} ${TZ_OFFSET}" \ | ||
| GIT_COMMITTER_DATE="${EPOCH} ${TZ_OFFSET}" \ | ||
| git commit --amend --no-edit | ||
|
|
||
| echo "✓ Amended latest commit date to ${YYYY}-${MM}-${DD} ${HH}:${MI}:${SS} ${TZ_OFFSET}" | ||
| } | ||
|
|
||
| # Function: Validate the input date format | ||
| validate_date() { | ||
| if [[ $1 =~ ^([1-9]|0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-([0-9]{4})$ ]]; then | ||
| IFS='-' read -r day month year <<< "$1" | ||
| # Check the date validity | ||
| if ! date -d"$month/$day/$year" >/dev/null 2>&1; then | ||
| echo "Invalid date: $1" | ||
| exit 1 | ||
| fi | ||
| else | ||
| echo "Incorrect date format: $1. Expected format: DD-MM-YYYY" | ||
| exit 1 | ||
| fi | ||
| shift_history() { | ||
| local shift_secs=$(( SHIFT_DAYS*86400 + SHIFT_HOURS*3600 )) | ||
| echo "Shifting ALL commits by ${SHIFT_DAYS} day(s) and ${SHIFT_HOURS} hour(s) [${shift_secs}s]; tz=${TZ_OFFSET}" | ||
| if [[ "$shift_secs" -eq 0 ]]; then | ||
| echo "Nothing to do (shift is zero)."; exit 0 | ||
| fi | ||
|
|
||
| git filter-branch -f --tag-name-filter cat --env-filter " | ||
| shift_secs=${shift_secs} | ||
| tz='${TZ_OFFSET}' | ||
| to_epoch() { $DATE_BIN -d \"\$1\" +%s; } | ||
|
|
||
| a_ep=\$(to_epoch \"\$GIT_AUTHOR_DATE\"); c_ep=\$(to_epoch \"\$GIT_COMMITTER_DATE\") | ||
| a_new=\$((a_ep + shift_secs)); c_new=\$((c_ep + shift_secs)) | ||
| export GIT_AUTHOR_DATE=\"\$a_new \$tz\" | ||
| export GIT_COMMITTER_DATE=\"\$c_new \$tz\" | ||
| " -- --branches --tags >/dev/null | ||
| echo "✓ Done. Remember to: git push --force-with-lease" | ||
| } | ||
|
|
||
| # Function: Main function to control the script flow | ||
| main() { | ||
| if [ $# -ne 1 ]; then | ||
| echo "Error: No arguments provided." | ||
| echo "Usage: commit_date_modifier.sh DD-MM-YYYY" | ||
| echo " The date is in the format day-month-year." | ||
| echo "Example: commit_date_modifier.sh 25-12-2022" | ||
| exit 1 | ||
| fi | ||
| move_history() { | ||
| local target="$1" | ||
|
|
||
| local hours_list="" | ||
| if [[ "$target" == "day" ]]; then | ||
| hours_list="$(expand_windows_to_hours "$DAY_WINDOW")" | ||
| echo "Moving ALL commits into DAY hours [${DAY_WINDOW}] in tz=${TZ_OFFSET}" | ||
| else | ||
| hours_list="$(expand_windows_to_hours "$NIGHT_WINDOW")" | ||
| echo "Moving ALL commits into NIGHT hours [${NIGHT_WINDOW}] in tz=${TZ_OFFSET}" | ||
| fi | ||
|
|
||
| # Pack the hours list into a bash array initializer string | ||
| local hours_csv="${hours_list// /,}" | ||
|
|
||
| git filter-branch -f --tag-name-filter cat --env-filter " | ||
| tz='${TZ_OFFSET}' | ||
| tz_secs=$(tz_to_seconds "${TZ_OFFSET}") | ||
| " -- --branches --tags >/dev/null 2>&1 && true | ||
|
|
||
| # We need the tz_to_seconds helper inside the filter; inject it plus logic: | ||
| git filter-branch -f --tag-name-filter cat --env-filter " | ||
| tz='${TZ_OFFSET}' | ||
| tz_to_seconds() { | ||
| local tz=\"\$1\" sign hh mm secs | ||
| [[ \"\$tz\" =~ ^[+-][0-9]{4}\$ ]] || { echo 'bad tz' >&2; exit 1; } | ||
| sign=\"\${tz:0:1}\"; hh=\$((10#\${tz:1:2})); mm=\$((10#\${tz:3:2})) | ||
| secs=\$((hh*3600 + mm*60)); [[ \"\$sign\" == '-' ]] && secs=\$((-secs)) | ||
| echo \"\$secs\" | ||
| } | ||
|
|
||
| tz_secs=\$(tz_to_seconds \"\$tz\") | ||
| to_epoch() { $DATE_BIN -d \"\$1\" +%s; } | ||
| from_local_YmdHMS_to_epoch() { # args: Y M D H M S, interpret as LOCAL (tz offset), return UTC epoch | ||
| local Y=\"\$1\" Mo=\"\$2\" D=\"\$3\" H=\"\$4\" Mi=\"\$5\" S=\"\$6\" | ||
| $DATE_BIN -d \"\${Y}-\${Mo}-\${D} \${H}:\${Mi}:\${S} UTC\" +%s | ||
| } | ||
|
Comment on lines
+249
to
+253
|
||
|
|
||
| # Pick a random allowed hour (uniform) | ||
| pick_hour() { | ||
| local IFS=','; local raw='${hours_csv}' | ||
| read -ra H <<< \"\$raw\" | ||
| local count=\${#H[@]} | ||
| local idx=\$((RANDOM % count)) | ||
| echo \${H[\$idx]} | ||
| } | ||
|
|
||
| # For each commit: | ||
| a_ep=\$(to_epoch \"\$GIT_AUTHOR_DATE\") | ||
| c_ep=\$(to_epoch \"\$GIT_COMMITTER_DATE\") | ||
|
|
||
| # Convert to 'local' by applying tz offset (so windows are in given tz) | ||
| a_local=\$((a_ep + tz_secs)) | ||
| c_local=\$((c_ep + tz_secs)) | ||
|
|
||
| # Extract local Y-m-d; choose random HH:MM:SS in allowed window | ||
| Y=\$($DATE_BIN -u -d @\$a_local +%Y) | ||
| Mo=\$($DATE_BIN -u -d @\$a_local +%m) | ||
| D=\$($DATE_BIN -u -d @\$a_local +%d) | ||
|
|
||
| H=\$(pick_hour) | ||
| Mi=\$((RANDOM % 60)) | ||
| S=\$((RANDOM % 60)) | ||
|
|
||
| new_local_epoch=\$($DATE_BIN -u -d \"\${Y}-\${Mo}-\${D} \${H}:\${Mi}:\${S}\" +%s) | ||
| a_new=\$((new_local_epoch - tz_secs)) | ||
|
|
||
| # Mirror author -> committer with same new time on that commit's day | ||
| Yc=\$($DATE_BIN -u -d @\$c_local +%Y) | ||
| Moc=\$($DATE_BIN -u -d @\$c_local +%m) | ||
| Dc=\$($DATE_BIN -u -d @\$c_local +%d) | ||
| new_local_epoch_c=\$($DATE_BIN -u -d \"\${Yc}-\${Moc}-\${Dc} \${H}:\${Mi}:\${S}\" +%s) | ||
| c_new=\$((new_local_epoch_c - tz_secs)) | ||
|
|
||
| export GIT_AUTHOR_DATE=\"\$a_new \$tz\" | ||
| export GIT_COMMITTER_DATE=\"\$c_new \$tz\" | ||
| " -- --branches --tags >/dev/null | ||
|
|
||
| local date="$1" | ||
| validate_date "$date" | ||
| IFS='-' read -r day month year <<< "$date" | ||
| local day_string | ||
| local month_string | ||
| local time_string | ||
| day_string=$(day_string_converter "$day" "$month" "$year") | ||
| month_string=$(month_string_converter "$month") | ||
| time_string=$(random_time) | ||
|
|
||
| GIT_COMMITTER_DATE="$day_string $month_string $day $time_string $year +0100" git commit --amend --no-edit --date "$day_string $month_string $day $time_string $year +0100" | ||
| echo "Commit date modified to $day_string $month_string $day $time_string $year" | ||
| echo "✓ Done. Remember to: git push --force-with-lease" | ||
| } | ||
|
|
||
| main "$@" | ||
| # ---------- Dispatch ---------- | ||
|
|
||
| case "$MODE" in | ||
| amend-latest) | ||
| [[ -n "$DATE_DDMMYYYY" ]] || { echo "amend-latest requires --date DD-MM-YYYY"; exit 1; } | ||
| amend_latest | ||
| ;; | ||
| shift) | ||
| shift_history | ||
| ;; | ||
| move) | ||
| [[ "$MOVE_TO" =~ ^(day|night)$ ]] || { echo "move requires --to day|night"; exit 1; } | ||
| move_history "$MOVE_TO" | ||
| ;; | ||
| *) | ||
| echo "Unknown mode: $MODE" | ||
| print_help | ||
| exit 1 | ||
| ;; | ||
| esac | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script filename in the comment doesn't match the actual filename 'change_commit_date.sh'. This should be updated to reflect the correct filename.