Repository: pantsbuild/pants

Issue:
Cannot exclude subsets from being build_ignored
https://www.pantsbuild.org/docs/reference-global#section-build-ignore states that build_ignore uses the gitignore pattern syntax. This implies that using this in build_ignore: `["foo/", "!foo/bar"]` will exclude everything under `foo` except `foo/bar`. 

But because we [apply](https://github.com/pantsbuild/pants/blob/e47a627db82425697969e2eca679125aca17c6e3/src/python/pants/base/specs.py#L187)  the build_ignores as PathGlobs prefixed by `!`, the exclusion in the example above will become `!!foo/bar`, which does not have the desired effect. 

Note that we can't simply strip the double `!!`, because it is not the case that we must always include `foo/bar`, only that we must not exclude it if it's included by the specs. So it's not immediately obvious to me how to solve this.


Discussion:
> This implies that using this in build_ignore: `["foo/", "!foo/bar"]` will exclude everything under `foo` except `foo/bar`.

Unfortunately, this is not supported by [the gitignore syntax](https://git-scm.com/docs/gitignore):
> It is not possible to re-include a file if a parent directory of that file is excluded. Git doesn’t list excluded directories for performance reasons, so any patterns on contained files have no effect, no matter where they are defined.

So we would need to diverge from Git's own implementation on this... and their justification applies to us as well.
Ah: but you're right that `build_ignore` is being parsed more naively than `pants_ignore` (which is given directly to a `GitIgnore` instance unmodified).

And... I think that my above comment doesn't necessarily apply in this case, because `build_ignore` is converted into globs, unlike `pants_ignore`. Will take a look.
Ah yeah, good point, we're no worse than gitignore for the case I mention. We can probably close this by rephrasing the documentation for build_ignore to say that we support filesystem globs, and not mention gitignore.
https://github.com/kenhowardpdx/pants-issues/tree/build-ignore-negation

Here's a reduced test case for the issue @benjyw describes. What would be an acceptable filesystem glob pattern for this use case?
> https://github.com/kenhowardpdx/pants-issues/tree/build-ignore-negation
> 
> Here's a reduced test case for the issue @benjyw describes. What would be an acceptable filesystem glob pattern for this use case?

```toml
build_ignore = [
  "projects/goodbye",
]
```
...would do it.

Resolving patch:
diff --git a/src/python/pants/option/global_options.py b/src/python/pants/option/global_options.py
--- a/src/python/pants/option/global_options.py
+++ b/src/python/pants/option/global_options.py
@@ -1460,12 +1460,10 @@ class GlobalOptions(BootstrapOptions, Subsystem):
         "--build-ignore",
         help=softwrap(
             """
-            Paths to ignore when identifying BUILD files.
+            Path globs or literals to ignore when identifying BUILD files.
 
             This does not affect any other filesystem operations; use `--pants-ignore` for
             that instead.
-
-            Patterns use the gitignore pattern syntax (https://git-scm.com/docs/gitignore).
             """
         ),
         advanced=True,
@@ -1620,6 +1618,13 @@ def validate_remote_headers(opt_name: str) -> None:
         validate_remote_headers("remote_execution_headers")
         validate_remote_headers("remote_store_headers")
 
+        illegal_build_ignores = [i for i in opts.build_ignore if i.startswith("!")]
+        if illegal_build_ignores:
+            raise OptionsError(
+                "The `--build-ignore` option does not support negated globs, but was "
+                f"given: {illegal_build_ignores}."
+            )
+
     @staticmethod
     def create_py_executor(bootstrap_options: OptionValueContainer) -> PyExecutor:
         rule_threads_max = (


Test patch: