Skip to content

Commit a0a35bd

Browse files
authored
Refactor commit date modifier to support multiple modes
Updated the script to provide powerful Git commit-date tools including options to amend the latest commit date, shift all commits, and move commits into day or night hours. Enhanced usage examples and added timezone handling.
1 parent 80fc684 commit a0a35bd

File tree

1 file changed

+296
-63
lines changed

1 file changed

+296
-63
lines changed

src/change_commit_date.sh

Lines changed: 296 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,317 @@
11
#!/usr/bin/env bash
2+
set -euo pipefail
23

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+
# -----------------------------------------------------------------------------
841

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; }
1253
}
1354

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+
}
1966

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+
}
2572

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+
}
2792

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
2997
}
3098

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)
34117
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
36131
}
37132

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}"
42195
}
43196

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"
57215
}
58216

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
68294

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"
81296
}
82297

83-
main "$@"
298+
# ---------- Dispatch ----------
84299

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

Comments
 (0)