use crate::{ worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree, WorktreeModelHandle,};use anyhow::Result;use fs::{FakeFs, Fs, RealFs, RemoveOptions};use git::{repository::GitFileStatus, GITIGNORE};use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};use parking_lot::Mutex;use postage::stream::Stream;use pretty_assertions::assert_eq;use rand::prelude::*;use serde_json::json;use settings::{Settings, SettingsStore};use std::{ env, fmt::Write, mem, path::{Path, PathBuf}, sync::Arc,};use util::{test::temp_tree, ResultExt}; #[gpui::test]async fn test_traversal(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ ".gitignore": "a/b\n", "a": { "b": "", "c": "", } }), ) .await; let tree = Worktree::local( Path::new("/root"), true, fs, Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(false, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new(".gitignore"), Path::new("a"), Path::new("a/c"), ] ); assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new(".gitignore"), Path::new("a"), Path::new("a/b"), Path::new("a/c"), ] ); })} #[gpui::test(iterations = 10)]async fn test_circular_symlinks(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ "lib": { "a": { "a.txt": "" }, "b": { "b.txt": "" } } }), ) .await; fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into()) .await .unwrap(); fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into()) .await .unwrap(); let tree = Worktree::local( Path::new("/root"), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(false, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new("lib"), Path::new("lib/a"), Path::new("lib/a/a.txt"), Path::new("lib/a/lib"), Path::new("lib/b"), Path::new("lib/b/b.txt"), Path::new("lib/b/lib"), ] ); }); fs.rename( Path::new("/root/lib/a/lib"), Path::new("/root/lib/a/lib-2"), Default::default(), ) .await .unwrap(); cx.executor().run_until_parked(); tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(false, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new("lib"), Path::new("lib/a"), Path::new("lib/a/a.txt"), Path::new("lib/a/lib-2"), Path::new("lib/b"), Path::new("lib/b/b.txt"), Path::new("lib/b/lib"), ] ); });} #[gpui::test]async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ "dir1": { "deps": { // symlinks here }, "src": { "a.rs": "", "b.rs": "", }, }, "dir2": { "src": { "c.rs": "", "d.rs": "", } }, "dir3": { "deps": {}, "src": { "e.rs": "", "f.rs": "", }, } }), ) .await; // These symlinks point to directories outside of the worktree's root, dir1. fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into()) .await .unwrap(); fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into()) .await .unwrap(); let tree = Worktree::local( Path::new("/root/dir1"), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; let tree_updates = Arc::new(Mutex::new(Vec::new())); tree.update(cx, |_, cx| { let tree_updates = tree_updates.clone(); cx.subscribe(&tree, move |_, _, event, _| { if let Event::UpdatedEntries(update) = event { tree_updates.lock().extend( update .iter() .map(|(path, _, change)| (path.clone(), *change)), ); } }) .detach(); }); // The symlinked directories are not scanned by default. tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_external)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new("deps"), false), (Path::new("deps/dep-dir2"), true), (Path::new("deps/dep-dir3"), true), (Path::new("src"), false), (Path::new("src/a.rs"), false), (Path::new("src/b.rs"), false), ] ); assert_eq!( tree.entry_for_path("deps/dep-dir2").unwrap().kind, EntryKind::UnloadedDir ); }); // Expand one of the symlinked directories. tree.read_with(cx, |tree, _| { tree.as_local() .unwrap() .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()]) }) .recv() .await; // The expanded directory's contents are loaded. Subdirectories are // not scanned yet. tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_external)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new("deps"), false), (Path::new("deps/dep-dir2"), true), (Path::new("deps/dep-dir3"), true), (Path::new("deps/dep-dir3/deps"), true), (Path::new("deps/dep-dir3/src"), true), (Path::new("src"), false), (Path::new("src/a.rs"), false), (Path::new("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (Path::new("deps/dep-dir3").into(), PathChange::Loaded), (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded), (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded) ] ); // Expand a subdirectory of one of the symlinked directories. tree.read_with(cx, |tree, _| { tree.as_local() .unwrap() .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()]) }) .recv() .await; // The expanded subdirectory's contents are loaded. tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_external)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new("deps"), false), (Path::new("deps/dep-dir2"), true), (Path::new("deps/dep-dir3"), true), (Path::new("deps/dep-dir3/deps"), true), (Path::new("deps/dep-dir3/src"), true), (Path::new("deps/dep-dir3/src/e.rs"), true), (Path::new("deps/dep-dir3/src/f.rs"), true), (Path::new("src"), false), (Path::new("src/a.rs"), false), (Path::new("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded), ( Path::new("deps/dep-dir3/src/e.rs").into(), PathChange::Loaded ), ( Path::new("deps/dep-dir3/src/f.rs").into(), PathChange::Loaded ) ] );} #[cfg(target_os = "macos")]#[gpui::test]async fn test_renaming_case_only(cx: &mut TestAppContext) { cx.executor().allow_parking(); init_test(cx); const OLD_NAME: &str = "aaa.rs"; const NEW_NAME: &str = "AAA.rs"; let fs = Arc::new(RealFs::default()); let temp_root = temp_tree(json!({ OLD_NAME: "", })); let tree = Worktree::local( temp_root.path(), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![Path::new(""), Path::new(OLD_NAME)] ); }); fs.rename( &temp_root.path().join(OLD_NAME), &temp_root.path().join(NEW_NAME), fs::RenameOptions { overwrite: true, ignore_if_exists: true, }, ) .await .unwrap(); tree.flush_fs_events(cx).await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![Path::new(""), Path::new(NEW_NAME)] ); });} #[gpui::test]async fn test_open_gitignored_files(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ ".gitignore": "node_modules\n", "one": { "node_modules": { "a": { "a1.js": "a1", "a2.js": "a2", }, "b": { "b1.js": "b1", "b2.js": "b2", }, "c": { "c1.js": "c1", "c2.js": "c2", } }, }, "two": { "x.js": "", "y.js": "", }, }), ) .await; let tree = Worktree::local( Path::new("/root"), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_ignored)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new(".gitignore"), false), (Path::new("one"), false), (Path::new("one/node_modules"), true), (Path::new("two"), false), (Path::new("two/x.js"), false), (Path::new("two/y.js"), false), ] ); }); // Open a file that is nested inside of a gitignored directory that // has not yet been expanded. let prev_read_dir_count = fs.read_dir_call_count(); let loaded = tree .update(cx, |tree, cx| { tree.load_file("one/node_modules/b/b1.js".as_ref(), cx) }) .await .unwrap(); tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_ignored)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new(".gitignore"), false), (Path::new("one"), false), (Path::new("one/node_modules"), true), (Path::new("one/node_modules/a"), true), (Path::new("one/node_modules/b"), true), (Path::new("one/node_modules/b/b1.js"), true), (Path::new("one/node_modules/b/b2.js"), true), (Path::new("one/node_modules/c"), true), (Path::new("two"), false), (Path::new("two/x.js"), false), (Path::new("two/y.js"), false), ] ); assert_eq!( loaded.file.path.as_ref(), Path::new("one/node_modules/b/b1.js") ); // Only the newly-expanded directories are scanned. assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2); }); // Open another file in a different subdirectory of the same // gitignored directory.Lines 1–500 of 2910. Load more
use crate::{ worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree, WorktreeModelHandle, }; use anyhow::Result; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; use git::{repository::GitFileStatus, GITIGNORE}; use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext}; use parking_lot::Mutex; use postage::stream::Stream; use pretty_assertions::assert_eq; use rand::prelude::*; use serde_json::json; use settings::{Settings, SettingsStore}; use std::{ env, fmt::Write, mem, path::{Path, PathBuf}, sync::Arc, }; use util::{test::temp_tree, ResultExt}; #[gpui::test] async fn test_traversal(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ ".gitignore": "a/b\n", "a": { "b": "", "c": "", } }), ) .await; let tree = Worktree::local( Path::new("/root"), true, fs, Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(false, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new(".gitignore"), Path::new("a"), Path::new("a/c"), ] ); assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new(".gitignore"), Path::new("a"), Path::new("a/b"), Path::new("a/c"), ] ); }) } #[gpui::test(iterations = 10)] async fn test_circular_symlinks(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ "lib": { "a": { "a.txt": "" }, "b": { "b.txt": "" } } }), ) .await; fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into()) .await .unwrap(); fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into()) .await .unwrap(); let tree = Worktree::local( Path::new("/root"), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(false, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new("lib"), Path::new("lib/a"), Path::new("lib/a/a.txt"), Path::new("lib/a/lib"), Path::new("lib/b"), Path::new("lib/b/b.txt"), Path::new("lib/b/lib"), ] ); }); fs.rename( Path::new("/root/lib/a/lib"), Path::new("/root/lib/a/lib-2"), Default::default(), ) .await .unwrap(); cx.executor().run_until_parked(); tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(false, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ Path::new(""), Path::new("lib"), Path::new("lib/a"), Path::new("lib/a/a.txt"), Path::new("lib/a/lib-2"), Path::new("lib/b"), Path::new("lib/b/b.txt"), Path::new("lib/b/lib"), ] ); }); } #[gpui::test] async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ "dir1": { "deps": { // symlinks here }, "src": { "a.rs": "", "b.rs": "", }, }, "dir2": { "src": { "c.rs": "", "d.rs": "", } }, "dir3": { "deps": {}, "src": { "e.rs": "", "f.rs": "", }, } }), ) .await; // These symlinks point to directories outside of the worktree's root, dir1. fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into()) .await .unwrap(); fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into()) .await .unwrap(); let tree = Worktree::local( Path::new("/root/dir1"), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; let tree_updates = Arc::new(Mutex::new(Vec::new())); tree.update(cx, |_, cx| { let tree_updates = tree_updates.clone(); cx.subscribe(&tree, move |_, _, event, _| { if let Event::UpdatedEntries(update) = event { tree_updates.lock().extend( update .iter() .map(|(path, _, change)| (path.clone(), *change)), ); } }) .detach(); }); // The symlinked directories are not scanned by default. tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_external)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new("deps"), false), (Path::new("deps/dep-dir2"), true), (Path::new("deps/dep-dir3"), true), (Path::new("src"), false), (Path::new("src/a.rs"), false), (Path::new("src/b.rs"), false), ] ); assert_eq!( tree.entry_for_path("deps/dep-dir2").unwrap().kind, EntryKind::UnloadedDir ); }); // Expand one of the symlinked directories. tree.read_with(cx, |tree, _| { tree.as_local() .unwrap() .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()]) }) .recv() .await; // The expanded directory's contents are loaded. Subdirectories are // not scanned yet. tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_external)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new("deps"), false), (Path::new("deps/dep-dir2"), true), (Path::new("deps/dep-dir3"), true), (Path::new("deps/dep-dir3/deps"), true), (Path::new("deps/dep-dir3/src"), true), (Path::new("src"), false), (Path::new("src/a.rs"), false), (Path::new("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (Path::new("deps/dep-dir3").into(), PathChange::Loaded), (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded), (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded) ] ); // Expand a subdirectory of one of the symlinked directories. tree.read_with(cx, |tree, _| { tree.as_local() .unwrap() .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()]) }) .recv() .await; // The expanded subdirectory's contents are loaded. tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_external)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new("deps"), false), (Path::new("deps/dep-dir2"), true), (Path::new("deps/dep-dir3"), true), (Path::new("deps/dep-dir3/deps"), true), (Path::new("deps/dep-dir3/src"), true), (Path::new("deps/dep-dir3/src/e.rs"), true), (Path::new("deps/dep-dir3/src/f.rs"), true), (Path::new("src"), false), (Path::new("src/a.rs"), false), (Path::new("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded), ( Path::new("deps/dep-dir3/src/e.rs").into(), PathChange::Loaded ), ( Path::new("deps/dep-dir3/src/f.rs").into(), PathChange::Loaded ) ] ); } #[cfg(target_os = "macos")] #[gpui::test] async fn test_renaming_case_only(cx: &mut TestAppContext) { cx.executor().allow_parking(); init_test(cx); const OLD_NAME: &str = "aaa.rs"; const NEW_NAME: &str = "AAA.rs"; let fs = Arc::new(RealFs::default()); let temp_root = temp_tree(json!({ OLD_NAME: "", })); let tree = Worktree::local( temp_root.path(), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![Path::new(""), Path::new(OLD_NAME)] ); }); fs.rename( &temp_root.path().join(OLD_NAME), &temp_root.path().join(NEW_NAME), fs::RenameOptions { overwrite: true, ignore_if_exists: true, }, ) .await .unwrap(); tree.flush_fs_events(cx).await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![Path::new(""), Path::new(NEW_NAME)] ); }); } #[gpui::test] async fn test_open_gitignored_files(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root", json!({ ".gitignore": "node_modules\n", "one": { "node_modules": { "a": { "a1.js": "a1", "a2.js": "a2", }, "b": { "b1.js": "b1", "b2.js": "b2", }, "c": { "c1.js": "c1", "c2.js": "c2", } }, }, "two": { "x.js": "", "y.js": "", }, }), ) .await; let tree = Worktree::local( Path::new("/root"), true, fs.clone(), Default::default(), &mut cx.to_async(), ) .await .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_ignored)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new(".gitignore"), false), (Path::new("one"), false), (Path::new("one/node_modules"), true), (Path::new("two"), false), (Path::new("two/x.js"), false), (Path::new("two/y.js"), false), ] ); }); // Open a file that is nested inside of a gitignored directory that // has not yet been expanded. let prev_read_dir_count = fs.read_dir_call_count(); let loaded = tree .update(cx, |tree, cx| { tree.load_file("one/node_modules/b/b1.js".as_ref(), cx) }) .await .unwrap(); tree.read_with(cx, |tree, _| { assert_eq!( tree.entries(true, 0) .map(|entry| (entry.path.as_ref(), entry.is_ignored)) .collect::<Vec<_>>(), vec![ (Path::new(""), false), (Path::new(".gitignore"), false), (Path::new("one"), false), (Path::new("one/node_modules"), true), (Path::new("one/node_modules/a"), true), (Path::new("one/node_modules/b"), true), (Path::new("one/node_modules/b/b1.js"), true), (Path::new("one/node_modules/b/b2.js"), true), (Path::new("one/node_modules/c"), true), (Path::new("two"), false), (Path::new("two/x.js"), false), (Path::new("two/y.js"), false), ] ); assert_eq!( loaded.file.path.as_ref(), Path::new("one/node_modules/b/b1.js") ); // Only the newly-expanded directories are scanned. assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2); }); // Open another file in a different subdirectory of the same // gitignored directory.