157 lines
5 KiB
Bash
Executable file
157 lines
5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# git-cadence-commit.sh — optional local WIP commit on a soft cadence
|
||
#
|
||
# Model (koopa-admin-log / francpaysan-admin-log histories + ops preference):
|
||
# - message: area: short description
|
||
# - size: small logical steps; **prefer more commits over few**
|
||
# - interval: soft range **7–13 minutes** while work is open (not a strict timer;
|
||
# denser bursts of 0–2 min are fine for tiny follow-ups)
|
||
# - push: **only at the end of a work session** (CADENCE_PUSH=1), never by default
|
||
#
|
||
# Usage:
|
||
# ./scripts/git-cadence-commit-push.sh # commit only if dirty
|
||
# ./scripts/git-cadence-commit-push.sh /path/to/repo
|
||
# CADENCE_MSG='monitoring: wip ladder' ./scripts/git-cadence-commit-push.sh
|
||
# CADENCE_DRY=1 ./scripts/git-cadence-commit-push.sh
|
||
# CADENCE_PUSH=1 ./scripts/git-cadence-commit-push.sh # explicit push (end of work)
|
||
# CADENCE_MIN_AGE_S=420 # skip if last commit younger than this (default 0)
|
||
#
|
||
# Soft timer (7–13 min, not fixed): fire often, skip until age ∈ [7,13] min target.
|
||
# # agent/cron every 7m — script enforces random target in range when CADENCE_JITTER=1
|
||
# CADENCE_JITTER=1 ./scripts/git-cadence-commit-push.sh
|
||
#
|
||
set -euo pipefail
|
||
|
||
REPO="${1:-}"
|
||
if [ -z "$REPO" ]; then
|
||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||
fi
|
||
REPO="$(cd "$REPO" && pwd)"
|
||
cd "$REPO"
|
||
|
||
CADENCE_DRY="${CADENCE_DRY:-0}"
|
||
# Push only when explicitly requested (end of session) — default OFF
|
||
CADENCE_PUSH="${CADENCE_PUSH:-0}"
|
||
# Soft range 7–13 minutes (seconds)
|
||
CADENCE_RANGE_MIN_S="${CADENCE_RANGE_MIN_S:-420}" # 7 min
|
||
CADENCE_RANGE_MAX_S="${CADENCE_RANGE_MAX_S:-780}" # 13 min
|
||
CADENCE_JITTER="${CADENCE_JITTER:-0}"
|
||
CADENCE_MIN_AGE_S="${CADENCE_MIN_AGE_S:-0}"
|
||
|
||
SKIP_GLOBS=(
|
||
'secrets.env'
|
||
'**/secrets.env'
|
||
'**/*password*'
|
||
'**/*secret*'
|
||
'**/*.pem'
|
||
'**/*token*'
|
||
)
|
||
|
||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||
echo "not a git repo: $REPO" >&2
|
||
exit 1
|
||
fi
|
||
|
||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||
remote=$(git remote 2>/dev/null | head -1 || true)
|
||
: "${remote:=origin}"
|
||
|
||
# Jitter: only proceed if last commit is at least a random target in [7,13] min
|
||
if [ "$CADENCE_JITTER" = "1" ]; then
|
||
last=$(git log -1 --format=%ct 2>/dev/null || echo 0)
|
||
now=$(date +%s)
|
||
age=$((now - last))
|
||
# stable-ish target per hour so repeated 7m fires don't all commit
|
||
target=$(python3 -c "
|
||
import random, time
|
||
random.seed(int(time.time()) // 60) # changes every minute
|
||
print(random.randint(${CADENCE_RANGE_MIN_S}, ${CADENCE_RANGE_MAX_S}))
|
||
" 2>/dev/null || echo 600)
|
||
if [ "$age" -lt "$target" ]; then
|
||
echo "jitter wait · age=${age}s < target=${target}s (range ${CADENCE_RANGE_MIN_S}-${CADENCE_RANGE_MAX_S}s) · $REPO"
|
||
exit 0
|
||
fi
|
||
elif [ "${CADENCE_MIN_AGE_S}" -gt 0 ] 2>/dev/null; then
|
||
last=$(git log -1 --format=%ct 2>/dev/null || echo 0)
|
||
now=$(date +%s)
|
||
age=$((now - last))
|
||
if [ "$age" -lt "$CADENCE_MIN_AGE_S" ]; then
|
||
echo "min-age wait · age=${age}s < ${CADENCE_MIN_AGE_S}s · $REPO"
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
do_push() {
|
||
local ahead
|
||
ahead=$(git rev-list --count "${remote}/${branch}..HEAD" 2>/dev/null || echo 0)
|
||
if [ "${ahead:-0}" -le 0 ]; then
|
||
echo "nothing to push · $REPO · $branch"
|
||
return 0
|
||
fi
|
||
echo "ahead of ${remote}/${branch} by $ahead"
|
||
if [ "$CADENCE_DRY" = "1" ]; then
|
||
echo "DRY: git push $remote $branch"
|
||
return 0
|
||
fi
|
||
if [ "$CADENCE_PUSH" != "1" ]; then
|
||
echo "skip push (CADENCE_PUSH!=1 — push only at end of work)"
|
||
return 0
|
||
fi
|
||
git push "$remote" "$branch" || {
|
||
echo "WARN: push failed for $REPO ($remote $branch)" >&2
|
||
return 0
|
||
}
|
||
echo "pushed · $remote $branch"
|
||
}
|
||
|
||
# Clean working tree → maybe push only
|
||
if git diff --quiet && git diff --cached --quiet \
|
||
&& [ -z "$(git ls-files --others --exclude-standard)" ]; then
|
||
echo "clean · $REPO · $branch (nothing to commit)"
|
||
do_push
|
||
exit 0
|
||
fi
|
||
|
||
git add -A
|
||
for g in "${SKIP_GLOBS[@]}"; do
|
||
git reset -q -- "$g" 2>/dev/null || true
|
||
done
|
||
while IFS= read -r f; do
|
||
case "$f" in
|
||
*secret*|*password*|*token*|secrets.env)
|
||
git reset -q -- "$f" 2>/dev/null || true
|
||
;;
|
||
esac
|
||
done < <(git diff --cached --name-only 2>/dev/null || true)
|
||
|
||
if git diff --cached --quiet; then
|
||
echo "only secret/skip paths dirty — not committing · $REPO"
|
||
exit 0
|
||
fi
|
||
|
||
msg="${CADENCE_MSG:-}"
|
||
if [ -z "$msg" ]; then
|
||
files=$(git diff --cached --name-only | head -20)
|
||
area="wip"
|
||
case "$files" in
|
||
*taler-monitoring*|*scripts/taler-monitoring*) area="monitoring" ;;
|
||
*landing*|*landings*|*configs/*landing*) area="landing" ;;
|
||
*docs*|*README*|*TODO*|*NOTES*) area="docs" ;;
|
||
*caddy*) area="caddy" ;;
|
||
*scripts*) area="scripts" ;;
|
||
esac
|
||
n=$(git diff --cached --name-only | wc -l | tr -d ' ')
|
||
sample=$(git diff --cached --name-only | head -1 | xargs -r basename)
|
||
msg="${area}: cadence ${n} file(s) · ${sample}"
|
||
fi
|
||
|
||
if [ "$CADENCE_DRY" = "1" ]; then
|
||
echo "DRY commit: $msg"
|
||
git diff --cached --stat
|
||
[ "$CADENCE_PUSH" = "1" ] && echo "DRY push: $remote $branch" || echo "DRY: no push"
|
||
exit 0
|
||
fi
|
||
|
||
git commit -m "$msg"
|
||
echo "committed · $msg"
|
||
do_push
|