diff --git a/src/snapshot_middleware/rbxm.rs b/src/snapshot_middleware/rbxm.rs index f3693d52..2060936c 100644 --- a/src/snapshot_middleware/rbxm.rs +++ b/src/snapshot_middleware/rbxm.rs @@ -2,10 +2,12 @@ use std::path::Path; use anyhow::Context; use memofs::Vfs; +use rbx_dom_weak::{types::Ref, InstanceBuilder, WeakDom}; use crate::{ + glob::Glob, snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, - syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, + syncback::{inst_path, FsSnapshot, SyncbackReturn, SyncbackSnapshot}, }; #[profiling::function] @@ -46,12 +48,26 @@ pub fn syncback_rbxm<'sync>( snapshot: &SyncbackSnapshot<'sync>, ) -> anyhow::Result> { let inst = snapshot.new_inst(); + let tree_globs = snapshot.compile_tree_globs(); - // Long-term, we probably want to have some logic for if this contains a - // script. That's a future endeavor though. - let mut serialized = Vec::new(); - rbx_binary::to_writer(&mut serialized, snapshot.new_tree(), &[inst.referent()]) - .context("failed to serialize new rbxm")?; + // If we have ignoreTrees patterns, filter the tree before serialization + let serialized = if tree_globs.is_empty() { + let mut serialized = Vec::new(); + rbx_binary::to_writer(&mut serialized, snapshot.new_tree(), &[inst.referent()]) + .context("failed to serialize new rbxm")?; + serialized + } else { + // Clone the subtree, filtering out ignored instances + let filtered_tree = clone_tree_filtered( + snapshot.new_tree(), + inst.referent(), + &tree_globs, + ); + let mut serialized = Vec::new(); + rbx_binary::to_writer(&mut serialized, &filtered_tree, &[filtered_tree.root_ref()]) + .context("failed to serialize filtered rbxm")?; + serialized + }; Ok(SyncbackReturn { fs_snapshot: FsSnapshot::new().with_added_file(&snapshot.path, serialized), @@ -60,6 +76,55 @@ pub fn syncback_rbxm<'sync>( }) } +/// Clones a subtree from the source WeakDom, filtering out instances that match +/// the provided ignoreTrees glob patterns. +pub fn clone_tree_filtered(source: &WeakDom, root_ref: Ref, ignore_globs: &[Glob]) -> WeakDom { + let root_inst = source.get_by_ref(root_ref).expect("root ref should exist"); + + // Create a new tree with the root instance + let mut builder = InstanceBuilder::new(root_inst.class) + .with_name(root_inst.name.clone()) + .with_properties(root_inst.properties.clone()); + + // Recursively add children, filtering out ignored ones + add_children_filtered(source, root_ref, &mut builder, ignore_globs); + + WeakDom::new(builder) +} + +/// Recursively adds children to an InstanceBuilder, filtering out ignored instances. +fn add_children_filtered( + source: &WeakDom, + parent_ref: Ref, + builder: &mut InstanceBuilder, + ignore_globs: &[Glob], +) { + let parent = source.get_by_ref(parent_ref).expect("parent ref should exist"); + + for &child_ref in parent.children() { + let child_path = inst_path(source, child_ref); + + // Check if this child should be ignored + let should_ignore = ignore_globs.iter().any(|glob| glob.is_match(&child_path)); + + if should_ignore { + log::debug!("Filtering out {child_path} from rbxm due to ignoreTrees pattern"); + continue; + } + + let child = source.get_by_ref(child_ref).expect("child ref should exist"); + + let mut child_builder = InstanceBuilder::new(child.class) + .with_name(child.name.clone()) + .with_properties(child.properties.clone()); + + // Recursively add this child's children + add_children_filtered(source, child_ref, &mut child_builder, ignore_globs); + + builder.add_child(child_builder); + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/snapshot_middleware/rbxmx.rs b/src/snapshot_middleware/rbxmx.rs index 59aff679..886e5706 100644 --- a/src/snapshot_middleware/rbxmx.rs +++ b/src/snapshot_middleware/rbxmx.rs @@ -9,6 +9,8 @@ use crate::{ syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, }; +use super::rbxm::clone_tree_filtered; + pub fn snapshot_rbxmx( context: &InstanceContext, vfs: &Vfs, @@ -49,20 +51,39 @@ pub fn syncback_rbxmx<'sync>( snapshot: &SyncbackSnapshot<'sync>, ) -> anyhow::Result> { let inst = snapshot.new_inst(); + let tree_globs = snapshot.compile_tree_globs(); let options = EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown); - // Long-term, we probably want to have some logic for if this contains a - // script. That's a future endeavor though. - let mut serialized = Vec::new(); - rbx_xml::to_writer( - &mut serialized, - snapshot.new_tree(), - &[inst.referent()], - options, - ) - .context("failed to serialize new rbxmx")?; + // If we have ignoreTrees patterns, filter the tree before serialization + let serialized = if tree_globs.is_empty() { + let mut serialized = Vec::new(); + rbx_xml::to_writer( + &mut serialized, + snapshot.new_tree(), + &[inst.referent()], + options, + ) + .context("failed to serialize new rbxmx")?; + serialized + } else { + // Clone the subtree, filtering out ignored instances + let filtered_tree = clone_tree_filtered( + snapshot.new_tree(), + inst.referent(), + &tree_globs, + ); + let mut serialized = Vec::new(); + rbx_xml::to_writer( + &mut serialized, + &filtered_tree, + &[filtered_tree.root_ref()], + options, + ) + .context("failed to serialize filtered rbxmx")?; + serialized + }; Ok(SyncbackReturn { fs_snapshot: FsSnapshot::new().with_added_file(&snapshot.path, serialized), diff --git a/src/syncback/mod.rs b/src/syncback/mod.rs index c42c51de..9d854d05 100644 --- a/src/syncback/mod.rs +++ b/src/syncback/mod.rs @@ -32,7 +32,7 @@ pub use file_names::{extension_for_middleware, name_for_inst, validate_file_name pub use fs_snapshot::FsSnapshot; pub use hash::*; pub use property_filter::{filter_properties, filter_properties_preallocated}; -pub use snapshot::{SyncbackData, SyncbackSnapshot}; +pub use snapshot::{inst_path, SyncbackData, SyncbackSnapshot}; /// The name of an enviroment variable to use to override the behavior of /// syncback on model files. @@ -59,6 +59,12 @@ pub fn syncback_loop( .map(|rules| rules.compile_globs()) .transpose()?; + let tree_globs = project + .syncback_rules + .as_ref() + .map(|rules| rules.compile_tree_globs()) + .transpose()?; + // TODO: Add a better way to tell if the root of a project is a directory let skip_pruning = if let Some(path) = &project.tree.path { let middleware = @@ -187,11 +193,11 @@ pub fn syncback_loop( log::debug!("Skipping {inst_path} because its path matches ignore pattern"); continue; } - if let Some(syncback_rules) = &project.syncback_rules { - // Ignore trees; - for ignored in &syncback_rules.ignore_trees { - if inst_path.starts_with(ignored.as_str()) { - log::debug!("Tree {inst_path} is blocked by project"); + // Check ignoreTrees with glob pattern support + if let Some(ref globs) = tree_globs { + for glob in globs { + if glob.is_match(&inst_path) { + log::debug!("Tree {inst_path} is blocked by ignoreTrees glob pattern"); continue 'syncback; } } @@ -261,10 +267,11 @@ pub fn syncback_loop( ); continue; } - if let Some(syncback_rules) = &project.syncback_rules { - for ignored in &syncback_rules.ignore_trees { - if inst_path.starts_with(ignored.as_str()) { - log::debug!("Skipping removing {inst_path} because its path is blocked by project"); + // Check ignoreTrees with glob pattern support + if let Some(ref globs) = tree_globs { + for glob in globs { + if glob.is_match(&inst_path) { + log::debug!("Skipping removing {inst_path} because its path is blocked by ignoreTrees glob pattern"); continue 'remove; } } @@ -439,6 +446,20 @@ impl SyncbackRules { Ok(globs) } + /// Compiles the ignoreTrees patterns into glob matchers. + /// Supports glob patterns like `**/Abc/Script` for flexible matching. + pub fn compile_tree_globs(&self) -> anyhow::Result> { + let mut globs = Vec::with_capacity(self.ignore_trees.len()); + + for pattern in &self.ignore_trees { + let glob = Glob::new(pattern) + .with_context(|| format!("the pattern '{pattern}' is not a valid ignoreTrees glob"))?; + globs.push(glob); + } + + Ok(globs) + } + /// Returns whether Windows-invalid characters should be encoded in file /// names during syncback. Defaults to `false`. #[inline] diff --git a/src/syncback/snapshot.rs b/src/syncback/snapshot.rs index 77d230e6..b314db05 100644 --- a/src/syncback/snapshot.rs +++ b/src/syncback/snapshot.rs @@ -3,6 +3,7 @@ use memofs::Vfs; use std::path::{Path, PathBuf}; use crate::{ + glob::Glob, snapshot::{InstanceWithMeta, RojoTree}, snapshot_middleware::Middleware, Project, @@ -239,13 +240,20 @@ impl<'sync> SyncbackSnapshot<'sync> { /// Checks if an instance should be ignored based on ignoreTrees rules. /// Takes a Ref to check against the new tree's instance path. + /// Supports glob patterns like `**/Abc/Script`. #[inline] pub fn should_ignore_tree(&self, referent: Ref) -> bool { if let Some(ignore_trees) = self.ignore_tree() { let path = inst_path(self.new_tree(), referent); - for ignored in ignore_trees { - if path.starts_with(ignored.as_str()) { - log::debug!("Ignoring {path} because it matches ignoreTrees entry: {ignored}"); + for pattern in ignore_trees { + // Try glob matching first, fall back to prefix matching for backwards compatibility + if let Ok(glob) = Glob::new(pattern) { + if glob.is_match(&path) { + log::debug!("Ignoring {path} because it matches ignoreTrees glob: {pattern}"); + return true; + } + } else if path.starts_with(pattern.as_str()) { + log::debug!("Ignoring {path} because it matches ignoreTrees prefix: {pattern}"); return true; } } @@ -253,6 +261,34 @@ impl<'sync> SyncbackSnapshot<'sync> { false } + /// Checks if an instance should be ignored using pre-compiled globs. + /// More efficient when checking many instances against the same patterns. + #[inline] + pub fn should_ignore_tree_with_globs(&self, referent: Ref, globs: &[Glob]) -> bool { + if globs.is_empty() { + return false; + } + let path = inst_path(self.new_tree(), referent); + for glob in globs { + if glob.is_match(&path) { + log::debug!("Ignoring {path} because it matches ignoreTrees glob pattern"); + return true; + } + } + false + } + + /// Compiles the ignoreTrees patterns into globs. + /// Returns an empty Vec if no patterns are configured. + pub fn compile_tree_globs(&self) -> Vec { + self.data + .project + .syncback_rules + .as_ref() + .and_then(|rules| rules.compile_tree_globs().ok()) + .unwrap_or_default() + } + /// Returns whether Windows-invalid characters should be encoded in file /// names during syncback. #[inline]