-
Notifications
You must be signed in to change notification settings - Fork 66
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||
|
Comment on lines
+232
to
+235
|
||||||||||
| git filter-branch -f --tag-name-filter cat --env-filter " | |
| tz='${TZ_OFFSET}' | |
| tz_secs=$(tz_to_seconds '${TZ_OFFSET}') # <- will be substituted by this script (not inside env-filter) | |
| " -- --branches --tags >/dev/null 2>&1 && true |
Copilot
AI
Sep 29, 2025
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 from_local_YmdHMS_to_epoch function is defined but never used in the script. This creates unnecessary complexity and should be removed.
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.