""" Git 操作工具模块。 负责在 CI 环境中检出上一版本代码、获取变更文件列表及提交元信息。 """ import os import subprocess from dataclasses import dataclass from pathlib import Path from typing import List, Optional @dataclass class CommitInfo: """单次 Git 提交的元信息。""" sha: str author: str commit_time: str message: str def run_git(args: List[str], cwd: Optional[Path] = None) -> str: """ 执行 git 命令并返回标准输出。 :param args: git 子命令及参数,如 ["log", "-1", "--format=%H"] :param cwd: 工作目录,默认为当前目录 :return: 命令 stdout 文本(已 strip) :raises RuntimeError: git 命令执行失败时抛出 """ cmd = ["git"] + args result = subprocess.run( cmd, cwd=cwd, capture_output=True, text=True, encoding="utf-8", errors="replace", ) if result.returncode != 0: raise RuntimeError(f"Git 命令失败: {' '.join(cmd)}\n{result.stderr}") return result.stdout.strip() def get_current_commit() -> CommitInfo: """ 获取当前 HEAD 提交的元信息(推送人、时间等,用于通知模板)。 :return: CommitInfo 对象 """ sha = run_git(["rev-parse", "HEAD"]) author = run_git(["log", "-1", "--format=%an"]) commit_time = run_git(["log", "-1", "--format=%ci"]) message = run_git(["log", "-1", "--format=%s"]) return CommitInfo(sha=sha, author=author, commit_time=commit_time, message=message) def get_previous_commit_sha() -> Optional[str]: """ 获取上一次提交的 SHA(HEAD~1)。 若是首次提交则返回 None。 :return: 上一 commit SHA,或 None """ try: return run_git(["rev-parse", "HEAD~1"]) except RuntimeError: return None def checkout_commit(sha: str, worktree_dir: Path) -> None: """ 将指定 commit 的代码检出到独立工作目录(不影响当前工作区)。 :param sha: 目标 commit SHA :param worktree_dir: git worktree 目录 """ worktree_dir.parent.mkdir(parents=True, exist_ok=True) if worktree_dir.exists(): # 已存在则先移除旧 worktree run_git(["worktree", "remove", "--force", str(worktree_dir)]) run_git(["worktree", "add", str(worktree_dir), sha]) def get_changed_java_controller_files(base_sha: str, head_sha: str) -> List[str]: """ 获取两次提交之间变更的 Controller 相关 Java 文件路径。 :param base_sha: 基准 commit(旧版本) :param head_sha: 目标 commit(新版本) :return: 相对路径列表,如 ["src/main/java/.../UserController.java"] """ diff_output = run_git(["diff", "--name-only", base_sha, head_sha]) if not diff_output: return [] changed = [] for line in diff_output.splitlines(): line = line.strip() if line.endswith(".java") and "Controller" in line: changed.append(line.replace("\\", "/")) return changed def get_controller_files_diff(base_sha: str, head_sha: str, changed_files: List[str]) -> str: """ 获取变更 Controller 文件的 Git diff,供 LLM 审核接口参数变更时参考。 :param base_sha: 旧版本 commit SHA :param head_sha: 新版本 commit SHA :param changed_files: 变更文件相对路径列表 :return: diff 文本 """ if not changed_files: return "" try: return run_git(["diff", base_sha, head_sha, "--"] + changed_files) except RuntimeError as exc: print(f"[警告] 获取 Git diff 失败: {exc}") return "" def prepare_worktrees(repo_root: Path) -> tuple: """ 准备新旧两个版本的代码工作目录,供 AST 解析器分别扫描。 :param repo_root: 仓库根目录 :return: (新版本目录, 旧版本目录, 旧版本SHA);首次提交时旧版本目录为 None """ prev_sha = get_previous_commit_sha() current_dir = repo_root if prev_sha is None: return current_dir, None, None prev_dir = repo_root / ".gitea" / ".cache" / "prev-worktree" checkout_commit(prev_sha, prev_dir) return current_dir, prev_dir, prev_sha