diff --git a/inc/Workspace/RemoteWorkspaceBackend.php b/inc/Workspace/RemoteWorkspaceBackend.php index 40735108..d502844b 100644 --- a/inc/Workspace/RemoteWorkspaceBackend.php +++ b/inc/Workspace/RemoteWorkspaceBackend.php @@ -149,7 +149,11 @@ public function worktree_remove( string $repo_name, string $branch ): array|\WP_ $handle = $repo_name . '@' . $this->branch_slug($branch); $state = $this->state(); if ( ! isset($state['worktrees'][ $handle ]) ) { - return new \WP_Error('remote_workspace_worktree_not_found', sprintf('Remote workspace worktree "%s" is not registered.', $handle), array( 'status' => 404 )); + $stored_handle = $this->find_worktree_handle_by_repo_branch($state, $repo_name, $branch); + if ( null === $stored_handle ) { + return new \WP_Error('remote_workspace_worktree_not_found', sprintf('Remote workspace worktree "%s" is not registered.', $handle), array( 'status' => 404 )); + } + $handle = $stored_handle; } unset($state['worktrees'][ $handle ]); @@ -163,6 +167,37 @@ public function worktree_remove( string $repo_name, string $branch ): array|\WP_ ); } + /** + * Find the stored worktree handle for a repo/branch pair. + * + * Remote worktree handles can outlive branch changes when an existing + * worktree is reused for a fresh branch. Remove by the current branch should + * still clear that registered row instead of requiring operators to know the + * stale handle slug. + * + * @param array $state Remote workspace state. + * @param string $repo_name Workspace repo name. + * @param string $branch Current branch name. + * @return string|null Stored handle, if exactly matched. + */ + private function find_worktree_handle_by_repo_branch( array $state, string $repo_name, string $branch ): ?string { + foreach ( (array) ( $state['worktrees'] ?? array() ) as $stored_handle => $worktree ) { + if ( ! is_array($worktree) ) { + continue; + } + if ( $repo_name !== (string) ( $worktree['repo_name'] ?? '' ) ) { + continue; + } + if ( $branch !== (string) ( $worktree['branch'] ?? '' ) ) { + continue; + } + + return (string) $stored_handle; + } + + return null; + } + /** * Prune remote worktree state whose primary repo registration disappeared. * diff --git a/tests/smoke-remote-workspace-backend.php b/tests/smoke-remote-workspace-backend.php index 434642be..b72ced8b 100644 --- a/tests/smoke-remote-workspace-backend.php +++ b/tests/smoke-remote-workspace-backend.php @@ -262,13 +262,28 @@ function update_option( string $key, mixed $value, bool $autoload = true ): bool $second_worktree = $backend->worktree_add('example', 'fix/remove-me'); $assert('second worktree add succeeds', ! is_wp_error($second_worktree) && 'example@fix-remove-me' === $second_worktree['handle']); - $remove = $backend->worktree_remove('example', 'fix/remove-me'); - $assert('worktree remove clears remote runtime state', ! is_wp_error($remove) && 'example@fix-remove-me' === $remove['handle']); - $removed_status = $backend->git_status('example@fix-remove-me'); - $assert('removed worktree no longer resolves', is_wp_error($removed_status) && 'remote_workspace_repo_not_found' === $removed_status->get_error_code()); - - $state = $GLOBALS['dmc_remote_workspace_options']['datamachine_code_remote_workspace_state']; - $state['worktrees']['missing@stale'] = array( + $remove = $backend->worktree_remove('example', 'fix/remove-me'); + $assert('worktree remove clears remote runtime state', ! is_wp_error($remove) && 'example@fix-remove-me' === $remove['handle']); + $removed_status = $backend->git_status('example@fix-remove-me'); + $assert('removed worktree no longer resolves', is_wp_error($removed_status) && 'remote_workspace_repo_not_found' === $removed_status->get_error_code()); + + $state = $GLOBALS['dmc_remote_workspace_options']['datamachine_code_remote_workspace_state']; + $state['worktrees']['example@old-reused-handle'] = array( + 'repo_name' => 'example', + 'repo' => 'chubes4/example', + 'branch' => 'fix/current-branch', + 'pending_files' => array(), + 'changed_files' => array(), + 'last_commit_sha' => '', + ); + $GLOBALS['dmc_remote_workspace_options']['datamachine_code_remote_workspace_state'] = $state; + $reused_remove = $backend->worktree_remove('example', 'fix/current-branch'); + $assert('worktree remove finds reused handle by stored branch', ! is_wp_error($reused_remove) && 'example@old-reused-handle' === $reused_remove['handle']); + $state = $GLOBALS['dmc_remote_workspace_options']['datamachine_code_remote_workspace_state']; + $assert('worktree remove clears reused handle row', ! isset($state['worktrees']['example@old-reused-handle'])); + + $state = $GLOBALS['dmc_remote_workspace_options']['datamachine_code_remote_workspace_state']; + $state['worktrees']['missing@stale'] = array( 'repo_name' => 'missing', 'repo' => 'chubes4/missing', 'branch' => 'stale',