Fix data loss when project settings opened with ".zed" in file_scan_exclusions (#29578)

Closes #28640

Before creating an entry for a file opened with `open_local_file`, make
sure it doesn't exist, in addition to checking that it isn't already
tracked in the workspace

Release Notes:

- Fixed an issue where the project settings file would be truncated when
opened with `zed: open project settings` if the ".zed" directory was
excluded from the files scanned in a workspace (in
"file_scan_exclusions")
This commit is contained in:
Ben Kunkle 2025-04-28 22:25:40 -04:00 committed by GitHub
parent 4dc8ce8cf7
commit 3a212e72a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1502,28 +1502,45 @@ fn open_local_file(
if let Some(worktree) = worktree {
let tree_id = worktree.read(cx).id();
cx.spawn_in(window, async move |workspace, cx| {
if let Some(dir_path) = settings_relative_path.parent() {
if worktree.update(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
// Check if the file actually exists on disk (even if it's excluded from worktree)
let file_exists = {
let full_path =
worktree.update(cx, |tree, _| tree.abs_path().join(settings_relative_path))?;
let fs = project.update(cx, |project, _| project.fs().clone())?;
let file_exists = fs
.metadata(&full_path)
.await
.ok()
.flatten()
.map_or(false, |metadata| !metadata.is_dir && !metadata.is_fifo);
file_exists
};
if !file_exists {
if let Some(dir_path) = settings_relative_path.parent() {
if worktree.update(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
project
.update(cx, |project, cx| {
project.create_entry((tree_id, dir_path), true, cx)
})?
.await
.context("worktree was removed")?;
}
}
if worktree.update(cx, |tree, _| {
tree.entry_for_path(settings_relative_path).is_none()
})? {
project
.update(cx, |project, cx| {
project.create_entry((tree_id, dir_path), true, cx)
project.create_entry((tree_id, settings_relative_path), false, cx)
})?
.await
.context("worktree was removed")?;
}
}
if worktree.update(cx, |tree, _| {
tree.entry_for_path(settings_relative_path).is_none()
})? {
project
.update(cx, |project, cx| {
project.create_entry((tree_id, settings_relative_path), false, cx)
})?
.await
.context("worktree was removed")?;
}
let editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
@ -4316,4 +4333,112 @@ mod tests {
);
}
}
#[gpui::test]
async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
// Use the proper initialization for runtime state
let app_state = init_keymap_test(cx);
eprintln!("Running test_opening_project_settings_when_excluded");
// 1. Set up a project with some project settings
let settings_init =
r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
app_state
.fs
.as_fake()
.insert_tree(
Path::new("/root"),
json!({
".zed": {
"settings.json": settings_init
}
}),
)
.await;
eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
// 2. Create a project with the file system and load it
let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
// Save original settings content for comparison
let original_settings = app_state
.fs
.load(Path::new("/root/.zed/settings.json"))
.await
.unwrap();
let original_settings_str = original_settings.clone();
// Verify settings exist on disk and have expected content
eprintln!("Original settings content: {}", original_settings_str);
assert!(
original_settings_str.contains("UNIQUEVALUE"),
"Test setup failed - settings file doesn't contain our marker"
);
// 3. Add .zed to file scan exclusions in user settings
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]);
});
});
eprintln!("Added .zed to file_scan_exclusions in settings");
// 4. Run tasks to apply settings
cx.background_executor.run_until_parked();
// 5. Critical: Verify .zed is actually excluded from worktree
let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().clone());
let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some());
eprintln!(
"Is .zed directory visible in worktree after exclusion: {}",
has_zed_entry
);
// This assertion verifies the test is set up correctly to show the bug
// If .zed is not excluded, the test will fail here
assert!(
!has_zed_entry,
"Test precondition failed: .zed directory should be excluded but was found in worktree"
);
// 6. Create workspace and trigger the actual function that causes the bug
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
window
.update(cx, |workspace, window, cx| {
// Call the exact function that contains the bug
eprintln!("About to call open_project_settings_file");
open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
})
.unwrap();
// 7. Run background tasks until completion
cx.background_executor.run_until_parked();
// 8. Verify file contents after calling function
let new_content = app_state
.fs
.load(Path::new("/root/.zed/settings.json"))
.await
.unwrap();
let new_content_str = new_content.clone();
eprintln!("New settings content: {}", new_content_str);
// The bug causes the settings to be overwritten with empty settings
// So if the unique value is no longer present, the bug has been reproduced
let bug_exists = !new_content_str.contains("UNIQUEVALUE");
eprintln!("Bug reproduced: {}", bug_exists);
// This assertion should fail if the bug exists - showing the bug is real
assert!(
new_content_str.contains("UNIQUEVALUE"),
"BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
);
}
}