Don’t look at ignore files outside initialized repos (#15941)
Right now, when Oxide is scanning for files, it considers ignore files in the "root" directory it is scanning as well as all parent directories. We honor .gitignore files even when not in a git repo as an optimization in case a project has been created, contains a .gitignore, but no repo has actually been initialized. However, this has an unintended side effect of including ignore files _ouside of a repo_ when there is one. This means that if you have a .gitignore file in your home folder it'll get applied even when you're inside a git repo which is not what you'd expect. This PR addresses this by checking to see the folder being scanned is inside a repo and turns on a flag that ensures .gitignore files from the repo are the only ones used (global ignore files configured in git still work tho). This still needs lots of tests to make sure things work as expected. Fixes #15876 --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
86264a9e66
commit
e02a29fa94
5 changed files with 260 additions and 10 deletions
|
|
@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Suggest container query variants ([#15857](https://github.com/tailwindlabs/tailwindcss/pull/15857))
|
||||
- Disable bare value suggestions when not using the `--spacing` variable ([#15857](https://github.com/tailwindlabs/tailwindcss/pull/15857))
|
||||
- Ensure suggested classes are properly sorted ([#15857](https://github.com/tailwindlabs/tailwindcss/pull/15857))
|
||||
- Don’t look at ignore files outside initialized repos ([#15941](https://github.com/tailwindlabs/tailwindcss/pull/15941))
|
||||
- _Upgrade_: Ensure JavaScript config files on different drives are correctly migrated ([#15927](https://github.com/tailwindlabs/tailwindcss/pull/15927))
|
||||
|
||||
## [4.0.0] - 2025-01-21
|
||||
|
|
|
|||
|
|
@ -33,17 +33,11 @@ pub fn resolve_allowed_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn resolve_paths(root: &Path) -> impl Iterator<Item = DirEntry> {
|
||||
WalkBuilder::new(root)
|
||||
.hidden(false)
|
||||
.require_git(false)
|
||||
.build()
|
||||
.filter_map(Result::ok)
|
||||
create_walk_builder(root).build().filter_map(Result::ok)
|
||||
}
|
||||
|
||||
pub fn read_dir(root: &Path, depth: Option<usize>) -> impl Iterator<Item = DirEntry> {
|
||||
WalkBuilder::new(root)
|
||||
.hidden(false)
|
||||
.require_git(false)
|
||||
create_walk_builder(root)
|
||||
.max_depth(depth)
|
||||
.filter_entry(move |entry| match entry.file_type() {
|
||||
Some(file_type) if file_type.is_dir() => match entry.file_name().to_str() {
|
||||
|
|
@ -59,6 +53,61 @@ pub fn read_dir(root: &Path, depth: Option<usize>) -> impl Iterator<Item = DirEn
|
|||
.filter_map(Result::ok)
|
||||
}
|
||||
|
||||
fn create_walk_builder(root: &Path) -> WalkBuilder {
|
||||
let mut builder = WalkBuilder::new(root);
|
||||
|
||||
// Scan hidden files / directories
|
||||
builder.hidden(false);
|
||||
|
||||
// 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
|
||||
// are first created and may not be in a git repo yet.
|
||||
builder.require_git(false);
|
||||
|
||||
// Don't descend into .git directories inside the root folder
|
||||
// This is necessary when `root` contains the `.git` dir.
|
||||
builder.filter_entry(|entry| entry.file_name() != ".git");
|
||||
|
||||
// If we are in a git repo then require it to ensure that only rules within
|
||||
// the repo are used. For example, we don't want to consider a .gitignore file
|
||||
// in the user's home folder if we're in a git repo.
|
||||
//
|
||||
// The alternative is using a call like `.parents(false)` but that will
|
||||
// prevent looking at parent directories for .gitignore files from within
|
||||
// the repo and that's not what we want.
|
||||
//
|
||||
// For example, in a project with this structure:
|
||||
//
|
||||
// home
|
||||
// .gitignore
|
||||
// my-project
|
||||
// .gitignore
|
||||
// apps
|
||||
// .gitignore
|
||||
// web
|
||||
// {root}
|
||||
//
|
||||
// We do want to consider all .gitignore files listed:
|
||||
// - home/.gitignore
|
||||
// - my-project/.gitignore
|
||||
// - my-project/apps/.gitignore
|
||||
//
|
||||
// However, if a repo is initialized inside my-project then only the following
|
||||
// make sense for consideration:
|
||||
// - my-project/.gitignore
|
||||
// - my-project/apps/.gitignore
|
||||
//
|
||||
// Setting the require_git(true) flag conditionally allows us to do this.
|
||||
for parent in root.ancestors() {
|
||||
if parent.join(".git").exists() {
|
||||
builder.require_git(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
pub fn is_allowed_content_path(path: &Path) -> bool {
|
||||
// Skip known ignored files
|
||||
if path
|
||||
|
|
|
|||
|
|
@ -586,4 +586,121 @@ mod scanner {
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_ignore_files_outside_of_a_repo() {
|
||||
// Create a temporary working directory
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
|
||||
// Create files
|
||||
create_files_in(
|
||||
&dir,
|
||||
&[
|
||||
// This file should always be picked up
|
||||
("home/project/apps/web/index.html", "content-['index.html']"),
|
||||
// Set up various ignore rules
|
||||
("home/.gitignore", "ignore-home.html"),
|
||||
("home/project/.gitignore", "ignore-project.html"),
|
||||
("home/project/apps/.gitignore", "ignore-apps.html"),
|
||||
("home/project/apps/web/.gitignore", "ignore-web.html"),
|
||||
// Some of these should be ignored depending on which dir is the repo root
|
||||
(
|
||||
"home/project/apps/web/ignore-home.html",
|
||||
"content-['ignore-home.html']",
|
||||
),
|
||||
(
|
||||
"home/project/apps/web/ignore-project.html",
|
||||
"content-['ignore-project.html']",
|
||||
),
|
||||
(
|
||||
"home/project/apps/web/ignore-apps.html",
|
||||
"content-['ignore-apps.html']",
|
||||
),
|
||||
(
|
||||
"home/project/apps/web/ignore-web.html",
|
||||
"content-['ignore-web.html']",
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
let sources = vec![GlobEntry {
|
||||
base: dir
|
||||
.join("home/project/apps/web")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
pattern: "**/*".to_owned(),
|
||||
}];
|
||||
|
||||
let candidates = Scanner::new(Some(sources.clone())).scan();
|
||||
|
||||
// All ignore files are applied because there's no git repo
|
||||
assert_eq!(candidates, vec!["content-['index.html']".to_owned(),]);
|
||||
|
||||
// Initialize `home` as a git repository and scan again
|
||||
// The results should be the same as before
|
||||
_ = Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(dir.join("home"))
|
||||
.output();
|
||||
let candidates = Scanner::new(Some(sources.clone())).scan();
|
||||
|
||||
assert_eq!(candidates, vec!["content-['index.html']".to_owned(),]);
|
||||
|
||||
// Drop the .git folder
|
||||
fs::remove_dir_all(dir.join("home/.git")).unwrap();
|
||||
|
||||
// Initialize `home/project` as a git repository and scan again
|
||||
_ = Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(dir.join("home/project"))
|
||||
.output();
|
||||
let candidates = Scanner::new(Some(sources.clone())).scan();
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['ignore-home.html']".to_owned(),
|
||||
"content-['index.html']".to_owned(),
|
||||
]
|
||||
);
|
||||
|
||||
// Drop the .git folder
|
||||
fs::remove_dir_all(dir.join("home/project/.git")).unwrap();
|
||||
|
||||
// Initialize `home/project/apps` as a git repository and scan again
|
||||
_ = Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(dir.join("home/project/apps"))
|
||||
.output();
|
||||
let candidates = Scanner::new(Some(sources.clone())).scan();
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['ignore-home.html']".to_owned(),
|
||||
"content-['ignore-project.html']".to_owned(),
|
||||
"content-['index.html']".to_owned(),
|
||||
]
|
||||
);
|
||||
|
||||
// Drop the .git folder
|
||||
fs::remove_dir_all(dir.join("home/project/apps/.git")).unwrap();
|
||||
|
||||
// Initialize `home/project/apps` as a git repository and scan again
|
||||
_ = Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(dir.join("home/project/apps/web"))
|
||||
.output();
|
||||
let candidates = Scanner::new(Some(sources.clone())).scan();
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['ignore-apps.html']".to_owned(),
|
||||
"content-['ignore-home.html']".to_owned(),
|
||||
"content-['ignore-project.html']".to_owned(),
|
||||
"content-['index.html']".to_owned(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -556,6 +556,83 @@ describe.each([
|
|||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'git ignore files outside of a repo are not considered',
|
||||
{
|
||||
fs: {
|
||||
// Ignore everything in the "home" directory
|
||||
'home/.gitignore': '*',
|
||||
|
||||
// Only ignore files called ignore-*.html in the actual git repo
|
||||
'home/project/.gitignore': 'ignore-*.html',
|
||||
|
||||
'home/project/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
'home/project/src/index.css': css` @import 'tailwindcss'; `,
|
||||
'home/project/src/index.html': html`
|
||||
<div
|
||||
class="content-['index.html']"
|
||||
></div>
|
||||
`,
|
||||
'home/project/src/ignore-1.html': html`
|
||||
<div
|
||||
class="content-['ignore-1.html']"
|
||||
></div>
|
||||
`,
|
||||
'home/project/src/ignore-2.html': html`
|
||||
<div
|
||||
class="content-['ignore-2.html']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
|
||||
installDependencies: false,
|
||||
},
|
||||
async ({ fs, root, exec }) => {
|
||||
await exec(`pnpm install --ignore-workspace`, {
|
||||
cwd: path.join(root, 'home/project'),
|
||||
})
|
||||
|
||||
// No git repo = all ignore files are considered
|
||||
await exec(`${command} --input src/index.css --output dist/out.css`, {
|
||||
cwd: path.join(root, 'home/project'),
|
||||
})
|
||||
|
||||
await fs.expectFileNotToContain('./home/project/dist/out.css', [
|
||||
candidate`content-['index.html']`,
|
||||
candidate`content-['ignore-1.html']`,
|
||||
candidate`content-['ignore-2.html']`,
|
||||
])
|
||||
|
||||
// Make home/project a git repo
|
||||
// Only ignore files within the repo are considered
|
||||
await exec(`git init`, {
|
||||
cwd: path.join(root, 'home/project'),
|
||||
})
|
||||
|
||||
await exec(`${command} --input src/index.css --output dist/out.css`, {
|
||||
cwd: path.join(root, 'home/project'),
|
||||
})
|
||||
|
||||
await fs.expectFileToContain('./home/project/dist/out.css', [
|
||||
candidate`content-['index.html']`,
|
||||
])
|
||||
|
||||
await fs.expectFileNotToContain('./home/project/dist/out.css', [
|
||||
candidate`content-['ignore-1.html']`,
|
||||
candidate`content-['ignore-2.html']`,
|
||||
])
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ interface TestConfig {
|
|||
fs: {
|
||||
[filePath: string]: string | Uint8Array
|
||||
}
|
||||
|
||||
installDependencies?: boolean
|
||||
}
|
||||
interface TestContext {
|
||||
root: string
|
||||
|
|
@ -382,14 +384,18 @@ export function test(
|
|||
await context.fs.write(filename, content)
|
||||
}
|
||||
|
||||
let shouldInstallDependencies = config.installDependencies ?? true
|
||||
|
||||
try {
|
||||
// In debug mode, the directory is going to be inside the pnpm workspace
|
||||
// of the tailwindcss package. This means that `pnpm install` will run
|
||||
// pnpm install on the workspace instead (expect if the root dir defines
|
||||
// a separate workspace). We work around this by using the
|
||||
// `--ignore-workspace` flag.
|
||||
let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml']
|
||||
await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
|
||||
if (shouldInstallDependencies) {
|
||||
let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml']
|
||||
await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
console.error(error.stdout?.toString())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue