use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use crate::api::schema::{ EventData, EventEnvelope, EventKind, Request, ResponseResult, WorktreeCreateParams, WorktreeRemoveParams, }; use crate::app::App; use crate::events::{ApiWorktreeAddRequest, ApiWorktreeRemoveRequest, AppEvent}; use super::super::responses::{encode_error, encode_success}; use super::{absolute_user_path, WorktreeSource}; impl App { pub(crate) fn handle_deferred_worktree_api_request( &mut self, request: Request, respond_to: std::sync::mpsc::Sender, ) -> bool { match request.method { crate::api::schema::Method::WorktreeCreate(params) => { self.start_api_worktree_create(request.id, params, respond_to); true } crate::api::schema::Method::WorktreeRemove(params) => { self.start_api_worktree_remove(request.id, params, respond_to); true } _ => false, } } fn send_api_response(respond_to: std::sync::mpsc::Sender, response: String) { let _ = respond_to.send(response); } fn next_api_worktree_operation_id(&mut self) -> u64 { let id = self.next_api_worktree_operation_id; self.next_api_worktree_operation_id = self.next_api_worktree_operation_id.saturating_add(1); id } fn api_create_source_workspace_idx(&self, api: &ApiWorktreeAddRequest) -> Option { let Some(source_workspace_id) = api.source_workspace_id.as_ref() else { return self.find_parent_workspace_by_key(&api.repo_key); }; let Some(ws_idx) = self .state .workspaces .iter() .position(|ws| &ws.id == source_workspace_id) else { return self.find_parent_workspace_by_key(&api.repo_key); }; let workspace = &self.state.workspaces[ws_idx]; if let Some(expected) = api.source_existing_membership.as_ref() { if workspace.worktree_space() == Some(expected) { return Some(ws_idx); } return self.find_parent_workspace_by_key(&api.repo_key); } if let Some(current) = workspace.worktree_space() { let expected = crate::workspace::WorktreeSpaceMembership { key: api.repo_key.clone(), label: api.repo_name.clone(), repo_root: api.source_repo_root.clone(), checkout_path: api.source_checkout_path.clone(), is_linked_worktree: false, }; if current == &expected { return Some(ws_idx); } return self.find_parent_workspace_by_key(&api.repo_key); } let git_space = workspace.git_space().cloned().or_else(|| { workspace .resolved_identity_cwd_from(&self.state.terminals, &self.terminal_runtimes) .as_deref() .and_then(crate::workspace::git_space_metadata) }); if git_space.is_some_and(|space| { !space.is_linked_worktree && space.key == api.repo_key && crate::worktree::canonical_or_original(&space.repo_root) == crate::worktree::canonical_or_original(&api.source_repo_root) }) { Some(ws_idx) } else { self.find_parent_workspace_by_key(&api.repo_key) } } fn start_api_worktree_create( &mut self, id: String, params: WorktreeCreateParams, respond_to: std::sync::mpsc::Sender, ) { let branch = params .branch .unwrap_or_else(|| { let seed = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_micros().min(u128::from(u64::MAX)) as u64) .unwrap_or(0); crate::worktree::generated_branch_slug(seed) }) .trim() .to_string(); if branch.is_empty() { Self::send_api_response( respond_to, encode_error(id, "invalid_request", "branch is required"), ); return; } let base = params.base.unwrap_or_else(|| "HEAD".into()); let source = match self.resolve_worktree_source(params.workspace_id, params.cwd) { Ok(source) => source, Err(err) => { Self::send_api_response(respond_to, encode_error(id, err.code, err.message)); return; } }; let checkout_path = match params.path { Some(path) => match absolute_user_path(&path) { Ok(path) => path, Err(err) => { Self::send_api_response(respond_to, encode_error(id, err.code, err.message)); return; } }, None => crate::worktree::default_checkout_path( &self.state.worktree_directory, &source.repo_name, &branch, ), }; let checkout_key = crate::worktree::canonical_or_original(&checkout_path); if self .pending_api_worktree_creates .contains_key(&checkout_key) || self .pending_api_worktree_remove_paths .contains_key(&checkout_key) { Self::send_api_response( respond_to, encode_error( id, "worktree_operation_in_progress", "worktree operation is already in progress for this checkout", ), ); return; } let operation_id = self.next_api_worktree_operation_id(); self.pending_api_worktree_creates .insert(checkout_key.clone(), operation_id); let parent_dir = checkout_path.parent().map(Path::to_path_buf); let source_workspace_id = source .workspace_idx .and_then(|idx| self.state.workspaces.get(idx).map(|ws| ws.id.clone())); let source_existing_membership = source_workspace_id.as_ref().and_then(|workspace_id| { self.state .workspaces .iter() .find(|ws| &ws.id == workspace_id) .and_then(|ws| ws.worktree_space().cloned()) }); let api_request = ApiWorktreeAddRequest { id, operation_id, checkout_key, source_workspace_id, source_existing_membership, source_checkout_path: source.source_checkout_path, source_repo_root: source.source_repo_root, repo_key: source.repo_key, repo_name: source.repo_name, label: params.label, focus: params.focus, respond_to, }; let path = checkout_path; let source_checkout_path = api_request.source_checkout_path.clone(); let event_tx = self.event_tx.clone(); std::thread::spawn(move || { let result = if let Some(parent_dir) = parent_dir { std::fs::create_dir_all(&parent_dir).map_err(|err| err.to_string()) } else { Ok(()) } .and_then(|()| { crate::worktree::run_worktree_add_command( &source_checkout_path, &path, &branch, &base, ) }); let _ = event_tx.blocking_send(AppEvent::WorktreeAddFinished(Box::new( crate::events::WorktreeAddResult { path, api_request: Some(api_request), result, }, ))); }); } fn start_api_worktree_remove( &mut self, id: String, params: WorktreeRemoveParams, respond_to: std::sync::mpsc::Sender, ) { let Some(ws_idx) = self.parse_workspace_id(¶ms.workspace_id) else { Self::send_api_response( respond_to, encode_error( id, "workspace_not_found", format!("workspace {} not found", params.workspace_id), ), ); return; }; let Some(space) = self .state .workspaces .get(ws_idx) .and_then(|ws| ws.worktree_space().cloned()) else { Self::send_api_response( respond_to, encode_error( id, "not_linked_worktree", "workspace is not a Herdr-managed worktree checkout", ), ); return; }; if !space.is_linked_worktree { Self::send_api_response( respond_to, encode_error( id, "not_linked_worktree", "workspace is not a linked worktree checkout", ), ); return; } #[cfg(windows)] { if !params.force && crate::worktree::checkout_has_dirty_files(&space.checkout_path).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; } } let workspace_internal_id = self.state.workspaces[ws_idx].id.clone(); let checkout_key = crate::worktree::canonical_or_original(&space.checkout_path); if self .pending_api_worktree_removes .contains_key(&workspace_internal_id) || self .pending_api_worktree_remove_paths .contains_key(&checkout_key) || self .pending_api_worktree_creates .contains_key(&checkout_key) { Self::send_api_response( respond_to, encode_error( id, "worktree_operation_in_progress", "worktree operation is already in progress for this checkout", ), ); return; } if Self::should_shutdown_workspace_terminal_runtimes_for_worktree_remove(params.force) { self.shutdown_workspace_terminal_runtimes_for_worktree_remove(ws_idx); } let operation_id = self.next_api_worktree_operation_id(); self.pending_api_worktree_removes .insert(workspace_internal_id.clone(), operation_id); self.pending_api_worktree_remove_paths .insert(checkout_key.clone(), operation_id); let workspace_snapshot = self.workspace_info(ws_idx); let worktree = self.worktree_info_for_membership(&space, None); let command = crate::worktree::build_worktree_remove_command( &space.repo_root, &space.checkout_path, params.force, ); let api_request = ApiWorktreeRemoveRequest { id, operation_id, checkout_key, respond_to, }; let repo_root = space.repo_root; let path = space.checkout_path; let force = params.force; 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, ); let _ = event_tx.blocking_send(AppEvent::WorktreeRemoveFinished(Box::new( crate::events::WorktreeRemoveResult { workspace_id: workspace_internal_id, path, workspace: Some(Box::new(workspace_snapshot)), worktree: Some(Box::new(worktree)), forced: force, api_request: Some(api_request), result, }, ))); }); } pub(crate) fn handle_api_worktree_add_finished( &mut self, mut result: crate::events::WorktreeAddResult, ) { let Some(api) = result.api_request.take() else { return; }; let checkout_key = api.checkout_key.clone(); let operation_matches = self .pending_api_worktree_creates .get(&checkout_key) .is_some_and(|operation_id| *operation_id == api.operation_id); if !operation_matches { Self::send_api_response( api.respond_to, encode_error( api.id, "stale_worktree_operation", "worktree create completed after the operation was superseded", ), ); return; } self.pending_api_worktree_creates.remove(&checkout_key); if let Err(err) = result.result { if let Some(create) = &mut self.state.worktree_create { if create.checkout_path == result.path { create.creating = false; create.error = Some(err.clone()); } } Self::send_api_response( api.respond_to, encode_error(api.id, "worktree_create_failed", err), ); return; } let source_workspace_idx = self.api_create_source_workspace_idx(&api); let mut source = WorktreeSource { workspace_idx: source_workspace_idx, source_checkout_path: api.source_checkout_path, source_repo_root: api.source_repo_root, repo_key: api.repo_key, repo_name: api.repo_name, }; if let Err(err) = self.ensure_source_parent_membership(&mut source, true) { Self::send_api_response(api.respond_to, encode_error(api.id, err.code, err.message)); return; } let (ws_idx, created_workspace) = if let Some(ws_idx) = self.open_workspace_idx_for_checkout(&result.path) { if api.focus { self.state.switch_workspace(ws_idx); } (ws_idx, false) } else { match self.create_workspace_with_options(result.path.clone(), api.focus) { Ok(ws_idx) => (ws_idx, true), Err(err) => { Self::send_api_response( api.respond_to, encode_error( api.id, "worktree_open_failed", format!("created worktree but failed to open workspace: {err}"), ), ); return; } } }; self.mark_worktree_membership( &source, ws_idx, result.path.clone(), true, !created_workspace, ); if let Some(label) = api.label { if let Some(ws) = self.state.workspaces.get_mut(ws_idx) { ws.set_custom_name(label); } } if self .state .worktree_create .as_ref() .is_some_and(|create| create.checkout_path == result.path) { self.state.worktree_create = None; self.state.name_input.clear(); self.state.name_input_replace_on_type = false; self.state.mode = crate::app::Mode::Terminal; } self.state.mark_session_dirty(); if created_workspace { self.emit_workspace_open_events(ws_idx); } let Some(worktree) = self.worktree_info_for_workspace(ws_idx) else { Self::send_api_response( api.respond_to, encode_error( api.id, "worktree_open_failed", "created worktree but failed to record workspace membership", ), ); return; }; self.emit_worktree_created_event(ws_idx, worktree.clone()); let tab_idx = self.state.workspaces[ws_idx].active_tab; let response = encode_success( api.id, ResponseResult::WorktreeCreated { workspace: self.workspace_info(ws_idx), tab: self .tab_info(ws_idx, tab_idx) .expect("created worktree workspace should have an active tab"), root_pane: self .root_pane_info(ws_idx, tab_idx) .expect("created worktree workspace should have an active root pane"), worktree, }, ); Self::send_api_response(api.respond_to, response); } pub(crate) fn handle_api_worktree_remove_finished( &mut self, mut result: crate::events::WorktreeRemoveResult, ) { let Some(api) = result.api_request.take() else { return; }; let operation_matches = self .pending_api_worktree_removes .get(&result.workspace_id) .is_some_and(|operation_id| *operation_id == api.operation_id) && self .pending_api_worktree_remove_paths .get(&api.checkout_key) .is_some_and(|operation_id| *operation_id == api.operation_id); if !operation_matches { Self::send_api_response( api.respond_to, encode_error( api.id, "stale_worktree_operation", "worktree remove completed after the operation was superseded", ), ); return; } self.pending_api_worktree_removes .remove(&result.workspace_id); self.pending_api_worktree_remove_paths .remove(&api.checkout_key); if let Err(message) = result.result { let code = if !result.forced && crate::worktree::is_dirty_worktree_remove_error(&message) { "dirty_worktree_requires_force" } else { "worktree_remove_failed" }; if let Some(remove) = &mut self.state.worktree_remove { if remove.workspace_id == result.workspace_id && remove.path == result.path { remove.removing = false; if code == "dirty_worktree_requires_force" && !remove.force_confirmation { remove.force_confirmation = true; remove.error = None; } else { remove.error = Some(message.clone()); } } } Self::send_api_response(api.respond_to, encode_error(api.id, code, message)); return; } let mut workspace_id = result.workspace_id.clone(); let mut workspace_snapshot = result.workspace.as_deref().cloned(); let mut worktree = result.worktree.as_deref().cloned(); if let Some(ws_idx) = self .state .workspaces .iter() .position(|ws| ws.id == result.workspace_id) { let current_matches = self.state.workspaces[ws_idx] .worktree_space() .is_some_and(|space| { space.is_linked_worktree && space.checkout_path == result.path }); if current_matches { workspace_id = self.public_workspace_id(ws_idx); workspace_snapshot.get_or_insert_with(|| self.workspace_info(ws_idx)); if worktree.is_none() { worktree = self.state.workspaces[ws_idx] .worktree_space() .cloned() .map(|space| self.worktree_info_for_membership(&space, None)); } self.close_removed_linked_worktree_workspace(ws_idx); self.shutdown_detached_terminal_runtimes(); self.emit_event(EventEnvelope { event: EventKind::WorkspaceClosed, data: EventData::WorkspaceClosed { workspace_id: workspace_id.clone(), workspace: workspace_snapshot.clone(), }, }); } else if let Some(snapshot) = workspace_snapshot.as_ref() { workspace_id = snapshot.workspace_id.clone(); } } else if let Some(snapshot) = workspace_snapshot.as_ref() { workspace_id = snapshot.workspace_id.clone(); } let Some(worktree) = worktree else { Self::send_api_response( api.respond_to, encode_error( api.id, "worktree_remove_failed", "removed worktree but lost worktree snapshot", ), ); return; }; self.emit_worktree_removed_event( workspace_id.clone(), workspace_snapshot, worktree, result.forced, ); if self.state.worktree_remove.as_ref().is_some_and(|remove| { remove.workspace_id == result.workspace_id && remove.path == result.path }) { self.state.worktree_remove = None; self.state.mode = if self.state.active.is_some() { crate::app::Mode::Terminal } else { crate::app::Mode::Navigate }; } let response = encode_success( api.id, ResponseResult::WorktreeRemoved { workspace_id, path: result.path.display().to_string(), forced: result.forced, }, ); Self::send_api_response(api.respond_to, response); } }