Privacy-Period-Tracker/scripts/commit-mine.sh

121 lines
4.3 KiB
Bash
Raw Normal View History

chore: adopt the project template and add the Kotlin/Compose skeleton Period was a bare directory holding one 2,527-line specification, with no git repository, no tracker and no documentation convention. This is the adoption from Projects/Template/START-HERE-New-Project.md, plus a project that compiles so the hooks and future guards have something real to run against. Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the capitalised Docs/ is gone. Every scaffolded document was filled in for Period. docs/OPERATIONS.md deleted — an offline app is not a deployed service. DOC_TRUST_MAP.md written last, describing what is actually here, including what this project deliberately does not have. Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm") and cannot see the Android SDK, so the engine is testable without an emulator — 17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51. BaselinePredictionEngine is a robust-median prototype and explicitly not the product; it exists so Batch 02's replacement can be shown to be better rather than merely different. Versions verified against their official sources today rather than inherited from the specification's own numbers, which that document asks for: Kotlin 2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt 2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk stays 36, Play's floor from 2026-08-31, and the difference is deliberate. Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md. Three hooks in .githooks/, with pre-commit adapted to Gradle. closes #1 closes #2
2026-08-18 02:16:47 -05:00
#!/usr/bin/env bash
#
# Commit only the paths you name, when something else is also writing the tree.
#
# ## The failure this catches
#
# Two agents, or an agent and a person, sharing one checkout share one **git
# index**. Staging is global state, and the window between staging a change and
# committing it is however long it takes to write the commit message. Anything
# that runs `git add -A` inside that window takes your files with it.
#
# Two real instances, one afternoon, one repository — both while the rule
# "stage by explicit path, and re-check the index immediately before committing"
# was being followed to the letter:
#
# - A commit about reviving a game carried two unrelated documentation edits.
# - A commit about art carried an entire renderer fix, a new class, its test
# and two documents. Its message had no `closes #N`, so the issue that work
# finished stayed open and had to be closed by hand afterwards.
#
# Nothing was lost either time. The attribution was wrong, and once the tracker
# was wrong with it. **The rule is not the fix, because the danger is the
# window** — so this closes the window instead: the message is written first and
# passed in, staging and scanning and committing happen back to back, and the
# commit itself names its paths.
#
# ## Why a pathspec commit rather than unstaging theirs
#
# `git commit -- <paths>` takes the working-tree content of exactly those paths
# and ignores the rest of the index. The other writer's staged work is neither
# swept into your commit nor removed from their index, so there is no step here
# that can break *their* commit either. What they have staged is reported, so you
# know somebody else is mid-flight before you add to the race.
#
# ## Usage
#
# bash scripts/commit-mine.sh <message-file> <path> [path…]
# bash scripts/commit-mine.sh --push <message-file> <path> [path…]
#
# ## Exit codes
#
# 0 committed (and pushed, with `--push`)
# 1 the secret scan objected, or a push left something unpushed
# 2 nothing was committed: bad arguments, or a path that does not exist.
# **Two is not a pass**
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SECRETS="$HERE/secrets.sh"
PUSH="no"
if [ "${1:-}" = "--push" ]; then PUSH="yes"; shift; fi
if [ "$#" -lt 2 ]; then
sed -n '2,45p' "$0" >&2
exit 2
fi
MESSAGE_FILE="$1"; shift
[ -f "$MESSAGE_FILE" ] || { echo "commit-mine: no message file: $MESSAGE_FILE" >&2; exit 2; }
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
echo "commit-mine: not inside a git repository" >&2
exit 2
}
for path in "$@"; do
[ -e "$path" ] || { echo "commit-mine: no such path: $path" >&2; exit 2; }
done
# Whatever anyone else has in flight. Not an error and not touched — but you
# should see it before committing into the same index.
OTHERS="$(git diff --cached --name-only | grep -vxF -f <(printf '%s\n' "$@") || true)"
if [ -n "$OTHERS" ]; then
echo "commit-mine: NOTE — the index also holds work that is not yours:"
printf ' %s\n' $OTHERS
echo " (left staged, and left out of this commit)"
echo
fi
# Staged only so the scanner sees exactly these paths, and only for as long as
# the scan takes. A credential is the one mistake here that cannot be undone by
# a later commit.
git add -- "$@"
if [ -x "$SECRETS" ] || [ -f "$SECRETS" ]; then
set +e
bash "$SECRETS"
SCAN=$?
set -e
# 2 means it scanned nothing, which the convention in this template treats as
# a failure rather than a pass — a scanner that did not run has not cleared
# anything.
if [ "$SCAN" -ne 0 ]; then
git restore --staged -- "$@"
echo "commit-mine: secret scan exited $SCAN; nothing committed" >&2
exit 1
fi
else
echo "commit-mine: WARNING — $SECRETS not found, committing unscanned" >&2
fi
git commit -F "$MESSAGE_FILE" -- "$@"
echo
echo "commit-mine: committed"
git show --stat --oneline HEAD | head -20
if [ "$PUSH" = "yes" ]; then
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
git push origin "$BRANCH"
# Finished work is pushed work. A local commit is invisible to everything that
# reports on the project, so the push is verified rather than assumed.
UNPUSHED="$(git log --oneline "origin/$BRANCH..HEAD" 2>/dev/null || true)"
if [ -n "$UNPUSHED" ]; then
echo "commit-mine: WARNING — still unpushed after push:" >&2
echo "$UNPUSHED" >&2
exit 1
fi
echo "commit-mine: pushed; origin/$BRANCH..HEAD is empty"
fi