From 4f875f5986f27065897dec18526e041713f44f2a Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 27 Sep 2026 11:52:35 +0200 Subject: [PATCH 1/4] Respect git global ignore files (core.excludesFile) in source scanner In #15941, the intention was to prevent .gitignore files outside of a git repository from taking effect while ensuring global ignore files configured in git (like core.excludesFile) continue to work. However, builder.git_global(false) was explicitly set in the oxide scanner walker, which caused git's global ignore configuration (core.excludesFile / ~/.config/git/ignore) to be completely ignored. This change enables git_global(true) so that global ignore rules configured in git are honored as expected during source detection. Fixes #20509 --- crates/oxide/src/scanner/mod.rs | 4 +-- crates/oxide/tests/scanner.rs | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) 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..a37301f22758 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -2923,6 +2923,55 @@ mod scanner { ); } + #[test] + fn respects_git_core_excludes_file() { + 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 '**/*'", + )]; + + let old_git_config_global = std::env::var_os("GIT_CONFIG_GLOBAL"); + std::env::set_var("GIT_CONFIG_GLOBAL", &global_gitconfig); + + let mut scanner = Scanner::new(sources); + let candidates = scanner.scan(); + + if let Some(old) = old_git_config_global { + std::env::set_var("GIT_CONFIG_GLOBAL", old); + } else { + std::env::remove_var("GIT_CONFIG_GLOBAL"); + } + + assert_eq!(candidates, vec!["content-['index.html']"]); + } + #[test] fn test_explicitly_ignore_explicitly_allowed_files() { // Create a temporary working directory From 8e1e925fabebdf2e8f32b30d6d0932ad346e15c3 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 30 Sep 2026 01:03:19 +0200 Subject: [PATCH 2/4] fix respect for global excludes paths with spaces --- crates/ignore/src/gitignore.rs | 11 +++++++++-- crates/oxide/tests/scanner.rs | 35 +++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 13 deletions(-) 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/tests/scanner.rs b/crates/oxide/tests/scanner.rs index a37301f22758..64720d716917 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -6,7 +6,8 @@ mod scanner { use std::process::Command; use std::thread::sleep; use std::time::Duration; - use std::{fs, path}; + use std::sync::{Mutex, OnceLock}; + use std::{env, fs, path}; use tailwindcss_oxide::*; use tempfile::tempdir; @@ -2925,15 +2926,21 @@ mod scanner { #[test] fn respects_git_core_excludes_file() { + static ENV_LOCK: OnceLock> = OnceLock::new(); + let _lock = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap(); + let dir = tempdir().unwrap().into_path(); - let global_ignore = dir.join("global_ignore"); + 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()), + format!( + "[core]\n excludesFile = \"{}\"\n", + global_ignore.display() + ), ) .unwrap(); @@ -2957,18 +2964,24 @@ mod scanner { "@source '**/*'", )]; - let old_git_config_global = std::env::var_os("GIT_CONFIG_GLOBAL"); - std::env::set_var("GIT_CONFIG_GLOBAL", &global_gitconfig); + struct GitConfigGlobalGuard(Option); + + impl Drop for GitConfigGlobalGuard { + fn drop(&mut self) { + if let Some(old) = self.0.take() { + env::set_var("GIT_CONFIG_GLOBAL", old); + } else { + env::remove_var("GIT_CONFIG_GLOBAL"); + } + } + } + + let _git_config_global = GitConfigGlobalGuard(env::var_os("GIT_CONFIG_GLOBAL")); + env::set_var("GIT_CONFIG_GLOBAL", &global_gitconfig); let mut scanner = Scanner::new(sources); let candidates = scanner.scan(); - if let Some(old) = old_git_config_global { - std::env::set_var("GIT_CONFIG_GLOBAL", old); - } else { - std::env::remove_var("GIT_CONFIG_GLOBAL"); - } - assert_eq!(candidates, vec!["content-['index.html']"]); } From 39cbde689f8045b010c1b0476326ccd2abf8fa9a Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 30 Sep 2026 07:11:18 +0200 Subject: [PATCH 3/4] ci: retrigger pull request checks From 172a4c02d80c902543f57890dafc9afedd3d9e5c Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 2 Oct 2026 18:25:04 +0200 Subject: [PATCH 4/4] test: isolate global git config environment --- crates/oxide/tests/scanner.rs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index 64720d716917..e36c2fe55ded 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -6,7 +6,6 @@ mod scanner { use std::process::Command; use std::thread::sleep; use std::time::Duration; - use std::sync::{Mutex, OnceLock}; use std::{env, fs, path}; use tailwindcss_oxide::*; @@ -2926,8 +2925,15 @@ mod scanner { #[test] fn respects_git_core_excludes_file() { - static ENV_LOCK: OnceLock> = OnceLock::new(); - let _lock = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap(); + 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(); @@ -2964,19 +2970,6 @@ mod scanner { "@source '**/*'", )]; - struct GitConfigGlobalGuard(Option); - - impl Drop for GitConfigGlobalGuard { - fn drop(&mut self) { - if let Some(old) = self.0.take() { - env::set_var("GIT_CONFIG_GLOBAL", old); - } else { - env::remove_var("GIT_CONFIG_GLOBAL"); - } - } - } - - let _git_config_global = GitConfigGlobalGuard(env::var_os("GIT_CONFIG_GLOBAL")); env::set_var("GIT_CONFIG_GLOBAL", &global_gitconfig); let mut scanner = Scanner::new(sources);