use crate::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};use anyhow::Result;use encoding_rs;use fs::{FakeFs, Fs, RealFs, RemoveOptions};use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};use parking_lot::Mutex;use postage::stream::Stream;use pretty_assertions::assert_eq;use rand::prelude::*; use serde_json::json;use settings::SettingsStore;use std::{ env, fmt::Write, mem, path::{Path, PathBuf}, sync::Arc,};use util::{ ResultExt, path, paths::PathStyle, rel_path::{RelPath, rel_path}, test::TempTree,}; #[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(), true, &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![ rel_path(""), rel_path(".gitignore"), rel_path("a"), rel_path("a/c"), ] ); assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ rel_path(""), rel_path(".gitignore"), rel_path("a"), rel_path("a/b"), rel_path("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(), true, &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![ rel_path(""), rel_path("lib"), rel_path("lib/a"), rel_path("lib/a/a.txt"), rel_path("lib/a/lib"), rel_path("lib/b"), rel_path("lib/b/b.txt"), rel_path("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![ rel_path(""), rel_path("lib"), rel_path("lib/a"), rel_path("lib/a/a.txt"), rel_path("lib/a/lib-2"), rel_path("lib/b"), rel_path("lib/b/b.txt"), rel_path("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(), true, &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![ (rel_path(""), false), (rel_path("deps"), false), (rel_path("deps/dep-dir2"), true), (rel_path("deps/dep-dir3"), true), (rel_path("src"), false), (rel_path("src/a.rs"), false), (rel_path("src/b.rs"), false), ] ); assert_eq!( tree.entry_for_path(rel_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![rel_path("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![ (rel_path(""), false), (rel_path("deps"), false), (rel_path("deps/dep-dir2"), true), (rel_path("deps/dep-dir3"), true), (rel_path("deps/dep-dir3/deps"), true), (rel_path("deps/dep-dir3/src"), true), (rel_path("src"), false), (rel_path("src/a.rs"), false), (rel_path("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (rel_path("deps/dep-dir3").into(), PathChange::Loaded), (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded), (rel_path("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![rel_path("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![ (rel_path(""), false), (rel_path("deps"), false), (rel_path("deps/dep-dir2"), true), (rel_path("deps/dep-dir3"), true), (rel_path("deps/dep-dir3/deps"), true), (rel_path("deps/dep-dir3/src"), true), (rel_path("deps/dep-dir3/src/e.rs"), true), (rel_path("deps/dep-dir3/src/f.rs"), true), (rel_path("src"), false), (rel_path("src/a.rs"), false), (rel_path("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded), ( rel_path("deps/dep-dir3/src/e.rs").into(), PathChange::Loaded ), ( rel_path("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::new(None, cx.executor())); let temp_root = TempTree::new(json!({ OLD_NAME: "", })); let tree = Worktree::local( temp_root.path(), true, fs.clone(), Default::default(), true, &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![rel_path(""), rel_path(OLD_NAME)] ); }); fs.rename( &temp_root.path().join(OLD_NAME), &temp_root.path().join(NEW_NAME), fs::RenameOptions { overwrite: true, ignore_if_exists: true, create_parents: false, }, ) .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![rel_path(""), rel_path(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(), true, &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![ (rel_path(""), false), (rel_path(".gitignore"), false), (rel_path("one"), false), (rel_path("one/node_modules"), true), (rel_path("two"), false), (rel_path("two/x.js"), false), (rel_path("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(rel_path("one/node_modules/b/b1.js"), 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![ (rel_path(""), false), (rel_path(".gitignore"), false), (rel_path("one"), false), (rel_path("one/node_modules"), true), (rel_path("one/node_modules/a"), true), (rel_path("one/node_modules/b"), true), (rel_path("one/node_modules/b/b1.js"), true), (rel_path("one/node_modules/b/b2.js"), true), (rel_path("one/node_modules/c"), true), (rel_path("two"), false), (rel_path("two/x.js"), false), (rel_path("two/y.js"), false), ] ); assert_eq!(Lines 1–500 of 2845. Load more
use crate::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle}; use anyhow::Result; use encoding_rs; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE}; use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext}; use parking_lot::Mutex; use postage::stream::Stream; use pretty_assertions::assert_eq; use rand::prelude::*; use serde_json::json; use settings::SettingsStore; use std::{ env, fmt::Write, mem, path::{Path, PathBuf}, sync::Arc, }; use util::{ ResultExt, path, paths::PathStyle, rel_path::{RelPath, rel_path}, test::TempTree, }; #[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(), true, &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![ rel_path(""), rel_path(".gitignore"), rel_path("a"), rel_path("a/c"), ] ); assert_eq!( tree.entries(true, 0) .map(|entry| entry.path.as_ref()) .collect::<Vec<_>>(), vec![ rel_path(""), rel_path(".gitignore"), rel_path("a"), rel_path("a/b"), rel_path("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(), true, &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![ rel_path(""), rel_path("lib"), rel_path("lib/a"), rel_path("lib/a/a.txt"), rel_path("lib/a/lib"), rel_path("lib/b"), rel_path("lib/b/b.txt"), rel_path("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![ rel_path(""), rel_path("lib"), rel_path("lib/a"), rel_path("lib/a/a.txt"), rel_path("lib/a/lib-2"), rel_path("lib/b"), rel_path("lib/b/b.txt"), rel_path("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(), true, &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![ (rel_path(""), false), (rel_path("deps"), false), (rel_path("deps/dep-dir2"), true), (rel_path("deps/dep-dir3"), true), (rel_path("src"), false), (rel_path("src/a.rs"), false), (rel_path("src/b.rs"), false), ] ); assert_eq!( tree.entry_for_path(rel_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![rel_path("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![ (rel_path(""), false), (rel_path("deps"), false), (rel_path("deps/dep-dir2"), true), (rel_path("deps/dep-dir3"), true), (rel_path("deps/dep-dir3/deps"), true), (rel_path("deps/dep-dir3/src"), true), (rel_path("src"), false), (rel_path("src/a.rs"), false), (rel_path("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (rel_path("deps/dep-dir3").into(), PathChange::Loaded), (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded), (rel_path("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![rel_path("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![ (rel_path(""), false), (rel_path("deps"), false), (rel_path("deps/dep-dir2"), true), (rel_path("deps/dep-dir3"), true), (rel_path("deps/dep-dir3/deps"), true), (rel_path("deps/dep-dir3/src"), true), (rel_path("deps/dep-dir3/src/e.rs"), true), (rel_path("deps/dep-dir3/src/f.rs"), true), (rel_path("src"), false), (rel_path("src/a.rs"), false), (rel_path("src/b.rs"), false), ] ); }); assert_eq!( mem::take(&mut *tree_updates.lock()), &[ (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded), ( rel_path("deps/dep-dir3/src/e.rs").into(), PathChange::Loaded ), ( rel_path("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::new(None, cx.executor())); let temp_root = TempTree::new(json!({ OLD_NAME: "", })); let tree = Worktree::local( temp_root.path(), true, fs.clone(), Default::default(), true, &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![rel_path(""), rel_path(OLD_NAME)] ); }); fs.rename( &temp_root.path().join(OLD_NAME), &temp_root.path().join(NEW_NAME), fs::RenameOptions { overwrite: true, ignore_if_exists: true, create_parents: false, }, ) .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![rel_path(""), rel_path(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(), true, &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![ (rel_path(""), false), (rel_path(".gitignore"), false), (rel_path("one"), false), (rel_path("one/node_modules"), true), (rel_path("two"), false), (rel_path("two/x.js"), false), (rel_path("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(rel_path("one/node_modules/b/b1.js"), 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![ (rel_path(""), false), (rel_path(".gitignore"), false), (rel_path("one"), false), (rel_path("one/node_modules"), true), (rel_path("one/node_modules/a"), true), (rel_path("one/node_modules/b"), true), (rel_path("one/node_modules/b/b1.js"), true), (rel_path("one/node_modules/b/b2.js"), true), (rel_path("one/node_modules/c"), true), (rel_path("two"), false), (rel_path("two/x.js"), false), (rel_path("two/y.js"), false), ] ); assert_eq!(