ElyTree: an unread stop says why; git am skips and aborts (G-02)

A failed read of what Git is doing shows as a danger alert in Git's
words. git am's Skip and Abort had no operation name in either
catalog and panicked; both catalogs now hold it. Tests cover the
unread stop and both buttons through their confirm to Git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-10 15:36:59 -04:00
1 parent 818491ad9f
commit 6d1373fe16
5 files changed
+56 -9

No files matched your search

+49 -8
View File
@@ -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");
}
+2
View File
@@ -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", "补丁应用"),
];
+1 -1
View File
@@ -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` 是死文案
+3
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
PASS