diff --git a/CHANGELOG.md b/CHANGELOG.md index a5b07d7..37535be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,45 +2,30 @@ All notable changes to **export_1c_help** are documented in this file. +## [0.3.0] - 2026-07-23 + +### Added + +- Export help from **any** 1C configuration dump (`-c` / `--config` or `-s` / `--src`) +- Auto wiki URL from the configuration git repo: `repo.git` → `repo.wiki.git` (Gitea/GitLab) +- Explicit `--wiki-url` still supported (any target wiki) +- `info` subcommand: src, version, git remote, derived wiki URL +- Per-config default output dirs: `out/wiki-md-`, `out/wiki_clone-` +- README: connect any submodule / root project to its wiki and run export + ## [0.2.0] - 2026-07-23 ### Added - Configuration version on each export (`VERSION` / `Configuration.xml`) -- Per-page lifecycle in wiki footer and `manifest.json`: - - `created_in` — версия конфигурации, в которой страница впервые попала в wiki - - `edited_in` — версия, в которой содержимое справки изменилось - - `deleted_in` — версия, в которой страница исчезла из `src` -- `History.md` — сводка выгрузки и накопительный список удалённых страниц -- `push` читает предыдущий `manifest.json` из wiki перед пересборкой +- Per-page lifecycle: `created_in` / `edited_in` / `deleted_in` +- `History.md`, `push` reads previous `manifest.json` from wiki - CLI: `--prev-manifest`, `--config-version` -### Changed - -- Home / Sidebar показывают версию конфигурации и ссылку на History - ## [0.1.0] - 2026-07-23 -### Status - -**In development.** Full dump of configuration Help to Gitea wiki. - -### Added - -- Discovery of `**/Ext/Help/ru.html` in a 1C configuration `src/` tree (incl. nested `Subsystems`) -- HTML → Markdown converter (stdlib only): headings, lists, bold/italic, links -- Rewrite of 1C help links (`Catalog.X/Help`, `….Form.Y/Help`) to Gitea `[[wiki]]` links -- Unique wiki page titles (H1 / synonym / meta key) with collision suffixes -- Gitea-compatible **percent-encoded** filenames for Cyrillic titles -- `Home.md` index and `_Sidebar.md` navigation -- `manifest.json` (meta_key → title mapping) -- CLI: `convert`, `push` (clone/commit/push wiki git repo) -- Option `--objects-only` to skip form-level help - -### Fixed - -- Nested subsystem help paths (`Subsystems/A/Subsystems/B/…`) no longer collapse to the parent meta key -- Unicode wiki filenames replaced with percent-encoded slugs (Gitea UI returned HTTP 500) +Initial release: Help HTML → Markdown, Gitea wiki push, percent-encoded Cyrillic filenames. +[0.3.0]: https://git.p7net.ru/tools/export_1c_help/releases/tag/v0.3.0 [0.2.0]: https://git.p7net.ru/tools/export_1c_help/releases/tag/v0.2.0 [0.1.0]: https://git.p7net.ru/tools/export_1c_help/releases/tag/v0.1.0 diff --git a/README.md b/README.md index ebee00a..0daa97f 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,187 @@ # export_1c_help -**Версия:** см. [`VERSION`](VERSION) (текущая: **0.2.0**) · [CHANGELOG](CHANGELOG.md) +**Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.0**) · [CHANGELOG](CHANGELOG.md) **Лицензия:** [MIT](LICENSE) -Выгрузка встроенной справки конфигурации 1С (`**/Ext/Help/ru.html`) в Markdown и публикация в [Gitea Wiki](https://docs.gitea.com/usage/wiki). +Выгрузка встроенной справки **любой** конфигурации 1С (`**/Ext/Help/ru.html`) в Markdown и публикация в wiki git-репозитория (Gitea / GitLab: `*.wiki.git`). Зависимости: **Python 3.10+**, только стандартная библиотека. --- -## Порядок обновления wiki (после доработки справки в конфигурации) +## Подключение произвольной конфигурации к wiki -Инструмент **всегда** пересобирает wiki из всего `src/` (инкрементального патча нет). Частичная правка Help в конфигураторе → полная перевыгрузка `src` → полный `push`. +Инструмент не привязан к `crm3-26`. Источник — каталог выгрузки конфигурации (подмодуль или корневой git-проект). Цель — wiki **этого** репозитория либо любой другой `*.wiki.git`. -1. Доработать справку в конфигураторе (нужные объекты/формы). -2. Выгрузить конфигурацию в git (`crm3-26/`) и закоммитить/запушить, как обычно. -3. В монорепо: `git pull` (актуальный `crm3-26/src`, в т.ч. `VERSION` / `Configuration.xml`). -4. Синхронизировать wiki: +### 1. Подготовка репозитория конфигурации + +1. Конфигурация лежит в git (отдельный репозиторий или submodule), типичная структура: + + ```text + / + VERSION # опционально + src/ + Configuration.xml + Catalogs/…/Ext/Help/ru.html + … + ``` + +2. На сервере (Gitea / GitLab) у репозитория конфигурации **включена Wiki**. +3. Убедитесь, что у вас есть права **push** в wiki (`…/repo.wiki.git`). + Первый push создаст/обновит страницы; на Gitea wiki должна быть уже инициализирована (хотя бы пустая/одна страница через UI, если сервер так требует). + +Проверка путей и URL: ```bash -cd tools/export_1c_help -python3 export_1c_help.py push \ - -s ../../crm3-26/src \ - --wiki-url https://git.p7net.ru/1c/crm3_26.wiki.git \ - -m "sync help after config update" +cd tools/export_1c_help # или путь к клону export_1c_help + +# подмодуль в монорепо +python3 export_1c_help.py info -c ../../crm3-26 + +# корневой клон конфигурации +python3 export_1c_help.py info -c /path/to/my_config + +# только src +python3 export_1c_help.py info -s /path/to/my_config/src ``` -Перед записью можно проверить без push: +Пример вывода: + +```text +config_root: …/crm3-26 +src: …/crm3-26/src +config_version: 3.1.36.33 +git_remote(origin): https://git.p7net.ru/1c/crm3_26.git +wiki_url: https://git.p7net.ru/1c/crm3_26.wiki.git +``` + +Правило авто-URL: `https://host/org/repo.git` → `https://host/org/repo.wiki.git` +(аналогично для `git@host:org/repo.git`). + +### 2. Первая выгрузка в wiki репозитория конфигурации + +Без `--wiki-url` цель берётся из `origin` git-репозитория, которому принадлежит `-c` / `-s`: + +```bash +# подмодуль +python3 export_1c_help.py push -c ../../crm3-26 + +# любой другой корень / submodule +python3 export_1c_help.py push -c /path/to/other_config + +# dry-run (только сборка MD, без push) +python3 export_1c_help.py push -c ../../crm3-26 --dry-run +``` + +Промежуточные каталоги по умолчанию (чтобы не смешивать конфигурации): + +- `out/wiki-md-<имя-корня>/` +- `out/wiki_clone-<имя-корня>/` + +### 3. Выгрузка в произвольный wiki + +Если wiki не у того же репозитория (или remote не определяется): ```bash python3 export_1c_help.py push \ - -s ../../crm3-26/src \ - --wiki-url https://git.p7net.ru/1c/crm3_26.wiki.git \ - --dry-run + -c /path/to/config \ + --wiki-url https://git.example/org/docs.wiki.git ``` -**Канон текста** — HTML в конфигурации. Ручные правки страниц в wiki сотрутся при следующем `push` (если не указан `--keep-unmanaged`). +Можно указать и URL кода — он будет преобразован в `.wiki.git`: + +```bash +python3 export_1c_help.py push \ + -c /path/to/config \ + --wiki-url https://git.example/org/docs.git +``` + +Другой remote (не `origin`): + +```bash +python3 export_1c_help.py push -c /path/to/config --remote upstream +``` + +### 4. Обновление после доработки справки в конфигураторе + +Инструмент **всегда** пересобирает wiki из всего `src/` (инкремента нет). + +1. Доработать Help в конфигураторе. +2. Выгрузить конфигурацию в её git-репозиторий (`git commit` / `push`). +3. Обновить рабочую копию (`git pull` / `git submodule update`). +4. Снова: + +```bash +python3 export_1c_help.py push -c /path/to/config +``` + +**Канон текста** — HTML в конфигурации. Ручные правки wiki сотрутся при следующем `push` (если нет `--keep-unmanaged`). --- ## Версии конфигурации на страницах -| Где | Что фиксируется | -|-----|-----------------| +| Где | Что | +|-----|-----| | `Home.md` | версия конфигурации **этой** выгрузки | -| подвал каждой страницы | версия выгрузки; **создана в** / **изменена в** | -| `History.md` | сводка +/−/~ и накопительный список **удалённых** (`deleted_in`) | -| `manifest.json` | те же поля + `content_hash`, история `exports[]` | +| подвал страницы | версия выгрузки; **создана в** / **изменена в** | +| `History.md` | сводка +/−/~ и удалённые (`deleted_in`) | +| `manifest.json` | lifecycle + `content_hash` + `exports[]` | -Версия берётся из `/VERSION` или `` в `Configuration.xml` (можно переопределить `--config-version`). +Версия: `/VERSION` или `` в `Configuration.xml` (`--config-version` перекрывает). -Жизненный цикл считается относительно предыдущего `manifest.json` в wiki (при `push` подтягивается автоматически). - ---- - -## Быстрый старт - -```bash -cd tools/export_1c_help - -python3 export_1c_help.py convert \ - -s ../../crm3-26/src \ - -o ./out/wiki-md \ - --clean - -python3 export_1c_help.py push \ - -s ../../crm3-26/src \ - --wiki-url https://git.p7net.ru/1c/crm3_26.wiki.git -``` - -```bash -python3 export_1c_help.py --version -``` +При `push` предыдущий `manifest.json` читается из целевой wiki автоматически. --- ## Команды -### `convert` +| Команда | Назначение | +|---------|------------| +| `info` | пути, версия, remote, wiki URL | +| `convert` | только Markdown на диск | +| `push` | convert + commit/push в wiki | + +Общие параметры источника (взаимоисключающие): | Параметр | Описание | |----------|----------| -| `-s` / `--src` | каталог `src` выгрузки конфигурации | -| `-o` / `--out` | каталог результата | +| `-c` / `--config` | корень конфигурации (`…/crm3-26`) **или** её `src/` | +| `-s` / `--src` | явно каталог `src` | + +| Параметр | Описание | +|----------|----------| +| `--wiki-url` | явный wiki (иначе из git remote конфигурации) | +| `--remote` | имя remote для авто-URL (`origin`) | | `--objects-only` | без справки форм | -| `--keep-empty` | не пропускать почти пустые страницы | -| `--clean` | очистить `--out` перед записью | -| `--prev-manifest` | предыдущий `manifest.json` (lifecycle) | -| `--config-version` | явная версия конфигурации | - -### `push` - -`prepare wiki` → `convert` (с `manifest.json` из wiki) → `git commit` / `push`. - -| Параметр | Описание | -|----------|----------| -| `--wiki-url` | URL `*.wiki.git` | -| `--dry-run` | только convert | -| `--keep-unmanaged` | не удалять чужие `.md` | -| `-m` / `--message` | сообщение коммита | +| `--dry-run` | без git push | +| `--prev-manifest` | свой предыдущий manifest | +| `--config-version` | явная версия | --- -## Репозиторий +## Примеры для монорепозитория миграции -- путь: `tools/export_1c_help/` +```bash +# целевая crm3-26 → её wiki +python3 export_1c_help.py push -c ../../crm3-26 + +# исходная crm3-dev → её wiki (если wiki включена у crm3_old) +python3 export_1c_help.py push -c ../../crm3-dev + +# расширение — только если в выгрузке есть Ext/Help +python3 export_1c_help.py info -c ../../crm3-26.rhana +``` + +Только локальная конвертация: + +```bash +python3 export_1c_help.py convert -c ../../crm3-26 -o ./out/wiki-md-crm3-26 --clean +``` + +--- + +## Репозиторий инструмента + +- путь в монорепо: `tools/export_1c_help/` - remote: https://git.p7net.ru/tools/export_1c_help.git diff --git a/VERSION b/VERSION index 0ea3a94..0d91a54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.3.0 diff --git a/export1c_help/resolve.py b/export1c_help/resolve.py new file mode 100644 index 0000000..8696345 --- /dev/null +++ b/export1c_help/resolve.py @@ -0,0 +1,137 @@ +"""Resolve configuration paths and Gitea/GitLab wiki clone URLs.""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + + +class ResolveError(ValueError): + pass + + +def resolve_src_root(config_or_src: Path) -> Path: + """ + Accept configuration root (…/crm3-26) or its src/ dump. + + Returns absolute path to the directory that contains Catalogs/, Documents/, … + """ + path = Path(config_or_src).expanduser().resolve() + if not path.exists(): + raise ResolveError(f"path does not exist: {path}") + + if path.is_file(): + raise ResolveError(f"expected directory, got file: {path}") + + if _looks_like_src(path): + return path + + src = path / "src" + if src.is_dir() and _looks_like_src(src): + return src.resolve() + + raise ResolveError( + f"not a 1C configuration dump: {path} " + "(need …/src with Catalogs|Documents|Configuration.xml, or the src itself)" + ) + + +def resolve_config_root(config_or_src: Path) -> Path: + """Configuration root (parent of src when applicable).""" + src = resolve_src_root(config_or_src) + if src.name == "src": + return src.parent + return src + + +def _looks_like_src(path: Path) -> bool: + if (path / "Configuration.xml").is_file(): + return True + markers = ("Catalogs", "Documents", "DataProcessors", "Reports", "CommonModules") + return any((path / name).is_dir() for name in markers) + + +def git_toplevel(path: Path) -> Path | None: + """Nearest git work tree containing path (submodule or root project).""" + path = Path(path).resolve() + try: + proc = subprocess.run( + ["git", "-C", str(path), "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=False, + ) + except OSError: + return None + if proc.returncode != 0: + return None + top = proc.stdout.strip() + return Path(top) if top else None + + +def git_remote_url(path: Path, *, remote: str = "origin") -> str | None: + """Clone URL of `remote` for the git repo that owns path.""" + top = git_toplevel(path) + if top is None: + return None + try: + proc = subprocess.run( + ["git", "-C", str(top), "remote", "get-url", remote], + capture_output=True, + text=True, + check=False, + ) + except OSError: + return None + if proc.returncode != 0: + return None + url = proc.stdout.strip() + return url or None + + +def to_wiki_clone_url(repo_url: str) -> str: + """ + Map configuration repository URL → wiki git URL. + + Gitea / GitLab: https://host/org/repo.git → https://host/org/repo.wiki.git + git@host:org/repo.git → git@host:org/repo.wiki.git + """ + url = repo_url.strip().rstrip("/") + if not url: + raise ResolveError("empty repository URL") + + if url.endswith(".wiki.git"): + return url + + if url.endswith(".git"): + return url[: -len(".git")] + ".wiki.git" + + return url + ".wiki.git" + + +def resolve_wiki_url( + config_or_src: Path, + *, + wiki_url: str | None = None, + remote: str = "origin", +) -> str: + """Explicit --wiki-url, or derive from git remote of the configuration repo.""" + if wiki_url: + return to_wiki_clone_url(wiki_url) + + root = resolve_config_root(config_or_src) + repo = git_remote_url(root, remote=remote) + if not repo: + raise ResolveError( + f"cannot detect git remote '{remote}' for {root}; " + "pass --wiki-url explicitly (…/repo.wiki.git)" + ) + return to_wiki_clone_url(repo) + + +def slug_for_path(path: Path) -> str: + """Short filesystem-safe slug from config root name.""" + name = resolve_config_root(path).name + slug = re.sub(r"[^\w.\-]+", "-", name, flags=re.UNICODE).strip("-") + return slug or "config" diff --git a/export_1c_help.py b/export_1c_help.py index 7dcdd4e..0de8007 100644 --- a/export_1c_help.py +++ b/export_1c_help.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""CLI: export 1C Help (ru.html) to Markdown / Gitea wiki.""" +"""CLI: export 1C Help (ru.html) to Markdown / Gitea/GitLab wiki.""" from __future__ import annotations @@ -12,8 +12,20 @@ if str(TOOL_ROOT) not in sys.path: sys.path.insert(0, str(TOOL_ROOT)) from export1c_help import __status__, __version__ # noqa: E402 -from export1c_help.config_version import read_config_version # noqa: E402 +from export1c_help.config_version import ( # noqa: E402 + read_config_name, + read_config_version, +) from export1c_help.export import export_help # noqa: E402 +from export1c_help.resolve import ( # noqa: E402 + ResolveError, + git_remote_url, + git_toplevel, + resolve_config_root, + resolve_src_root, + resolve_wiki_url, + slug_for_path, +) from export1c_help.wiki import WikiPushError, prepare_wiki_clone, push_wiki # noqa: E402 @@ -21,7 +33,7 @@ def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="export_1c_help", description=( - f"Выгрузка встроенной справки 1С (Ext/Help/ru.html) в Markdown / Gitea wiki. " + f"Выгрузка встроенной справки любой конфигурации 1С в Markdown / wiki. " f"v{__version__} ({__status__})." ), ) @@ -29,29 +41,53 @@ def build_parser() -> argparse.ArgumentParser: sub = p.add_subparsers(dest="cmd", required=True) + i = sub.add_parser( + "info", + help="Показать src, версию, git remote и wiki URL для конфигурации", + ) + _add_config_path_args(i) + i.add_argument("--remote", default="origin", help="Git remote (по умолчанию origin)") + i.add_argument( + "--wiki-url", + default=None, + help="Явный wiki URL (иначе из remote конфигурации)", + ) + i.set_defaults(func=cmd_info) + c = sub.add_parser("convert", help="Конвертация Help → Markdown в каталог") _add_convert_args(c) c.set_defaults(func=cmd_convert) - w = sub.add_parser("push", help="convert + push в Gitea wiki (.wiki.git)") + w = sub.add_parser( + "push", + help="convert + push в wiki (явный --wiki-url или wiki репозитория конфигурации)", + ) _add_convert_args(w, require_out=False) w.add_argument( "--wiki-url", - required=True, - help="URL wiki-репозитория, напр. https://git.p7net.ru/1c/crm3_26.wiki.git", + default=None, + help=( + "URL wiki-репозитория (…/repo.wiki.git). " + "Если не задан — берётся из git remote конфигурации (…/repo.git → …/repo.wiki.git)" + ), + ) + w.add_argument( + "--remote", + default="origin", + help="Git remote конфигурации для авто-wiki URL (по умолчанию origin)", ) w.add_argument( "--work-dir", type=Path, default=None, - help="Локальный clone wiki (по умолчанию: /wiki_clone)", + help="Локальный clone wiki (по умолчанию: out/wiki_clone-)", ) w.add_argument( "-o", "--out", type=Path, default=None, - help="Промежуточный каталог MD (по умолчанию: ./out/wiki-md)", + help="Промежуточный каталог MD (по умолчанию: out/wiki-md-)", ) w.add_argument( "--keep-unmanaged", @@ -66,14 +102,25 @@ def build_parser() -> argparse.ArgumentParser: return p -def _add_convert_args(p: argparse.ArgumentParser, *, require_out: bool = True) -> None: - p.add_argument( +def _add_config_path_args(p: argparse.ArgumentParser) -> argparse._MutuallyExclusiveGroup: + g = p.add_mutually_exclusive_group(required=True) + g.add_argument( + "-c", + "--config", + type=Path, + help="Корень выгрузки конфигурации (каталог с src/) или сам src/", + ) + g.add_argument( "-s", "--src", type=Path, - required=True, - help="Каталог src выгрузки конфигурации (…/crm3-26/src)", + help="Каталог src выгрузки (эквивалент -c …/src)", ) + return g + + +def _add_convert_args(p: argparse.ArgumentParser, *, require_out: bool = True) -> None: + _add_config_path_args(p) if require_out: p.add_argument( "-o", @@ -111,6 +158,14 @@ def _add_convert_args(p: argparse.ArgumentParser, *, require_out: bool = True) - ) +def _config_input(args: argparse.Namespace) -> Path: + return args.config if getattr(args, "config", None) else args.src + + +def _resolve_src(args: argparse.Namespace) -> Path: + return resolve_src_root(_config_input(args)) + + def _print_stats(stats, dest: Path) -> None: print( f"OK: config={stats.config_version} " @@ -122,14 +177,42 @@ def _print_stats(stats, dest: Path) -> None: ) +def cmd_info(args: argparse.Namespace) -> int: + try: + src = _resolve_src(args) + root = resolve_config_root(src) + wiki = resolve_wiki_url(src, wiki_url=args.wiki_url, remote=args.remote) + except ResolveError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + top = git_toplevel(root) + remote_url = git_remote_url(root, remote=args.remote) + print(f"config_root: {root}") + print(f"src: {src}") + print(f"config_name: {read_config_name(src) or '—'}") + print(f"config_version: {read_config_version(src)}") + print(f"git_toplevel: {top or '—'}") + print(f"git_remote({args.remote}): {remote_url or '—'}") + print(f"wiki_url: {wiki}") + print(f"slug: {slug_for_path(src)}") + return 0 + + def cmd_convert(args: argparse.Namespace) -> int: + try: + src = _resolve_src(args) + except ResolveError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + stats = export_help( - args.src, + src, args.out, include_forms=not args.objects_only, skip_empty=not args.keep_empty, clean=args.clean, - source_label=args.source_label, + source_label=args.source_label or str(src), prev_manifest_path=args.prev_manifest, config_version=args.config_version, ) @@ -138,27 +221,40 @@ def cmd_convert(args: argparse.Namespace) -> int: def cmd_push(args: argparse.Namespace) -> int: - out = args.out or (TOOL_ROOT / "out" / "wiki-md") - work = args.work_dir or (out.parent / "wiki_clone") + try: + src = _resolve_src(args) + wiki_url = resolve_wiki_url( + src, wiki_url=args.wiki_url, remote=args.remote + ) + except ResolveError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + slug = slug_for_path(src) + out = args.out or (TOOL_ROOT / "out" / f"wiki-md-{slug}") + work = args.work_dir or (TOOL_ROOT / "out" / f"wiki_clone-{slug}") branch = args.branch + print(f"wiki_url: {wiki_url}") + print(f"src: {src}") + try: - prepare_wiki_clone(args.wiki_url, work, branch=branch) + prepare_wiki_clone(wiki_url, work, branch=branch) except WikiPushError as exc: print(f"wiki prepare failed:\n{exc}", file=sys.stderr) return 1 prev = args.prev_manifest or (work / "manifest.json") - cfg = args.config_version or read_config_version(args.src) + cfg = args.config_version or read_config_version(src) stats = export_help( - args.src, + src, out, include_forms=not args.objects_only, skip_empty=not args.keep_empty, clean=True, - source_label=args.source_label or str(args.src), - prev_manifest_path=prev if prev.is_file() else None, + source_label=args.source_label or str(src), + prev_manifest_path=prev if Path(prev).is_file() else None, config_version=cfg, ) print( @@ -178,7 +274,7 @@ def cmd_push(args: argparse.Namespace) -> int: try: push_wiki( out, - args.wiki_url, + wiki_url, work_dir=work, message=message, branch=branch, @@ -188,7 +284,7 @@ def cmd_push(args: argparse.Namespace) -> int: except WikiPushError as exc: print(f"push failed:\n{exc}", file=sys.stderr) return 1 - print(f"OK: pushed → {args.wiki_url}") + print(f"OK: pushed → {wiki_url}") return 0