arm-ka-docs/scripts/sync-statements.sh
2026-10-18 19:10:11 +04:00

233 lines
13 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 разложить копию в --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, по умолчанию <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,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 <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=""
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 <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 <VERSION>
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