From a8b0870ef91207aa01410e72577bccf8709accb9 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 2 Oct 2026 11:21:54 +0800 Subject: [PATCH] fix(cli): force sync remote tags during updates --- docs/cli.md | 1 + moviepilot | 1 + scripts/local_setup.py | 7 +++- tests/test_local_setup_update.py | 68 ++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 2bc7b0115..e041415e3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -376,6 +376,7 @@ moviepilot update all --skip-resources 说明: - `update backend` 会更新 Git 仓库并重新安装后端依赖,包括 `moviepilot-rust` 加速扩展 +- 在线更新后端时会强制同步 `origin` 的全部标签,覆盖同名本地标签,以支持 Release 重建;仅存在于本地的标签会保留。分支仍仅允许快进更新,离线更新不同步标签 - 后端更新前如果检测到已跟踪源码改动,CLI 会列出部分文件并询问是否清空;确认后执行清理并继续更新,拒绝或非交互模式会取消更新 - `MOVIEPILOT_UPDATE_DEV=true` 时,`moviepilot update` 默认使用 DEV 模式;`--dev` / `--no-dev` 可临时覆盖,省略目标时更新全部组件。进程环境变量优先于配置目录中的 `app.env` - DEV 模式后端跟踪当前开发分支,处于 Release 的 detached HEAD 时回到 `v3`;前端下载最新 Release 的 `dist.zip`。显式 `--ref` / `--frontend-version` 优先于默认选择 diff --git a/moviepilot b/moviepilot index 3a416b8c3..7c446f0e9 100755 --- a/moviepilot +++ b/moviepilot @@ -210,6 +210,7 @@ Options: 说明: - 更新后端依赖时会同步更新 moviepilot-rust PyPI 依赖 + - 在线更新后端时会以 origin 强制覆盖同名本地标签;分支仍仅允许快进更新 - 后端更新前如果检测到已跟踪源码改动,CLI 会询问是否清空本地改动;确认后才会继续更新,拒绝或非交互模式会取消更新 - 可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速 EOF diff --git a/scripts/local_setup.py b/scripts/local_setup.py index 4f725f597..4f8eab857 100644 --- a/scripts/local_setup.py +++ b/scripts/local_setup.py @@ -3802,6 +3802,7 @@ def uninstall_local( def _git_output(*args: str) -> str: + """读取当前源码仓库的 Git 命令输出。""" return capture(["git", *args], cwd=ROOT) @@ -3839,14 +3840,16 @@ def _ensure_git_clean() -> None: def _update_backend_ref(ref: str, *, fetch: bool = True) -> str: - """同步后端 Git 引用;分支快进到远端,离线标签不访问网络。""" + """同步后端 Git 引用;在线强制同步标签、分支仅快进,离线不访问网络。""" if not (ROOT / ".git").exists(): raise RuntimeError("当前目录不是 Git 仓库,无法更新后端代码。") _ensure_git_clean() if fetch: print_step("获取远端更新") - run(["git", "fetch", "--tags", "origin"], cwd=ROOT) + run(["git", "fetch", "--no-tags", "origin"], cwd=ROOT) + # Release 重建可能移动同名标签;强制覆盖仅用于标签引用。 + run(["git", "fetch", "--no-tags", "origin", "+refs/tags/*:refs/tags/*"], cwd=ROOT) else: # Release 下载阶段已获取并验证标签,重启安装不得再次依赖网络。 run( diff --git a/tests/test_local_setup_update.py b/tests/test_local_setup_update.py index c60197452..94ab3f548 100644 --- a/tests/test_local_setup_update.py +++ b/tests/test_local_setup_update.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib.util +import subprocess import uuid from pathlib import Path @@ -88,3 +89,70 @@ def test_dirty_update_stops_without_an_interactive_input(monkeypatch): module._ensure_git_clean() assert commands == [] + + +@pytest.fixture +def moved_tag_repository(tmp_path): + """用临时本地仓库模拟远端重建同名标签,不访问外部网络。""" + remote = tmp_path / "remote" + checkout = tmp_path / "checkout" + remote.mkdir() + + def git(path, *args): + """隔离 Git 身份和签名配置,执行测试仓库操作。""" + return subprocess.check_output( + ["git", "-c", "user.name=CLI Test", "-c", "user.email=cli@example.test", + "-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", "-C", str(path), *args], + text=True, + ).strip() + + git(remote, "init", "-b", "v3") + git(remote, "commit", "--allow-empty", "-m", "initial") + git(remote, "tag", "v3.1.0") + old_sha = git(remote, "rev-parse", "HEAD") + git(tmp_path, "clone", str(remote), str(checkout)) + git(checkout, "tag", "local-only") + git(remote, "commit", "--allow-empty", "-m", "rebuild") + git(remote, "tag", "--force", "v3.1.0") + return checkout, git, old_sha, git(remote, "rev-parse", "HEAD") + + +@pytest.mark.parametrize("ref", ["latest", "v3", "v3.1.0"]) +def test_update_overwrites_moved_remote_tag(monkeypatch, moved_tag_repository, ref): + """同名标签随远端更新,同时保留远端没有的本地标签。""" + checkout, git, old_sha, new_sha = moved_tag_repository + module = load_local_setup_module() + monkeypatch.setattr(module, "ROOT", checkout) + + assert module._update_backend_ref(ref) == ("v3" if ref == "latest" else ref) + + assert git(checkout, "rev-parse", "v3.1.0") == new_sha + assert git(checkout, "rev-parse", "HEAD") == new_sha + assert git(checkout, "rev-parse", "local-only") == old_sha + + +def test_update_moved_tag_does_not_reset_diverged_branch(monkeypatch, moved_tag_repository): + """强制同步标签不得覆盖分叉分支上的本地提交。""" + checkout, git, _, new_sha = moved_tag_repository + git(checkout, "commit", "--allow-empty", "-m", "local work") + local_sha = git(checkout, "rev-parse", "HEAD") + module = load_local_setup_module() + monkeypatch.setattr(module, "ROOT", checkout) + + with pytest.raises(subprocess.CalledProcessError): + module._update_backend_ref("latest") + + assert git(checkout, "rev-parse", "HEAD") == local_sha + assert git(checkout, "rev-parse", "v3.1.0") == new_sha + + +def test_offline_update_preserves_local_tag(monkeypatch, moved_tag_repository): + """离线更新只使用已验证的本地标签,即使远端已更换标签。""" + checkout, git, old_sha, _ = moved_tag_repository + git(checkout, "remote", "remove", "origin") + module = load_local_setup_module() + monkeypatch.setattr(module, "ROOT", checkout) + + assert module._update_backend_ref("v3.1.0", fetch=False) == "v3.1.0" + + assert git(checkout, "rev-parse", "HEAD") == old_sha