Skip to content

Commit f7b66ba

Browse files
authored
fix: normalize Windows workspace namespace paths (Dimillian#572)
1 parent 8dad36b commit f7b66ba

8 files changed

Lines changed: 497 additions & 30 deletions

File tree

src-tauri/src/shared/settings_core.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use tokio::sync::Mutex;
55
use crate::codex::config as codex_config;
66
use crate::storage::write_settings;
77
use crate::types::AppSettings;
8+
use crate::utils::normalize_windows_namespace_path;
89

910
fn normalize_personality(value: &str) -> Option<&'static str> {
1011
match value.trim() {
@@ -40,10 +41,13 @@ pub(crate) async fn get_app_settings_core(app_settings: &Mutex<AppSettings>) ->
4041
}
4142

4243
pub(crate) async fn update_app_settings_core(
43-
settings: AppSettings,
44+
mut settings: AppSettings,
4445
app_settings: &Mutex<AppSettings>,
4546
settings_path: &PathBuf,
4647
) -> Result<AppSettings, String> {
48+
settings.global_worktrees_folder = settings
49+
.global_worktrees_folder
50+
.map(|path| normalize_windows_namespace_path(&path));
4751
let _ = codex_config::write_collaboration_modes_enabled(settings.collaboration_modes_enabled);
4852
let _ = codex_config::write_steer_enabled(settings.steer_enabled);
4953
let _ = codex_config::write_unified_exec_enabled(settings.unified_exec_enabled);

src-tauri/src/shared/workspaces_core/crud_persistence.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ use crate::shared::process_core::kill_child_process_tree;
1313
use crate::shared::{git_core, worktree_core};
1414
use crate::storage::write_workspaces;
1515
use crate::types::{AppSettings, WorkspaceEntry, WorkspaceInfo, WorkspaceKind, WorkspaceSettings};
16+
use crate::utils::normalize_windows_namespace_path;
1617

1718
use super::connect::{kill_session_by_id, take_live_shared_session, workspace_session_spawn_lock};
18-
use super::helpers::{normalize_setup_script, normalize_workspace_path_input};
19+
use super::helpers::{
20+
normalize_setup_script, normalize_workspace_path_input, workspace_path_to_string,
21+
};
1922

2023
pub(crate) async fn add_workspace_core<F, Fut>(
2124
path: String,
@@ -33,7 +36,7 @@ where
3336
if !normalized_path.is_dir() {
3437
return Err("Workspace path must be a folder.".to_string());
3538
}
36-
let path = normalized_path.to_string_lossy().to_string();
39+
let path = workspace_path_to_string(&normalized_path);
3740

3841
let name = PathBuf::from(&path)
3942
.file_name()
@@ -154,6 +157,7 @@ where
154157
let destination_path =
155158
worktree_core::build_clone_destination_path(&copies_folder_path, &copy_name);
156159
let destination_path_string = destination_path.to_string_lossy().to_string();
160+
let stored_destination_path = workspace_path_to_string(&destination_path);
157161

158162
if let Err(error) = git_core::run_git_command(
159163
&copies_folder_path,
@@ -189,7 +193,7 @@ where
189193
let entry = WorkspaceEntry {
190194
id: Uuid::new_v4().to_string(),
191195
name: copy_name,
192-
path: destination_path_string,
196+
path: stored_destination_path,
193197
kind: WorkspaceKind::Main,
194198
parent_id: None,
195199
worktree: None,
@@ -342,6 +346,7 @@ where
342346
}
343347

344348
let clone_path_string = clone_path.to_string_lossy().to_string();
349+
let stored_clone_path = workspace_path_to_string(&clone_path);
345350
if let Err(error) =
346351
git_core::run_git_command(&destination_parent, &["clone", &url, &clone_path_string]).await
347352
{
@@ -357,7 +362,7 @@ where
357362
let entry = WorkspaceEntry {
358363
id: Uuid::new_v4().to_string(),
359364
name: workspace_name,
360-
path: clone_path_string,
365+
path: stored_clone_path,
361366
kind: WorkspaceKind::Main,
362367
parent_id: None,
363368
worktree: None,
@@ -553,6 +558,9 @@ where
553558
FutSpawn: Future<Output = Result<Arc<WorkspaceSession>, String>>,
554559
{
555560
settings.worktree_setup_script = normalize_setup_script(settings.worktree_setup_script);
561+
settings.worktrees_folder = settings
562+
.worktrees_folder
563+
.map(|path| normalize_windows_namespace_path(&path));
556564

557565
let (entry_snapshot, previous_worktree_setup_script, child_entries) = {
558566
let mut workspaces = workspaces.lock().await;

src-tauri/src/shared/workspaces_core/helpers.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use tokio::sync::Mutex;
66

77
use crate::backend::app_server::WorkspaceSession;
88
use crate::types::{WorkspaceEntry, WorkspaceInfo};
9+
use crate::utils::normalize_windows_namespace_path;
910

1011
pub(crate) const WORKTREE_SETUP_MARKERS_DIR: &str = "worktree-setup";
1112
pub(crate) const WORKTREE_SETUP_MARKER_EXT: &str = "ran";
@@ -81,6 +82,10 @@ pub(crate) fn normalize_workspace_path_input(path: &str) -> PathBuf {
8182
PathBuf::from(trimmed)
8283
}
8384

85+
pub(crate) fn workspace_path_to_string(path: &PathBuf) -> String {
86+
normalize_windows_namespace_path(&path.to_string_lossy())
87+
}
88+
8489
pub(crate) async fn list_workspaces_core(
8590
workspaces: &Mutex<HashMap<String, WorkspaceEntry>>,
8691
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
@@ -147,7 +152,8 @@ pub(super) fn sort_workspaces(workspaces: &mut [WorkspaceInfo]) {
147152
#[cfg(test)]
148153
mod tests {
149154
use super::{
150-
copy_agents_md_from_parent_to_worktree, normalize_workspace_path_input, AGENTS_MD_FILE_NAME,
155+
copy_agents_md_from_parent_to_worktree, normalize_workspace_path_input,
156+
workspace_path_to_string, AGENTS_MD_FILE_NAME,
151157
};
152158
use std::path::PathBuf;
153159
use std::sync::Mutex;
@@ -217,4 +223,12 @@ mod tests {
217223
None => std::env::remove_var("HOME"),
218224
}
219225
}
226+
227+
#[test]
228+
fn workspace_path_to_string_strips_windows_namespace_prefixes() {
229+
assert_eq!(
230+
workspace_path_to_string(&PathBuf::from(r"\\?\I:\gpt-projects\CodexMonitor")),
231+
r"I:\gpt-projects\CodexMonitor"
232+
);
233+
}
220234
}

src-tauri/src/shared/workspaces_core/worktree.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use crate::types::{
1717

1818
use super::connect::{kill_session_by_id, take_live_shared_session, workspace_session_spawn_lock};
1919
use super::helpers::{
20-
copy_agents_md_from_parent_to_worktree, normalize_setup_script, worktree_setup_marker_path,
21-
AGENTS_MD_FILE_NAME,
20+
copy_agents_md_from_parent_to_worktree, normalize_setup_script, workspace_path_to_string,
21+
worktree_setup_marker_path, AGENTS_MD_FILE_NAME,
2222
};
2323

2424
pub(crate) async fn worktree_setup_status_core(
@@ -136,14 +136,15 @@ where
136136

137137
// Determine worktree root: per-workspace setting > global setting > default
138138
let worktree_root = if let Some(custom_folder) = &parent_entry.settings.worktrees_folder {
139-
PathBuf::from(custom_folder)
139+
PathBuf::from(workspace_path_to_string(&PathBuf::from(custom_folder)))
140140
} else {
141141
let global_folder = {
142142
let settings = app_settings.lock().await;
143143
settings.global_worktrees_folder.clone()
144144
};
145145
if let Some(global_folder) = global_folder {
146-
PathBuf::from(global_folder).join(&parent_entry.id)
146+
PathBuf::from(workspace_path_to_string(&PathBuf::from(global_folder)))
147+
.join(&parent_entry.id)
147148
} else {
148149
data_dir.join("worktrees").join(&parent_entry.id)
149150
}
@@ -154,6 +155,7 @@ where
154155
let safe_name = sanitize_worktree_name(&branch);
155156
let worktree_path = unique_worktree_path(&worktree_root, &safe_name)?;
156157
let worktree_path_string = worktree_path.to_string_lossy().to_string();
158+
let stored_worktree_path = workspace_path_to_string(&worktree_path);
157159

158160
let repo_path = PathBuf::from(&parent_entry.path);
159161
let branch_exists = git_branch_exists(&repo_path, &branch).await?;
@@ -206,7 +208,7 @@ where
206208
let entry = WorkspaceEntry {
207209
id: Uuid::new_v4().to_string(),
208210
name: name.clone().unwrap_or_else(|| branch.clone()),
209-
path: worktree_path_string,
211+
path: stored_worktree_path,
210212
kind: WorkspaceKind::Worktree,
211213
parent_id: Some(parent_entry.id.clone()),
212214
worktree: Some(WorktreeInfo { branch }),
@@ -409,14 +411,14 @@ where
409411
// Use the same priority logic as add_worktree_core:
410412
// per-workspace setting > global setting > default
411413
let worktree_root = if let Some(custom_folder) = &parent.settings.worktrees_folder {
412-
PathBuf::from(custom_folder)
414+
PathBuf::from(workspace_path_to_string(&PathBuf::from(custom_folder)))
413415
} else {
414416
let global_folder = {
415417
let settings = app_settings.lock().await;
416418
settings.global_worktrees_folder.clone()
417419
};
418420
if let Some(global_folder) = global_folder {
419-
PathBuf::from(global_folder).join(&parent.id)
421+
PathBuf::from(workspace_path_to_string(&PathBuf::from(global_folder))).join(&parent.id)
420422
} else {
421423
data_dir.join("worktrees").join(&parent.id)
422424
}
@@ -428,12 +430,13 @@ where
428430
let current_path = PathBuf::from(&entry.path);
429431
let next_path = unique_worktree_path_for_rename(&worktree_root, &safe_name, &current_path)?;
430432
let next_path_string = next_path.to_string_lossy().to_string();
433+
let stored_next_path = workspace_path_to_string(&next_path);
431434
let old_path_string = entry.path.clone();
432435

433436
run_git_command(&parent_root, &["branch", "-m", &old_branch, &final_branch]).await?;
434437

435438
let mut moved_worktree = false;
436-
if next_path_string != old_path_string {
439+
if stored_next_path != old_path_string {
437440
if let Err(error) = run_git_command(
438441
&parent_root,
439442
&["worktree", "move", &old_path_string, &next_path_string],
@@ -454,7 +457,7 @@ where
454457
if entry.name.trim() == old_branch {
455458
entry.name = final_branch.clone();
456459
}
457-
entry.path = next_path_string.clone();
460+
entry.path = stored_next_path.clone();
458461
match entry.worktree.as_mut() {
459462
Some(worktree) => {
460463
worktree.branch = final_branch.clone();

0 commit comments

Comments
 (0)