arm-ka-docs/scripts/sync-statements.sh

510 lines
27 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Синхронизация копии пакета arm_statements (репозиторий RSHB/arm-statements) с хостами: arm-core, tabular-parser,
# scan-gateway. Копия лежит в <хост>/api/arm_statements/ и принадлежит источнику: правят только в arm-statements.
#
# Точка синхронизации — файл <копия>/VERSION: полный хэш коммита arm-statements, из которого сделана копия.
# Файлы копии должны совпасть с `git archive <VERSION> arm_statements` (без VERSION и кэшей).
#
# Запуск из workspace: docs/scripts/sync-statements.sh [--check|--in-place|--dry-run] (--host <имя> | --host-dir <dir>)
# [--ref <ref>] [--host-ref <ref>] [--source <dir>] [--docs <dir>]
# [--core-dir <dir>] [--wt-root <dir>]
# --check только проверить рабочее дерево хоста (незакоммиченная правка тоже расхождение):
# 0 — копия синхронна, 10 — предупреждение (копия отстала или перечень кодов в docs отличается),
# 20 — расхождение (прямая правка копии, чужая версия, нет копии, нет зависимости, код вне перечня)
# --in-place разложить копию в рабочее дерево хоста (без git-операций); 1 — защиты (.gitignore хоста, пути для контура),
# 20 — отказ (правка копии, чужая версия, нет зависимости, код вне перечня)
# (без режима) 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 каталог хоста (корень репозитория)
# --ref ветка источника, по умолчанию origin/main; --host-ref — база worktree хоста, по умолчанию origin/main
# --source каталог arm-statements, по умолчанию <workspace>/arm-statements; --docs — каталог docs
# STATEMENTS_PY — интерпретатор помощника (Python >= 3.11 с PyYAML); иначе docs/.venv, хост api/.venv, python3
# --check важнее --dry-run и --in-place при любом порядке аргументов.
# Итог --check: «statements-check <хост>: <код> — …»; код возврата равен коду в строке; 2 — неверные аргументы или окружение.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# Корень workspace — где рядом лежит arm-statements. Скрипт запускают и из docs/ (корень = ../..), и из worktree
# .wt/<задача>/docs (корень = ../../../..); внешний кандидат проверяется первым — там основные checkout'ы.
WS="$(cd "$HERE/../.." && pwd)"
for cand in "$HERE/../../../.." "$HERE/../.."; do
if [ -e "$cand/arm-statements/.git" ]; then WS="$(cd "$cand" && pwd)"; break; fi
done
SRC="$WS/arm-statements"
DOCS="$(cd "$HERE/.." && pwd)"
CORE_DIR="$WS/arm-core"
WT_ROOT="$WS/.wt"
REF=origin/main
HOST_REF=origin/main
HOST_NAME=""
HOST_DIR=""
MODE=""
CHECK=0
IN_PLACE=0
DRY=0
die() { echo "sync-statements: $1" >&2; exit "${2:-1}"; } # die <сообщение> [код]
while [ $# -gt 0 ]; do
case "$1" in
--check) CHECK=1 ;; # приоритет над --dry-run и --in-place независимо от порядка
--in-place) IN_PLACE=1 ;;
--dry-run) DRY=1 ;;
--host) [ $# -ge 2 ] || die "--host требует значение" 2; HOST_NAME=$2; shift ;;
--host-dir) [ $# -ge 2 ] || die "--host-dir требует значение" 2; HOST_DIR=$2; shift ;;
--ref) [ $# -ge 2 ] || die "--ref требует значение" 2; REF=$2; shift ;;
--host-ref) [ $# -ge 2 ] || die "--host-ref требует значение" 2; HOST_REF=$2; shift ;;
--source) [ $# -ge 2 ] || die "--source требует значение" 2; SRC=$2; shift ;;
--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,26p" "$0"; exit 0 ;;
*) die "неизвестный аргумент $1" 2 ;;
esac
shift
done
if [ "$CHECK" = 1 ]; then MODE=check
elif [ "$IN_PLACE" = 1 ] && [ "$DRY" = 1 ]; then die "--in-place несовместим с --dry-run" 2
elif [ "$IN_PLACE" = 1 ]; then MODE=in-place
elif [ "$DRY" = 1 ]; then MODE=dry
else MODE=sync; fi
[ -n "$HOST_NAME" ] || [ -n "$HOST_DIR" ] || die "укажите --host <имя> или --host-dir <dir>" 2
case "$HOST_NAME" in ""|arm-core|tabular-parser|scan-gateway) ;; *) die "неизвестный хост: $HOST_NAME (arm-core | tabular-parser | scan-gateway)" 2 ;; esac
[ -d "$SRC/.git" ] || [ -f "$SRC/.git" ] || [ -d "$SRC/objects" ] || die "arm-statements не найден: $SRC (--source <dir>)" 2
SRC=$(cd "$SRC" && pwd)
[ -d "$DOCS" ] || die "docs не найден: $DOCS (--docs <dir>)" 2
DOCS=$(cd "$DOCS" && pwd)
if [ -z "$HOST_DIR" ]; then
case "$HOST_NAME" in
arm-core) HOST_DIR=$CORE_DIR ;;
*) HOST_DIR="$WS/$HOST_NAME" ;;
esac
fi
[ -d "$HOST_DIR" ] || die "каталог хоста не найден: $HOST_DIR" 2
HOST_DIR=$(cd "$HOST_DIR" && pwd)
if [ -z "$HOST_NAME" ]; then
ORIGIN=$(git -C "$HOST_DIR" remote get-url origin 2>/dev/null || true)
base=$(basename "${ORIGIN:-$HOST_DIR}"); base=${base%.git}
case "$base" in
arm-core) HOST_NAME=arm-core ;;
arm-tabular-parser|tabular-parser) HOST_NAME=tabular-parser ;;
arm-scan-gateway|scan-gateway) HOST_NAME=scan-gateway ;;
*) base=$(basename "$HOST_DIR")
case "$base" in
arm-core|tabular-parser|scan-gateway) HOST_NAME=$base ;;
*) die "не удалось определить хост по origin и имени каталога: укажите --host" 2 ;;
esac ;;
esac
fi
# --- интерпретатор помощника ---------------------------------------------------------------------------------------
PY=""
for cand in "${STATEMENTS_PY:-}" "$DOCS/.venv/bin/python" "$HOST_DIR/api/.venv/bin/python" python3; do
[ -n "$cand" ] || continue
if "$cand" -c 'import tomllib, yaml' 2>/dev/null; then PY=$cand; break; fi
done
[ -n "$PY" ] || die "нужен Python >= 3.11 с PyYAML: выполните make -C docs venv (или задайте STATEMENTS_PY)" 2
CHK="$HERE/statements_check.py"
# --- источник ------------------------------------------------------------------------------------------------------
FETCH_OK=1
if ! git -C "$SRC" fetch -q origin 2>/dev/null; then
FETCH_OK=0
echo "sync-statements: fetch не удался, сравниваю с локальным $REF" >&2
fi
REF_SHA=$(git -C "$SRC" rev-parse -q --verify "$REF^{commit}") || die "в arm-statements нет $REF" 2
REF7=${REF_SHA:0:7}
TMPD=$(mktemp -d)
trap 'rm -rf "$TMPD"' EXIT
# helper <аргументы…>: вывод помощника в $HELPER_OUT, код в $HELPER_RC; код 1 — выход скрипта с кодом 1
HELPER_OUT=""; HELPER_RC=0
helper() {
HELPER_RC=0
HELPER_OUT=$("$PY" "$CHK" "$@" 2>&1) || HELPER_RC=$?
case "$HELPER_RC" in
0|20) ;;
*) echo "$HELPER_OUT" >&2; exit 1 ;;
esac
}
# --- evaluate ------------------------------------------------------------------------------------------------------
# evaluate <copy_dir> <requirements_file> <lines_file|""> <host_name>
# Печатает находки, выставляет EVAL_CODE (0/10/20) и EVAL_WARNS (причины предупреждения через «; »).
EVAL_CODE=0
EVAL_WARNS=""
COPY_BAD=0 # 1 — copy негодна по условиям 1–4 (не из-за зависимостей и словаря)
evaluate() {
local copy=$1 reqs=$2 lines=$3 host=$4
local version="" v7="" archive="$TMPD/archive" known=1 line
EVAL_CODE=0; EVAL_WARNS=""; COPY_BAD=0
bad() { echo "$1"; EVAL_CODE=20; }
badcopy() { bad "$1"; COPY_BAD=1; } # условия 1–4: сама копия негодна (правка, чужая версия)
warn() { echo "$1"; [ "$EVAL_CODE" = 20 ] || EVAL_CODE=10; EVAL_WARNS="${EVAL_WARNS:+$EVAL_WARNS; }$2"; }
if [ ! -d "$copy" ]; then
bad "копии нет: разложите sync-statements.sh --in-place --host-dir <dir> ($copy)"
return 0
fi
if [ -f "$copy/VERSION" ]; then version=$(tr -d ' \t\r\n' < "$copy/VERSION"); fi
v7=${version:0:7}
if ! printf '%s' "$version" | grep -Eq '^[0-9a-f]{40}$' \
|| ! git -C "$SRC" cat-file -e "$version^{commit}" 2>/dev/null; then
known=0
badcopy "VERSION ${version:-(нет файла)} нет в истории arm-statements$([ "$FETCH_OK" = 1 ] || echo '; fetch не удался — сделайте fetch в arm-statements и повторите')"
fi
if [ "$known" = 1 ]; then
# 3. версия должна быть из истории ref
if ! git -C "$SRC" merge-base --is-ancestor "$version" "$REF_SHA" 2>/dev/null; then
badcopy "чужая версия: $v7 не из истории $REF"
fi
# 4. файлы копии против git archive <VERSION>
rm -rf "$archive"; mkdir -p "$archive"
local diffs="" have_archive=1
if ! git -C "$SRC" archive "$version" arm_statements 2>/dev/null | tar -x -C "$archive" 2>/dev/null \
|| [ ! -d "$archive/arm_statements" ]; then
have_archive=0
badcopy "в коммите VERSION ($v7) нет arm_statements/ — чужая версия"
fi
if [ "$have_archive" = 1 ]; then
diffs=$(diff -rq -x VERSION -x __pycache__ -x '*.pyc' -x .pytest_cache -x .mypy_cache -x .ruff_cache -x .DS_Store \
"$archive/arm_statements" "$copy" 2>&1 || true)
fi
if [ -n "$diffs" ]; then
badcopy "прямая правка копии (файлы относительно arm_statements/):"
while IFS= read -r line; do
[ -n "$line" ] || continue
case "$line" in
"Files "*" differ")
line=${line#Files }; line=${line%% and *}
echo " изменён: ${line#"$archive/arm_statements"/}" ;;
"Only in "*)
line=${line#Only in }
local d=${line%%: *} n=${line#*: }
if [ "${d#"$copy"}" != "$d" ]; then
d=${d#"$copy"}; echo " только в копии: ${d#/}${d:+/}$n" | sed 's#: /#: #; s#//#/#g'
else
d=${d#"$archive/arm_statements"}; echo " только в источнике: ${d#/}${d:+/}$n" | sed 's#: /#: #; s#//#/#g'
fi ;;
*) echo " $line" ;;
esac
done <<<"$diffs"
fi
# 5. зависимости пакета в requirements хоста
git -C "$SRC" show "$version:pyproject.toml" > "$TMPD/pyproject.toml" 2>/dev/null || : > "$TMPD/pyproject.toml"
helper deps "$TMPD/pyproject.toml" "$reqs"
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; elif [ -n "$HELPER_OUT" ]; then echo "$HELPER_OUT"; fi
fi
# 6. коды словаря имён строк (только у хоста со словарём)
if [ -n "$lines" ]; then
helper lines "$lines" "$copy/rules/statement_code_orders.yaml"
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; fi
fi
# 7. перечень кодов в docs
if [ ! -f "$DOCS/contracts/statement-code-orders.yaml" ]; then
warn "перечня кодов нет в docs: contracts/statement-code-orders.yaml" "перечня кодов нет в docs"
elif ! cmp -s "$DOCS/contracts/statement-code-orders.yaml" "$copy/rules/statement_code_orders.yaml"; then
warn "перечень кодов в docs отличается от копии: contracts/statement-code-orders.yaml" "перечень кодов в docs отличается от копии"
fi
# 8. отставание от ref
if [ "$known" = 1 ] && [ "$version" != "$REF_SHA" ]; then
warn "копия отстала: $v7, $REF — $REF7" "копия отстала: $v7, $REF — $REF7"
git -C "$SRC" log --format=' %h %s' "$version..$REF_SHA" 2>/dev/null || true
fi
VERSION7=$v7
return 0
}
VERSION7=""
# summary: итоговая строка как у --check
summary() {
case "$EVAL_CODE" in
0) echo "statements-check $HOST_NAME: 0 — копия синхронна с arm-statements@$REF ($VERSION7)" ;;
10) echo "statements-check $HOST_NAME: 10 — ПРЕДУПРЕЖДЕНИЕ: $EVAL_WARNS" ;;
*) echo "statements-check $HOST_NAME: 20 — расхождение, копию не принимать" ;;
esac
}
# --- режимы --------------------------------------------------------------------------------------------------------
if [ "$MODE" = check ]; then
LINES=""
if [ "$HOST_NAME" = arm-core ] && [ -f "$HOST_DIR/api/rules/statement_lines.yaml" ]; then
LINES="$HOST_DIR/api/rules/statement_lines.yaml"
fi
evaluate "$HOST_DIR/api/arm_statements" "$HOST_DIR/api/requirements.txt" "$LINES" "$HOST_NAME"
case "$EVAL_CODE" in
0) echo "statements-check $HOST_NAME: 0 — копия синхронна с arm-statements@$REF ($VERSION7)" ;;
10) echo "statements-check $HOST_NAME: 10 — ПРЕДУПРЕЖДЕНИЕ: $EVAL_WARNS" ;;
*) echo "statements-check $HOST_NAME: 20 — расхождение, копию не принимать" ;;
esac
exit "$EVAL_CODE"
fi
# guard_copy <host_root>: защиты копии (игнор .gitignore хоста, содержимое, на котором остановится перенос в контур);
# код 1 со списком, если копию нельзя коммитить. Вызывается и после раскладки, и когда раскладывать нечего.
guard_copy() {
local root=$1 dest="$1/api/arm_statements" ign pth bad=0
ign=$(git -C "$root" ls-files --others --ignored --exclude-standard -- api/arm_statements 2>/dev/null \
| grep -Ev '(^|/)(__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache)(/|$)|\.pyc$|(^|/)\.DS_Store$' || true)
if [ -n "$ign" ]; then
echo "файлы копии игнорируются .gitignore хоста, в коммит не попадут:"
printf '%s\n' "$ign" | sed 's/^/ /'
bad=1
fi
pth=$(grep -rIn "/Users/\|/home/[a-z]\|git.aepif.ru\|uv venv\|uv pip" "$dest" || true)
if [ -n "$pth" ]; then
echo "копия сломает снимок для контура:"
printf '%s\n' "$pth" | sed 's/^/ /'
bad=1
fi
[ "$bad" = 0 ] || return 1
return 0
}
# lay_copy <host_root> <commit>: заменить <host_root>/api/arm_statements копией из источника, записать VERSION, защиты
lay_copy() {
local root=$1 commit=$2 dest="$1/api/arm_statements"
rm -rf "$dest"; mkdir -p "$root/api"
git -C "$SRC" archive "$commit" arm_statements | tar -x -C "$root/api"
printf '%s\n' "$commit" > "$dest/VERSION"
guard_copy "$root"
}
if [ "$MODE" = in-place ]; then
COPY="$HOST_DIR/api/arm_statements"
REQS="$HOST_DIR/api/requirements.txt"
LINES=""
if [ "$HOST_NAME" = arm-core ] && [ -f "$HOST_DIR/api/rules/statement_lines.yaml" ]; then
LINES="$HOST_DIR/api/rules/statement_lines.yaml"
fi
# 1. существующая копия не должна быть правленой или чужой
if [ -d "$COPY" ]; then
evaluate "$COPY" "$REQS" "" "$HOST_NAME" > "$TMPD/ev.out"
if [ "$COPY_BAD" = 1 ]; then
cat "$TMPD/ev.out"
echo "statements-check $HOST_NAME: 20 — отказ: копия правлена или чужая, ничего не записано"
exit 20
fi
fi
# 2. зависимости и словарь имён по коммиту --ref
refuse=""
git -C "$SRC" show "$REF_SHA:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml"
helper deps "$TMPD/ref-pyproject.toml" "$REQS"
if [ "$HELPER_RC" = 20 ]; then refuse="$HELPER_OUT"; fi
if [ -n "$LINES" ]; then
git -C "$SRC" show "$REF_SHA:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \
|| : > "$TMPD/ref-orders.yaml"
helper lines "$LINES" "$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 несовместим с хостом, ничего не записано"
exit 20
fi
# 3. уже на ref?
LAID=0
if [ -d "$COPY" ] && [ -f "$COPY/VERSION" ] && [ "$(tr -d ' \t\r\n' < "$COPY/VERSION")" = "$REF_SHA" ]; then
echo "раскладывать нечего: копия уже на $REF ($REF7)"
guard_copy "$HOST_DIR" || die "копию нельзя коммитить (см. выше)" 1
else
# 4. раскладка
lay_copy "$HOST_DIR" "$REF_SHA" || die "раскладка выполнена, но копию нельзя коммитить (см. выше)" 1
LAID=1
fi
# 5. итог
evaluate "$COPY" "$REQS" "$LINES" "$HOST_NAME"
summary
if [ "$LAID" = 1 ]; then
echo "закоммитьте api/arm_statements отдельным коммитом: git -C $HOST_DIR add api/arm_statements"
fi
exit "$EVAL_CODE"
fi
# --- обычный режим и --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
chore(arm_statements): копия arm-statements@$T7 (#$ISSUE)
Копия api/arm_statements: $OLD7..$T7 ($N_COMMITS коммитов), скрипт docs/scripts/sync-statements.sh.
Коммиты arm-statements:
$(printf '%s\n' "$COMMITS" | sed 's/^/- /')
$SC_LINE
Co-Authored-By: sync-statements.sh <noreply@aepif.ru>
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 остаются до закрытия."