"""Fast-forward a clean Git installation from the public O1Key release repo.""" import os import subprocess from pathlib import Path PLUGIN_DIR = Path(__file__).resolve().parent.parent RELEASE_REPOSITORY_URL = "https://git.o1key.com/publisher/comfyui_o1key.git" class UpdateError(Exception): def __init__(self, code, message, suggestion, status=409): super().__init__(message) self.code = code self.suggestion = suggestion self.status = status def as_dict(self): return { "code": self.code, "error": str(self), "suggestion": self.suggestion, } def _git(*args, timeout=60, check=True): env = os.environ.copy() env["GIT_TERMINAL_PROMPT"] = "0" env["GCM_INTERACTIVE"] = "Never" try: result = subprocess.run( ["git", *args], cwd=PLUGIN_DIR, env=env, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, ) except FileNotFoundError as exc: raise UpdateError( "git_missing", "未找到 Git。", "安装 Git 后重启 ComfyUI,再重新检查更新。", 503, ) from exc except subprocess.TimeoutExpired as exc: raise UpdateError( "timeout", "连接发布仓库超时。", "检查网络连接,稍后再试。", 504, ) from exc if check and result.returncode: raise UpdateError( "git_failed", "Git 操作未完成。", "检查插件目录的 Git 状态后重试。", ) return result def _check_release(): """Inspect the release before considering any local file changes.""" if not (PLUGIN_DIR / ".git").exists(): raise UpdateError( "not_git", "当前插件不是 Git 安装。", "请从发布仓库重新以 Git 安装;现有配置文件先单独备份。", ) branch = _git("symbolic-ref", "--quiet", "--short", "HEAD", check=False) if branch.returncode or branch.stdout.strip() != "main": raise UpdateError( "wrong_branch", "当前不在 main 分支。", "请先检查并切换分支;本地分支上的修改不会自动合并。", ) old_commit = _git("rev-parse", "HEAD").stdout.strip() fetched = _git("fetch", "--no-tags", RELEASE_REPOSITORY_URL, "main", timeout=90, check=False) if fetched.returncode: raise UpdateError( "fetch_failed", "无法获取发布仓库的 main 分支。", "检查 git.o1key.com 的网络连接与仓库读取权限,稍后重试。", 503, ) new_commit = _git("rev-parse", "FETCH_HEAD").stdout.strip() if old_commit == new_commit: return old_commit, new_commit if _git("merge-base", "--is-ancestor", "HEAD", "FETCH_HEAD", check=False).returncode: raise UpdateError( "diverged", "本地提交与发布仓库已分叉,无法自动快进。", "请手动比较两个分支并合并,不要强制重置本地文件。", ) if _git("status", "--porcelain", "--untracked-files=no").stdout.strip(): raise UpdateError( "local_changes", "插件目录有未提交的代码修改。", "请先保存、提交或暂存修改;更新不会覆盖这些文件。", ) return old_commit, new_commit def check_for_update(): """Check whether the installed version can be updated without changing files.""" old_commit, new_commit = _check_release() return {"update_available": old_commit != new_commit} def update_package(): """Fetch the release main branch and fast-forward only a clean local main.""" old_commit, new_commit = _check_release() if old_commit == new_commit: return {"updated": False, "version": old_commit[:7], "requirements_changed": False} old_requirements = _git("show", "HEAD:requirements.txt", check=False).stdout new_requirements = _git("show", "FETCH_HEAD:requirements.txt", check=False).stdout merged = _git("merge", "--ff-only", "FETCH_HEAD", check=False) if merged.returncode: raise UpdateError( "merge_blocked", "更新被本地文件阻止。", "检查是否有与新版本重名的未跟踪文件,保留文件后手动处理。", ) requirements_changed = old_requirements != new_requirements return { "updated": True, "version": new_commit[:7], "requirements_changed": requirements_changed, }