From 17c0fd20f94619227d58d35db1a659734788ed6e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 19:18:14 +0400 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20sync-statements.sh=20=E2=80=94?= =?UTF-8?q?=20worktree=20=D1=85=D0=BE=D1=81=D1=82=D0=B0,=20issue,=20make?= =?UTF-8?q?=20check=20=D0=B8=20PR;=20--dry-run;=20=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=C2=AB=D1=81=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D0=B0=20=D1=8F=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=C2=BB=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- scripts/sync-statements.sh | 183 +++++++++++++++++++++++++++++++- scripts/test_sync_statements.sh | 43 ++++++++ 2 files changed, 223 insertions(+), 3 deletions(-) diff --git a/scripts/sync-statements.sh b/scripts/sync-statements.sh index 968a062..4d87595 100755 --- a/scripts/sync-statements.sh +++ b/scripts/sync-statements.sh @@ -13,7 +13,9 @@ # 20 — расхождение (прямая правка копии, чужая версия, нет копии, нет зависимости, код вне перечня) # --in-place разложить копию в рабочее дерево хоста (без git-операций); 1 — защиты (.gitignore хоста, пути для контура), # 20 — отказ (правка копии, чужая версия, нет зависимости, код вне перечня) -# --dry-run worktree хоста без issue и PR (режим пока не реализован); вместе с --in-place — код 2 +# (без режима) worktree хоста от --host-ref, issue, make check хоста, коммит и PR; 20 — отказ до worktree, 2 — копии нет +# на --host-ref или worktree уже есть, 0 — копия уже на --ref +# --dry-run то же до раскладки включительно, без issue, проверок, коммита и PR (worktree остаётся); с --in-place — код 2 # --host arm-core | tabular-parser | scan-gateway; по умолчанию — по origin хоста (arm-core, arm-tabular-parser, # arm-scan-gateway), иначе по имени каталога # --host-dir каталог хоста (корень репозитория) @@ -59,7 +61,7 @@ while [ $# -gt 0 ]; do --docs) [ $# -ge 2 ] || die "--docs требует значение" 2; DOCS=$2; shift ;; --core-dir) [ $# -ge 2 ] || die "--core-dir требует значение" 2; CORE_DIR=$2; shift ;; --wt-root) [ $# -ge 2 ] || die "--wt-root требует значение" 2; WT_ROOT=$2; shift ;; - -h|--help) sed -n "2,24p" "$0"; exit 0 ;; + -h|--help) sed -n "2,26p" "$0"; exit 0 ;; *) die "неизвестный аргумент $1" 2 ;; esac shift @@ -329,4 +331,179 @@ if [ "$MODE" = in-place ]; then exit "$EVAL_CODE" fi -die "режим не реализован" 1 +# --- обычный режим и --dry-run: worktree хоста, issue, проверки, PR ------------------------------------------------ +TARGET=$REF_SHA +T7=${TARGET:0:7} +case "$HOST_NAME" in + arm-core) GITEA_REPO=RSHB/arm-core ;; + tabular-parser) GITEA_REPO=RSHB/arm-tabular-parser ;; + scan-gateway) GITEA_REPO=RSHB/arm-scan-gateway ;; +esac + +# 1. хост +if ! git -C "$HOST_DIR" fetch -q origin 2>/dev/null; then + echo "sync-statements: fetch хоста не удался, работаю с локальным $HOST_REF" >&2 +fi +git -C "$HOST_DIR" rev-parse -q --verify "$HOST_REF^{commit}" >/dev/null || die "в хосте нет $HOST_REF" 2 + +# 2. состояние хоста на --host-ref +git -C "$HOST_DIR" rev-parse -q --verify "$HOST_REF:api/arm_statements" >/dev/null 2>&1 \ + || die "копии нет на $HOST_REF: подключение хоста — вручную, --in-place в worktree задачи подключения (docs/dev/checks.md)" 2 +HT="$TMPD/host-tree"; mkdir -p "$HT" +git -C "$HOST_DIR" archive "$HOST_REF" api | tar -x -C "$HT" +HLINES="" +if [ "$HOST_NAME" = arm-core ] && [ -f "$HT/api/rules/statement_lines.yaml" ]; then HLINES="$HT/api/rules/statement_lines.yaml"; fi +evaluate "$HT/api/arm_statements" "$HT/api/requirements.txt" "$HLINES" "$HOST_NAME" +if [ "$EVAL_CODE" = 20 ]; then + echo "statements-check $HOST_NAME: 20 — отказ: копия на $HOST_REF правлена, чужая или несовместима, worktree не создан" + exit 20 +fi +HOST_WARNS=$EVAL_WARNS +OLD=$(tr -d ' \t\r\n' < "$HT/api/arm_statements/VERSION") +OLD7=${OLD:0:7} + +# 3. зависимости и словарь имён против TARGET +refuse="" +git -C "$SRC" show "$TARGET:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml" +helper deps "$TMPD/ref-pyproject.toml" "$HT/api/requirements.txt" +if [ "$HELPER_RC" = 20 ]; then refuse=$HELPER_OUT; fi +if [ -n "$HLINES" ]; then + git -C "$SRC" show "$TARGET:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \ + || : > "$TMPD/ref-orders.yaml" + helper lines "$HLINES" "$TMPD/ref-orders.yaml" + if [ "$HELPER_RC" = 20 ]; then refuse="${refuse:+$refuse +}$HELPER_OUT"; fi +fi +if [ -n "$refuse" ]; then + echo "$refuse" + echo "statements-check $HOST_NAME: 20 — отказ: $REF несовместим с хостом, worktree не создан" + exit 20 +fi + +# 4. уже на TARGET +if [ "$OLD" = "$TARGET" ]; then + echo "копия уже на $T7, раскладывать нечего" + [ -z "$HOST_WARNS" ] || echo "предупреждение: $HOST_WARNS" + exit 0 +fi + +# 5. «сначала ядро» +CORE_WARN="" +if [ "$HOST_NAME" != arm-core ]; then + CORE_VER="" + if [ -d "$CORE_DIR" ]; then + git -C "$CORE_DIR" fetch -q origin 2>/dev/null || true + CORE_VER=$(git -C "$CORE_DIR" show origin/main:api/arm_statements/VERSION 2>/dev/null | tr -d ' \t\r\n' || true) + fi + if [ -z "$CORE_VER" ] || { [ "$CORE_VER" != "$TARGET" ] && ! git -C "$SRC" merge-base --is-ancestor "$TARGET" "$CORE_VER" 2>/dev/null; }; then + CORE_WARN="ядро (arm-core@origin/main) ещё не на $T7: по порядку выкладки сначала ядро" + echo "предупреждение: $CORE_WARN" + fi +fi + +# 6. worktree хоста +TMP_BRANCH="tmp/statements-sync-$T7" +WT="$WT_ROOT/statements-sync-$T7/$HOST_NAME" +[ ! -e "$WT" ] || die "worktree уже существует: $WT — завершите или удалите предыдущую синхронизацию" 2 +if git -C "$HOST_DIR" rev-parse -q --verify "$TMP_BRANCH" >/dev/null; then + die "ветка $TMP_BRANCH уже существует в $HOST_NAME — завершите или удалите предыдущую синхронизацию" 2 +fi +mkdir -p "$(dirname "$WT")" +git -C "$HOST_DIR" worktree add -q -b "$TMP_BRANCH" "$WT" "$HOST_REF" +touch "$WT/.bounded" +echo "worktree: $WT ($TMP_BRANCH от $HOST_REF)" + +# 7. issue и ветка (не в dry-run) +# tea 0.16+ принимает --login/--repo только после подкоманды (arm-ka-docs#110) +TEA_FLAGS=(--login home --repo "$GITEA_REPO") +COMMITS=$(git -C "$SRC" log --format='%h %s' --no-merges "$OLD..$TARGET" 2>/dev/null || true) +N_COMMITS=$(printf '%s\n' "$COMMITS" | grep -c . || true) +ISSUE=""; ISSUE_URL="" +BRANCH=$TMP_BRANCH +if [ "$MODE" = sync ]; then + TITLE="arm_statements: копия пакета arm-statements@$T7" + BODY="Копия пакета \`api/arm_statements\` в \`$HOST_NAME\` была на \`$OLD7\`, станет \`$T7\` (источник — \`RSHB/arm-statements@${REF#origin/}\`). +Коммиты arm-statements \`$OLD7..$T7\` ($N_COMMITS): +$(printf '%s\n' "$COMMITS" | sed 's/^/- /') + +Bounded-задача, выполнена \`docs/scripts/sync-statements.sh\`: копия раскладывается из \`git archive\`, не правится." + ISSUE_URL=$(tea issues create "${TEA_FLAGS[@]}" --title "$TITLE" --description "$BODY" | grep -o 'https://[^ ]*/issues/[0-9]*' | tail -1) + ISSUE=${ISSUE_URL##*/} + [ -n "$ISSUE" ] || die "не удалось создать issue через tea (worktree оставлен: $WT)" + BRANCH="feat/$ISSUE-statements-sync-$T7" + git -C "$WT" branch -m "$TMP_BRANCH" "$BRANCH" + echo "issue: $ISSUE_URL; ветка: $BRANCH" +fi + +# 8. раскладка и проверка результата +lay_copy "$WT" "$TARGET" || die "раскладка выполнена, но копию нельзя коммитить (см. выше); worktree оставлен: $WT" 1 +evaluate "$WT/api/arm_statements" "$WT/api/requirements.txt" "$([ -n "$HLINES" ] && echo "$WT/api/rules/statement_lines.yaml")" "$HOST_NAME" +if [ "$EVAL_CODE" = 20 ]; then + echo "statements-check $HOST_NAME: 20 — после раскладки расхождение; worktree оставлен: $WT" >&2 + exit 1 +fi +SC_LINE=$(summary) +echo "перенесено: api/arm_statements = arm-statements@$T7" +echo "$SC_LINE" + +# 9. dry-run +if [ "$MODE" = dry ]; then + echo "dry-run: issue, проверки, коммит и PR не выполнялись. Посмотреть: git -C $WT status; убрать:" + echo " git -C $HOST_DIR worktree remove --force $WT && git -C $HOST_DIR branch -D $TMP_BRANCH" + exit 0 +fi + +# 10. проверки хоста +run_make() { # run_make <имя> <цель>: хвост вывода при ошибке, worktree остаётся + local log; log=$(mktemp) + if make -C "$WT" "$2" >"$log" 2>&1; then rm -f "$log"; return 0; fi + echo "sync-statements: проверка «$1» не прошла; worktree оставлен для разбора: $WT" >&2 + tail -40 "$log" >&2; rm -f "$log"; exit 1 +} +run_make "make venv" venv +run_make "make check" check +echo "проверки хоста: make check зелёный" + +# 11. коммит, push, PR +git -C "$WT" add -A api/arm_statements +git -C "$WT" -c commit.gpgsign=false commit -q -F - < +EOF +COMMIT=$(git -C "$WT" rev-parse --short=7 HEAD) +git -C "$WT" push -q -u origin "$BRANCH" +PR_BODY="Закрывает #$ISSUE. + +Копия \`api/arm_statements\` в \`$HOST_NAME\` обновлена скриптом \`docs/scripts/sync-statements.sh\`: \`$OLD7\` -> \`$T7\` (источник — \`RSHB/arm-statements@${REF#origin/}\`). + +Коммиты arm-statements ($N_COMMITS): +$(printf '%s\n' "$COMMITS" | sed 's/^/- /') + +Проверки: \`make check\` в worktree зелёный; $SC_LINE +${CORE_WARN:+ +Предупреждение: $CORE_WARN +} +## Rulings I made +- R-1 — копия не правится: перенос проревьюенного в arm-statements кода, содержимое равно \`git archive\` коммита \`$T7\`. +- R-2 — bounded-задача без OpenSpec change. + +🤖 Generated with [Claude Code](https://claude.com/claude-code)" +PR_URL=$(tea pulls create "${TEA_FLAGS[@]}" --head "$BRANCH" --base "${HOST_REF#origin/}" \ + --title "chore(arm_statements): копия arm-statements@$T7 (#$ISSUE)" --description "$PR_BODY" \ + | grep -o 'https://[^ ]*/pulls/[0-9]*' | tail -1) + +# 12. итог +echo +echo "готово: коммит $COMMIT, ветка $BRANCH" +echo "issue: $ISSUE_URL" +echo "PR: ${PR_URL:-создать вручную: tea pulls create ${TEA_FLAGS[*]} --head $BRANCH --base ${HOST_REF#origin/}}" +[ -z "$CORE_WARN" ] || echo "предупреждение: $CORE_WARN" +echo "исполнителю: ревью диапазона не нужно — код проревьюен в arm-statements; merge и «закрывай» по команде человека." +echo "worktree $WT и .bounded остаются до закрытия." diff --git a/scripts/test_sync_statements.sh b/scripts/test_sync_statements.sh index 8c60f5c..bc01731 100755 --- a/scripts/test_sync_statements.sh +++ b/scripts/test_sync_statements.sh @@ -296,5 +296,48 @@ sync 1 "in-place: повтор после защиты путей остаётс check "$(has '/Users/' && echo 0 || echo 1)" "in-place: повтор — снова /Users/" git -C "$SRC" checkout -q main +# --- sync-statements.sh --dry-run (обычный режим без issue и PR) -------------------------------------------------- +S27=${S2:0:7} +git -C "$SRC" checkout -q main +rm_wt() { # rm_wt <хост-каталог> <имя хоста>: убрать worktree и ветку синхронизации + git -C "$1" worktree remove --force "$WORK/wt/statements-sync-$S27/$2" 2>/dev/null || true + git -C "$1" branch -D "tmp/statements-sync-$S27" >/dev/null 2>&1 || true +} +WTC="$WORK/wt/statements-sync-$S27/arm-core" +sync 0 "dry-run: arm-core" --dry-run --host-dir "$WORK/arm-core" +check "$([ -d "$WTC" ] && git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && [ -f "$WTC/.bounded" ] && echo 0 || echo 1)" "dry-run: worktree, ветка и .bounded" +check "$([ "$(tr -d '\n' < "$WTC/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "dry-run: VERSION = S2" +rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc" +check "$(diff -r -x VERSION "$WORK/arc/arm_statements" "$WTC/api/arm_statements" >/dev/null && echo 0 || echo 1)" "dry-run: копия равна git archive S2" +check "$(has 'dry-run: issue, проверки, коммит и PR не выполнялись' && echo 0 || echo 1)" "dry-run: сообщение и команды удаления" +check "$([ -z "$(git -C "$WORK/arm-core" status --porcelain)" ] && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && echo 0 || echo 1)" "dry-run: хост не тронут" +sync 2 "dry-run: повтор" --dry-run --host-dir "$WORK/arm-core" +check "$(has 'уже существует' && echo 0 || echo 1)" "dry-run: повтор — уже существует" +rm_wt "$WORK/arm-core" arm-core +check "$([ ! -d "$WTC" ] && ! git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && echo 0 || echo 1)" "dry-run: worktree и ветка удалены" + +sync 0 "dry-run: сначала ядро" --dry-run --host-dir "$WORK/tabular-parser" +check "$(has 'сначала ядро' && echo 0 || echo 1)" "dry-run: предупреждение «сначала ядро»" +rm_wt "$WORK/tabular-parser" tabular-parser + +# arm-core на S2 закоммичена и запушена -> раскладывать нечего +"$SYNC" "${COMMON[@]}" --in-place --host-dir "$WORK/arm-core" >/dev/null +commit_push "$WORK/arm-core" "copy S2" main +sync 0 "dry-run: уже синхронно" --dry-run --host-dir "$WORK/arm-core" +check "$(has 'раскладывать нечего' && [ ! -e "$WTC" ] && echo 0 || echo 1)" "dry-run: уже синхронно — worktree не создан" + +# прямая правка на origin/main хоста -> отказ 20 +echo "# прямая правка" >> "$WORK/tabular-parser/api/arm_statements/steps.py" +commit_push "$WORK/tabular-parser" "прямая правка" main +sync 20 "dry-run: прямая правка на origin/main" --dry-run --host-dir "$WORK/tabular-parser" +check "$(has steps.py && [ ! -e "$WORK/wt/statements-sync-$S27/tabular-parser" ] && echo 0 || echo 1)" "dry-run: отказ — steps.py, worktree не создан" + +# хост без копии +sync 2 "dry-run: хост без копии" --dry-run --host-dir "$FR" +check "$(has -- '--in-place' && echo 0 || echo 1)" "dry-run: хост без копии — подсказка --in-place" + +# tea: флаги только после подкоманды +check "$(grep -nE '(^|[^[:alnum:]_-])tea[[:space:]]+-' "$SYNC" >/dev/null && echo 1 || echo 0)" "tea: флаги после подкоманды" + [ "$fail" = 0 ] || exit 1 echo "ok: sync-statements.sh — все сценарии пройдены"