118 lines
5.4 KiB
Python
118 lines
5.4 KiB
Python
"""为无损底座迁移保存并校验工作区的只读 Git 状态证据。"""
|
|
|
|
# 导入命令行参数解析器以支持生成和校验报告。
|
|
import argparse
|
|
# 导入 JSON 序列化工具以写入 UTF-8 状态证据。
|
|
import json
|
|
# 导入子进程工具以直接调用 Git 而不经过 shell。
|
|
import subprocess
|
|
# 导入可调用协议和类型别名支持。
|
|
from collections.abc import Callable
|
|
# 导入路径类型以约束工作区和报告位置。
|
|
from pathlib import Path
|
|
# 导入任意 JSON 对象的静态类型。
|
|
from typing import Any
|
|
|
|
# 定义只允许执行的 Git 只读子命令首参数集合。
|
|
READ_ONLY_GIT_COMMANDS = frozenset({"branch", "rev-parse", "status"})
|
|
# 定义便于测试注入的 Git 调用函数类型。
|
|
GitRunner = Callable[..., str]
|
|
|
|
|
|
# 执行受限的 Git 只读命令并返回标准输出文本。
|
|
def run_git(worktree: Path, *args: str) -> str:
|
|
# 解析目标工作区以确保 Git 和安全目录使用同一个绝对路径。
|
|
resolved = worktree.resolve()
|
|
# 拒绝空命令,避免形成未约束的 Git 调用。
|
|
if not args:
|
|
raise ValueError("git command is required")
|
|
# 拒绝任何不在白名单中的 Git 子命令。
|
|
if args[0] not in READ_ONLY_GIT_COMMANDS:
|
|
raise ValueError("git command is not read-only")
|
|
# 将安全目录限定为当前查询工作区,避免写入全局 Git 配置。
|
|
safe_directory = resolved.as_posix()
|
|
# 以参数数组执行 Git,禁止 shell 解释路径或输入内容。
|
|
result = subprocess.run(
|
|
["git", "-c", f"safe.directory={safe_directory}", "-C", str(resolved), *args],
|
|
check=True,
|
|
capture_output=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
# 返回 Git 的标准输出,供调用方以确定性方式解析。
|
|
return result.stdout
|
|
|
|
|
|
# 收集一个工作区的分支、提交与简短状态,不读取环境变量或业务数据。
|
|
def collect_workspace_state(worktree: Path, runner: GitRunner = run_git) -> dict[str, object]:
|
|
# 解析绝对路径,防止报告中出现随当前目录变化的相对路径。
|
|
resolved = worktree.resolve()
|
|
# 依次执行已白名单化的 Git 查询并构造安全状态字典。
|
|
return {
|
|
"path": str(resolved),
|
|
"branch": runner(resolved, "branch", "--show-current").strip(),
|
|
"head": runner(resolved, "rev-parse", "HEAD").strip(),
|
|
"status": runner(resolved, "status", "--short").splitlines(),
|
|
}
|
|
|
|
|
|
# 将已收集的状态以 UTF-8 JSON 写入调用者指定的报告文件。
|
|
def write_preflight_report(target: Path, states: list[dict[str, object]]) -> None:
|
|
# 确保报告父目录存在,但不创建或改动任何工作区内容。
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
# 使用稳定缩进和 UTF-8 编码写入仅由调用方提供的状态数据。
|
|
target.write_text(json.dumps(states, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
# 从磁盘读取先前报告并验证指定工作区状态完全一致。
|
|
def verify_preflight_report(target: Path, worktrees: list[Path]) -> list[str]:
|
|
# 以 UTF-8 读取 JSON 报告,避免系统默认编码影响比较。
|
|
expected_value: Any = json.loads(target.read_text(encoding="utf-8"))
|
|
# 拒绝非列表报告,防止错误文件被误当作迁移证据。
|
|
if not isinstance(expected_value, list):
|
|
raise ValueError("preflight report must be a list")
|
|
# 为每个当前工作区重新采集只读 Git 状态。
|
|
current = [collect_workspace_state(worktree) for worktree in worktrees]
|
|
# 返回 JSON 表示不同的工作区路径,空列表代表完全一致。
|
|
return [
|
|
str(item["path"])
|
|
for item, expected in zip(current, expected_value, strict=True)
|
|
if item != expected
|
|
]
|
|
|
|
|
|
# 解析 CLI 工作区参数并执行报告生成或校验。
|
|
def main() -> int:
|
|
# 创建命令行解析器并限定所有输入为显式路径。
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
# 要求调用者指定报告文件路径。
|
|
parser.add_argument("--report", type=Path, required=True)
|
|
# 允许多次提供要采集或校验的工作区路径。
|
|
parser.add_argument("--worktree", type=Path, action="append", required=True)
|
|
# 启用校验模式时不覆盖报告。
|
|
parser.add_argument("--verify", action="store_true")
|
|
# 解析用户传入的参数。
|
|
arguments = parser.parse_args()
|
|
# 在校验模式下输出差异并返回非零状态。
|
|
if arguments.verify:
|
|
# 比较当前状态和报告状态。
|
|
differences = verify_preflight_report(arguments.report, arguments.worktree)
|
|
# 输出机器和人工都可识别的校验结果。
|
|
print("UNCHANGED" if not differences else f"CHANGED: {', '.join(differences)}")
|
|
# 有任何差异时返回失败状态。
|
|
return 0 if not differences else 1
|
|
# 收集调用方明确列出的工作区状态。
|
|
states = [collect_workspace_state(worktree) for worktree in arguments.worktree]
|
|
# 写入新的迁移前证据报告。
|
|
write_preflight_report(arguments.report, states)
|
|
# 输出报告保存位置,避免输出任何敏感运行配置。
|
|
print(f"WROTE: {arguments.report}")
|
|
# 报告创建成功时返回零状态。
|
|
return 0
|
|
|
|
|
|
# 仅在脚本直接执行时运行命令行入口。
|
|
if __name__ == "__main__":
|
|
# 用 main 的返回值作为进程退出码。
|
|
raise SystemExit(main())
|