Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:33:18.453Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

worktree_service.rs

1771 lines · 62.8 KB · rust
1use std::error::Error;
2use std::fmt;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use anyhow::anyhow;
7use askpass::AskPassDelegate;
8use collections::HashSet;
9use fs::Fs;
10use gpui::{
11    AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
12    Task, TaskExt, WeakEntity,
13};
14use project::Project;
15use project::git_store::Repository;
16use project::project_settings::ProjectSettings;
17use project::trusted_worktrees::{PathTrust, TrustedWorktrees};
18use remote::RemoteConnectionOptions;
19use settings::Settings;
20use ui::prelude::*;
21use workspace::{
22    MultiWorkspace, OpenMode, PreviousWorkspaceState, ToastView, Workspace, dock::DockPosition,
23};
24use zed_actions::NewWorktreeBranchTarget;
25
26use git::repository::{FetchOptions, Remote};
27
28use util::ResultExt as _;
29
30use crate::askpass_modal::AskPassModal;
31use crate::git_panel::{open_output, show_error_toast};
32use crate::worktree_names;
33
34/// A remote-tracking branch reference parsed into its remote and branch parts,
35/// e.g. `origin/main` -> remote `origin`, branch `main`.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct RemoteBranchName {
38    pub remote_name: String,
39    pub branch_name: String,
40}
41
42impl RemoteBranchName {
43    pub fn parse(name: &str) -> Option<Self> {
44        let name = name.strip_prefix("refs/remotes/").unwrap_or(name);
45        let (remote_name, branch_name) = name.split_once('/')?;
46        if remote_name.is_empty() || branch_name.is_empty() {
47            return None;
48        }
49        Some(Self {
50            remote_name: remote_name.to_string(),
51            branch_name: branch_name.to_string(),
52        })
53    }
54
55    pub fn display_name(&self) -> String {
56        format!("{}/{}", self.remote_name, self.branch_name)
57    }
58}
59
60/// A "create new worktree" option offered to the user. The set of targets is
61/// derived from repository state by [`worktree_create_targets`] so that the
62/// worktree picker and the sidebar's new-thread menu stay in sync.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum WorktreeCreateTarget {
65    CurrentBranch,
66    DefaultBranch(RemoteBranchName),
67}
68
69impl WorktreeCreateTarget {
70    pub fn branch_target(&self) -> NewWorktreeBranchTarget {
71        match self {
72            WorktreeCreateTarget::CurrentBranch => NewWorktreeBranchTarget::CurrentBranch,
73            WorktreeCreateTarget::DefaultBranch(default_branch) => {
74                NewWorktreeBranchTarget::RemoteBranch {
75                    remote_name: default_branch.remote_name.clone(),
76                    branch_name: default_branch.branch_name.clone(),
77                }
78            }
79        }
80    }
81
82    pub fn branch_label(
83        &self,
84        has_multiple_repositories: bool,
85        current_branch_name: Option<&str>,
86    ) -> String {
87        match self {
88            WorktreeCreateTarget::DefaultBranch(default_branch) => default_branch.display_name(),
89            WorktreeCreateTarget::CurrentBranch => {
90                if has_multiple_repositories {
91                    "current branches".to_string()
92                } else {
93                    current_branch_name.unwrap_or("HEAD").to_string()
94                }
95            }
96        }
97    }
98}
99
100/// Determines which "create new worktree" options to surface for the given
101/// repository state: prefer the remote default branch when it differs from the
102/// current branch, and otherwise offer the current branch.
103pub fn worktree_create_targets(
104    has_multiple_repositories: bool,
105    default_branch: Option<RemoteBranchName>,
106    current_branch_name: Option<&str>,
107) -> Vec<WorktreeCreateTarget> {
108    if has_multiple_repositories {
109        return vec![WorktreeCreateTarget::CurrentBranch];
110    }
111    let Some(default_branch) = default_branch else {
112        return vec![WorktreeCreateTarget::CurrentBranch];
113    };
114    let is_different =
115        current_branch_name.is_none_or(|current| current != default_branch.branch_name);
116    let mut targets = vec![WorktreeCreateTarget::DefaultBranch(default_branch)];
117    if is_different {
118        targets.push(WorktreeCreateTarget::CurrentBranch);
119    }
120    targets
121}
122
123/// Whether a worktree operation is creating a new one or switching to an
124/// existing one. Controls whether the source workspace's state (dock layout,
125/// open files, agent panel draft) is inherited by the destination.
126enum WorktreeOperation {
127    Create,
128    Switch,
129}
130
131#[derive(Clone, Copy, PartialEq, Eq)]
132enum RemoteBranchFetchMode {
133    Fetch,
134    UseLocal,
135}
136
137impl RemoteBranchFetchMode {
138    fn should_fetch(self) -> bool {
139        matches!(self, Self::Fetch)
140    }
141}
142
143#[derive(Debug)]
144struct WorktreeFetchError {
145    remote_name: String,
146    branch_name: String,
147    source: anyhow::Error,
148}
149
150impl WorktreeFetchError {
151    fn remote_branch_name(&self) -> String {
152        format!("{}/{}", self.remote_name, self.branch_name)
153    }
154
155    fn output(&self) -> String {
156        format!("git fetch {} failed:\n{:#}", self.remote_name, self.source)
157    }
158}
159
160impl fmt::Display for WorktreeFetchError {
161    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(
163            formatter,
164            "git fetch {} failed while creating worktree from {}: {}",
165            self.remote_name,
166            self.remote_branch_name(),
167            self.source
168        )
169    }
170}
171
172impl Error for WorktreeFetchError {
173    fn source(&self) -> Option<&(dyn Error + 'static)> {
174        Some(self.source.as_ref())
175    }
176}
177
178struct WorktreeFetchFailedToast {
179    workspace: WeakEntity<Workspace>,
180    worktree_name: Option<String>,
181    branch_target: NewWorktreeBranchTarget,
182    focused_dock: Option<DockPosition>,
183    remote_branch_name: String,
184    operation: SharedString,
185    output: String,
186    focus_handle: FocusHandle,
187}
188
189impl WorktreeFetchFailedToast {
190    fn new(
191        workspace: WeakEntity<Workspace>,
192        worktree_name: Option<String>,
193        branch_target: NewWorktreeBranchTarget,
194        focused_dock: Option<DockPosition>,
195        fetch_error: &WorktreeFetchError,
196        cx: &mut gpui::Context<Self>,
197    ) -> Self {
198        Self {
199            workspace,
200            worktree_name,
201            branch_target,
202            focused_dock,
203            remote_branch_name: fetch_error.remote_branch_name(),
204            operation: format!("fetch {}", fetch_error.remote_name).into(),
205            output: fetch_error.output(),
206            focus_handle: cx.focus_handle(),
207        }
208    }
209}
210
211impl Focusable for WorktreeFetchFailedToast {
212    fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
213        self.focus_handle.clone()
214    }
215}
216
217impl EventEmitter<DismissEvent> for WorktreeFetchFailedToast {}
218
219impl ToastView for WorktreeFetchFailedToast {
220    fn action(&self) -> Option<workspace::ToastAction> {
221        None
222    }
223
224    fn auto_dismiss(&self) -> bool {
225        false
226    }
227}
228
229impl Render for WorktreeFetchFailedToast {
230    fn render(&mut self, _window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
231        let workspace_for_retry = self.workspace.clone();
232        let worktree_name = self.worktree_name.clone();
233        let branch_target = self.branch_target.clone();
234        let focused_dock = self.focused_dock;
235
236        let workspace_for_log = self.workspace.clone();
237        let operation = self.operation.clone();
238        let output = self.output.clone();
239
240        h_flex()
241            .id("worktree-fetch-failed-toast")
242            .elevation_3(cx)
243            .gap_2()
244            .py_1p5()
245            .pl_2p5()
246            .pr_1p5()
247            .flex_none()
248            .bg(cx.theme().colors().surface_background)
249            .shadow_lg()
250            .child(
251                Icon::new(IconName::XCircle)
252                    .size(IconSize::Small)
253                    .color(Color::Error),
254            )
255            .child(Label::new(format!(
256                "git fetch failed for {}",
257                self.remote_branch_name
258            )))
259            .child(
260                Button::new(
261                    "use-local-worktree-base",
262                    format!("Use local {}", self.remote_branch_name),
263                )
264                .color(Color::Muted)
265                .on_click(cx.listener(move |_, _event, window, cx| {
266                    cx.emit(DismissEvent);
267                    if let Some(workspace) = workspace_for_retry.upgrade() {
268                        workspace.update(cx, |workspace, cx| {
269                            let task = create_worktree_workspace_inner(
270                                workspace,
271                                &zed_actions::CreateWorktree {
272                                    worktree_name: worktree_name.clone(),
273                                    branch_target: branch_target.clone(),
274                                },
275                                window,
276                                focused_dock,
277                                RemoteBranchFetchMode::UseLocal,
278                                // User-initiated retry of a foreground create.
279                                true,
280                                cx,
281                            );
282                            task.detach_and_log_err(cx);
283                        });
284                    }
285                })),
286            )
287            .child(
288                Button::new("view-worktree-fetch-log", "Show Error Logs")
289                    .color(Color::Muted)
290                    .on_click(cx.listener(move |_, _event, window, cx| {
291                        cx.emit(DismissEvent);
292                        let output = output.clone();
293                        let operation = operation.clone();
294                        workspace_for_log
295                            .update(cx, move |workspace, cx| {
296                                open_output(operation, workspace, &output, window, cx)
297                            })
298                            .ok();
299                    })),
300            )
301            .child(
302                IconButton::new("dismiss-worktree-fetch-failed-toast", IconName::Close)
303                    .shape(ui::IconButtonShape::Square)
304                    .icon_size(IconSize::Small)
305                    .icon_color(Color::Muted)
306                    .on_click(cx.listener(|_, _event, _window, cx| {
307                        cx.emit(DismissEvent);
308                    })),
309            )
310    }
311}
312
313/// Classifies the project's visible worktrees into git-managed repositories
314/// and non-git paths. Each unique repository is returned only once.
315pub fn classify_worktrees(
316    project: &Project,
317    cx: &gpui::App,
318) -> (Vec<Entity<Repository>>, Vec<PathBuf>) {
319    let repositories = project.repositories(cx).clone();
320    let mut git_repos: Vec<Entity<Repository>> = Vec::new();
321    let mut non_git_paths: Vec<PathBuf> = Vec::new();
322    let mut seen_repo_ids = HashSet::default();
323
324    for worktree in project.visible_worktrees(cx) {
325        let wt_path = worktree.read(cx).abs_path();
326
327        let matching_repo = repositories
328            .iter()
329            .filter_map(|(id, repo)| {
330                let work_dir = repo.read(cx).work_directory_abs_path.clone();
331                if wt_path.starts_with(work_dir.as_ref()) {
332                    Some((*id, repo.clone(), work_dir.as_ref().components().count()))
333                } else {
334                    None
335                }
336            })
337            .max_by(
338                |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
339                    left_depth
340                        .cmp(right_depth)
341                        .then_with(|| left_id.cmp(right_id))
342                },
343            );
344
345        if let Some((id, repo, _)) = matching_repo {
346            if seen_repo_ids.insert(id) {
347                git_repos.push(repo);
348            }
349        } else {
350            non_git_paths.push(wt_path.to_path_buf());
351        }
352    }
353
354    (git_repos, non_git_paths)
355}
356
357/// Resolves a branch target into the ref the new worktree should be based on.
358/// Returns `None` for `CurrentBranch`, meaning "use the current HEAD".
359pub fn resolve_worktree_branch_target(branch_target: &NewWorktreeBranchTarget) -> Option<String> {
360    match branch_target {
361        NewWorktreeBranchTarget::CurrentBranch => None,
362        NewWorktreeBranchTarget::ExistingBranch { name } => Some(name.clone()),
363        NewWorktreeBranchTarget::RemoteBranch {
364            remote_name,
365            branch_name,
366        } => Some(format!("refs/remotes/{remote_name}/{branch_name}")),
367    }
368}
369
370fn remote_branch_to_fetch(branch_target: &NewWorktreeBranchTarget) -> Option<(&str, &str)> {
371    match branch_target {
372        NewWorktreeBranchTarget::RemoteBranch {
373            remote_name,
374            branch_name,
375        } => Some((remote_name, branch_name)),
376        NewWorktreeBranchTarget::CurrentBranch | NewWorktreeBranchTarget::ExistingBranch { .. } => {
377            None
378        }
379    }
380}
381
382fn create_worktree_askpass_delegate(
383    workspace: WeakEntity<Workspace>,
384    operation: impl Into<SharedString>,
385    window: &mut Window,
386    cx: &mut Context<Workspace>,
387) -> AskPassDelegate {
388    let operation = operation.into();
389    let window = window.window_handle();
390    AskPassDelegate::new(&mut cx.to_async(), move |prompt, tx, cx| {
391        window
392            .update(cx, |_, window, cx| {
393                workspace.update(cx, |workspace, cx| {
394                    workspace.toggle_modal(window, cx, |window, cx| {
395                        AskPassModal::new(operation.clone(), prompt.into(), tx, window, cx)
396                    });
397                })
398            })
399            .ok();
400    })
401}
402
403async fn fetch_remote_for_worktree_base(
404    git_repos: &[Entity<Repository>],
405    remote_name: String,
406    askpass_delegates: Vec<AskPassDelegate>,
407    cx: &mut AsyncWindowContext,
408) -> anyhow::Result<()> {
409    if askpass_delegates.len() != git_repos.len() {
410        return Err(anyhow!(
411            "Unable to fetch {remote_name}: missing credential prompt delegate"
412        ));
413    }
414
415    let fetches = cx.update(|_, cx| {
416        git_repos
417            .iter()
418            .cloned()
419            .zip(askpass_delegates)
420            .map(|(repo, askpass)| {
421                repo.update(cx, |repo, cx| {
422                    repo.fetch(
423                        FetchOptions::Remote(Remote {
424                            name: remote_name.clone().into(),
425                        }),
426                        askpass,
427                        cx,
428                    )
429                })
430            })
431            .collect::<Vec<_>>()
432    })?;
433
434    for fetch in futures::future::join_all(fetches).await {
435        fetch??;
436    }
437
438    Ok(())
439}
440
441/// Kicks off an async git-worktree creation for each repository. Returns:
442///
443/// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples.
444/// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs for remapping editor tabs.
445///
446/// Multiple entries in `git_repos` can be linked worktrees of the *same*
447/// underlying repository (e.g. a project that has both the main checkout and
448/// one of its linked worktrees open as separate Zed worktrees). Those entries
449/// resolve to the same target path via [`Repository::path_for_new_linked_worktree`],
450/// so we create the new worktree only once and remap every contributing
451/// work directory onto it. Without this dedup, the second `git worktree add`
452/// fails with "already exists".
453fn start_worktree_creations(
454    git_repos: &[Entity<Repository>],
455    worktree_name: Option<String>,
456    existing_worktree_names: &[String],
457    existing_worktree_paths: &HashSet<PathBuf>,
458    base_ref: Option<String>,
459    worktree_directory_setting: &str,
460    rng: &mut impl rand::Rng,
461    cx: &mut gpui::App,
462) -> anyhow::Result<(
463    Vec<(
464        Entity<Repository>,
465        PathBuf,
466        futures::channel::oneshot::Receiver<anyhow::Result<()>>,
467    )>,
468    Vec<(PathBuf, PathBuf)>,
469)> {
470    let mut creation_infos = Vec::new();
471    let mut path_remapping = Vec::new();
472    let mut scheduled_paths: HashSet<PathBuf> = HashSet::default();
473
474    let worktree_name = worktree_name.unwrap_or_else(|| {
475        let existing_refs: Vec<&str> = existing_worktree_names.iter().map(|s| s.as_str()).collect();
476        worktree_names::generate_worktree_name(&existing_refs, rng)
477            .unwrap_or_else(|| "worktree".to_string())
478    });
479
480    for repo in git_repos {
481        let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
482            let new_path =
483                repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?;
484            if existing_worktree_paths.contains(&new_path) {
485                anyhow::bail!("A worktree already exists at {}", new_path.display());
486            }
487            let work_dir = repo.work_directory_abs_path.clone();
488            // Only the first repo that resolves to a given target path
489            // actually creates the worktree; subsequent linked worktrees of
490            // the same repository just contribute a path remapping.
491            let receiver = if scheduled_paths.contains(&new_path) {
492                None
493            } else {
494                let target = git::repository::CreateWorktreeTarget::Detached {
495                    base_sha: base_ref.clone(),
496                };
497                Some(repo.create_worktree(target, new_path.clone()))
498            };
499            anyhow::Ok((work_dir, new_path, receiver))
500        })?;
501        path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
502        if let Some(receiver) = receiver {
503            scheduled_paths.insert(new_path.clone());
504            creation_infos.push((repo.clone(), new_path, receiver));
505        }
506    }
507
508    Ok((creation_infos, path_remapping))
509}
510
511/// Waits for every in-flight worktree creation to complete. If any
512/// creation fails, all successfully-created worktrees are rolled back
513/// (removed) so the project isn't left in a half-migrated state.
514pub async fn await_and_rollback_on_failure(
515    creation_infos: Vec<(
516        Entity<Repository>,
517        PathBuf,
518        futures::channel::oneshot::Receiver<anyhow::Result<()>>,
519    )>,
520    fs: Arc<dyn Fs>,
521    cx: &mut AsyncWindowContext,
522) -> anyhow::Result<Vec<PathBuf>> {
523    let mut created_paths: Vec<PathBuf> = Vec::new();
524    let mut repos_and_paths: Vec<(Entity<Repository>, PathBuf)> = Vec::new();
525    let mut first_error: Option<anyhow::Error> = None;
526
527    for (repo, new_path, receiver) in creation_infos {
528        repos_and_paths.push((repo.clone(), new_path.clone()));
529        match receiver.await {
530            Ok(Ok(())) => {
531                created_paths.push(new_path);
532            }
533            Ok(Err(err)) => {
534                if first_error.is_none() {
535                    first_error = Some(err);
536                }
537            }
538            Err(_canceled) => {
539                if first_error.is_none() {
540                    first_error = Some(anyhow!("Worktree creation was canceled"));
541                }
542            }
543        }
544    }
545
546    let Some(err) = first_error else {
547        return Ok(created_paths);
548    };
549
550    // Rollback all attempted worktrees
551    let mut rollback_futures = Vec::new();
552    for (rollback_repo, rollback_path) in &repos_and_paths {
553        let receiver = cx
554            .update(|_, cx| {
555                rollback_repo.update(cx, |repo, _cx| {
556                    repo.remove_worktree(rollback_path.clone(), true)
557                })
558            })
559            .ok();
560
561        rollback_futures.push((rollback_path.clone(), receiver));
562    }
563
564    let mut rollback_failures: Vec<String> = Vec::new();
565    for (path, receiver_opt) in rollback_futures {
566        let mut git_remove_failed = false;
567
568        if let Some(receiver) = receiver_opt {
569            match receiver.await {
570                Ok(Ok(())) => {}
571                Ok(Err(rollback_err)) => {
572                    log::error!(
573                        "git worktree remove failed for {}: {rollback_err}",
574                        path.display()
575                    );
576                    git_remove_failed = true;
577                }
578                Err(canceled) => {
579                    log::error!(
580                        "git worktree remove failed for {}: {canceled}",
581                        path.display()
582                    );
583                    git_remove_failed = true;
584                }
585            }
586        } else {
587            log::error!(
588                "failed to dispatch git worktree remove for {}",
589                path.display()
590            );
591            git_remove_failed = true;
592        }
593
594        if git_remove_failed {
595            if let Err(fs_err) = fs
596                .remove_dir(
597                    &path,
598                    fs::RemoveOptions {
599                        recursive: true,
600                        ignore_if_not_exists: true,
601                    },
602                )
603                .await
604            {
605                let msg = format!("{}: failed to remove directory: {fs_err}", path.display());
606                log::error!("{}", msg);
607                rollback_failures.push(msg);
608            }
609        }
610    }
611    let mut error_message = format!("Failed to create worktree: {err}");
612    if !rollback_failures.is_empty() {
613        error_message.push_str("\n\nFailed to clean up: ");
614        error_message.push_str(&rollback_failures.join(", "));
615    }
616    Err(anyhow!(error_message))
617}
618
619/// Propagates worktree trust from the source workspace to the new workspace.
620/// If the source project's worktrees are all trusted, the new worktree paths
621/// will also be trusted automatically.
622fn maybe_propagate_worktree_trust(
623    source_workspace: &WeakEntity<Workspace>,
624    new_workspace: &Entity<Workspace>,
625    paths: &[PathBuf],
626    cx: &mut AsyncWindowContext,
627) {
628    cx.update(|_, cx| {
629        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
630            return;
631        }
632        let source_is_trusted = source_workspace
633            .upgrade()
634            .map(|workspace| {
635                let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store();
636                !TrustedWorktrees::has_restricted_worktrees(&source_worktree_store, cx)
637            })
638            .unwrap_or(false);
639
640        if !source_is_trusted {
641            return;
642        }
643
644        let worktree_store = new_workspace.read(cx).project().read(cx).worktree_store();
645        let paths_to_trust: HashSet<_> = paths
646            .iter()
647            .filter_map(|path| {
648                let (worktree, _) = worktree_store.read(cx).find_worktree(path, cx)?;
649                Some(PathTrust::Worktree(worktree.read(cx).id()))
650            })
651            .collect();
652
653        if !paths_to_trust.is_empty() {
654            if let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) {
655                trusted_store.update(cx, |store, cx| {
656                    store.trust(&worktree_store, paths_to_trust, cx);
657                });
658            }
659        }
660    })
661    .ok();
662
663}
664
665/// Handles the `CreateWorktree` action generically, without any agent panel involvement.
666/// Creates a new git worktree, opens the workspace, restores layout and files.
667/// Errors are surfaced to the user via toasts; the new workspace handle is
668/// discarded. Use [`create_worktree_workspace`] when you need the resulting
669/// workspace (e.g., the `create_thread` agent tool spawns a thread in it).
670pub fn handle_create_worktree(
671    workspace: &mut Workspace,
672    action: &zed_actions::CreateWorktree,
673    window: &mut gpui::Window,
674    fallback_focused_dock: Option<DockPosition>,
675    cx: &mut gpui::Context<Workspace>,
676) {
677    let task = create_worktree_workspace_inner(
678        workspace,
679        action,
680        window,
681        fallback_focused_dock,
682        RemoteBranchFetchMode::Fetch,
683        // The user explicitly asked to create a worktree, so foreground it.
684        true,
685        cx,
686    );
687    task.detach_and_log_err(cx);
688}
689
690/// Outcome of [`create_worktree_workspace`].
691pub struct CreatedWorktreeWorkspace {
692    /// The newly opened workspace.
693    pub workspace: Entity<Workspace>,
694    /// True when the project contained more than one Zed worktree backed by
695    /// the same underlying git repository, so they were consolidated into a
696    /// single new worktree (they resolve to the same target path). Callers
697    /// that care — like the `create_thread` agent tool — can use this to warn
698    /// that the result may not reflect every source worktree's state.
699    pub consolidated_worktrees: bool,
700}
701
702/// Same as [`handle_create_worktree`], but returns a `Task` that resolves to
703/// the new workspace once worktree creation and post-open setup are
704/// complete. The caller receives errors as `Result`s and is expected to
705/// handle them. Note that a small set of early failures (no git repositories,
706/// disconnected remote, mid-creation `git fetch` failure) still surface a
707/// toast on the source workspace so the user understands why the action
708/// didn't take effect; the same error is also returned to the caller.
709///
710/// Used by the `create_thread` agent tool to spawn a sibling thread inside
711/// the newly-opened workspace.
712///
713/// The new workspace is opened in the **background** (added as a retained
714/// tab without switching to it or moving focus), and it's a clean checkout
715/// rather than inheriting the source workspace's open files and dock layout.
716/// This mirrors how the agent's non-worktree threads are created in the
717/// background rather than yanking the user away from what they're doing.
718pub fn create_worktree_workspace(
719    workspace: &mut Workspace,
720    action: &zed_actions::CreateWorktree,
721    window: &mut gpui::Window,
722    fallback_focused_dock: Option<DockPosition>,
723    cx: &mut gpui::Context<Workspace>,
724) -> Task<anyhow::Result<CreatedWorktreeWorkspace>> {
725    create_worktree_workspace_inner(
726        workspace,
727        action,
728        window,
729        fallback_focused_dock,
730        RemoteBranchFetchMode::Fetch,
731        // Agent-created worktree workspaces open in the background.
732        false,
733        cx,
734    )
735}
736
737fn create_worktree_workspace_inner(
738    workspace: &mut Workspace,
739    action: &zed_actions::CreateWorktree,
740    window: &mut gpui::Window,
741    fallback_focused_dock: Option<DockPosition>,
742    remote_branch_fetch_mode: RemoteBranchFetchMode,
743    activate: bool,
744    cx: &mut gpui::Context<Workspace>,
745) -> Task<anyhow::Result<CreatedWorktreeWorkspace>> {
746    let project = workspace.project().clone();
747
748    if project.read(cx).repositories(cx).is_empty() {
749        return Task::ready(Err(anyhow!(
750            "create_worktree: no git repository in the project"
751        )));
752    }
753    if project.read(cx).is_via_collab() {
754        return Task::ready(Err(anyhow!(
755            "create_worktree: not supported in collab projects"
756        )));
757    }
758
759    // Guard against concurrent creation. We treat a concurrent creation as
760    // a hard error here so the caller can surface it; the user-facing
761    // wrapper [`handle_create_worktree`] swallows the error via
762    // `detach_and_log_err`, matching the pre-existing silent return.
763    if workspace.active_worktree_creation().label.is_some() {
764        return Task::ready(Err(anyhow!("A worktree creation is already in progress")));
765    }
766
767    let previous_state =
768        workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
769    let workspace_handle = workspace.weak_handle();
770    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
771    let remote_connection_options = project.read(cx).remote_connection_options(cx);
772
773    let (git_repos, non_git_paths) = classify_worktrees(project.read(cx), cx);
774
775    if git_repos.is_empty() {
776        let toast_workspace = cx.entity();
777        show_error_toast(
778            toast_workspace,
779            "worktree create",
780            anyhow!("No git repositories found in the project"),
781            cx,
782        );
783        return Task::ready(Err(anyhow!("No git repositories found in the project")));
784    }
785
786    if remote_connection_options.is_some() {
787        let is_disconnected = project
788            .read(cx)
789            .remote_client()
790            .is_some_and(|client| client.read(cx).is_disconnected());
791        if is_disconnected {
792            let toast_workspace = cx.entity();
793            show_error_toast(
794                toast_workspace,
795                "worktree create",
796                anyhow!("Cannot create worktree: remote connection is not active"),
797                cx,
798            );
799            return Task::ready(Err(anyhow!(
800                "Cannot create worktree: remote connection is not active"
801            )));
802        }
803    }
804
805    let worktree_name = action.worktree_name.clone();
806    let branch_target = action.branch_target.clone();
807    let fetch_askpass_delegates = if remote_branch_fetch_mode.should_fetch() {
808        remote_branch_to_fetch(&branch_target)
809            .map(|(remote_name, _branch_name)| {
810                git_repos
811                    .iter()
812                    .map(|_| {
813                        create_worktree_askpass_delegate(
814                            workspace_handle.clone(),
815                            format!("git fetch {remote_name}"),
816                            window,
817                            cx,
818                        )
819                    })
820                    .collect()
821            })
822            .unwrap_or_default()
823    } else {
824        Vec::new()
825    };
826    let display_name: SharedString = worktree_name
827        .as_deref()
828        .unwrap_or("worktree")
829        .to_string()
830        .into();
831
832    workspace.set_active_worktree_creation(Some(display_name), false, cx);
833
834    cx.spawn_in(window, async move |_workspace_entity, mut cx| {
835        let result = do_create_worktree(
836            git_repos,
837            non_git_paths,
838            worktree_name.clone(),
839            branch_target.clone(),
840            fetch_askpass_delegates,
841            remote_branch_fetch_mode,
842            previous_state,
843            workspace_handle.clone(),
844            window_handle,
845            remote_connection_options,
846            activate,
847            &mut cx,
848        )
849        .await;
850
851        if let Err(err) = &result {
852            log::error!("Failed to create worktree: {err}");
853            workspace_handle
854                .update(cx, |workspace, cx| {
855                    workspace.set_active_worktree_creation(None, false, cx);
856                    if let Some(fetch_error) = err.downcast_ref::<WorktreeFetchError>() {
857                        let toast = cx.new(|cx| {
858                            WorktreeFetchFailedToast::new(
859                                workspace.weak_handle(),
860                                worktree_name,
861                                branch_target,
862                                fallback_focused_dock,
863                                fetch_error,
864                                cx,
865                            )
866                        });
867                        workspace.toggle_status_toast(toast, cx);
868                    } else {
869                        show_error_toast(cx.entity(), "worktree create", anyhow!("{err:#}"), cx);
870                    }
871                })
872                .ok();
873        }
874
875        result
876    })
877}
878
879pub fn handle_switch_worktree(
880    workspace: &mut Workspace,
881    action: &zed_actions::SwitchWorktree,
882    window: &mut gpui::Window,
883    fallback_focused_dock: Option<DockPosition>,
884    cx: &mut gpui::Context<Workspace>,
885) {
886    let project = workspace.project().clone();
887
888    if project.read(cx).repositories(cx).is_empty() {
889        log::error!("switch_to_worktree: no git repository in the project");
890        return;
891    }
892    if project.read(cx).is_via_collab() {
893        log::error!("switch_to_worktree: not supported in collab projects");
894        return;
895    }
896
897    // Guard against concurrent creation
898    if workspace.active_worktree_creation().label.is_some() {
899        return;
900    }
901
902    let previous_state =
903        workspace.capture_state_for_worktree_switch(window, fallback_focused_dock, cx);
904    let workspace_handle = workspace.weak_handle();
905    let window_handle = window.window_handle().downcast::<MultiWorkspace>();
906    let remote_connection_options = project.read(cx).remote_connection_options(cx);
907
908    let (git_repos, non_git_paths) = classify_worktrees(project.read(cx), cx);
909
910    let git_repo_work_dirs: Vec<PathBuf> = git_repos
911        .iter()
912        .map(|repo| repo.read(cx).work_directory_abs_path.to_path_buf())
913        .collect();
914
915    let display_name: SharedString = action.display_name.clone().into();
916
917    workspace.set_active_worktree_creation(Some(display_name), true, cx);
918
919    let worktree_path = action.path.clone();
920
921    cx.spawn_in(window, async move |_workspace_entity, mut cx| {
922        let result = do_switch_worktree(
923            worktree_path,
924            git_repo_work_dirs,
925            non_git_paths,
926            previous_state,
927            workspace_handle.clone(),
928            window_handle,
929            remote_connection_options,
930            &mut cx,
931        )
932        .await;
933
934        if let Err(err) = &result {
935            log::error!("Failed to switch worktree: {err}");
936            workspace_handle
937                .update(cx, |workspace, cx| {
938                    workspace.set_active_worktree_creation(None, false, cx);
939                    show_error_toast(cx.entity(), "worktree switch", anyhow!("{err:#}"), cx);
940                })
941                .ok();
942        }
943
944        result
945    })
946    .detach_and_log_err(cx);
947}
948
949async fn do_create_worktree(
950    git_repos: Vec<Entity<Repository>>,
951    non_git_paths: Vec<PathBuf>,
952    worktree_name: Option<String>,
953    branch_target: NewWorktreeBranchTarget,
954    fetch_askpass_delegates: Vec<AskPassDelegate>,
955    remote_branch_fetch_mode: RemoteBranchFetchMode,
956    previous_state: PreviousWorkspaceState,
957    workspace: WeakEntity<Workspace>,
958    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
959    remote_connection_options: Option<RemoteConnectionOptions>,
960    activate: bool,
961    cx: &mut AsyncWindowContext,
962) -> anyhow::Result<CreatedWorktreeWorkspace> {
963    // List existing worktrees from all repos to detect name collisions
964    let worktree_receivers: Vec<_> = cx.update(|_, cx| {
965        git_repos
966            .iter()
967            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
968            .collect()
969    })?;
970    let worktree_directory_setting = cx.update(|_, cx| {
971        ProjectSettings::get_global(cx)
972            .git
973            .worktree_directory
974            .clone()
975    })?;
976
977    let mut existing_worktree_names = Vec::new();
978    let mut existing_worktree_paths = HashSet::default();
979    for result in futures::future::join_all(worktree_receivers).await {
980        match result {
981            Ok(Ok(worktrees)) => {
982                for worktree in worktrees {
983                    if let Some(name) = worktree
984                        .path
985                        .parent()
986                        .and_then(|p| p.file_name())
987                        .and_then(|n| n.to_str())
988                    {
989                        existing_worktree_names.push(name.to_string());
990                    }
991                    existing_worktree_paths.insert(worktree.path.clone());
992                }
993            }
994            Ok(Err(err)) => {
995                Err::<(), _>(err).log_err();
996            }
997            Err(_) => {}
998        }
999    }
1000
1001    if remote_branch_fetch_mode.should_fetch()
1002        && let Some((remote_name, branch_name)) = remote_branch_to_fetch(&branch_target)
1003    {
1004        let remote_name = remote_name.to_string();
1005        let branch_name = branch_name.to_string();
1006        if let Err(error) = fetch_remote_for_worktree_base(
1007            &git_repos,
1008            remote_name.clone(),
1009            fetch_askpass_delegates,
1010            cx,
1011        )
1012        .await
1013        {
1014            return Err(WorktreeFetchError {
1015                remote_name,
1016                branch_name,
1017                source: error,
1018            }
1019            .into());
1020        }
1021    }
1022
1023    let mut rng = rand::rng();
1024
1025    let base_ref = resolve_worktree_branch_target(&branch_target);
1026
1027    let (creation_infos, path_remapping) = cx.update(|_, cx| {
1028        start_worktree_creations(
1029            &git_repos,
1030            worktree_name,
1031            &existing_worktree_names,
1032            &existing_worktree_paths,
1033            base_ref,
1034            &worktree_directory_setting,
1035            &mut rng,
1036            cx,
1037        )
1038    })??;
1039
1040    let fs = cx.update(|_, cx| <dyn Fs>::global(cx))?;
1041
1042    let creation_pairs: Vec<(Entity<Repository>, PathBuf)> = creation_infos
1043        .iter()
1044        .map(|(repo, path, _)| (repo.clone(), path.clone()))
1045        .collect();
1046
1047    let created_paths = await_and_rollback_on_failure(creation_infos, fs, cx).await?;
1048
1049    // Record each created worktree so thread archival can later verify that
1050    // Zed created it before deleting it from disk. Failures are non-fatal:
1051    // the worktree just won't be eligible for automatic archival.
1052    for (repo, path) in creation_pairs {
1053        crate::created_worktrees::record_created_worktree_for_repo(
1054            &repo,
1055            &path,
1056            remote_connection_options.as_ref(),
1057            cx,
1058        )
1059        .await;
1060    }
1061
1062    // `path_remapping` has one entry per source git repo, while `created_paths`
1063    // has one per *unique* target worktree. When the former is larger, two or
1064    // more source repos were linked worktrees of the same underlying
1065    // repository and `start_worktree_creations` consolidated them.
1066    let consolidated_worktrees = path_remapping.len() > created_paths.len();
1067
1068    let mut all_paths = created_paths;
1069    let has_non_git = !non_git_paths.is_empty();
1070    all_paths.extend(non_git_paths.iter().cloned());
1071
1072    let workspace = open_worktree_workspace(
1073        all_paths,
1074        path_remapping,
1075        non_git_paths,
1076        has_non_git,
1077        previous_state,
1078        workspace,
1079        window_handle,
1080        remote_connection_options,
1081        WorktreeOperation::Create,
1082        activate,
1083        cx,
1084    )
1085    .await?;
1086
1087    Ok(CreatedWorktreeWorkspace {
1088        workspace,
1089        consolidated_worktrees,
1090    })
1091}
1092
1093async fn do_switch_worktree(
1094    worktree_path: PathBuf,
1095    git_repo_work_dirs: Vec<PathBuf>,
1096    non_git_paths: Vec<PathBuf>,
1097    previous_state: PreviousWorkspaceState,
1098    workspace: WeakEntity<Workspace>,
1099    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
1100    remote_connection_options: Option<RemoteConnectionOptions>,
1101    cx: &mut AsyncWindowContext,
1102) -> anyhow::Result<Entity<Workspace>> {
1103    let path_remapping: Vec<(PathBuf, PathBuf)> = git_repo_work_dirs
1104        .iter()
1105        .map(|work_dir| (work_dir.clone(), worktree_path.clone()))
1106        .collect();
1107
1108    let mut all_paths = vec![worktree_path];
1109    let has_non_git = !non_git_paths.is_empty();
1110    all_paths.extend(non_git_paths.iter().cloned());
1111
1112    open_worktree_workspace(
1113        all_paths,
1114        path_remapping,
1115        non_git_paths,
1116        has_non_git,
1117        previous_state,
1118        workspace,
1119        window_handle,
1120        remote_connection_options,
1121        WorktreeOperation::Switch,
1122        // Switching is always an explicit, foreground user action.
1123        true,
1124        cx,
1125    )
1126    .await
1127}
1128
1129/// Core workspace opening logic shared by both create and switch flows.
1130/// Returns the newly opened workspace entity so callers can do post-open
1131/// work (e.g., the `create_thread` agent tool spawns a thread inside it).
1132async fn open_worktree_workspace(
1133    all_paths: Vec<PathBuf>,
1134    path_remapping: Vec<(PathBuf, PathBuf)>,
1135    non_git_paths: Vec<PathBuf>,
1136    has_non_git: bool,
1137    previous_state: PreviousWorkspaceState,
1138    workspace: WeakEntity<Workspace>,
1139    window_handle: Option<gpui::WindowHandle<MultiWorkspace>>,
1140    remote_connection_options: Option<RemoteConnectionOptions>,
1141    operation: WorktreeOperation,
1142    activate: bool,
1143    cx: &mut AsyncWindowContext,
1144) -> anyhow::Result<Entity<Workspace>> {
1145    let window_handle = window_handle
1146        .ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
1147
1148    let focused_dock = previous_state.focused_dock;
1149
1150    let is_creating_new_worktree = matches!(operation, WorktreeOperation::Create);
1151
1152    // When `activate` is false the new workspace is opened in the background
1153    // (e.g. the agent's `create_thread` tool), so it should be a clean
1154    // checkout rather than inheriting the source workspace's open files and
1155    // dock layout. The state transfer only applies when we're foregrounding
1156    // a freshly-created worktree for the user.
1157    let transfer_state = is_creating_new_worktree && activate;
1158
1159    let source_for_transfer = if transfer_state {
1160        Some(workspace.clone())
1161    } else {
1162        None
1163    };
1164
1165    let (workspace_task, modal_workspace) =
1166        window_handle.update(cx, |multi_workspace, window, cx| {
1167            let path_list = util::path_list::PathList::new(&all_paths);
1168            let active_workspace = multi_workspace.workspace().clone();
1169            let modal_workspace = active_workspace.clone();
1170
1171            let init: Option<
1172                Box<
1173                    dyn FnOnce(&mut Workspace, &mut gpui::Window, &mut gpui::Context<Workspace>)
1174                        + Send,
1175                >,
1176            > = if transfer_state {
1177                let dock_structure = previous_state.dock_structure;
1178                Some(Box::new(
1179                    move |workspace: &mut Workspace,
1180                          window: &mut gpui::Window,
1181                          cx: &mut gpui::Context<Workspace>| {
1182                        workspace.set_dock_structure(dock_structure, window, cx);
1183                    },
1184                ))
1185            } else {
1186                None
1187            };
1188
1189            let task = multi_workspace.find_or_create_workspace_with_source_workspace(
1190                path_list,
1191                remote_connection_options,
1192                None,
1193                move |connection_options, window, cx| {
1194                    remote_connection::connect_with_modal(
1195                        &active_workspace,
1196                        connection_options,
1197                        window,
1198                        cx,
1199                    )
1200                },
1201                &[],
1202                init,
1203                OpenMode::Add,
1204                source_for_transfer.clone(),
1205                window,
1206                cx,
1207            );
1208            (task, modal_workspace)
1209        })?;
1210
1211    let result = workspace_task.await;
1212    remote_connection::dismiss_connection_modal(&modal_workspace, cx);
1213    let new_workspace = result?;
1214
1215    let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
1216
1217    if let Some(task) = panels_task {
1218        task.await.log_err();
1219    }
1220
1221    new_workspace
1222        .update(cx, |workspace, cx| {
1223            workspace.project().read(cx).wait_for_initial_scan(cx)
1224        })
1225        .await;
1226
1227    new_workspace
1228        .update(cx, |workspace, cx| {
1229            let repos = workspace
1230                .project()
1231                .read(cx)
1232                .repositories(cx)
1233                .values()
1234                .cloned()
1235                .collect::<Vec<_>>();
1236
1237            let tasks = repos
1238                .into_iter()
1239                .map(|repo| repo.update(cx, |repo, _| repo.barrier()));
1240            futures::future::join_all(tasks)
1241        })
1242        .await;
1243
1244    maybe_propagate_worktree_trust(&workspace, &new_workspace, &all_paths, cx);
1245
1246    if transfer_state {
1247        window_handle.update(cx, |_multi_workspace, window, cx| {
1248            new_workspace.update(cx, |workspace, cx| {
1249                if has_non_git {
1250                    struct WorktreeCreationToast;
1251                    let toast_id =
1252                        workspace::notifications::NotificationId::unique::<WorktreeCreationToast>();
1253                    workspace.show_toast(
1254                        workspace::Toast::new(
1255                            toast_id,
1256                            "Some project folders are not git repositories. \
1257                             They were included as-is without creating a worktree.",
1258                        ),
1259                        cx,
1260                    );
1261                }
1262
1263                // Remap every previously-open file path into the new worktree.
1264                let remap_path = |original_path: PathBuf| -> Option<PathBuf> {
1265                    let best_match = path_remapping
1266                        .iter()
1267                        .filter_map(|(old_root, new_root)| {
1268                            original_path.strip_prefix(old_root).ok().map(|relative| {
1269                                (old_root.components().count(), new_root.join(relative))
1270                            })
1271                        })
1272                        .max_by_key(|(depth, _)| *depth);
1273
1274                    if let Some((_, remapped_path)) = best_match {
1275                        return Some(remapped_path);
1276                    }
1277
1278                    for non_git in &non_git_paths {
1279                        if original_path.starts_with(non_git) {
1280                            return Some(original_path);
1281                        }
1282                    }
1283                    None
1284                };
1285
1286                let remapped_active_path =
1287                    previous_state.active_file_path.and_then(|p| remap_path(p));
1288
1289                let mut paths_to_open: Vec<PathBuf> = Vec::new();
1290                let mut seen = HashSet::default();
1291                for path in previous_state.open_file_paths {
1292                    if let Some(remapped) = remap_path(path) {
1293                        if remapped_active_path.as_ref() != Some(&remapped)
1294                            && seen.insert(remapped.clone())
1295                        {
1296                            paths_to_open.push(remapped);
1297                        }
1298                    }
1299                }
1300
1301                if let Some(active) = &remapped_active_path {
1302                    if seen.insert(active.clone()) {
1303                        paths_to_open.push(active.clone());
1304                    }
1305                }
1306
1307                if !paths_to_open.is_empty() {
1308                    let should_focus_center = focused_dock.is_none();
1309                    let open_task = workspace.open_paths(
1310                        paths_to_open,
1311                        workspace::OpenOptions {
1312                            focus: Some(false),
1313                            ..Default::default()
1314                        },
1315                        None,
1316                        window,
1317                        cx,
1318                    );
1319                    cx.spawn_in(window, async move |workspace, cx| {
1320                        for item in open_task.await.into_iter().flatten() {
1321                            item.log_err();
1322                        }
1323                        if should_focus_center {
1324                            workspace.update_in(cx, |workspace, window, cx| {
1325                                workspace.focus_center_pane(window, cx);
1326                            })?;
1327                        }
1328                        anyhow::Ok(())
1329                    })
1330                    .detach_and_log_err(cx);
1331                }
1332            });
1333        })?;
1334    }
1335
1336    // Clear the creation status on the SOURCE workspace so its title bar
1337    // stops showing the loading indicator immediately.
1338    workspace
1339        .update(cx, |ws, cx| {
1340            ws.set_active_worktree_creation(None, false, cx);
1341        })
1342        .ok();
1343
1344    window_handle.update(cx, |multi_workspace, window, cx| {
1345        if activate {
1346            multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
1347        } else {
1348            // Background open: register the new workspace as a retained tab
1349            // but leave the user where they are.
1350            multi_workspace.add_background_workspace(new_workspace.clone(), window, cx);
1351        }
1352
1353        if is_creating_new_worktree {
1354            new_workspace.update(cx, |workspace, cx| {
1355                // Run create-worktree setup hooks regardless of foreground vs
1356                // background — the worktree was created either way.
1357                workspace.run_create_worktree_tasks(window, cx);
1358
1359                if activate && let Some(dock_position) = focused_dock {
1360                    let dock = workspace.dock_at_position(dock_position);
1361                    if let Some(panel) = dock.read(cx).active_panel() {
1362                        panel.panel_focus_handle(cx).focus(window, cx);
1363                    }
1364                }
1365            });
1366        }
1367    })?;
1368
1369    Ok(new_workspace)
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374    use super::*;
1375    use fs::Fs;
1376    use gpui::{App, Task, TestAppContext};
1377    use language::language_settings::AllLanguageSettings;
1378    use project::project_settings::ProjectSettings;
1379    use project::task_store::{TaskSettingsLocation, TaskStore};
1380    use project::{FakeFs, WorktreeSettings};
1381    use serde_json::json;
1382    use settings::{SettingsLocation, SettingsStore};
1383    use std::path::{Path, PathBuf};
1384    use std::process::ExitStatus;
1385    use std::sync::Mutex;
1386    use task::SpawnInTerminal;
1387    use theme::LoadThemes;
1388    use util::path;
1389    use util::rel_path::rel_path;
1390    use workspace::{TerminalProvider, WorkspaceSettings};
1391
1392    struct CountingTerminalProvider {
1393        spawned_task_labels: Arc<Mutex<Vec<String>>>,
1394    }
1395
1396    impl TerminalProvider for CountingTerminalProvider {
1397        fn spawn(
1398            &self,
1399            task: SpawnInTerminal,
1400            _window: &mut ui::Window,
1401            _cx: &mut App,
1402        ) -> Task<Option<anyhow::Result<ExitStatus>>> {
1403            self.spawned_task_labels
1404                .lock()
1405                .expect("terminal spawn mutex should not be poisoned")
1406                .push(task.label);
1407            Task::ready(Some(Ok(ExitStatus::default())))
1408        }
1409    }
1410
1411    fn init_test(cx: &mut TestAppContext) {
1412        zlog::init_test();
1413        cx.update(|cx| {
1414            let settings_store = SettingsStore::test(cx);
1415            cx.set_global(settings_store);
1416            theme_settings::init(LoadThemes::JustBase, cx);
1417            AllLanguageSettings::register(cx);
1418            editor::init(cx);
1419            ProjectSettings::register(cx);
1420            WorktreeSettings::register(cx);
1421            WorkspaceSettings::register(cx);
1422            TaskStore::init(None);
1423        });
1424    }
1425
1426    fn install_counting_provider_and_worktree_hook(
1427        workspace: &Entity<Workspace>,
1428        spawned_task_labels: &Arc<Mutex<Vec<String>>>,
1429        main_project_root: &Path,
1430        hook_tasks_json: &str,
1431        cx: &mut App,
1432    ) {
1433        workspace.update(cx, |workspace, cx| {
1434            workspace.set_terminal_provider(CountingTerminalProvider {
1435                spawned_task_labels: spawned_task_labels.clone(),
1436            });
1437
1438            let project = workspace.project().clone();
1439            let Some(worktree) = project.read(cx).worktrees(cx).next() else {
1440                return;
1441            };
1442            let worktree = worktree.read(cx);
1443            let worktree_id = worktree.id();
1444            let worktree_root = worktree.abs_path().to_path_buf();
1445            if worktree_root == main_project_root {
1446                return;
1447            }
1448
1449            let Some(task_inventory) = project
1450                .read(cx)
1451                .task_store()
1452                .read(cx)
1453                .task_inventory()
1454                .cloned()
1455            else {
1456                return;
1457            };
1458            task_inventory.update(cx, |inventory, _| {
1459                inventory
1460                    .update_file_based_tasks(
1461                        TaskSettingsLocation::Worktree(SettingsLocation {
1462                            worktree_id,
1463                            path: rel_path(".zed"),
1464                        }),
1465                        Some(hook_tasks_json),
1466                    )
1467                    .expect("should inject create_worktree hook tasks for linked worktree");
1468            });
1469        });
1470    }
1471
1472    #[gpui::test]
1473    async fn test_create_worktree_hook_does_not_run_when_switching_back_to_main_worktree(
1474        cx: &mut TestAppContext,
1475    ) {
1476        init_test(cx);
1477
1478        let hook_tasks_json = r#"[{"label":"setup worktree","command":"echo","hide":"never","hooks":["create_worktree"]}]"#;
1479        let fs = FakeFs::new(cx.background_executor.clone());
1480        cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
1481        fs.insert_tree(
1482            "/root",
1483            json!({
1484                "project": {
1485                    ".git": {},
1486                    ".zed": {
1487                        "tasks.json": hook_tasks_json,
1488                    },
1489                    "src": {
1490                        "main.rs": "fn main() {}",
1491                    },
1492                },
1493            }),
1494        )
1495        .await;
1496
1497        let main_project_root = PathBuf::from(path!("/root/project"));
1498        let project = Project::test(fs.clone(), [main_project_root.as_path()], cx).await;
1499        project
1500            .update(cx, |project, cx| project.git_scans_complete(cx))
1501            .await;
1502
1503        let (multi_workspace, cx) =
1504            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1505
1506        let spawned_task_labels = Arc::new(Mutex::new(Vec::new()));
1507        multi_workspace.update(cx, |multi_workspace, cx| {
1508            multi_workspace.retain_active_workspace(cx);
1509            let active_workspace = multi_workspace.workspace().clone();
1510            install_counting_provider_and_worktree_hook(
1511                &active_workspace,
1512                &spawned_task_labels,
1513                &main_project_root,
1514                hook_tasks_json,
1515                cx,
1516            );
1517        });
1518
1519        let main_workspace =
1520            multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
1521        main_workspace.update_in(cx, |workspace, window, cx| {
1522            handle_create_worktree(
1523                workspace,
1524                &zed_actions::CreateWorktree {
1525                    worktree_name: Some("feature".to_string()),
1526                    branch_target: NewWorktreeBranchTarget::CurrentBranch,
1527                },
1528                window,
1529                None,
1530                cx,
1531            );
1532        });
1533        cx.run_until_parked();
1534
1535        let active_workspace =
1536            multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
1537        cx.update(|_, cx| {
1538            install_counting_provider_and_worktree_hook(
1539                &active_workspace,
1540                &spawned_task_labels,
1541                &main_project_root,
1542                hook_tasks_json,
1543                cx,
1544            );
1545        });
1546        active_workspace.update_in(cx, |workspace, window, cx| {
1547            workspace.run_create_worktree_tasks(window, cx);
1548        });
1549        cx.run_until_parked();
1550
1551        assert_eq!(
1552            spawned_task_labels
1553                .lock()
1554                .expect("terminal spawn mutex should not be poisoned")
1555                .as_slice(),
1556            ["setup worktree"],
1557            "create_worktree hook should run once for the created linked worktree"
1558        );
1559
1560        active_workspace.update_in(cx, |workspace, window, cx| {
1561            handle_switch_worktree(
1562                workspace,
1563                &zed_actions::SwitchWorktree {
1564                    path: main_project_root.clone(),
1565                    display_name: "project".to_string(),
1566                },
1567                window,
1568                None,
1569                cx,
1570            );
1571        });
1572        cx.run_until_parked();
1573
1574        assert_eq!(
1575            spawned_task_labels
1576                .lock()
1577                .expect("terminal spawn mutex should not be poisoned")
1578                .as_slice(),
1579            ["setup worktree"],
1580            "switching back to the main worktree should not rerun create_worktree hooks"
1581        );
1582    }
1583
1584    #[gpui::test]
1585    async fn test_linked_worktree_inherits_trust_from_main_worktree(cx: &mut TestAppContext) {
1586        init_test(cx);
1587        cx.update(|cx| {
1588            project::trusted_worktrees::init(collections::HashMap::default(), cx);
1589        });
1590
1591        let fs = FakeFs::new(cx.background_executor.clone());
1592        cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
1593        fs.insert_tree(
1594            "/root",
1595            json!({
1596                "project": {
1597                    ".git": {},
1598                    "src": {
1599                        "main.rs": "fn main() {}",
1600                    },
1601                },
1602            }),
1603        )
1604        .await;
1605
1606        let main_project_root = PathBuf::from(path!("/root/project"));
1607        let project =
1608            Project::test_with_worktree_trust(fs.clone(), [main_project_root.as_path()], cx).await;
1609        project
1610            .update(cx, |project, cx| project.git_scans_complete(cx))
1611            .await;
1612
1613        // The main worktree starts restricted; trust it explicitly
1614        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
1615        let main_worktree_id = worktree_store.read_with(cx, |store, cx| {
1616            store
1617                .worktrees()
1618                .next()
1619                .map(|wt| wt.read(cx).id())
1620                .expect("should have a worktree")
1621        });
1622        let trusted_store = cx
1623            .read(|cx| project::trusted_worktrees::TrustedWorktrees::try_get_global(cx))
1624            .expect("trust store should exist");
1625        trusted_store.update(cx, |store, cx| {
1626            store.trust(
1627                &worktree_store,
1628                collections::HashSet::from_iter([project::trusted_worktrees::PathTrust::Worktree(
1629                    main_worktree_id,
1630                )]),
1631                cx,
1632            );
1633        });
1634
1635        // Verify main worktree is now trusted
1636        let has_restricted = cx.read(|cx| {
1637            project::trusted_worktrees::TrustedWorktrees::has_restricted_worktrees(
1638                &worktree_store,
1639                cx,
1640            )
1641        });
1642        assert!(
1643            !has_restricted,
1644            "main worktree should be trusted after explicit trust"
1645        );
1646
1647        let (multi_workspace, cx) =
1648            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1649        multi_workspace.update(cx, |multi_workspace, cx| {
1650            multi_workspace.retain_active_workspace(cx);
1651        });
1652
1653        // Create a linked worktree from the trusted main worktree
1654        let main_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1655        main_workspace.update_in(cx, |workspace, window, cx| {
1656            handle_create_worktree(
1657                workspace,
1658                &zed_actions::CreateWorktree {
1659                    worktree_name: Some("feature".to_string()),
1660                    branch_target: NewWorktreeBranchTarget::CurrentBranch,
1661                },
1662                window,
1663                None,
1664                cx,
1665            );
1666        });
1667        cx.run_until_parked();
1668
1669        // The new workspace (linked worktree) should inherit trust
1670        let new_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1671        let new_worktree_store =
1672            new_workspace.read_with(cx, |ws, cx| ws.project().read(cx).worktree_store());
1673        let new_has_restricted = cx.read(|cx| {
1674            project::trusted_worktrees::TrustedWorktrees::has_restricted_worktrees(
1675                &new_worktree_store,
1676                cx,
1677            )
1678        });
1679        assert!(
1680            !new_has_restricted,
1681            "linked worktree should inherit trust from the main worktree"
1682        );
1683
1684        // The security modal it used to assert against no longer exists:
1685        // OMEGA-DELTA-0009 deleted the Restricted Mode UI outright. The
1686        // meaningful half of this test is the trust inheritance asserted
1687        // above, which still holds.
1688    }
1689
1690    #[test]
1691    fn test_remote_branch_name_parse() {
1692        assert_eq!(
1693            RemoteBranchName::parse("refs/remotes/origin/main"),
1694            Some(RemoteBranchName {
1695                remote_name: "origin".to_string(),
1696                branch_name: "main".to_string(),
1697            })
1698        );
1699        assert_eq!(
1700            RemoteBranchName::parse("upstream/feature/foo"),
1701            Some(RemoteBranchName {
1702                remote_name: "upstream".to_string(),
1703                branch_name: "feature/foo".to_string(),
1704            })
1705        );
1706        assert_eq!(RemoteBranchName::parse("main"), None);
1707        assert_eq!(RemoteBranchName::parse("origin/"), None);
1708    }
1709
1710    #[test]
1711    fn test_worktree_create_targets() {
1712        let origin_main = RemoteBranchName {
1713            remote_name: "origin".to_string(),
1714            branch_name: "main".to_string(),
1715        };
1716
1717        // Multiple repositories: only the current branch, regardless of default.
1718        assert_eq!(
1719            worktree_create_targets(true, Some(origin_main.clone()), Some("feature")),
1720            vec![WorktreeCreateTarget::CurrentBranch]
1721        );
1722
1723        // Default branch differs from current: offer both, default first.
1724        assert_eq!(
1725            worktree_create_targets(false, Some(origin_main.clone()), Some("feature")),
1726            vec![
1727                WorktreeCreateTarget::DefaultBranch(origin_main.clone()),
1728                WorktreeCreateTarget::CurrentBranch,
1729            ]
1730        );
1731
1732        // Current branch matches the default: only the default branch entry.
1733        assert_eq!(
1734            worktree_create_targets(false, Some(origin_main.clone()), Some("main")),
1735            vec![WorktreeCreateTarget::DefaultBranch(origin_main)]
1736        );
1737
1738        // No default branch resolved: fall back to the current branch.
1739        assert_eq!(
1740            worktree_create_targets(false, None, Some("feature")),
1741            vec![WorktreeCreateTarget::CurrentBranch]
1742        );
1743    }
1744
1745    #[test]
1746    fn test_worktree_create_target_branch_label() {
1747        let origin_main = RemoteBranchName {
1748            remote_name: "origin".to_string(),
1749            branch_name: "main".to_string(),
1750        };
1751        assert_eq!(
1752            WorktreeCreateTarget::DefaultBranch(origin_main).branch_label(false, Some("feature")),
1753            "origin/main"
1754        );
1755        assert_eq!(
1756            WorktreeCreateTarget::CurrentBranch.branch_label(false, Some("feature")),
1757            "feature"
1758        );
1759        // Detached HEAD falls back to "HEAD".
1760        assert_eq!(
1761            WorktreeCreateTarget::CurrentBranch.branch_label(false, None),
1762            "HEAD"
1763        );
1764        // Multiple repositories pluralize the current branch.
1765        assert_eq!(
1766            WorktreeCreateTarget::CurrentBranch.branch_label(true, Some("feature")),
1767            "current branches"
1768        );
1769    }
1770}
1771
Served at tenant.openagents/omega Member data and write actions are omitted.