mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-28 19:06:36 +08:00
fix(cli): confirm local changes before upgrade
This commit is contained in:
@@ -27,7 +27,7 @@ curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v3/scripts/bootst
|
||||
- 如果系统里已经有可用的 `Python 3.14+`,脚本会优先直接复用本地解释器
|
||||
- 如果系统里没有可用解释器,脚本会通过最新稳定版 uv 安装 Python 3.14
|
||||
- Linux 下安装系统依赖时通常需要 `sudo`
|
||||
- 复用已有仓库时,脚本现在只会因为已跟踪源码改动而阻止自动更新,不会再被 `.DS_Store` 之类未跟踪文件卡住
|
||||
- 复用已有仓库时,如果检测到已跟踪源码改动,脚本会先询问是否清空本地改动;确认后才会继续更新,拒绝或非交互模式会取消更新,不会删除被 Git 忽略的配置和运行时文件
|
||||
|
||||
如果安装完成后当前终端仍提示找不到 `moviepilot`:
|
||||
|
||||
@@ -357,6 +357,7 @@ moviepilot update all --skip-resources
|
||||
说明:
|
||||
|
||||
- `update backend` 会更新 Git 仓库并重新安装后端依赖,包括 `moviepilot-rust` 加速扩展
|
||||
- 后端更新前如果检测到已跟踪源码改动,CLI 会列出部分文件并询问是否清空;确认后执行清理并继续更新,拒绝或非交互模式会取消更新
|
||||
- `MOVIEPILOT_UPDATE_DEV=true` 时,`moviepilot update` 默认使用 DEV 模式;`--dev` / `--no-dev` 可临时覆盖,省略目标时更新全部组件。进程环境变量优先于配置目录中的 `app.env`
|
||||
- DEV 模式后端跟踪当前开发分支,处于 Release 的 detached HEAD 时回到 `v3`;前端下载最新 Release 的 `dist.zip`。显式 `--ref` / `--frontend-version` 优先于默认选择
|
||||
- DEV 模式每次更新前端都会重新下载并替换发布包,即使版本号相同或显式指定了 `--frontend-version`,以获取同版本重新打包的内容
|
||||
|
||||
@@ -202,6 +202,7 @@ Options:
|
||||
|
||||
说明:
|
||||
- 更新后端依赖时会同步更新 moviepilot-rust PyPI 依赖
|
||||
- 后端更新前如果检测到已跟踪源码改动,CLI 会询问是否清空本地改动;确认后才会继续更新,拒绝或非交互模式会取消更新
|
||||
- 可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -69,9 +69,19 @@ sync_repo() {
|
||||
|
||||
echo "==> 复用已有 MoviePilot 仓库: $APP_DIR"
|
||||
if repo_dirty "$APP_DIR"; then
|
||||
echo "检测到现有仓库包含未提交改动,已停止自动更新。" >&2
|
||||
echo "请先清理 $APP_DIR 的本地修改,或换一个新的安装目录后重试。" >&2
|
||||
exit 1
|
||||
echo "检测到现有仓库包含未提交的源码改动。"
|
||||
if [[ "$NON_INTERACTIVE" != "true" && "$HAS_TTY" == "true" ]] && \
|
||||
prompt_yes_no "是否清空本地改动并继续更新" "n"; then
|
||||
(
|
||||
cd "$APP_DIR"
|
||||
git reset --hard HEAD
|
||||
)
|
||||
echo "==> 已清理本地已跟踪源码改动,继续更新"
|
||||
else
|
||||
echo "已取消更新,未清理 $APP_DIR 的本地改动。" >&2
|
||||
echo "请先提交或清理本地修改,或换一个新的安装目录后重试。" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
(
|
||||
|
||||
@@ -3789,6 +3789,7 @@ def _git_output(*args: str) -> str:
|
||||
|
||||
|
||||
def _ensure_git_clean() -> None:
|
||||
"""检查源码工作树,并在用户确认后清除已跟踪的本地改动。"""
|
||||
status = _git_output("status", "--porcelain", "--untracked-files=no")
|
||||
if not status.strip():
|
||||
return
|
||||
@@ -3806,9 +3807,18 @@ def _ensure_git_clean() -> None:
|
||||
preview += " 等"
|
||||
detail = f":{preview}"
|
||||
|
||||
raise RuntimeError(
|
||||
f"检测到当前仓库有未提交的源码改动{detail},请先提交或清理后再执行更新。"
|
||||
)
|
||||
try:
|
||||
confirmed = _prompt_yes_no(
|
||||
f"检测到当前仓库有未提交的源码改动{detail},是否清空本地改动并继续更新",
|
||||
default=False,
|
||||
)
|
||||
except (EOFError, OSError) as exc:
|
||||
raise RuntimeError("当前终端不支持交互确认,已取消更新。") from exc
|
||||
if not confirmed:
|
||||
raise RuntimeError("已取消更新,未清理本地源码改动。")
|
||||
|
||||
print_step("清理本地已跟踪源码改动")
|
||||
run(["git", "reset", "--hard", "HEAD"], cwd=ROOT)
|
||||
|
||||
|
||||
def _update_backend_ref(ref: str, *, fetch: bool = True) -> str:
|
||||
|
||||
91
tests/test_local_setup_update.py
Normal file
91
tests/test_local_setup_update.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""本地 CLI 更新遇到源码改动时的确认与清理测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts" / "local_setup.py"
|
||||
|
||||
|
||||
def load_local_setup_module():
|
||||
"""加载独立的本地安装脚本模块,避免共享全局配置。"""
|
||||
module_name = f"moviepilot_local_setup_update_{uuid.uuid4().hex}"
|
||||
spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_dirty_update_clears_tracked_changes_after_confirmation(monkeypatch):
|
||||
"""确认清理后应重置已跟踪源码改动,供后续 Git 更新继续。"""
|
||||
module = load_local_setup_module()
|
||||
prompts: list[tuple[str, bool]] = []
|
||||
commands: list[list[str]] = []
|
||||
monkeypatch.setattr(module, "_git_output", lambda *_args: " M app/example.py\n")
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_prompt_yes_no",
|
||||
lambda label, default: prompts.append((label, default)) or True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"run",
|
||||
lambda command, cwd=None: commands.append(command),
|
||||
)
|
||||
|
||||
module._ensure_git_clean()
|
||||
|
||||
assert prompts == [
|
||||
(
|
||||
"检测到当前仓库有未提交的源码改动:app/example.py,是否清空本地改动并继续更新",
|
||||
False,
|
||||
)
|
||||
]
|
||||
assert commands == [["git", "reset", "--hard", "HEAD"]]
|
||||
|
||||
|
||||
def test_dirty_update_stops_when_local_change_cleanup_is_rejected(monkeypatch):
|
||||
"""拒绝清理时应取消更新且不执行任何 Git 写操作。"""
|
||||
module = load_local_setup_module()
|
||||
commands: list[list[str]] = []
|
||||
monkeypatch.setattr(module, "_git_output", lambda *_args: " M app/example.py\n")
|
||||
monkeypatch.setattr(module, "_prompt_yes_no", lambda *_args, **_kwargs: False)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"run",
|
||||
lambda command, cwd=None: commands.append(command),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="已取消更新"):
|
||||
module._ensure_git_clean()
|
||||
|
||||
assert commands == []
|
||||
|
||||
|
||||
def test_dirty_update_stops_without_an_interactive_input(monkeypatch):
|
||||
"""没有可用输入时应取消更新,而不是默认清除源码改动。"""
|
||||
module = load_local_setup_module()
|
||||
commands: list[list[str]] = []
|
||||
monkeypatch.setattr(module, "_git_output", lambda *_args: " M app/example.py\n")
|
||||
|
||||
def raise_eof(*_args, **_kwargs):
|
||||
"""模拟没有可用终端输入。"""
|
||||
raise EOFError
|
||||
|
||||
monkeypatch.setattr(module, "_prompt_yes_no", raise_eof)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"run",
|
||||
lambda command, cwd=None: commands.append(command),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="不支持交互确认"):
|
||||
module._ensure_git_clean()
|
||||
|
||||
assert commands == []
|
||||
Reference in New Issue
Block a user