use std::error::Error;use std::fmt;use std::path::PathBuf;use std::sync::Arc; use anyhow::anyhow;use askpass::AskPassDelegate;use collections::HashSet;use fs::Fs;use gpui::{ AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString, TaskExt, WeakEntity,};use project::Project;use project::git_store::Repository;use project::project_settings::ProjectSettings;use project::trusted_worktrees::{PathTrust, TrustedWorktrees};use remote::RemoteConnectionOptions;use settings::Settings;use ui::prelude::*;use workspace::{ MultiWorkspace, OpenMode, PreviousWorkspaceState, ToastView, Workspace, dock::DockPosition,};use zed_actions::NewWorktreeBranchTarget; use git::repository::{FetchOptions, Remote}; use util::ResultExt as _; use crate::askpass_modal::AskPassModal;use crate::git_panel::{open_output, show_error_toast};use crate::worktree_names; /// Whether a worktree operation is creating a new one or switching to an/// existing one. Controls whether the source workspace's state (dock layout,/// open files, agent panel draft) is inherited by the destination.enum WorktreeOperation { Create, Switch,} #[derive(Clone, Copy, PartialEq, Eq)]enum RemoteBranchFetchMode { Fetch, UseLocal,} impl RemoteBranchFetchMode { fn should_fetch(self) -> bool { matches!(self, Self::Fetch) }} #[derive(Debug)]struct WorktreeFetchError { remote_name: String, branch_name: String, source: anyhow::Error,} impl WorktreeFetchError { fn remote_branch_name(&self) -> String { format!("{}/{}", self.remote_name, self.branch_name) } fn output(&self) -> String { format!("git fetch {} failed:\n{:#}", self.remote_name, self.source) }} impl fmt::Display for WorktreeFetchError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!( formatter, "git fetch {} failed while creating worktree from {}: {}", self.remote_name, self.remote_branch_name(), self.source ) }} impl Error for WorktreeFetchError { fn source(&self) -> Option<&(dyn Error + 'static)> { Some(self.source.as_ref()) }} struct WorktreeFetchFailedToast { workspace: WeakEntity<Workspace>, worktree_name: Option<String>, branch_target: NewWorktreeBranchTarget, focused_dock: Option<DockPosition>, remote_branch_name: String, operation: SharedString, output: String, focus_handle: FocusHandle,} impl WorktreeFetchFailedToast { fn new( workspace: WeakEntity<Workspace>, worktree_name: Option<String>, branch_target: NewWorktreeBranchTarget, focused_dock: Option<DockPosition>, fetch_error: &WorktreeFetchError, cx: &mut gpui::Context<Self>, ) -> Self { Self { workspace, worktree_name, branch_target, focused_dock, remote_branch_name: fetch_error.remote_branch_name(), operation: format!("fetch {}", fetch_error.remote_name).into(), output: fetch_error.output(), focus_handle: cx.focus_handle(), } }} impl Focusable for WorktreeFetchFailedToast { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() }} impl EventEmitter<DismissEvent> for WorktreeFetchFailedToast {} impl ToastView for WorktreeFetchFailedToast { fn action(&self) -> Option<workspace::ToastAction> { None } fn auto_dismiss(&self) -> bool { false }} impl Render for WorktreeFetchFailedToast { fn render(&mut self, _window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement { let workspace_for_retry = self.workspace.clone(); let worktree_name = self.worktree_name.clone(); let branch_target = self.branch_target.clone(); let focused_dock = self.focused_dock; let workspace_for_log = self.workspace.clone(); let operation = self.operation.clone(); let output = self.output.clone(); h_flex() .id("worktree-fetch-failed-toast") .elevation_3(cx) .gap_2() .py_1p5() .pl_2p5() .pr_1p5() .flex_none() .bg(cx.theme().colors().surface_background) .shadow_lg() .child( Icon::new(IconName::XCircle) .size(IconSize::Small) .color(Color::Error), ) .child(Label::new(format!( "git fetch failed for {}", self.remote_branch_name ))) .child( Button::new( "use-local-worktree-base", format!("Use local {}", self.remote_branch_name), ) .color(Color::Muted) .on_click(cx.listener(move |_, _event, window, cx| { cx.emit(DismissEvent); if let Some(workspace) = workspace_for_retry.upgrade() { workspace.update(cx, |workspace, cx| { handle_create_worktree_inner( workspace, &zed_actions::CreateWorktree { worktree_name: worktree_name.clone(), branch_target: branch_target.clone(), }, window, focused_dock, RemoteBranchFetchMode::UseLocal, cx, ); }); } })), ) .child( Button::new("view-worktree-fetch-log", "Show Error Logs") .color(Color::Muted) .on_click(cx.listener(move |_, _event, window, cx| { cx.emit(DismissEvent); let output = output.clone(); let operation = operation.clone(); workspace_for_log .update(cx, move |workspace, cx| { open_output(operation, workspace, &output, window, cx) }) .ok(); })), ) .child( IconButton::new("dismiss-worktree-fetch-failed-toast", IconName::Close) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(cx.listener(|_, _event, _window, cx| { cx.emit(DismissEvent); })), ) }} /// Classifies the project's visible worktrees into git-managed repositories/// and non-git paths. Each unique repository is returned only once.pub fn classify_worktrees( project: &Project, cx: &gpui::App,) -> (Vec<Entity<Repository>>, Vec<PathBuf>) { let repositories = project.repositories(cx).clone(); let mut git_repos: Vec<Entity<Repository>> = Vec::new(); let mut non_git_paths: Vec<PathBuf> = Vec::new(); let mut seen_repo_ids = HashSet::default(); for worktree in project.visible_worktrees(cx) { let wt_path = worktree.read(cx).abs_path(); let matching_repo = repositories .iter() .filter_map(|(id, repo)| { let work_dir = repo.read(cx).work_directory_abs_path.clone(); if wt_path.starts_with(work_dir.as_ref()) { Some((*id, repo.clone(), work_dir.as_ref().components().count())) } else { None } }) .max_by( |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| { left_depth .cmp(right_depth) .then_with(|| left_id.cmp(right_id)) }, ); if let Some((id, repo, _)) = matching_repo { if seen_repo_ids.insert(id) { git_repos.push(repo); } } else { non_git_paths.push(wt_path.to_path_buf()); } } (git_repos, non_git_paths)} /// Resolves a branch target into the ref the new worktree should be based on./// Returns `None` for `CurrentBranch`, meaning "use the current HEAD".pub fn resolve_worktree_branch_target(branch_target: &NewWorktreeBranchTarget) -> Option<String> { match branch_target { NewWorktreeBranchTarget::CurrentBranch => None, NewWorktreeBranchTarget::ExistingBranch { name } => Some(name.clone()), NewWorktreeBranchTarget::RemoteBranch { remote_name, branch_name, } => Some(format!("refs/remotes/{remote_name}/{branch_name}")), }} fn remote_branch_to_fetch(branch_target: &NewWorktreeBranchTarget) -> Option<(&str, &str)> { match branch_target { NewWorktreeBranchTarget::RemoteBranch { remote_name, branch_name, } => Some((remote_name, branch_name)), NewWorktreeBranchTarget::CurrentBranch | NewWorktreeBranchTarget::ExistingBranch { .. } => { None } }} fn create_worktree_askpass_delegate( workspace: WeakEntity<Workspace>, operation: impl Into<SharedString>, window: &mut Window, cx: &mut Context<Workspace>,) -> AskPassDelegate { let operation = operation.into(); let window = window.window_handle(); AskPassDelegate::new(&mut cx.to_async(), move |prompt, tx, cx| { window .update(cx, |_, window, cx| { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { AskPassModal::new(operation.clone(), prompt.into(), tx, window, cx) }); }) }) .ok(); })} async fn fetch_remote_for_worktree_base( git_repos: &[Entity<Repository>], remote_name: String, askpass_delegates: Vec<AskPassDelegate>, cx: &mut AsyncWindowContext,) -> anyhow::Result<()> { if askpass_delegates.len() != git_repos.len() { return Err(anyhow!( "Unable to fetch {remote_name}: missing credential prompt delegate" )); } let fetches = cx.update(|_, cx| { git_repos .iter() .cloned() .zip(askpass_delegates) .map(|(repo, askpass)| { repo.update(cx, |repo, cx| { repo.fetch( FetchOptions::Remote(Remote { name: remote_name.clone().into(), }), askpass, cx, ) }) }) .collect::<Vec<_>>() })?; for fetch in futures::future::join_all(fetches).await { fetch??; } Ok(())} /// Kicks off an async git-worktree creation for each repository. Returns:////// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples./// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs for remapping editor tabs.fn start_worktree_creations( git_repos: &[Entity<Repository>], worktree_name: Option<String>, existing_worktree_names: &[String], existing_worktree_paths: &HashSet<PathBuf>, base_ref: Option<String>, worktree_directory_setting: &str, rng: &mut impl rand::Rng, cx: &mut gpui::App,) -> anyhow::Result<( Vec<( Entity<Repository>, PathBuf, futures::channel::oneshot::Receiver<anyhow::Result<()>>, )>, Vec<(PathBuf, PathBuf)>,)> { let mut creation_infos = Vec::new(); let mut path_remapping = Vec::new(); let worktree_name = worktree_name.unwrap_or_else(|| { let existing_refs: Vec<&str> = existing_worktree_names.iter().map(|s| s.as_str()).collect(); worktree_names::generate_worktree_name(&existing_refs, rng) .unwrap_or_else(|| "worktree".to_string()) }); for repo in git_repos { let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| { let new_path = repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?; if existing_worktree_paths.contains(&new_path) { anyhow::bail!("A worktree already exists at {}", new_path.display()); } let target = git::repository::CreateWorktreeTarget::Detached { base_sha: base_ref.clone(), }; let receiver = repo.create_worktree(target, new_path.clone()); let work_dir = repo.work_directory_abs_path.clone(); anyhow::Ok((work_dir, new_path, receiver)) })?; path_remapping.push((work_dir.to_path_buf(), new_path.clone())); creation_infos.push((repo.clone(), new_path, receiver)); } Ok((creation_infos, path_remapping))} /// Waits for every in-flight worktree creation to complete. If any/// creation fails, all successfully-created worktrees are rolled back/// (removed) so the project isn't left in a half-migrated state.pub async fn await_and_rollback_on_failure( creation_infos: Vec<( Entity<Repository>, PathBuf, futures::channel::oneshot::Receiver<anyhow::Result<()>>, )>, fs: Arc<dyn Fs>, cx: &mut AsyncWindowContext,) -> anyhow::Result<Vec<PathBuf>> { let mut created_paths: Vec<PathBuf> = Vec::new(); let mut repos_and_paths: Vec<(Entity<Repository>, PathBuf)> = Vec::new(); let mut first_error: Option<anyhow::Error> = None; for (repo, new_path, receiver) in creation_infos { repos_and_paths.push((repo.clone(), new_path.clone())); match receiver.await { Ok(Ok(())) => { created_paths.push(new_path); } Ok(Err(err)) => { if first_error.is_none() { first_error = Some(err); } } Err(_canceled) => { if first_error.is_none() { first_error = Some(anyhow!("Worktree creation was canceled")); } } } } let Some(err) = first_error else { return Ok(created_paths); }; // Rollback all attempted worktrees let mut rollback_futures = Vec::new(); for (rollback_repo, rollback_path) in &repos_and_paths { let receiver = cx .update(|_, cx| { rollback_repo.update(cx, |repo, _cx| { repo.remove_worktree(rollback_path.clone(), true) }) }) .ok(); rollback_futures.push((rollback_path.clone(), receiver)); } let mut rollback_failures: Vec<String> = Vec::new(); for (path, receiver_opt) in rollback_futures { let mut git_remove_failed = false; if let Some(receiver) = receiver_opt { match receiver.await { Ok(Ok(())) => {} Ok(Err(rollback_err)) => { log::error!( "git worktree remove failed for {}: {rollback_err}", path.display() ); git_remove_failed = true; } Err(canceled) => { log::error!( "git worktree remove failed for {}: {canceled}", path.display() ); git_remove_failed = true; } } } else { log::error!( "failed to dispatch git worktree remove for {}", path.display() ); git_remove_failed = true; } if git_remove_failed { if let Err(fs_err) = fs .remove_dir( &path, fs::RemoveOptions { recursive: true, ignore_if_not_exists: true, }, ) .await { let msg = format!("{}: failed to remove directory: {fs_err}", path.display()); log::error!("{}", msg); rollback_failures.push(msg); } } } let mut error_message = format!("Failed to create worktree: {err}");Lines 1–500 of 1477. Load more
use std::error::Error; use std::fmt; use std::path::PathBuf; use std::sync::Arc; use anyhow::anyhow; use askpass::AskPassDelegate; use collections::HashSet; use fs::Fs; use gpui::{ AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString, TaskExt, WeakEntity, }; use project::Project; use project::git_store::Repository; use project::project_settings::ProjectSettings; use project::trusted_worktrees::{PathTrust, TrustedWorktrees}; use remote::RemoteConnectionOptions; use settings::Settings; use ui::prelude::*; use workspace::{ MultiWorkspace, OpenMode, PreviousWorkspaceState, ToastView, Workspace, dock::DockPosition, }; use zed_actions::NewWorktreeBranchTarget; use git::repository::{FetchOptions, Remote}; use util::ResultExt as _; use crate::askpass_modal::AskPassModal; use crate::git_panel::{open_output, show_error_toast}; use crate::worktree_names; /// Whether a worktree operation is creating a new one or switching to an /// existing one. Controls whether the source workspace's state (dock layout, /// open files, agent panel draft) is inherited by the destination. enum WorktreeOperation { Create, Switch, } #[derive(Clone, Copy, PartialEq, Eq)] enum RemoteBranchFetchMode { Fetch, UseLocal, } impl RemoteBranchFetchMode { fn should_fetch(self) -> bool { matches!(self, Self::Fetch) } } #[derive(Debug)] struct WorktreeFetchError { remote_name: String, branch_name: String, source: anyhow::Error, } impl WorktreeFetchError { fn remote_branch_name(&self) -> String { format!("{}/{}", self.remote_name, self.branch_name) } fn output(&self) -> String { format!("git fetch {} failed:\n{:#}", self.remote_name, self.source) } } impl fmt::Display for WorktreeFetchError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!( formatter, "git fetch {} failed while creating worktree from {}: {}", self.remote_name, self.remote_branch_name(), self.source ) } } impl Error for WorktreeFetchError { fn source(&self) -> Option<&(dyn Error + 'static)> { Some(self.source.as_ref()) } } struct WorktreeFetchFailedToast { workspace: WeakEntity<Workspace>, worktree_name: Option<String>, branch_target: NewWorktreeBranchTarget, focused_dock: Option<DockPosition>, remote_branch_name: String, operation: SharedString, output: String, focus_handle: FocusHandle, } impl WorktreeFetchFailedToast { fn new( workspace: WeakEntity<Workspace>, worktree_name: Option<String>, branch_target: NewWorktreeBranchTarget, focused_dock: Option<DockPosition>, fetch_error: &WorktreeFetchError, cx: &mut gpui::Context<Self>, ) -> Self { Self { workspace, worktree_name, branch_target, focused_dock, remote_branch_name: fetch_error.remote_branch_name(), operation: format!("fetch {}", fetch_error.remote_name).into(), output: fetch_error.output(), focus_handle: cx.focus_handle(), } } } impl Focusable for WorktreeFetchFailedToast { fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { self.focus_handle.clone() } } impl EventEmitter<DismissEvent> for WorktreeFetchFailedToast {} impl ToastView for WorktreeFetchFailedToast { fn action(&self) -> Option<workspace::ToastAction> { None } fn auto_dismiss(&self) -> bool { false } } impl Render for WorktreeFetchFailedToast { fn render(&mut self, _window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement { let workspace_for_retry = self.workspace.clone(); let worktree_name = self.worktree_name.clone(); let branch_target = self.branch_target.clone(); let focused_dock = self.focused_dock; let workspace_for_log = self.workspace.clone(); let operation = self.operation.clone(); let output = self.output.clone(); h_flex() .id("worktree-fetch-failed-toast") .elevation_3(cx) .gap_2() .py_1p5() .pl_2p5() .pr_1p5() .flex_none() .bg(cx.theme().colors().surface_background) .shadow_lg() .child( Icon::new(IconName::XCircle) .size(IconSize::Small) .color(Color::Error), ) .child(Label::new(format!( "git fetch failed for {}", self.remote_branch_name ))) .child( Button::new( "use-local-worktree-base", format!("Use local {}", self.remote_branch_name), ) .color(Color::Muted) .on_click(cx.listener(move |_, _event, window, cx| { cx.emit(DismissEvent); if let Some(workspace) = workspace_for_retry.upgrade() { workspace.update(cx, |workspace, cx| { handle_create_worktree_inner( workspace, &zed_actions::CreateWorktree { worktree_name: worktree_name.clone(), branch_target: branch_target.clone(), }, window, focused_dock, RemoteBranchFetchMode::UseLocal, cx, ); }); } })), ) .child( Button::new("view-worktree-fetch-log", "Show Error Logs") .color(Color::Muted) .on_click(cx.listener(move |_, _event, window, cx| { cx.emit(DismissEvent); let output = output.clone(); let operation = operation.clone(); workspace_for_log .update(cx, move |workspace, cx| { open_output(operation, workspace, &output, window, cx) }) .ok(); })), ) .child( IconButton::new("dismiss-worktree-fetch-failed-toast", IconName::Close) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(cx.listener(|_, _event, _window, cx| { cx.emit(DismissEvent); })), ) } } /// Classifies the project's visible worktrees into git-managed repositories /// and non-git paths. Each unique repository is returned only once. pub fn classify_worktrees( project: &Project, cx: &gpui::App, ) -> (Vec<Entity<Repository>>, Vec<PathBuf>) { let repositories = project.repositories(cx).clone(); let mut git_repos: Vec<Entity<Repository>> = Vec::new(); let mut non_git_paths: Vec<PathBuf> = Vec::new(); let mut seen_repo_ids = HashSet::default(); for worktree in project.visible_worktrees(cx) { let wt_path = worktree.read(cx).abs_path(); let matching_repo = repositories .iter() .filter_map(|(id, repo)| { let work_dir = repo.read(cx).work_directory_abs_path.clone(); if wt_path.starts_with(work_dir.as_ref()) { Some((*id, repo.clone(), work_dir.as_ref().components().count())) } else { None } }) .max_by( |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| { left_depth .cmp(right_depth) .then_with(|| left_id.cmp(right_id)) }, ); if let Some((id, repo, _)) = matching_repo { if seen_repo_ids.insert(id) { git_repos.push(repo); } } else { non_git_paths.push(wt_path.to_path_buf()); } } (git_repos, non_git_paths) } /// Resolves a branch target into the ref the new worktree should be based on. /// Returns `None` for `CurrentBranch`, meaning "use the current HEAD". pub fn resolve_worktree_branch_target(branch_target: &NewWorktreeBranchTarget) -> Option<String> { match branch_target { NewWorktreeBranchTarget::CurrentBranch => None, NewWorktreeBranchTarget::ExistingBranch { name } => Some(name.clone()), NewWorktreeBranchTarget::RemoteBranch { remote_name, branch_name, } => Some(format!("refs/remotes/{remote_name}/{branch_name}")), } } fn remote_branch_to_fetch(branch_target: &NewWorktreeBranchTarget) -> Option<(&str, &str)> { match branch_target { NewWorktreeBranchTarget::RemoteBranch { remote_name, branch_name, } => Some((remote_name, branch_name)), NewWorktreeBranchTarget::CurrentBranch | NewWorktreeBranchTarget::ExistingBranch { .. } => { None } } } fn create_worktree_askpass_delegate( workspace: WeakEntity<Workspace>, operation: impl Into<SharedString>, window: &mut Window, cx: &mut Context<Workspace>, ) -> AskPassDelegate { let operation = operation.into(); let window = window.window_handle(); AskPassDelegate::new(&mut cx.to_async(), move |prompt, tx, cx| { window .update(cx, |_, window, cx| { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { AskPassModal::new(operation.clone(), prompt.into(), tx, window, cx) }); }) }) .ok(); }) } async fn fetch_remote_for_worktree_base( git_repos: &[Entity<Repository>], remote_name: String, askpass_delegates: Vec<AskPassDelegate>, cx: &mut AsyncWindowContext, ) -> anyhow::Result<()> { if askpass_delegates.len() != git_repos.len() { return Err(anyhow!( "Unable to fetch {remote_name}: missing credential prompt delegate" )); } let fetches = cx.update(|_, cx| { git_repos .iter() .cloned() .zip(askpass_delegates) .map(|(repo, askpass)| { repo.update(cx, |repo, cx| { repo.fetch( FetchOptions::Remote(Remote { name: remote_name.clone().into(), }), askpass, cx, ) }) }) .collect::<Vec<_>>() })?; for fetch in futures::future::join_all(fetches).await { fetch??; } Ok(()) } /// Kicks off an async git-worktree creation for each repository. Returns: /// /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples. /// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs for remapping editor tabs. fn start_worktree_creations( git_repos: &[Entity<Repository>], worktree_name: Option<String>, existing_worktree_names: &[String], existing_worktree_paths: &HashSet<PathBuf>, base_ref: Option<String>, worktree_directory_setting: &str, rng: &mut impl rand::Rng, cx: &mut gpui::App, ) -> anyhow::Result<( Vec<( Entity<Repository>, PathBuf, futures::channel::oneshot::Receiver<anyhow::Result<()>>, )>, Vec<(PathBuf, PathBuf)>, )> { let mut creation_infos = Vec::new(); let mut path_remapping = Vec::new(); let worktree_name = worktree_name.unwrap_or_else(|| { let existing_refs: Vec<&str> = existing_worktree_names.iter().map(|s| s.as_str()).collect(); worktree_names::generate_worktree_name(&existing_refs, rng) .unwrap_or_else(|| "worktree".to_string()) }); for repo in git_repos { let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| { let new_path = repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?; if existing_worktree_paths.contains(&new_path) { anyhow::bail!("A worktree already exists at {}", new_path.display()); } let target = git::repository::CreateWorktreeTarget::Detached { base_sha: base_ref.clone(), }; let receiver = repo.create_worktree(target, new_path.clone()); let work_dir = repo.work_directory_abs_path.clone(); anyhow::Ok((work_dir, new_path, receiver)) })?; path_remapping.push((work_dir.to_path_buf(), new_path.clone())); creation_infos.push((repo.clone(), new_path, receiver)); } Ok((creation_infos, path_remapping)) } /// Waits for every in-flight worktree creation to complete. If any /// creation fails, all successfully-created worktrees are rolled back /// (removed) so the project isn't left in a half-migrated state. pub async fn await_and_rollback_on_failure( creation_infos: Vec<( Entity<Repository>, PathBuf, futures::channel::oneshot::Receiver<anyhow::Result<()>>, )>, fs: Arc<dyn Fs>, cx: &mut AsyncWindowContext, ) -> anyhow::Result<Vec<PathBuf>> { let mut created_paths: Vec<PathBuf> = Vec::new(); let mut repos_and_paths: Vec<(Entity<Repository>, PathBuf)> = Vec::new(); let mut first_error: Option<anyhow::Error> = None; for (repo, new_path, receiver) in creation_infos { repos_and_paths.push((repo.clone(), new_path.clone())); match receiver.await { Ok(Ok(())) => { created_paths.push(new_path); } Ok(Err(err)) => { if first_error.is_none() { first_error = Some(err); } } Err(_canceled) => { if first_error.is_none() { first_error = Some(anyhow!("Worktree creation was canceled")); } } } } let Some(err) = first_error else { return Ok(created_paths); }; // Rollback all attempted worktrees let mut rollback_futures = Vec::new(); for (rollback_repo, rollback_path) in &repos_and_paths { let receiver = cx .update(|_, cx| { rollback_repo.update(cx, |repo, _cx| { repo.remove_worktree(rollback_path.clone(), true) }) }) .ok(); rollback_futures.push((rollback_path.clone(), receiver)); } let mut rollback_failures: Vec<String> = Vec::new(); for (path, receiver_opt) in rollback_futures { let mut git_remove_failed = false; if let Some(receiver) = receiver_opt { match receiver.await { Ok(Ok(())) => {} Ok(Err(rollback_err)) => { log::error!( "git worktree remove failed for {}: {rollback_err}", path.display() ); git_remove_failed = true; } Err(canceled) => { log::error!( "git worktree remove failed for {}: {canceled}", path.display() ); git_remove_failed = true; } } } else { log::error!( "failed to dispatch git worktree remove for {}", path.display() ); git_remove_failed = true; } if git_remove_failed { if let Err(fs_err) = fs .remove_dir( &path, fs::RemoveOptions { recursive: true, ignore_if_not_exists: true, }, ) .await { let msg = format!("{}: failed to remove directory: {fs_err}", path.display()); log::error!("{}", msg); rollback_failures.push(msg); } } } let mut error_message = format!("Failed to create worktree: {err}");