"""Read-only Git access, with a dedicated deployment key and pinned host key.""" from __future__ import annotations import os import io import tarfile import shlex import subprocess from pathlib import Path from typing import Any from state import directory def git(cfg: dict[str, Any], *args: str, timeout: int = 120) -> str: env = {**os.environ, 'GIT_TERMINAL_PROMPT': '0', 'GIT_SSH_COMMAND': shlex.join([ 'ssh', '-i', cfg['ssh_key'], '-o', 'IdentitiesOnly=yes', '-o', f"UserKnownHostsFile={cfg['known_hosts']}", '-o', 'StrictHostKeyChecking=yes', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15'])} result = subprocess.run(['git', '--git-dir', str(directory(cfg) / 'repository.git'), *args], env=env, capture_output=True, text=True, timeout=timeout) if result.returncode: raise RuntimeError(f"Git 读取失败:{result.stderr.strip()[-800:]}") return result.stdout.strip() def fetch(cfg: dict[str, Any]) -> str: repo = directory(cfg) / 'repository.git' if not repo.exists(): repo.mkdir() subprocess.run(['git', 'init', '--bare', str(repo)], check=True, capture_output=True) git(cfg, 'remote', 'add', 'origin', cfg['repository']) git(cfg, 'fetch', 'origin', f"+refs/heads/{cfg['branch']}:refs/remotes/origin/{cfg['branch']}") return git(cfg, 'rev-parse', f"refs/remotes/origin/{cfg['branch']}") def commit(cfg: dict[str, Any], revision: str) -> dict[str, str]: parts = git(cfg, 'show', '-s', '--format=%H%n%cI%n%s', revision).split('\n', 2) return dict(zip(('revision', 'date', 'subject'), parts)) def paths(cfg: dict[str, Any], revision: str) -> list[str]: return git(cfg, 'ls-tree', '-r', '--name-only', revision).splitlines() def ensure_forward(cfg: dict[str, Any], current: str, target: str) -> None: git(cfg, 'merge-base', '--is-ancestor', current, target) def protected(path: str) -> bool: p = Path(path) return p.is_absolute() or '..' in p.parts or any( part in {'.git', '.venv', 'node_modules', '.state', 'uploads'} for part in p.parts ) or p.name in {'.env', '.env.prod', '.env.dev'} or p.suffix in {'.pem', '.key'} def ensure_clean(cfg: dict[str, Any], revision: str, root: Path) -> None: result = subprocess.run(['git', '--git-dir', str(directory(cfg) / 'repository.git'), 'archive', '--format=tar', revision], capture_output=True, check=True) different = [] with tarfile.open(fileobj=io.BytesIO(result.stdout)) as tar: for member in tar: if not member.isfile() or protected(member.name): continue actual = root / member.name if not actual.exists() or actual.read_bytes() != tar.extractfile(member).read(): different.append(member.name) if different: raise RuntimeError('服务器有未同步的源码,请先同步到 Gitea:' + '、'.join(different[:5]))