Files
mihailkudravcev cd79549c27 fix(export_docs): Gitea wiki link targets and INDEX.md pages
Strip .md from internal links, merge folder INDEX bodies, rewrite out-of-wiki repo paths to Gitea src URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 13:37:34 +03:00

266 lines
9.8 KiB
Python

#!/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
RepoLinkContext,
_git_web_base,
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)
repo_remote = git_remote_url(src if src_input is None else git_toplevel(src) or src, remote=args.remote)
repo_root = git_toplevel(src) or src
repo_ctx: RepoLinkContext | None = None
if repo_remote:
repo_ctx = RepoLinkContext(
root=repo_root.resolve(),
web_base=_git_web_base(repo_remote),
branch=args.branch,
)
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,
repo=repo_ctx,
)
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())