554 lines
30 KiB
Bash
Executable File
554 lines
30 KiB
Bash
Executable File
#!/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 <хост>: <код> — …»; код возврата равен коду в строке.
|
||
# Коды выхода: 0 — готово/синхронно (--in-place: итог проверки 0 или 10, он же код); 1 — сбой раскладки, защит, проверок хоста,
|
||
# коммита или push; 2 — неверные аргументы, окружение, нет копии на --host-ref, worktree уже есть; 10 — предупреждение
|
||
# (только --check и --in-place); 20 — расхождение или отказ (правка копии, чужая версия, нет зависимости, код вне перечня).
|
||
# --in-place требует --host-dir (клон/worktree хоста на ветке не main); основной checkout и ветку main он отвергает.
|
||
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,30p" "$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
|
||
[ -n "$HOST_DIR" ] || die "--in-place требует --host-dir <worktree или клон хоста на ветке задачи>; --host один не годится: он указывает на основной checkout" 2
|
||
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"
|
||
if [ ! -f "$reqs" ]; then
|
||
bad "нет requirements хоста (api/requirements.txt): зависимости пакета не проверены"
|
||
else
|
||
helper deps "$TMPD/pyproject.toml" "$reqs"
|
||
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; elif [ -n "$HELPER_OUT" ]; then echo "$HELPER_OUT"; fi
|
||
fi
|
||
fi
|
||
|
||
# 6. коды словаря имён строк (только у хоста со словарём)
|
||
if [ -n "$lines" ]; then
|
||
if [ ! -f "$copy/rules/statement_code_orders.yaml" ]; then
|
||
bad "в копии нет rules/statement_code_orders.yaml: коды словаря имён не проверены"
|
||
else
|
||
helper lines "$lines" "$copy/rules/statement_code_orders.yaml"
|
||
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; fi
|
||
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
|
||
HB=$(git -C "$HOST_DIR" symbolic-ref -q --short HEAD 2>/dev/null || true)
|
||
if [ "$HOST_DIR" = "$WS/$HOST_NAME" ]; then
|
||
die "--in-place не пишет в основной checkout хоста ($HOST_DIR): создайте worktree задачи и укажите его в --host-dir" 2
|
||
fi
|
||
case "$HB" in main|master) die "--in-place не пишет в ветку $HB хоста: перейдите на ветку задачи (worktree)" 2 ;; esac
|
||
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 "если копия «чужая» после squash-слияния ветки arm-statements: удалите api/arm_statements и разложите заново"
|
||
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"
|
||
if [ ! -f "$REQS" ]; then refuse="нет requirements хоста (api/requirements.txt)"
|
||
else
|
||
helper deps "$TMPD/ref-pyproject.toml" "$REQS"
|
||
if [ "$HELPER_RC" = 20 ]; then refuse="$HELPER_OUT"; fi
|
||
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
|
||
|
||
bullets() { if [ -n "$1" ]; then printf '%s\n' "$1" | sed 's/^/- /'; else echo "- (нет коммитов)"; fi; }
|
||
|
||
# 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"
|
||
if [ ! -f "$HT/api/requirements.txt" ]; then refuse="нет requirements хоста (api/requirements.txt)"
|
||
else
|
||
helper deps "$TMPD/ref-pyproject.toml" "$HT/api/requirements.txt"
|
||
if [ "$HELPER_RC" = 20 ]; then refuse=$HELPER_OUT; fi
|
||
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")"
|
||
if ! git -C "$HOST_DIR" worktree add -q -b "$TMP_BRANCH" "$WT" "$HOST_REF"; then
|
||
rmdir "$(dirname "$WT")" 2>/dev/null || true
|
||
die "не удалось создать worktree $WT" 1
|
||
fi
|
||
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):
|
||
$(bullets "$COMMITS")
|
||
|
||
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) || true
|
||
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
|
||
# make check мог переписать копию (форматтеры, кэши): повторная оценка перед коммитом
|
||
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 "sync-statements: после make check копия расходится с arm-statements@$T7; коммит не сделан, worktree оставлен: $WT" >&2
|
||
exit 1
|
||
fi
|
||
SC_LINE=$(summary)
|
||
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 || die "коммит не удался; worktree оставлен: $WT" 1
|
||
chore(arm_statements): копия arm-statements@$T7 (#$ISSUE)
|
||
|
||
Копия api/arm_statements: $OLD7..$T7 ($N_COMMITS коммитов), скрипт docs/scripts/sync-statements.sh.
|
||
Коммиты arm-statements:
|
||
$(bullets "$COMMITS")
|
||
|
||
$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" || die "push не удался; коммит $COMMIT в ветке $BRANCH, worktree оставлен: $WT" 1
|
||
PR_BODY="Закрывает #$ISSUE.
|
||
|
||
Копия \`api/arm_statements\` в \`$HOST_NAME\` обновлена скриптом \`docs/scripts/sync-statements.sh\`: \`$OLD7..$T7\` (источник — \`RSHB/arm-statements@${REF#origin/}\`).
|
||
|
||
Коммиты arm-statements ($N_COMMITS):
|
||
$(bullets "$COMMITS")
|
||
|
||
Проверки: \`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) || true
|
||
|
||
# 12. итог
|
||
if [ -z "$PR_URL" ]; then
|
||
echo
|
||
echo "sync-statements: PR не создан (tea не вернул ссылку). Коммит $COMMIT, ветка $BRANCH (запушена), issue: $ISSUE_URL; worktree $WT" >&2
|
||
echo "создать вручную: tea pulls create ${TEA_FLAGS[*]} --head $BRANCH --base ${HOST_REF#origin/}" >&2
|
||
exit 1
|
||
fi
|
||
echo
|
||
echo "готово: коммит $COMMIT, ветка $BRANCH"
|
||
echo "issue: $ISSUE_URL"
|
||
echo "PR: $PR_URL"
|
||
[ -z "$CORE_WARN" ] || echo "предупреждение: $CORE_WARN"
|
||
echo "исполнителю: ревью диапазона не нужно — код проревьюен в arm-statements; merge и «закрывай» по команде человека."
|
||
echo "worktree $WT и .bounded остаются до закрытия."
|