use fxhash::{FxHashMap, FxHashSet};
use glob_match::glob_match;
use std::path::{Path, PathBuf};
use tracing::event;
use crate::GlobEntry;
pub fn hoist_static_glob_parts(entries: &Vec<GlobEntry>) -> Vec<GlobEntry> {
let mut result = vec![];
for entry in entries {
let (static_part, dynamic_part) = split_pattern(&entry.pattern);
let base: PathBuf = entry.base.clone().into();
let base = match static_part {
Some(static_part) => base.join(static_part),
None => base,
};
let base = match dunce::canonicalize(&base) {
Ok(base) => base,
Err(err) => {
event!(tracing::Level::ERROR, "Failed to resolve glob: {:?}", err);
continue;
}
};
let pattern = match dynamic_part {
Some(dynamic_part) => dynamic_part,
None => {
if base.is_dir() {
"**/*".to_owned()
} else {
"".to_owned()
}
}
};
if pattern.is_empty() && base.is_file() {
result.push(GlobEntry {
base: base.parent().unwrap().to_string_lossy().to_string(),
pattern: base.file_name().unwrap().to_string_lossy().to_string(),
});
}
result.push(GlobEntry {
base: base.to_string_lossy().to_string(),
pattern,
});
}
result
}
pub fn optimize_patterns(entries: &Vec<GlobEntry>) -> Vec<GlobEntry> {
let entries = hoist_static_glob_parts(entries);
let mut pattern_map: FxHashMap<String, FxHashSet<String>> = FxHashMap::default();
for glob_entry in entries {
let entry = pattern_map.entry(glob_entry.base).or_default();
entry.insert(glob_entry.pattern.clone());
}
let mut glob_entries = pattern_map
.into_iter()
.map(|(base, patterns)| {
let size = patterns.len();
let mut patterns = patterns.into_iter();
GlobEntry {
base,
pattern: match size {
1 => patterns.next().unwrap(),
_ => {
let mut patterns = patterns.collect::<Vec<_>>();
patterns.sort();
format!("{{{}}}", patterns.join(","))
}
},
}
})
.collect::<Vec<GlobEntry>>();
glob_entries.sort_by(|a, z| a.base.cmp(&z.base));
glob_entries
}
fn split_pattern(pattern: &str) -> (Option<String>, Option<String>) {
if !pattern.contains('*') {
return (Some(pattern.to_owned()), None);
}
let mut last_slash_position = None;
for (i, c) in pattern.char_indices() {
if c == '/' {
last_slash_position = Some(i);
}
if c == '*' || c == '!' {
break;
}
}
let Some(last_slash_position) = last_slash_position else {
return (None, Some(pattern.to_owned()));
};
let static_part = pattern[..last_slash_position].to_owned();
let dynamic_part = pattern[last_slash_position + 1..].to_owned();
let static_part = (!static_part.is_empty()).then_some(static_part);
let dynamic_part = (!dynamic_part.is_empty()).then_some(dynamic_part);
(static_part, dynamic_part)
}
pub fn path_matches_globs(path: &Path, globs: &[GlobEntry]) -> bool {
let path = path.to_string_lossy();
globs
.iter()
.any(|g| glob_match(&format!("{}/{}", g.base, g.pattern), &path))
}
#[cfg(test)]
mod tests {
use super::optimize_patterns;
use crate::GlobEntry;
use bexpand::Expression;
use std::process::Command;
use std::{fs, path};
use tempfile::tempdir;
fn create_folders(folders: &[&str]) -> String {
let dir = tempdir().unwrap().into_path();
let _ = Command::new("git").arg("init").current_dir(&dir).output();
for path in folders {
let path = dir.join(path.replace('/', path::MAIN_SEPARATOR.to_string().as_str()));
let parent = path.parent().unwrap();
if !parent.exists() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, "").unwrap();
}
let base = format!("{}", dir.display());
base
}
fn test(base: &str, sources: &[GlobEntry]) -> Vec<GlobEntry> {
let sources: Vec<GlobEntry> = sources
.iter()
.map(|x| GlobEntry {
base: format!("{}{}", base, x.base),
pattern: x.pattern.clone(),
})
.collect();
let sources = sources
.iter()
.flat_map(|source| {
let expression: Result<Expression, _> = source.pattern[..].try_into();
let Ok(expression) = expression else {
return vec![source.clone()];
};
expression
.into_iter()
.filter_map(Result::ok)
.map(move |pattern| GlobEntry {
base: source.base.clone(),
pattern: pattern.into(),
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let optimized_sources = optimize_patterns(&sources);
let parent_dir =
format!("{}{}", dunce::canonicalize(base).unwrap().display(), "/").replace('\\', "/");
optimized_sources
.into_iter()
.map(|source| {
GlobEntry {
base: source.base.replace('\\', "/").replace(&parent_dir, "/"),
pattern: source.pattern,
}
})
.collect()
}
#[test]
fn it_should_keep_globs_that_start_with_file_wildcards_as_is() {
let base = create_folders(&["projects"]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "*.html".to_string(),
}],
);
let expected = vec![GlobEntry {
base: "/projects".to_string(),
pattern: "*.html".to_string(),
}];
assert_eq!(actual, expected);
}
#[test]
fn it_should_keep_globs_that_start_with_folder_wildcards_as_is() {
let base = create_folders(&["projects"]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "**/*.html".to_string(),
}],
);
let expected = vec![GlobEntry {
base: "/projects".to_string(),
pattern: "**/*.html".to_string(),
}];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_move_the_starting_folder_to_the_path() {
let base = create_folders(&["projects/example"]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "example/*.html".to_string(),
}],
);
let expected = vec![GlobEntry {
base: "/projects/example".to_string(),
pattern: "*.html".to_string(),
}];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_move_the_starting_folders_to_the_path() {
let base = create_folders(&["projects/example/other"]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "example/other/*.html".to_string(),
}],
);
let expected = vec![GlobEntry {
base: "/projects/example/other".to_string(),
pattern: "*.html".to_string(),
}];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_branch_expandable_folders() {
let base = create_folders(&["projects/foo", "projects/bar"]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "{foo,bar}/*.html".to_string(),
}],
);
let expected = vec![
GlobEntry {
base: "/projects/bar".to_string(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/foo".to_string(),
pattern: "*.html".to_string(),
},
];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_expand_multiple_expansions_in_the_same_folder() {
let base = create_folders(&[
"projects/a-b-d-e-g",
"projects/a-b-d-f-g",
"projects/a-c-d-e-g",
"projects/a-c-d-f-g",
]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "a-{b,c}-d-{e,f}-g/*.html".to_string(),
}],
);
let expected = vec![
GlobEntry {
base: "/projects/a-b-d-e-g".to_string(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-b-d-f-g".to_string(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-d-e-g".to_string(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-d-f-g".to_string(),
pattern: "*.html".to_string(),
},
];
assert_eq!(actual, expected,);
}
#[test]
fn multiple_expansions_per_folder_starting_at_the_root() {
let base = create_folders(&[
"projects/a-c-d-f/b-d-e-g",
"projects/a-c-d-f/b-d-f-g",
"projects/a-c-d-f/c-d-e-g",
"projects/a-c-d-f/c-d-f-g",
"projects/a-c-e-f/b-d-e-g",
"projects/a-c-e-f/b-d-f-g",
"projects/a-c-e-f/c-d-e-g",
"projects/a-c-e-f/c-d-f-g",
"projects/b-c-d-f/b-d-e-g",
"projects/b-c-d-f/b-d-f-g",
"projects/b-c-d-f/c-d-e-g",
"projects/b-c-d-f/c-d-f-g",
"projects/b-c-e-f/b-d-e-g",
"projects/b-c-e-f/b-d-f-g",
"projects/b-c-e-f/c-d-e-g",
"projects/b-c-e-f/c-d-f-g",
]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "{a,b}-c-{d,e}-f/{b,c}-d-{e,f}-g/*.html".to_string(),
}],
);
let expected = vec![
GlobEntry {
base: "/projects/a-c-d-f/b-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-d-f/b-d-f-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-d-f/c-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-d-f/c-d-f-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-e-f/b-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-e-f/b-d-f-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-e-f/c-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a-c-e-f/c-d-f-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-d-f/b-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-d-f/b-d-f-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-d-f/c-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-d-f/c-d-f-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-e-f/b-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-e-f/b-d-f-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-e-f/c-d-e-g".into(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/b-c-e-f/c-d-f-g".into(),
pattern: "*.html".to_string(),
},
];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_stop_expanding_once_we_hit_a_wildcard() {
let base = create_folders(&["projects/bar/example", "projects/foo/example"]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "{foo,bar}/example/**/{baz,qux}/*.html".to_string(),
}],
);
let expected = vec![
GlobEntry {
base: "/projects/bar/example".to_string(),
pattern: "{**/baz/*.html,**/qux/*.html}".to_string(),
},
GlobEntry {
base: "/projects/foo/example".to_string(),
pattern: "{**/baz/*.html,**/qux/*.html}".to_string(),
},
];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_keep_the_negation_symbol_for_all_new_patterns() {
let base = create_folders(&["projects"]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "!{foo,bar}/*.html".to_string(),
}],
);
let expected = vec![GlobEntry {
base: "/projects".to_string(),
pattern: "{!bar/*.html,!foo/*.html}".to_string(),
}];
assert_eq!(actual, expected,);
}
#[test]
fn it_should_expand_a_complex_example() {
let base = create_folders(&[
"projects/a/b/d/e/g",
"projects/a/b/d/f/g",
"projects/a/c/d/e/g",
"projects/a/c/d/f/g",
]);
let actual = test(
&base,
&[GlobEntry {
base: "/projects".to_string(),
pattern: "a/{b,c}/d/{e,f}/g/*.html".to_string(),
}],
);
let expected = vec![
GlobEntry {
base: "/projects/a/b/d/e/g".to_string(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a/b/d/f/g".to_string(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a/c/d/e/g".to_string(),
pattern: "*.html".to_string(),
},
GlobEntry {
base: "/projects/a/c/d/f/g".to_string(),
pattern: "*.html".to_string(),
},
];
assert_eq!(actual, expected,);
}
}