Skip to content

Commit 36172e9

Browse files
authored
Merge pull request #254 from dallay/perf-optimize-nested-glob-dir-skipping-1896709277221384780
2 parents e81b4c5 + a394af0 commit 36172e9

2 files changed

Lines changed: 119 additions & 86 deletions

File tree

src/config.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,11 @@ pub struct TargetConfig {
113113
pub pattern: Option<String>,
114114

115115
/// Glob patterns that exclude paths from a `nested-glob` search.
116-
/// Each pattern is matched against the path of a discovered file relative
117-
/// to the search root. Common defaults include `node_modules/**` and
118-
/// `**/.git/**`. Has no effect on other target types.
116+
/// Each pattern is matched against discovered paths relative to the search
117+
/// root, including both files and directories. Directory matches are used
118+
/// to prune whole subtrees during traversal, so `node_modules`,
119+
/// `node_modules/**`, `.git`, and `**/.git/**` all prevent descending into
120+
/// those directories. Has no effect on other target types.
119121
#[serde(default)]
120122
pub exclude: Vec<String>,
121123

src/linker.rs

Lines changed: 114 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -606,42 +606,97 @@ impl Linker {
606606
return Ok(result);
607607
}
608608

609-
for entry in WalkDir::new(search_root)
610-
.follow_links(false)
611-
.into_iter()
612-
.filter_map(|e| e.ok())
613-
{
614-
// Only process files
615-
if !entry.file_type().is_file() {
616-
continue;
617-
}
609+
self.for_each_nested_glob_match(
610+
search_root,
611+
glob_pattern,
612+
excludes,
613+
options,
614+
|full_path, rel_path| {
615+
let dest_str = Self::expand_destination_template(dest_template, rel_path);
616+
if dest_str.is_empty() {
617+
if options.verbose {
618+
println!(
619+
" {} Destination template produced empty path for: {}",
620+
"!".yellow(),
621+
full_path.display()
622+
);
623+
}
624+
result.skipped += 1;
625+
return Ok(());
626+
}
627+
628+
let dest = self.project_root.join(&dest_str);
629+
630+
let resolved = ResolvedSource {
631+
path: full_path.to_path_buf(),
632+
exists: true,
633+
};
634+
635+
let item_result = self.create_symlink(&resolved, &dest, options)?;
636+
result.created += item_result.created;
637+
result.updated += item_result.updated;
638+
result.skipped += item_result.skipped;
639+
640+
Ok(())
641+
},
642+
)?;
643+
644+
Ok(result)
645+
}
646+
647+
fn for_each_nested_glob_match<F>(
648+
&self,
649+
search_root: &Path,
650+
glob_pattern: &str,
651+
excludes: &[String],
652+
options: &SyncOptions,
653+
mut on_match: F,
654+
) -> Result<()>
655+
where
656+
F: FnMut(&Path, &Path) -> Result<()>,
657+
{
658+
let mut it = WalkDir::new(search_root).follow_links(false).into_iter();
659+
660+
while let Some(entry) = it.next() {
661+
let entry = match entry {
662+
Ok(entry) => entry,
663+
Err(err) => {
664+
if options.verbose {
665+
let path = err
666+
.path()
667+
.map(|p| p.display().to_string())
668+
.unwrap_or_else(|| "<unknown path>".to_string());
669+
println!(
670+
" {} WalkDir error while traversing {}: {}",
671+
"!".yellow(),
672+
path,
673+
err
674+
);
675+
}
676+
tracing::debug!(error = %err, path = ?err.path(), "WalkDir entry skipped during nested-glob traversal");
677+
continue;
678+
}
679+
};
618680

619-
// Compute path relative to search root
620681
let full_path = entry.path();
621682
let rel_path = match full_path.strip_prefix(search_root) {
622-
Ok(p) => p,
683+
Ok(path) => path,
623684
Err(_) => continue,
624685
};
625686

626-
// Convert to forward-slash string for pattern matching
627687
let rel_str = rel_path
628688
.components()
629-
.map(|c| c.as_os_str().to_string_lossy().into_owned())
689+
.map(|component| component.as_os_str().to_string_lossy().into_owned())
630690
.collect::<Vec<_>>()
631691
.join("/");
632692

633-
// Match against the glob pattern
634-
if !matches_path_glob(&rel_str, glob_pattern) {
693+
if rel_str.is_empty() {
635694
continue;
636695
}
637696

638-
// Check exclusion patterns.
639-
// Uses `find()` instead of `any()` to:
640-
// 1. Skip iteration when excludes list is empty (short-circuit)
641-
// 2. Determine which pattern matched (for verbose logging)
642697
if let Some(matched_exclude) = excludes
643698
.iter()
644-
.find(|excl| matches_path_glob(&rel_str, excl))
699+
.find(|exclude| matches_path_glob(&rel_str, exclude))
645700
{
646701
if options.verbose {
647702
println!(
@@ -651,37 +706,25 @@ impl Linker {
651706
full_path.display()
652707
);
653708
}
654-
continue;
655-
}
656709

657-
// Expand the destination template
658-
let dest_str = Self::expand_destination_template(dest_template, rel_path);
659-
if dest_str.is_empty() {
660-
if options.verbose {
661-
println!(
662-
" {} Destination template produced empty path for: {}",
663-
"!".yellow(),
664-
full_path.display()
665-
);
710+
if entry.file_type().is_dir() {
711+
it.skip_current_dir();
666712
}
667-
result.skipped += 1;
668713
continue;
669714
}
670715

671-
let dest = self.project_root.join(&dest_str);
716+
if !entry.file_type().is_file() {
717+
continue;
718+
}
672719

673-
let resolved = ResolvedSource {
674-
path: full_path.to_path_buf(),
675-
exists: true,
676-
};
720+
if !matches_path_glob(&rel_str, glob_pattern) {
721+
continue;
722+
}
677723

678-
let item_result = self.create_symlink(&resolved, &dest, options)?;
679-
result.created += item_result.created;
680-
result.updated += item_result.updated;
681-
result.skipped += item_result.skipped;
724+
on_match(full_path, rel_path)?;
682725
}
683726

684-
Ok(result)
727+
Ok(())
685728
}
686729

687730
/// Process a `module-map` target: iterate mappings and create a symlink
@@ -813,48 +856,36 @@ impl Linker {
813856
let dest_template = &target_config.destination;
814857
let excludes = &target_config.exclude;
815858

816-
for entry in WalkDir::new(&search_root)
817-
.follow_links(false)
818-
.into_iter()
819-
.filter_map(|e| e.ok())
820-
{
821-
if !entry.file_type().is_file() {
822-
continue;
823-
}
824-
let rel_path = match entry.path().strip_prefix(&search_root) {
825-
Ok(p) => p,
826-
Err(_) => continue,
827-
};
828-
let rel_str = rel_path
829-
.components()
830-
.map(|c| c.as_os_str().to_string_lossy().into_owned())
831-
.collect::<Vec<_>>()
832-
.join("/");
833-
if !matches_path_glob(&rel_str, glob_pattern) {
834-
continue;
835-
}
836-
if excludes
837-
.iter()
838-
.any(|excl| matches_path_glob(&rel_str, excl))
839-
{
840-
continue;
841-
}
842-
let dest_str =
843-
Self::expand_destination_template(dest_template, rel_path);
844-
if dest_str.is_empty() {
845-
continue;
846-
}
847-
let dest = self.project_root.join(&dest_str);
848-
if dest.is_symlink() {
849-
if options.dry_run {
850-
println!(" {} Would remove: {}", "→".cyan(), dest.display());
851-
} else {
852-
fs::remove_file(&dest)?;
853-
println!(" {} Removed: {}", "✔".green(), dest.display());
859+
self.for_each_nested_glob_match(
860+
&search_root,
861+
glob_pattern,
862+
excludes,
863+
options,
864+
|_, rel_path| {
865+
let dest_str =
866+
Self::expand_destination_template(dest_template, rel_path);
867+
if dest_str.is_empty() {
868+
return Ok(());
854869
}
855-
result.removed += 1;
856-
}
857-
}
870+
871+
let dest = self.project_root.join(&dest_str);
872+
if dest.is_symlink() {
873+
if options.dry_run {
874+
println!(
875+
" {} Would remove: {}",
876+
"→".cyan(),
877+
dest.display()
878+
);
879+
} else {
880+
fs::remove_file(&dest)?;
881+
println!(" {} Removed: {}", "✔".green(), dest.display());
882+
}
883+
result.removed += 1;
884+
}
885+
886+
Ok(())
887+
},
888+
)?;
858889
}
859890
SyncType::SymlinkContents => {
860891
let dest = self.project_root.join(&target_config.destination);

0 commit comments

Comments
 (0)