diff options
| author | akbash <akbash@herdr.dev> | 2026-09-20 17:49:20 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-20 16:49:20 +0200 |
| commit | 6c62707a3a43427a34fb418ce20c834a88089ba1 (patch) | |
| tree | 0940b3d41853316fcd861b5902ed7a4ebd155066 /src | |
| parent | 74505861e40c48e070e711bd4de662a17b0939b3 (diff) | |
fix: preserve explicit worktree workspace membership (#4301)
refs #4293
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/api/worktrees.rs | 110 |
1 files changed, 106 insertions, 4 deletions
diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs index 71f2e03a..f578f101 100644 --- a/src/app/api/worktrees.rs +++ b/src/app/api/worktrees.rs @@ -597,10 +597,10 @@ impl App { let canonical_checkout = crate::worktree::canonical_or_original(checkout_path); let checkout_key = canonical_checkout.display().to_string(); self.state.workspaces.iter().position(|ws| { - if ws.worktree_space().is_some_and(|space| { - crate::worktree::canonical_or_original(&space.checkout_path) == canonical_checkout - }) { - return true; + if let Some(space) = ws.worktree_space() { + // Explicit checkout provenance must not be overridden by shell navigation. + return crate::worktree::canonical_or_original(&space.checkout_path) + == canonical_checkout; } let git_space = ws.git_space().cloned().or_else(|| { @@ -1485,6 +1485,108 @@ mod tests { } #[test] + fn api_worktree_open_preserves_explicit_membership_after_shell_cd() { + let repo = create_committed_repo("api-worktree-membership-repo"); + let checkout = unique_temp_path("api-worktree-membership-checkout"); + run_git( + &repo, + &[ + "worktree", + "add", + "--quiet", + "-b", + "worktree/membership", + checkout.to_str().unwrap(), + "HEAD", + ], + ); + // Use Git's path spelling, as worktree create/open do when assigning membership. + let checkout = crate::worktree::list_existing_worktrees(&repo, false) + .unwrap() + .into_iter() + .find(|entry| entry.branch.as_deref() == Some("worktree/membership")) + .unwrap() + .path; + let mut app = app_with_parent(&repo); + let parent_id = app.state.workspaces[0].id.clone(); + let source = app + .resolve_worktree_source(Some(parent_id.clone()), None) + .unwrap_or_else(|err| panic!("{}", err.message)); + app.mark_worktree_membership(&source, 0, repo.clone(), false, false); + let parent_membership = app.state.workspaces[0].worktree_space().cloned(); + + // Shell navigation and refreshed Git metadata must not override provenance. + let parent = &mut app.state.workspaces[0]; + parent.cached_git_space = crate::workspace::git_space_metadata(&checkout); + let terminal_id = parent.tabs[0] + .terminal_id(parent.tabs[0].root_pane) + .unwrap(); + app.state.terminals.get_mut(terminal_id).unwrap().cwd = checkout.clone(); + assert_eq!(app.open_workspace_idx_for_checkout(&checkout), None); + + let mut child = Workspace::test_new("child"); + child.identity_cwd = checkout.clone(); + let child_id = child.id.clone(); + app.state.workspaces.push(child); + app.state.ensure_test_terminals(); + app.mark_worktree_membership(&source, 1, checkout.clone(), true, false); + let child_membership = app.state.workspaces[1].worktree_space().cloned(); + + let response = app.handle_api_request(Request { + id: "list".into(), + method: crate::api::schema::Method::WorktreeList(WorktreeListParams { + workspace_id: Some(parent_id.clone()), + ..WorktreeListParams::default() + }), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + let ResponseResult::WorktreeList { worktrees, .. } = success.result else { + panic!("expected worktree_list response"); + }; + let entry = worktrees + .iter() + .find(|entry| entry.branch.as_deref() == Some("worktree/membership")) + .unwrap(); + assert_eq!(entry.open_workspace_id.as_deref(), Some(child_id.as_str())); + + let response = app.handle_api_request(Request { + id: "open".into(), + method: crate::api::schema::Method::WorktreeOpen(WorktreeOpenParams { + workspace_id: Some(parent_id), + branch: Some("worktree/membership".into()), + focus: false, + ..WorktreeOpenParams::default() + }), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + let ResponseResult::WorktreeOpened { + workspace, + already_open, + .. + } = success.result + else { + panic!("expected worktree_opened response"); + }; + assert!(already_open); + assert_eq!(workspace.workspace_id, child_id); + assert_eq!(app.state.workspaces.len(), 2); + assert_eq!(app.state.active, Some(0)); + assert_eq!( + app.state.workspaces[0].worktree_space(), + parent_membership.as_ref() + ); + assert_eq!( + app.state.workspaces[1].worktree_space(), + child_membership.as_ref() + ); + app.state.assert_invariants_for_test(); + + 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); + } + + #[test] fn api_worktree_open_label_on_already_open_checkout_emits_rename_event() { let repo = create_committed_repo("api-worktree-open-label-repo"); let checkout = unique_temp_path("api-worktree-open-label-checkout"); |
