diff --git a/apps/ely-tree/src/tests/flows.rs b/apps/ely-tree/src/tests/flows.rs index f34c970..275db12 100644 --- a/apps/ely-tree/src/tests/flows.rs +++ b/apps/ely-tree/src/tests/flows.rs @@ -263,15 +263,9 @@ fn a_git_am_stopped_on_a_conflict_says_so() { let (root, _) = conflicted(dir.path()); let patch = sh(&root, &["format-patch", "-1", "side", "--stdout"]); std::fs::write(dir.path().join("p.patch"), patch).expect("writes"); + let patch_at = dir.path().join("p.patch").to_str().expect("utf-8").to_owned(); // `git am` stops on the conflict, as in a terminal. - let stopped = super::git().run( - &root, - &[ - "am", - "--3way", - dir.path().join("p.patch").to_str().expect("utf-8"), - ], - ); + let stopped = super::git().run(&root, &["am", "--3way", patch_at.as_str()]); assert!(stopped.is_err(), "the patch conflicts"); let mut cx = TestAppContext::single(); let (tree, cx) = window(&mut cx, dir.path()); @@ -285,6 +279,32 @@ fn a_git_am_stopped_on_a_conflict_says_so() { cx.update(|_, app| op(app)), Some((crate::ops::Op::Am, "1/1".to_string())) ); + let applying = || root.join(".git/rebase-apply").exists(); + // Skip, then abort a second stop: each asks first. + for skip in [true, false] { + if !skip { + assert!(super::git().run(&root, &["am", "--3way", patch_at.as_str()]).is_err()); + settle(cx, |app| op(app).is_some()); + } + cx.update(|_, app| { + tree.update(app, |tree, cx| match skip { + true => tree.op_skip(crate::ops::Op::Am, cx), + false => tree.op_abort(crate::ops::Op::Am, cx), + }) + }); + let write = cx.update(|_, app| match &tree.read(app).ask { + Some(Ask::Confirm { write, .. }) => write.clone(), + _ => panic!("a confirm"), + }); + assert!(applying(), "nothing runs before the yes"); + cx.update(|_, app| { + tree.update(app, |tree, cx| { + tree.ask = None; + tree.write(1, write, cx) + }) + }); + settle(cx, |_| !applying()); + } } #[test] @@ -320,3 +340,24 @@ fn a_settled_conflict_says_so_when_picked_again() { let toasts = cx.update(|_, app| shell.read(app).toasts(app)); assert!(toasts.iter().any(|(text, _)| *text == want), "{toasts:?}"); } + +#[test] +fn a_stop_that_does_not_read_says_why() { + let dir = tempfile::tempdir().expect("a temp dir"); + let root = super::committed(dir.path()); + // A merge whose other side no longer exists. + let gone = "0123456789abcdef0123456789abcdef01234567"; + std::fs::write(root.join(".git/MERGE_HEAD"), format!("{gone}\n")).expect("writes"); + let mut cx = TestAppContext::single(); + let (tree, cx) = window(&mut cx, dir.path()); + cx.update(|_, app| tree.update(app, |tree, cx| tree.open_path(root.clone(), cx))); + let unread = |app: &App| match tree.read(app).active().and_then(|o| o.doing.clone()) { + Some(Err(error)) => Some(error), + _ => None, + }; + settle(cx, |app| unread(app).is_some()); + let error = cx.update(|_, app| unread(app)).expect("an error"); + assert!(error.contains("bad object MERGE_HEAD"), "Git's own words: {error}"); + let alert = cx.update(|_, app| tree.update(app, |tree, cx| tree.op_alert(cx).is_some())); + assert!(alert, "the failure shows above the work tree"); +} diff --git a/crates/ely-ui/src/i18n/tree_ops.rs b/crates/ely-ui/src/i18n/tree_ops.rs index ffd2694..d9dd4ba 100644 --- a/crates/ely-ui/src/i18n/tree_ops.rs +++ b/crates/ely-ui/src/i18n/tree_ops.rs @@ -149,6 +149,7 @@ pub(super) const EN: &[(&str, &str)] = &[ ("tree.opname.rebase", "rebase"), ("tree.opname.cherry-pick", "cherry-pick"), ("tree.opname.revert", "revert"), + ("tree.opname.am", "patch application"), ]; pub(super) const ZH_CN: &[(&str, &str)] = &[ @@ -273,4 +274,5 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[ ("tree.opname.rebase", "变基"), ("tree.opname.cherry-pick", "拣选"), ("tree.opname.revert", "撤销"), + ("tree.opname.am", "补丁应用"), ]; diff --git a/tasks/BUGS.md b/tasks/BUGS.md index e6feb40..b72bcab 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -77,7 +77,7 @@ - [x] M-14 `render_pdf` 在界面线程内嵌图片并写文件 — 打印页的内嵌、读样式与写入经 paper::stage 在后台执行,GPUI 单测证明执行器运行前未写(审查 2 轮) - [x] M-15 打印与导出 PDF 的暂存页执行文档里的脚本 — 打印与导出 PDF 的暂存页以 WebView::open_inert 打开,脚本关;webprobe 以同页开脚本为对照(审查 2 轮) - [x] G-01 打开“文件历史”或“逐行追溯”后,中间区不再响应任何别的选择 — 每个新选择(文件、提交、侧栏、分支、位置)都清掉文件历史与逐行追溯;测试窗口先装菜单(审查 2 轮) -- [ ] G-02 进行中操作读取失败被吞掉;`git am` 冲突无提示条无错误 +- [x] G-02 进行中操作读取失败被吞掉;`git am` 冲突无提示条无错误 — 读不出进行中的操作即以 Git 原话的红色提示与日志说明;无 onto 的 rebase-apply 视为 git am,跳过、中止有文案(审查 2 轮) - [ ] G-03 新建的空仓库在“仓库”页显示两段 Git 致命错误(引用日志、LFS) - [ ] G-04 停止取回/拉取/推送失败的提示写成“克隆没有停下” - [ ] G-05 合并结果保存失败报成“Git 失败”,`tree.error.save` 是死文案 diff --git a/tasks/reviews/G-02-r1.md b/tasks/reviews/G-02-r1.md new file mode 100644 index 0000000..271d10d --- /dev/null +++ b/tasks/reviews/G-02-r1.md @@ -0,0 +1,3 @@ +FAIL + +apps/ely-tree/src/moves.rs:211 — Abort and Skip both request `tree.opname.am` for the new `Op::Am`, but neither language catalog defines that key. Open a repository where `git am --3way` stopped on a conflict, then press Abort or Skip: both handlers panic with `no message "tree.opname.am" in en-US` (also `zh-CN`) before opening their confirmation, breaking operation recovery. Native Windows and WSL probes reproduce all four action/locale combinations; logs: `.work/G-02-r1/windows-probe-actions.log` and `.work/G-02-r1/linux-probe-actions.log`. Add the operation name to both catalogs and cover both actions through their confirmation and Git command paths; the existing am regression only checks detection and misses these broken buttons. diff --git a/tasks/reviews/G-02-r2.md b/tasks/reviews/G-02-r2.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/G-02-r2.md @@ -0,0 +1 @@ +PASS