"""把成品章节与年表推送到自建 Gitea。 - 推送凭据复用现有 ~/.ssh/id_ed25519(已验证在 Gitea 注册为 gitadmin 的 chendy-ubuntu-wsl)。 - 原始 legends 数据与存档不入库(见 .gitignore)。 - 自动直推,不设人工审核环节(按访谈决策)。 """ from __future__ import annotations import subprocess from pathlib import Path from dfannals.config import GITEA_REMOTE, PROJECT_DIR, SSH_KEY GITIGNORE = """\ # 原始数据与存档:不入库 data/ vendor/ __pycache__/ *.pyc *.log """ def _git(*args: str, check: bool = True) -> subprocess.CompletedProcess: env = { "GIT_SSH_COMMAND": f"ssh -i {SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new", "GIT_TERMINAL_PROMPT": "0", } proc = subprocess.run( ["git", *args], cwd=PROJECT_DIR, env={**_base_env(), **env}, capture_output=True, text=True, check=False, # 下面是自定义错误处理,故意不用 check=True ) if check and proc.returncode != 0: raise RuntimeError(f"git {' '.join(args)} 失败:{proc.stderr.strip() or proc.stdout.strip()}") return proc def _base_env() -> dict: import os env = dict(os.environ) env.pop("GIT_DIR", None) return env def ensure_repo() -> None: """初始化仓库、写 .gitignore、设置 remote 与提交身份。""" if not (PROJECT_DIR / ".git").is_dir(): _git("init", "-b", "main") (PROJECT_DIR / ".gitignore").write_text(GITIGNORE) remotes = _git("remote").stdout.split() if "origin" in remotes: _git("remote", "set-url", "origin", GITEA_REMOTE) else: _git("remote", "add", "origin", GITEA_REMOTE) if not _git("config", "user.name", check=False).stdout.strip(): _git("config", "user.name", "DF Annals Bot") if not _git("config", "user.email", check=False).stdout.strip(): _git("config", "user.email", "df-annals@hajim1.art") def _tracked_empty_sources() -> list[str]: """已跟踪的源码/文档里,哪些是 0 字节。""" suffixes = (".py", ".sh", ".lua", ".md", ".toml", ".json") out: list[str] = [] for rel in _git("ls-files").stdout.split(): if not rel.endswith(suffixes): continue path = PROJECT_DIR / rel if path.is_file() and path.stat().st_size == 0: out.append(rel) return out def commit_and_push(message: str, paths: list[Path] | None = None) -> str: """提交并推送。返回提交哈希;无改动时返回空串。 提交前硬拦空文件:实测发生过一次 cli.py 被提交为 0 字节的事故—— 而 cli.py 是唯一入口,于是远端仓库里的管道完全不可运行,且失败是静默的。 这种错误不能靠人复核,必须在提交口拦住。 """ empty = _tracked_empty_sources() if empty: raise RuntimeError("拒绝提交:以下已跟踪文件是空的 → " + "、".join(empty)) if paths: for p in paths: _git("add", "--", str(p.relative_to(PROJECT_DIR)) if p.is_absolute() else str(p)) else: _git("add", "-A") if not _git("status", "--porcelain").stdout.strip(): return "" _git("commit", "-m", message) head = _git("rev-parse", "--short", "HEAD").stdout.strip() _git("push", "-u", "origin", "HEAD:main") return head