From 45b8d1c7838706bc95304a5f4dfa0271d80314ce Mon Sep 17 00:00:00 2001 From: hacker07 Date: Mon, 12 Oct 2026 15:16:32 +0300 Subject: [PATCH] =?UTF-8?q?fork:=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20(=D1=88=D0=B2=D1=8B,=20=D1=81=D0=B8=D0=BD=D1=85?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=BE=D0=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + FORK.md | 85 +++++++++++++++++ scripts/kit-local.sh | 93 +++++++++++++++++++ tools/fork-sync.sh | 93 +++++++++++++++++++ tools/fork_apply.py | 213 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 485 insertions(+) create mode 100644 .gitattributes create mode 100644 FORK.md create mode 100644 scripts/kit-local.sh create mode 100644 tools/fork-sync.sh create mode 100644 tools/fork_apply.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/FORK.md b/FORK.md new file mode 100644 index 0000000..284fce9 --- /dev/null +++ b/FORK.md @@ -0,0 +1,85 @@ +# Форк 3X-UI KIT (hacker07) + +Это личный форк [3X-UI KIT](https://github.com/itsnotkubrick/3X-UI_KIT): наш код живёт рядом с +кодом автора и не ломается, когда автор обновляет свой. Здесь описано, как это устроено и что +делать после каждого обновления автора. + +## Что где лежит + +| Что | Где | Правит | +|---|---|---| +| Наши доработки (команды, хуки) | `scripts/kit-local.sh` | только мы | +| Описание «швов» в коде автора | `tools/fork_apply.py` | только мы | +| Синхронизация с автором и проверки | `tools/fork-sync.sh` | только мы | +| Файлы автора (`scripts/kit.sh`, `scripts/3x-ui.sh`, …) | как есть | только автор | + +Ключевая идея: **в файлах автора мы правим только «швы»** — несколько констант и одну строку +подключения. Всё, что мы дописываем по существу, лежит в нашем файле, которого автор не знает. +Швы описаны в одном месте (`tools/fork_apply.py`) и накатываются только на чистое дерево автора; +если автор изменил нужную строку — инструмент останавливается и говорит об этом, а не делает +полу-патч. + +## Ветки + +- `upstream` — точная копия ветки автора (`main`), только вперёд. +- `dev` — наша сборка: `upstream` + швы + наш файл. Из неё делаются релизы. +- релизы — теги `v<номера>`; номер = `<версия автора>.<наш уровень>` (например, автор 1.2 → наш + первый релиз 1.2.1, следующий наш — 1.2.2, после релиза автора 1.3 — 1.3.1). + +Номер обязан строго расти: на серверах `kit update` не принимает откат назад. + +## Обновление автора (по шагам) + +```bash +cd H:/My_Proj/Fork_3X-UI_KIT +bash tools/fork-sync.sh # подтянуть автора, перенести наши правки, прогнать проверки +# при конфликте — разобрать его вручную, инструмент сам ничего не публикует + +bash tools/release.sh ~/.ssh/3x-ui-kit-fork/release_key # SHA256SUMS + подпись нашим ключом +git add -A && git commit -m "Релиз v$(cat VERSION): швы форка + подпись" +git tag v$(cat VERSION) +git push origin dev v$(cat VERSION) upstream +``` + +## Установка и обновление из этого репозитория + +```bash +# установка на чистый сервер (вместо команды автора с GitHub) +bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_KIT/raw/branch/main/scripts/3x-ui.sh) + +# обновление уже стоящего сервера (тянет наш релиз и проверяет нашу подпись) +kit update --force && kit check +``` + +Внутри `kit` источником файлов служит `KIT_RAW`/`kit_ref_raw()` — они указывают на этот +репозиторий, а подпись релиза проверяется ключом из `KIT_SIGNERS` (только ключ форка). + +## Подпись релизов + +- Открытая часть ключа — `tools/fork-signer.pub` и в `KIT_SIGNERS` (в `scripts/kit.sh`). +- Закрытая часть — только на рабочей станции владельца (`~/.ssh/3x-ui-kit-fork/release_key`), + с паролем; в репозиторий, CI и на серверы не попадает. Кто получит ключ — получит root на всех + серверах, которые обновляются из этого репозитория. +- Смена ключа: подпиши новый релиз старым ключом, иначе серверы его не примут. + +## Наши команды + +```bash +kit local help # справка по дополнениям форка +kit local version # версия, источник обновлений, дата сборки нашего файла +kit local channel # откуда берутся обновления и чем проверяется подпись +kit local seams # какие швы форка сейчас в установленном /usr/local/bin/kit +``` + +## Грабли, о которых стоит помнить + +1. **Ночное автообновление** (`kit-update.timer`, 03:00) перезаписывает `/usr/local/bin/kit` + подписанным релизом из настроенного репозитория. Поэтому канал обновления и швы — один шаг: + сервер должен смотреть в этот репозиторий, иначе наши правки исчезнут ночью. +2. **Переводы строк.** Релиз подписывается по контрольным суммам файлов в рабочей копии, а сервер + сверяет их с файлами из Gitea. При `core.autocrlf=true` суммы не сойдутся (CRLF против LF): + в этом клоне должно быть `git config core.autocrlf false`. +3. **Наш файл обязан быть в списке релиза** (`tools/release.sh`, `FILES=(…)`) и в списке загрузки + `fetch_release()` — иначе при обновлении `kit.sh` приедет новый, а `kit-local.sh` останется старым. +4. **Панель 3X-UI ставится с GitHub** по закреплённой версии и SHA256 — это шаг автора, форк его не + трогает. Для установки с нуля серверу нужен доступ к GitHub. diff --git a/scripts/kit-local.sh b/scripts/kit-local.sh new file mode 100644 index 0000000..38274b1 --- /dev/null +++ b/scripts/kit-local.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# 3X-UI_KIT (форк hacker07) — локальные дополнения. +# +# Этот файл наш: автор его не знает и не правит, поэтому обновления автора его не ломают. +# kit подключает его одной строкой из kit.sh (шов в tools/fork_apply.py) и передаёт сюда +# аргументы командной строки. Всё, что мы дописываем к kit, живёт ЗДЕСЬ. +# +# Команды: kit local <команда> (справка: kit local help) +# +# Правила работы с файлом: только функции и никакого кода на верхнем уровне (он выполняется +# при каждом запуске kit). Всё, что нужно от автора, читается лениво — сам kit на момент +# подключения ещё не объявил свои функции. + +kit_local_help() { + cat <<'EOF' +3X-UI KIT — дополнения форка (hacker07): + kit local help эта справка + kit local version версия kit, источник обновлений и номер нашего патч-уровня + kit local channel откуда берутся обновления и чем проверяется подпись + kit local seams какие места форк правит в файлах автора (метки в /usr/local/bin/kit) + kit local masking состояние маскировки: REALITY (свой домен) и прочие входы + +Файл дополнений: /usr/local/lib/kit/kit-local.sh (ставится установщиком и обновляется +вместе с релизом форка). Правки автора этот файл не перезаписывают. +EOF +} + +kit_local_version() { + printf 'kit: %s (форк hacker07)\n' "${KIT_VERSION:-?}" + printf 'источник: %s\n' "${KIT_RAW:-?}" + local lib=${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh} + printf 'наш файл: %s\n' "$lib" + [[ -r $lib ]] && printf 'собран: %s\n' "$(date -r "$lib" '+%Y-%m-%d %H:%M' 2>/dev/null || echo '?')" + local latest + latest=$(cat /etc/kit/latest-version 2>/dev/null || true) + [[ -n $latest ]] && printf 'последняя: %s\n' "$latest" + return 0 +} + +kit_local_channel() { + printf 'файлы релиза: %s\n' "${KIT_RAW:-?}" + printf 'подпись: id=%s namespace=%s\n' "${KIT_SIG_ID:-?}" "${KIT_SIG_NS:-?}" + local n=0 k + for k in "${KIT_SIGNERS[@]:-}"; do [[ -n $k ]] && n=$((n + 1)); done + printf 'ключей в KIT_SIGNERS: %d (должен быть только ключ форка)\n' "$n" + if [[ $n -eq 0 ]]; then + printf 'ВНИМАНИЕ: список ключей пуст — обновление не пройдёт проверку подписи.\n' + fi + return 0 +} + +kit_local_seams() { + local bin=/usr/local/bin/kit + [[ -r $bin ]] || { printf 'нет %s\n' "$bin"; return 1; } + grep -n '3X-UI_KIT (форк hacker07)' "$bin" 2>/dev/null \ + || printf 'метки форка в %s не найдены — сборка не наша?\n' "$bin" + return 0 +} + +# Короткая сводка по маскировке: у REALITY должно быть «свой домен», у остальных — сосед. +kit_local_masking() { + command -v kit >/dev/null 2>&1 || { printf 'нет команды kit\n'; return 1; } + kit net site 2>/dev/null || printf 'посмотреть: kit net site (в этой версии — kit sni rotate)\n' + return 0 +} + +kit_local_cmd() { + case ${1:-help} in + help | -h | --help) kit_local_help ;; + version | -v) kit_local_version ;; + channel) kit_local_channel ;; + seams) kit_local_seams ;; + masking | mask) kit_local_masking ;; + *) + printf 'Неизвестная команда форка: %s\n' "$1" >&2 + kit_local_help >&2 + return 2 + ;; + esac +} + +# Точка входа: вызывается из kit.sh с аргументами командной строки. +# Если команда не наша — возвращаем 0, и kit работает ровно как раньше. +kit_local_dispatch() { + case ${1:-} in + local) + shift + kit_local_cmd "$@" + exit $? + ;; + *) return 0 ;; + esac +} diff --git a/tools/fork-sync.sh b/tools/fork-sync.sh new file mode 100644 index 0000000..eca3f7d --- /dev/null +++ b/tools/fork-sync.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# 3X-UI_KIT (форк hacker07): подтянуть обновление автора в ветку dev и прогнать проверки. +# +# Ничего не публикует и ничего не обновляет на серверах: после проверок печатает, что делать +# дальше (версия, подпись, тег, push). Запускать с рабочей станции, по шагам. +# +# bash tools/fork-sync.sh # подтянуть автора и проверить +# bash tools/fork-sync.sh --apply # ещё и вписать VERSION/KIT_VERSION (номер версии) +# +# Логика версии: у автора 1.3 → наш номер 1.3.1; если база та же, а мы что-то доработали — +# 1.3.2. Номер обязан строго расти: kit у серверов не принимает откат назад. +set -Eeuo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +PY=${PY:-$(command -v python3 || command -v python || true)} +[[ -n $PY ]] || { echo "! нужен python3" >&2; exit 1; } + +apply=no +case ${1:-} in --apply) apply=yes ;; "") ;; *) echo "Использование: bash tools/fork-sync.sh [--apply]" >&2; exit 1 ;; esac + +say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } +die() { printf '! %s\n' "$*" >&2; exit 1; } + +[[ -z $(git status --porcelain) ]] || die "в рабочей копии есть незакоммиченные правки — сначала закоммить или убери их" + +say "Забираю ветку автора" +git fetch --prune upstream 'refs/heads/main:refs/heads/upstream' +author_version=$(git show upstream:VERSION | tr -d '[:space:]') +echo "у автора: $author_version" + +say "Переношу наши правки на свежую ветку автора" +git checkout -q dev +if ! git rebase upstream; then + echo + echo "Конфликт при переносе. Это ожидаемо, если автор переписал те же строки." + echo " 1) разбери конфликт: git status, правь файлы, git add, git rebase --continue" + echo " 2) если проще собрать швы заново: git rebase --abort, затем" + echo " git checkout -B dev upstream && $PY tools/fork_apply.py && git commit -am 'fork: швы'" + die "остановился, ничего не публикую" +fi + +say "Проверяю, что наши швы ещё на месте" +"$PY" tools/fork_apply.py --check + +say "Проверки кода (как на стороне сервера и в CI автора)" +for f in scripts/*.sh tools/*.sh; do bash -n "$f" || die "$f: синтаксис"; done +echo " ✓ bash -n" +"$PY" -m py_compile scripts/kit-sub.py && echo " ✓ python-скрипты собираются" +if grep -qU $'\r' scripts/*.sh tools/*.sh VERSION 2>/dev/null; then + die "в файлах есть CRLF — сервер сверяет SHA256, и его сумма не сойдётся. Проверь core.autocrlf (нужен false)" +fi +echo " ✓ переводы строк LF" +if command -v shellcheck >/dev/null; then + shellcheck -S warning -e SC1090,SC2034,SC2211 scripts/*.sh tools/release.sh tools/fork-sync.sh && echo " ✓ shellcheck" +else + echo " – shellcheck не установлен, пропускаю" +fi +if command -v node >/dev/null; then + node --test tools/test/*.test.js >/dev/null && echo " ✓ node --test" +else + echo " – node нет, пропускаю тесты генераторов" +fi +"$PY" tools/test/restore_env_test.py >/dev/null && "$PY" tools/test/restore_archive_test.py >/dev/null \ + && echo " ✓ тесты копии (--restore)" + +say "Готово: наши правки лежат на свежем коде автора" +git --no-pager log --oneline upstream..dev +echo +echo "Версия автора: $author_version · наша сейчас: $(tr -d '[:space:]' VERSION + "$PY" tools/fork_apply.py >/dev/null + echo "Версия сборки → $new (VERSION и KIT_VERSION в трёх скриптах)" +fi + +cat <<'NEXT' + +Дальше вручную, по шагам: + 1) подпись и релиз: bash tools/release.sh ~/.ssh/3x-ui-kit-fork/release_key + 2) коммит и тег: git add -A && git commit -m "Релиз v$(cat VERSION): швы форка + подпись" + git tag v$(cat VERSION) + 3) публикация: git push origin dev v$(cat VERSION) upstream + 4) на сервере: kit update --force && kit check +NEXT diff --git a/tools/fork_apply.py b/tools/fork_apply.py new file mode 100644 index 0000000..84f5f18 --- /dev/null +++ b/tools/fork_apply.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""3X-UI_KIT (форк hacker07): накатывает наши «швы» на чистый код автора. + +Это источник правды для форка: единственное место, где описано, ЧТО именно мы правим в +файлах автора. Всё, что мы дописываем по существу, живёт в scripts/kit-local.sh — файле, +который автор не трогает, поэтому обновления автора его не ломают. + +Правила: + * каждый якорь обязан найтись РОВНО один раз, иначе — стоп (автор изменил код, правку + надо пересмотреть руками); + * при любой ошибке ничего не пишется наполовину: сначала прогоняй --check; + * открытый ключ подписи берётся из tools/fork-signer.pub (закрытая часть — только на + рабочей станции владельца, в репозиторий не попадает). + +Использование: + python3 tools/fork_apply.py --check [корень-репозитория] + python3 tools/fork_apply.py [корень-репозитория] +""" + +import os +import pathlib +import re +import sys + +GITEA = os.environ.get("FORK_GITEA_BASE", "https://git.rukavishnikoff.ru/hacker07/3X-UI_KIT") +SIGNER_FILE = os.environ.get("FORK_SIGNER_PUB", "tools/fork-signer.pub") + +HOOK = """ +# --- 3X-UI_KIT (форк hacker07): локальные команды и хуки --- +# Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются. +# Все команды форка — под «kit local …» (см. kit local help). +KIT_LOCAL_LIB="${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}" +if [[ -r $KIT_LOCAL_LIB ]]; then . "$KIT_LOCAL_LIB"; kit_local_dispatch "$@"; fi +""" + + +def die(msg): + print("! " + msg, file=sys.stderr) + sys.exit(1) + + +def ok(msg): + print("==> " + msg) + + +def count(text, anchor, regex): + if regex: + return len(re.findall(anchor, text, re.MULTILINE)) + return text.count(anchor) + + +def main(): + args = [a for a in sys.argv[1:]] + check = False + if "--check" in args: + check = True + args.remove("--check") + root = pathlib.Path(args[0] if args else ".").resolve() + if not (root / "VERSION").exists(): + die("%s не похож на репозиторий 3X-UI KIT (нет VERSION)" % root) + os.chdir(root) + + version = (root / "VERSION").read_text(encoding="utf-8").strip() + if not re.fullmatch(r"[0-9]+(\.[0-9]+)+", version): + die("VERSION=%r: нужен числовой номер вида 1.2.1" % version) + + signer_path = pathlib.Path(SIGNER_FILE) + if not signer_path.exists(): + die( + "нет %s — сначала создай ключ подписи:\n" + " ssh-keygen -t ed25519 -C \"3X-UI KIT fork releases\" -f ~/.ssh/3x-ui-kit-fork/release_key\n" + " mkdir -p tools && ssh-keygen -y -f ~/.ssh/3x-ui-kit-fork/release_key > tools/fork-signer.pub" + % SIGNER_FILE + ) + pub = "" + for line in signer_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if line.startswith(("ssh-ed25519 ", "ssh-rsa ")): + pub = line + break + if not pub: + die("%s: не нашёл строку открытого ключа (ssh-ed25519 …)" % signer_path) + + version_re = r'^KIT_VERSION="[0-9.]+"$' + edits = [ + # --- scripts/kit.sh ------------------------------------------------- + ("scripts/kit.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), + ( + "scripts/kit.sh", + 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"', + False, + 'KIT_RAW="%s/raw/branch/main"' % GITEA, + "источник VERSION и подсказки об обновлении", + ), + ( + "scripts/kit.sh", + 'kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }', + False, + 'kit_ref_raw() { echo "%s/raw/tag/v$1"; }' % GITEA + "\n" + HOOK, + "файлы релиза из нашего тега + подключение kit-local.sh", + ), + ( + "scripts/kit.sh", + r' # SHA256:[^\n]*\n "ssh-ed25519 [^\n]*"\n', + True, + ' # Ключ подписи форка (владелец: hacker07). Закрытая часть — только на рабочей станции.\n' + ' "%s"\n' % pub, + "ключ проверки подписи релизов", + ), + ( + "scripts/kit.sh", + "for f in scripts/kit.sh scripts/kit-sub.py; do", + False, + "for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do", + "наш файл в списке проверяемых и скачиваемых", + ), + ( + "scripts/kit.sh", + ' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit', + False, + ' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n' + " # Форк: ставим наши локальные дополнения из того же подписанного релиза.\n" + " install -d -m 755 /usr/local/lib/kit\n" + ' install -m 644 "$tmp/kit-local.sh" /usr/local/lib/kit/kit-local.sh', + "установка kit-local.sh при обновлении", + ), + # --- scripts/3x-ui.sh ----------------------------------------------- + ("scripts/3x-ui.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), + ( + "scripts/3x-ui.sh", + 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"', + False, + 'KIT_RAW="%s/raw/tag/v$KIT_VERSION"' % GITEA, + "запасная загрузка kit из нашего релиза", + ), + ( + "scripts/3x-ui.sh", + 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"', + False, + 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\nKIT_LOCAL_URL="$KIT_RAW/scripts/kit-local.sh"', + "адрес нашего файла для установщика", + ), + ( + "scripts/3x-ui.sh", + 'KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"', + False, + 'KIT_INSTALL_CMD="bash <(curl -fsSL %s/raw/branch/main/scripts/3x-ui.sh)"' % GITEA, + "команда установки, которую показывает панель", + ), + ( + "scripts/3x-ui.sh", + ' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"', + False, + " # Форк: ставим наши локальные дополнения (см. scripts/kit-local.sh).\n" + " install -d -m 755 /usr/local/lib/kit\n" + ' if [[ -n $src && -f $d/kit-local.sh ]]; then\n' + ' install -m 644 "$d/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n' + " else\n" + ' curl -fsSL --retry 3 -o /usr/local/lib/kit/kit-local.sh "$KIT_LOCAL_URL" \\\n' + ' || warn "Локальные дополнения kit не скачались: kit local не будет работать"\n' + " fi\n" + ' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"', + "установка kit-local.sh установщиком", + ), + # --- scripts/hysteria2.sh ------------------------------------------- + ("scripts/hysteria2.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), + # --- tools/release.sh ----------------------------------------------- + ( + "tools/release.sh", + "FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py VERSION)", + False, + "FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh VERSION)", + "наш файл в подписанном списке релиза", + ), + ] + + bad = 0 + for path, anchor, is_re, new, why in edits: + p = pathlib.Path(path) + if not p.exists(): + print("✗ %s: файла нет" % path) + bad += 1 + continue + text = p.read_text(encoding="utf-8", newline="") + n = count(text, anchor, is_re) + label = anchor if not is_re else anchor + short = label if len(label) <= 54 else label[:51] + "…" + if n != 1: + print("✗ %-20s %-56s найдено %d раз (%s)" % (path, short, n, why)) + bad += 1 + continue + if check: + print("✓ %-20s %-56s (%s)" % (path, short, why)) + continue + if is_re: + text = re.sub(anchor, new, text, count=1, flags=re.MULTILINE) + else: + text = text.replace(anchor, new, 1) + p.write_text(text, encoding="utf-8", newline="") + print("✓ %-20s %-56s (%s)" % (path, short, why)) + + if bad: + if check: + die("якорей не найдено: %d — автор изменил код, пересмотри правки в tools/fork_apply.py" % bad) + die("якорей не найдено: %d — правки не применены" % bad) + if check: + ok("все якоря на месте (версия сборки %s)" % version) + else: + ok("швы накатаны, версия сборки %s" % version) + + +if __name__ == "__main__": + main()