mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Merge pull request #7 from UserGeneratedLLC/vfs_exclusions
Enhance VFS watch functionality and introduce recording mode
This commit is contained in:
commit
f1992a61b1
9 changed files with 533 additions and 60 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -31,6 +31,18 @@ Making a new release? Simply add the new header with the version and date undern
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
* Add VFS recording mode and non-recursive watch support to reduce OS file-watch handles during `atlas serve` startup on large projects
|
||||||
|
* Enhance scripts-only mode to filter non-script files at the snapshot middleware, VFS event, and prefetch layers
|
||||||
|
* Set `core.longpaths` git config during repository initialization for Windows long-path support
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Full commit log</summary>
|
||||||
|
|
||||||
|
- `2ea1d493` Enhance VFS watch functionality and introduce recording mode
|
||||||
|
- `bfb5815f` Update TODO and enhance git configuration
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## [8.5.2] (March 3rd, 2026)
|
## [8.5.2] (March 3rd, 2026)
|
||||||
|
|
||||||
* Fix `atlas init` to commit project files before adding agent submodules, producing cleaner git history
|
* Fix `atlas init` to commit project files before adding agent submodules, producing cleaner git history
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ impl VfsBackend for InMemoryFs {
|
||||||
inner.event_receiver.clone()
|
inner.event_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
fn watch(&mut self, _path: &Path, _recursive: bool) -> io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ mod noop_backend;
|
||||||
mod snapshot;
|
mod snapshot;
|
||||||
mod std_backend;
|
mod std_backend;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
use std::{io, str};
|
use std::{io, str};
|
||||||
|
|
@ -94,7 +94,7 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
fn watch(&mut self, path: &Path, recursive: bool) -> io::Result<()>;
|
||||||
fn unwatch(&mut self, path: &Path) -> io::Result<()>;
|
fn unwatch(&mut self, path: &Path) -> io::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,17 +159,28 @@ pub enum VfsEvent {
|
||||||
struct VfsInner {
|
struct VfsInner {
|
||||||
backend: Box<dyn VfsBackend>,
|
backend: Box<dyn VfsBackend>,
|
||||||
watch_enabled: bool,
|
watch_enabled: bool,
|
||||||
|
watch_recursive: bool,
|
||||||
prefetch_cache: Option<PrefetchCache>,
|
prefetch_cache: Option<PrefetchCache>,
|
||||||
|
recorded_watch_paths: Option<HashSet<PathBuf>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VfsInner {
|
impl VfsInner {
|
||||||
|
fn watch_or_record(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
if let Some(ref mut recorded) = self.recorded_watch_paths {
|
||||||
|
recorded.insert(path.to_path_buf());
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.backend.watch(path, self.watch_recursive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read raw bytes from the prefetch cache or the backend.
|
/// Read raw bytes from the prefetch cache or the backend.
|
||||||
/// Removes the entry from the cache on hit to free memory.
|
/// Removes the entry from the cache on hit to free memory.
|
||||||
fn read_raw(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
fn read_raw(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||||
if let Some(cache) = &mut self.prefetch_cache {
|
if let Some(cache) = &mut self.prefetch_cache {
|
||||||
if let Some(contents) = cache.files.remove(path) {
|
if let Some(contents) = cache.files.remove(path) {
|
||||||
if self.watch_enabled {
|
if self.watch_enabled && self.watch_recursive {
|
||||||
self.backend.watch(path)?;
|
self.watch_or_record(path)?;
|
||||||
}
|
}
|
||||||
return Ok(contents);
|
return Ok(contents);
|
||||||
}
|
}
|
||||||
|
|
@ -177,8 +188,8 @@ impl VfsInner {
|
||||||
|
|
||||||
let contents = self.backend.read(path)?;
|
let contents = self.backend.read(path)?;
|
||||||
|
|
||||||
if self.watch_enabled {
|
if self.watch_enabled && self.watch_recursive {
|
||||||
self.backend.watch(path)?;
|
self.watch_or_record(path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(contents)
|
Ok(contents)
|
||||||
|
|
@ -220,7 +231,7 @@ impl VfsInner {
|
||||||
if let Some(cache) = &mut self.prefetch_cache {
|
if let Some(cache) = &mut self.prefetch_cache {
|
||||||
if let Some(child_paths) = cache.children.remove(path) {
|
if let Some(child_paths) = cache.children.remove(path) {
|
||||||
if self.watch_enabled {
|
if self.watch_enabled {
|
||||||
self.backend.watch(path)?;
|
self.watch_or_record(path)?;
|
||||||
}
|
}
|
||||||
let inner = child_paths.into_iter().map(|p| Ok(DirEntry { path: p }));
|
let inner = child_paths.into_iter().map(|p| Ok(DirEntry { path: p }));
|
||||||
return Ok(ReadDir {
|
return Ok(ReadDir {
|
||||||
|
|
@ -232,7 +243,7 @@ impl VfsInner {
|
||||||
let dir = self.backend.read_dir(path)?;
|
let dir = self.backend.read_dir(path)?;
|
||||||
|
|
||||||
if self.watch_enabled {
|
if self.watch_enabled {
|
||||||
self.backend.watch(path)?;
|
self.watch_or_record(path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(dir)
|
Ok(dir)
|
||||||
|
|
@ -345,7 +356,9 @@ impl Vfs {
|
||||||
let lock = VfsInner {
|
let lock = VfsInner {
|
||||||
backend: Box::new(backend),
|
backend: Box::new(backend),
|
||||||
watch_enabled: true,
|
watch_enabled: true,
|
||||||
|
watch_recursive: true,
|
||||||
prefetch_cache: None,
|
prefetch_cache: None,
|
||||||
|
recorded_watch_paths: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -388,6 +401,36 @@ impl Vfs {
|
||||||
inner.watch_enabled = enabled;
|
inner.watch_enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets whether new watches use recursive or non-recursive mode.
|
||||||
|
///
|
||||||
|
/// When false, each `backend.watch()` call only watches the specific
|
||||||
|
/// directory -- not its children. This allows selective watching where
|
||||||
|
/// only directories actually traversed during snapshot get OS handles.
|
||||||
|
pub fn set_watch_recursive(&self, recursive: bool) {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.watch_recursive = recursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begin recording mode: `read`/`read_dir` calls record touched paths
|
||||||
|
/// into an internal set instead of calling `backend.watch()`.
|
||||||
|
pub fn start_watch_recording(&self) {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.recorded_watch_paths = Some(HashSet::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End recording mode and return the set of paths that were touched.
|
||||||
|
pub fn take_recorded_paths(&self) -> Option<HashSet<PathBuf>> {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.recorded_watch_paths.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly watch a path using the current `watch_recursive` setting.
|
||||||
|
pub fn watch<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
let recursive = inner.watch_recursive;
|
||||||
|
inner.backend.watch(path.as_ref(), recursive)
|
||||||
|
}
|
||||||
|
|
||||||
/// Read a file from the VFS, or the underlying backend if it isn't
|
/// Read a file from the VFS, or the underlying backend if it isn't
|
||||||
/// resident.
|
/// resident.
|
||||||
///
|
///
|
||||||
|
|
@ -767,6 +810,50 @@ mod test {
|
||||||
assert_eq!(vfs.read("test").unwrap().as_slice(), b"data");
|
assert_eq!(vfs.read("test").unwrap().as_slice(), b"data");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recording_mode_collects_read_dir_paths() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/root",
|
||||||
|
VfsSnapshot::dir(HashMap::from([
|
||||||
|
("child.txt", VfsSnapshot::file("hello")),
|
||||||
|
(
|
||||||
|
"subdir",
|
||||||
|
VfsSnapshot::dir(HashMap::from([("nested.txt", VfsSnapshot::file("world"))])),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
vfs.start_watch_recording();
|
||||||
|
|
||||||
|
let _ = vfs.read_dir("/root").unwrap();
|
||||||
|
let _ = vfs.read_dir("/root/subdir").unwrap();
|
||||||
|
let _ = vfs.read("/root/child.txt").unwrap();
|
||||||
|
|
||||||
|
let recorded = vfs.take_recorded_paths().unwrap();
|
||||||
|
assert!(recorded.contains(&PathBuf::from("/root")));
|
||||||
|
assert!(recorded.contains(&PathBuf::from("/root/subdir")));
|
||||||
|
assert!(recorded.contains(&PathBuf::from("/root/child.txt")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recording_mode_returns_none_when_not_started() {
|
||||||
|
let imfs = InMemoryFs::new();
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
assert!(vfs.take_recorded_paths().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recording_mode_cleared_after_take() {
|
||||||
|
let imfs = InMemoryFs::new();
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
vfs.start_watch_recording();
|
||||||
|
let _ = vfs.take_recorded_paths();
|
||||||
|
assert!(vfs.take_recorded_paths().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prefetch_cache_watch_registered_on_cache_hit() {
|
fn prefetch_cache_watch_registered_on_cache_hit() {
|
||||||
let contents = "hello world".to_string();
|
let contents = "hello world".to_string();
|
||||||
|
|
@ -779,7 +866,6 @@ mod test {
|
||||||
cache_files.insert(file_path.clone(), contents.as_bytes().to_vec());
|
cache_files.insert(file_path.clone(), contents.as_bytes().to_vec());
|
||||||
vfs.set_prefetch_cache(PrefetchCache {
|
vfs.set_prefetch_cache(PrefetchCache {
|
||||||
files: cache_files,
|
files: cache_files,
|
||||||
canonical: HashMap::new(),
|
|
||||||
is_file: HashMap::new(),
|
is_file: HashMap::new(),
|
||||||
children: HashMap::new(),
|
children: HashMap::new(),
|
||||||
});
|
});
|
||||||
|
|
@ -825,7 +911,6 @@ mod test {
|
||||||
let vfs = Arc::new(Vfs::new(StdBackend::new_for_testing()));
|
let vfs = Arc::new(Vfs::new(StdBackend::new_for_testing()));
|
||||||
vfs.set_prefetch_cache(PrefetchCache {
|
vfs.set_prefetch_cache(PrefetchCache {
|
||||||
files: cache_files,
|
files: cache_files,
|
||||||
canonical: HashMap::new(),
|
|
||||||
is_file: HashMap::new(),
|
is_file: HashMap::new(),
|
||||||
children: HashMap::new(),
|
children: HashMap::new(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ impl VfsBackend for NoopBackend {
|
||||||
crossbeam_channel::never()
|
crossbeam_channel::never()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
fn watch(&mut self, _path: &Path, _recursive: bool) -> io::Result<()> {
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ pub struct StdBackend {
|
||||||
debouncer: Debouncer<notify::RecommendedWatcher, RecommendedCache>,
|
debouncer: Debouncer<notify::RecommendedWatcher, RecommendedCache>,
|
||||||
watcher_receiver: Receiver<VfsEvent>,
|
watcher_receiver: Receiver<VfsEvent>,
|
||||||
watches: HashSet<PathBuf>,
|
watches: HashSet<PathBuf>,
|
||||||
|
recursive_watches: HashSet<PathBuf>,
|
||||||
/// Receiver for critical errors from the watcher thread.
|
/// Receiver for critical errors from the watcher thread.
|
||||||
/// Callers should poll this to detect when watching becomes unreliable.
|
/// Callers should poll this to detect when watching becomes unreliable.
|
||||||
critical_error_receiver: Receiver<WatcherCriticalError>,
|
critical_error_receiver: Receiver<WatcherCriticalError>,
|
||||||
|
|
@ -113,6 +114,7 @@ impl StdBackend {
|
||||||
debouncer,
|
debouncer,
|
||||||
watcher_receiver: event_rx,
|
watcher_receiver: event_rx,
|
||||||
watches: HashSet::new(),
|
watches: HashSet::new(),
|
||||||
|
recursive_watches: HashSet::new(),
|
||||||
critical_error_receiver: error_rx,
|
critical_error_receiver: error_rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -328,26 +330,47 @@ impl VfsBackend for StdBackend {
|
||||||
self.watcher_receiver.clone()
|
self.watcher_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
fn watch(&mut self, path: &Path, recursive: bool) -> io::Result<()> {
|
||||||
if self.watches.contains(path)
|
if self.watches.contains(path) {
|
||||||
|| path
|
if !recursive || self.recursive_watches.contains(path) {
|
||||||
.ancestors()
|
return Ok(());
|
||||||
.any(|ancestor| self.watches.contains(ancestor))
|
}
|
||||||
|
// Upgrade: path was watched non-recursively, now needs recursive.
|
||||||
|
// Fall through to re-issue the watch with RecursiveMode::Recursive.
|
||||||
|
}
|
||||||
|
if path
|
||||||
|
.ancestors()
|
||||||
|
.skip(1)
|
||||||
|
.any(|ancestor| self.recursive_watches.contains(ancestor))
|
||||||
{
|
{
|
||||||
Ok(())
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode = if recursive {
|
||||||
|
RecursiveMode::Recursive
|
||||||
} else {
|
} else {
|
||||||
// Only add to watches AFTER the watch succeeds
|
RecursiveMode::NonRecursive
|
||||||
// This prevents a failed watch from permanently marking the path as "watched"
|
};
|
||||||
match self.debouncer.watch(path, RecursiveMode::Recursive) {
|
match self.debouncer.watch(path, mode) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::debug!("Watching path: {}", path.display());
|
log::debug!(
|
||||||
self.watches.insert(path.to_path_buf());
|
"Watching path ({}): {}",
|
||||||
Ok(())
|
if recursive {
|
||||||
}
|
"recursive"
|
||||||
Err(err) => {
|
} else {
|
||||||
log::warn!("Failed to watch path {}: {:?}", path.display(), err);
|
"non-recursive"
|
||||||
Err(io::Error::other(format!("{:?}", err)))
|
},
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
self.watches.insert(path.to_path_buf());
|
||||||
|
if recursive {
|
||||||
|
self.recursive_watches.insert(path.to_path_buf());
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("Failed to watch path {}: {:?}", path.display(), err);
|
||||||
|
Err(io::Error::other(format!("{:?}", err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -366,6 +389,7 @@ impl VfsBackend for StdBackend {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.watches.remove(path);
|
self.watches.remove(path);
|
||||||
|
self.recursive_watches.remove(path);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
@ -382,6 +406,7 @@ impl VfsBackend for StdBackend {
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
self.watches.remove(path);
|
self.watches.remove(path);
|
||||||
|
self.recursive_watches.remove(path);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
log::warn!("Failed to unwatch path {}: {:?}", path.display(), err);
|
log::warn!("Failed to unwatch path {}: {:?}", path.display(), err);
|
||||||
|
|
@ -414,17 +439,17 @@ mod tests {
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
|
|
||||||
// Watch should succeed and add to internal set
|
// Watch should succeed and add to internal set
|
||||||
assert!(backend.watch(&file_path).is_ok());
|
assert!(backend.watch(&file_path, true).is_ok());
|
||||||
|
|
||||||
// Watching again should be a no-op (already watched)
|
// Watching again should be a no-op (already watched)
|
||||||
assert!(backend.watch(&file_path).is_ok());
|
assert!(backend.watch(&file_path, true).is_ok());
|
||||||
|
|
||||||
// Watch a non-existent path - behavior varies by platform
|
// Watch a non-existent path - behavior varies by platform
|
||||||
// On some systems, watching a non-existent file might succeed
|
// On some systems, watching a non-existent file might succeed
|
||||||
// because notify watches the parent directory. We just verify
|
// because notify watches the parent directory. We just verify
|
||||||
// that calling watch doesn't panic or corrupt state.
|
// that calling watch doesn't panic or corrupt state.
|
||||||
let nonexistent = dir.path().join("nonexistent.txt");
|
let nonexistent = dir.path().join("nonexistent.txt");
|
||||||
let _ = backend.watch(&nonexistent); // Result varies by platform
|
let _ = backend.watch(&nonexistent, true); // Result varies by platform
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -451,13 +476,13 @@ mod tests {
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
|
|
||||||
// Watch the file
|
// Watch the file
|
||||||
assert!(backend.watch(&file_path).is_ok());
|
assert!(backend.watch(&file_path, true).is_ok());
|
||||||
|
|
||||||
// Unwatch should succeed
|
// Unwatch should succeed
|
||||||
assert!(backend.unwatch(&file_path).is_ok());
|
assert!(backend.unwatch(&file_path).is_ok());
|
||||||
|
|
||||||
// Should be able to watch again after unwatch
|
// Should be able to watch again after unwatch
|
||||||
assert!(backend.watch(&file_path).is_ok());
|
assert!(backend.watch(&file_path, true).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -471,10 +496,43 @@ mod tests {
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
|
|
||||||
// Watch the parent directory
|
// Watch the parent directory
|
||||||
assert!(backend.watch(&subdir).is_ok());
|
assert!(backend.watch(&subdir, true).is_ok());
|
||||||
|
|
||||||
// Watching a file inside should be a no-op (covered by parent)
|
// Watching a file inside should be a no-op (covered by parent)
|
||||||
assert!(backend.watch(&file_path).is_ok());
|
assert!(backend.watch(&file_path, true).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_recursive_watch_upgrades_to_recursive() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let subdir = dir.path().join("subdir");
|
||||||
|
fs_err::create_dir(&subdir).unwrap();
|
||||||
|
let child = subdir.join("child.txt");
|
||||||
|
fs_err::write(&child, "content").unwrap();
|
||||||
|
|
||||||
|
let mut backend = StdBackend::new_for_testing();
|
||||||
|
|
||||||
|
// Watch non-recursively first
|
||||||
|
assert!(backend.watch(&subdir, false).is_ok());
|
||||||
|
assert!(backend.watches.contains(subdir.as_path()));
|
||||||
|
assert!(!backend.recursive_watches.contains(subdir.as_path()));
|
||||||
|
|
||||||
|
// Child should NOT be covered (parent is non-recursive)
|
||||||
|
assert!(!subdir
|
||||||
|
.ancestors()
|
||||||
|
.skip(1)
|
||||||
|
.any(|a| backend.recursive_watches.contains(a)));
|
||||||
|
|
||||||
|
// Upgrade to recursive
|
||||||
|
assert!(backend.watch(&subdir, true).is_ok());
|
||||||
|
assert!(backend.recursive_watches.contains(subdir.as_path()));
|
||||||
|
|
||||||
|
// Now a child path should be recognized as covered by the recursive ancestor
|
||||||
|
assert!(child
|
||||||
|
.ancestors()
|
||||||
|
.skip(1)
|
||||||
|
.any(|a| backend.recursive_watches.contains(a)));
|
||||||
|
assert!(backend.watch(&child, true).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -504,12 +562,12 @@ mod tests {
|
||||||
|
|
||||||
// Rapidly cycle watch/unwatch to stress test state consistency
|
// Rapidly cycle watch/unwatch to stress test state consistency
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
assert!(backend.watch(&file_path).is_ok());
|
assert!(backend.watch(&file_path, true).is_ok());
|
||||||
assert!(backend.unwatch(&file_path).is_ok());
|
assert!(backend.unwatch(&file_path).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final watch should still work
|
// Final watch should still work
|
||||||
assert!(backend.watch(&file_path).is_ok());
|
assert!(backend.watch(&file_path, true).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -522,7 +580,7 @@ mod tests {
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
|
|
||||||
// Watch the directory (more reliable than watching the file directly)
|
// Watch the directory (more reliable than watching the file directly)
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
|
|
||||||
// Give the watcher time to start
|
// Give the watcher time to start
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
@ -597,7 +655,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Rapid modifications - 20 writes in quick succession
|
// Rapid modifications - 20 writes in quick succession
|
||||||
|
|
@ -635,7 +693,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// 10 cycles of create/delete
|
// 10 cycles of create/delete
|
||||||
|
|
@ -670,7 +728,7 @@ mod tests {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Create 20 files rapidly
|
// Create 20 files rapidly
|
||||||
|
|
@ -722,7 +780,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Rename the file
|
// Rename the file
|
||||||
|
|
@ -758,7 +816,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Chain of renames
|
// Chain of renames
|
||||||
|
|
@ -781,7 +839,7 @@ mod tests {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Create nested structure
|
// Create nested structure
|
||||||
|
|
@ -826,7 +884,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// 100 writes with no delay
|
// 100 writes with no delay
|
||||||
|
|
@ -863,7 +921,7 @@ mod tests {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
let files: Vec<_> = (0..5)
|
let files: Vec<_> = (0..5)
|
||||||
|
|
@ -915,7 +973,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Simulate multiple undo/redo cycles
|
// Simulate multiple undo/redo cycles
|
||||||
|
|
@ -969,7 +1027,7 @@ mod tests {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Rapidly create subdirectories with files
|
// Rapidly create subdirectories with files
|
||||||
|
|
@ -1005,7 +1063,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
// Create empty file
|
// Create empty file
|
||||||
|
|
@ -1041,7 +1099,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
fs_err::write(&file_path, "-- long name test").unwrap();
|
fs_err::write(&file_path, "-- long name test").unwrap();
|
||||||
|
|
@ -1067,7 +1125,7 @@ mod tests {
|
||||||
|
|
||||||
let mut backend = StdBackend::new_for_testing();
|
let mut backend = StdBackend::new_for_testing();
|
||||||
let event_rx = backend.event_receiver();
|
let event_rx = backend.event_receiver();
|
||||||
assert!(backend.watch(dir.path()).is_ok());
|
assert!(backend.watch(dir.path(), true).is_ok());
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
for name in &special_files {
|
for name in &special_files {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use crate::{
|
||||||
snapshot::{
|
snapshot::{
|
||||||
apply_patch_set, compute_patch_set, AppliedPatchSet, InstigatingSource, PatchSet, RojoTree,
|
apply_patch_set, compute_patch_set, AppliedPatchSet, InstigatingSource, PatchSet, RojoTree,
|
||||||
},
|
},
|
||||||
snapshot_middleware::{snapshot_from_vfs, snapshot_project_node},
|
snapshot_middleware::{is_script_relevant_path, snapshot_from_vfs, snapshot_project_node},
|
||||||
syncback::{
|
syncback::{
|
||||||
dedup_suffix::{compute_cleanup_action, parse_dedup_suffix, DedupCleanupAction},
|
dedup_suffix::{compute_cleanup_action, parse_dedup_suffix, DedupCleanupAction},
|
||||||
deduplicate_name, name_needs_slugify, slugify_name, strip_script_suffix,
|
deduplicate_name, name_needs_slugify, slugify_name, strip_script_suffix,
|
||||||
|
|
@ -92,6 +92,7 @@ impl ChangeProcessor {
|
||||||
project_file_path: PathBuf,
|
project_file_path: PathBuf,
|
||||||
critical_error_receiver: Option<Receiver<memofs::WatcherCriticalError>>,
|
critical_error_receiver: Option<Receiver<memofs::WatcherCriticalError>>,
|
||||||
git_repo_root: Option<PathBuf>,
|
git_repo_root: Option<PathBuf>,
|
||||||
|
sync_scripts_only: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (shutdown_sender, shutdown_receiver) = crossbeam_channel::bounded(1);
|
let (shutdown_sender, shutdown_receiver) = crossbeam_channel::bounded(1);
|
||||||
let vfs_receiver = vfs.event_receiver();
|
let vfs_receiver = vfs.event_receiver();
|
||||||
|
|
@ -109,6 +110,7 @@ impl ChangeProcessor {
|
||||||
project_file_path,
|
project_file_path,
|
||||||
ref_path_index,
|
ref_path_index,
|
||||||
git_repo_root,
|
git_repo_root,
|
||||||
|
sync_scripts_only,
|
||||||
};
|
};
|
||||||
|
|
||||||
let job_thread = jod_thread::Builder::new()
|
let job_thread = jod_thread::Builder::new()
|
||||||
|
|
@ -268,6 +270,9 @@ struct JobThreadContext {
|
||||||
/// Git repository root, if the project is in a git repo.
|
/// Git repository root, if the project is in a git repo.
|
||||||
/// Used for auto-staging Source writes.
|
/// Used for auto-staging Source writes.
|
||||||
git_repo_root: Option<PathBuf>,
|
git_repo_root: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// When true, only script-related VFS events are processed.
|
||||||
|
sync_scripts_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JobThreadContext {
|
impl JobThreadContext {
|
||||||
|
|
@ -723,6 +728,24 @@ impl JobThreadContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.sync_scripts_only {
|
||||||
|
if let Some(ref path) = event_path {
|
||||||
|
if path.is_file() {
|
||||||
|
let not_in_tree = {
|
||||||
|
let tree = self.tree.lock().unwrap();
|
||||||
|
tree.get_ids_at_path(path).is_empty()
|
||||||
|
};
|
||||||
|
if not_in_tree && !is_script_relevant_path(path) {
|
||||||
|
log::trace!(
|
||||||
|
"VFS event SKIPPED (scripts-only, non-script file): {}",
|
||||||
|
self.display_path(path)
|
||||||
|
);
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For a given VFS event, we might have many changes to different parts
|
// For a given VFS event, we might have many changes to different parts
|
||||||
// of the tree. Calculate and apply all of these changes.
|
// of the tree. Calculate and apply all of these changes.
|
||||||
let applied_patches = match event {
|
let applied_patches = match event {
|
||||||
|
|
@ -890,7 +913,8 @@ impl JobThreadContext {
|
||||||
use crate::snapshot::InstanceContext;
|
use crate::snapshot::InstanceContext;
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let instance_context = InstanceContext::new();
|
let mut instance_context = InstanceContext::new();
|
||||||
|
instance_context.sync_scripts_only = self.sync_scripts_only;
|
||||||
|
|
||||||
let snapshot =
|
let snapshot =
|
||||||
match snapshot_from_vfs(&instance_context, &self.vfs, &self.project_file_path) {
|
match snapshot_from_vfs(&instance_context, &self.vfs, &self.project_file_path) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use crate::{
|
||||||
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
|
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
|
||||||
PatchSet, RojoTree,
|
PatchSet, RojoTree,
|
||||||
},
|
},
|
||||||
snapshot_middleware::snapshot_from_vfs,
|
snapshot_middleware::{is_script_relevant_path, snapshot_from_vfs},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Set to `true` to validate on plugin connect (useful for testing, do not enable on production).
|
/// Set to `true` to validate on plugin connect (useful for testing, do not enable on production).
|
||||||
|
|
@ -131,7 +131,7 @@ pub struct ServeSession {
|
||||||
|
|
||||||
/// Collect all filesystem paths reachable from the project tree's `$path`
|
/// Collect all filesystem paths reachable from the project tree's `$path`
|
||||||
/// entries, then read file contents in parallel.
|
/// entries, then read file contents in parallel.
|
||||||
fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
|
fn prefetch_project_files(project: &Project, sync_scripts_only: bool) -> io::Result<PrefetchCache> {
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
@ -200,6 +200,7 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
|
||||||
let file_data: Vec<_> = entries
|
let file_data: Vec<_> = entries
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter(|e| e.file_type().is_file())
|
.filter(|e| e.file_type().is_file())
|
||||||
|
.filter(|e| !sync_scripts_only || is_script_relevant_path(e.path()))
|
||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
let path = e.path().to_path_buf();
|
let path = e.path().to_path_buf();
|
||||||
std::fs::read(&path).ok().map(|c| (path, c))
|
std::fs::read(&path).ok().map(|c| (path, c))
|
||||||
|
|
@ -266,10 +267,11 @@ impl ServeSession {
|
||||||
log::trace!("Starting new ServeSession at path {}", start_path.display());
|
log::trace!("Starting new ServeSession at path {}", start_path.display());
|
||||||
|
|
||||||
let root_project = Project::load_initial_project(vfs, start_path)?;
|
let root_project = Project::load_initial_project(vfs, start_path)?;
|
||||||
|
let sync_scripts_only = root_project.sync_scripts_only.unwrap_or(false);
|
||||||
|
|
||||||
if std::env::var("ATLAS_SEQUENTIAL").is_err() {
|
if std::env::var("ATLAS_SEQUENTIAL").is_err() {
|
||||||
let prefetch_start = Instant::now();
|
let prefetch_start = Instant::now();
|
||||||
match prefetch_project_files(&root_project) {
|
match prefetch_project_files(&root_project, sync_scripts_only) {
|
||||||
Ok(cache) => {
|
Ok(cache) => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Prefetch total: {} files in {:.1?}",
|
"Prefetch total: {} files in {:.1?}",
|
||||||
|
|
@ -286,7 +288,25 @@ impl ServeSession {
|
||||||
|
|
||||||
let mut tree = RojoTree::new(InstanceSnapshot::new());
|
let mut tree = RojoTree::new(InstanceSnapshot::new());
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
let instance_context = InstanceContext::new();
|
let mut instance_context = InstanceContext::new();
|
||||||
|
instance_context.sync_scripts_only = sync_scripts_only;
|
||||||
|
|
||||||
|
let watch_start = Instant::now();
|
||||||
|
let mut path_roots = Vec::new();
|
||||||
|
collect_path_roots(
|
||||||
|
&root_project.tree,
|
||||||
|
root_project.folder_location(),
|
||||||
|
&mut path_roots,
|
||||||
|
);
|
||||||
|
let _ = vfs.watch(root_project.folder_location());
|
||||||
|
for root in &path_roots {
|
||||||
|
let _ = vfs.watch(root);
|
||||||
|
}
|
||||||
|
log::debug!(
|
||||||
|
"Set up {} recursive watches in {:.1?}",
|
||||||
|
path_roots.len() + 1,
|
||||||
|
watch_start.elapsed(),
|
||||||
|
);
|
||||||
|
|
||||||
let snap_start = Instant::now();
|
let snap_start = Instant::now();
|
||||||
log::trace!("Generating snapshot of instances from VFS");
|
log::trace!("Generating snapshot of instances from VFS");
|
||||||
|
|
@ -356,6 +376,7 @@ impl ServeSession {
|
||||||
root_project.file_location.clone(),
|
root_project.file_location.clone(),
|
||||||
critical_error_receiver,
|
critical_error_receiver,
|
||||||
git_repo_root.clone(),
|
git_repo_root.clone(),
|
||||||
|
root_project.sync_scripts_only.unwrap_or(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
@ -526,7 +547,8 @@ impl ServeSession {
|
||||||
pub fn check_tree_freshness(&self) -> TreeFreshnessReport {
|
pub fn check_tree_freshness(&self) -> TreeFreshnessReport {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let start_path: &Path = &self.root_project.file_location;
|
let start_path: &Path = &self.root_project.file_location;
|
||||||
let instance_context = InstanceContext::new();
|
let mut instance_context = InstanceContext::new();
|
||||||
|
instance_context.sync_scripts_only = self.sync_scripts_only();
|
||||||
|
|
||||||
let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, start_path) {
|
let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, start_path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
|
@ -589,7 +611,8 @@ impl ServeSession {
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let start_path: &Path = &self.root_project.file_location;
|
let start_path: &Path = &self.root_project.file_location;
|
||||||
let instance_context = InstanceContext::new();
|
let mut instance_context = InstanceContext::new();
|
||||||
|
instance_context.sync_scripts_only = self.sync_scripts_only();
|
||||||
|
|
||||||
let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, start_path) {
|
let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, start_path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,8 @@ pub struct InstanceContext {
|
||||||
pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
|
pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub sync_rules: Vec<SyncRule>,
|
pub sync_rules: Vec<SyncRule>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub sync_scripts_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceContext {
|
impl InstanceContext {
|
||||||
|
|
@ -162,6 +164,7 @@ impl InstanceContext {
|
||||||
Self {
|
Self {
|
||||||
path_ignore_rules: Arc::new(Vec::new()),
|
path_ignore_rules: Arc::new(Vec::new()),
|
||||||
sync_rules: Vec::new(),
|
sync_rules: Vec::new(),
|
||||||
|
sync_scripts_only: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,13 @@ pub fn snapshot_from_vfs(
|
||||||
// If and when we do, make sure to go support it in
|
// If and when we do, make sure to go support it in
|
||||||
// `Project::set_file_name`, as right now it special-cases
|
// `Project::set_file_name`, as right now it special-cases
|
||||||
// `default.project.json5` as an `init` path.
|
// `default.project.json5` as an `init` path.
|
||||||
|
if context.sync_scripts_only
|
||||||
|
&& !middleware.is_script()
|
||||||
|
&& middleware != Middleware::Dir
|
||||||
|
&& middleware != Middleware::Project
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
match middleware {
|
match middleware {
|
||||||
Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name),
|
Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name),
|
||||||
_ => middleware.snapshot(context, vfs, &init_path, dir_name),
|
_ => middleware.snapshot(context, vfs, &init_path, dir_name),
|
||||||
|
|
@ -168,11 +175,23 @@ fn snapshot_from_path(
|
||||||
// name is needed (e.g. for names with special chars), it comes from the
|
// name is needed (e.g. for names with special chars), it comes from the
|
||||||
// `name` field in adjacent .meta.json / .model.json files.
|
// `name` field in adjacent .meta.json / .model.json files.
|
||||||
if let Some(rule) = context.get_user_sync_rule(path) {
|
if let Some(rule) = context.get_user_sync_rule(path) {
|
||||||
|
if context.sync_scripts_only
|
||||||
|
&& !rule.middleware.is_script()
|
||||||
|
&& rule.middleware != Middleware::Project
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
let name = rule.file_name_for_path(path)?;
|
let name = rule.file_name_for_path(path)?;
|
||||||
return rule.middleware.snapshot(context, vfs, path, name);
|
return rule.middleware.snapshot(context, vfs, path, name);
|
||||||
} else {
|
} else {
|
||||||
for rule in default_sync_rules() {
|
for rule in default_sync_rules() {
|
||||||
if rule.matches(path) {
|
if rule.matches(path) {
|
||||||
|
if context.sync_scripts_only
|
||||||
|
&& !rule.middleware.is_script()
|
||||||
|
&& rule.middleware != Middleware::Project
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
let name = rule.file_name_for_path(path)?;
|
let name = rule.file_name_for_path(path)?;
|
||||||
return rule.middleware.snapshot(context, vfs, path, name);
|
return rule.middleware.snapshot(context, vfs, path, name);
|
||||||
}
|
}
|
||||||
|
|
@ -319,6 +338,27 @@ impl Middleware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether this middleware produces a script instance
|
||||||
|
/// (Script, LocalScript, or ModuleScript).
|
||||||
|
#[inline]
|
||||||
|
pub fn is_script(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::ServerScript
|
||||||
|
| Self::ClientScript
|
||||||
|
| Self::ModuleScript
|
||||||
|
| Self::PluginScript
|
||||||
|
| Self::LocalScript
|
||||||
|
| Self::LegacyScript
|
||||||
|
| Self::ServerScriptDir
|
||||||
|
| Self::ClientScriptDir
|
||||||
|
| Self::ModuleScriptDir
|
||||||
|
| Self::PluginScriptDir
|
||||||
|
| Self::LocalScriptDir
|
||||||
|
| Self::LegacyScriptDir
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns whether this particular middleware would become a directory.
|
/// Returns whether this particular middleware would become a directory.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_dir(&self) -> bool {
|
pub fn is_dir(&self) -> bool {
|
||||||
|
|
@ -454,3 +494,231 @@ pub fn default_sync_rules() -> &'static [SyncRule] {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether a filesystem path is relevant in scripts-only mode.
|
||||||
|
///
|
||||||
|
/// Matches script files (`.luau`, `.lua`), meta files (`.meta.json5`,
|
||||||
|
/// `.meta.json`), and project files (`.project.json5`, `.project.json`).
|
||||||
|
pub fn is_script_relevant_path(path: &Path) -> bool {
|
||||||
|
let name = match path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
Some(n) => n.to_lowercase(),
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
name.ends_with(".luau")
|
||||||
|
|| name.ends_with(".lua")
|
||||||
|
|| name.ends_with(".meta.json5")
|
||||||
|
|| name.ends_with(".meta.json")
|
||||||
|
|| name.ends_with(".project.json5")
|
||||||
|
|| name.ends_with(".project.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use memofs::{InMemoryFs, VfsSnapshot};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_script_covers_all_script_types() {
|
||||||
|
assert!(Middleware::ServerScript.is_script());
|
||||||
|
assert!(Middleware::ClientScript.is_script());
|
||||||
|
assert!(Middleware::ModuleScript.is_script());
|
||||||
|
assert!(Middleware::PluginScript.is_script());
|
||||||
|
assert!(Middleware::LocalScript.is_script());
|
||||||
|
assert!(Middleware::LegacyScript.is_script());
|
||||||
|
assert!(Middleware::ServerScriptDir.is_script());
|
||||||
|
assert!(Middleware::ClientScriptDir.is_script());
|
||||||
|
assert!(Middleware::ModuleScriptDir.is_script());
|
||||||
|
assert!(Middleware::PluginScriptDir.is_script());
|
||||||
|
assert!(Middleware::LocalScriptDir.is_script());
|
||||||
|
assert!(Middleware::LegacyScriptDir.is_script());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_script_excludes_non_scripts() {
|
||||||
|
assert!(!Middleware::Dir.is_script());
|
||||||
|
assert!(!Middleware::Project.is_script());
|
||||||
|
assert!(!Middleware::Csv.is_script());
|
||||||
|
assert!(!Middleware::JsonModel.is_script());
|
||||||
|
assert!(!Middleware::Json.is_script());
|
||||||
|
assert!(!Middleware::Rbxm.is_script());
|
||||||
|
assert!(!Middleware::Rbxmx.is_script());
|
||||||
|
assert!(!Middleware::Toml.is_script());
|
||||||
|
assert!(!Middleware::Text.is_script());
|
||||||
|
assert!(!Middleware::Yaml.is_script());
|
||||||
|
assert!(!Middleware::Ignore.is_script());
|
||||||
|
assert!(!Middleware::CsvDir.is_script());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scripts_only_skips_non_script_file() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/project",
|
||||||
|
VfsSnapshot::dir(HashMap::from([
|
||||||
|
("data.json5", VfsSnapshot::file("{}")),
|
||||||
|
("script.server.luau", VfsSnapshot::file("print('hi')")),
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
let mut context = InstanceContext::new();
|
||||||
|
context.sync_scripts_only = true;
|
||||||
|
|
||||||
|
let json_result =
|
||||||
|
snapshot_from_vfs(&context, &vfs, Path::new("/project/data.json5")).unwrap();
|
||||||
|
assert!(json_result.is_none());
|
||||||
|
|
||||||
|
let script_result =
|
||||||
|
snapshot_from_vfs(&context, &vfs, Path::new("/project/script.server.luau")).unwrap();
|
||||||
|
assert!(script_result.is_some());
|
||||||
|
assert_eq!(script_result.unwrap().class_name.as_str(), "Script");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scripts_only_preserves_directories() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/project",
|
||||||
|
VfsSnapshot::dir(HashMap::from([(
|
||||||
|
"models",
|
||||||
|
VfsSnapshot::dir(HashMap::from([(
|
||||||
|
"part.rbxm",
|
||||||
|
VfsSnapshot::file(b"\x00".as_ref()),
|
||||||
|
)])),
|
||||||
|
)])),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
let mut context = InstanceContext::new();
|
||||||
|
context.sync_scripts_only = true;
|
||||||
|
|
||||||
|
let result = snapshot_from_vfs(&context, &vfs, Path::new("/project/models")).unwrap();
|
||||||
|
assert!(result.is_some());
|
||||||
|
let snapshot = result.unwrap();
|
||||||
|
assert_eq!(snapshot.class_name.as_str(), "Folder");
|
||||||
|
assert!(snapshot.children.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scripts_only_allows_project_files() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/nested.project.json5",
|
||||||
|
VfsSnapshot::file(r#"{"name": "Nested", "tree": {"$className": "Folder"}}"#),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
let mut context = InstanceContext::new();
|
||||||
|
context.sync_scripts_only = true;
|
||||||
|
|
||||||
|
let result = snapshot_from_vfs(&context, &vfs, Path::new("/nested.project.json5")).unwrap();
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_mode_includes_all_files() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/project",
|
||||||
|
VfsSnapshot::dir(HashMap::from([
|
||||||
|
("data.json5", VfsSnapshot::file("{}")),
|
||||||
|
("script.server.luau", VfsSnapshot::file("print('hi')")),
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
let context = InstanceContext::new();
|
||||||
|
|
||||||
|
let json_result =
|
||||||
|
snapshot_from_vfs(&context, &vfs, Path::new("/project/data.json5")).unwrap();
|
||||||
|
assert!(json_result.is_some());
|
||||||
|
|
||||||
|
let script_result =
|
||||||
|
snapshot_from_vfs(&context, &vfs, Path::new("/project/script.server.luau")).unwrap();
|
||||||
|
assert!(script_result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_script_relevant_path_accepts_scripts_and_meta() {
|
||||||
|
assert!(is_script_relevant_path(Path::new("/src/main.luau")));
|
||||||
|
assert!(is_script_relevant_path(Path::new("/src/init.server.luau")));
|
||||||
|
assert!(is_script_relevant_path(Path::new("/src/old.lua")));
|
||||||
|
assert!(is_script_relevant_path(Path::new("/src/old.server.lua")));
|
||||||
|
assert!(is_script_relevant_path(Path::new("/src/file.meta.json5")));
|
||||||
|
assert!(is_script_relevant_path(Path::new("/src/file.meta.json")));
|
||||||
|
assert!(is_script_relevant_path(Path::new("/nested.project.json5")));
|
||||||
|
assert!(is_script_relevant_path(Path::new("/nested.project.json")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_script_relevant_path_rejects_non_scripts() {
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/data.json5")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/model.rbxm")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/notes.txt")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/table.csv")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/config.toml")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/data.yaml")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/model.rbxmx")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src")));
|
||||||
|
assert!(!is_script_relevant_path(Path::new("/src/no_ext")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scripts_only_filters_csv_dir() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/project",
|
||||||
|
VfsSnapshot::dir(HashMap::from([(
|
||||||
|
"localization",
|
||||||
|
VfsSnapshot::dir(HashMap::from([(
|
||||||
|
"init.csv",
|
||||||
|
VfsSnapshot::file("Key,Source,Example\nkey1,Hello,Hello"),
|
||||||
|
)])),
|
||||||
|
)])),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
let mut context = InstanceContext::new();
|
||||||
|
context.sync_scripts_only = true;
|
||||||
|
|
||||||
|
let result = snapshot_from_vfs(&context, &vfs, Path::new("/project/localization")).unwrap();
|
||||||
|
assert!(
|
||||||
|
result.is_none(),
|
||||||
|
"CsvDir should be filtered in scripts-only mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scripts_only_preserves_script_dir() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot(
|
||||||
|
"/project",
|
||||||
|
VfsSnapshot::dir(HashMap::from([(
|
||||||
|
"MyScript",
|
||||||
|
VfsSnapshot::dir(HashMap::from([(
|
||||||
|
"init.server.luau",
|
||||||
|
VfsSnapshot::file("print('hello')"),
|
||||||
|
)])),
|
||||||
|
)])),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
let mut context = InstanceContext::new();
|
||||||
|
context.sync_scripts_only = true;
|
||||||
|
|
||||||
|
let result = snapshot_from_vfs(&context, &vfs, Path::new("/project/MyScript")).unwrap();
|
||||||
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"Script dir should pass through in scripts-only mode"
|
||||||
|
);
|
||||||
|
assert_eq!(result.unwrap().class_name.as_str(), "Script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue