summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuriy Andamasov <yuriy@vyos.io>2026-04-27 04:56:04 +0300
committerYuriy Andamasov <yuriy@vyos.io>2026-05-06 16:18:03 +0300
commit1396d89137d5a6e6702a5696f9ea9fe63b5a8366 (patch)
tree1d54ecbe9e25ffb725f2d1c0c558d75fa61cab26
parentb73f17d61c79475c2e741f62455bb5e90ad1e287 (diff)
downloadvyos-documentation-1396d89137d5a6e6702a5696f9ea9fe63b5a8366.tar.gz
vyos-documentation-1396d89137d5a6e6702a5696f9ea9fe63b5a8366.zip
feat: add import_myst.py for importing MyST files from myst/* branches
Adds scripts/import_myst.py with import_page, git_show, list_myst_files, list_rst_files, and do_import. Imported files are written as md-{name}.md alongside existing RST files; importing is decoupled from swap activation. Adds tests/test_import_myst.py covering single-page write, identical-skip, warn-on-different-without-force, force-overwrite, and nested-path creation. All 5 tests pass on Python 3.9. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--scripts/import_myst.py246
-rw-r--r--tests/test_import_myst.py101
2 files changed, 347 insertions, 0 deletions
diff --git a/scripts/import_myst.py b/scripts/import_myst.py
new file mode 100644
index 00000000..88ba7006
--- /dev/null
+++ b/scripts/import_myst.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python3
+"""import_myst.py — Import converted MD files from myst/* branches.
+
+Usage
+-----
+ python scripts/import_myst.py --source myst/current --all
+ python scripts/import_myst.py --source myst/current --pages quick-start configuration/firewall/zone
+ python scripts/import_myst.py --source myst/current --all --force
+ python scripts/import_myst.py --source myst/current --all --dry-run
+
+Each imported file is written as md-{name}.md alongside the existing RST
+file. Importing does not activate a page — use swap_sources.py for that.
+"""
+
+import argparse
+import subprocess
+import sys
+import warnings
+from pathlib import Path
+from typing import Optional
+
+
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+DOCS_DIR = REPO_ROOT / "docs"
+
+
+# ---------------------------------------------------------------------------
+# Core helpers
+# ---------------------------------------------------------------------------
+
+def git_show(ref: str, path: str) -> Optional[bytes]:
+ """Run `git show {ref}:{path}` and return raw bytes, or None on error."""
+ result = subprocess.run(
+ ["git", "show", f"{ref}:{path}"],
+ capture_output=True,
+ cwd=REPO_ROOT,
+ )
+ if result.returncode != 0:
+ return None
+ return result.stdout
+
+
+def list_myst_files(source: str) -> list[str]:
+ """Return list of MD stems from `git ls-tree -r --name-only {source} -- docs/`.
+
+ Stems are relative to docs/ with the .md extension stripped.
+ Only .md files are returned.
+ """
+ result = subprocess.run(
+ ["git", "ls-tree", "-r", "--name-only", source, "--", "docs/"],
+ capture_output=True,
+ text=True,
+ cwd=REPO_ROOT,
+ )
+ if result.returncode != 0:
+ return []
+
+ stems = []
+ for line in result.stdout.splitlines():
+ line = line.strip()
+ if not line.endswith(".md"):
+ continue
+ # Strip leading "docs/" prefix and .md extension
+ rel = line[len("docs/"):] if line.startswith("docs/") else line
+ stem = rel[:-3] # strip ".md"
+ stems.append(stem)
+ return stems
+
+
+def list_rst_files(docs_dir: Path) -> set[str]:
+ """Return set of stems (relative to docs_dir, no extension) for all *.rst files."""
+ stems = set()
+ for rst in docs_dir.rglob("*.rst"):
+ rel = rst.relative_to(docs_dir)
+ stem = str(rel)[:-4] # strip ".rst"
+ stems.add(stem)
+ return stems
+
+
+def import_page(stem: str, md_content: bytes, docs_dir: Path, force: bool) -> bool:
+ """Write md-{name}.md for the given stem.
+
+ - Creates parent dirs as needed.
+ - Skips (returns False) if identical content already exists.
+ - Without --force: warns and skips (returns False) if different content exists.
+ - With --force: overwrites (returns True).
+ - Returns True on successful write.
+ """
+ p = Path(stem)
+ name = p.name
+ parent = p.parent
+ if str(parent) == ".":
+ target_dir = docs_dir
+ else:
+ target_dir = docs_dir / parent
+
+ dest = target_dir / f"md-{name}.md"
+
+ if dest.exists():
+ existing = dest.read_bytes()
+ if existing == md_content:
+ return False # identical — skip silently
+ if not force:
+ warnings.warn(
+ f"import_myst: {dest} exists with different content; "
+ f"use --force to overwrite",
+ stacklevel=2,
+ )
+ return False
+ # force=True — fall through to write
+
+ target_dir.mkdir(parents=True, exist_ok=True)
+ dest.write_bytes(md_content)
+ return True
+
+
+# ---------------------------------------------------------------------------
+# do_import
+# ---------------------------------------------------------------------------
+
+def do_import(
+ source: str,
+ pages: Optional[list],
+ all_pages: bool,
+ force: bool,
+ dry_run: bool,
+ docs_dir: Path,
+) -> None:
+ """Orchestrate import from source branch.
+
+ If --all: list MD files from source, intersect with RST stems in docs_dir,
+ import only those that have an RST counterpart.
+ If --pages: import only the listed stems.
+ """
+ docs_dir = Path(docs_dir)
+
+ if all_pages:
+ myst_stems = list_myst_files(source)
+ rst_stems = list_rst_files(docs_dir)
+ candidate_stems = [s for s in myst_stems if s in rst_stems]
+ else:
+ candidate_stems = list(pages or [])
+
+ imported = 0
+ skipped = 0
+
+ for stem in candidate_stems:
+ # Path inside the branch: docs/{stem}.md
+ ref_path = f"docs/{stem}.md"
+ content = git_show(source, ref_path)
+ if content is None:
+ print(
+ f"import_myst: WARNING — could not fetch {source}:{ref_path}",
+ file=sys.stderr,
+ )
+ skipped += 1
+ continue
+
+ if dry_run:
+ print(f" (dry-run) would import {stem}")
+ imported += 1
+ continue
+
+ wrote = import_page(stem, content, docs_dir, force)
+ if wrote:
+ imported += 1
+ else:
+ skipped += 1
+
+ print(
+ f"import_myst: imported {imported}, skipped {skipped}.",
+ file=sys.stderr,
+ )
+
+
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
+def main(argv=None):
+ parser = argparse.ArgumentParser(
+ description="Import MyST MD files from a myst/* branch with md- prefix."
+ )
+ parser.add_argument(
+ "--source",
+ required=True,
+ help="Git ref to import from (e.g. myst/current)",
+ )
+
+ pages_group = parser.add_mutually_exclusive_group(required=True)
+ pages_group.add_argument(
+ "--pages",
+ nargs="+",
+ metavar="STEM",
+ help="Specific page stems to import (e.g. quick-start configuration/firewall/zone)",
+ )
+ pages_group.add_argument(
+ "--all",
+ action="store_true",
+ dest="all_pages",
+ help="Import all MD files that have a matching RST counterpart",
+ )
+
+ parser.add_argument(
+ "--force",
+ action="store_true",
+ help="Overwrite existing md- files even if content differs",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Print what would be imported without writing files",
+ )
+ parser.add_argument(
+ "--docs-dir",
+ type=Path,
+ default=None,
+ help="Path to docs/ directory (default: auto-detected from script location)",
+ )
+
+ args = parser.parse_args(argv)
+
+ if args.docs_dir is None:
+ docs_dir = DOCS_DIR
+ else:
+ docs_dir = args.docs_dir.resolve()
+
+ if not docs_dir.is_dir():
+ parser.error(f"docs directory not found: {docs_dir}")
+
+ do_import(
+ source=args.source,
+ pages=args.pages,
+ all_pages=args.all_pages,
+ force=args.force,
+ dry_run=args.dry_run,
+ docs_dir=docs_dir,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_import_myst.py b/tests/test_import_myst.py
new file mode 100644
index 00000000..1d975b45
--- /dev/null
+++ b/tests/test_import_myst.py
@@ -0,0 +1,101 @@
+"""Tests for import_myst.py — run with:
+ cd tests && PYTHONPATH=../scripts python3 -m pytest test_import_myst.py -v
+"""
+import warnings
+from pathlib import Path
+
+import pytest
+
+
+# ---------------------------------------------------------------------------
+# import_page tests (no git dependency)
+# ---------------------------------------------------------------------------
+
+def test_import_single_page(tmp_path):
+ """Creates md-{name}.md with the given content."""
+ from import_myst import import_page
+
+ docs = tmp_path / "docs"
+ docs.mkdir()
+ content = b"# quick-start\nHello MyST\n"
+
+ result = import_page("quick-start", content, docs, force=False)
+
+ assert result is True
+ dest = docs / "md-quick-start.md"
+ assert dest.exists()
+ assert dest.read_bytes() == content
+
+
+def test_import_skip_identical(tmp_path):
+ """Returns False and does not modify the file when content is identical."""
+ from import_myst import import_page
+
+ docs = tmp_path / "docs"
+ docs.mkdir()
+ content = b"# quick-start\nIdentical content\n"
+ dest = docs / "md-quick-start.md"
+ dest.write_bytes(content)
+ mtime_before = dest.stat().st_mtime_ns
+
+ result = import_page("quick-start", content, docs, force=False)
+
+ assert result is False
+ assert dest.read_bytes() == content
+ assert dest.stat().st_mtime_ns == mtime_before
+
+
+def test_import_warn_different_without_force(tmp_path):
+ """Returns False and keeps old content when content differs and force=False."""
+ from import_myst import import_page
+
+ docs = tmp_path / "docs"
+ docs.mkdir()
+ old_content = b"# old content\n"
+ new_content = b"# new content\n"
+ dest = docs / "md-quick-start.md"
+ dest.write_bytes(old_content)
+
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ result = import_page("quick-start", new_content, docs, force=False)
+
+ assert result is False
+ assert dest.read_bytes() == old_content
+ assert len(caught) == 1
+ assert "force" in str(caught[0].message).lower()
+
+
+def test_import_overwrite_with_force(tmp_path):
+ """Returns True and writes new content when force=True."""
+ from import_myst import import_page
+
+ docs = tmp_path / "docs"
+ docs.mkdir()
+ old_content = b"# old content\n"
+ new_content = b"# new content\n"
+ dest = docs / "md-quick-start.md"
+ dest.write_bytes(old_content)
+
+ result = import_page("quick-start", new_content, docs, force=True)
+
+ assert result is True
+ assert dest.read_bytes() == new_content
+
+
+def test_import_nested_path(tmp_path):
+ """Creates parent dirs and writes md-{name}.md in the correct nested location."""
+ from import_myst import import_page
+
+ docs = tmp_path / "docs"
+ docs.mkdir()
+ content = b"# zone\nFirewall zone docs\n"
+
+ result = import_page("configuration/firewall/zone", content, docs, force=False)
+
+ assert result is True
+ dest = docs / "configuration" / "firewall" / "md-zone.md"
+ assert dest.exists()
+ assert dest.read_bytes() == content
+ # Parent dirs were created
+ assert (docs / "configuration" / "firewall").is_dir()