From f6dc37e400f0679b6e32fafad60872cf112124d0 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 29 Dec 2025 00:02:40 -0600 Subject: [PATCH] Enhance syncback functionality by implementing tree filtering based on ignoreTrees patterns. Added support for glob pattern matching to filter instances during serialization in both rbxm and rbxmx. Updated SyncbackSnapshot to compile and utilize these patterns efficiently. --- src/snapshot_middleware/rbxm.rs | 77 +++++++++++++++++++++++++++++--- src/snapshot_middleware/rbxmx.rs | 41 ++++++++++++----- src/syncback/mod.rs | 41 ++++++++++++----- src/syncback/snapshot.rs | 42 +++++++++++++++-- 4 files changed, 172 insertions(+), 29 deletions(-) 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]