Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src-tauri/src/shared/workspaces_core/crud_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ where
};

let repo_path = PathBuf::from(&entry.path);
let repo_path_exists = repo_path.is_dir();
let mut removed_child_ids = Vec::new();
let mut failures: Vec<(String, String)> = Vec::new();

Expand All @@ -265,7 +266,15 @@ where

let child_path = PathBuf::from(&child.path);
if child_path.exists() {
if let Err(error) =
if !repo_path_exists {
if let Err(fs_error) = remove_dir_all(&child_path) {
if continue_on_child_error {
failures.push((child.id.clone(), fs_error));
continue;
}
return Err(fs_error);
}
} else if let Err(error) =
run_git_command(&repo_path, &["worktree", "remove", "--force", &child.path]).await
{
if is_missing_worktree_error(&error) {
Expand All @@ -290,7 +299,9 @@ where
removed_child_ids.push(child.id.clone());
}

let _ = run_git_command(&repo_path, &["worktree", "prune", "--expire", "now"]).await;
if repo_path_exists {
let _ = run_git_command(&repo_path, &["worktree", "prune", "--expire", "now"]).await;
}

let mut ids_to_remove = removed_child_ids;
if failures.is_empty() || !require_all_children_removed_to_remove_parent {
Expand Down
9 changes: 7 additions & 2 deletions src-tauri/src/shared/workspaces_core/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,14 @@ where
};

let parent_path = PathBuf::from(&parent.path);
let parent_path_exists = parent_path.is_dir();
let entry_path = PathBuf::from(&entry.path);
kill_session_by_id(sessions, &entry.id).await;

if entry_path.exists() {
if let Err(error) = run_git_command(
if !parent_path_exists {
remove_dir_all(&entry_path)?;
} else if let Err(error) = run_git_command(
&parent_path,
&["worktree", "remove", "--force", &entry.path],
)
Expand All @@ -293,7 +296,9 @@ where
}
}
}
let _ = run_git_command(&parent_path, &["worktree", "prune", "--expire", "now"]).await;
if parent_path_exists {
let _ = run_git_command(&parent_path, &["worktree", "prune", "--expire", "now"]).await;
}

{
let mut workspaces = workspaces.lock().await;
Expand Down
121 changes: 120 additions & 1 deletion src-tauri/src/workspaces/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use super::worktree::{
build_clone_destination_path, sanitize_clone_dir_name, sanitize_worktree_name,
};
use crate::backend::app_server::WorkspaceSession;
use crate::shared::workspaces_core::rename_worktree_core;
use crate::shared::workspaces_core::{
remove_workspace_core, remove_worktree_core, rename_worktree_core,
};
use crate::storage::{read_workspaces, write_workspaces};
use crate::types::{
AppSettings, WorkspaceEntry, WorkspaceInfo, WorkspaceKind, WorkspaceSettings, WorktreeInfo,
Expand Down Expand Up @@ -394,3 +396,120 @@ fn rename_worktree_updates_name_when_unmodified() {
assert_eq!(updated.name, "feature/new");
});
}

#[test]
fn remove_workspace_succeeds_when_parent_repo_folder_is_missing() {
run_async(async {
let temp_dir = std::env::temp_dir().join(format!("codex-monitor-test-{}", Uuid::new_v4()));
let parent_repo_path = temp_dir.join("deleted-parent-repo");
let child_path = temp_dir.join("worktrees").join("parent").join("feature-a");
std::fs::create_dir_all(&child_path).expect("create child path");

let parent = WorkspaceEntry {
id: "parent".to_string(),
name: "Parent".to_string(),
path: parent_repo_path.to_string_lossy().to_string(),
codex_bin: None,
kind: WorkspaceKind::Main,
parent_id: None,
worktree: None,
settings: WorkspaceSettings::default(),
};
let child = WorkspaceEntry {
id: "wt-missing-parent".to_string(),
name: "feature-a".to_string(),
path: child_path.to_string_lossy().to_string(),
codex_bin: None,
kind: WorkspaceKind::Worktree,
parent_id: Some(parent.id.clone()),
worktree: Some(WorktreeInfo {
branch: "feature-a".to_string(),
}),
settings: WorkspaceSettings::default(),
};
let workspaces = Mutex::new(HashMap::from([
(parent.id.clone(), parent.clone()),
(child.id.clone(), child.clone()),
]));
let sessions: Mutex<HashMap<String, Arc<WorkspaceSession>>> = Mutex::new(HashMap::new());
let storage_path = temp_dir.join("workspaces.json");

remove_workspace_core(
parent.id.clone(),
&workspaces,
&sessions,
&storage_path,
|_root, _args| async move {
panic!("git should not run when parent repo folder is missing");
},
|_error| false,
|path| std::fs::remove_dir_all(path).map_err(|err| err.to_string()),
true,
true,
)
.await
.expect("remove workspace");

assert!(!child_path.exists());
let workspaces_guard = workspaces.lock().await;
assert!(workspaces_guard.is_empty());
});
}

#[test]
fn remove_worktree_succeeds_when_parent_repo_folder_is_missing() {
run_async(async {
let temp_dir = std::env::temp_dir().join(format!("codex-monitor-test-{}", Uuid::new_v4()));
let parent_repo_path = temp_dir.join("deleted-parent-repo");
let child_path = temp_dir.join("worktrees").join("parent").join("feature-b");
std::fs::create_dir_all(&child_path).expect("create child path");

let parent = WorkspaceEntry {
id: "parent".to_string(),
name: "Parent".to_string(),
path: parent_repo_path.to_string_lossy().to_string(),
codex_bin: None,
kind: WorkspaceKind::Main,
parent_id: None,
worktree: None,
settings: WorkspaceSettings::default(),
};
let child = WorkspaceEntry {
id: "wt-remove-only".to_string(),
name: "feature-b".to_string(),
path: child_path.to_string_lossy().to_string(),
codex_bin: None,
kind: WorkspaceKind::Worktree,
parent_id: Some(parent.id.clone()),
worktree: Some(WorktreeInfo {
branch: "feature-b".to_string(),
}),
settings: WorkspaceSettings::default(),
};
let workspaces = Mutex::new(HashMap::from([
(parent.id.clone(), parent.clone()),
(child.id.clone(), child.clone()),
]));
let sessions: Mutex<HashMap<String, Arc<WorkspaceSession>>> = Mutex::new(HashMap::new());
let storage_path = temp_dir.join("workspaces.json");

remove_worktree_core(
child.id.clone(),
&workspaces,
&sessions,
&storage_path,
|_root, _args| async move {
panic!("git should not run when parent repo folder is missing");
},
|_error| false,
|path| std::fs::remove_dir_all(path).map_err(|err| err.to_string()),
)
.await
.expect("remove worktree");

assert!(!child_path.exists());
let workspaces_guard = workspaces.lock().await;
assert!(workspaces_guard.contains_key(&parent.id));
assert!(!workspaces_guard.contains_key(&child.id));
});
}