diff --git a/CLAUDE.md b/CLAUDE.md index 9aba3b6..ce0b753 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,8 +9,7 @@ code in this repository. Канон конвенций разработки для личных проектов. Сами конвенции лежат в `conventions/{arch,lang/<язык>,stack/<стек>}/`; обвязка канона (`README.md`, -`LANGUAGE.md`, `GUIDE.md`, `READING.md`, `.conventions-suite.toml`, `conv`) -живёт в +`LANGUAGE.md`, `GUIDE.md`, `READING.md`, `.conventions-suite.toml`) живёт в корне. К потребителю из неё едет только `READING.md` — короткое описание языка для читателя копий. @@ -209,14 +208,12 @@ META-38: ось объявлена в шапке ключами `lang:` и `stac ## Состояние репозитория -- Тестов, линтеров и CI нет. `conv` — python3 CLI на одной stdlib; запускают - его из корня репозитория-потребителя (`~/projects/private/dev-conventions/` - плюс команда). -- Модель копий, описанная в `README.md`, согласована, но не реализована: - `conv` собран под прежнюю (зеркальное дерево, именованные регионы, - `origin_hash`, команды `status`/`diff`/`push`). Сами конвенции к новой - модели приведены — регионов в каноне нет. При правке обвязки истина — - README, а не код `conv`. +- Тестов, линтеров и CI здесь нет: репозиторий — данные, а не код. Проверяет + их `convy suite check`, живущий в своём репозитории и ставящийся бинарём. +- Модель копий, описанная в `README.md`, реализована в `convy`. Прежний + питоновский `conv` удалён вместе со своей моделью (зеркальное дерево, + именованные регионы, `origin_hash`). При расхождении обвязки с инструментом + истина — README, а не код. - Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:` в природе нет. - `TODO.md` — площадка для обсуждения на будущее, а не принятые решения; при diff --git a/README.md b/README.md index b18ddc8..3d8c5e0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ | [GUIDE.md](GUIDE.md) | как ведут конвенции: когда заводить, механизация, отступления | | [READING.md](READING.md) | как читать конвенцию: то, что едет к потребителю | | `.conventions-suite.toml` | манифест набора: язык, темы, префиксы правил | -| `conv` | сборка копий | К потребителю едет содержимое `conventions/` и один файл обвязки — `READING.md`; остальная обвязка остаётся в каноне. Самодостаточность копии это @@ -366,18 +365,30 @@ topics = ["client-logging"] ## Команды +Копии собирает `convy` — отдельный инструмент, живущий в своём репозитории и +ставящийся бинарём. Запускают его из корня репозитория-потребителя: + ```bash -conv list # какие темы есть в каноне и что подключено -conv add time # добавить тему в манифест и собрать файл -conv add time --for backend # то же, когда компонентов несколько -conv pull # пересобрать всё, что перечислено в манифесте - # (и обновить READING.md рядом с копиями) -conv pull --for web # только один компонент +convy init --source <ссылка на канон> --component backend \ + --dir docs/conventions --lang go +convy add time # подписаться на тему и собрать файл +convy add time --for backend # то же, когда компонентов несколько +convy pull # пересобрать всё, что перечислено в манифесте + # (и обновить READING.md рядом с копиями) +convy pull --for web # только один компонент +convy sync # подвести раскладку файлов под манифест +convy list # что подключено и что ещё есть в каноне +convy check # проверить форму того, что лежит здесь ``` При одном компоненте `--for` не нужен. При нескольких команда без него не угадывает, а отказывает и перечисляет имена. +Манифест подключения правится и руками — это данные, а не текст с +комментариями. Что бы в нём ни поменяли, раскладку под него подводит `convy +sync`: чего не хватает — соберёт, что осиротело — уберёт, а копию с локальной +частью не тронет и назовёт. + Отчёт о том, что изменилось, отдельной командой не выдаётся: после `pull` его показывает `git diff`, а решение — принять, поправить или откатить — принимает человек перед коммитом. @@ -386,11 +397,9 @@ conv pull --for web # только один компонент репозитории, переносится в канон руками: это редкая операция, и её цена — не аргумент против того, чтобы направление оставалось односторонним. -Запускать из корня репозитория: - -```bash -~/projects/private/dev-conventions/conv pull -``` +Сам канон ведут те же командой под `suite`: `convy suite add` заводит +конвенцию, `convy suite rule` дописывает правило, `convy suite retire` +снимает, `convy suite check` проверяет целостность набора. Обёртка в раннере репозитория (`inv conventions -- pull` для ansible, `task conventions -- pull` для Go) — тонкий проброс аргументов, чтобы @@ -410,11 +419,11 @@ conv pull --for web # только один компонент ## Состояние -Модель выше — согласованная, а не реализованная. `conv` пока собран под -прежнюю: зеркальное дерево копий вместо плоского, именованные регионы -`` вместо одного маркера, `origin_hash` в шапке и команды -`status`, `diff`, `push`; `READING.md` рядом с копиями он тоже пока не -кладёт, компонентов и объявленной оси не знает и выбирает слои по пути. Сами -конвенции уже приведены к новой модели — именованных регионов в каноне нет, -ось объявлена в шапках. Ни один репозиторий-потребитель не подключён, поэтому -переход никого не ломает. +Модель выше реализована в `convy`: сборка копий, отбор слоёв по объявленной +оси, маркер локальной части, `READING.md` рядом с копиями, проверка +целостности набора. Прежний питоновский `conv` — с зеркальным деревом, +именованными регионами и `origin_hash` — удалён вместе со своей моделью. + +Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:` в +природе нет. Пока это так, непроверенным остаётся главное — как всё это живёт +в чужом репозитории через полгода после первой сборки. diff --git a/TODO.md b/TODO.md index d8ab2b3..28bbdf3 100644 --- a/TODO.md +++ b/TODO.md @@ -2,10 +2,10 @@ Черновик для следующего разговора: вопросы и варианты, а не принятые решения. Закрытый вопрос отсюда удаляется — принятое решение живёт в -`README.md`, `GUIDE.md`, `LANGUAGE.md` или `TOOL.md`, а не в этом файле. +`README.md`, `GUIDE.md` или `LANGUAGE.md`, а не в этом файле. -Вопросы про инструмент здесь не живут — они собраны в `TOOL.md`, раздел -«Открытые вопросы». +Вопросы про инструмент здесь не живут — они собраны в его собственном +репозитории. Две секции: сначала язык и подход, потом сам набор и подключение. diff --git a/TOOL.md b/TOOL.md deleted file mode 100644 index 8a9428a..0000000 --- a/TOOL.md +++ /dev/null @@ -1,205 +0,0 @@ -# Инструмент: convy - -Стартовая точка для разработки. Здесь — принятые решения об инструменте и -открытые вопросы к нему; модель, которую он реализует, описана в -[README.md](README.md), а форма правил — в [LANGUAGE.md](LANGUAGE.md). При -расхождении истина там, а не здесь. - -## Что это - -`convy` — CLI для управления конвенциями: собирает копии в проектах и -проверяет целостность набора. Пишется на Go, живёт в отдельном репозитории, -ставится готовым бинарём. - -Имя выбрано за то, что держит корень предметной области и звучит как маленький -помощник, а уменьшительное `-y` обещает малость — что правда: инструмент -копирует файлы, собирает тему из слоёв и проверяет форму, не более. Рядом -существует английское слово `convey`, отличающееся одной буквой и подходящее -по смыслу, — опечатку в текстах стоит проверять отдельно. - -Отдельный репозиторий нужен по двум причинам. Инструмент и данные в одном -репозитории правятся одним движением, и это мешает: лежащий отдельно бинарь -физически не даёт починить инструмент «заодно» с правкой конвенции. Вторая — -независимый бинарь работает против любого набора и любого проекта, а не против -одного конкретного канона. - -## Термины - -| Уровень | Английский | Русский | Что там лежит | -|---|---|---|---| -| набор | `suite` | набор | `conventions/`, манифест набора, обвязка | -| проект | `project` | проект | манифест подключения, компоненты | -| компонент | `component` | компонент | директория копий: один язык, один стек, один вид приложения | - -`package`, `bundle`, `library` не берём: они тащат багаж менеджеров -зависимостей — версии, разрешение, лок, — которого в модели нет. `set` не -годится в CLI: в позиции подкоманды читается глаголом. - -Компонент — адресат сборки: подписка принадлежит проекту, а собранный файл -читает тот, кто правит конкретный код. Определение — область, где все -выбранные слои действуют одновременно (`sqlite` и `postgres` — да, go и -javascript — никогда). Уровнем CLI компонент не становится: это аргумент -`--for`, а не подкоманда. - -«Канон» — имя этого конкретного набора, а не термин уровня; в общих -формулировках употребляется «набор». «Потребитель» — слово про роль -репозитория, а не про уровень. - -Манифесты названы по тому, что описывают, а не по уровню ради симметрии: -`.conventions-suite.toml` в наборе — идентичность набора, `.conventions.toml` -в проекте — подключённые конвенции. Оба начинаются с точки, потому что оба — -данные инструмента, а не документы репозитория: они читаются и **переписываются -целиком** командами, комментариев не держат, и объяснения к ним живут в -соседних файлах. Отсюда же бесплатное определение контекста: по имени рядом -видно, где ты. - -Прежде манифест набора назывался `suite.toml` и точки не имел — на том -основании, что правится он руками при каждой новой теме. Основание отпало: -правит его инструмент. - -## Раскладка команд - -Глубина команды отражает частоту и адресата. Проектные команды выполняются в -каждом репозитории и часто; ведение набора — в одном репозитории и редко. - -``` -convy add <тема> подписаться и собрать -convy pull пересобрать подписанное -convy list что подключено и что можно взять -convy check проверить форму того, что здесь - -convy suite check целостность набора: префиксы, темы, оси, ссылки, форма -convy suite new новая тема: шапка, префикс, запись в манифест -``` - -Проектные команды принимают `--for <компонент>`. При одном компоненте флаг не -нужен; при нескольких команда без него отказывает и перечисляет имена — тот -же принцип, что и с контекстом: наугад не делается ничего. `convy list` -группирует вывод по компонентам. - -Граница проходит не по «проектное против наборного», а по «частое и -повсеместное» против «только у автора». Поэтому `check` остаётся наверху: -форма правила одна и та же, локальные правила проекта на `X`-префиксах -написаны по ней же, и проверять их у себя нужно без подкоманды. Под `suite` -уходит то, что в проекте не имеет смысла. - -Три следствия для реализации: - -- **контекст определяется и отказ говорится явно.** `.conventions.toml` рядом - — проект, `.conventions-suite.toml` — набор. Проектная команда, набранная в - наборе, не делает ничего наугад: она отказывает и подсказывает наборный - аналог; -- **помощь группируется заголовками** «В проекте» и «В наборе»: в плоском - списке уровни не видны; -- **синонимов нет.** `convy project pull` рядом с `convy pull` не заводим: два - способа сказать одно — то, от чего модель избавлялась в остальных местах. - -В проектах вызов идёт через обёртку раннера (`inv conventions -- pull`, -`task conventions -- pull`), которая пробрасывает аргументы. Обёртка уже -называет предмет, поэтому короткая проектная команда здесь тоже выигрывает. - -## Две задачи, которые нельзя смешивать - -Они расходятся по частоте, по адресату и по тому, что считается провалом. - -**Целостность набора.** Префиксы уникальны и не переиспользованы, шапка -совпадает с манифестом, тема объявлена и зарегистрирована, ось объявлена -ключами и у темы не больше одного базового слоя, у каждого правила -модальность с нормой и обоснование либо заглушка, нумерация сплошная, ссылки -разрешаются, путей набора в тексте конвенции нет, строка о версии языка на -месте. Запускается в наборе при каждой правке; провал — ошибка. - -**Установка в проект.** Манифест подключения, сборка файла темы из слоёв на -каждый компонент, сохранение локальной части, `READING.md` рядом с копиями. -Запускается в проекте изредка; провал чаще означает «посмотри глазами», чем -«ошибка». Отчёта «набор ушёл вперёд» нет: его делает `git diff` после -пересборки. - -## Что делает установка - -Подробности — в README, раздел «Копия в репозитории». Коротко, что важно для -реализации: - -- сборка идёт **по разу на компонент**, в директорию `dir` из его секции; - директории компонентов обязаны различаться — иначе копии столкнутся - именами, и это ошибка манифеста, а не повод переименовывать файлы; -- копия плоская, **один файл на тему**; слои идут секциями в порядке - база → язык → стек, выбор слоёв — по `lang` и `stack` компонента, сверяемым - с ключами оси в шапке слоя (META-38), а не с путём файла в наборе; слой без - ключей оси — базовый и попадает в копию всегда; -- набор может быть плоским: у темы один слой, ключей оси нет, `lang` и - `stack` в компоненте отсутствуют. Отдельной ветки в коде это не требует — - сборка из одного слоя есть копирование; -- шапка копии — только `origin:` с именем темы; отпечатков и дат нет; -- всё ниже маркера `` переживает пересборку, всё выше - перезаписывается; маркер ставит сборщик; -- `README.md` в директории принадлежит проекту и не трогается; `READING.md` - принадлежит набору и перезаписывается целиком; -- транспорта обратно нет: ни `push`, ни отчёта о расхождении, ни лок-файла. - -## Что делает проверка - -Список проверок — в `LANGUAGE.md`, раздел «Что стоит проверять машиной». Он -уже разделён на три части, и это деление прямо ложится в код: - -- **форма правила** — разбором текста, в любом файле, который язык - употребляет: конвенции и `GUIDE.md`; -- **распространение** — разбором текста, только в файлах конвенций: тема в - шапке, префикс чужой темы в норме, пути набора, `X` у локальных правил; -- **чтением** — то, что машине не даётся: взаимоисключительность строк - таблицы, покрытие области действия, самодостаточность нормы, обоснование - отвечает на «что сломается», примеры не расширяют норму. - -Третья часть — не работа `convy`. Её выполняет агент, и по ней стоит завести -скилл, а не пытаться выразить регуляркой. - -## Независимость от набора - -Инструмент не зашивает правила языка в код: версия и документы объявлены в -манифесте набора, секция `[language]`. Пока версия одна, это выглядит -избыточным — но именно здесь разница между «инструмент для этого канона» и -«инструмент для любого набора». - -Отсюда же: никаких упоминаний конкретных тем, префиксов и путей в коде. - -## Чего инструмент не делает - -- не сливает трёхсторонне и не разрешает конфликты: правка выше маркера - теряется, и это заявленное поведение; -- не ведёт лок-файл: копии закоммичены, автоматического обновления нет, - ответ на «что было в прошлый раз» даёт git; -- не хранит список подписчиков: подписка — свойство проекта; -- не переносит правки из проекта в набор: это ручная и редкая операция. - -## Открытые вопросы - -- **Один бинарь или два.** Пока — один с подкомандой `suite`. Разделение - дешевле заложить сразу, чем отпиливать потом: если задачи разъедутся, у них - уже будут разные точки входа. -- **Установка.** `eget` умеет GitHub-релизы, а git у меня свой - (`git.vakhrushev.me`) — проверить, тянет ли `eget` релизы Gitea, иначе - остаётся прямой URL или `go install`. -- **TOML.** В stdlib парсера нет, значит одна внешняя зависимость - (`BurntSushi/toml` или `pelletier/go-toml`). Выбрать и больше зависимостей - не заводить. -- **Предупреждение о висячих ссылках** на неподписанные темы: это установка, - а не целостность, но список подписок ему нужен из манифеста подключения. -- **Проверка на `convey`** в текстах — вместе с остальными проверками формы - или отдельной мелочью. -- **Прототипы, на которые стоит посмотреть** до того, как писать: дистрибуция - пакетов Vale (`.vale.ini` → `vale sync` → `styles/`) как образец манифеста и - `vendir.yml` — как пример границы между «чего хочу» и «что получил». - -## Состояние - -`convy` не начат. В наборе лежит `conv` — питоновский скрипт под прежнюю -модель копий: зеркальное дерево, именованные регионы, `origin_hash`, команды -`status`, `diff`, `push`. Он не истина ни в чём: модель описана в README, -проверки — в `LANGUAGE.md`. - -Логика проверок целостности написана и много раз прогнана руками, но живёт в -скретчпадах сессий, а не в репозитории. При старте разработки её стоит -перенести первой — это готовая спецификация в виде кода. - -Переименование `conv` → `convy` в обвязке (`README.md`, `CLAUDE.md`) делается -одним заходом, когда инструмент будет готов. diff --git a/conv b/conv deleted file mode 100755 index 6c58355..0000000 --- a/conv +++ /dev/null @@ -1,516 +0,0 @@ -#!/usr/bin/env python3 -"""conv — синхронизация конвенций между каноном и репозиторием. - -Канон — директория conventions/ рядом с этим скриптом. Репозиторий держит -закоммиченные копии нужных конвенций в docs/conventions/, повторяя её -структуру. Копия — источник правды для репозитория; канон — лавка, из -которой берут. Пути в origin даются относительно conventions/. - -Служебная разметка копии: - - --- - origin: arch/time.md # откуда взято - origin_hash: a1b2c3d4 # отпечаток канона на момент синхронизации - synced: 2026-07-25 - local: нет # или текст: чем и почему разошлись - --- - -Прочие ключи шапки (status, extends) — часть документа: они сравниваются -наравне с телом и приезжают из канона. - -Локальные регионы — куски, которые по определению принадлежат репозиторию -(механизация, отступления, «здесь решили так»). Из сравнения исключаются: - - - ... - - -Имя региона обязательно: перенос при pull идёт по именам. - -Команды: - conv list что есть в каноне - conv add arch/time.md [...] взять конвенцию в репозиторий - conv status состояние копий репозитория - conv diff [arch/time.md] чем копия отличается от канона - conv pull arch/time.md забрать обновление канона - conv push arch/time.md вернуть локальное улучшение в канон - conv push --new lang/go/x.md завести в каноне новую конвенцию - -Везде можно указать --repo (по умолчанию — текущая директория) -и --dir (по умолчанию docs/conventions), до или после команды. -""" - -from __future__ import annotations - -import argparse -import datetime -import difflib -import hashlib -import re -import sys -from pathlib import Path -from typing import NoReturn - -CANON = Path(__file__).resolve().parent / "conventions" -CANON_TREES = ("arch", "lang", "stack") -SERVICE_KEYS = ("origin", "origin_hash", "synced", "local") -DEFAULT_DIR = "docs/conventions" -ENC = "utf-8" - -# Маркеры распознаются только в начале строки: так пример разметки внутри -# текста конвенции не превращается в настоящий регион. -REGION_RE = re.compile( - r"^(.*?)^", - re.DOTALL | re.MULTILINE, -) -OPEN_RE = re.compile(r"^" if raw else "" - return f"{head}\n" - - return REGION_RE.sub(repl, body) - - -def fill_regions(body: str, values: dict[str, str]) -> tuple[str, list[str]]: - """Вставляет содержимое регионов по имени. Возвращает тело и имена, - которым не нашлось места.""" - used: set[str] = set() - - def repl(match: re.Match[str]) -> str: - raw = (match.group(1) or "").strip() - head = f"" if raw else "" - if raw in values: - used.add(raw) - return f"{head}{values[raw]}" - return match.group(0) - - filled = REGION_RE.sub(repl, body) - lost = [n for n, v in values.items() if n not in used and v.strip()] - return filled, lost - - -def fingerprint(meta: dict[str, str], body: str) -> str: - """Отпечаток документа: ключи шапки плюс тело без локальных регионов.""" - head = "\n".join(f"{k}={v}" for k, v in sorted(doc_keys(meta).items())) - return hashlib.sha256(f"{head}\n\n{blank_regions(body)}".encode(ENC)).hexdigest()[ - :8 - ] - - -def today() -> str: - return datetime.date.today().isoformat() - - -# --- канон и репозиторий --------------------------------------------------- - - -def canon_list() -> list[str]: - out: list[str] = [] - for tree in CANON_TREES: - root = CANON / tree - if root.is_dir(): - out += [str(p.relative_to(CANON)) for p in sorted(root.rglob("*.md"))] - return out - - -def canon_read(origin: str) -> tuple[dict[str, str], str]: - path = CANON / origin - if not path.is_file(): - die(f"в каноне нет {origin}") - return split_front(read(path)) - - -def normalize_origin(name: str, *, must_exist: bool = True) -> str: - """Принимает 'arch/time.md', 'arch/time' и однозначный хвост вроде 'time'.""" - name = name.strip("/") - if not name.endswith(".md"): - name += ".md" - candidate = (CANON / name).resolve() - if candidate.is_relative_to(CANON): - rel = str(candidate.relative_to(CANON)) - if rel.split("/")[0] in CANON_TREES and (not must_exist or candidate.is_file()): - return rel - matches = [c for c in canon_list() if c == name or c.endswith("/" + name)] - if len(matches) == 1: - return matches[0] - if not matches: - die(f"в каноне нет {name} (путь должен начинаться с {'/'.join(CANON_TREES)})") - die(f"неоднозначно: {name} → {', '.join(matches)}") - - -def repo_dir(args: argparse.Namespace) -> Path: - return (Path(str(args.repo)) / str(args.dir)).resolve() - - -def repo_copies(base: Path) -> tuple[dict[str, Path], list[Path], list[str]]: - """origin → копия; плюс .md без шапки и сообщения о нечитаемых файлах.""" - found: dict[str, Path] = {} - untracked: list[Path] = [] - problems: list[str] = [] - if not base.is_dir(): - return found, untracked, problems - for path in sorted(base.rglob("*.md")): - try: - meta, _ = split_front(read(path)) - except (OSError, UnicodeDecodeError) as exc: - problems.append(f"{path.name}: не читается ({type(exc).__name__})") - continue - origin = meta.get("origin") - if not origin: - if path.name != "README.md": - untracked.append(path) - continue - if origin in found: - problems.append( - f"{origin}: две копии ({found[origin]}, {path}) — вторая скрыта" - ) - continue - found[origin] = path - return found, untracked, problems - - -def locate(args: argparse.Namespace, origin: str) -> Path: - """Путь копии: по шапке, если она лежит не по канонному пути.""" - base = repo_dir(args) - copies, _, _ = repo_copies(base) - return copies.get(origin, base / origin) - - -# --- состояние ------------------------------------------------------------- - - -def state( - meta: dict[str, str], body: str, canon_meta: dict[str, str], canon_body: str -) -> str: - copy_fp = fingerprint(meta, body) - canon_fp = fingerprint(canon_meta, canon_body) - if copy_fp == canon_fp: - return "ok" - base = meta.get("origin_hash") - if not base: - return "нет origin_hash в шапке" - if base == canon_fp: - return "изменено локально" - if base == copy_fp: - return "канон обновился" - return "разошлись" - - -# --- команды --------------------------------------------------------------- - - -def cmd_list(args: argparse.Namespace) -> int: - for origin in canon_list(): - meta, _ = canon_read(origin) - marks = [] - if "extends" in meta: - marks.append(f"расширяет {meta['extends']}") - if "status" in meta: - marks.append(meta["status"]) - tail = f" ({'; '.join(marks)})" if marks else "" - print(f"{origin}{tail}") - return 0 - - -def cmd_add(args: argparse.Namespace) -> int: - base = repo_dir(args) - added = False - for raw in args.names: - origin = normalize_origin(raw) - target = base / origin - if target.exists(): - print(f"{origin}: уже есть ({target}), пропускаю") - continue - canon_meta, canon_body = canon_read(origin) - checked_regions(canon_body, f"канон/{origin}") - meta: dict[str, str] = { - "origin": origin, - "origin_hash": fingerprint(canon_meta, canon_body), - "synced": today(), - "local": "нет", - } - meta.update(doc_keys(canon_meta)) - target.parent.mkdir(parents=True, exist_ok=True) - write(target, join_front(meta, canon_body)) - added = True - print(f"{origin} → {target}") - if "extends" in canon_meta: - print(f" расширяет {canon_meta['extends']} — возможно, нужна и она") - if added: - print("не забудь строку в docs/conventions/README.md") - return 0 - - -def cmd_status(args: argparse.Namespace) -> int: - base = repo_dir(args) - copies, untracked, problems = repo_copies(base) - if not copies and not untracked and not problems: - print(f"в {base} нет копий конвенций") - return 0 - width = max((len(o) for o in copies), default=0) - for origin, path in copies.items(): - try: - meta, body = split_front(read(path)) - except (OSError, UnicodeDecodeError) as exc: - print(f"{origin:<{width}} не читается ({type(exc).__name__})") - continue - if not (CANON / origin).is_file(): - print(f"{origin:<{width}} нет в каноне") - continue - canon_meta, canon_body = canon_read(origin) - try: - regions(body) - except ValueError as exc: - print(f"{origin:<{width}} разметка: {exc}") - continue - local = meta.get("local", "нет") - note = "" if local == "нет" else f" [{local}]" - print(f"{origin:<{width}} {state(meta, body, canon_meta, canon_body)}{note}") - for path in untracked: - print(f"{path.name}: без шапки origin — не отслеживается") - for problem in problems: - print(problem) - return 0 - - -def cmd_diff(args: argparse.Namespace) -> int: - base = repo_dir(args) - copies, _, _ = repo_copies(base) - targets = [normalize_origin(args.name)] if args.name else list(copies) - for origin in targets: - path = copies.get(origin) - if path is None: - print(f"{origin}: нет копии в репозитории") - continue - if not (CANON / origin).is_file(): - print(f"{origin}: нет в каноне") - continue - meta, body = split_front(read(path)) - canon_meta, canon_body = canon_read(origin) - if fingerprint(meta, body) == fingerprint(canon_meta, canon_body): - continue - sys.stdout.writelines( - difflib.unified_diff( - join_front(doc_keys(canon_meta), blank_regions(canon_body)).splitlines( - keepends=True - ), - join_front(doc_keys(meta), blank_regions(body)).splitlines( - keepends=True - ), - fromfile=f"канон/{origin}", - tofile=f"репо/{origin}", - ) - ) - return 0 - - -def cmd_pull(args: argparse.Namespace) -> int: - origin = normalize_origin(args.name) - path = locate(args, origin) - if not path.is_file(): - die(f"нет копии {origin} — сначала conv add {origin}") - meta, body = split_front(read(path)) - canon_meta, canon_body = canon_read(origin) - checked_regions(canon_body, f"канон/{origin}") - local = checked_regions(body, f"репо/{origin}") - st = state(meta, body, canon_meta, canon_body) - if st == "ok": - fresh = fingerprint(canon_meta, canon_body) - if meta.get("origin_hash") != fresh: - meta["origin_hash"] = fresh - meta["synced"] = today() - write(path, join_front(meta, body)) - print(f"{origin}: тексты совпадают, отпечаток освежён") - else: - print(f"{origin}: уже совпадает") - return 0 - if st in ("изменено локально", "разошлись") and not args.force: - die( - f"{origin}: {st} — правки вне локальных регионов будут потеряны.\n" - f" посмотри conv diff {origin}, затем conv pull --force " - f"или conv push {origin}" - ) - merged, lost = fill_regions(canon_body, local) - if lost and not args.force: - die( - f"{origin}: в каноне нет регионов {', '.join(lost)} — их содержимое " - f"пропадёт.\n перенеси вручную или conv pull --force" - ) - for name in lost: - print(f" потерян локальный регион {name}") - new_meta = {k: meta[k] for k in SERVICE_KEYS if k in meta} - new_meta["origin_hash"] = fingerprint(canon_meta, canon_body) - new_meta["synced"] = today() - new_meta.update(doc_keys(canon_meta)) - write(path, join_front(new_meta, merged)) - print(f"{origin}: обновлено из канона — перечитай глазами, регионы могли устареть") - return 0 - - -def cmd_push(args: argparse.Namespace) -> int: - origin = normalize_origin(args.name, must_exist=not args.new) - path = locate(args, origin) - if not path.is_file(): - die(f"нет копии {origin}") - meta, body = split_front(read(path)) - checked_regions(body, f"репо/{origin}") - target = CANON / origin - if not target.is_file(): - if not args.new: - die(f"в каноне нет {origin} — заведи новую конвенцию через conv push --new") - target.parent.mkdir(parents=True, exist_ok=True) - write(target, join_front(doc_keys(meta), blank_regions(body))) - meta["origin_hash"] = fingerprint(doc_keys(meta), blank_regions(body)) - meta["synced"] = today() - write(path, join_front(meta, body)) - print(f"{origin}: заведена в каноне") - return 0 - canon_meta, canon_body = canon_read(origin) - st = state(meta, body, canon_meta, canon_body) - if st == "ok": - print(f"{origin}: канон уже такой") - return 0 - if st == "канон обновился": - die( - f"{origin}: копия не менялась, а канон ушёл вперёд — пушить нечего, нужен pull" - ) - if st == "разошлись" and not args.force: - die( - f"{origin}: разошлись — канон менялся после синхронизации, " - f"его правки затрутся.\n посмотри conv diff {origin}, " - f"затем conv push --force" - ) - write(target, join_front(doc_keys(meta), blank_regions(body))) - meta["origin_hash"] = fingerprint(doc_keys(meta), blank_regions(body)) - meta["synced"] = today() - write(path, join_front(meta, body)) - print(f"{origin}: канон обновлён из репозитория") - return 0 - - -def main() -> int: - common = argparse.ArgumentParser(add_help=False) - common.add_argument("--repo", default=".", help="корень репозитория") - common.add_argument("--dir", default=DEFAULT_DIR, help="где лежат конвенции") - - parser = argparse.ArgumentParser(prog="conv", parents=[common], description=__doc__) - sub = parser.add_subparsers(dest="cmd", required=True) - - sub.add_parser("list", parents=[common], help="что есть в каноне").set_defaults( - fn=cmd_list - ) - - p_add = sub.add_parser( - "add", parents=[common], help="взять конвенцию в репозиторий" - ) - p_add.add_argument("names", nargs="+") - p_add.set_defaults(fn=cmd_add) - - sub.add_parser("status", parents=[common], help="состояние копий").set_defaults( - fn=cmd_status - ) - - p_diff = sub.add_parser( - "diff", parents=[common], help="чем копия отличается от канона" - ) - p_diff.add_argument("name", nargs="?") - p_diff.set_defaults(fn=cmd_diff) - - p_pull = sub.add_parser("pull", parents=[common], help="забрать обновление канона") - p_pull.add_argument("name") - p_pull.add_argument("--force", action="store_true") - p_pull.set_defaults(fn=cmd_pull) - - p_push = sub.add_parser("push", parents=[common], help="вернуть улучшение в канон") - p_push.add_argument("name") - p_push.add_argument("--force", action="store_true") - p_push.add_argument("--new", action="store_true", help="завести новый файл канона") - p_push.set_defaults(fn=cmd_push) - - args = parser.parse_args() - return int(args.fn(args)) - - -if __name__ == "__main__": - sys.exit(main())