From 543e05a2d5e5c6f68c693838edaff6f85ac7428b Mon Sep 17 00:00:00 2001 From: EgoMoose Date: Tue, 2 Jun 2026 12:56:20 -0400 Subject: [PATCH] Add syncback ignore glob support --- src/syncback/mod.rs | 85 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/src/syncback/mod.rs b/src/syncback/mod.rs index 5dc3028f..e1666abc 100644 --- a/src/syncback/mod.rs +++ b/src/syncback/mod.rs @@ -21,7 +21,7 @@ use std::{ }; use crate::{ - glob::Glob, + glob::{Glob, IgnorableGlob}, snapshot::{InstanceWithMeta, RojoTree}, snapshot_middleware::Middleware, syncback::ref_properties::{collect_referents, link_referents}, @@ -414,18 +414,18 @@ pub struct SyncbackRules { } impl SyncbackRules { - pub fn compile_globs(&self) -> anyhow::Result> { + pub fn compile_globs(&self) -> anyhow::Result> { let mut globs = Vec::with_capacity(self.ignore_paths.len()); let dir_ignore_paths = self.create_ignore_dir_paths.unwrap_or(true); for pattern in &self.ignore_paths { - let glob = Glob::new(pattern) + let glob = IgnorableGlob::new(pattern) .with_context(|| format!("the pattern '{pattern}' is not a valid glob"))?; globs.push(glob); if dir_ignore_paths { if let Some(dir_pattern) = pattern.strip_suffix("/**") { - if let Ok(glob) = Glob::new(dir_pattern) { + if let Ok(glob) = IgnorableGlob::new(dir_pattern) { globs.push(glob) } } @@ -436,7 +436,7 @@ impl SyncbackRules { } } -fn is_valid_path(globs: &Option>, base_path: &Path, path: &Path) -> bool { +fn is_valid_path(globs: &Option>, base_path: &Path, path: &Path) -> bool { let git_glob = GIT_IGNORE_GLOB.get_or_init(|| Glob::new(".git/**").unwrap()); let test_path = match path.strip_prefix(base_path) { Ok(suffix) => suffix, @@ -446,11 +446,16 @@ fn is_valid_path(globs: &Option>, base_path: &Path, path: &Path) -> bo return false; } if let Some(ref ignore_paths) = globs { + // Gitignore-style "last match wins" + let mut ignored = false; for glob in ignore_paths { if glob.is_match(test_path) { - return false; + ignored = !glob.is_negation(); } } + if ignored { + return false; + } } true } @@ -538,3 +543,71 @@ fn strip_unknown_root_children(new: &mut WeakDom, old: &RojoTree) { new.destroy(child_ref); } } + +#[cfg(test)] +mod test { + use super::*; + + fn rules(ignore_paths: &[&str], create_ignore_dir_paths: Option) -> SyncbackRules { + SyncbackRules { + ignore_trees: Vec::new(), + ignore_paths: ignore_paths.iter().map(|s| s.to_string()).collect(), + ignore_properties: IndexMap::new(), + sync_current_camera: None, + sync_unscriptable: None, + ignore_referents: None, + create_ignore_dir_paths, + } + } + + #[test] + fn ignore_paths_negation() { + let globs = Some( + rules(&["**/*.lua", "!keep.lua"], Some(false)) + .compile_globs() + .unwrap(), + ); + let base = Path::new("/test"); + + // A later negation re-includes a path matched by an earlier pattern. + assert!(!is_valid_path(&globs, base, Path::new("/test/foo.lua"))); + assert!(is_valid_path(&globs, base, Path::new("/test/keep.lua"))); + // Paths matched by no rule are valid. + assert!(is_valid_path(&globs, base, Path::new("/test/plain.txt"))); + } + + #[test] + fn ignore_paths_negation_with_dir_expansion() { + // With `create_ignore_dir_paths`, a negated `foo/**` pattern should also + // re-include the `foo` directory itself, mirroring the file rule. + let globs = Some( + rules(&["**/*", "!keep/**"], Some(true)) + .compile_globs() + .unwrap(), + ); + let base = Path::new("/test"); + + assert!(!is_valid_path(&globs, base, Path::new("/test/drop/a.lua"))); + assert!(is_valid_path(&globs, base, Path::new("/test/keep"))); + assert!(is_valid_path(&globs, base, Path::new("/test/keep/a.lua"))); + } + + #[test] + fn ignore_paths_escaped_bang_is_literal() { + // `\!literal.lua` should ignore a file literally named `!literal.lua` + // rather than being parsed as a negation. + let globs = Some( + rules(&[r"\!literal.lua"], Some(false)) + .compile_globs() + .unwrap(), + ); + let base = Path::new("/test"); + + assert!(!globs.as_ref().unwrap()[0].is_negation()); + assert!(!is_valid_path( + &globs, + base, + Path::new("/test/!literal.lua") + )); + } +}