cd79549c27
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>
266 lines
9.8 KiB
Python
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())
|