From e838aaf737283219e7c9b37a377b902f0811e0cf Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 30 Aug 2026 08:50:32 -0400 Subject: [PATCH] Say which wj commits belong on main The scripts could move a commit to main but nothing said which commits should go, and that decision does not survive being deferred: months later it is a list of subject lines and a guess. So it is recorded at commit time, as a trailer: Scope: product the rest of the world should have it Scope: site this site alone, keep it here tools/wj-status.sh reads them and prints main..wj in three groups, with the promote command for the ones marked product. The path fallback is deliberately weak. 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 what differs between sites. So "touches only site_imports" is the one confident site verdict paths can give, and everything else is reported UNMARKED rather than guessed into a bucket. A tool that quietly sorted commits by directory would be wrong often enough to stop being read. The unmarked message says which way to lean, because the failure mode here is not promoting something site-specific by accident - the ADR-015 gate catches that on the way in. It is product work sitting on wj forever because nobody decided, which is how the wilm branch became a port rather than a merge. Scope: product (cherry picked from commit 0400a587869eba4022c72212778da14b680ec4ed) --- tools/wj-status.sh | 133 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100755 tools/wj-status.sh diff --git a/tools/wj-status.sh b/tools/wj-status.sh new file mode 100755 index 00000000..d133cd53 --- /dev/null +++ b/tools/wj-status.sh @@ -0,0 +1,133 @@ +#!/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