diff --git a/CHANGELOG.md b/CHANGELOG.md index e18833d..c7be0af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ All notable changes to **export_1c_help** are documented in this file. -## [0.3.8] - 2026-07-29 +## [0.3.10] - 2026-07-29 + +### Fixed + +- `export_docs` wiki push: clear Gitea `assume-unchanged` on `Home.md` / `_Sidebar.md` before `git add`, and commit only when the index has staged changes (fixes «no changes added to commit» when nav pages were modified). +- `_Sidebar` folder links: short folder name only (`📁 meta`), without «Папка» and without full `docs/...` path. ## [0.3.9] - 2026-07-29 diff --git a/VERSION b/VERSION index 940ac09..5503126 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.9 +0.3.10 diff --git a/export1c_help/docs_export.py b/export1c_help/docs_export.py index a3effb7..4b8b566 100644 --- a/export1c_help/docs_export.py +++ b/export1c_help/docs_export.py @@ -249,6 +249,23 @@ def _encode_md_target(target: str) -> str: return quote(target, safe="/#._-~") +def _folder_label(folder_rel: Path, folder_clean: str) -> str: + if folder_rel == Path("."): + return folder_clean or "root" + return folder_rel.as_posix() + + +def _folder_short_name(folder_rel: Path, folder_clean: str) -> str: + if folder_rel == Path("."): + return folder_clean or "root" + return folder_rel.name or folder_rel.as_posix() + + +def _rel_link(from_rel: Path, to_rel: Path) -> str: + rel = os.path.relpath(to_rel.as_posix(), start=from_rel.parent.as_posix()) + return _encode_md_target(rel.replace("\\", "/")) + + def _merge_marker_block(existing: str, block: str) -> str: import re @@ -281,6 +298,53 @@ def _docs_index_rel(folder: str, managed_files: set[str]) -> str | None: return None +def _sidebar_folder_links(folder: str, managed_files: set[str]) -> list[tuple[int, str, str]]: + """ + Return (depth, label, target) for folder index pages in wiki. + + Supports both: + - nested mode: docs/sub/INDEX.md + - flat mode: docs__sub__INDEX.md + """ + folder_clean = folder.strip().strip("/").strip("\\") + rows: list[tuple[int, str, str]] = [] + + if not folder_clean: + return rows + + # nested mode + for rel in managed_files: + if not rel.endswith("/INDEX.md"): + continue + if not rel.startswith(folder_clean + "/"): + continue + folder_rel = rel[: -len("/INDEX.md")] + depth = max(folder_rel.count("/") - folder_clean.count("/"), 0) + rows.append((depth, folder_rel, rel)) + + # flat mode + root_flat = f"{folder_clean}__INDEX.md" + if root_flat in managed_files: + rows.append((0, folder_clean, root_flat)) + prefix = f"{folder_clean}__" + suffix = "__INDEX.md" + for rel in managed_files: + if not rel.startswith(prefix) or not rel.endswith(suffix) or rel == root_flat: + continue + mid = rel[len(prefix) : -len(suffix)] + if not mid: + continue + folder_rel = folder_clean + "/" + mid.replace("__", "/") + depth = max(folder_rel.count("/") - folder_clean.count("/"), 0) + rows.append((depth, folder_rel, rel)) + + # dedupe by target + uniq: dict[str, tuple[int, str, str]] = {} + for row in rows: + uniq[row[2]] = row + return sorted(uniq.values(), key=lambda x: (x[0], x[1].casefold())) + + def ensure_root_navigation( *, work_dir: Path, @@ -314,11 +378,22 @@ def ensure_root_navigation( f"- [{display}]({_encode_md_target(link_target)})", ] ) + tree = _sidebar_folder_links(folder, managed_files) + tree_lines: list[str] = [] + for depth, label, target in tree: + indent = " " * depth + short = Path(label).name or label + tree_lines.append( + f"{indent}- [📁 {short}]({_encode_md_target(target)})" + ) + if not tree_lines: + root_short = Path(link_target).name or display + tree_lines = [f"- [📁 {root_short}]({_encode_md_target(link_target)})"] sidebar_block = "\n".join( [ "### Документация (docs)", "", - f"- [{display}]({_encode_md_target(link_target)})", + *tree_lines, "", ] ) @@ -408,8 +483,23 @@ def build_docs_output( map_src_abs_to_dst_rel=map_src_abs_to_dst_rel, attachments=attachments, ) - write_text(out_dir / dst_rel, rendered) src_folder = item.dst_rel.parent + folder_index_rel = _target_rel_for( + (src_folder / "INDEX.md") if src_folder != Path(".") else Path("INDEX.md"), + flat_root=flat_root, + ) + folder_title = _folder_label(src_folder, folder_clean) + short = _folder_short_name(src_folder, folder_clean) + nav = "\n".join( + [ + f"## 📁 {short}", + "", + f"[⤴️ Вернуться в {short}]({_rel_link(dst_rel, folder_index_rel)})", + "", + ] + ) + rendered = nav + rendered.lstrip() + write_text(out_dir / dst_rel, rendered) folder_docs.setdefault(src_folder, []).append( (title_by_src[item.src.resolve()], dst_rel) ) @@ -443,21 +533,22 @@ def build_docs_output( for f in all_folders: lines: list[str] = [] - heading = f.as_posix() if f != Path(".") else (folder_clean or "docs") + heading = _folder_label(f, folder_clean) lines.append(f"# Индекс: {heading}") lines.append("") - docs = sorted(folder_docs.get(f, []), key=lambda x: x[0].casefold()) - if docs: - lines.append("## Статьи") - lines.append("") - for title, target_rel in docs: - rel_link = os.path.relpath( - target_rel.as_posix(), - start=index_target_by_folder[f].parent.as_posix(), - ).replace("\\", "/") - lines.append(f"- [{title}]({_encode_md_target(rel_link)})") - lines.append("") + if f != Path("."): + parent = f.parent if f.parent != Path(".") else Path(".") + parent_label = _folder_label(parent, folder_clean) + parent_short = _folder_short_name(parent, folder_clean) + parent_target = index_target_by_folder.get(parent) + if parent_target is not None: + lines.append(f"## 📁 {parent_short}") + lines.append("") + lines.append( + f"[⤴️ Вернуться в {parent_short}]({_rel_link(index_target_by_folder[f], parent_target)})" + ) + lines.append("") children = sorted(child_folders.get(f, set()), key=lambda p: p.as_posix().casefold()) if children: @@ -465,11 +556,18 @@ def build_docs_output( lines.append("") for ch in children: target_rel = index_target_by_folder[ch] - rel_link = os.path.relpath( - target_rel.as_posix(), - start=index_target_by_folder[f].parent.as_posix(), - ).replace("\\", "/") - lines.append(f"- [{ch.as_posix()}]({_encode_md_target(rel_link)})") + ch_label = _folder_short_name(ch, folder_clean) + rel_link = _rel_link(index_target_by_folder[f], target_rel) + lines.append(f"- [📁 {ch_label}]({rel_link})") + lines.append("") + + docs = sorted(folder_docs.get(f, []), key=lambda x: x[0].casefold()) + if docs: + lines.append("## Статьи") + lines.append("") + for title, target_rel in docs: + rel_link = _rel_link(index_target_by_folder[f], target_rel) + lines.append(f"- [{title}]({rel_link})") lines.append("") idx_target = index_target_by_folder[f] @@ -511,6 +609,32 @@ def _run_git(cmd: list[str], *, cwd: Path) -> str: return proc.stdout +def _wiki_clear_index_flags(work_dir: Path, *names: str) -> None: + """Gitea may mark Home/_Sidebar assume-unchanged; git add then skips them.""" + for name in names: + if not (work_dir / name).is_file(): + continue + _run_git(["git", "update-index", "--no-assume-unchanged", name], cwd=work_dir) + _run_git(["git", "update-index", "--no-skip-worktree", name], cwd=work_dir) + + +def _git_has_staged_changes(work_dir: Path) -> bool: + proc = subprocess.run( + ["git", "diff", "--cached", "--quiet"], + cwd=str(work_dir), + capture_output=True, + text=True, + check=False, + ) + return proc.returncode != 0 + + +def _git_stage_and_ready(work_dir: Path) -> bool: + _wiki_clear_index_flags(work_dir, "Home.md", "_Sidebar.md") + _run_git(["git", "add", "-A"], cwd=work_dir) + return _git_has_staged_changes(work_dir) + + def _load_prev_managed(work_dir: Path, manifest_name: str) -> set[str]: p = work_dir / manifest_name if not p.is_file(): @@ -565,9 +689,18 @@ def push_docs_wiki( managed_files=new_files, ) - _run_git(["git", "add", "-A"], cwd=work_dir) - status = _run_git(["git", "status", "--porcelain"], cwd=work_dir) - if not status.strip() or dry_run: + if dry_run: + _wiki_clear_index_flags(work_dir, "Home.md", "_Sidebar.md") + _run_git(["git", "add", "-A"], cwd=work_dir) + return DocsStats( + files_total=len(new_files), + files_written=len(new_files), + files_deleted=deleted, + markdown_files=0, + attachment_files=0, + folders={}, + ) + if not _git_stage_and_ready(work_dir): return DocsStats( files_total=len(new_files), files_written=len(new_files), @@ -634,9 +767,18 @@ def clean_docs_wiki( if cleaned != raw: write_text(p, cleaned) - _run_git(["git", "add", "-A"], cwd=work_dir) - status = _run_git(["git", "status", "--porcelain"], cwd=work_dir) - if not status.strip() or dry_run: + if dry_run: + _wiki_clear_index_flags(work_dir, "Home.md", "_Sidebar.md") + _run_git(["git", "add", "-A"], cwd=work_dir) + return DocsStats( + files_total=deleted, + files_written=0, + files_deleted=deleted, + markdown_files=0, + attachment_files=0, + folders={}, + ) + if not _git_stage_and_ready(work_dir): return DocsStats( files_total=deleted, files_written=0,