#!/usr/bin/env python3 """CLI: publish arbitrary markdown docs to repository wiki.""" from __future__ import annotations import argparse import re import sys from pathlib import Path TOOL_ROOT = Path(__file__).resolve().parent 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.docs_export import ( # noqa: E402 build_docs_output, clean_docs_wiki, collect_docs, format_folder_summary, format_patterns, push_docs_wiki, validate_folder, ) from export1c_help.resolve import ( # noqa: E402 ResolveError, git_remote_url, git_toplevel, resolve_src_root, resolve_wiki_url, slug_for_path, to_wiki_clone_url, ) from export1c_help.wiki import WikiPushError # noqa: E402 def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="export_docs", description=f"Публикация Markdown-документов в wiki-папки. v{__version__} ({__status__}).", ) p.add_argument("--version", action="version", version=f"%(prog)s {__version__}") sub = p.add_subparsers(dest="cmd", required=True) push = sub.add_parser("push", help="Собрать Markdown по маскам и выгрузить в wiki") _add_source_args(push) push.add_argument( "--files", nargs="+", required=True, help="Глоб-маски исходных файлов (экспортируются только .md)", ) push.add_argument( "--folder", default="", help="Папка назначения в wiki. Пусто = корень wiki (например docs или migration/docs)", ) push.add_argument("--wiki-url", default=None, help="Явный URL wiki git (иначе из remote источника)") push.add_argument("--remote", default="origin", help="Имя git remote (по умолчанию origin)") push.add_argument("--work-dir", type=Path, default=None, help="Локальный clone wiki") push.add_argument("-o", "--out", type=Path, default=None, help="Промежуточный каталог сборки") push.add_argument("--branch", default="main") push.add_argument("-m", "--message", default=None, help="Сообщение коммита") push.add_argument("--dry-run", action="store_true", help="Без git push") push.add_argument("--verbose", action="store_true", help="Печатать полный список входных файлов") push.add_argument( "--no-flat-root", action="store_true", help="Не преобразовывать пути в root-файлы (оставлять иерархию каталогов wiki)", ) push.set_defaults(func=cmd_push) clean = sub.add_parser( "clean", help="Удалить из wiki статьи, опубликованные export_docs (по --folder/manifest, без --files)", ) _add_source_args(clean) clean.add_argument( "--folder", default="", help="Папка назначения в wiki, которую чистим. Пусто = root-набор export_docs", ) clean.add_argument("--wiki-url", default=None, help="Явный URL wiki git (иначе из remote источника)") clean.add_argument("--remote", default="origin", help="Имя git remote (по умолчанию origin)") clean.add_argument("--work-dir", type=Path, default=None, help="Локальный clone wiki") clean.add_argument("--branch", default="main") clean.add_argument("-m", "--message", default=None, help="Сообщение коммита") clean.add_argument("--dry-run", action="store_true", help="Без git push") clean.set_defaults(func=cmd_clean) return p def _add_source_args(p: argparse.ArgumentParser) -> None: # Если -s/-c не указаны, wiki URL выводим из git remote текущей папки. g = p.add_mutually_exclusive_group(required=False) g.add_argument("-c", "--config", type=Path, help="Корень выгрузки конфигурации или src/") g.add_argument("-s", "--src", type=Path, help="Путь к src/") def _src_input(args: argparse.Namespace) -> Path: return args.config if args.config is not None else args.src def cmd_push(args: argparse.Namespace) -> int: try: src_input = _src_input(args) if src_input is not None: src = resolve_src_root(src_input) wiki_url = resolve_wiki_url(src, wiki_url=args.wiki_url, remote=args.remote) slug = slug_for_path(src) else: top = git_toplevel(Path.cwd()) if top is None: raise ResolveError("cannot detect git repository for current directory") repo_url = git_remote_url(top, remote=args.remote) if not repo_url: raise ResolveError(f"cannot detect git remote '{args.remote}' for {top}") wiki_url = to_wiki_clone_url(args.wiki_url or repo_url) src = top slug = re.sub(r"[^\w.\-]+", "-", top.name, flags=re.UNICODE).strip("-") or "repo" folder = validate_folder(args.folder) except (ResolveError, ValueError) as exc: print(f"error: {exc}", file=sys.stderr) return 1 out = (args.out or (TOOL_ROOT / "out" / f"wiki-docs-md-{slug}")).resolve() work = (args.work_dir or (TOOL_ROOT / "out" / f"wiki_clone-{slug}")).resolve() print(f"wiki_url: {wiki_url}") print(f"src: {src}") print(f"folder: {folder or '.'}") if args.verbose: print(f"files: {format_patterns(args.files)}") else: print(f"inputs: {len(args.files)} path args") print(f"out: {out}") print(f"work: {work}") try: items = collect_docs(args.files, folder=folder) except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return 1 if not items: print("error: no markdown files matched --files patterns", file=sys.stderr) return 1 build_stats, _manifest_name = build_docs_output( items, out_dir=out, folder=folder, flat_root=not args.no_flat_root, clean=True, ) print( f"collect: md={build_stats.markdown_files}, attachments={build_stats.attachment_files}, " f"prepared={build_stats.files_written} → {out}" ) print("folders:") print(format_folder_summary(build_stats)) msg = args.message or ( f"export_docs v{__version__}: sync docs to {folder or '/'} " f"({build_stats.files_written} files)" ) try: push_stats = push_docs_wiki( wiki_url=wiki_url, out_dir=out, work_dir=work, message=msg, folder=folder, branch=args.branch, dry_run=args.dry_run, ) except WikiPushError as exc: print(f"push failed:\n{exc}", file=sys.stderr) return 1 if args.dry_run: print( f"dry-run: files={push_stats.files_written}, deleted={push_stats.files_deleted}; push skipped" ) return 0 print( f"OK: pushed docs files={push_stats.files_written}, deleted={push_stats.files_deleted} → {wiki_url}" ) return 0 def _resolve_wiki_context(args: argparse.Namespace) -> tuple[Path, str, str]: src_input = _src_input(args) if src_input is not None: src = resolve_src_root(src_input) wiki_url = resolve_wiki_url(src, wiki_url=args.wiki_url, remote=args.remote) slug = slug_for_path(src) else: top = git_toplevel(Path.cwd()) if top is None: raise ResolveError("cannot detect git repository for current directory") repo_url = git_remote_url(top, remote=args.remote) if not repo_url: raise ResolveError(f"cannot detect git remote '{args.remote}' for {top}") wiki_url = to_wiki_clone_url(args.wiki_url or repo_url) src = top slug = re.sub(r"[^\w.\-]+", "-", top.name, flags=re.UNICODE).strip("-") or "repo" return src, wiki_url, slug def cmd_clean(args: argparse.Namespace) -> int: try: src, wiki_url, slug = _resolve_wiki_context(args) folder = validate_folder(args.folder) except (ResolveError, ValueError) as exc: print(f"error: {exc}", file=sys.stderr) return 1 work = (args.work_dir or (TOOL_ROOT / "out" / f"wiki_clone-{slug}")).resolve() print(f"wiki_url: {wiki_url}") print(f"src: {src}") print(f"folder: {folder or '.'}") print(f"work: {work}") msg = args.message or ( f"export_docs v{__version__}: cleanup docs in {folder or '/'}" ) try: stats = clean_docs_wiki( wiki_url=wiki_url, work_dir=work, folder=folder, message=msg, branch=args.branch, dry_run=args.dry_run, ) except WikiPushError as exc: print(f"clean failed:\n{exc}", file=sys.stderr) return 1 if args.dry_run: print(f"dry-run: would delete={stats.files_deleted}; push skipped") return 0 print(f"OK: cleaned docs files deleted={stats.files_deleted} → {wiki_url}") return 0 def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) return args.func(args) if __name__ == "__main__": raise SystemExit(main())