#!/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 arm_statements` (без VERSION и кэшей). # # Запуск из workspace: docs/scripts/sync-statements.sh [--check|--in-place|--dry-run] (--host <имя> | --host-dir ) # [--ref ] [--host-ref ] [--source ] [--docs ] # [--core-dir ] [--wt-root ] # --check только проверить рабочее дерево хоста (незакоммиченная правка тоже расхождение): # 0 — копия синхронна, 10 — предупреждение (копия отстала или перечень кодов в docs отличается), # 20 — расхождение (прямая правка копии, чужая версия, нет копии, нет зависимости, код вне перечня) # --in-place разложить копию в --host-dir (режим пока не реализован) # --dry-run worktree хоста без issue и PR (режим пока не реализован); вместе с --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, по умолчанию /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,23p" "$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 " 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 )" 2 SRC=$(cd "$SRC" && pwd) [ -d "$DOCS" ] || die "docs не найден: $DOCS (--docs )" 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 # Печатает находки, выставляет EVAL_CODE (0/10/20) и EVAL_WARNS (причины предупреждения через «; »). EVAL_CODE=0 EVAL_WARNS="" evaluate() { local copy=$1 reqs=$2 lines=$3 host=$4 local version="" v7="" archive="$TMPD/archive" known=1 line EVAL_CODE=0; EVAL_WARNS="" bad() { echo "$1"; EVAL_CODE=20; } 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 ($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 bad "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 bad "чужая версия: $v7 не из истории $REF" fi # 4. файлы копии против git archive rm -rf "$archive"; mkdir -p "$archive" git -C "$SRC" archive "$version" arm_statements | tar -x -C "$archive" local diffs 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) if [ -n "$diffs" ]; then bad "прямая правка копии (файлы относительно 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="" # --- режимы -------------------------------------------------------------------------------------------------------- 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 die "режим не реализован" 1