Skip to content

Commit 1ef9775

Browse files
Add @source not support (#17255)
This PR adds a new source detection feature: `@source not "…"`. It can be used to exclude files specifically from your source configuration without having to think about creating a rule that matches all but the requested file: ```css @import "tailwindcss"; @source not "../src/my-tailwind-js-plugin.js"; ``` While working on this feature, we noticed that there are multiple places with different heuristics we used to scan the file system. These are: - Auto source detection (so the default configuration or an `@source "./my-dir"`) - Custom sources ( e.g. `@source "./**/*.bin"` — these contain file extensions) - The code to detect updates on the file system Because of the different heuristics, we were able to construct failing cases (e.g. when you create a new file into `my-dir` that would be thrown out by auto-source detection, it'd would actually be scanned). We were also leaving a lot of performance on the table as the file system is traversed multiple times for certain problems. To resolve these issues, we're now unifying all of these systems into one `ignore` crate walker setup. We also implemented features like auto-source-detection and the `not` flag as additional _gitignore_ rules only, avoid the need for a lot of custom code needed to make decisions. High level, this is what happens after the now: - We collect all non-negative `@source` rules into a list of _roots_ (that is the source directory for this rule) and optional _globs_ (that is the actual rules for files in this file). For custom sources (i.e with a custom `glob`), we add an allowlist rule to the gitignore setup, so that we can be sure these files are always included. - For every negative `@source` rule, we create respective ignore rules. - Furthermore we have a custom filter that ensures files are only read if they have been changed since the last time they were read. So, consider the following setup: ```css /* packages/web/src/index.css */ @import "tailwindcss"; @source "../../lib/ui/**/*.bin"; @source not "../../lib/ui/expensive.bin"; ``` This creates a git ignore file that (simplified) looks like this: ```gitignore # Auto-source rules *.{exe,node,bin,…} *.{css,scss,sass,…} {node_modules,git}/ # Custom sources can overwrite auto-source rules !lib/ui/**/*.bin # Negative rules lib/ui/expensive.bin ``` We then use this information _on top of your existing `.gitignore` setup_ to resolve files (i.e so if your `.gitignore` contains rules e.g. `dist/` this line is going to be added _before_ any of the rules lined out in the example above. This allows negative rules to allow-list your `.gitignore` rules. To implement this, we're rely on the `ignore` crate but we had to make various changes, very specific, to it so we decided to fork the crate. All changes are prefixed with a `// CHANGED:` block but here are the most-important ones: - We added a way to add custom ignore rules that _extend_ (rather than overwrite) your existing `.gitignore` rules - We updated the order in which files are resolved and made it so that more-specific files can allow-list more generic ignore rules. - We resolved various issues related to adding more than one base path to the traversal and ensured it works consistent for Linux, macOS, and Windows. ## Behavioral changes 1. Any custom glob defined via `@source` now wins over your `.gitignore` file and the auto-content rules. - Resolves #16920 3. The `node_modules` and `.git` folders as well as the `.gitignore` file are now ignored by default (but can be overridden by an explicit `@source` rule). - Resolves #17318 - Resolves #15882 4. Source paths into ignored-by-default folders (like `node_modules`) now also win over your `.gitignore` configuration and auto-content rules. - Resolves #16669 5. Introduced `@source not "…"` to negate any previous rules. - Resolves #17058 6. Negative `content` rules in your legacy JavaScript configuration (e.g. `content: ['!./src']`) now work with v4. - Resolves #15943 7. The order of `@source` definitions matter now, because you can technically include or negate previous rules. This is similar to your `.gitingore` file. 9. Rebuilds in watch mode now take the `@source` configuration into account - Resolves #15684 ## Combining with other features Note that the `not` flag is also already compatible with [`@source inline(…)`](#17147) added in an earlier commit: ```css @import "tailwindcss"; @source not inline("container"); ``` ## Test plan - We added a bunch of oxide unit tests to ensure that the right files are scanned - We updated the existing integration tests with new `@source not "…"` specific examples and updated the existing tests to match the subtle behavior changes - We also added a new special tag `[ci-all]` that, when added to the description of a PR, causes the PR to run unit and integration tests on all operating systems. [ci-all] --------- Co-authored-by: Philipp Spiess <[email protected]>
1 parent f44cfff commit 1ef9775

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+10163
-1721
lines changed

.github/workflows/ci.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ jobs:
2525
os: macos-14
2626

2727
# Exclude windows and macos from being built on feature branches
28-
on-main-branch:
29-
- ${{ github.ref == 'refs/heads/main' }}
28+
run-all:
29+
- ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') }}
3030
exclude:
31-
- on-main-branch: false
31+
- run-all: false
3232
runner:
3333
name: Windows
34-
- on-main-branch: false
34+
- run-all: false
3535
runner:
3636
name: macOS
3737

.github/workflows/integration-tests.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ jobs:
3333
- workers
3434

3535
# Exclude windows and macos from being built on feature branches
36-
on-main-branch:
37-
- ${{ github.ref == 'refs/heads/main' }}
36+
run-all:
37+
- ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.body, '[ci-all]') }}
3838
exclude:
39-
- on-main-branch: false
39+
- run-all: false
4040
runner:
4141
name: Windows
42-
- on-main-branch: false
42+
- run-all: false
4343
runner:
4444
name: macOS
4545

Cargo.lock

+21-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ignore/COPYING

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This project is dual-licensed under the Unlicense and MIT licenses.
2+
3+
You may use this code under the terms of either license.

crates/ignore/Cargo.toml

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[package]
2+
name = "ignore"
3+
version = "0.4.23" #:version
4+
authors = ["Andrew Gallant <[email protected]>"]
5+
description = """
6+
A fast library for efficiently matching ignore files such as `.gitignore`
7+
against file paths.
8+
"""
9+
documentation = "https://docs.rs/ignore"
10+
homepage = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
11+
repository = "https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore"
12+
readme = "README.md"
13+
keywords = ["glob", "ignore", "gitignore", "pattern", "file"]
14+
license = "Unlicense OR MIT"
15+
edition = "2021"
16+
17+
[lib]
18+
name = "ignore"
19+
bench = false
20+
21+
[dependencies]
22+
crossbeam-deque = "0.8.3"
23+
globset = "0.4.16"
24+
log = "0.4.20"
25+
memchr = "2.6.3"
26+
same-file = "1.0.6"
27+
walkdir = "2.4.0"
28+
dunce = "1.0.5"
29+
30+
[dependencies.regex-automata]
31+
version = "0.4.0"
32+
default-features = false
33+
features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"]
34+
35+
[target.'cfg(windows)'.dependencies.winapi-util]
36+
version = "0.1.2"
37+
38+
[dev-dependencies]
39+
bstr = { version = "1.6.2", default-features = false, features = ["std"] }
40+
crossbeam-channel = "0.5.8"
41+
42+
[features]
43+
# DEPRECATED. It is a no-op. SIMD is done automatically through runtime
44+
# dispatch.
45+
simd-accel = []

crates/ignore/LICENSE-MIT

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Andrew Gallant
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

crates/ignore/README.md

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# ignore
2+
3+
The ignore crate provides a fast recursive directory iterator that respects
4+
various filters such as globs, file types and `.gitignore` files. This crate
5+
also provides lower level direct access to gitignore and file type matchers.
6+
7+
[![Build status](https://github.com/BurntSushi/ripgrep/workflows/ci/badge.svg)](https://github.com/BurntSushi/ripgrep/actions)
8+
[![](https://img.shields.io/crates/v/ignore.svg)](https://crates.io/crates/ignore)
9+
10+
Dual-licensed under MIT or the [UNLICENSE](https://unlicense.org/).
11+
12+
### Documentation
13+
14+
[https://docs.rs/ignore](https://docs.rs/ignore)
15+
16+
### Usage
17+
18+
Add this to your `Cargo.toml`:
19+
20+
```toml
21+
[dependencies]
22+
ignore = "0.4"
23+
```
24+
25+
### Example
26+
27+
This example shows the most basic usage of this crate. This code will
28+
recursively traverse the current directory while automatically filtering out
29+
files and directories according to ignore globs found in files like
30+
`.ignore` and `.gitignore`:
31+
32+
```rust,no_run
33+
use ignore::Walk;
34+
35+
for result in Walk::new("./") {
36+
// Each item yielded by the iterator is either a directory entry or an
37+
// error, so either print the path or the error.
38+
match result {
39+
Ok(entry) => println!("{}", entry.path().display()),
40+
Err(err) => println!("ERROR: {}", err),
41+
}
42+
}
43+
```
44+
45+
### Example: advanced
46+
47+
By default, the recursive directory iterator will ignore hidden files and
48+
directories. This can be disabled by building the iterator with `WalkBuilder`:
49+
50+
```rust,no_run
51+
use ignore::WalkBuilder;
52+
53+
for result in WalkBuilder::new("./").hidden(false).build() {
54+
println!("{:?}", result);
55+
}
56+
```
57+
58+
See the documentation for `WalkBuilder` for many other options.

crates/ignore/UNLICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <http://unlicense.org/>

crates/ignore/examples/walk.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::{env, io::Write, path::Path};
2+
3+
use {bstr::ByteVec, ignore::WalkBuilder, walkdir::WalkDir};
4+
5+
fn main() {
6+
let mut path = env::args().nth(1).unwrap();
7+
let mut parallel = false;
8+
let mut simple = false;
9+
let (tx, rx) = crossbeam_channel::bounded::<DirEntry>(100);
10+
if path == "parallel" {
11+
path = env::args().nth(2).unwrap();
12+
parallel = true;
13+
} else if path == "walkdir" {
14+
path = env::args().nth(2).unwrap();
15+
simple = true;
16+
}
17+
18+
let stdout_thread = std::thread::spawn(move || {
19+
let mut stdout = std::io::BufWriter::new(std::io::stdout());
20+
for dent in rx {
21+
stdout.write(&*Vec::from_path_lossy(dent.path())).unwrap();
22+
stdout.write(b"\n").unwrap();
23+
}
24+
});
25+
26+
if parallel {
27+
let walker = WalkBuilder::new(path).threads(6).build_parallel();
28+
walker.run(|| {
29+
let tx = tx.clone();
30+
Box::new(move |result| {
31+
use ignore::WalkState::*;
32+
33+
tx.send(DirEntry::Y(result.unwrap())).unwrap();
34+
Continue
35+
})
36+
});
37+
} else if simple {
38+
let walker = WalkDir::new(path);
39+
for result in walker {
40+
tx.send(DirEntry::X(result.unwrap())).unwrap();
41+
}
42+
} else {
43+
let walker = WalkBuilder::new(path).build();
44+
for result in walker {
45+
tx.send(DirEntry::Y(result.unwrap())).unwrap();
46+
}
47+
}
48+
drop(tx);
49+
stdout_thread.join().unwrap();
50+
}
51+
52+
enum DirEntry {
53+
X(walkdir::DirEntry),
54+
Y(ignore::DirEntry),
55+
}
56+
57+
impl DirEntry {
58+
fn path(&self) -> &Path {
59+
match *self {
60+
DirEntry::X(ref x) => x.path(),
61+
DirEntry::Y(ref y) => y.path(),
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)