worktree_service.rs

   1use std::path::PathBuf;
   2use std::sync::Arc;
   3
   4use anyhow::anyhow;
   5use collections::HashSet;
   6use fs::Fs;
   7use gpui::{AsyncWindowContext, Entity, SharedString, WeakEntity};
   8use project::Project;
   9use project::git_store::Repository;
  10use project::project_settings::ProjectSettings;
  11use project::trusted_worktrees::{PathTrust, TrustedWorktrees};
  12use remote::RemoteConnectionOptions;
  13use settings::Settings;
  14use workspace::{MultiWorkspace, OpenMode, PreviousWorkspaceState, Workspace, dock::DockPosition};
  15use zed_actions::NewWorktreeBranchTarget;
  16
  17use util::ResultExt as _;
  18
  19use crate::git_panel::show_error_toast;
  20use crate::worktree_names;
  21
  22/// Whether a worktree operation is creating a new one or switching to an
  23/// existing one. Controls whether the source workspace's state (dock layout,
  24/// open files, agent panel draft) is inherited by the destination.
  25enum WorktreeOperation {
  26    Create,
  27    Switch,
  28}
  29
  30/// Classifies the project's visible worktrees into git-managed repositories
  31/// and non-git paths. Each unique repository is returned only once.
  32pub fn classify_worktrees(
  33    project: &Project,
  34    cx: &gpui::App,
  35) -> (Vec<Entity<Repository>>, Vec<PathBuf>) {
  36    let repositories = project.repositories(cx).clone();
  37    let mut git_repos: Vec<Entity<Repository>> = Vec::new();
  38    let mut non_git_paths: Vec<PathBuf> = Vec::new();
  39    let mut seen_repo_ids = HashSet::default();
  40
  41    for worktree in project.visible_worktrees(cx) {
  42        let wt_path = worktree.read(cx).abs_path();
  43
  44        let matching_repo = repositories
  45            .iter()
  46            .filter_map(|(id, repo)| {
  47                let work_dir = repo.read(cx).work_directory_abs_path.clone();
  48                if wt_path.starts_with(work_dir.as_ref()) {
  49                    Some((*id, repo.clone(), work_dir.as_ref().components().count()))
  50                } else {
  51                    None
  52                }
  53            })
  54            .max_by(
  55                |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
  56                    left_depth
  57                        .cmp(right_depth)
  58                        .then_with(|| left_id.cmp(right_id))
  59                },
  60            );
  61
  62        if let Some((id, repo, _)) = matching_repo {
  63            if seen_repo_ids.insert(id) {
  64                git_repos.push(repo);
  65            }
  66        } else {
  67            non_git_paths.push(wt_path.to_path_buf());
  68        }
  69    }
  70
  71    (git_repos, non_git_paths)
  72}
  73
  74/// Resolves a branch target into the ref the new worktree should be based on.
  75/// Returns `None` for `CurrentBranch`, meaning "use the current HEAD".
  76pub fn resolve_worktree_branch_target(branch_target: &NewWorktreeBranchTarget) -> Option<String> {
  77    match branch_target {
  78        NewWorktreeBranchTarget::CurrentBranch => None,
  79        NewWorktreeBranchTarget::ExistingBranch { name } => Some(name.clone()),
  80    }
  81}
  82
  83/// Kicks off an async git-worktree creation for each repository. Returns:
  84///
  85/// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples.
  86/// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs for remapping editor tabs.
  87fn start_worktree_creations(
  88    git_repos: &[Entity<Repository>],
  89    worktree_name: Option<String>,
  90    existing_worktree_names: &[String],
  91    existing_worktree_paths: &HashSet<PathBuf>,
  92    base_ref: Option<String>,
  93    worktree_directory_setting: &str,
  94    rng: &mut impl rand::Rng,
  95    cx: &mut gpui::App,
  96) -> anyhow::Result<(
  97    Vec<(
  98        Entity<Repository>,
  99        PathBuf,
 100        futures::channel::oneshot::Receiver<anyhow::Result<()>>,
 101    )>,
 102    Vec<(PathBuf, PathBuf)>,
 103)> {
 104    let mut creation_infos = Vec::new();
 105    let mut path_remapping = Vec::new();
 106
 107    let worktree_name = worktree_name.unwrap_or_else(|| {
 108        let existing_refs: Vec<&str> = existing_worktree_names.iter().map(|s| s.as_str()).collect();
 109        worktree_names::generate_worktree_name(&existing_refs, rng)
 110            .unwrap_or_else(|| "worktree".to_string())
 111    });
 112
 113    for repo in git_repos {
 114        let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
 115            let new_path =
 116                repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?;
 117            if existing_worktree_paths.contains(&new_path) {
 118                anyhow::bail!("A worktree already exists at {}", new_path.display());
 119            }
 120            let target = git::repository::CreateWorktreeTarget::Detached {
 121                base_sha: base_ref.clone(),
 122            };
 123            let receiver = repo.create_worktree(target, new_path.clone());
 124            let work_dir = repo.work_directory_abs_path.clone();
 125            anyhow::Ok((work_dir, new_path, receiver))
 126        })?;
 127        path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
 128        creation_infos.push((repo.clone(), new_path, receiver));
 129    }
 130
 131    Ok((creation_infos, path_remapping))
 132}
 133
 134/// Waits for every in-flight worktree creation to complete. If any
 135/// creation fails, all successfully-created worktrees are rolled back
 136/// (removed) so the project isn't left in a half-migrated state.
 137pub async fn await_and_rollback_on_failure(
 138    creation_infos: Vec<(
 139        Entity<Repository>,
 140        PathBuf,
 141        futures::channel::oneshot::Receiver<anyhow::Result<()>>,
 142    )>,
 143    fs: Arc<dyn Fs>,
 144    cx: &mut AsyncWindowContext,
 145) -> anyhow::Result<Vec<PathBuf>> {
 146    let mut created_paths: Vec<PathBuf> = Vec::new();
 147    let mut repos_and_paths: Vec<(Entity<Repository>, PathBuf)> = Vec::new();
 148    let mut first_error: Option<anyhow::Error> = None;
 149
 150    for (repo, new_path, receiver) in creation_infos {
 151        repos_and_paths.push((repo.clone(), new_path.clone()));
 152        match receiver.await {
 153            Ok(Ok(())) => {
 154                created_paths.push(new_path);
 155            }
 156            Ok(Err(err)) => {
 157                if first_error.is_none() {
 158                    first_error = Some(err);
 159                }
 160            }
 161            Err(_canceled) => {
 162                if first_error.is_none() {
 163                    first_error = Some(anyhow!("Worktree creation was canceled"));
 164                }
 165            }
 166        }
 167    }
 168
 169    let Some(err) = first_error else {
 170        return Ok(created_paths);
 171    };
 172
 173    // Rollback all attempted worktrees
 174    let mut rollback_futures = Vec::new();
 175    for (rollback_repo, rollback_path) in &repos_and_paths {
 176        let receiver = cx
 177            .update(|_, cx| {
 178                rollback_repo.update(cx, |repo, _cx| {
 179                    repo.remove_worktree(rollback_path.clone(), true)
 180                })
 181            })
 182            .ok();
 183
 184        rollback_futures.push((rollback_path.clone(), receiver));
 185    }
 186
 187    let mut rollback_failures: Vec<String> = Vec::new();
 188    for (path, receiver_opt) in rollback_futures {
 189        let mut git_remove_failed = false;
 190
 191        if let Some(receiver) = receiver_opt {
 192            match receiver.await {
 193                Ok(Ok(())) => {}
 194                Ok(Err(rollback_err)) => {
 195                    log::error!(
 196                        "git worktree remove failed for {}: {rollback_err}",
 197                        path.display()
 198                    );
 199                    git_remove_failed = true;
 200                }
 201                Err(canceled) => {
 202                    log::error!(
 203                        "git worktree remove failed for {}: {canceled}",
 204                        path.display()
 205                    );
 206                    git_remove_failed = true;
 207                }
 208            }
 209        } else {
 210            log::error!(
 211                "failed to dispatch git worktree remove for {}",
 212                path.display()
 213            );
 214            git_remove_failed = true;
 215        }
 216
 217        if git_remove_failed {
 218            if let Err(fs_err) = fs
 219                .remove_dir(
 220                    &path,
 221                    fs::RemoveOptions {
 222                        recursive: true,
 223                        ignore_if_not_exists: true,
 224                    },
 225                )
 226                .await
 227            {
 228                let msg = format!("{}: failed to remove directory: {fs_err}", path.display());
 229                log::error!("{}", msg);
 230                rollback_failures.push(msg);
 231            }
 232        }
 233    }
 234    let mut error_message = format!("Failed to create worktree: {err}");
 235    if !rollback_failures.is_empty() {
 236        error_message.push_str("\n\nFailed to clean up: ");
 237        error_message.push_str(&rollback_failures.join(", "));
 238    }
 239    Err(anyhow!(error_message))
 240}
 241
 242/// Propagates worktree trust from the source workspace to the new workspace.
 243/// If the source project's worktrees are all trusted, the new worktree paths
 244/// will also be trusted automatically.
 245fn maybe_propagate_worktree_trust(
 246    source_workspace: &WeakEntity<Workspace>,
 247    new_workspace: &Entity<Workspace>,
 248    paths: &[PathBuf],
 249    cx: &mut AsyncWindowContext,
 250) {
 251    cx.update(|_, cx| {
 252        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 253            return;
 254        }
 255        let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) else {
 256            return;
 257        };
 258
 259        let source_is_trusted = source_workspace
 260            .upgrade()
 261            .map(|workspace| {
 262                let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store();
 263                !trusted_store
 264                    .read(cx)
 265                    .has_restricted_worktrees(&source_worktree_store, cx)
 266            })
 267            .unwrap_or(false);
 268
 269        if !source_is_trusted {
 270            return;
 271        }
 272
 273        let worktree_store = new_workspace.read(cx).project().read(cx).worktree_store();
 274        let paths_to_trust: HashSet<_> = paths
 275            .iter()
 276            .filter_map(|path| {
 277                let (worktree, _) = worktree_store.read(cx).find_worktree(path, cx)?;
 278                Some(PathTrust::Worktree(worktree.read(cx).id()))
 279            })
 280            .collect();
 281
 282        if !paths_to_trust.is_empty() {
 283            trusted_store.update(cx, |store, cx| {
 284                store.trust(&worktree_store, paths_to_trust, cx);
 285            });
 286        }
 287    })
 288    .ok();
 289}
 290
 291/// Handles the `CreateWorktree` action generically, without any agent panel involvement.
 292/// Creates a new git worktree, opens the workspace, restores layout and files.
 293pub fn handle_create_worktree(
 294    workspace: &mut Workspace,
 295    action: &zed_actions::CreateWorktree,
 296    window: &mut gpui::Window,
 297    fallback_focused_dock: Option<DockPosition>,
 298    cx: &mut gpui::Context<Workspace>,
 299) {
 300    let project = workspace.project().clone();
 301
 302    if project.read(cx).repositories(cx).is_empty() {
 303        log::error!("create_worktree: no git repository in the project");
 304        return;
 305    }
 306    if project.read(cx).is_via_collab() {
 307        log::error!("create_worktree: not supported in collab projects");
 308        return;
 309    }
 310
 311    // Guard against concurrent creation
 312    if workspace.active_worktree_creation().label.is_some() {
 313        return;
 314    }
 315
 316    let previous_state =
 317        workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
 318    let workspace_handle = workspace.weak_handle();
 319    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
 320    let remote_connection_options = project.read(cx).remote_connection_options(cx);
 321
 322    let (git_repos, non_git_paths) = classify_worktrees(project.read(cx), cx);
 323
 324    if git_repos.is_empty() {
 325        show_error_toast(
 326            cx.entity(),
 327            "worktree create",
 328            anyhow!("No git repositories found in the project"),
 329            cx,
 330        );
 331        return;
 332    }
 333
 334    if remote_connection_options.is_some() {
 335        let is_disconnected = project
 336            .read(cx)
 337            .remote_client()
 338            .is_some_and(|client| client.read(cx).is_disconnected());
 339        if is_disconnected {
 340            show_error_toast(
 341                cx.entity(),
 342                "worktree create",
 343                anyhow!("Cannot create worktree: remote connection is not active"),
 344                cx,
 345            );
 346            return;
 347        }
 348    }
 349
 350    let worktree_name = action.worktree_name.clone();
 351    let branch_target = action.branch_target.clone();
 352    let display_name: SharedString = worktree_name
 353        .as_deref()
 354        .unwrap_or("worktree")
 355        .to_string()
 356        .into();
 357
 358    workspace.set_active_worktree_creation(Some(display_name), false, cx);
 359
 360    cx.spawn_in(window, async move |_workspace_entity, mut cx| {
 361        let result = do_create_worktree(
 362            git_repos,
 363            non_git_paths,
 364            worktree_name,
 365            branch_target,
 366            previous_state,
 367            workspace_handle.clone(),
 368            window_handle,
 369            remote_connection_options,
 370            &mut cx,
 371        )
 372        .await;
 373
 374        if let Err(err) = &result {
 375            log::error!("Failed to create worktree: {err}");
 376            workspace_handle
 377                .update(cx, |workspace, cx| {
 378                    workspace.set_active_worktree_creation(None, false, cx);
 379                    show_error_toast(cx.entity(), "worktree create", anyhow!("{err:#}"), cx);
 380                })
 381                .ok();
 382        }
 383
 384        result
 385    })
 386    .detach_and_log_err(cx);
 387}
 388
 389pub fn handle_switch_worktree(
 390    workspace: &mut Workspace,
 391    action: &zed_actions::SwitchWorktree,
 392    window: &mut gpui::Window,
 393    fallback_focused_dock: Option<DockPosition>,
 394    cx: &mut gpui::Context<Workspace>,
 395) {
 396    let project = workspace.project().clone();
 397
 398    if project.read(cx).repositories(cx).is_empty() {
 399        log::error!("switch_to_worktree: no git repository in the project");
 400        return;
 401    }
 402    if project.read(cx).is_via_collab() {
 403        log::error!("switch_to_worktree: not supported in collab projects");
 404        return;
 405    }
 406
 407    // Guard against concurrent creation
 408    if workspace.active_worktree_creation().label.is_some() {
 409        return;
 410    }
 411
 412    let previous_state =
 413        workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
 414    let workspace_handle = workspace.weak_handle();
 415    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
 416    let remote_connection_options = project.read(cx).remote_connection_options(cx);
 417
 418    let (git_repos, non_git_paths) = classify_worktrees(project.read(cx), cx);
 419
 420    let git_repo_work_dirs: Vec<PathBuf> = git_repos
 421        .iter()
 422        .map(|repo| repo.read(cx).work_directory_abs_path.to_path_buf())
 423        .collect();
 424
 425    let display_name: SharedString = action.display_name.clone().into();
 426
 427    workspace.set_active_worktree_creation(Some(display_name), true, cx);
 428
 429    let worktree_path = action.path.clone();
 430
 431    cx.spawn_in(window, async move |_workspace_entity, mut cx| {
 432        let result = do_switch_worktree(
 433            worktree_path,
 434            git_repo_work_dirs,
 435            non_git_paths,
 436            previous_state,
 437            workspace_handle.clone(),
 438            window_handle,
 439            remote_connection_options,
 440            &mut cx,
 441        )
 442        .await;
 443
 444        if let Err(err) = &result {
 445            log::error!("Failed to switch worktree: {err}");
 446            workspace_handle
 447                .update(cx, |workspace, cx| {
 448                    workspace.set_active_worktree_creation(None, false, cx);
 449                    show_error_toast(cx.entity(), "worktree switch", anyhow!("{err:#}"), cx);
 450                })
 451                .ok();
 452        }
 453
 454        result
 455    })
 456    .detach_and_log_err(cx);
 457}
 458
 459async fn do_create_worktree(
 460    git_repos: Vec<Entity<Repository>>,
 461    non_git_paths: Vec<PathBuf>,
 462    worktree_name: Option<String>,
 463    branch_target: NewWorktreeBranchTarget,
 464    previous_state: PreviousWorkspaceState,
 465    workspace: WeakEntity<Workspace>,
 466    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
 467    remote_connection_options: Option<RemoteConnectionOptions>,
 468    cx: &mut AsyncWindowContext,
 469) -> anyhow::Result<()> {
 470    // List existing worktrees from all repos to detect name collisions
 471    let worktree_receivers: Vec<_> = cx.update(|_, cx| {
 472        git_repos
 473            .iter()
 474            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
 475            .collect()
 476    })?;
 477    let worktree_directory_setting = cx.update(|_, cx| {
 478        ProjectSettings::get_global(cx)
 479            .git
 480            .worktree_directory
 481            .clone()
 482    })?;
 483
 484    let mut existing_worktree_names = Vec::new();
 485    let mut existing_worktree_paths = HashSet::default();
 486    for result in futures::future::join_all(worktree_receivers).await {
 487        match result {
 488            Ok(Ok(worktrees)) => {
 489                for worktree in worktrees {
 490                    if let Some(name) = worktree
 491                        .path
 492                        .parent()
 493                        .and_then(|p| p.file_name())
 494                        .and_then(|n| n.to_str())
 495                    {
 496                        existing_worktree_names.push(name.to_string());
 497                    }
 498                    existing_worktree_paths.insert(worktree.path.clone());
 499                }
 500            }
 501            Ok(Err(err)) => {
 502                Err::<(), _>(err).log_err();
 503            }
 504            Err(_) => {}
 505        }
 506    }
 507
 508    let mut rng = rand::rng();
 509
 510    let base_ref = resolve_worktree_branch_target(&branch_target);
 511
 512    let (creation_infos, path_remapping) = cx.update(|_, cx| {
 513        start_worktree_creations(
 514            &git_repos,
 515            worktree_name,
 516            &existing_worktree_names,
 517            &existing_worktree_paths,
 518            base_ref,
 519            &worktree_directory_setting,
 520            &mut rng,
 521            cx,
 522        )
 523    })??;
 524
 525    let fs = cx.update(|_, cx| <dyn Fs>::global(cx))?;
 526
 527    let created_paths = await_and_rollback_on_failure(creation_infos, fs, cx).await?;
 528
 529    let mut all_paths = created_paths;
 530    let has_non_git = !non_git_paths.is_empty();
 531    all_paths.extend(non_git_paths.iter().cloned());
 532
 533    open_worktree_workspace(
 534        all_paths,
 535        path_remapping,
 536        non_git_paths,
 537        has_non_git,
 538        previous_state,
 539        workspace,
 540        window_handle,
 541        remote_connection_options,
 542        WorktreeOperation::Create,
 543        cx,
 544    )
 545    .await
 546}
 547
 548async fn do_switch_worktree(
 549    worktree_path: PathBuf,
 550    git_repo_work_dirs: Vec<PathBuf>,
 551    non_git_paths: Vec<PathBuf>,
 552    previous_state: PreviousWorkspaceState,
 553    workspace: WeakEntity<Workspace>,
 554    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
 555    remote_connection_options: Option<RemoteConnectionOptions>,
 556    cx: &mut AsyncWindowContext,
 557) -> anyhow::Result<()> {
 558    let path_remapping: Vec<(PathBuf, PathBuf)> = git_repo_work_dirs
 559        .iter()
 560        .map(|work_dir| (work_dir.clone(), worktree_path.clone()))
 561        .collect();
 562
 563    let mut all_paths = vec![worktree_path];
 564    let has_non_git = !non_git_paths.is_empty();
 565    all_paths.extend(non_git_paths.iter().cloned());
 566
 567    open_worktree_workspace(
 568        all_paths,
 569        path_remapping,
 570        non_git_paths,
 571        has_non_git,
 572        previous_state,
 573        workspace,
 574        window_handle,
 575        remote_connection_options,
 576        WorktreeOperation::Switch,
 577        cx,
 578    )
 579    .await
 580}
 581
 582/// Core workspace opening logic shared by both create and switch flows.
 583async fn open_worktree_workspace(
 584    all_paths: Vec<PathBuf>,
 585    path_remapping: Vec<(PathBuf, PathBuf)>,
 586    non_git_paths: Vec<PathBuf>,
 587    has_non_git: bool,
 588    previous_state: PreviousWorkspaceState,
 589    workspace: WeakEntity<Workspace>,
 590    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
 591    remote_connection_options: Option<RemoteConnectionOptions>,
 592    operation: WorktreeOperation,
 593    cx: &mut AsyncWindowContext,
 594) -> anyhow::Result<()> {
 595    let window_handle = window_handle
 596        .ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
 597
 598    let focused_dock = previous_state.focused_dock;
 599
 600    let is_creating_new_worktree = matches!(operation, WorktreeOperation::Create);
 601
 602    let source_for_transfer = if is_creating_new_worktree {
 603        Some(workspace.clone())
 604    } else {
 605        None
 606    };
 607
 608    let (workspace_task, modal_workspace) =
 609        window_handle.update(cx, |multi_workspace, window, cx| {
 610            let path_list = util::path_list::PathList::new(&all_paths);
 611            let active_workspace = multi_workspace.workspace().clone();
 612            let modal_workspace = active_workspace.clone();
 613
 614            let init: Option<
 615                Box<
 616                    dyn FnOnce(&mut Workspace, &mut gpui::Window, &mut gpui::Context<Workspace>)
 617                        + Send,
 618                >,
 619            > = if is_creating_new_worktree {
 620                let dock_structure = previous_state.dock_structure;
 621                Some(Box::new(
 622                    move |workspace: &mut Workspace,
 623                          window: &mut gpui::Window,
 624                          cx: &mut gpui::Context<Workspace>| {
 625                        workspace.set_dock_structure(dock_structure, window, cx);
 626                    },
 627                ))
 628            } else {
 629                None
 630            };
 631
 632            let task = multi_workspace.find_or_create_workspace_with_source_workspace(
 633                path_list,
 634                remote_connection_options,
 635                None,
 636                move |connection_options, window, cx| {
 637                    remote_connection::connect_with_modal(
 638                        &active_workspace,
 639                        connection_options,
 640                        window,
 641                        cx,
 642                    )
 643                },
 644                &[],
 645                init,
 646                OpenMode::Add,
 647                source_for_transfer.clone(),
 648                window,
 649                cx,
 650            );
 651            (task, modal_workspace)
 652        })?;
 653
 654    let result = workspace_task.await;
 655    remote_connection::dismiss_connection_modal(&modal_workspace, cx);
 656    let new_workspace = result?;
 657
 658    let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
 659
 660    if let Some(task) = panels_task {
 661        task.await.log_err();
 662    }
 663
 664    new_workspace
 665        .update(cx, |workspace, cx| {
 666            workspace.project().read(cx).wait_for_initial_scan(cx)
 667        })
 668        .await;
 669
 670    new_workspace
 671        .update(cx, |workspace, cx| {
 672            let repos = workspace
 673                .project()
 674                .read(cx)
 675                .repositories(cx)
 676                .values()
 677                .cloned()
 678                .collect::<Vec<_>>();
 679
 680            let tasks = repos
 681                .into_iter()
 682                .map(|repo| repo.update(cx, |repo, _| repo.barrier()));
 683            futures::future::join_all(tasks)
 684        })
 685        .await;
 686
 687    maybe_propagate_worktree_trust(&workspace, &new_workspace, &all_paths, cx);
 688
 689    if is_creating_new_worktree {
 690        window_handle.update(cx, |_multi_workspace, window, cx| {
 691            new_workspace.update(cx, |workspace, cx| {
 692                if has_non_git {
 693                    struct WorktreeCreationToast;
 694                    let toast_id =
 695                        workspace::notifications::NotificationId::unique::<WorktreeCreationToast>();
 696                    workspace.show_toast(
 697                        workspace::Toast::new(
 698                            toast_id,
 699                            "Some project folders are not git repositories. \
 700                             They were included as-is without creating a worktree.",
 701                        ),
 702                        cx,
 703                    );
 704                }
 705
 706                // Remap every previously-open file path into the new worktree.
 707                let remap_path = |original_path: PathBuf| -> Option<PathBuf> {
 708                    let best_match = path_remapping
 709                        .iter()
 710                        .filter_map(|(old_root, new_root)| {
 711                            original_path.strip_prefix(old_root).ok().map(|relative| {
 712                                (old_root.components().count(), new_root.join(relative))
 713                            })
 714                        })
 715                        .max_by_key(|(depth, _)| *depth);
 716
 717                    if let Some((_, remapped_path)) = best_match {
 718                        return Some(remapped_path);
 719                    }
 720
 721                    for non_git in &non_git_paths {
 722                        if original_path.starts_with(non_git) {
 723                            return Some(original_path);
 724                        }
 725                    }
 726                    None
 727                };
 728
 729                let remapped_active_path =
 730                    previous_state.active_file_path.and_then(|p| remap_path(p));
 731
 732                let mut paths_to_open: Vec<PathBuf> = Vec::new();
 733                let mut seen = HashSet::default();
 734                for path in previous_state.open_file_paths {
 735                    if let Some(remapped) = remap_path(path) {
 736                        if remapped_active_path.as_ref() != Some(&remapped)
 737                            && seen.insert(remapped.clone())
 738                        {
 739                            paths_to_open.push(remapped);
 740                        }
 741                    }
 742                }
 743
 744                if let Some(active) = &remapped_active_path {
 745                    if seen.insert(active.clone()) {
 746                        paths_to_open.push(active.clone());
 747                    }
 748                }
 749
 750                if !paths_to_open.is_empty() {
 751                    let should_focus_center = focused_dock.is_none();
 752                    let open_task = workspace.open_paths(
 753                        paths_to_open,
 754                        workspace::OpenOptions {
 755                            focus: Some(false),
 756                            ..Default::default()
 757                        },
 758                        None,
 759                        window,
 760                        cx,
 761                    );
 762                    cx.spawn_in(window, async move |workspace, cx| {
 763                        for item in open_task.await.into_iter().flatten() {
 764                            item.log_err();
 765                        }
 766                        if should_focus_center {
 767                            workspace.update_in(cx, |workspace, window, cx| {
 768                                workspace.focus_center_pane(window, cx);
 769                            })?;
 770                        }
 771                        anyhow::Ok(())
 772                    })
 773                    .detach_and_log_err(cx);
 774                }
 775            });
 776        })?;
 777    }
 778
 779    // Clear the creation status on the SOURCE workspace so its title bar
 780    // stops showing the loading indicator immediately.
 781    workspace
 782        .update(cx, |ws, cx| {
 783            ws.set_active_worktree_creation(None, false, cx);
 784        })
 785        .ok();
 786
 787    window_handle.update(cx, |multi_workspace, window, cx| {
 788        multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
 789
 790        if is_creating_new_worktree {
 791            new_workspace.update(cx, |workspace, cx| {
 792                workspace.run_create_worktree_tasks(window, cx);
 793
 794                if let Some(dock_position) = focused_dock {
 795                    let dock = workspace.dock_at_position(dock_position);
 796                    if let Some(panel) = dock.read(cx).active_panel() {
 797                        panel.panel_focus_handle(cx).focus(window, cx);
 798                    }
 799                }
 800            });
 801        }
 802    })?;
 803
 804    anyhow::Ok(())
 805}
 806
 807#[cfg(test)]
 808mod tests {
 809    use super::*;
 810    use fs::Fs;
 811    use gpui::{App, Task, TestAppContext};
 812    use language::language_settings::AllLanguageSettings;
 813    use project::project_settings::ProjectSettings;
 814    use project::task_store::{TaskSettingsLocation, TaskStore};
 815    use project::{FakeFs, WorktreeSettings};
 816    use serde_json::json;
 817    use settings::{SettingsLocation, SettingsStore};
 818    use std::path::{Path, PathBuf};
 819    use std::process::ExitStatus;
 820    use std::sync::Mutex;
 821    use task::SpawnInTerminal;
 822    use theme::LoadThemes;
 823    use util::path;
 824    use util::rel_path::rel_path;
 825    use workspace::{TerminalProvider, WorkspaceSettings};
 826
 827    struct CountingTerminalProvider {
 828        spawned_task_labels: Arc<Mutex<Vec<String>>>,
 829    }
 830
 831    impl TerminalProvider for CountingTerminalProvider {
 832        fn spawn(
 833            &self,
 834            task: SpawnInTerminal,
 835            _window: &mut ui::Window,
 836            _cx: &mut App,
 837        ) -> Task<Option<anyhow::Result<ExitStatus>>> {
 838            self.spawned_task_labels
 839                .lock()
 840                .expect("terminal spawn mutex should not be poisoned")
 841                .push(task.label);
 842            Task::ready(Some(Ok(ExitStatus::default())))
 843        }
 844    }
 845
 846    fn init_test(cx: &mut TestAppContext) {
 847        zlog::init_test();
 848        cx.update(|cx| {
 849            let settings_store = SettingsStore::test(cx);
 850            cx.set_global(settings_store);
 851            theme_settings::init(LoadThemes::JustBase, cx);
 852            AllLanguageSettings::register(cx);
 853            editor::init(cx);
 854            ProjectSettings::register(cx);
 855            WorktreeSettings::register(cx);
 856            WorkspaceSettings::register(cx);
 857            TaskStore::init(None);
 858        });
 859    }
 860
 861    fn install_counting_provider_and_worktree_hook(
 862        workspace: &Entity<Workspace>,
 863        spawned_task_labels: &Arc<Mutex<Vec<String>>>,
 864        main_project_root: &Path,
 865        hook_tasks_json: &str,
 866        cx: &mut App,
 867    ) {
 868        workspace.update(cx, |workspace, cx| {
 869            workspace.set_terminal_provider(CountingTerminalProvider {
 870                spawned_task_labels: spawned_task_labels.clone(),
 871            });
 872
 873            let project = workspace.project().clone();
 874            let Some(worktree) = project.read(cx).worktrees(cx).next() else {
 875                return;
 876            };
 877            let worktree = worktree.read(cx);
 878            let worktree_id = worktree.id();
 879            let worktree_root = worktree.abs_path().to_path_buf();
 880            if worktree_root == main_project_root {
 881                return;
 882            }
 883
 884            let Some(task_inventory) = project
 885                .read(cx)
 886                .task_store()
 887                .read(cx)
 888                .task_inventory()
 889                .cloned()
 890            else {
 891                return;
 892            };
 893            task_inventory.update(cx, |inventory, _| {
 894                inventory
 895                    .update_file_based_tasks(
 896                        TaskSettingsLocation::Worktree(SettingsLocation {
 897                            worktree_id,
 898                            path: rel_path(".zed"),
 899                        }),
 900                        Some(hook_tasks_json),
 901                    )
 902                    .expect("should inject create_worktree hook tasks for linked worktree");
 903            });
 904        });
 905    }
 906
 907    #[gpui::test]
 908    async fn test_create_worktree_hook_does_not_run_when_switching_back_to_main_worktree(
 909        cx: &mut TestAppContext,
 910    ) {
 911        init_test(cx);
 912
 913        let hook_tasks_json = r#"[{"label":"setup worktree","command":"echo","hide":"never","hooks":["create_worktree"]}]"#;
 914        let fs = FakeFs::new(cx.background_executor.clone());
 915        cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
 916        fs.insert_tree(
 917            "/root",
 918            json!({
 919                "project": {
 920                    ".git": {},
 921                    ".zed": {
 922                        "tasks.json": hook_tasks_json,
 923                    },
 924                    "src": {
 925                        "main.rs": "fn main() {}",
 926                    },
 927                },
 928            }),
 929        )
 930        .await;
 931
 932        let main_project_root = PathBuf::from(path!("/root/project"));
 933        let project = Project::test(fs.clone(), [main_project_root.as_path()], cx).await;
 934        project
 935            .update(cx, |project, cx| project.git_scans_complete(cx))
 936            .await;
 937
 938        let (multi_workspace, cx) =
 939            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
 940
 941        let spawned_task_labels = Arc::new(Mutex::new(Vec::new()));
 942        multi_workspace.update(cx, |multi_workspace, cx| {
 943            multi_workspace.retain_active_workspace(cx);
 944            let active_workspace = multi_workspace.workspace().clone();
 945            install_counting_provider_and_worktree_hook(
 946                &active_workspace,
 947                &spawned_task_labels,
 948                &main_project_root,
 949                hook_tasks_json,
 950                cx,
 951            );
 952        });
 953
 954        let main_workspace =
 955            multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
 956        main_workspace.update_in(cx, |workspace, window, cx| {
 957            handle_create_worktree(
 958                workspace,
 959                &zed_actions::CreateWorktree {
 960                    worktree_name: Some("feature".to_string()),
 961                    branch_target: NewWorktreeBranchTarget::CurrentBranch,
 962                },
 963                window,
 964                None,
 965                cx,
 966            );
 967        });
 968        cx.run_until_parked();
 969
 970        let active_workspace =
 971            multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
 972        cx.update(|_, cx| {
 973            install_counting_provider_and_worktree_hook(
 974                &active_workspace,
 975                &spawned_task_labels,
 976                &main_project_root,
 977                hook_tasks_json,
 978                cx,
 979            );
 980        });
 981        active_workspace.update_in(cx, |workspace, window, cx| {
 982            workspace.run_create_worktree_tasks(window, cx);
 983        });
 984        cx.run_until_parked();
 985
 986        assert_eq!(
 987            spawned_task_labels
 988                .lock()
 989                .expect("terminal spawn mutex should not be poisoned")
 990                .as_slice(),
 991            ["setup worktree"],
 992            "create_worktree hook should run once for the created linked worktree"
 993        );
 994
 995        active_workspace.update_in(cx, |workspace, window, cx| {
 996            handle_switch_worktree(
 997                workspace,
 998                &zed_actions::SwitchWorktree {
 999                    path: main_project_root.clone(),
1000                    display_name: "project".to_string(),
1001                },
1002                window,
1003                None,
1004                cx,
1005            );
1006        });
1007        cx.run_until_parked();
1008
1009        assert_eq!(
1010            spawned_task_labels
1011                .lock()
1012                .expect("terminal spawn mutex should not be poisoned")
1013                .as_slice(),
1014            ["setup worktree"],
1015            "switching back to the main worktree should not rerun create_worktree hooks"
1016        );
1017    }
1018}