1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use agent_client_protocol as acp;
7use anyhow::{Context as _, Result, anyhow};
8use git::repository::{AskPassDelegate, CommitOptions, ResetMode};
9use gpui::{App, AsyncApp, Entity, Task};
10use project::{
11 LocalProjectFlags, Project, WorktreeId,
12 git_store::{Repository, resolve_git_worktree_to_main_repo},
13};
14use util::ResultExt;
15use workspace::{AppState, MultiWorkspace, Workspace};
16
17use crate::thread_metadata_store::{ArchivedGitWorktree, ThreadMetadataStore};
18
19#[derive(Clone)]
20pub struct RootPlan {
21 pub root_path: PathBuf,
22 pub main_repo_path: PathBuf,
23 pub affected_projects: Vec<AffectedProject>,
24 pub worktree_repo: Option<Entity<Repository>>,
25 pub branch_name: Option<String>,
26}
27
28#[derive(Clone)]
29pub struct AffectedProject {
30 pub project: Entity<Project>,
31 pub worktree_id: WorktreeId,
32}
33
34fn archived_worktree_ref_name(id: i64) -> String {
35 format!("refs/archived-worktrees/{}", id)
36}
37
38pub struct PersistOutcome {
39 pub archived_worktree_id: i64,
40 pub staged_commit_hash: String,
41}
42
43pub fn build_root_plan(
44 path: &Path,
45 workspaces: &[Entity<Workspace>],
46 cx: &App,
47) -> Option<RootPlan> {
48 let path = path.to_path_buf();
49
50 let affected_projects = workspaces
51 .iter()
52 .filter_map(|workspace| {
53 let project = workspace.read(cx).project().clone();
54 let worktree = project
55 .read(cx)
56 .visible_worktrees(cx)
57 .find(|worktree| worktree.read(cx).abs_path().as_ref() == path.as_path())?;
58 let worktree_id = worktree.read(cx).id();
59 Some(AffectedProject {
60 project,
61 worktree_id,
62 })
63 })
64 .collect::<Vec<_>>();
65
66 let (linked_snapshot, worktree_repo) = workspaces
67 .iter()
68 .flat_map(|workspace| {
69 workspace
70 .read(cx)
71 .project()
72 .read(cx)
73 .repositories(cx)
74 .values()
75 .cloned()
76 .collect::<Vec<_>>()
77 })
78 .find_map(|repo| {
79 let snapshot = repo.read(cx).snapshot();
80 (snapshot.is_linked_worktree()
81 && snapshot.work_directory_abs_path.as_ref() == path.as_path())
82 .then_some((snapshot, repo))
83 })?;
84
85 let branch_name = linked_snapshot
86 .branch
87 .as_ref()
88 .map(|b| b.name().to_string());
89
90 Some(RootPlan {
91 root_path: path,
92 main_repo_path: linked_snapshot.original_repo_abs_path.to_path_buf(),
93 affected_projects,
94 worktree_repo: Some(worktree_repo),
95 branch_name,
96 })
97}
98
99pub fn path_is_referenced_by_other_unarchived_threads(
100 current_session_id: &acp::SessionId,
101 path: &Path,
102 cx: &App,
103) -> bool {
104 ThreadMetadataStore::global(cx)
105 .read(cx)
106 .entries()
107 .filter(|thread| thread.session_id != *current_session_id)
108 .filter(|thread| !thread.archived)
109 .any(|thread| {
110 thread
111 .folder_paths
112 .paths()
113 .iter()
114 .any(|other_path| other_path.as_path() == path)
115 })
116}
117
118pub async fn remove_root(root: RootPlan, cx: &mut AsyncApp) -> Result<()> {
119 let release_tasks: Vec<_> = root
120 .affected_projects
121 .iter()
122 .map(|affected| {
123 let project = affected.project.clone();
124 let worktree_id = affected.worktree_id;
125 project.update(cx, |project, cx| {
126 let wait = project.wait_for_worktree_release(worktree_id, cx);
127 project.remove_worktree(worktree_id, cx);
128 wait
129 })
130 })
131 .collect();
132
133 if let Err(error) = remove_root_after_worktree_removal(&root, release_tasks, cx).await {
134 rollback_root(&root, cx).await;
135 return Err(error);
136 }
137
138 Ok(())
139}
140
141async fn remove_root_after_worktree_removal(
142 root: &RootPlan,
143 release_tasks: Vec<Task<Result<()>>>,
144 cx: &mut AsyncApp,
145) -> Result<()> {
146 for task in release_tasks {
147 if let Err(error) = task.await {
148 log::error!("Failed waiting for worktree release: {error:#}");
149 }
150 }
151
152 let (repo, _temp_project) = find_or_create_repository(&root.main_repo_path, cx).await?;
153 let receiver = repo.update(cx, |repo: &mut Repository, _cx| {
154 repo.remove_worktree(root.root_path.clone(), false)
155 });
156 let result = receiver
157 .await
158 .map_err(|_| anyhow!("git worktree removal was canceled"))?;
159 result
160}
161
162/// Finds a live `Repository` entity for the given path, or creates a temporary
163/// `Project::local` to obtain one.
164///
165/// `Repository` entities can only be obtained through a `Project` because
166/// `GitStore` (which creates and manages `Repository` entities) is owned by
167/// `Project`. When no open workspace contains the repo we need, we spin up a
168/// headless `Project::local` just to get a `Repository` handle. The caller
169/// keeps the returned `Option<Entity<Project>>` alive for the duration of the
170/// git operations, then drops it.
171///
172/// Future improvement: decoupling `GitStore` from `Project` so that
173/// `Repository` entities can be created standalone would eliminate this
174/// temporary-project workaround.
175async fn find_or_create_repository(
176 repo_path: &Path,
177 cx: &mut AsyncApp,
178) -> Result<(Entity<Repository>, Option<Entity<Project>>)> {
179 let repo_path_owned = repo_path.to_path_buf();
180 let live_repo = cx.update(|cx| {
181 all_open_workspaces(cx)
182 .into_iter()
183 .flat_map(|workspace| {
184 workspace
185 .read(cx)
186 .project()
187 .read(cx)
188 .repositories(cx)
189 .values()
190 .cloned()
191 .collect::<Vec<_>>()
192 })
193 .find(|repo| {
194 repo.read(cx).snapshot().work_directory_abs_path.as_ref()
195 == repo_path_owned.as_path()
196 })
197 });
198
199 if let Some(repo) = live_repo {
200 return Ok((repo, None));
201 }
202
203 let app_state =
204 current_app_state(cx).context("no app state available for temporary project")?;
205 let temp_project = cx.update(|cx| {
206 Project::local(
207 app_state.client.clone(),
208 app_state.node_runtime.clone(),
209 app_state.user_store.clone(),
210 app_state.languages.clone(),
211 app_state.fs.clone(),
212 None,
213 LocalProjectFlags::default(),
214 cx,
215 )
216 });
217
218 let repo_path_for_worktree = repo_path.to_path_buf();
219 let create_worktree = temp_project.update(cx, |project, cx| {
220 project.create_worktree(repo_path_for_worktree, true, cx)
221 });
222 let _worktree = create_worktree.await?;
223 let initial_scan = temp_project.read_with(cx, |project, cx| project.wait_for_initial_scan(cx));
224 initial_scan.await;
225
226 let repo_path_for_find = repo_path.to_path_buf();
227 let repo = temp_project
228 .update(cx, |project, cx| {
229 project
230 .repositories(cx)
231 .values()
232 .find(|repo| {
233 repo.read(cx).snapshot().work_directory_abs_path.as_ref()
234 == repo_path_for_find.as_path()
235 })
236 .cloned()
237 })
238 .context("failed to resolve temporary repository handle")?;
239
240 let barrier = repo.update(cx, |repo: &mut Repository, _cx| repo.barrier());
241 barrier
242 .await
243 .map_err(|_| anyhow!("temporary repository barrier canceled"))?;
244 Ok((repo, Some(temp_project)))
245}
246
247async fn rollback_root(root: &RootPlan, cx: &mut AsyncApp) {
248 for affected in &root.affected_projects {
249 let task = affected.project.update(cx, |project, cx| {
250 project.create_worktree(root.root_path.clone(), true, cx)
251 });
252 task.await.log_err();
253 }
254}
255
256pub async fn persist_worktree_state(root: &RootPlan, cx: &mut AsyncApp) -> Result<PersistOutcome> {
257 let worktree_repo = root
258 .worktree_repo
259 .clone()
260 .context("no worktree repo entity for persistence")?;
261
262 // Read original HEAD SHA before creating any WIP commits
263 let original_commit_hash = worktree_repo
264 .update(cx, |repo, _cx| repo.head_sha())
265 .await
266 .map_err(|_| anyhow!("head_sha canceled"))?
267 .context("failed to read original HEAD SHA")?
268 .context("HEAD SHA is None before WIP commits")?;
269
270 // Create WIP commit #1 (staged state)
271 let askpass = AskPassDelegate::new(cx, |_, _, _| {});
272 let commit_rx = worktree_repo.update(cx, |repo, cx| {
273 repo.commit(
274 "WIP staged".into(),
275 None,
276 CommitOptions {
277 allow_empty: true,
278 ..Default::default()
279 },
280 askpass,
281 cx,
282 )
283 });
284 commit_rx
285 .await
286 .map_err(|_| anyhow!("WIP staged commit canceled"))??;
287
288 // Read SHA after staged commit
289 let staged_sha_result = worktree_repo
290 .update(cx, |repo, _cx| repo.head_sha())
291 .await
292 .map_err(|_| anyhow!("head_sha canceled"))
293 .and_then(|r| r.context("failed to read HEAD SHA after staged commit"))
294 .and_then(|opt| opt.context("HEAD SHA is None after staged commit"));
295 let staged_commit_hash = match staged_sha_result {
296 Ok(sha) => sha,
297 Err(error) => {
298 let rx = worktree_repo.update(cx, |repo, cx| {
299 repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
300 });
301 rx.await.ok().and_then(|r| r.log_err());
302 return Err(error);
303 }
304 };
305
306 // Stage all files including untracked
307 let stage_rx = worktree_repo.update(cx, |repo, _cx| repo.stage_all_including_untracked());
308 if let Err(error) = stage_rx
309 .await
310 .map_err(|_| anyhow!("stage all canceled"))
311 .and_then(|inner| inner)
312 {
313 let rx = worktree_repo.update(cx, |repo, cx| {
314 repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
315 });
316 rx.await.ok().and_then(|r| r.log_err());
317 return Err(error.context("failed to stage all files including untracked"));
318 }
319
320 // Create WIP commit #2 (unstaged/untracked state)
321 let askpass = AskPassDelegate::new(cx, |_, _, _| {});
322 let commit_rx = worktree_repo.update(cx, |repo, cx| {
323 repo.commit(
324 "WIP unstaged".into(),
325 None,
326 CommitOptions {
327 allow_empty: true,
328 ..Default::default()
329 },
330 askpass,
331 cx,
332 )
333 });
334 if let Err(error) = commit_rx
335 .await
336 .map_err(|_| anyhow!("WIP unstaged commit canceled"))
337 .and_then(|inner| inner)
338 {
339 let rx = worktree_repo.update(cx, |repo, cx| {
340 repo.reset("HEAD~1".to_string(), ResetMode::Mixed, cx)
341 });
342 rx.await.ok().and_then(|r| r.log_err());
343 return Err(error);
344 }
345
346 // Read HEAD SHA after WIP commits
347 let head_sha_result = worktree_repo
348 .update(cx, |repo, _cx| repo.head_sha())
349 .await
350 .map_err(|_| anyhow!("head_sha canceled"))
351 .and_then(|r| r.context("failed to read HEAD SHA after WIP commits"))
352 .and_then(|opt| opt.context("HEAD SHA is None after WIP commits"));
353 let unstaged_commit_hash = match head_sha_result {
354 Ok(sha) => sha,
355 Err(error) => {
356 let rx = worktree_repo.update(cx, |repo, cx| {
357 repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
358 });
359 rx.await.ok().and_then(|r| r.log_err());
360 return Err(error);
361 }
362 };
363
364 // Create DB record
365 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
366 let worktree_path_str = root.root_path.to_string_lossy().to_string();
367 let main_repo_path_str = root.main_repo_path.to_string_lossy().to_string();
368 let branch_name = root.branch_name.clone();
369
370 let db_result = store
371 .read_with(cx, |store, cx| {
372 store.create_archived_worktree(
373 worktree_path_str.clone(),
374 main_repo_path_str.clone(),
375 branch_name.clone(),
376 staged_commit_hash.clone(),
377 unstaged_commit_hash.clone(),
378 original_commit_hash.clone(),
379 cx,
380 )
381 })
382 .await
383 .context("failed to create archived worktree DB record");
384 let archived_worktree_id = match db_result {
385 Ok(id) => id,
386 Err(error) => {
387 let rx = worktree_repo.update(cx, |repo, cx| {
388 repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
389 });
390 rx.await.ok().and_then(|r| r.log_err());
391 return Err(error);
392 }
393 };
394
395 // Link all threads on this worktree to the archived record
396 let session_ids: Vec<acp::SessionId> = store.read_with(cx, |store, _cx| {
397 store
398 .entries()
399 .filter(|thread| {
400 thread
401 .folder_paths
402 .paths()
403 .iter()
404 .any(|p| p.as_path() == root.root_path)
405 })
406 .map(|thread| thread.session_id.clone())
407 .collect()
408 });
409
410 for session_id in &session_ids {
411 let link_result = store
412 .read_with(cx, |store, cx| {
413 store.link_thread_to_archived_worktree(
414 session_id.0.to_string(),
415 archived_worktree_id,
416 cx,
417 )
418 })
419 .await;
420 if let Err(error) = link_result {
421 if let Err(delete_error) = store
422 .read_with(cx, |store, cx| {
423 store.delete_archived_worktree(archived_worktree_id, cx)
424 })
425 .await
426 {
427 log::error!(
428 "Failed to delete archived worktree DB record during link rollback: {delete_error:#}"
429 );
430 }
431 let rx = worktree_repo.update(cx, |repo, cx| {
432 repo.reset(format!("{}~1", staged_commit_hash), ResetMode::Mixed, cx)
433 });
434 rx.await.ok().and_then(|r| r.log_err());
435 return Err(error.context("failed to link thread to archived worktree"));
436 }
437 }
438
439 // Create git ref on main repo (non-fatal)
440 let ref_name = archived_worktree_ref_name(archived_worktree_id);
441 let main_repo_result = find_or_create_repository(&root.main_repo_path, cx).await;
442 match main_repo_result {
443 Ok((main_repo, _temp_project)) => {
444 let rx = main_repo.update(cx, |repo, _cx| {
445 repo.update_ref(ref_name.clone(), unstaged_commit_hash.clone())
446 });
447 if let Err(error) = rx
448 .await
449 .map_err(|_| anyhow!("update_ref canceled"))
450 .and_then(|r| r)
451 {
452 log::warn!(
453 "Failed to create ref {} on main repo (non-fatal): {error}",
454 ref_name
455 );
456 }
457 }
458 Err(error) => {
459 log::warn!(
460 "Could not find main repo to create ref {} (non-fatal): {error}",
461 ref_name
462 );
463 }
464 }
465
466 Ok(PersistOutcome {
467 archived_worktree_id,
468 staged_commit_hash,
469 })
470}
471
472pub async fn rollback_persist(outcome: &PersistOutcome, root: &RootPlan, cx: &mut AsyncApp) {
473 // Undo WIP commits on the worktree repo
474 if let Some(worktree_repo) = &root.worktree_repo {
475 let rx = worktree_repo.update(cx, |repo, cx| {
476 repo.reset(
477 format!("{}~1", outcome.staged_commit_hash),
478 ResetMode::Mixed,
479 cx,
480 )
481 });
482 rx.await.ok().and_then(|r| r.log_err());
483 }
484
485 // Delete the git ref on main repo
486 if let Ok((main_repo, _temp_project)) =
487 find_or_create_repository(&root.main_repo_path, cx).await
488 {
489 let ref_name = archived_worktree_ref_name(outcome.archived_worktree_id);
490 let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
491 rx.await.ok().and_then(|r| r.log_err());
492 }
493
494 // Delete the DB record
495 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
496 if let Err(error) = store
497 .read_with(cx, |store, cx| {
498 store.delete_archived_worktree(outcome.archived_worktree_id, cx)
499 })
500 .await
501 {
502 log::error!("Failed to delete archived worktree DB record during rollback: {error:#}");
503 }
504}
505
506pub async fn restore_worktree_via_git(
507 row: &ArchivedGitWorktree,
508 cx: &mut AsyncApp,
509) -> Result<PathBuf> {
510 let (main_repo, _temp_project) = find_or_create_repository(&row.main_repo_path, cx).await?;
511
512 // Check if worktree path already exists on disk
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 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 // Already a git worktree — another thread on the same worktree
525 // already restored it. Reuse as-is.
526 return Ok(worktree_path.clone());
527 }
528
529 // Path exists but isn't a git worktree. Ask git to adopt it.
530 let rx = main_repo.update(cx, |repo, _cx| repo.repair_worktrees());
531 rx.await
532 .map_err(|_| anyhow!("worktree repair was canceled"))?
533 .context("failed to repair worktrees")?;
534 } else {
535 // Create detached worktree at the unstaged commit
536 let rx = main_repo.update(cx, |repo, _cx| {
537 repo.create_worktree_detached(worktree_path.clone(), row.unstaged_commit_hash.clone())
538 });
539 rx.await
540 .map_err(|_| anyhow!("worktree creation was canceled"))?
541 .context("failed to create worktree")?;
542 }
543
544 // Get the worktree's repo entity
545 let (wt_repo, _temp_wt_project) = find_or_create_repository(worktree_path, cx).await?;
546
547 // Reset past the WIP commits to recover original state
548 let mixed_reset_ok = {
549 let rx = wt_repo.update(cx, |repo, cx| {
550 repo.reset(row.staged_commit_hash.clone(), ResetMode::Mixed, cx)
551 });
552 match rx.await {
553 Ok(Ok(())) => true,
554 Ok(Err(error)) => {
555 log::error!("Mixed reset to staged commit failed: {error:#}");
556 false
557 }
558 Err(_) => {
559 log::error!("Mixed reset to staged commit was canceled");
560 false
561 }
562 }
563 };
564
565 let soft_reset_ok = if mixed_reset_ok {
566 let rx = wt_repo.update(cx, |repo, cx| {
567 repo.reset(row.original_commit_hash.clone(), ResetMode::Soft, cx)
568 });
569 match rx.await {
570 Ok(Ok(())) => true,
571 Ok(Err(error)) => {
572 log::error!("Soft reset to original commit failed: {error:#}");
573 false
574 }
575 Err(_) => {
576 log::error!("Soft reset to original commit was canceled");
577 false
578 }
579 }
580 } else {
581 false
582 };
583
584 // If either WIP reset failed, fall back to a mixed reset directly to
585 // original_commit_hash so we at least land on the right commit.
586 if !mixed_reset_ok || !soft_reset_ok {
587 log::warn!(
588 "WIP reset(s) failed (mixed_ok={mixed_reset_ok}, soft_ok={soft_reset_ok}); \
589 falling back to mixed reset to original commit {}",
590 row.original_commit_hash
591 );
592 let rx = wt_repo.update(cx, |repo, cx| {
593 repo.reset(row.original_commit_hash.clone(), ResetMode::Mixed, cx)
594 });
595 match rx.await {
596 Ok(Ok(())) => {}
597 Ok(Err(error)) => {
598 return Err(error.context(format!(
599 "fallback reset to original commit {} also failed",
600 row.original_commit_hash
601 )));
602 }
603 Err(_) => {
604 return Err(anyhow!(
605 "fallback reset to original commit {} was canceled",
606 row.original_commit_hash
607 ));
608 }
609 }
610 }
611
612 // Verify HEAD is at original_commit_hash
613 let current_head = wt_repo
614 .update(cx, |repo, _cx| repo.head_sha())
615 .await
616 .map_err(|_| anyhow!("post-restore head_sha was canceled"))?
617 .context("failed to read HEAD after restore")?
618 .context("HEAD is None after restore")?;
619
620 if current_head != row.original_commit_hash {
621 anyhow::bail!(
622 "After restore, HEAD is at {current_head} but expected {}. \
623 The worktree may be in an inconsistent state.",
624 row.original_commit_hash
625 );
626 }
627
628 // Restore the branch
629 if let Some(branch_name) = &row.branch_name {
630 // Check if the branch exists and points at original_commit_hash.
631 // If it does, switch to it. If not, create a new branch there.
632 let rx = wt_repo.update(cx, |repo, _cx| repo.change_branch(branch_name.clone()));
633 if matches!(rx.await, Ok(Ok(()))) {
634 // Verify the branch actually points at original_commit_hash after switching
635 let head_after_switch = wt_repo
636 .update(cx, |repo, _cx| repo.head_sha())
637 .await
638 .ok()
639 .and_then(|r| r.ok())
640 .flatten();
641
642 if head_after_switch.as_deref() != Some(&row.original_commit_hash) {
643 // Branch exists but doesn't point at the right commit.
644 // Switch back to detached HEAD at original_commit_hash.
645 log::warn!(
646 "Branch '{}' exists but points at {:?}, not {}. Creating fresh branch.",
647 branch_name,
648 head_after_switch,
649 row.original_commit_hash
650 );
651 let rx = wt_repo.update(cx, |repo, cx| {
652 repo.reset(row.original_commit_hash.clone(), ResetMode::Mixed, cx)
653 });
654 rx.await.ok().and_then(|r| r.log_err());
655 // Delete the old branch and create fresh
656 let rx = wt_repo.update(cx, |repo, _cx| {
657 repo.create_branch(branch_name.clone(), None)
658 });
659 rx.await.ok().and_then(|r| r.log_err());
660 }
661 } else {
662 // Branch doesn't exist or can't be switched to — create it.
663 let rx = wt_repo.update(cx, |repo, _cx| {
664 repo.create_branch(branch_name.clone(), None)
665 });
666 if let Ok(Err(error)) | Err(error) = rx.await.map_err(|e| anyhow::anyhow!("{e}")) {
667 log::warn!(
668 "Could not create branch '{}': {error} — \
669 restored worktree is in detached HEAD state.",
670 branch_name
671 );
672 }
673 }
674 }
675
676 Ok(worktree_path.clone())
677}
678
679pub async fn cleanup_archived_worktree_record(row: &ArchivedGitWorktree, cx: &mut AsyncApp) {
680 // Delete the git ref from the main repo
681 if let Ok((main_repo, _temp_project)) = find_or_create_repository(&row.main_repo_path, cx).await
682 {
683 let ref_name = archived_worktree_ref_name(row.id);
684 let rx = main_repo.update(cx, |repo, _cx| repo.delete_ref(ref_name));
685 match rx.await {
686 Ok(Ok(())) => {}
687 Ok(Err(error)) => log::warn!("Failed to delete archive ref: {error}"),
688 Err(_) => log::warn!("Archive ref deletion was canceled"),
689 }
690 }
691
692 // Delete the DB records
693 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
694 store
695 .read_with(cx, |store, cx| store.delete_archived_worktree(row.id, cx))
696 .await
697 .log_err();
698}
699
700/// Cleans up all archived worktree data associated with a thread being deleted.
701///
702/// This unlinks the thread from all its archived worktrees and, for any
703/// archived worktree that is no longer referenced by any other thread,
704/// deletes the git ref and DB records.
705pub async fn cleanup_thread_archived_worktrees(session_id: &acp::SessionId, cx: &mut AsyncApp) {
706 let store = cx.update(|cx| ThreadMetadataStore::global(cx));
707
708 let archived_worktrees = store
709 .read_with(cx, |store, cx| {
710 store.get_archived_worktrees_for_thread(session_id.0.to_string(), cx)
711 })
712 .await;
713 let archived_worktrees = match archived_worktrees {
714 Ok(rows) => rows,
715 Err(error) => {
716 log::error!(
717 "Failed to fetch archived worktrees for thread {}: {error:#}",
718 session_id.0
719 );
720 return;
721 }
722 };
723
724 if archived_worktrees.is_empty() {
725 return;
726 }
727
728 if let Err(error) = store
729 .read_with(cx, |store, cx| {
730 store.unlink_thread_from_all_archived_worktrees(session_id.0.to_string(), cx)
731 })
732 .await
733 {
734 log::error!(
735 "Failed to unlink thread {} from archived worktrees: {error:#}",
736 session_id.0
737 );
738 return;
739 }
740
741 for row in &archived_worktrees {
742 let still_referenced = store
743 .read_with(cx, |store, cx| {
744 store.is_archived_worktree_referenced(row.id, cx)
745 })
746 .await;
747 match still_referenced {
748 Ok(true) => {}
749 Ok(false) => {
750 cleanup_archived_worktree_record(row, cx).await;
751 }
752 Err(error) => {
753 log::error!(
754 "Failed to check if archived worktree {} is still referenced: {error:#}",
755 row.id
756 );
757 }
758 }
759 }
760}
761
762pub fn all_open_workspaces(cx: &App) -> Vec<Entity<Workspace>> {
763 cx.windows()
764 .into_iter()
765 .filter_map(|window| window.downcast::<MultiWorkspace>())
766 .flat_map(|multi_workspace| {
767 multi_workspace
768 .read(cx)
769 .map(|multi_workspace| multi_workspace.workspaces().cloned().collect::<Vec<_>>())
770 .unwrap_or_default()
771 })
772 .collect()
773}
774
775fn current_app_state(cx: &mut AsyncApp) -> Option<Arc<AppState>> {
776 cx.update(|cx| {
777 all_open_workspaces(cx)
778 .into_iter()
779 .next()
780 .map(|workspace| workspace.read(cx).app_state().clone())
781 })
782}