diff options
Diffstat (limited to 'cli/util')
-rw-r--r-- | cli/util/fs.rs | 104 | ||||
-rw-r--r-- | cli/util/gitignore.rs | 151 | ||||
-rw-r--r-- | cli/util/mod.rs | 1 | ||||
-rw-r--r-- | cli/util/path.rs | 35 |
4 files changed, 268 insertions, 23 deletions
diff --git a/cli/util/fs.rs b/cli/util/fs.rs index c81686f95..f6354097a 100644 --- a/cli/util/fs.rs +++ b/cli/util/fs.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use std::env::current_dir; use std::fmt::Write as FmtWrite; +use std::fs::FileType; use std::fs::OpenOptions; use std::io::Error; use std::io::ErrorKind; @@ -26,6 +27,8 @@ use deno_runtime::deno_crypto::rand; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node::PathClean; +use crate::util::gitignore::DirGitIgnores; +use crate::util::gitignore::GitIgnoreTree; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; use crate::util::progress_bar::ProgressMessagePrompt; @@ -244,22 +247,31 @@ pub fn resolve_from_cwd(path: &Path) -> Result<PathBuf, AnyError> { Ok(normalize_path(resolved_path)) } +#[derive(Debug, Clone)] +pub struct WalkEntry<'a> { + pub path: &'a Path, + pub file_type: &'a FileType, + pub patterns: &'a FilePatterns, +} + /// Collects file paths that satisfy the given predicate, by recursively walking `files`. /// If the walker visits a path that is listed in `ignore`, it skips descending into the directory. -pub struct FileCollector<TFilter: Fn(&Path, &FilePatterns) -> bool> { +pub struct FileCollector<TFilter: Fn(WalkEntry) -> bool> { file_filter: TFilter, ignore_git_folder: bool, ignore_node_modules: bool, ignore_vendor_folder: bool, + use_gitignore: bool, } -impl<TFilter: Fn(&Path, &FilePatterns) -> bool> FileCollector<TFilter> { +impl<TFilter: Fn(WalkEntry) -> bool> FileCollector<TFilter> { pub fn new(file_filter: TFilter) -> Self { Self { file_filter, ignore_git_folder: false, ignore_node_modules: false, ignore_vendor_folder: false, + use_gitignore: false, } } @@ -278,10 +290,46 @@ impl<TFilter: Fn(&Path, &FilePatterns) -> bool> FileCollector<TFilter> { self } + pub fn use_gitignore(mut self) -> Self { + self.use_gitignore = true; + self + } + pub fn collect_file_patterns( &self, file_patterns: FilePatterns, ) -> Result<Vec<PathBuf>, AnyError> { + fn is_pattern_matched( + maybe_git_ignore: Option<&DirGitIgnores>, + path: &Path, + is_dir: bool, + file_patterns: &FilePatterns, + ) -> bool { + use deno_config::glob::FilePatternsMatch; + + let path_kind = match is_dir { + true => deno_config::glob::PathKind::Directory, + false => deno_config::glob::PathKind::File, + }; + match file_patterns.matches_path_detail(path, path_kind) { + FilePatternsMatch::Passed => { + // check gitignore + let is_gitignored = maybe_git_ignore + .as_ref() + .map(|git_ignore| git_ignore.is_ignored(path, is_dir)) + .unwrap_or(false); + !is_gitignored + } + FilePatternsMatch::PassedOptedOutExclude => true, + FilePatternsMatch::Excluded => false, + } + } + + let mut maybe_git_ignores = if self.use_gitignore { + Some(GitIgnoreTree::new(Arc::new(deno_runtime::deno_fs::RealFs))) + } else { + None + }; let mut target_files = Vec::new(); let mut visited_paths = HashSet::new(); let file_patterns_by_base = file_patterns.split_by_base(); @@ -299,20 +347,23 @@ impl<TFilter: Fn(&Path, &FilePatterns) -> bool> FileCollector<TFilter> { }; let file_type = e.file_type(); let is_dir = file_type.is_dir(); - let c = e.path().to_path_buf(); - if file_patterns.exclude.matches_path(&c) - || !is_dir - && !file_patterns - .include - .as_ref() - .map(|i| i.matches_path(&c)) - .unwrap_or(true) - { + let path = e.path().to_path_buf(); + let maybe_gitignore = + maybe_git_ignores.as_mut().and_then(|git_ignores| { + let dir_path = if is_dir { &path } else { path.parent()? }; + git_ignores.get_resolved_git_ignore(dir_path) + }); + if !is_pattern_matched( + maybe_gitignore.as_deref(), + &path, + is_dir, + &file_patterns, + ) { if is_dir { iterator.skip_current_dir(); } } else if is_dir { - let should_ignore_dir = c + let should_ignore_dir = path .file_name() .map(|dir_name| { let dir_name = dir_name.to_string_lossy().to_lowercase(); @@ -323,17 +374,20 @@ impl<TFilter: Fn(&Path, &FilePatterns) -> bool> FileCollector<TFilter> { _ => false, }; // allow the user to opt out of ignoring by explicitly specifying the dir - file != c && is_ignored_file + file != path && is_ignored_file }) .unwrap_or(false) - || !visited_paths.insert(c.clone()); + || !visited_paths.insert(path.clone()); if should_ignore_dir { iterator.skip_current_dir(); } - } else if (self.file_filter)(&c, &file_patterns) - && visited_paths.insert(c.clone()) + } else if (self.file_filter)(WalkEntry { + path: &path, + file_type: &file_type, + patterns: &file_patterns, + }) && visited_paths.insert(path.clone()) { - target_files.push(c); + target_files.push(path); } } } @@ -346,7 +400,7 @@ impl<TFilter: Fn(&Path, &FilePatterns) -> bool> FileCollector<TFilter> { /// Note: This ignores all .git and node_modules folders. pub fn collect_specifiers( mut files: FilePatterns, - predicate: impl Fn(&Path, &FilePatterns) -> bool, + predicate: impl Fn(WalkEntry) -> bool, ) -> Result<Vec<ModuleSpecifier>, AnyError> { let mut prepared = vec![]; @@ -365,6 +419,10 @@ pub fn collect_specifiers( prepared.push(url); } } + PathOrPattern::NegatedPath(path) => { + // add it back + result.push(PathOrPattern::NegatedPath(path)); + } PathOrPattern::RemoteUrl(remote_url) => { prepared.push(remote_url); } @@ -819,9 +877,9 @@ mod tests { ignore_dir_path.to_path_buf(), )]), }; - let file_collector = FileCollector::new(|path, _| { + let file_collector = FileCollector::new(|e| { // exclude dotfiles - path + e.path .file_name() .and_then(|f| f.to_str()) .map(|f| !f.starts_with('.')) @@ -943,9 +1001,9 @@ mod tests { let ignore_dir_files = ["g.d.ts", ".gitignore"]; create_files(&ignore_dir_path, &ignore_dir_files); - let predicate = |path: &Path, _: &FilePatterns| { + let predicate = |e: WalkEntry| { // exclude dotfiles - path + e.path .file_name() .and_then(|f| f.to_str()) .map(|f| !f.starts_with('.')) @@ -956,7 +1014,7 @@ mod tests { FilePatterns { base: root_dir_path.to_path_buf(), include: Some( - PathOrPatternSet::from_relative_path_or_patterns( + PathOrPatternSet::from_include_relative_path_or_patterns( root_dir_path.as_path(), &[ "http://localhost:8080".to_string(), diff --git a/cli/util/gitignore.rs b/cli/util/gitignore.rs new file mode 100644 index 000000000..da9065494 --- /dev/null +++ b/cli/util/gitignore.rs @@ -0,0 +1,151 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; + +/// Resolved gitignore for a directory. +pub struct DirGitIgnores { + current: Option<Rc<ignore::gitignore::Gitignore>>, + parent: Option<Rc<DirGitIgnores>>, +} + +impl DirGitIgnores { + pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool { + let mut is_ignored = false; + if let Some(parent) = &self.parent { + is_ignored = parent.is_ignored(path, is_dir); + } + if let Some(current) = &self.current { + match current.matched(path, is_dir) { + ignore::Match::None => {} + ignore::Match::Ignore(_) => { + is_ignored = true; + } + ignore::Match::Whitelist(_) => { + is_ignored = false; + } + } + } + is_ignored + } +} + +/// Resolves gitignores in a directory tree taking into account +/// ancestor gitignores that may be found in a directory. +pub struct GitIgnoreTree { + fs: Arc<dyn deno_runtime::deno_fs::FileSystem>, + ignores: HashMap<PathBuf, Option<Rc<DirGitIgnores>>>, +} + +impl GitIgnoreTree { + pub fn new(fs: Arc<dyn deno_runtime::deno_fs::FileSystem>) -> Self { + Self { + fs, + ignores: Default::default(), + } + } + + pub fn get_resolved_git_ignore( + &mut self, + dir_path: &Path, + ) -> Option<Rc<DirGitIgnores>> { + self.get_resolved_git_ignore_inner(dir_path, None) + } + + fn get_resolved_git_ignore_inner( + &mut self, + dir_path: &Path, + maybe_parent: Option<&Path>, + ) -> Option<Rc<DirGitIgnores>> { + let maybe_resolved = self.ignores.get(dir_path).cloned(); + if let Some(resolved) = maybe_resolved { + resolved + } else { + let resolved = self.resolve_gitignore_in_dir(dir_path, maybe_parent); + self.ignores.insert(dir_path.to_owned(), resolved.clone()); + resolved + } + } + + fn resolve_gitignore_in_dir( + &mut self, + dir_path: &Path, + maybe_parent: Option<&Path>, + ) -> Option<Rc<DirGitIgnores>> { + if let Some(parent) = maybe_parent { + // stop searching if the parent dir had a .git directory in it + if self.fs.exists_sync(&parent.join(".git")) { + return None; + } + } + + let parent = dir_path.parent().and_then(|parent| { + self.get_resolved_git_ignore_inner(parent, Some(dir_path)) + }); + let current = self + .fs + .read_text_file_sync(&dir_path.join(".gitignore")) + .ok() + .and_then(|text| { + let mut builder = ignore::gitignore::GitignoreBuilder::new(dir_path); + for line in text.lines() { + builder.add_line(None, line).ok()?; + } + let gitignore = builder.build().ok()?; + Some(Rc::new(gitignore)) + }); + if parent.is_none() && current.is_none() { + None + } else { + Some(Rc::new(DirGitIgnores { current, parent })) + } + } +} + +#[cfg(test)] +mod test { + use deno_runtime::deno_fs::InMemoryFs; + + use super::*; + + #[test] + fn git_ignore_tree() { + let fs = InMemoryFs::default(); + fs.setup_text_files(vec![ + ("/.gitignore".into(), "file.txt".into()), + ("/sub_dir/.gitignore".into(), "data.txt".into()), + ( + "/sub_dir/sub_dir/.gitignore".into(), + "!file.txt\nignore.txt".into(), + ), + ]); + let mut ignore_tree = GitIgnoreTree::new(Arc::new(fs)); + let mut run_test = |path: &str, expected: bool| { + let path = PathBuf::from(path); + let gitignore = ignore_tree + .get_resolved_git_ignore(path.parent().unwrap()) + .unwrap(); + assert_eq!( + gitignore.is_ignored(&path, /* is_dir */ false), + expected, + "Path: {}", + path.display() + ); + }; + run_test("/file.txt", true); + run_test("/other.txt", false); + run_test("/data.txt", false); + run_test("/sub_dir/file.txt", true); + run_test("/sub_dir/other.txt", false); + run_test("/sub_dir/data.txt", true); + run_test("/sub_dir/sub_dir/file.txt", false); // unignored up here + run_test("/sub_dir/sub_dir/sub_dir/file.txt", false); + run_test("/sub_dir/sub_dir/sub_dir/ignore.txt", true); + run_test("/sub_dir/sub_dir/ignore.txt", true); + run_test("/sub_dir/ignore.txt", false); + run_test("/ignore.txt", false); + } +} diff --git a/cli/util/mod.rs b/cli/util/mod.rs index a6f72bc04..7e0e1bd37 100644 --- a/cli/util/mod.rs +++ b/cli/util/mod.rs @@ -8,6 +8,7 @@ pub mod display; pub mod draw_thread; pub mod file_watcher; pub mod fs; +pub mod gitignore; pub mod logger; pub mod path; pub mod progress_bar; diff --git a/cli/util/path.rs b/cli/util/path.rs index 496b37c5e..fed74cb06 100644 --- a/cli/util/path.rs +++ b/cli/util/path.rs @@ -6,6 +6,9 @@ use std::path::PathBuf; use deno_ast::MediaType; use deno_ast::ModuleSpecifier; +use deno_config::glob::PathGlobMatch; +use deno_config::glob::PathOrPattern; +use deno_config::glob::PathOrPatternSet; use deno_core::error::uri_error; use deno_core::error::AnyError; @@ -244,6 +247,38 @@ pub fn root_url_to_safe_local_dirname(root: &ModuleSpecifier) -> PathBuf { result } +/// Slightly different behaviour than the default matching +/// where an exact path needs to be matched to be opted-in +/// rather than just a partial directory match. +/// +/// This is used by the test and bench filtering. +pub fn matches_pattern_or_exact_path( + path_or_pattern_set: &PathOrPatternSet, + path: &Path, +) -> bool { + for p in path_or_pattern_set.inner().iter().rev() { + match p { + PathOrPattern::Path(p) => { + if p == path { + return true; + } + } + PathOrPattern::NegatedPath(p) => { + if path.starts_with(p) { + return false; + } + } + PathOrPattern::RemoteUrl(_) => {} + PathOrPattern::Pattern(p) => match p.matches_path(path) { + PathGlobMatch::Matched => return true, + PathGlobMatch::MatchedNegated => return false, + PathGlobMatch::NotMatched => {} + }, + } + } + false +} + #[cfg(test)] mod test { use super::*; |