diff --git a/crates/ignore/src/gitignore.rs b/crates/ignore/src/gitignore.rs index 8824139b5c28..45f1263b7b5b 100644 --- a/crates/ignore/src/gitignore.rs +++ b/crates/ignore/src/gitignore.rs @@ -702,14 +702,14 @@ fn parse_excludes_file(data: &[u8]) -> Option { Regex::builder() .configure(Regex::config().utf8_empty(false)) .syntax(syntax::Config::new().utf8(false)) - .build(r#"(?im-u)^\s*excludesfile\s*=\s*"?\s*(\S+?)\s*"?\s*$"#) + .build(r#"(?im-u)^\s*excludesfile\s*=\s*(?:"(\S(?:[^"\r\n]*\S)?)"|(\S+))\s*$"#) .unwrap() }); // We don't care about amortizing allocs here I think. This should only // be called ~once per traversal or so? (Although it's not guaranteed...) let mut caps = re.create_captures(); re.captures(data, &mut caps); - let span = caps.get_group(1)?; + let span = caps.get_group(1).or_else(|| caps.get_group(2))?; let candidate = &data[span]; std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s))) } @@ -879,6 +879,13 @@ mod tests { ); } + #[test] + fn parse_excludes_file_with_spaces() { + let data = bytes("[core]\nexcludesFile = \"~/foo bar/ignore\""); + let got = super::parse_excludes_file(&data).unwrap(); + assert_eq!(path_string(got), super::expand_tilde("~/foo bar/ignore")); + } + #[test] fn parse_excludes_file5() { let data = bytes("[core]\nexcludesFile = \" \"~/foo/bar \" \""); diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index b7da9aa171ef..ae13207fc6ef 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -781,8 +781,8 @@ fn create_walker(sources: &Sources) -> Option { // Scan hidden files / directories builder.hidden(false); - // Don't respect global gitignore files - builder.git_global(false); + // Respect global gitignore files configured in git (e.g. `core.excludesFile`) + builder.git_global(true); // By default, allow .gitignore files to be used regardless of whether or not // a .git directory is present. This is an optimization for when projects diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index fb5354a87d37..e36c2fe55ded 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -6,7 +6,7 @@ mod scanner { use std::process::Command; use std::thread::sleep; use std::time::Duration; - use std::{fs, path}; + use std::{env, fs, path}; use tailwindcss_oxide::*; use tempfile::tempdir; @@ -2923,6 +2923,61 @@ mod scanner { ); } + #[test] + fn respects_git_core_excludes_file() { + if env::var_os("TAILWIND_OXIDE_CORE_EXCLUDES_CHILD").is_none() { + let status = Command::new(env::current_exe().unwrap()) + .args(["--exact", "scanner::respects_git_core_excludes_file"]) + .env("TAILWIND_OXIDE_CORE_EXCLUDES_CHILD", "1") + .status() + .unwrap(); + assert!(status.success()); + return; + } + + let dir = tempdir().unwrap().into_path(); + + let global_ignore = dir.join("global ignore"); + fs::write(&global_ignore, "ignored-by-global.html\n").unwrap(); + + let global_gitconfig = dir.join(".gitconfig"); + fs::write( + &global_gitconfig, + format!( + "[core]\n excludesFile = \"{}\"\n", + global_ignore.display() + ), + ) + .unwrap(); + + let repo_dir = dir.join("repo"); + fs::create_dir_all(&repo_dir).unwrap(); + _ = Command::new("git").arg("init").current_dir(&repo_dir).output(); + + create_files_in( + &repo_dir, + &[ + ("src/index.html", "content-['index.html']"), + ( + "src/ignored-by-global.html", + "content-['ignored-by-global.html']", + ), + ], + ); + + let sources = vec![public_source_entry_from_pattern( + repo_dir.clone(), + "@source '**/*'", + )]; + + env::set_var("GIT_CONFIG_GLOBAL", &global_gitconfig); + + let mut scanner = Scanner::new(sources); + let candidates = scanner.scan(); + + assert_eq!(candidates, vec!["content-['index.html']"]); + } + #[test] fn test_explicitly_ignore_explicitly_allowed_files() { // Create a temporary working directory