From b5ad8a0f4c64e7331b0f048d7d18db074efd02c5 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 19:13:16 +0400 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20sync-statements.sh=20--in-plac?= =?UTF-8?q?e=20=E2=80=94=20=D1=80=D0=B0=D1=81=D0=BA=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BF=D0=B8=D0=B8=20=D0=B2=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=87=D0=B5=D0=B5=20=D0=B4=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D0=BE=20=D1=85=D0=BE=D1=81=D1=82=D0=B0=20=D1=81=20=D0=B7?= =?UTF-8?q?=D0=B0=D1=89=D0=B8=D1=82=D0=BE=D0=B9=20=D0=BE=D1=82=20.gitignor?= =?UTF-8?q?e=20=D0=B8=20=D0=BF=D1=83=D1=82=D0=B5=D0=B9=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83=D1=80=D0=B0=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Также: evaluate при коммите VERSION без arm_statements/ даёт код 20 вместо падения с 128 (T2.1). Co-Authored-By: Claude Opus 5.5 --- scripts/sync-statements.sh | 113 +++++++++++++++++++++++++++++--- scripts/test_sync_statements.sh | 61 +++++++++++++++++ 2 files changed, 164 insertions(+), 10 deletions(-) diff --git a/scripts/sync-statements.sh b/scripts/sync-statements.sh index 7a1c978..e4c0e84 100755 --- a/scripts/sync-statements.sh +++ b/scripts/sync-statements.sh @@ -11,7 +11,8 @@ # --check только проверить рабочее дерево хоста (незакоммиченная правка тоже расхождение): # 0 — копия синхронна, 10 — предупреждение (копия отстала или перечень кодов в docs отличается), # 20 — расхождение (прямая правка копии, чужая версия, нет копии, нет зависимости, код вне перечня) -# --in-place разложить копию в --host-dir (режим пока не реализован) +# --in-place разложить копию в рабочее дерево хоста (без git-операций); 1 — защиты (.gitignore хоста, пути для контура), +# 20 — отказ (правка копии, чужая версия, нет зависимости, код вне перечня) # --dry-run worktree хоста без issue и PR (режим пока не реализован); вместе с --in-place — код 2 # --host arm-core | tabular-parser | scan-gateway; по умолчанию — по origin хоста (arm-core, arm-tabular-parser, # arm-scan-gateway), иначе по имени каталога @@ -58,7 +59,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,23p" "$0"; exit 0 ;; + -h|--help) sed -n "2,24p" "$0"; exit 0 ;; *) die "неизвестный аргумент $1" 2 ;; esac shift @@ -137,11 +138,13 @@ helper() { # Печатает находки, выставляет 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="" + 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 @@ -153,22 +156,28 @@ evaluate() { 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 и повторите')" + 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 - bad "чужая версия: $v7 не из истории $REF" + badcopy "чужая версия: $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) + 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 - bad "прямая правка копии (файлы относительно arm_statements/):" + badcopy "прямая правка копии (файлы относительно arm_statements/):" while IFS= read -r line; do [ -n "$line" ] || continue case "$line" in @@ -214,6 +223,15 @@ evaluate() { } 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="" @@ -229,4 +247,79 @@ if [ "$MODE" = check ]; then exit "$EVAL_CODE" fi +# lay_copy : заменить /api/arm_statements копией из источника, записать VERSION; +# затем защиты (игнор .gitignore хоста, содержимое, на котором остановится перенос в контур) — любая даёт код 1 +lay_copy() { + local root=$1 commit=$2 dest="$1/api/arm_statements" ign pth bad=0 + 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" + 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$' || 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 +} + +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)" + 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 + die "режим не реализован" 1 diff --git a/scripts/test_sync_statements.sh b/scripts/test_sync_statements.sh index f8568ce..1c8df41 100755 --- a/scripts/test_sync_statements.sh +++ b/scripts/test_sync_statements.sh @@ -230,5 +230,66 @@ echo "# правка" >> "$AC/arm_statements/steps.py" sync 20 "check: правка + отставание = 20" --check --host-dir "$WORK/arm-core" reset_hosts +# коммит без arm_statements/ (T2.1): код 20 с итоговой строкой, а не 128 +NOARCH=$(git -C "$SRC" commit-tree "$(git -C "$SRC" hash-object -t tree /dev/null)" -m "без пакета") +printf '%s\n' "$NOARCH" > "$AC/arm_statements/VERSION" +sync 20 "check: в коммите VERSION нет arm_statements/" --check --host-dir "$WORK/arm-core" +check "$(has 'нет arm_statements/' && has 'statements-check arm-core: 20' && echo 0 || echo 1)" "check: нет arm_statements/ — сообщение и итог" +reset_hosts + +# --- sync-statements.sh --in-place -------------------------------------------------------------------------------- +S2=$(git -C "$SRC" rev-parse origin/main) +FR="$WORK/fresh/tabular-parser" +mkdir -p "$WORK/fresh" +git init -q --bare "$WORK/fresh/arm-tabular-parser.git" +git clone -q "$WORK/fresh/arm-tabular-parser.git" "$FR" 2>/dev/null +git -C "$FR" checkout -q -b main +put "$FR/api/requirements.txt" $'PyYAML==6.0.3' +commit_push "$FR" "fresh" main + +sync 0 "in-place: первичная раскладка" --in-place --host-dir "$FR" --ref origin/main +check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: 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" "$FR/api/arm_statements" >/dev/null && echo 0 || echo 1)" "in-place: копия равна git archive S2" +check "$(git -C "$FR" status --porcelain | grep -qv '^?? api/arm_statements' && echo 1 || echo 0)" "in-place: хост менялся только в api/arm_statements" +check "$(has закоммитьте && echo 0 || echo 1)" "in-place: подсказка про коммит" +sync 0 "in-place: повтор" --in-place --host-dir "$FR" --ref origin/main +check "$(has 'раскладывать нечего' && echo 0 || echo 1)" "in-place: повтор — раскладывать нечего" + +sync 0 "in-place: обновление arm-core S1 -> S2" --in-place --host-dir "$WORK/arm-core" +check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: arm-core VERSION = S2" +echo "# правка" >> "$AC/arm_statements/steps.py" +sync 20 "in-place: отказ при правке" --in-place --host-dir "$WORK/arm-core" --ref origin/main +check "$(grep -q '^# правка' "$AC/arm_statements/steps.py" && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: правка на месте, VERSION прежний" +reset_hosts + +TPR="$WORK/tabular-parser/api" +put "$TPR/requirements.txt" "aiohttp==3.14.3" +sync 20 "in-place: отказ без зависимости (обновление)" --in-place --host-dir "$WORK/tabular-parser" +check "$([ "$(tr -d '\n' < "$TPR/arm_statements/VERSION")" = "$S1" ] && has PyYAML && echo 0 || echo 1)" "in-place: копия не тронута, в выводе PyYAML" +reset_hosts +cp "$FR/api/requirements.txt" "$WORK/req.bak" +put "$FR/api/requirements.txt" "aiohttp==3.14.3" +sync 20 "in-place: отказ без зависимости (fresh)" --in-place --host-dir "$FR" --ref origin/main +cp "$WORK/req.bak" "$FR/api/requirements.txt" +cp "$H/lines_x.yaml" "$AC/rules/statement_lines.yaml" +sync 20 "in-place: код словаря вне перечня" --in-place --host-dir "$WORK/arm-core" --ref origin/main +check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && has 1120 && echo 0 || echo 1)" "in-place: словарь — копия не тронута, 1120 в выводе" +reset_hosts + +# защиты: коммиты S3/S4 на ветке test-guards, чтобы origin/main (S2) остался прежним +git -C "$SRC" checkout -q -b test-guards +put "$SRC/arm_statements/tests/golden/case.json" '{}' +commit_push "$SRC" "S3 json" test-guards +put "$FR/.gitignore" '*.json' +sync 1 "in-place: .gitignore прячет файл" --in-place --host-dir "$FR" --ref origin/test-guards +check "$(has case.json && has .gitignore && echo 0 || echo 1)" "in-place: в выводе case.json и .gitignore" +rm "$FR/.gitignore" +echo "# /Users/someone" >> "$SRC/arm_statements/steps.py" +commit_push "$SRC" "S4 путь" test-guards +sync 1 "in-place: содержимое для контура" --in-place --host-dir "$FR" --ref origin/test-guards +check "$(has '/Users/' && echo 0 || echo 1)" "in-place: в выводе /Users/" +git -C "$SRC" checkout -q main + [ "$fail" = 0 ] || exit 1 echo "ok: sync-statements.sh — все сценарии пройдены"