fix: clarify worktree removal failures and recover partial removals

Report initialized submodules as their own force-required cause and avoid shutting down panes before Git can refuse an unforced removal. Forced removals now clean up a checkout that Git unregistered but could not delete.

refs #5478
This commit is contained in:
Jonathan Liebig
2026-10-22 18:06:37 +02:00
parent 5bd900d88c
commit 4a41fa5cad
12 changed files with 277 additions and 66 deletions
@@ -183,7 +183,7 @@ herdr worktree remove --workspace ID [--force] [--trust-repository]
Worktrees are normal Herdr workspaces with Git checkout provenance. `worktree create` creates a Git worktree checkout, opens it as a workspace, and groups it with the parent repo workspace. If `--branch` names an existing local branch, Herdr checks it out; otherwise it creates the branch from `--base` or `HEAD`. Without `--path`, Herdr creates the checkout under `<worktrees.directory>/<repo>/<branch-slug>`.
`workspace close` closes only Herdr state. Closing a primary workspace while linked-worktree workspaces are open requires `--group`; without it, the command leaves the group open and returns `workspace_group_close_required`. To delete the checkout, run `worktree remove`. It runs `git worktree remove`, never deletes the branch, and requires `--force` when Git refuses a dirty checkout.
`workspace close` closes only Herdr state. Closing a primary workspace while linked-worktree workspaces are open requires `--group`; without it, the command leaves the group open and returns `workspace_group_close_required`. To delete the checkout, run `worktree remove`. It runs `git worktree remove`, never deletes the branch, and requires `--force` when Git refuses a dirty checkout or a checkout with initialized submodules.
Git rejects repositories owned by another user by default. If you have independently verified the repository, pass `--trust-repository` to trust its resolved path for that command only. Herdr does not change your Git configuration.
@@ -179,7 +179,7 @@ herdr worktree remove --workspace ID [--force] [--trust-repository]
worktree は、Git チェックアウトの出自情報を持つ通常の Herdr ワークスペースです。`worktree create` は Git worktree のチェックアウトを作成し、ワークスペースとして開き、親リポジトリのワークスペースとグループ化します。`--branch` が既存のローカルブランチを指す場合はそれをチェックアウトし、そうでなければ `--base` または `HEAD` からブランチを作成します。`--path` がない場合、チェックアウトは `<worktrees.directory>/<repo>/<branch-slug>` の下に作成されます。
`workspace close` は Herdr の状態だけを閉じます。リンクされた worktree ワークスペースが開いている間に親ワークスペースを閉じるには `--group` が必要です。指定しない場合、グループは開いたままになり `workspace_group_close_required` が返されます。`worktree remove` が明示的なチェックアウト削除の経路です。`git worktree remove` を実行し、ブランチは決して削除せず、Git がダーティなチェックアウトを拒否する場合は `--force` が必要です。
`workspace close` は Herdr の状態だけを閉じます。リンクされた worktree ワークスペースが開いている間に親ワークスペースを閉じるには `--group` が必要です。指定しない場合、グループは開いたままになり `workspace_group_close_required` が返されます。`worktree remove` が明示的なチェックアウト削除の経路です。`git worktree remove` を実行し、ブランチは決して削除せず、Git がダーティなチェックアウト、または初期化済みサブモジュールを含むチェックアウトを拒否する場合は `--force` が必要です。
Git はデフォルトで、別のユーザーが所有するリポジトリを拒否します。リポジトリを別途確認済みの場合は `--trust-repository` を渡すと、解決されたパスをそのコマンドでのみ信頼します。Herdr は Git 設定を変更しません。
@@ -266,7 +266,7 @@ reason の候補は `shown`、`disabled`、`rate_limited`、`no_foreground_clien
`client.window_title.clear` はタイトルを `ui.window_title` に戻します。レスポンスは `type: "client_window_title"` で、`changed` と、`set`、`cleared`、`no_foreground_client` のいずれかの reason を含みます。
Worktree メソッドは Git チェックアウトを Herdr ワークスペースとして管理します。`worktree.create` はチェックアウトを作成し、新しい `workspace`、`tab`、`root_pane`、`worktree` のレコードを返します。要求されたブランチがローカルに既存ならそれをチェックアウトし、なければ要求されたベースまたは `HEAD` からブランチを作成します。`worktree.open` は既存のチェックアウトを開くか、すでに開いているワークスペースを返します。`worktree.remove` はリンクされた子ワークスペースに対して `git worktree remove` を実行し、ブランチは決して削除しません。
Worktree メソッドは Git チェックアウトを Herdr ワークスペースとして管理します。`worktree.create` はチェックアウトを作成し、新しい `workspace`、`tab`、`root_pane`、`worktree` のレコードを返します。要求されたブランチがローカルに既存ならそれをチェックアウトし、なければ要求されたベースまたは `HEAD` からブランチを作成します。`worktree.open` は既存のチェックアウトを開くか、すでに開いているワークスペースを返します。`worktree.remove` はリンクされた子ワークスペースに対して `git worktree remove` を実行し、ブランチは決して削除しません。Git が安全な削除を拒否した場合は `dirty_worktree_requires_force` または `submodule_worktree_requires_force` を返します。失われる作業を確認したうえで `force: true` で再試行してください。
元のワークスペースから worktree を作成する:
@@ -341,7 +341,7 @@ Set or clear the foreground client's outer terminal window title:
`type: "client_window_title"` with `changed` and reason `set`, `cleared`, or
`no_foreground_client`.
Worktree methods manage Git checkouts as Herdr workspaces. `worktree.create` creates a checkout and returns the new `workspace`, `tab`, `root_pane`, and `worktree` records. If the requested branch already exists locally, it checks out that branch; otherwise it creates the branch from the requested base or `HEAD`. `worktree.open` opens an existing checkout or returns the already-open workspace. `worktree.remove` runs `git worktree remove` against a linked child workspace and never deletes the branch.
Worktree methods manage Git checkouts as Herdr workspaces. `worktree.create` creates a checkout and returns the new `workspace`, `tab`, `root_pane`, and `worktree` records. If the requested branch already exists locally, it checks out that branch; otherwise it creates the branch from the requested base or `HEAD`. `worktree.open` opens an existing checkout or returns the already-open workspace. `worktree.remove` runs `git worktree remove` against a linked child workspace and never deletes the branch. It returns `dirty_worktree_requires_force` or `submodule_worktree_requires_force` when Git refuses a safe removal; check for work that would be lost before retrying with `force: true`.
Create a worktree from a source workspace:
@@ -179,7 +179,7 @@ herdr worktree remove --workspace ID [--force] [--trust-repository]
worktree 是带有 Git 检出来源信息的普通 Herdr 工作区。`worktree create` 创建一个 Git worktree 检出,作为工作区打开,并与父仓库工作区分到一组。如果 `--branch` 指向已有的本地分支,Herdr 检出它;否则从 `--base` 或 `HEAD` 创建分支。没有 `--path` 时,Herdr 在 `<worktrees.directory>/<repo>/<branch-slug>` 下创建检出。
`workspace close` 只关闭 Herdr 状态。当关联的 worktree 工作区仍然打开时,关闭父工作区需要 `--group`;否则命令会保留整个组并返回 `workspace_group_close_required`。`worktree remove` 是显式的检出删除路径;它运行 `git worktree remove`,从不删除分支,并在 Git 拒绝脏检出时要求 `--force`。
`workspace close` 只关闭 Herdr 状态。当关联的 worktree 工作区仍然打开时,关闭父工作区需要 `--group`;否则命令会保留整个组并返回 `workspace_group_close_required`。`worktree remove` 是显式的检出删除路径;它运行 `git worktree remove`,从不删除分支,并在 Git 拒绝脏检出或包含已初始化子模块的检出时要求 `--force`。
Git 默认拒绝由其他用户拥有的仓库。如果你已独立验证该仓库,可传入 `--trust-repository`,仅在本次命令中信任解析后的仓库路径。Herdr 不会修改你的 Git 配置。
@@ -266,7 +266,7 @@ herdr notification show "build failed" --body "api workspace" --position top-lef
`client.window_title.clear` 把标题交还给 `ui.window_title`。响应是 `type: "client_window_title"`,带 `changed` 和 `set`、`cleared` 或 `no_foreground_client` 之一的 reason。
Worktree 方法把 Git 检出作为 Herdr 工作区管理。`worktree.create` 创建检出,并返回新的 `workspace`、`tab`、`root_pane` 和 `worktree` 记录。请求的分支已在本地存在时检出它;否则从请求的 base 或 `HEAD` 创建分支。`worktree.open` 打开已有检出,或返回已打开的工作区。`worktree.remove` 对关联的子工作区运行 `git worktree remove`,从不删除分支。
Worktree 方法把 Git 检出作为 Herdr 工作区管理。`worktree.create` 创建检出,并返回新的 `workspace`、`tab`、`root_pane` 和 `worktree` 记录。请求的分支已在本地存在时检出它;否则从请求的 base 或 `HEAD` 创建分支。`worktree.open` 打开已有检出,或返回已打开的工作区。`worktree.remove` 对关联的子工作区运行 `git worktree remove`,从不删除分支。Git 拒绝安全删除时,它会返回 `dirty_worktree_requires_force` 或 `submodule_worktree_requires_force`;确认可能丢失的工作后,使用 `force: true` 重试。
从来源工作区创建 worktree:
+101
View File
@@ -2123,6 +2123,107 @@ mod tests {
let _ = std::fs::remove_dir_all(repo);
}
#[tokio::test]
async fn api_worktree_remove_reports_submodule_force_requirement_without_shutting_down_panes() {
let repo = create_committed_repo("api-worktree-remove-submodule-repo");
let submodule = create_committed_repo("api-worktree-remove-submodule-source");
let checkout = unique_temp_path("api-worktree-remove-submodule-checkout");
run_git(
&repo,
&[
"-c",
"protocol.file.allow=always",
"submodule",
"add",
"--quiet",
submodule.to_str().unwrap(),
"sub",
],
);
run_git(&repo, &["commit", "--quiet", "-am", "add submodule"]);
run_git(
&repo,
&[
"worktree",
"add",
"--quiet",
"-b",
"worktree/api-remove-submodule",
checkout.to_str().unwrap(),
"HEAD",
],
);
run_git(
&checkout,
&[
"-c",
"protocol.file.allow=always",
"submodule",
"update",
"--init",
"--quiet",
],
);
let mut app = app_with_parent(&repo);
let mut child = Workspace::test_new("child");
child.identity_cwd = checkout.clone();
child.worktree_space = Some(crate::workspace::WorktreeSpaceMembership {
key: crate::workspace::git_space_metadata(&repo).unwrap().key,
label: "api-worktree-remove-submodule-repo".into(),
repo_root: repo.clone(),
checkout_path: checkout.clone(),
is_linked_worktree: true,
});
let child_id = child.id.clone();
let pane_id = child.tabs[0].root_pane;
let terminal_id = child.terminal_id(pane_id).cloned().unwrap();
app.state.workspaces.push(child);
app.state.ensure_test_terminals();
let (runtime, _input_rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24);
app.terminal_runtimes.insert(terminal_id.clone(), runtime);
let response = run_deferred_api_request(
&mut app,
Request {
id: "req".into(),
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id.clone(),
force: false,
trust_repository: false,
}),
},
);
let error: ErrorResponse = serde_json::from_str(&response).unwrap();
assert_eq!(error.error.code, "submodule_worktree_requires_force");
assert!(checkout.exists());
assert!(app.terminal_runtimes.get(&terminal_id).is_some());
assert!(app.pending_worktree_remove_runtime_exits.is_empty());
let response = run_deferred_api_request(
&mut app,
Request {
id: "req".into(),
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id,
force: true,
trust_repository: false,
}),
},
);
let success: SuccessResponse = serde_json::from_str(&response).unwrap();
let ResponseResult::WorktreeRemoved { forced, path, .. } = success.result else {
panic!("expected worktree_removed response");
};
assert!(forced);
assert_eq!(path, checkout.display().to_string());
assert!(!checkout.exists());
assert_eq!(app.state.workspaces.len(), 1);
let _ = std::fs::remove_dir_all(repo);
let _ = std::fs::remove_dir_all(submodule);
}
#[test]
fn api_worktree_remove_emits_close_event_and_drains_runtime_shutdowns() {
let repo = create_committed_repo("api-worktree-remove-event-repo");
+40 -18
View File
@@ -293,22 +293,37 @@ impl App {
#[cfg(windows)]
{
if !params.force
&& crate::worktree::checkout_has_dirty_files(
if !params.force {
if crate::worktree::checkout_has_dirty_files(
&space.checkout_path,
params.trust_repository,
)
.unwrap_or(false)
{
Self::send_api_response(
respond_to,
encode_error(
id,
"dirty_worktree_requires_force",
crate::worktree::worktree_dirty_remove_message(&space.checkout_path),
),
);
return;
{
Self::send_api_response(
respond_to,
encode_error(
id,
"dirty_worktree_requires_force",
crate::worktree::worktree_dirty_remove_message(&space.checkout_path),
),
);
return;
}
if crate::worktree::checkout_has_initialized_submodules(
&space.checkout_path,
params.trust_repository,
) {
Self::send_api_response(
respond_to,
encode_error(
id,
"submodule_worktree_requires_force",
crate::worktree::SUBMODULE_WORKTREE_REMOVE_ERROR,
),
);
return;
}
}
}
@@ -386,12 +401,16 @@ impl App {
"herdr-worktree-remove",
finished,
move |mut finished| {
// The workspace membership was verified before spawning this
// thread, so a forced request may clean a checkout that Git has
// already unregistered but could not delete.
finished.result = crate::worktree::run_worktree_remove_command_with_recovery(
&command,
&repo_root,
&finished.path,
finished.forced,
trust_repository,
finished.forced,
);
let _ =
event_tx.blocking_send(AppEvent::WorktreeRemoveFinished(Box::new(finished)));
@@ -554,12 +573,15 @@ impl App {
api.operation_id,
&result.path,
);
let code =
if !result.forced && crate::worktree::is_dirty_worktree_remove_error(&message) {
"dirty_worktree_requires_force"
} else {
"worktree_remove_failed"
};
let code = if !result.forced
&& crate::worktree::is_submodule_worktree_remove_error(&message)
{
"submodule_worktree_requires_force"
} else if !result.forced && crate::worktree::is_dirty_worktree_remove_error(&message) {
"dirty_worktree_requires_force"
} else {
"worktree_remove_failed"
};
Self::send_api_response(api.respond_to, encode_error(api.id, code, message));
return pane_updates;
}
@@ -1422,6 +1422,11 @@ fn worktree_open_filters_and_clicks_a_stable_public_entry() {
fn worktree_remove_escalates_recoverable_failure_to_force_confirmation() {
for (code, message, expect_force) in [
("dirty_worktree_requires_force", "dirty worktree", true),
(
"submodule_worktree_requires_force",
"submodule worktree",
true,
),
(
"worktree_remove_failed",
"fatal: '/repo-feature' is not a working tree",
+1 -1
View File
@@ -353,7 +353,7 @@ pub(super) fn render_worktree_remove_overlay(
inner.x,
inner.y + 4,
inner.width,
" Dirty or untracked files will be permanently deleted.",
" Dirty files and unpushed submodule work will be permanently deleted.",
Style::default().fg(p.red).bg(p.panel_bg),
);
}
+5 -3
View File
@@ -497,9 +497,11 @@ impl ClientShellState {
true
}
(PendingEndpointKind::WorktreeRemove { forced: false }, Err(error))
if error.code.as_deref() == Some("dirty_worktree_requires_force")
|| (error.code.as_deref() == Some("worktree_remove_failed")
&& crate::worktree::is_not_working_tree_remove_error(&error.message)) =>
if matches!(
error.code.as_deref(),
Some("dirty_worktree_requires_force" | "submodule_worktree_requires_force")
) || (error.code.as_deref() == Some("worktree_remove_failed")
&& crate::worktree::is_not_working_tree_remove_error(&error.message)) =>
{
if let Some(ClientShellOverlay::WorktreeRemove(remove)) = self.overlay.as_mut() {
remove.removing = false;
+119 -38
View File
@@ -191,11 +191,20 @@ pub(crate) fn build_worktree_remove_command(
}
}
#[cfg(windows)]
pub(crate) const SUBMODULE_WORKTREE_REMOVE_ERROR: &str =
"fatal: working trees containing submodules cannot be moved or removed";
pub(crate) fn is_dirty_worktree_remove_error(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
(lower.contains("contains modified or untracked files")
&& lower.contains("use --force to delete it"))
|| lower.contains("working trees containing submodules cannot be moved or removed")
lower.contains("contains modified or untracked files")
&& lower.contains("use --force to delete it")
}
pub(crate) fn is_submodule_worktree_remove_error(message: &str) -> bool {
message
.to_ascii_lowercase()
.contains("working trees containing submodules cannot be moved or removed")
}
pub(crate) fn is_not_working_tree_remove_error(message: &str) -> bool {
@@ -236,6 +245,26 @@ pub(crate) fn checkout_has_dirty_files(
}
}
#[cfg(any(windows, test))]
pub(crate) fn checkout_has_initialized_submodules(path: &Path, trust_repository: bool) -> bool {
if !path.join(".gitmodules").is_file() {
return false;
}
let Ok(output) = repository_git_command(path, trust_repository)
.args(["submodule", "status", "--recursive"])
.output()
else {
return false;
};
if !output.status.success() {
return false;
}
// `git submodule status` prefixes uninitialized entries with `-`.
String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| matches!(line.as_bytes().first(), Some(b' ' | b'+' | b'U')))
}
pub(crate) fn build_worktree_add_new_branch_command(
repo_root: &Path,
path: &Path,
@@ -349,52 +378,69 @@ pub(crate) fn run_worktree_remove_command_with_recovery(
path: &Path,
force: bool,
trust_repository: bool,
allow_unregistered_leftover: bool,
) -> Result<(), String> {
// Capture the checkout identity before Git removes `.git`. A forced removal
// can unregister the worktree and then fail to delete the directory, at
// which point the checkout no longer proves it belongs to this repository.
let checkout_gitdir = force.then(|| checkout_gitdir(path)).flatten();
match run_worktree_command(command) {
Ok(()) => Ok(()),
Err(err) if force && is_not_working_tree_remove_error(&err) => {
Err(err) if force => {
if worktree_list_contains_path(repo_root, path, trust_repository)? {
return Err(err);
}
if path.exists() {
if !leftover_worktree_checkout_matches_repo(repo_root, path, trust_repository) {
return Err(err);
}
std::fs::remove_dir_all(path).map_err(|remove_err| {
format!(
"{err}; failed to remove leftover checkout {}: {remove_err}",
path.display()
)
})?;
if !path.exists() {
return Ok(());
}
let known_checkout = checkout_gitdir
.as_deref()
.is_some_and(|gitdir| gitdir_belongs_to_repo(gitdir, repo_root, trust_repository))
|| leftover_worktree_checkout_matches_repo(repo_root, path, trust_repository)
|| allow_unregistered_leftover;
if !known_checkout {
return Err(err);
}
std::fs::remove_dir_all(path).map_err(|remove_err| {
format!(
"{err}; failed to remove leftover checkout {}: {remove_err}",
path.display()
)
})?;
Ok(())
}
Err(err) => Err(err),
}
}
fn checkout_gitdir(path: &Path) -> Option<PathBuf> {
let content = std::fs::read_to_string(path.join(".git")).ok()?;
let gitdir = content.trim().strip_prefix("gitdir:")?.trim();
if gitdir.is_empty() {
return None;
}
let gitdir = PathBuf::from(gitdir);
Some(if gitdir.is_absolute() {
gitdir
} else {
path.join(gitdir)
})
}
fn gitdir_belongs_to_repo(gitdir: &Path, repo_root: &Path, trust_repository: bool) -> bool {
let Some(worktrees_dir) = git_common_worktrees_dir(repo_root, trust_repository) else {
return false;
};
canonical_or_original(gitdir).starts_with(canonical_or_original(&worktrees_dir))
}
fn leftover_worktree_checkout_matches_repo(
repo_root: &Path,
path: &Path,
trust_repository: bool,
) -> bool {
let git_file = path.join(".git");
let Ok(content) = std::fs::read_to_string(&git_file) else {
return false;
};
let Some(gitdir) = content.trim().strip_prefix("gitdir:") else {
return false;
};
let gitdir = PathBuf::from(gitdir.trim());
let gitdir = if gitdir.is_absolute() {
gitdir
} else {
path.join(gitdir)
};
let Some(worktrees_dir) = git_common_worktrees_dir(repo_root, trust_repository) else {
return false;
};
canonical_or_original(&gitdir).starts_with(canonical_or_original(&worktrees_dir))
checkout_gitdir(path)
.is_some_and(|gitdir| gitdir_belongs_to_repo(&gitdir, repo_root, trust_repository))
}
fn git_common_worktrees_dir(repo_root: &Path, trust_repository: bool) -> Option<PathBuf> {
@@ -871,13 +917,19 @@ prunable stale
assert!(!is_dirty_worktree_remove_error(
"fatal: '/w/herdr' contains a locked worktree, use --force only if you know why"
));
assert!(!is_dirty_worktree_remove_error(
"fatal: working trees containing submodules cannot be moved or removed"
));
}
#[test]
fn submodule_remove_error_requires_force_confirmation() {
assert!(is_dirty_worktree_remove_error(
assert!(is_submodule_worktree_remove_error(
"fatal: working trees containing submodules cannot be moved or removed"
));
assert!(!is_submodule_worktree_remove_error(
"fatal: '/w/herdr' contains modified or untracked files, use --force to delete it"
));
}
#[test]
@@ -906,6 +958,7 @@ prunable stale
false,
);
run_worktree_command(&add).unwrap();
assert!(!checkout_has_initialized_submodules(&checkout, false));
run_git(
&checkout,
&[
@@ -917,20 +970,22 @@ prunable stale
"--quiet",
],
);
assert!(checkout_has_initialized_submodules(&checkout, false));
assert!(!checkout_has_dirty_files(&checkout, false).unwrap());
let remove = build_worktree_remove_command(&repo, &checkout, false, false);
let error = run_worktree_command(&remove).unwrap_err();
assert!(is_dirty_worktree_remove_error(&error), "{error}");
assert!(is_submodule_worktree_remove_error(&error), "{error}");
assert!(checkout.join("sub/README.md").exists());
std::fs::write(checkout.join("sub/README.md"), "uncommitted change\n").unwrap();
let error = run_worktree_command(&remove).unwrap_err();
assert!(is_dirty_worktree_remove_error(&error), "{error}");
assert!(is_submodule_worktree_remove_error(&error), "{error}");
assert!(checkout.join("sub/README.md").exists());
let forced = build_worktree_remove_command(&repo, &checkout, true, false);
run_worktree_remove_command_with_recovery(&forced, &repo, &checkout, true, false).unwrap();
run_worktree_remove_command_with_recovery(&forced, &repo, &checkout, true, false, false)
.unwrap();
assert!(!checkout.exists());
assert!(!worktree_list_contains_path(&repo, &checkout, false).unwrap());
assert!(repo.join("sub/README.md").exists());
@@ -1036,7 +1091,31 @@ prunable stale
.unwrap();
std::fs::write(checkout.join("leftover"), "leftover\n").unwrap();
run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false).unwrap();
run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false, false)
.unwrap();
assert!(!checkout.exists());
let _ = std::fs::remove_dir_all(repo);
}
#[test]
fn forced_worktree_remove_recovery_cleans_managed_unregistered_checkout() {
let repo = create_committed_repo("worktree-recovery-managed-repo");
let checkout = unique_temp_path("worktree-recovery-managed-checkout");
let branch = "worktree/recovery-managed";
let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false);
run_worktree_command(&add).unwrap();
let remove = build_worktree_remove_command(&repo, &checkout, true, false);
run_worktree_command(&remove).unwrap();
std::fs::create_dir_all(&checkout).unwrap();
std::fs::write(checkout.join("leftover"), "leftover\n").unwrap();
// Git unregistered the worktree and deleted `.git` before it failed to
// remove the directory. A confirmed forced removal of the workspace's
// checkout may still clean up the leftover.
run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false, true)
.unwrap();
assert!(!checkout.exists());
let _ = std::fs::remove_dir_all(repo);
@@ -1055,8 +1134,10 @@ prunable stale
std::fs::create_dir_all(&checkout).unwrap();
std::fs::write(checkout.join("unrelated"), "do not delete\n").unwrap();
let err = run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false)
.expect_err("unrelated replacement directory should not be removed");
let err = run_worktree_remove_command_with_recovery(
&remove, &repo, &checkout, true, false, false,
)
.expect_err("unrelated replacement directory should not be removed");
assert!(is_not_working_tree_remove_error(&err));
assert!(checkout.join("unrelated").exists());