|
1 | 1 | #!/usr/bin/env bash |
| 2 | +set -euo pipefail |
2 | 3 |
|
3 | | -# Script Name: commit_date_modifier.sh |
4 | | -# Description: Modifies the date of the latest Git commit to a user-provided date |
5 | | -# Usage: commit_date_modifier.sh DD-MM-YYYY |
6 | | -# The date is in the format day-month-year |
7 | | -# Example: commit_date_modifier.sh 25-12-2022 |
| 4 | +# ----------------------------------------------------------------------------- |
| 5 | +# commit_date_tools.sh |
| 6 | +# |
| 7 | +# Powerful Git commit-date tools: |
| 8 | +# 1) amend-latest: set latest commit to a specific date/time |
| 9 | +# 2) shift: shift ALL commits by hours/days |
| 10 | +# 3) move: move ALL commits into day or night hours (randomized within window) |
| 11 | +# |
| 12 | +# Defaults: |
| 13 | +# - Timezone offset: +0200 (UTC+2) |
| 14 | +# - Day window: 09-18 (9am..6pm) |
| 15 | +# - Night window: 20-23,00-05 (8pm..11pm and midnight..5am) |
| 16 | +# |
| 17 | +# Requirements: git, GNU date (Linux 'date' or macOS 'gdate' from coreutils) |
| 18 | +# |
| 19 | +# Examples: |
| 20 | +# # 1) Set latest commit to specific day (random time) in UTC+2 |
| 21 | +# ./commit_date_tools.sh amend-latest --date 25-12-2022 |
| 22 | +# |
| 23 | +# # 1b) Set latest commit to specific day & time with custom tz |
| 24 | +# ./commit_date_tools.sh amend-latest --date 25-12-2022 --time 14:30 --tz +0530 |
| 25 | +# |
| 26 | +# # 2) Shift entire history forward 7 hours |
| 27 | +# ./commit_date_tools.sh shift --hours 7 |
| 28 | +# |
| 29 | +# # 2b) Shift entire history back 2 days and 3 hours, timezone +0200 |
| 30 | +# ./commit_date_tools.sh shift --days -2 --hours -3 --tz +0200 |
| 31 | +# |
| 32 | +# # 3) Move all commits into daytime (random hour in window) |
| 33 | +# ./commit_date_tools.sh move --to day |
| 34 | +# |
| 35 | +# # 3b) Move to night hours with custom windows and timezone |
| 36 | +# ./commit_date_tools.sh move --to night --night-window 21-23,00-04 --tz -0500 |
| 37 | +# |
| 38 | +# After rewriting history: |
| 39 | +# git push --force-with-lease |
| 40 | +# ----------------------------------------------------------------------------- |
8 | 41 |
|
9 | | -# Function: Returns absolute value of a number |
10 | | -abs() { |
11 | | - echo "${1#-}" |
| 42 | +# ---------- Utilities ---------- |
| 43 | + |
| 44 | +# Pick GNU date (Linux: date, macOS: gdate) |
| 45 | +DATE_BIN="$(command -v gdate || command -v date)" |
| 46 | +if ! "$DATE_BIN" -d "@0" +%s >/dev/null 2>&1; then |
| 47 | + echo "Error: GNU 'date' required. On macOS: brew install coreutils (use gdate)." >&2 |
| 48 | + exit 1 |
| 49 | +fi |
| 50 | + |
| 51 | +require_git_repo() { |
| 52 | + git rev-parse --git-dir >/dev/null 2>&1 || { echo "Not a git repo."; exit 1; } |
12 | 53 | } |
13 | 54 |
|
14 | | -# Function: Convert the input date into a weekday string |
15 | | -day_string_converter() { |
16 | | - local day=$1 |
17 | | - local month=$2 |
18 | | - local year=$3 |
| 55 | +# Parse "+HHMM" or "-HHMM" into seconds (supports half-hours like +0530) |
| 56 | +tz_to_seconds() { |
| 57 | + local tz="$1" sign hh mm secs |
| 58 | + [[ "$tz" =~ ^[+-][0-9]{4}$ ]] || { echo "Invalid tz offset '$tz' (use +HHMM/-HHMM)"; exit 1; } |
| 59 | + sign="${tz:0:1}" |
| 60 | + hh=$((10#${tz:1:2})) |
| 61 | + mm=$((10#${tz:3:2})) |
| 62 | + secs=$((hh*3600 + mm*60)) |
| 63 | + if [[ "$sign" == "-" ]]; then secs=$((-secs)); fi |
| 64 | + echo "$secs" |
| 65 | +} |
19 | 66 |
|
20 | | - # Zeller's Congruence algorithm for calculating the day of the week |
21 | | - local a=$(( (14 - month) / 12 )) |
22 | | - local y=$(( year - a )) |
23 | | - local m=$(( month + (12 * a) - 2 )) |
24 | | - local d=$(( (day + y + (y / 4) - (y / 100) + (y / 400) + ((31 * m) / 12)) % 7 )) |
| 67 | +# Random int in [min,max] inclusive |
| 68 | +rand_int() { |
| 69 | + local min=$1 max=$2 |
| 70 | + echo $(( min + RANDOM % (max - min + 1) )) |
| 71 | +} |
25 | 72 |
|
26 | | - local days=("Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat") |
| 73 | +# Build a space-separated list of allowed hours from a window spec like "09-18" or "20-23,00-05" |
| 74 | +expand_windows_to_hours() { |
| 75 | + local spec="$1" part start end h hours=() |
| 76 | + IFS=',' read -ra PARTS <<< "$spec" |
| 77 | + for part in "${PARTS[@]}"; do |
| 78 | + [[ "$part" =~ ^([0-1][0-9]|2[0-3])\-([0-1][0-9]|2[0-3])$ ]] \ |
| 79 | + || { echo "Invalid window '$part' (use HH-HH,HH-HH)"; exit 1; } |
| 80 | + start="${part%-*}"; end="${part#*-}" |
| 81 | + start=$((10#$start)); end=$((10#$end)) |
| 82 | + if (( start <= end )); then |
| 83 | + for ((h=start; h<=end; h++)); do hours+=("$h"); done |
| 84 | + else |
| 85 | + # Wrap around midnight (e.g., 20-05) |
| 86 | + for ((h=start; h<=23; h++)); do hours+=("$h"); done |
| 87 | + for ((h=0; h<=end; h++)); do hours+=("$h"); done |
| 88 | + fi |
| 89 | + done |
| 90 | + echo "${hours[*]}" |
| 91 | +} |
27 | 92 |
|
28 | | - echo "${days[d]}" |
| 93 | +# Turn Y-m-d H:M:S in the *local tz* into epoch seconds: |
| 94 | +ymd_hms_to_epoch_in_tz() { |
| 95 | + local y="$1" m="$2" d="$3" H="$4" M="$5" S="$6" tz="$7" |
| 96 | + "$DATE_BIN" -d "${y}-${m}-${d} ${H}:${M}:${S} ${tz}" +%s |
29 | 97 | } |
30 | 98 |
|
31 | | -# Function: Convert numeric month into a month string |
32 | | -month_string_converter() { |
33 | | - local month_strs=("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec") |
| 99 | +# ---------- Modes ---------- |
| 100 | + |
| 101 | +print_help() { |
| 102 | +cat <<'EOF' |
| 103 | +Usage: |
| 104 | + commit_date_tools.sh <mode> [options] |
| 105 | +
|
| 106 | +Modes: |
| 107 | + amend-latest Set the latest commit to a specific date/time. |
| 108 | + shift Shift ALL commits by hours/days. |
| 109 | + move Move ALL commits into day or night hours (randomized). |
| 110 | +
|
| 111 | +Common options: |
| 112 | + --tz <+HHMM|-HHMM> Timezone offset to use when writing dates (default +0200). |
| 113 | +
|
| 114 | +amend-latest options: |
| 115 | + --date DD-MM-YYYY Required (e.g., 25-12-2024) |
| 116 | + --time HH:MM Optional (random if omitted) |
34 | 117 |
|
35 | | - echo "${month_strs[$1-1]}" |
| 118 | +shift options: |
| 119 | + --hours N Integer hours to shift (can be negative) |
| 120 | + --days N Integer days to shift (can be negative) |
| 121 | +
|
| 122 | +move options: |
| 123 | + --to day|night Required |
| 124 | + --day-window HH-HH Default 09-18 |
| 125 | + --night-window HH-HH,HH-HH Default 20-23,00-05 |
| 126 | +
|
| 127 | +Notes: |
| 128 | + • This rewrites history (all branches & tags). Backup first. |
| 129 | + • After running: git push --force-with-lease |
| 130 | +EOF |
36 | 131 | } |
37 | 132 |
|
38 | | -# Function: Generate a random time string in the format HH:MM |
39 | | -random_time() { |
40 | | - printf -v time "%02d:%02d" $((RANDOM % 24)) $((RANDOM % 60)) |
41 | | - echo "$time" |
| 133 | +# ---------- Argument parsing ---------- |
| 134 | + |
| 135 | +MODE="${1:-}" |
| 136 | +[[ -z "${MODE}" ]] && { print_help; exit 1; } |
| 137 | +shift || true |
| 138 | + |
| 139 | +TZ_OFFSET="+0200" |
| 140 | +DATE_DDMMYYYY="" |
| 141 | +TIME_HHMM="" |
| 142 | +SHIFT_HOURS="0" |
| 143 | +SHIFT_DAYS="0" |
| 144 | +MOVE_TO="" |
| 145 | +DAY_WINDOW="09-18" |
| 146 | +NIGHT_WINDOW="20-23,00-05" |
| 147 | + |
| 148 | +while (( "$#" )); do |
| 149 | + case "$1" in |
| 150 | + --tz) TZ_OFFSET="${2:?}"; shift 2;; |
| 151 | + --date) DATE_DDMMYYYY="${2:?}"; shift 2;; |
| 152 | + --time) TIME_HHMM="${2:?}"; shift 2;; |
| 153 | + --hours) SHIFT_HOURS="${2:?}"; shift 2;; |
| 154 | + --days) SHIFT_DAYS="${2:?}"; shift 2;; |
| 155 | + --to) MOVE_TO="${2:?}"; shift 2;; |
| 156 | + --day-window) DAY_WINDOW="${2:?}"; shift 2;; |
| 157 | + --night-window) NIGHT_WINDOW="${2:?}"; shift 2;; |
| 158 | + -h|--help) print_help; exit 0;; |
| 159 | + *) echo "Unknown option: $1"; echo; print_help; exit 1;; |
| 160 | + esac |
| 161 | +done |
| 162 | + |
| 163 | +# ---------- Validations ---------- |
| 164 | + |
| 165 | +require_git_repo |
| 166 | +TZ_SECS="$(tz_to_seconds "$TZ_OFFSET")" |
| 167 | + |
| 168 | +# ---------- Implementations ---------- |
| 169 | + |
| 170 | +amend_latest() { |
| 171 | + [[ "$DATE_DDMMYYYY" =~ ^([0-2][0-9]|3[0-1])-([0][1-9]|1[0-2])-[0-9]{4}$ ]] \ |
| 172 | + || { echo "Invalid --date. Use DD-MM-YYYY"; exit 1; } |
| 173 | + IFS='-' read -r DD MM YYYY <<< "$DATE_DDMMYYYY" |
| 174 | + |
| 175 | + if [[ -z "$TIME_HHMM" ]]; then |
| 176 | + HH=$(rand_int 0 23) |
| 177 | + MMm=$(rand_int 0 59) |
| 178 | + printf -v TIME_HHMM "%02d:%02d" "$HH" "$MMm" |
| 179 | + else |
| 180 | + [[ "$TIME_HHMM" =~ ^([0-1][0-9]|2[0-3]):([0-5][0-9])$ ]] \ |
| 181 | + || { echo "Invalid --time. Use HH:MM"; exit 1; } |
| 182 | + fi |
| 183 | + |
| 184 | + HH="${TIME_HHMM%:*}" |
| 185 | + MI="${TIME_HHMM#*:}" |
| 186 | + SS="00" |
| 187 | + |
| 188 | + EPOCH="$("$DATE_BIN" -d "${YYYY}-${MM}-${DD} ${HH}:${MI}:${SS} ${TZ_OFFSET}" +%s)" |
| 189 | + |
| 190 | + GIT_AUTHOR_DATE="${EPOCH} ${TZ_OFFSET}" \ |
| 191 | + GIT_COMMITTER_DATE="${EPOCH} ${TZ_OFFSET}" \ |
| 192 | + git commit --amend --no-edit |
| 193 | + |
| 194 | + echo "✓ Amended latest commit date to ${YYYY}-${MM}-${DD} ${HH}:${MI}:${SS} ${TZ_OFFSET}" |
42 | 195 | } |
43 | 196 |
|
44 | | -# Function: Validate the input date format |
45 | | -validate_date() { |
46 | | - if [[ $1 =~ ^([1-9]|0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-([0-9]{4})$ ]]; then |
47 | | - IFS='-' read -r day month year <<< "$1" |
48 | | - # Check the date validity |
49 | | - if ! date -d"$month/$day/$year" >/dev/null 2>&1; then |
50 | | - echo "Invalid date: $1" |
51 | | - exit 1 |
52 | | - fi |
53 | | - else |
54 | | - echo "Incorrect date format: $1. Expected format: DD-MM-YYYY" |
55 | | - exit 1 |
56 | | - fi |
| 197 | +shift_history() { |
| 198 | + local shift_secs=$(( SHIFT_DAYS*86400 + SHIFT_HOURS*3600 )) |
| 199 | + echo "Shifting ALL commits by ${SHIFT_DAYS} day(s) and ${SHIFT_HOURS} hour(s) [${shift_secs}s]; tz=${TZ_OFFSET}" |
| 200 | + if [[ "$shift_secs" -eq 0 ]]; then |
| 201 | + echo "Nothing to do (shift is zero)."; exit 0 |
| 202 | + fi |
| 203 | + |
| 204 | + git filter-branch -f --tag-name-filter cat --env-filter " |
| 205 | + shift_secs=${shift_secs} |
| 206 | + tz='${TZ_OFFSET}' |
| 207 | + to_epoch() { $DATE_BIN -d \"\$1\" +%s; } |
| 208 | +
|
| 209 | + a_ep=\$(to_epoch \"\$GIT_AUTHOR_DATE\"); c_ep=\$(to_epoch \"\$GIT_COMMITTER_DATE\") |
| 210 | + a_new=\$((a_ep + shift_secs)); c_new=\$((c_ep + shift_secs)) |
| 211 | + export GIT_AUTHOR_DATE=\"\$a_new \$tz\" |
| 212 | + export GIT_COMMITTER_DATE=\"\$c_new \$tz\" |
| 213 | + " -- --branches --tags >/dev/null |
| 214 | + echo "✓ Done. Remember to: git push --force-with-lease" |
57 | 215 | } |
58 | 216 |
|
59 | | -# Function: Main function to control the script flow |
60 | | -main() { |
61 | | - if [ $# -ne 1 ]; then |
62 | | - echo "Error: No arguments provided." |
63 | | - echo "Usage: commit_date_modifier.sh DD-MM-YYYY" |
64 | | - echo " The date is in the format day-month-year." |
65 | | - echo "Example: commit_date_modifier.sh 25-12-2022" |
66 | | - exit 1 |
67 | | - fi |
| 217 | +move_history() { |
| 218 | + local target="$1" |
| 219 | + |
| 220 | + local hours_list="" |
| 221 | + if [[ "$target" == "day" ]]; then |
| 222 | + hours_list="$(expand_windows_to_hours "$DAY_WINDOW")" |
| 223 | + echo "Moving ALL commits into DAY hours [${DAY_WINDOW}] in tz=${TZ_OFFSET}" |
| 224 | + else |
| 225 | + hours_list="$(expand_windows_to_hours "$NIGHT_WINDOW")" |
| 226 | + echo "Moving ALL commits into NIGHT hours [${NIGHT_WINDOW}] in tz=${TZ_OFFSET}" |
| 227 | + fi |
| 228 | + |
| 229 | + # Pack the hours list into a bash array initializer string |
| 230 | + local hours_csv="${hours_list// /,}" |
| 231 | + |
| 232 | + git filter-branch -f --tag-name-filter cat --env-filter " |
| 233 | + tz='${TZ_OFFSET}' |
| 234 | + tz_secs=$(tz_to_seconds '${TZ_OFFSET}') # <- will be substituted by this script (not inside env-filter) |
| 235 | + " -- --branches --tags >/dev/null 2>&1 && true |
| 236 | + |
| 237 | + # We need the tz_to_seconds helper inside the filter; inject it plus logic: |
| 238 | + git filter-branch -f --tag-name-filter cat --env-filter " |
| 239 | + tz='${TZ_OFFSET}' |
| 240 | + tz_to_seconds() { |
| 241 | + local tz=\"\$1\" sign hh mm secs |
| 242 | + [[ \"\$tz\" =~ ^[+-][0-9]{4}\$ ]] || { echo 'bad tz' >&2; exit 1; } |
| 243 | + sign=\"\${tz:0:1}\"; hh=\$((10#\${tz:1:2})); mm=\$((10#\${tz:3:2})) |
| 244 | + secs=\$((hh*3600 + mm*60)); [[ \"\$sign\" == '-' ]] && secs=\$((-secs)) |
| 245 | + echo \"\$secs\" |
| 246 | + } |
| 247 | +
|
| 248 | + tz_secs=\$(tz_to_seconds \"\$tz\") |
| 249 | + to_epoch() { $DATE_BIN -d \"\$1\" +%s; } |
| 250 | + from_local_YmdHMS_to_epoch() { # args: Y M D H M S, interpret as LOCAL (tz offset), return UTC epoch |
| 251 | + local Y=\"\$1\" Mo=\"\$2\" D=\"\$3\" H=\"\$4\" Mi=\"\$5\" S=\"\$6\" |
| 252 | + $DATE_BIN -d \"\${Y}-\${Mo}-\${D} \${H}:\${Mi}:\${S} UTC\" +%s |
| 253 | + } |
| 254 | +
|
| 255 | + # Pick a random allowed hour (uniform) |
| 256 | + pick_hour() { |
| 257 | + local IFS=','; local raw='${hours_csv}' |
| 258 | + read -ra H <<< \"\$raw\" |
| 259 | + local count=\${#H[@]} |
| 260 | + local idx=\$((RANDOM % count)) |
| 261 | + echo \${H[\$idx]} |
| 262 | + } |
| 263 | +
|
| 264 | + # For each commit: |
| 265 | + a_ep=\$(to_epoch \"\$GIT_AUTHOR_DATE\") |
| 266 | + c_ep=\$(to_epoch \"\$GIT_COMMITTER_DATE\") |
| 267 | +
|
| 268 | + # Convert to 'local' by applying tz offset (so windows are in given tz) |
| 269 | + a_local=\$((a_ep + tz_secs)) |
| 270 | + c_local=\$((c_ep + tz_secs)) |
| 271 | +
|
| 272 | + # Extract local Y-m-d; choose random HH:MM:SS in allowed window |
| 273 | + Y=\$($DATE_BIN -u -d @\$a_local +%Y) |
| 274 | + Mo=\$($DATE_BIN -u -d @\$a_local +%m) |
| 275 | + D=\$($DATE_BIN -u -d @\$a_local +%d) |
| 276 | +
|
| 277 | + H=\$(pick_hour) |
| 278 | + Mi=\$((RANDOM % 60)) |
| 279 | + S=\$((RANDOM % 60)) |
| 280 | +
|
| 281 | + new_local_epoch=\$($DATE_BIN -u -d \"\${Y}-\${Mo}-\${D} \${H}:\${Mi}:\${S}\" +%s) |
| 282 | + a_new=\$((new_local_epoch - tz_secs)) |
| 283 | +
|
| 284 | + # Mirror author -> committer with same new time on that commit's day |
| 285 | + Yc=\$($DATE_BIN -u -d @\$c_local +%Y) |
| 286 | + Moc=\$($DATE_BIN -u -d @\$c_local +%m) |
| 287 | + Dc=\$($DATE_BIN -u -d @\$c_local +%d) |
| 288 | + new_local_epoch_c=\$($DATE_BIN -u -d \"\${Yc}-\${Moc}-\${Dc} \${H}:\${Mi}:\${S}\" +%s) |
| 289 | + c_new=\$((new_local_epoch_c - tz_secs)) |
| 290 | +
|
| 291 | + export GIT_AUTHOR_DATE=\"\$a_new \$tz\" |
| 292 | + export GIT_COMMITTER_DATE=\"\$c_new \$tz\" |
| 293 | + " -- --branches --tags >/dev/null |
68 | 294 |
|
69 | | - local date="$1" |
70 | | - validate_date "$date" |
71 | | - IFS='-' read -r day month year <<< "$date" |
72 | | - local day_string |
73 | | - local month_string |
74 | | - local time_string |
75 | | - day_string=$(day_string_converter "$day" "$month" "$year") |
76 | | - month_string=$(month_string_converter "$month") |
77 | | - time_string=$(random_time) |
78 | | - |
79 | | - 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" |
80 | | - echo "Commit date modified to $day_string $month_string $day $time_string $year" |
| 295 | + echo "✓ Done. Remember to: git push --force-with-lease" |
81 | 296 | } |
82 | 297 |
|
83 | | -main "$@" |
| 298 | +# ---------- Dispatch ---------- |
84 | 299 |
|
| 300 | +case "$MODE" in |
| 301 | + amend-latest) |
| 302 | + [[ -n "$DATE_DDMMYYYY" ]] || { echo "amend-latest requires --date DD-MM-YYYY"; exit 1; } |
| 303 | + amend_latest |
| 304 | + ;; |
| 305 | + shift) |
| 306 | + shift_history |
| 307 | + ;; |
| 308 | + move) |
| 309 | + [[ "$MOVE_TO" =~ ^(day|night)$ ]] || { echo "move requires --to day|night"; exit 1; } |
| 310 | + move_history "$MOVE_TO" |
| 311 | + ;; |
| 312 | + *) |
| 313 | + echo "Unknown mode: $MODE" |
| 314 | + print_help |
| 315 | + exit 1 |
| 316 | + ;; |
| 317 | +esac |
0 commit comments