#!/usr/bin/env bash # # What is on wj that is not on main, and where each commit belongs. # # tools/wj-status.sh classify everything unpromoted # tools/wj-status.sh --product print just the promotable SHAs # # The second form is meant to be read by a person and then pasted, oldest # first: # # tools/promote-to-main.sh $(tools/wj-status.sh --product) # # HOW A COMMIT IS CLASSIFIED # # By a trailer you write at commit time: # # Scope: product belongs on main, the rest of the world should have it # Scope: site belongs to this site alone, keep it on wj # # Intent is the only thing that can actually decide this, and you have it when # you write the commit, not months later when you are staring at a list of # subject lines trying to remember. # # Where there is no trailer this falls back to paths, but the fallback is # deliberately weak, and says so. Under ADR-015 the only site-specific home in # the tree is scripts/site_imports// - everything else is meant to be # product with settings and seed data carrying the differences. So "touches # only site_imports" is the one confident site verdict available, and anything # else is reported as UNMARKED rather than guessed into a bucket. set -euo pipefail cd "$(git rev-parse --show-toplevel)" MODE="${1:-report}" if ! git show-ref --verify --quiet refs/heads/main; then echo "ERROR: no main branch here." >&2 exit 1 fi RANGE="main..wj" if ! git rev-parse --verify --quiet refs/heads/wj >/dev/null; then echo "ERROR: no wj branch here." >&2 exit 1 fi COMMITS="$(git rev-list --reverse "$RANGE")" if [ -z "$COMMITS" ]; then echo "Nothing on wj that main does not have." exit 0 fi classify() { local sha="$1" local scope scope="$(git log -1 --format='%(trailers:key=Scope,valueonly)' "$sha" | tr -d '[:space:]')" case "$(printf '%s' "$scope" | tr '[:upper:]' '[:lower:]')" in product) echo product; return ;; site) echo site; return ;; esac # No trailer. The only confident verdict from paths alone. local files outside files="$(git show --pretty=format: --name-only "$sha" | sed '/^$/d')" outside="$(printf '%s\n' "$files" | grep -v '^scripts/site_imports/' || true)" if [ -n "$files" ] && [ -z "$outside" ]; then echo site-by-path else echo unmarked fi } if [ "$MODE" = "--product" ]; then for sha in $COMMITS; do if [ "$(classify "$sha")" = "product" ]; then printf '%s ' "$sha" fi done echo exit 0 fi product_shas="" unmarked_count=0 echo "On wj, not on main - oldest first" echo for sha in $COMMITS; do verdict="$(classify "$sha")" subject="$(git log -1 --format='%h %s' "$sha")" case "$verdict" in product) echo " [PRODUCT ] $subject" product_shas="$product_shas $sha" ;; site) echo " [SITE ] $subject" ;; site-by-path) echo " [site? ] $subject" echo " touches only scripts/site_imports/ - looks site-only, not marked" ;; *) echo " [UNMARKED] $subject" unmarked_count=$((unmarked_count + 1)) ;; esac done echo if [ -n "$product_shas" ]; then echo "Marked for main:" echo " tools/promote-to-main.sh$product_shas" echo fi if [ "$unmarked_count" -gt 0 ]; then echo "$unmarked_count commit(s) carry no Scope trailer." echo echo "That is not a failure - it just means nobody decided yet. Under ADR-015" echo "most work is product: site differences are meant to live in settings and" echo "seed data, not in a branch. So an unmarked commit is more likely to" echo "belong on main than not, and leaving it unmarked is how it ends up" echo "stranded here." echo echo "Mark future ones as you go:" echo " git commit -m \"...\" -m \"Scope: product\"" echo echo "Or decide now and promote the ones that belong:" echo " tools/promote-to-main.sh --dry-run " fi