fix(cli): force sync remote tags during updates

This commit is contained in:
jxxghp
2026-10-02 11:21:54 +08:00
parent 5b0863819f
commit a8b0870ef9
4 changed files with 75 additions and 2 deletions

View File

@@ -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` 优先于默认选择

View File

@@ -210,6 +210,7 @@ Options:
说明:
- 更新后端依赖时会同步更新 moviepilot-rust PyPI 依赖
- 在线更新后端时会以 origin 强制覆盖同名本地标签;分支仍仅允许快进更新
- 后端更新前如果检测到已跟踪源码改动,CLI 会询问是否清空本地改动;确认后才会继续更新,拒绝或非交互模式会取消更新
- 可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速
EOF

View File

@@ -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(

View File

@@ -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