1477 lines · 49.3 KB · Rust
RawDownload
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.