2025-12-14 22:28:37 +08:00
|
|
|
|
"""环节钩子:PlantUML 与 CHANGELOG 校验。"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
from pathlib import Path
|
2025-12-15 18:12:50 +08:00
|
|
|
|
from typing import Any
|
2025-12-14 22:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
from aide.core import output
|
|
|
|
|
|
from aide.flow.errors import FlowError
|
|
|
|
|
|
from aide.flow.git import GitIntegration
|
|
|
|
|
|
from aide.flow.types import FlowStatus
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_pre_commit_hooks(
|
|
|
|
|
|
*,
|
|
|
|
|
|
root: Path,
|
|
|
|
|
|
git: GitIntegration,
|
|
|
|
|
|
status: FlowStatus | None,
|
|
|
|
|
|
from_phase: str | None,
|
|
|
|
|
|
to_phase: str,
|
|
|
|
|
|
action: str,
|
2025-12-15 18:12:50 +08:00
|
|
|
|
config: dict[str, Any] | None = None,
|
2025-12-14 22:28:37 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
if from_phase == "flow-design" and action in {"next-part", "back-part"}:
|
2025-12-15 18:12:50 +08:00
|
|
|
|
_hook_plantuml(root=root, config=config)
|
2025-12-14 22:28:37 +08:00
|
|
|
|
if from_phase == "docs" and action in {"next-part", "back-part"}:
|
|
|
|
|
|
_hook_changelog_on_leave_docs(root=root, git=git, status=status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_post_commit_hooks(*, to_phase: str, action: str) -> None:
|
|
|
|
|
|
if to_phase == "docs" and action in {"start", "next-part", "back-part"}:
|
|
|
|
|
|
output.info("请更新 CHANGELOG.md")
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 18:12:50 +08:00
|
|
|
|
def _get_plantuml_command(config: dict[str, Any] | None) -> list[str] | None:
|
|
|
|
|
|
"""获取 PlantUML 命令,优先使用配置的 jar 文件。"""
|
|
|
|
|
|
if config:
|
|
|
|
|
|
jar_path = config.get("plantuml", {}).get("jar_path")
|
|
|
|
|
|
java_path = config.get("plantuml", {}).get("java_path", "java")
|
|
|
|
|
|
|
|
|
|
|
|
if jar_path:
|
|
|
|
|
|
# 尝试解析 jar 路径
|
|
|
|
|
|
jar_file = Path(jar_path)
|
|
|
|
|
|
if not jar_file.is_absolute():
|
|
|
|
|
|
# 相对路径,相对于 aide-program 目录
|
|
|
|
|
|
aide_program_dir = Path(__file__).parent.parent.parent
|
|
|
|
|
|
jar_file = aide_program_dir / jar_path
|
|
|
|
|
|
|
|
|
|
|
|
if jar_file.exists():
|
|
|
|
|
|
return [java_path, "-jar", str(jar_file)]
|
|
|
|
|
|
|
|
|
|
|
|
# 回退到系统 plantuml 命令
|
|
|
|
|
|
if shutil.which("plantuml"):
|
|
|
|
|
|
return ["plantuml"]
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _hook_plantuml(*, root: Path, config: dict[str, Any] | None = None) -> None:
|
|
|
|
|
|
"""PlantUML 校验和构建钩子。"""
|
|
|
|
|
|
# 获取流程图目录
|
|
|
|
|
|
diagram_path = ".aide/diagrams"
|
|
|
|
|
|
if config:
|
|
|
|
|
|
diagram_path = config.get("flow", {}).get("diagram_path", diagram_path)
|
|
|
|
|
|
|
|
|
|
|
|
diagram_dir = root / diagram_path
|
|
|
|
|
|
|
|
|
|
|
|
# 收集所有 .puml 文件
|
2025-12-14 22:28:37 +08:00
|
|
|
|
candidates: list[Path] = []
|
2025-12-15 18:12:50 +08:00
|
|
|
|
|
|
|
|
|
|
# 从配置的流程图目录
|
|
|
|
|
|
if diagram_dir.exists():
|
|
|
|
|
|
candidates.extend([p for p in diagram_dir.rglob("*.puml") if p.is_file()])
|
|
|
|
|
|
candidates.extend([p for p in diagram_dir.rglob("*.plantuml") if p.is_file()])
|
|
|
|
|
|
|
|
|
|
|
|
# 也检查 docs 和 discuss 目录(向后兼容)
|
|
|
|
|
|
for base in (root / "docs", root / "discuss"):
|
2025-12-14 22:28:37 +08:00
|
|
|
|
if not base.exists():
|
|
|
|
|
|
continue
|
|
|
|
|
|
candidates.extend([p for p in base.rglob("*.puml") if p.is_file()])
|
|
|
|
|
|
candidates.extend([p for p in base.rglob("*.plantuml") if p.is_file()])
|
|
|
|
|
|
|
|
|
|
|
|
if not candidates:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-15 18:12:50 +08:00
|
|
|
|
# 获取 PlantUML 命令
|
|
|
|
|
|
plantuml_cmd = _get_plantuml_command(config)
|
|
|
|
|
|
if plantuml_cmd is None:
|
|
|
|
|
|
output.warn("未找到 PlantUML(jar 或系统命令),已跳过校验/PNG 生成")
|
2025-12-14 22:28:37 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-15 18:12:50 +08:00
|
|
|
|
# 先校验所有文件
|
|
|
|
|
|
errors: list[str] = []
|
2025-12-14 22:28:37 +08:00
|
|
|
|
for file_path in candidates:
|
|
|
|
|
|
result = subprocess.run(
|
2025-12-15 18:12:50 +08:00
|
|
|
|
plantuml_cmd + ["-checkonly", str(file_path)],
|
2025-12-14 22:28:37 +08:00
|
|
|
|
cwd=root,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
|
detail = (result.stderr or "").strip() or (result.stdout or "").strip()
|
2025-12-15 18:12:50 +08:00
|
|
|
|
errors.append(f"{file_path.name}: {detail}")
|
|
|
|
|
|
|
|
|
|
|
|
if errors:
|
|
|
|
|
|
error_msg = "\n".join(errors)
|
|
|
|
|
|
raise FlowError(f"PlantUML 语法校验失败:\n{error_msg}")
|
|
|
|
|
|
|
|
|
|
|
|
# 校验通过,生成 PNG
|
|
|
|
|
|
for file_path in candidates:
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
|
plantuml_cmd + ["-tpng", str(file_path)],
|
|
|
|
|
|
cwd=root,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
capture_output=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
|
detail = (result.stderr or "").strip() or (result.stdout or "").strip()
|
|
|
|
|
|
raise FlowError(f"PlantUML PNG 生成失败: {file_path} {detail}".strip())
|
|
|
|
|
|
|
|
|
|
|
|
output.ok(f"PlantUML 处理完成: {len(candidates)} 个文件")
|
2025-12-14 22:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _hook_changelog_on_leave_docs(*, root: Path, git: GitIntegration, status: FlowStatus | None) -> None:
|
|
|
|
|
|
changelog = root / "CHANGELOG.md"
|
|
|
|
|
|
if not changelog.exists():
|
|
|
|
|
|
raise FlowError("离开 docs 前需要更新 CHANGELOG.md(当前文件不存在)")
|
|
|
|
|
|
|
|
|
|
|
|
git.ensure_repo()
|
|
|
|
|
|
if git.status_porcelain("CHANGELOG.md").strip():
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if status is None:
|
|
|
|
|
|
raise FlowError("离开 docs 前需要更新 CHANGELOG.md(未找到流程状态)")
|
|
|
|
|
|
|
|
|
|
|
|
for entry in status.history:
|
|
|
|
|
|
if entry.phase != "docs":
|
|
|
|
|
|
continue
|
|
|
|
|
|
if not entry.git_commit:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if git.commit_touches_path(entry.git_commit, "CHANGELOG.md"):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
raise FlowError("离开 docs 前需要更新 CHANGELOG.md(未检测到 docs 阶段的更新记录)")
|
|
|
|
|
|
|