1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use agent_client_protocol as acp;
7use anyhow::{Context as _, Result, anyhow};
8use gpui::{App, AsyncApp, Entity, Task};
9use project::{
10 LocalProjectFlags, Project, WorktreeId,
11 git_store::{Repository, resolve_git_worktree_to_main_repo},
12};
13use util::ResultExt;
14use workspace::{AppState, MultiWorkspace, Workspace};
15
16use crate::thread_metadata_store::{ArchivedGitWorktree, ThreadMetadataStore};
17
18/// The plan for archiving a single git worktree root.
19///
20/// A thread can have multiple folder paths open, so there may be multiple
21/// `RootPlan`s per archival operation. Each one captures everything needed to
22/// persist the worktree's git state and then remove it from disk.
23///
24/// All fields are gathered synchronously by [`build_root_plan`] while the
25/// worktree is still loaded in open projects. This is important because
26/// workspace removal tears down project and repository entities, making
27/// them unavailable for the later async persist/remove steps.
28#[derive(Clone)]
29pub struct RootPlan {
30 /// Absolute path of the git worktree on disk.
31 pub root_path: PathBuf,
32 /// Absolute path to the main git repository this worktree is linked to.
33 /// Used both for creating a git ref to prevent GC of WIP commits during
34 /// [`persist_worktree_state`], and for `git worktree remove` during
35 /// [`remove_root`].
36 pub main_repo_path: PathBuf,
37 /// Every open `Project` that has this worktree loaded, so they can all
38 /// call `remove_worktree` and release it during [`remove_root`].
39 /// Multiple projects can reference the same path when the user has the
40 /// worktree open in more than one workspace.
41 pub affected_projects: Vec<AffectedProject>,
42 /// The `Repository` entity for this worktree, used to run git commands
43 /// (create WIP commits, stage files, reset) during
44 /// [`persist_worktree_state`]. `None` when the `GitStore` hasn't created
45 /// a `Repository` for this worktree yet — in that case,
46 /// `persist_worktree_state` falls back to creating a temporary headless
47 /// project to obtain one.
48 pub worktree_repo: Option<Entity<Repository>>,
49 /// The branch the worktree was on, so it can be restored later.
50 /// `None` if the worktree was in detached HEAD state or if no
51 /// `Repository` entity was available at planning time (in which case
52 /// `persist_worktree_state` reads it from the repo snapshot instead).
53 pub branch_name: Option<String>,
54}
55
56/// A `Project` that references a worktree being archived, paired with the
57/// `WorktreeId` it uses for that worktree.
58///
59/// The same worktree path can appear in multiple open workspaces/projects
60/// (e.g. when the user has two windows open that both include the same
61/// linked worktree). Each one needs to call `remove_worktree` and wait for
62/// the release during [`remove_root`], otherwise the project would still
63/// hold a reference to the directory and `git worktree remove` would fail.
64#[derive(Clone)]
65pub struct AffectedProject {
66 pub project: Entity<Project>,
67 pub worktree_id: WorktreeId,
68}
69
70fn archived_worktree_ref_name(id: i64) -> String {
71 format!("refs/archived-worktrees/{}", id)
72}
73
74/// Builds a [`RootPlan`] for archiving the git worktree at `path`.
75///
76/// This is a synchronous planning step that must run *before* any workspace
77/// removal, because it needs live project and repository entities that are
78/// torn down when a workspace is removed. It does three things:
79///
80/// 1. Finds every `Project` across all open workspaces that has this
81/// worktree loaded (`affected_projects`).
82/// 2. Looks for a `Repository` entity whose snapshot identifies this path
83/// as a linked worktree (`worktree_repo`), which is needed for the git
84/// operations in [`persist_worktree_state`].
85/// 3. Determines the `main_repo_path` — the parent repo that owns this
86/// linked worktree — needed for both git ref creation and
87/// `git worktree remove`.
88///
89/// When no `Repository` entity is available (e.g. the `GitStore` hasn't
90/// finished scanning), the function falls back to deriving `main_repo_path`
91/// from the worktree snapshot's `root_repo_common_dir`. In that case
92/// `worktree_repo` is `None` and [`persist_worktree_state`] will create a
93/// temporary headless project to obtain one.
94///
95/// Returns `None` if no open project has this path as a visible worktree.
96pub fn build_root_plan(
97 path: &Path,
98 workspaces: &[Entity<Workspace>],
99 cx: &App,
100) -> Option<RootPlan> {
101 let path = path.to_path_buf();
102
103 let affected_projects = workspaces
104 .iter()
105 .filter_map(|workspace| {
106 let project = workspace.read(cx).project().clone();
107 let worktree = project
108 .read(cx)
109 .visible_worktrees(cx)
110 .find(|worktree| worktree.read(cx).abs_path().as_ref() == path.as_path())?;
111 let worktree_id = worktree.read(cx).id();
112 Some(AffectedProject {
113 project,
114 worktree_id,
115 })
116 })
117 .collect::<Vec<_>>();
118
119 if affected_projects.is_empty() {
120 return None;
121 }
122
123 let linked_repo = workspaces
124 .iter()
125 .flat_map(|workspace| {
126 workspace
127 .read(cx)
128 .project()
129 .read(cx)
130 .repositories(cx)
131 .values()
132 .cloned()
133 .collect::<Vec<_>>()
134 })
135 .find_map(|repo| {
136 let snapshot = repo.read(cx).snapshot();
137 (snapshot.is_linked_worktree()
138 && snapshot.work_directory_abs_path.as_ref() == path.as_path())
139 .then_some((snapshot, repo))
140 });
141
142 let (main_repo_path, worktree_repo, branch_name) =
143 if let Some((linked_snapshot, repo)) = linked_repo {
144 (
145 linked_snapshot.original_repo_abs_path.to_path_buf(),
146 Some(repo),
147 linked_snapshot
148 .branch
149 .as_ref()
150 .map(|branch| branch.name().to_string()),
151 )
152 } else {
153 // Not a linked worktree — nothing to archive from disk.
154 // `remove_root` would try to remove the main worktree from
155 // the project and then run `git worktree remove`, both of
156 // which fail for main working trees.
157 return None;
158 };
159
160 Some(RootPlan {
161 root_path: path,
162 main_repo_path,
163 affected_projects,
164 worktree_repo,
165 branch_name,
166 })
167}
168
169/// Returns `true` if any unarchived thread other than `current_session_id`
170/// references `path` in its folder paths. Used to determine whether a
171/// worktree can safely be removed from disk.
172pub fn path_is_referenced_by_other_unarchived_threads(
173 current_session_id: &acp::SessionId,
174 path: &Path,
175 cx: &App,
176) -> bool {
177 ThreadMetadataStore::global(cx)
178 .read(cx)
179 .entries()
180 .filter(|thread| thread.session_id != *current_session_id)
181 .filter(|thread| !thread.archived)
182 .any(|thread| {
183 thread
184 .folder_paths()
185 .paths()
186 .iter()
187 .any(|other_path| other_path.as_path() == path)
188 })
189}
190
191/// Removes a worktree from all affected projects and deletes it from disk
192/// via `git worktree remove`.
193///
194/// This is the destructive counterpart to [`persist_worktree_state`]. It
195/// first detaches the worktree from every [`AffectedProject`], waits for
196/// each project to fully release it, then asks the main repository to
197/// delete the worktree directory. If the git removal fails, the worktree
198/// is re-added to each project via [`rollback_root`].
199pub async fn remove_root(root: RootPlan, cx: &mut AsyncApp) -> Result<()> {
200 let release_tasks: Vec<_> = root
201 .affected_projects
202 .iter()
203 .map(|affected| {
204 let project = affected.project.clone();
205 let worktree_id = affected.worktree_id;
206 project.update(cx, |project, cx| {
207 let wait = project.wait_for_worktree_release(worktree_id, cx);
208 project.remove_worktree(worktree_id, cx);
209 wait
210 })
211 })
212 .collect();
213
214 if let Err(error) = remove_root_after_worktree_removal(&root, release_tasks, cx).await {
215 rollback_root(&root, cx).await;
216 return Err(error);
217 }
218
219 Ok(())
220}
221
222async fn remove_root_after_worktree_removal(
223 root: &RootPlan,
224 release_tasks: Vec<Task<Result<()>>>,
225 cx: &mut AsyncApp,
226) -> Result<()> {
227 for task in release_tasks {
228 if let Err(error) = task.await {
229 log::error!("Failed waiting for worktree release: {error:#}");
230 }
231 }
232
233 let (repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx).await?;
234 // force=true is required because the working directory is still dirty
235 // — persist_worktree_state captures state into detached commits without
236 // modifying the real index or working tree, so git refuses to delete
237 // the worktree without --force.
238 let receiver = repo.update(cx, |repo: &mut Repository, _cx| {
239 repo.remove_worktree(root.root_path.clone(), true)
240 });
241 let result = receiver
242 .await
243 .map_err(|_| anyhow!("git worktree removal was canceled"))?;
244 // Keep _temp_project alive until after the await so the headless project isn't dropped mid-operation
245 drop(_temp_project);
246 result
247}
248
249/// Finds a live `Repository` entity for the given path, or creates a temporary
250/// `Project::local` to obtain one.
251///
252/// `Repository` entities can only be obtained through a `Project` because
253/// `GitStore` (which creates and manages `Repository` entities) is owned by
254/// `Project`. When no open workspace contains the repo we need, we spin up a
255/// headless `Project::local` just to get a `Repository` handle. The caller
256/// keeps the returned `Option<Entity<Project>>` alive for the duration of the
257/// git operations, then drops it.
258///
259/// Future improvement: decoupling `GitStore` from `Project` so that
260/// `Repository` entities can be created standalone would eliminate this
261/// temporary-project workaround.
262async fn find_or_create_repository(
263 repo_path: &Path,
264 cx: &mut AsyncApp,
265) -> Result<(Entity<Repository>, Option<Entity<Project>>)> {
266 let repo_path_owned = repo_path.to_path_buf();
267 let live_repo = cx.update(|cx| {
268 all_open_workspaces(cx)
269 .into_iter()
270 .flat_map(|workspace| {
271 workspace
272 .read(cx)
273 .project()
274 .read(cx)
275 .repositories(cx)
276 .values()
277 .cloned()
278 .collect::<Vec<_>>()
279 })
280 .find(|repo| {
281 repo.read(cx).snapshot().work_directory_abs_path.as_ref()
282 == repo_path_owned.as_path()
283 })
284 });
285
286 if let Some(repo) = live_repo {
287 return Ok((repo, None));
288 }
289
290 let app_state =
291 current_app_state(cx).context("no app state available for temporary project")?;
292 let temp_project = cx.update(|cx| {
293 Project::local(
294 app_state.client.clone(),
295 app_state.node_runtime.clone(),
296 app_state.user_store.clone(),
297 app_state.languages.clone(),
298 app_state.fs.clone(),
299 None,
300 LocalProjectFlags::default(),
301 cx,
302 )
303 });
304
305 let repo_path_for_worktree = repo_path.to_path_buf();
306 let create_worktree = temp_project.update(cx, |project, cx| {
307 project.create_worktree(repo_path_for_worktree, true, cx)
308 });
309 let _worktree = create_worktree.await?;
310 let initial_scan = temp_project.read_with(cx, |project, cx| project.wait_for_initial_scan(cx));
311 initial_scan.await;
312
313 let repo_path_for_find = repo_path.to_path_buf();
314 let repo = temp_project
315 .update(cx, |project, cx| {
316 project
317 .repositories(cx)
318 .values()
319 .find(|repo| {
320 repo.read(cx).snapshot().work_directory_abs_path.as_ref()
321 == repo_path_for_find.as_path()
322 })
323 .cloned()
324 })
325 .context("failed to resolve temporary repository handle")?;
326
327 let barrier = repo.update(cx, |repo: &mut Repository, _cx| repo.barrier());
328 barrier
329 .await
330 .map_err(|_| anyhow!("temporary repository barrier canceled"))?;
331 Ok((repo, Some(temp_project)))
332}
333
334/// Re-adds the worktree to every affected project after a failed
335/// [`remove_root`].
336async fn rollback_root(root: &RootPlan, cx: &mut AsyncApp) {
337 for affected in &root.affected_projects {
338 let task = affected.project.update(cx, |project, cx| {
339 project.create_worktree(root.root_path.clone(), true, cx)
340 });
341 task.await.log_err();
342 }
343}
344
345/// Saves the worktree's full git state so it can be restored later.
346///
347/// This creates two detached commits (via [`create_archive_checkpoint`] on
348/// the `GitRepository` trait) that capture the staged and unstaged state
349/// without moving any branch ref. The commits are:
350/// - "WIP staged": a tree matching the current index, parented on HEAD
351/// - "WIP unstaged": a tree with all files (including untracked),
352/// parented on the staged commit
353///
354/// After creating the commits, this function:
355/// 1. Records the commit SHAs, branch name, and paths in a DB record.
356/// 2. Links every thread referencing this worktree to that record.
357/// 3. Creates a git ref on the main repo to prevent GC of the commits.
358///
359/// On success, returns the archived worktree DB row ID for rollback.
360pub async fn persist_worktree_state(root: &RootPlan, cx: &mut AsyncApp) -> Result<i64> {
361 let (worktree_repo, _temp_worktree_project) = match &root.worktree_repo {
362 Some(worktree_repo) => (worktree_repo.clone(), None),
363 None => find_or_create_repository(&root.root_path, cx).await?,
364 };
365
366 let original_commit_hash = worktree_repo
367 .update(cx, |repo, _cx| repo.head_sha())
368 .await
369 .map_err(|_| anyhow!("head_sha canceled"))?
370 .context("failed to read original HEAD SHA")?
371 .context("HEAD SHA is None")?;
372
373 // Create two detached WIP commits without moving the branch.
374 let checkpoint_rx = worktree_repo.update(cx, |repo, _cx| repo.create_archive_checkpoint());
375 let (staged_commit_hash, unstaged_commit_hash) = checkpoint_rx
376 .await
377 .map_err(|_| anyhow!("create_archive_checkpoint canceled"))?
378 .context("failed to create archive checkpoint")?;
379
380 // Create DB record
381 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
382 let worktree_path_str = root.root_path.to_string_lossy().to_string();
383 let main_repo_path_str = root.main_repo_path.to_string_lossy().to_string();
384 let branch_name = root.branch_name.clone().or_else(|| {
385 worktree_repo.read_with(cx, |repo, _cx| {
386 repo.snapshot()
387 .branch
388 .as_ref()
389 .map(|branch| branch.name().to_string())
390 })
391 });
392
393 let db_result = store
394 .read_with(cx, |store, cx| {
395 store.create_archived_worktree(
396 worktree_path_str.clone(),
397 main_repo_path_str.clone(),
398 branch_name.clone(),
399 staged_commit_hash.clone(),
400 unstaged_commit_hash.clone(),
401 original_commit_hash.clone(),
402 cx,
403 )
404 })
405 .await
406 .context("failed to create archived worktree DB record");
407 let archived_worktree_id = match db_result {
408 Ok(id) => id,
409 Err(error) => {
410 return Err(error);
411 }
412 };
413
414 // Link all threads on this worktree to the archived record
415 let session_ids: Vec<acp::SessionId> = store.read_with(cx, |store, _cx| {
416 store
417 .entries()
418 .filter(|thread| {
419 thread
420 .folder_paths()
421 .paths()
422 .iter()
423 .any(|p| p.as_path() == root.root_path)
424 })
425 .map(|thread| thread.session_id.clone())
426 .collect()
427 });
428
429 for session_id in &session_ids {
430 let link_result = store
431 .read_with(cx, |store, cx| {
432 store.link_thread_to_archived_worktree(
433 session_id.0.to_string(),
434 archived_worktree_id,
435 cx,
436 )
437 })
438 .await;
439 if let Err(error) = link_result {
440 if let Err(delete_error) = store
441 .read_with(cx, |store, cx| {
442 store.delete_archived_worktree(archived_worktree_id, cx)
443 })
444 .await
445 {
446 log::error!(
447 "Failed to delete archived worktree DB record during link rollback: \
448 {delete_error:#}"
449 );
450 }
451 return Err(error.context("failed to link thread to archived worktree"));
452 }
453 }
454
455 // Create git ref on main repo to prevent GC of the detached commits.
456 // This is fatal: without the ref, git gc will eventually collect the
457 // WIP commits and a later restore will silently fail.
458 let ref_name = archived_worktree_ref_name(archived_worktree_id);
459 let (main_repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx)
460 .await
461 .context("could not open main repo to create archive ref")?;
462 let rx = main_repo.update(cx, |repo, _cx| {
463 repo.update_ref(ref_name.clone(), unstaged_commit_hash.clone())
464 });
465 rx.await
466 .map_err(|_| anyhow!("update_ref canceled"))
467 .and_then(|r| r)
468 .with_context(|| format!("failed to create ref {ref_name} on main repo"))?;
469 drop(_temp_project);
470
471 Ok(archived_worktree_id)
472}
473
474/// Undoes a successful [`persist_worktree_state`] by deleting the git ref
475/// on the main repo and removing the DB record. Since the WIP commits are
476/// detached (they don't move any branch), no git reset is needed — the
477/// commits will be garbage-collected once the ref is removed.
478pub async fn rollback_persist(archived_worktree_id: i64, root: &RootPlan, cx: &mut AsyncApp) {
479 // Delete the git ref on main repo
480 if let Ok((main_repo, _temp_project)) =
481 find_or_create_repository(&root.main_repo_path, cx).await
482 {
483 let ref_name = archived_worktree_ref_name(archived_worktree_id);
484 let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
485 rx.await.ok().and_then(|r| r.log_err());
486 drop(_temp_project);
487 }
488
489 // Delete the DB record
490 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
491 if let Err(error) = store
492 .read_with(cx, |store, cx| {
493 store.delete_archived_worktree(archived_worktree_id, cx)
494 })
495 .await
496 {
497 log::error!("Failed to delete archived worktree DB record during rollback: {error:#}");
498 }
499}
500
501/// Restores a previously archived worktree back to disk from its DB record.
502///
503/// Creates the git worktree at the original commit (the branch never moved
504/// during archival since WIP commits are detached), switches to the branch,
505/// then uses [`restore_archive_checkpoint`] to reconstruct the staged/
506/// unstaged state from the WIP commit trees.
507pub async fn restore_worktree_via_git(
508 row: &ArchivedGitWorktree,
509 cx: &mut AsyncApp,
510) -> Result<PathBuf> {
511 let (main_repo, _temp_project) = find_or_create_repository(&row.main_repo_path, cx).await?;
512
513 let worktree_path = &row.worktree_path;
514 let app_state = current_app_state(cx).context("no app state available")?;
515 let already_exists = app_state.fs.metadata(worktree_path).await?.is_some();
516
517 let created_new_worktree = if already_exists {
518 let is_git_worktree =
519 resolve_git_worktree_to_main_repo(app_state.fs.as_ref(), worktree_path)
520 .await
521 .is_some();
522
523 if !is_git_worktree {
524 let rx = main_repo.update(cx, |repo, _cx| repo.repair_worktrees());
525 rx.await
526 .map_err(|_| anyhow!("worktree repair was canceled"))?
527 .context("failed to repair worktrees")?;
528 }
529 false
530 } else {
531 // Create worktree at the original commit — the branch still points
532 // here because archival used detached commits.
533 let rx = main_repo.update(cx, |repo, _cx| {
534 repo.create_worktree_detached(worktree_path.clone(), row.original_commit_hash.clone())
535 });
536 rx.await
537 .map_err(|_| anyhow!("worktree creation was canceled"))?
538 .context("failed to create worktree")?;
539 true
540 };
541
542 let (wt_repo, _temp_wt_project) = match find_or_create_repository(worktree_path, cx).await {
543 Ok(result) => result,
544 Err(error) => {
545 remove_new_worktree_on_error(created_new_worktree, &main_repo, worktree_path, cx).await;
546 return Err(error);
547 }
548 };
549
550 // Switch to the branch. Since the branch was never moved during
551 // archival (WIP commits are detached), it still points at
552 // original_commit_hash, so this is essentially a no-op for HEAD.
553 if let Some(branch_name) = &row.branch_name {
554 let rx = wt_repo.update(cx, |repo, _cx| repo.change_branch(branch_name.clone()));
555 if let Err(checkout_error) = rx.await.map_err(|e| anyhow!("{e}")).and_then(|r| r) {
556 log::debug!(
557 "change_branch('{}') failed: {checkout_error:#}, trying create_branch",
558 branch_name
559 );
560 let rx = wt_repo.update(cx, |repo, _cx| {
561 repo.create_branch(branch_name.clone(), None)
562 });
563 if let Ok(Err(error)) | Err(error) = rx.await.map_err(|e| anyhow!("{e}")) {
564 log::warn!(
565 "Could not create branch '{}': {error} — \
566 restored worktree will be in detached HEAD state.",
567 branch_name
568 );
569 }
570 }
571 }
572
573 // Restore the staged/unstaged state from the WIP commit trees.
574 // read-tree --reset -u applies the unstaged tree (including deletions)
575 // to the working directory, then a bare read-tree sets the index to
576 // the staged tree without touching the working directory.
577 let restore_rx = wt_repo.update(cx, |repo, _cx| {
578 repo.restore_archive_checkpoint(
579 row.staged_commit_hash.clone(),
580 row.unstaged_commit_hash.clone(),
581 )
582 });
583 if let Err(error) = restore_rx
584 .await
585 .map_err(|_| anyhow!("restore_archive_checkpoint canceled"))
586 .and_then(|r| r)
587 {
588 remove_new_worktree_on_error(created_new_worktree, &main_repo, worktree_path, cx).await;
589 return Err(error.context("failed to restore archive checkpoint"));
590 }
591
592 Ok(worktree_path.clone())
593}
594
595async fn remove_new_worktree_on_error(
596 created_new_worktree: bool,
597 main_repo: &Entity<Repository>,
598 worktree_path: &PathBuf,
599 cx: &mut AsyncApp,
600) {
601 if created_new_worktree {
602 let rx = main_repo.update(cx, |repo, _cx| {
603 repo.remove_worktree(worktree_path.clone(), true)
604 });
605 rx.await.ok().and_then(|r| r.log_err());
606 }
607}
608
609/// Deletes the git ref and DB records for a single archived worktree.
610/// Used when an archived worktree is no longer referenced by any thread.
611pub async fn cleanup_archived_worktree_record(row: &ArchivedGitWorktree, cx: &mut AsyncApp) {
612 // Delete the git ref from the main repo
613 if let Ok((main_repo, _temp_project)) = find_or_create_repository(&row.main_repo_path, cx).await
614 {
615 let ref_name = archived_worktree_ref_name(row.id);
616 let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
617 match rx.await {
618 Ok(Ok(())) => {}
619 Ok(Err(error)) => log::warn!("Failed to delete archive ref: {error}"),
620 Err(_) => log::warn!("Archive ref deletion was canceled"),
621 }
622 // Keep _temp_project alive until after the await so the headless project isn't dropped mid-operation
623 drop(_temp_project);
624 }
625
626 // Delete the DB records
627 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
628 store
629 .read_with(cx, |store, cx| store.delete_archived_worktree(row.id, cx))
630 .await
631 .log_err();
632}
633
634/// Cleans up all archived worktree data associated with a thread being deleted.
635///
636/// This unlinks the thread from all its archived worktrees and, for any
637/// archived worktree that is no longer referenced by any other thread,
638/// deletes the git ref and DB records.
639pub async fn cleanup_thread_archived_worktrees(session_id: &acp::SessionId, cx: &mut AsyncApp) {
640 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
641
642 let archived_worktrees = store
643 .read_with(cx, |store, cx| {
644 store.get_archived_worktrees_for_thread(session_id.0.to_string(), cx)
645 })
646 .await;
647 let archived_worktrees = match archived_worktrees {
648 Ok(rows) => rows,
649 Err(error) => {
650 log::error!(
651 "Failed to fetch archived worktrees for thread {}: {error:#}",
652 session_id.0
653 );
654 return;
655 }
656 };
657
658 if archived_worktrees.is_empty() {
659 return;
660 }
661
662 if let Err(error) = store
663 .read_with(cx, |store, cx| {
664 store.unlink_thread_from_all_archived_worktrees(session_id.0.to_string(), cx)
665 })
666 .await
667 {
668 log::error!(
669 "Failed to unlink thread {} from archived worktrees: {error:#}",
670 session_id.0
671 );
672 return;
673 }
674
675 for row in &archived_worktrees {
676 let still_referenced = store
677 .read_with(cx, |store, cx| {
678 store.is_archived_worktree_referenced(row.id, cx)
679 })
680 .await;
681 match still_referenced {
682 Ok(true) => {}
683 Ok(false) => {
684 cleanup_archived_worktree_record(row, cx).await;
685 }
686 Err(error) => {
687 log::error!(
688 "Failed to check if archived worktree {} is still referenced: {error:#}",
689 row.id
690 );
691 }
692 }
693 }
694}
695
696/// Collects every `Workspace` entity across all open `MultiWorkspace` windows.
697pub fn all_open_workspaces(cx: &App) -> Vec<Entity<Workspace>> {
698 cx.windows()
699 .into_iter()
700 .filter_map(|window| window.downcast::<MultiWorkspace>())
701 .flat_map(|multi_workspace| {
702 multi_workspace
703 .read(cx)
704 .map(|multi_workspace| multi_workspace.workspaces().cloned().collect::<Vec<_>>())
705 .unwrap_or_default()
706 })
707 .collect()
708}
709
710fn current_app_state(cx: &mut AsyncApp) -> Option<Arc<AppState>> {
711 cx.update(|cx| {
712 all_open_workspaces(cx)
713 .into_iter()
714 .next()
715 .map(|workspace| workspace.read(cx).app_state().clone())
716 })
717}