Skip to content

Commit 51d061d

Browse files
authored
move history preservers relative order of commits
1 parent f4d85ff commit 51d061d

File tree

1 file changed

+134
-72
lines changed

1 file changed

+134
-72
lines changed

src/change_commit_date.sh

Lines changed: 134 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -214,85 +214,147 @@ shift_history() {
214214
echo "✓ Done. Remember to: git push --force-with-lease"
215215
}
216216

217+
parse_windows_to_intervals() {
218+
local spec="$1"
219+
WIN_STARTS=()
220+
WIN_ENDS=()
221+
IFS=',' read -ra PARTS <<< "$spec"
222+
for part in "${PARTS[@]}"; do
223+
[[ "$part" =~ ^([0-1][0-9]|2[0-3])\-([0-1][0-9]|2[0-3])$ ]] \
224+
|| { echo "Invalid window segment '$part' (use HH-HH)"; exit 1; }
225+
local s="${BASH_REMATCH[1]}" e="${BASH_REMATCH[2]}"
226+
local ssec=$((10#$s * 3600))
227+
local esec=$(( (10#$e + 1) * 3600 )) # end is exclusive
228+
if (( esec > 86400 )); then esec=86400; fi
229+
if (( 10#$s <= 10#$e )); then
230+
WIN_STARTS+=("$ssec"); WIN_ENDS+=("$esec")
231+
else
232+
# wrap across midnight, split into two intervals
233+
WIN_STARTS+=("$ssec"); WIN_ENDS+=("86400")
234+
WIN_STARTS+=("0"); WIN_ENDS+=("$esec")
235+
fi
236+
done
237+
# sort by start (tiny N, simple insertion sort)
238+
local i j keyS keyE
239+
for ((i=1; i<${#WIN_STARTS[@]}; i++)); do
240+
keyS=${WIN_STARTS[i]}; keyE=${WIN_ENDS[i]}; j=$((i-1))
241+
while (( j>=0 && WIN_STARTS[j] > keyS )); do
242+
WIN_STARTS[j+1]=${WIN_STARTS[j]}; WIN_ENDS[j+1]=${WIN_ENDS[j]}; j=$((j-1))
243+
done
244+
WIN_STARTS[j+1]=$keyS; WIN_ENDS[j+1]=$keyE
245+
done
246+
# merge overlaps / adjacents
247+
local mergedS=() mergedE=()
248+
for ((i=0;i<${#WIN_STARTS[@]};i++)); do
249+
if (( ${#mergedS[@]}==0 )); then
250+
mergedS+=("${WIN_STARTS[i]}"); mergedE+=("${WIN_ENDS[i]}")
251+
else
252+
local last=$(( ${#mergedS[@]} - 1 ))
253+
if (( WIN_STARTS[i] <= mergedE[last] )); then
254+
# extend
255+
if (( WIN_ENDS[i] > mergedE[last] )); then mergedE[last]=${WIN_ENDS[i]}; fi
256+
else
257+
mergedS+=("${WIN_STARTS[i]}"); mergedE+=("${WIN_ENDS[i]}")
258+
fi
259+
fi
260+
done
261+
WIN_STARTS=("${mergedS[@]}"); WIN_ENDS=("${mergedE[@]}")
262+
# total allowed seconds
263+
ALLOWED_LEN=0
264+
for ((i=0;i<${#WIN_STARTS[@]};i++)); do
265+
ALLOWED_LEN=$((ALLOWED_LEN + WIN_ENDS[i] - WIN_STARTS[i]))
266+
done
267+
(( ALLOWED_LEN > 0 )) || { echo "Window has zero length"; exit 1; }
268+
}
269+
270+
# Map a position p in [0, ALLOWED_LEN-1] to absolute seconds since local midnight
271+
# inside the union of intervals.
272+
union_pos_to_seconds() {
273+
local p=$1
274+
for ((i=0;i<${#WIN_STARTS[@]};i++)); do
275+
local seglen=$(( WIN_ENDS[i] - WIN_STARTS[i] ))
276+
if (( p < seglen )); then
277+
echo $(( WIN_STARTS[i] + p ))
278+
return
279+
fi
280+
p=$(( p - seglen ))
281+
done
282+
echo $(( WIN_ENDS[${#WIN_ENDS[@]}-1] - 1 ))
283+
}
284+
285+
# --- move_history(): preserves per-day order & keeps same local day ---
217286
move_history() {
218287
local target="$1"
219-
220-
local hours_list=""
288+
local window_spec
221289
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}"
290+
window_spec="$DAY_WINDOW"
291+
echo "Moving ALL commits into DAY hours [$DAY_WINDOW] in tz=${TZ_OFFSET}"
224292
else
225-
hours_list="$(expand_windows_to_hours "$NIGHT_WINDOW")"
226-
echo "Moving ALL commits into NIGHT hours [${NIGHT_WINDOW}] in tz=${TZ_OFFSET}"
293+
window_spec="$NIGHT_WINDOW"
294+
echo "Moving ALL commits into NIGHT hours [$NIGHT_WINDOW] in tz=${TZ_OFFSET}"
227295
fi
228296

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}")
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
297+
parse_windows_to_intervals "$window_spec"
298+
299+
# 1) First pass: count commits per local day (by committer date)
300+
declare -A DAY_COUNT=()
301+
local tz_secs
302+
tz_secs="$(tz_to_seconds "$TZ_OFFSET")"
303+
304+
while IFS=' ' read -r sha ct; do
305+
[[ -n "$sha" ]] || continue
306+
# local day key in target tz
307+
local local_ep=$(( ct + tz_secs ))
308+
local day
309+
day="$($DATE_BIN -u -d "@$local_ep" +%Y-%m-%d)"
310+
DAY_COUNT["$day"]=$(( ${DAY_COUNT["$day"]:-0} + 1 ))
311+
done < <(git log --all --reverse --pretty=format:'%H %ct')
312+
313+
# 2) Second pass: assign evenly spaced times inside window per day, preserving order
314+
local STATE
315+
STATE="$(mktemp -d)"
316+
trap 'rm -rf "$STATE"' EXIT
317+
mkdir -p "$STATE/map"
318+
319+
declare -A DAY_INDEX=()
320+
while IFS=' ' read -r sha ct; do
321+
[[ -n "$sha" ]] || continue
322+
local local_ep=$(( ct + tz_secs ))
323+
local day
324+
day="$($DATE_BIN -u -d "@$local_ep" +%Y-%m-%d)"
325+
local idx=${DAY_INDEX["$day"]:-0}
326+
local n=${DAY_COUNT["$day"]}
327+
328+
# Even spacing strictly preserves order; places commits inside the window.
329+
# pos = floor( (idx+1) * (ALLOWED_LEN-1) / (n+1) )
330+
local pos=$(( ((idx + 1) * (ALLOWED_LEN - 1)) / (n + 1) ))
331+
(( pos < 0 )) && pos=0
332+
(( pos >= ALLOWED_LEN )) && pos=$((ALLOWED_LEN - 1))
333+
334+
local sec_in_day
335+
sec_in_day="$(union_pos_to_seconds "$pos")"
336+
337+
# Build final epoch: interpret as local time in TZ_OFFSET, then convert to UTC epoch
338+
local new_epoch
339+
new_epoch="$($DATE_BIN -d "${day} 00:00:00 ${TZ_OFFSET}" +%s)"
340+
new_epoch=$(( new_epoch + sec_in_day ))
341+
342+
printf '%s' "$new_epoch" > "$STATE/map/$sha"
343+
DAY_INDEX["$day"]=$(( idx + 1 ))
344+
done < <(git log --all --reverse --pretty=format:'%H %ct')
345+
346+
# 3) Apply mapping in one pass
347+
MAPPING_DIR="$STATE/map" TZ_OFFSET_APPLY="$TZ_OFFSET" \
348+
git filter-branch -f --tag-name-filter cat --env-filter '
349+
if [ -f "$MAPPING_DIR/$GIT_COMMIT" ]; then
350+
new_epoch=$(cat "$MAPPING_DIR/$GIT_COMMIT")
351+
export GIT_AUTHOR_DATE="$new_epoch $TZ_OFFSET_APPLY"
352+
export GIT_COMMITTER_DATE="$new_epoch $TZ_OFFSET_APPLY"
353+
fi
354+
' -- --branches --tags >/dev/null
294355

295-
echo "✓ Done. Remember to: git push --force-with-lease"
356+
echo "✓ Done. Per-day order preserved; commits stay on the same local day."
357+
echo " Next: git push --force-with-lease"
296358
}
297359

298360
# ---------- Dispatch ----------

0 commit comments

Comments
 (0)