diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-28 18:25:07 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-28 18:25:07 +0300 |
| commit | 095f1337d6502081658973ae3487dee8c6b34e1a (patch) | |
| tree | 4174cac82dd51dabcc1fcef57b45df2462ef1918 | |
| parent | 13a9f6402fb77a31499bf758b56ca42b79936706 (diff) | |
fix: trust worktree repositories per request (#3344)
refs #3044
| -rw-r--r-- | docs/next/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | docs/next/api/herdr-api.schema.json | 12 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/cli-reference.mdx | 10 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/ja/cli-reference.mdx | 10 | ||||
| -rw-r--r-- | docs/next/website/src/content/docs/zh-cn/cli-reference.mdx | 10 | ||||
| -rw-r--r-- | src/api/schema/tests.rs | 2 | ||||
| -rw-r--r-- | src/api/schema/worktrees.rs | 8 | ||||
| -rw-r--r-- | src/app/api/worktrees.rs | 101 | ||||
| -rw-r--r-- | src/app/api/worktrees/deferred.rs | 15 | ||||
| -rw-r--r-- | src/app/worktrees.rs | 35 | ||||
| -rw-r--r-- | src/cli/spec.rs | 12 | ||||
| -rw-r--r-- | src/cli/worktree.rs | 47 | ||||
| -rw-r--r-- | src/worktree.rs | 175 |
13 files changed, 307 insertions, 131 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 8a3ffe37..c94c15e2 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -13,6 +13,7 @@ - Oh My Pi panes now stay working when a turn ends with an automatic continuation already scheduled, instead of briefly reporting idle and completing `agent wait` early. (#2851, thanks @taoeffect) - Retained mouse selections now copy when Ctrl+C or Cmd+C arrives before a delayed mouse release instead of forwarding the copy shortcut to the pane. (#3100, thanks @moret) - Removing a background worktree workspace no longer changes focus to its parent workspace. (#3098) +- Worktree commands can now trust an explicitly selected repository for one request, allowing the full lifecycle on accessible Windows repositories owned by another SID without changing global Git configuration. (#3044) - Prefix bindings such as `prefix+|` now recognize characters produced by macOS Option and custom keyboard layouts, while exact chords such as `prefix+alt+w` keep priority. (#3079, thanks @vlcinsky) - Direct terminal attaches now preserve multiline pastes as one paste instead of submitting each line separately. (#3054) - Windows clients now preserve layout-generated text for Shift-only keys, so characters such as `/` on German keyboards reach shell panes and pasted input. (#3045) diff --git a/docs/next/api/herdr-api.schema.json b/docs/next/api/herdr-api.schema.json index 8ff90f20..aa0a8c22 100644 --- a/docs/next/api/herdr-api.schema.json +++ b/docs/next/api/herdr-api.schema.json @@ -4358,6 +4358,9 @@ "null" ] }, + "trust_repository": { + "type": "boolean" + }, "workspace_id": { "type": [ "string", @@ -4375,6 +4378,9 @@ "null" ] }, + "trust_repository": { + "type": "boolean" + }, "workspace_id": { "type": [ "string", @@ -4414,6 +4420,9 @@ "null" ] }, + "trust_repository": { + "type": "boolean" + }, "workspace_id": { "type": [ "string", @@ -4429,6 +4438,9 @@ "default": false, "type": "boolean" }, + "trust_repository": { + "type": "boolean" + }, "workspace_id": { "type": "string" } diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx index f559d902..189be5b0 100644 --- a/docs/next/website/src/content/docs/cli-reference.mdx +++ b/docs/next/website/src/content/docs/cli-reference.mdx @@ -135,16 +135,18 @@ A workspace is a top-level project or work context. Creating one also creates it ## Worktrees ```bash -herdr worktree list [--workspace ID | --cwd PATH] -herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] -herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] -herdr worktree remove --workspace ID [--force] +herdr worktree list [--workspace ID | --cwd PATH] [--trust-repository] +herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] [--trust-repository] +herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository] +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. +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. + ## Tabs ```bash diff --git a/docs/next/website/src/content/docs/ja/cli-reference.mdx b/docs/next/website/src/content/docs/ja/cli-reference.mdx index 8466bd7b..0b3e06f3 100644 --- a/docs/next/website/src/content/docs/ja/cli-reference.mdx +++ b/docs/next/website/src/content/docs/ja/cli-reference.mdx @@ -131,16 +131,18 @@ herdr workspace create --cwd ~/project --label api --no-focus ## Worktree ```bash -herdr worktree list [--workspace ID | --cwd PATH] -herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] -herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] -herdr worktree remove --workspace ID [--force] +herdr worktree list [--workspace ID | --cwd PATH] [--trust-repository] +herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] [--trust-repository] +herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository] +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` が必要です。 +Git はデフォルトで、別のユーザーが所有するリポジトリを拒否します。リポジトリを別途確認済みの場合は `--trust-repository` を渡すと、解決されたパスをそのコマンドでのみ信頼します。Herdr は Git 設定を変更しません。 + ## タブ ```bash diff --git a/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx b/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx index 91ddbc65..b00ad592 100644 --- a/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx +++ b/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx @@ -131,16 +131,18 @@ herdr workspace create --cwd ~/project --label api --no-focus ## Worktree ```bash -herdr worktree list [--workspace ID | --cwd PATH] -herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] -herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] -herdr worktree remove --workspace ID [--force] +herdr worktree list [--workspace ID | --cwd PATH] [--trust-repository] +herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] [--trust-repository] +herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository] +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`。 +Git 默认拒绝由其他用户拥有的仓库。如果你已独立验证该仓库,可传入 `--trust-repository`,仅在本次命令中信任解析后的仓库路径。Herdr 不会修改你的 Git 配置。 + ## 标签页 ```bash diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs index 5199038d..663f3fc4 100644 --- a/src/api/schema/tests.rs +++ b/src/api/schema/tests.rs @@ -720,10 +720,12 @@ fn worktree_request_and_response_round_trip() { branch: Some("worktree/api".into()), base: Some("HEAD".into()), focus: true, + trust_repository: true, ..WorktreeCreateParams::default() }), }; let json = serde_json::to_string(&request).unwrap(); + assert!(json.contains("\"trust_repository\":true")); let restored: Request = serde_json::from_str(&json).unwrap(); assert_eq!(restored, request); diff --git a/src/api/schema/worktrees.rs b/src/api/schema/worktrees.rs index dc35128e..3b0d4b35 100644 --- a/src/api/schema/worktrees.rs +++ b/src/api/schema/worktrees.rs @@ -6,6 +6,8 @@ pub struct WorktreeListParams { pub workspace_id: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub cwd: Option<String>, + #[serde(default, skip_serializing_if = "super::is_false")] + pub trust_repository: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Default)] @@ -24,6 +26,8 @@ pub struct WorktreeCreateParams { pub label: Option<String>, #[serde(default)] pub focus: bool, + #[serde(default, skip_serializing_if = "super::is_false")] + pub trust_repository: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Default)] @@ -40,6 +44,8 @@ pub struct WorktreeOpenParams { pub label: Option<String>, #[serde(default)] pub focus: bool, + #[serde(default, skip_serializing_if = "super::is_false")] + pub trust_repository: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] @@ -47,6 +53,8 @@ pub struct WorktreeRemoveParams { pub workspace_id: String, #[serde(default)] pub force: bool, + #[serde(default, skip_serializing_if = "super::is_false")] + pub trust_repository: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs index 774b981a..4a3e23a8 100644 --- a/src/app/api/worktrees.rs +++ b/src/app/api/worktrees.rs @@ -50,11 +50,18 @@ impl App { id: String, params: WorktreeListParams, ) -> String { - let source = match self.resolve_worktree_list_source(params.workspace_id, params.cwd) { + let source = match self.resolve_worktree_list_source( + params.workspace_id, + params.cwd, + params.trust_repository, + ) { Ok(source) => source, Err(err) => return encode_error(id, err.code, err.message), }; - let entries = match crate::worktree::list_existing_worktrees(&source.source_repo_root) { + let entries = match crate::worktree::list_existing_worktrees( + &source.source_repo_root, + params.trust_repository, + ) { Ok(entries) => entries, Err(err) => return encode_error(id, "worktree_list_failed", err), }; @@ -88,7 +95,12 @@ impl App { Ok(source) => source, Err(err) => return encode_error(id, err.code, err.message), }; - let entry = match self.find_worktree_entry(&source, params.path, params.branch) { + let entry = match self.find_worktree_entry( + &source, + params.path, + params.branch, + params.trust_repository, + ) { Ok(entry) => entry, Err(err) => return encode_error(id, err.code, err.message), }; @@ -235,6 +247,7 @@ impl App { &mut self, workspace_id: Option<String>, cwd: Option<String>, + trust_repository: bool, ) -> Result<WorktreeSource, ApiFailure> { if workspace_id.is_some() && cwd.is_some() { return Err(ApiFailure::new( @@ -250,7 +263,7 @@ impl App { format!("workspace {workspace_id} not found"), )); }; - return self.worktree_list_source_from_workspace(ws_idx); + return self.worktree_list_source_from_workspace(ws_idx, trust_repository); } if let Some(cwd) = cwd { @@ -261,8 +274,13 @@ impl App { "Herdr worktree actions require a path inside a Git work tree", ) })?; - let workspace_idx = self.list_source_workspace_idx_for_space(&space); - return Ok(worktree_source_from_space(space, workspace_idx, true)); + let workspace_idx = self.list_source_workspace_idx_for_space(&space, trust_repository); + return Ok(worktree_source_from_space( + space, + workspace_idx, + true, + trust_repository, + )); } let Some(ws_idx) = self.state.active.or_else(|| { @@ -276,7 +294,7 @@ impl App { "workspace_id or cwd is required when no workspace is active", )); }; - self.worktree_list_source_from_workspace(ws_idx) + self.worktree_list_source_from_workspace(ws_idx, trust_repository) } fn worktree_source_from_workspace(&self, ws_idx: usize) -> Result<WorktreeSource, ApiFailure> { @@ -331,6 +349,7 @@ impl App { fn worktree_list_source_from_workspace( &self, ws_idx: usize, + trust_repository: bool, ) -> Result<WorktreeSource, ApiFailure> { let Some(ws) = self.state.workspaces.get(ws_idx) else { return Err(ApiFailure::new( @@ -370,11 +389,16 @@ impl App { )); }; let workspace_idx = if space.is_linked_worktree { - self.list_source_workspace_idx_for_space(&space) + self.list_source_workspace_idx_for_space(&space, trust_repository) } else { Some(ws_idx) }; - Ok(worktree_source_from_space(space, workspace_idx, true)) + Ok(worktree_source_from_space( + space, + workspace_idx, + true, + trust_repository, + )) } fn ensure_source_parent_membership( @@ -415,9 +439,10 @@ impl App { fn list_source_workspace_idx_for_space( &self, space: &crate::workspace::GitSpaceMetadata, + trust_repository: bool, ) -> Option<usize> { if space.is_linked_worktree { - let parent_checkout = parent_checkout_path_for_space(space); + let parent_checkout = parent_checkout_path_for_space(space, trust_repository); self.open_workspace_idx_for_checkout(&parent_checkout) } else { self.find_parent_workspace_for_space(space) @@ -475,9 +500,11 @@ impl App { source: &WorktreeSource, path: Option<String>, branch: Option<String>, + trust_repository: bool, ) -> Result<crate::worktree::ExistingWorktree, ApiFailure> { - let entries = crate::worktree::list_existing_worktrees(&source.source_repo_root) - .map_err(|err| ApiFailure::new("worktree_list_failed", err))?; + let entries = + crate::worktree::list_existing_worktrees(&source.source_repo_root, trust_repository) + .map_err(|err| ApiFailure::new("worktree_list_failed", err))?; if let Some(path) = path { let expected = absolute_user_path(&path)?; let expected = crate::worktree::canonical_or_original(&expected); @@ -667,9 +694,10 @@ fn worktree_source_from_space( space: crate::workspace::GitSpaceMetadata, workspace_idx: Option<usize>, allow_linked: bool, + trust_repository: bool, ) -> WorktreeSource { let source_checkout_path = if allow_linked { - parent_checkout_path_for_space(&space) + parent_checkout_path_for_space(&space, trust_repository) } else { space.repo_root.clone() }; @@ -682,12 +710,15 @@ fn worktree_source_from_space( } } -fn parent_checkout_path_for_space(space: &crate::workspace::GitSpaceMetadata) -> PathBuf { +fn parent_checkout_path_for_space( + space: &crate::workspace::GitSpaceMetadata, + trust_repository: bool, +) -> PathBuf { if !space.is_linked_worktree { return space.repo_root.clone(); } - crate::worktree::list_existing_worktrees(&space.repo_root) + crate::worktree::list_existing_worktrees(&space.repo_root, trust_repository) .ok() .and_then(|entries| { entries.into_iter().find_map(|entry| { @@ -957,8 +988,12 @@ mod tests { for (_, runtime) in app.terminal_runtimes.drain() { runtime.shutdown(); } - let remove = - crate::worktree::build_worktree_remove_command(&repo, Path::new(&worktree.path), false); + let remove = crate::worktree::build_worktree_remove_command( + &repo, + Path::new(&worktree.path), + false, + false, + ); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(worktree_root); let _ = std::fs::remove_dir_all(repo); @@ -1271,8 +1306,12 @@ mod tests { for (_, runtime) in app.terminal_runtimes.drain() { runtime.shutdown(); } - let remove = - crate::worktree::build_worktree_remove_command(&repo, Path::new(&worktree.path), false); + let remove = crate::worktree::build_worktree_remove_command( + &repo, + Path::new(&worktree.path), + false, + false, + ); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(worktree_root); let _ = std::fs::remove_dir_all(repo); @@ -1440,7 +1479,7 @@ mod tests { ) })); - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(repo); } @@ -1518,7 +1557,7 @@ mod tests { ) })); - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(repo); } @@ -1603,6 +1642,7 @@ mod tests { method: crate::api::schema::Method::WorktreeList(WorktreeListParams { workspace_id: Some(app.state.workspaces[0].id.clone()), cwd: None, + trust_repository: false, }), }); @@ -1620,7 +1660,7 @@ mod tests { ); assert!(entry.is_linked_worktree); - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(repo); } @@ -1653,10 +1693,12 @@ mod tests { crate::api::schema::Method::WorktreeList(WorktreeListParams { workspace_id: Some(child_id), cwd: None, + trust_repository: false, }), crate::api::schema::Method::WorktreeList(WorktreeListParams { workspace_id: None, cwd: Some(checkout.display().to_string()), + trust_repository: false, }), ] { let response = app.handle_api_request(Request { @@ -1681,7 +1723,7 @@ mod tests { })); } - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(repo); } @@ -1710,6 +1752,7 @@ mod tests { method: crate::api::schema::Method::WorktreeList(WorktreeListParams { workspace_id: Some(app.state.workspaces[0].id.clone()), cwd: None, + trust_repository: false, }), }); @@ -1767,6 +1810,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: child_id.clone(), force: false, + trust_repository: false, }), }, ); @@ -1782,6 +1826,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: child_id, force: true, + trust_repository: false, }), }, ); @@ -1838,6 +1883,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: child_id.clone(), force: false, + trust_repository: false, }), }, ); @@ -1923,6 +1969,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: child_id.clone(), force: false, + trust_repository: false, }), }, respond_to, @@ -1996,6 +2043,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: child_id.clone(), force: false, + trust_repository: false, }), }, first_tx, @@ -2006,6 +2054,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: child_id, force: false, + trust_repository: false, }), }, second_tx, @@ -2065,6 +2114,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: first_id, force: true, + trust_repository: false, }), }, first_tx, @@ -2075,6 +2125,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: second_id, force: true, + trust_repository: false, }), }, second_tx, @@ -2110,6 +2161,7 @@ mod tests { path: Some(checkout.display().to_string()), label: None, focus: false, + trust_repository: false, }), }, respond_to, @@ -2164,6 +2216,7 @@ mod tests { method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams { workspace_id: child_id, force: false, + trust_repository: false, }), }, respond_to, @@ -2175,7 +2228,7 @@ mod tests { let error: ErrorResponse = serde_json::from_str(&response).unwrap(); assert_eq!(error.error.code, "worktree_operation_in_progress"); assert!(app.event_rx.try_recv().is_err()); - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, true); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, true, false); let _ = crate::worktree::run_worktree_command(&remove); let _ = std::fs::remove_dir_all(repo); } diff --git a/src/app/api/worktrees/deferred.rs b/src/app/api/worktrees/deferred.rs index 3cd5e9f1..4755b38d 100644 --- a/src/app/api/worktrees/deferred.rs +++ b/src/app/api/worktrees/deferred.rs @@ -199,6 +199,7 @@ impl App { &path, &branch, &base, + params.trust_repository, ) }); let _ = event_tx.blocking_send(AppEvent::WorktreeAddFinished(Box::new( @@ -259,7 +260,11 @@ impl App { #[cfg(windows)] { if !params.force - && crate::worktree::checkout_has_dirty_files(&space.checkout_path).unwrap_or(false) + && crate::worktree::checkout_has_dirty_files( + &space.checkout_path, + params.trust_repository, + ) + .unwrap_or(false) { Self::send_api_response( respond_to, @@ -311,6 +316,7 @@ impl App { &space.repo_root, &space.checkout_path, params.force, + params.trust_repository, ); let api_request = ApiWorktreeRemoveRequest { id, @@ -321,10 +327,15 @@ impl App { let repo_root = space.repo_root; let path = space.checkout_path; let force = params.force; + let trust_repository = params.trust_repository; let event_tx = self.event_tx.clone(); std::thread::spawn(move || { let result = crate::worktree::run_worktree_remove_command_with_recovery( - &command, &repo_root, &path, force, + &command, + &repo_root, + &path, + force, + trust_repository, ); let _ = event_tx.blocking_send(AppEvent::WorktreeRemoveFinished(Box::new( crate::events::WorktreeRemoveResult { diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs index 414d74b5..9993a70e 100644 --- a/src/app/worktrees.rs +++ b/src/app/worktrees.rs @@ -161,7 +161,7 @@ impl App { } }; - let list = match crate::worktree::list_existing_worktrees(&space.repo_root) { + let list = match crate::worktree::list_existing_worktrees(&space.repo_root, false) { Ok(list) => list, Err(err) => { self.state.config_diagnostic = Some(err); @@ -551,6 +551,7 @@ impl App { &path, &branch, "HEAD", + false, ) }); let _ = event_tx.blocking_send(AppEvent::WorktreeAddFinished(Box::new( @@ -599,6 +600,7 @@ impl App { base: Some("HEAD".into()), focus: true, label: None, + trust_repository: false, }, ); if let Some(message) = immediate_api_error_message(immediate_response.as_deref()) { @@ -641,7 +643,8 @@ impl App { } #[cfg(windows)] if !remove.force_confirmation - && crate::worktree::checkout_has_dirty_files(&remove.path).unwrap_or(false) + && crate::worktree::checkout_has_dirty_files(&remove.path, false) + .unwrap_or(false) { remove.force_confirmation = true; remove.error = None; @@ -686,12 +689,13 @@ impl App { }) .unwrap_or((None, None)); - let command = crate::worktree::build_worktree_remove_command(&repo_root, &path, force); + let command = + crate::worktree::build_worktree_remove_command(&repo_root, &path, force, false); tracing::info!(workspace_id = %workspace_id, path = %path.display(), force, "starting git worktree remove"); let event_tx = self.event_tx.clone(); std::thread::spawn(move || { let result = crate::worktree::run_worktree_remove_command_with_recovery( - &command, &repo_root, &path, force, + &command, &repo_root, &path, force, false, ); let _ = event_tx.blocking_send(AppEvent::WorktreeRemoveFinished(Box::new( WorktreeRemoveResult { @@ -728,6 +732,7 @@ impl App { branch: None, focus: true, label: None, + trust_repository: false, }, ); if serde_json::from_str::<crate::api::schema::SuccessResponse>(&response).is_ok() { @@ -751,7 +756,7 @@ impl App { } #[cfg(windows)] if !remove.force_confirmation - && crate::worktree::checkout_has_dirty_files(&remove.path).unwrap_or(false) + && crate::worktree::checkout_has_dirty_files(&remove.path, false).unwrap_or(false) { remove.force_confirmation = true; remove.error = None; @@ -767,6 +772,7 @@ impl App { crate::api::schema::WorktreeRemoveParams { workspace_id, force, + trust_repository: false, }, ); if let Some(message) = immediate_api_error_message(immediate_response.as_deref()) { @@ -1831,7 +1837,7 @@ mod tests { })); shutdown_test_runtimes(&mut app); - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(worktree_root); let _ = std::fs::remove_dir_all(repo); @@ -1929,7 +1935,7 @@ mod tests { } assert!(checkout.join("README.md").exists()); - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(worktree_root); let _ = std::fs::remove_dir_all(repo); @@ -1988,7 +1994,7 @@ mod tests { branch ); - let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(worktree_root); let _ = std::fs::remove_dir_all(repo); @@ -2036,8 +2042,12 @@ mod tests { } assert!(checkout.join("README.md").exists()); - let remove_new = - crate::worktree::build_worktree_remove_command(&source_checkout_path, &checkout, false); + let remove_new = crate::worktree::build_worktree_remove_command( + &source_checkout_path, + &checkout, + false, + false, + ); crate::worktree::run_worktree_command(&remove_new).unwrap(); let _ = std::fs::remove_dir_all(worktree_root); let _ = std::fs::remove_dir_all(source_checkout_path); @@ -2096,10 +2106,11 @@ mod tests { } assert!(checkout.join("SOURCE.md").exists()); - let remove_new = crate::worktree::build_worktree_remove_command(&repo, &checkout, false); + let remove_new = + crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false); crate::worktree::run_worktree_command(&remove_new).unwrap(); let remove_source = - crate::worktree::build_worktree_remove_command(&repo, &source_checkout, false); + crate::worktree::build_worktree_remove_command(&repo, &source_checkout, false, false); crate::worktree::run_worktree_command(&remove_source).unwrap(); let _ = std::fs::remove_dir_all(worktree_root); let _ = std::fs::remove_dir_all(repo); diff --git a/src/cli/spec.rs b/src/cli/spec.rs index 134bf0bf..ed3c74a8 100644 --- a/src/cli/spec.rs +++ b/src/cli/spec.rs @@ -239,7 +239,8 @@ fn worktree_command() -> Command { Command::new("list") .about("List worktree workspaces") .arg(option("workspace", "ID")) - .arg(path_option("cwd", "PATH")), + .arg(path_option("cwd", "PATH")) + .arg(flag("trust-repository")), ) .subcommand( Command::new("create") @@ -251,7 +252,8 @@ fn worktree_command() -> Command { .arg(path_option("path", "PATH")) .arg(option("label", "TEXT")) .arg(flag("focus")) - .arg(flag("no-focus")), + .arg(flag("no-focus")) + .arg(flag("trust-repository")), ) .subcommand( Command::new("open") @@ -262,13 +264,15 @@ fn worktree_command() -> Command { .arg(option("branch", "NAME")) .arg(option("label", "TEXT")) .arg(flag("focus")) - .arg(flag("no-focus")), + .arg(flag("no-focus")) + .arg(flag("trust-repository")), ) .subcommand( Command::new("remove") .about("Remove a worktree checkout") .arg(option("workspace", "ID")) - .arg(flag("force")), + .arg(flag("force")) + .arg(flag("trust-repository")), ) } diff --git a/src/cli/worktree.rs b/src/cli/worktree.rs index 4a36876a..4003eb6a 100644 --- a/src/cli/worktree.rs +++ b/src/cli/worktree.rs @@ -28,6 +28,7 @@ pub(super) fn run_worktree_command(args: &[String]) -> std::io::Result<i32> { fn worktree_list(args: &[String]) -> std::io::Result<i32> { let mut workspace_id = None; let mut cwd = None; + let mut trust_repository = false; let mut index = 0; while index < args.len() { @@ -48,6 +49,10 @@ fn worktree_list(args: &[String]) -> std::io::Result<i32> { cwd = Some(normalize_path_arg(value)?); index += 2; } + "--trust-repository" => { + trust_repository = true; + index += 1; + } "--json" => index += 1, other => { eprintln!("unknown option: {other}"); @@ -56,11 +61,15 @@ fn worktree_list(args: &[String]) -> std::io::Result<i32> { } } if workspace_id.is_some() && cwd.is_some() { - eprintln!("usage: herdr worktree list [--workspace ID | --cwd PATH]"); + eprintln!("usage: herdr worktree list [--workspace ID | --cwd PATH] [--trust-repository]"); return Ok(2); } - super::runtime::worktree_list(WorktreeListParams { workspace_id, cwd }) + super::runtime::worktree_list(WorktreeListParams { + workspace_id, + cwd, + trust_repository, + }) } fn worktree_create(args: &[String]) -> std::io::Result<i32> { @@ -71,6 +80,7 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> { let mut path = None; let mut label = None; let mut focus = false; + let mut trust_repository = false; let mut index = 0; while index < args.len() { @@ -131,6 +141,10 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> { focus = false; index += 1; } + "--trust-repository" => { + trust_repository = true; + index += 1; + } "--json" => index += 1, other => { eprintln!("unknown option: {other}"); @@ -140,7 +154,7 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> { } if workspace_id.is_some() && cwd.is_some() { eprintln!( - "usage: herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus]" + "usage: herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] [--trust-repository]" ); return Ok(2); } @@ -153,6 +167,7 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> { path, label, focus, + trust_repository, }) } @@ -163,6 +178,7 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> { let mut branch = None; let mut label = None; let mut focus = false; + let mut trust_repository = false; let mut index = 0; while index < args.len() { @@ -215,6 +231,10 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> { focus = false; index += 1; } + "--trust-repository" => { + trust_repository = true; + index += 1; + } "--json" => index += 1, other => { eprintln!("unknown option: {other}"); @@ -224,13 +244,13 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> { } if workspace_id.is_some() && cwd.is_some() { eprintln!( - "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus]" + "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository]" ); return Ok(2); } if path.is_some() == branch.is_some() { eprintln!( - "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus]" + "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository]" ); return Ok(2); } @@ -242,12 +262,14 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> { branch, label, focus, + trust_repository, }) } fn worktree_remove(args: &[String]) -> std::io::Result<i32> { let mut workspace_id = None; let mut force = false; + let mut trust_repository = false; let mut index = 0; while index < args.len() { @@ -264,6 +286,10 @@ fn worktree_remove(args: &[String]) -> std::io::Result<i32> { force = true; index += 1; } + "--trust-repository" => { + trust_repository = true; + index += 1; + } "--json" => index += 1, other => { eprintln!("unknown option: {other}"); @@ -273,26 +299,27 @@ fn worktree_remove(args: &[String]) -> std::io::Result<i32> { } let Some(workspace_id) = workspace_id else { - eprintln!("usage: herdr worktree remove --workspace ID [--force]"); + eprintln!("usage: herdr worktree remove --workspace ID [--force] [--trust-repository]"); return Ok(2); }; super::runtime::worktree_remove(WorktreeRemoveParams { workspace_id, force, + trust_repository, }) } fn print_worktree_help() { eprintln!("herdr worktree commands:"); - eprintln!(" herdr worktree list [--workspace ID | --cwd PATH]"); + eprintln!(" herdr worktree list [--workspace ID | --cwd PATH] [--trust-repository]"); eprintln!( - " herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus]" + " herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] [--trust-repository]" ); eprintln!( - " herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus]" + " herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository]" ); - eprintln!(" herdr worktree remove --workspace ID [--force]"); + eprintln!(" herdr worktree remove --workspace ID [--force] [--trust-repository]"); } fn normalize_path_arg(value: &str) -> std::io::Result<String> { diff --git a/src/worktree.rs b/src/worktree.rs index 0bb091d7..523bc502 100644 --- a/src/worktree.rs +++ b/src/worktree.rs @@ -151,6 +151,23 @@ pub(crate) fn canonical_or_original(path: &Path) -> PathBuf { std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } +fn repository_git_command(repo_root: &Path, trust_repository: bool) -> std::process::Command { + let mut command = crate::noninteractive_process::command("git"); + command.args(repository_git_args(repo_root, trust_repository)); + command +} + +fn repository_git_args(repo_root: &Path, trust_repository: bool) -> Vec<String> { + let mut args = Vec::new(); + if trust_repository { + args.push("-c".to_string()); + args.push(format!("safe.directory={}", repo_root.display())); + } + args.push("-C".to_string()); + args.push(repo_root.display().to_string()); + args +} + pub(crate) fn default_checkout_path(root: &Path, repo_name: &str, branch: &str) -> PathBuf { root.join(repo_name).join(branch_to_path_slug(branch)) } @@ -159,13 +176,10 @@ pub(crate) fn build_worktree_remove_command( repo_root: &Path, path: &Path, force: bool, + trust_repository: bool, ) -> WorktreeCommand { - let mut args = vec![ - "-C".to_string(), - repo_root.display().to_string(), - "worktree".to_string(), - "remove".to_string(), - ]; + let mut args = repository_git_args(repo_root, trust_repository); + args.extend(["worktree".to_string(), "remove".to_string()]); if force { args.push("--force".to_string()); } @@ -197,16 +211,12 @@ pub(crate) fn worktree_dirty_remove_message(path: &Path) -> String { } #[cfg(any(windows, test))] -pub(crate) fn checkout_has_dirty_files(path: &Path) -> Result<bool, String> { - let path_arg = path.display().to_string(); - let output = crate::noninteractive_process::command("git") - .args([ - "-C", - &path_arg, - "status", - "--porcelain", - "--untracked-files=all", - ]) +pub(crate) fn checkout_has_dirty_files( + path: &Path, + trust_repository: bool, +) -> Result<bool, String> { + let output = repository_git_command(path, trust_repository) + .args(["status", "--porcelain", "--untracked-files=all"]) .output() .map_err(|err| err.to_string())?; @@ -230,19 +240,20 @@ pub(crate) fn build_worktree_add_new_branch_command( path: &Path, branch: &str, base: &str, + trust_repository: bool, ) -> WorktreeCommand { + let mut args = repository_git_args(repo_root, trust_repository); + args.extend([ + "worktree".to_string(), + "add".to_string(), + "-b".to_string(), + branch.to_string(), + path.display().to_string(), + base.to_string(), + ]); WorktreeCommand { program: "git".to_string(), - args: vec![ - "-C".to_string(), - repo_root.display().to_string(), - "worktree".to_string(), - "add".to_string(), - "-b".to_string(), - branch.to_string(), - path.display().to_string(), - base.to_string(), - ], + args, } } @@ -250,24 +261,27 @@ pub(crate) fn build_worktree_add_existing_branch_command( repo_root: &Path, path: &Path, branch: &str, + trust_repository: bool, ) -> WorktreeCommand { + let mut args = repository_git_args(repo_root, trust_repository); + args.extend([ + "worktree".to_string(), + "add".to_string(), + path.display().to_string(), + branch.to_string(), + ]); WorktreeCommand { program: "git".to_string(), - args: vec![ - "-C".to_string(), - repo_root.display().to_string(), - "worktree".to_string(), - "add".to_string(), - path.display().to_string(), - branch.to_string(), - ], + args, } } -pub(crate) fn local_branch_exists(repo_root: &Path, branch: &str) -> Result<bool, String> { - let output = crate::noninteractive_process::command("git") - .arg("-C") - .arg(repo_root) +fn local_branch_exists( + repo_root: &Path, + branch: &str, + trust_repository: bool, +) -> Result<bool, String> { + let output = repository_git_command(repo_root, trust_repository) .args(["show-ref", "--verify", "--quiet"]) .arg(format!("refs/heads/{branch}")) .output() @@ -296,11 +310,12 @@ pub(crate) fn run_worktree_add_command( path: &Path, branch: &str, base: &str, + trust_repository: bool, ) -> Result<(), String> { - let command = if local_branch_exists(repo_root, branch)? { - build_worktree_add_existing_branch_command(repo_root, path, branch) + let command = if local_branch_exists(repo_root, branch, trust_repository)? { + build_worktree_add_existing_branch_command(repo_root, path, branch, trust_repository) } else { - build_worktree_add_new_branch_command(repo_root, path, branch, base) + build_worktree_add_new_branch_command(repo_root, path, branch, base, trust_repository) }; run_worktree_command(&command) } @@ -330,15 +345,16 @@ pub(crate) fn run_worktree_remove_command_with_recovery( repo_root: &Path, path: &Path, force: bool, + trust_repository: bool, ) -> Result<(), String> { match run_worktree_command(command) { Ok(()) => Ok(()), Err(err) if force && is_not_working_tree_remove_error(&err) => { - if worktree_list_contains_path(repo_root, path)? { + 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) { + if !leftover_worktree_checkout_matches_repo(repo_root, path, trust_repository) { return Err(err); } std::fs::remove_dir_all(path).map_err(|remove_err| { @@ -354,7 +370,11 @@ pub(crate) fn run_worktree_remove_command_with_recovery( } } -fn leftover_worktree_checkout_matches_repo(repo_root: &Path, path: &Path) -> bool { +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; @@ -368,16 +388,14 @@ fn leftover_worktree_checkout_matches_repo(repo_root: &Path, path: &Path) -> boo } else { path.join(gitdir) }; - let Some(worktrees_dir) = git_common_worktrees_dir(repo_root) else { + 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 git_common_worktrees_dir(repo_root: &Path) -> Option<PathBuf> { - let output = crate::noninteractive_process::command("git") - .arg("-C") - .arg(repo_root) +fn git_common_worktrees_dir(repo_root: &Path, trust_repository: bool) -> Option<PathBuf> { + let output = repository_git_command(repo_root, trust_repository) .args(["rev-parse", "--git-common-dir"]) .output() .ok()?; @@ -470,10 +488,11 @@ pub(crate) fn parse_worktree_list_porcelain(output: &str) -> Vec<ExistingWorktre entries } -pub(crate) fn list_existing_worktrees(repo_root: &Path) -> Result<Vec<ExistingWorktree>, String> { - let output = crate::noninteractive_process::command("git") - .arg("-C") - .arg(repo_root) +pub(crate) fn list_existing_worktrees( + repo_root: &Path, + trust_repository: bool, +) -> Result<Vec<ExistingWorktree>, String> { + let output = repository_git_command(repo_root, trust_repository) .args(["worktree", "list", "--porcelain"]) .output() .map_err(|err| err.to_string())?; @@ -491,9 +510,13 @@ pub(crate) fn list_existing_worktrees(repo_root: &Path) -> Result<Vec<ExistingWo }) } -pub(crate) fn worktree_list_contains_path(repo_root: &Path, path: &Path) -> Result<bool, String> { +fn worktree_list_contains_path( + repo_root: &Path, + path: &Path, + trust_repository: bool, +) -> Result<bool, String> { let expected = canonical_or_original(path); - Ok(list_existing_worktrees(repo_root)? + Ok(list_existing_worktrees(repo_root, trust_repository)? .into_iter() .any(|entry| canonical_or_original(&entry.path) == expected)) } @@ -538,6 +561,18 @@ mod tests { } #[test] + fn trusted_repository_git_args_are_request_scoped() { + assert_eq!( + repository_git_args(Path::new("/repo/herdr"), false), + ["-C", "/repo/herdr"] + ); + assert_eq!( + repository_git_args(Path::new("/repo/herdr"), true), + ["-c", "safe.directory=/repo/herdr", "-C", "/repo/herdr",] + ); + } + + #[test] fn generated_branch_slug_is_worktree_namespaced_and_stable() { assert_eq!(generated_branch_slug(0), "worktree/brave-river-0000"); assert_eq!(generated_branch_slug(9), "worktree/calm-cloud-0009"); @@ -728,11 +763,11 @@ prunable stale ], ); - assert_eq!(checkout_has_dirty_files(&checkout), Ok(false)); + assert_eq!(checkout_has_dirty_files(&checkout, false), Ok(false)); std::fs::write(checkout.join("README.md"), "dirty\n").unwrap(); - assert_eq!(checkout_has_dirty_files(&checkout), Ok(true)); + assert_eq!(checkout_has_dirty_files(&checkout, false), Ok(true)); - let remove = build_worktree_remove_command(&repo, &checkout, true); + let remove = build_worktree_remove_command(&repo, &checkout, true, false); run_worktree_command(&remove).unwrap(); let _ = std::fs::remove_dir_all(repo); } @@ -743,6 +778,7 @@ prunable stale Path::new("/repo/herdr"), Path::new("/w/herdr/issue-137"), false, + false, ); assert_eq!(command.program, "git"); assert_eq!( @@ -763,6 +799,7 @@ prunable stale Path::new("/repo/herdr"), Path::new("/w/herdr/issue-137"), true, + false, ); assert_eq!( command.args, @@ -797,6 +834,7 @@ prunable stale Path::new("/w/herdr/worktree-brave-river"), "worktree/brave-river", "HEAD", + false, ); assert_eq!(command.program, "git"); assert_eq!( @@ -820,6 +858,7 @@ prunable stale Path::new("/repo/herdr"), Path::new("/w/herdr/worktree-brave-river"), "worktree/brave-river", + false, ); assert_eq!(command.program, "git"); assert_eq!( @@ -841,7 +880,7 @@ prunable stale let checkout = unique_temp_path("worktree-run-checkout"); let branch = "worktree/test-create-remove"; - let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD"); + let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false); run_worktree_command(&add).unwrap(); assert!(checkout.join("README.md").exists()); @@ -857,7 +896,7 @@ prunable stale branch ); - let remove = build_worktree_remove_command(&repo, &checkout, false); + let remove = build_worktree_remove_command(&repo, &checkout, false, false); run_worktree_command(&remove).unwrap(); assert!(!checkout.exists()); @@ -870,12 +909,14 @@ prunable stale let checkout = unique_temp_path("worktree-recovery-checkout"); let branch = "worktree/recovery"; - let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD"); + 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); + let remove = build_worktree_remove_command(&repo, &checkout, true, false); run_worktree_command(&remove).unwrap(); std::fs::create_dir_all(&checkout).unwrap(); - let stale_admin_dir = git_common_worktrees_dir(&repo).unwrap().join("stale"); + let stale_admin_dir = git_common_worktrees_dir(&repo, false) + .unwrap() + .join("stale"); std::fs::write( checkout.join(".git"), format!("gitdir: {}\n", stale_admin_dir.display()), @@ -883,7 +924,7 @@ prunable stale .unwrap(); std::fs::write(checkout.join("leftover"), "leftover\n").unwrap(); - run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true).unwrap(); + run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false).unwrap(); assert!(!checkout.exists()); let _ = std::fs::remove_dir_all(repo); @@ -895,14 +936,14 @@ prunable stale let checkout = unique_temp_path("worktree-recovery-unrelated-checkout"); let branch = "worktree/recovery-unrelated"; - let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD"); + 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); + 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("unrelated"), "do not delete\n").unwrap(); - let err = run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true) + let err = run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false) .expect_err("unrelated replacement directory should not be removed"); assert!(is_not_working_tree_remove_error(&err)); |
