Repository: pantsbuild/pants

Issue:
Deleted files cause `pants tailor` with `--changed-since` to fail
**Describe the bug**

I use the following command in CI to validate the monorepo, as recommended by the docs:

```shell
> pants \
	  --changed-since=origin/main \
	  tailor --check \
	  update-build-files --check \
	  lint
```

However, if I delete a package, including its `BUILD` file in a PR, the `--changed-since` flag causes `tailor` to try to run on those files, which `pants` blows up on:

```shell
16:40:57.91 [ERROR] 1 Exception encountered:

Engine traceback:
  in `tailor` goal

IntrinsicError: Unmatched glob from `--changed-since`: "aws/projects/my_project_name/*"

Do the file(s) exist? If so, check if the file(s) are in your `.gitignore` or the global `pants_ignore` option, which may result in Pants not being able to see the file(s) even though they exist on disk. Refer to https://www.pantsbuild.org/v2.19/docs/troubleshooting#pants-cannot-find-a-file-in-your-project.


Exited with code exit status 1
```

If files are deleted, yes, they are changed, but they shouldn't throw an error.

**Pants version**

2.19.0

**OS**

Linux (CircleCI Ubuntu executor)

**Additional info**

N/A



Discussion:
Thanks for the bug report. This feels familiar, but I can't find an existing issue for it.
Sure - thanks for the project! I also filed https://github.com/pantsbuild/pants/issues/20479 last week that's similar, but not exactly this issue.

Resolving patch:
diff --git a/src/python/pants/init/specs_calculator.py b/src/python/pants/init/specs_calculator.py
--- a/src/python/pants/init/specs_calculator.py
+++ b/src/python/pants/init/specs_calculator.py
@@ -4,6 +4,7 @@
 import logging
 from typing import cast
 
+from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
 from pants.base.specs import AddressLiteralSpec, FileLiteralSpec, RawSpecs, Specs
 from pants.base.specs_parser import SpecsParser
 from pants.core.util_rules.environments import determine_bootstrap_environment
@@ -96,7 +97,11 @@ def calculate_specs(
             # target-aware vs. target-less goals, e.g. `list` vs `count-loc`.
             address_literals=tuple(address_literal_specs),
             file_literals=file_literal_specs,
-            unmatched_glob_behavior=unmatched_cli_globs,
+            # The globs here are synthesized from VCS data by the `changed` mechanism.
+            # As such it does not make sense to apply user-facing matching errors to them.
+            # In particular, they can legitimately not match anything, if entire git
+            # subtrees were deleted for example.
+            unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,
             filter_by_global_options=True,
             from_change_detection=True,
             description_of_origin="`--changed-since`",
diff --git a/src/python/pants/vcs/git.py b/src/python/pants/vcs/git.py
--- a/src/python/pants/vcs/git.py
+++ b/src/python/pants/vcs/git.py
@@ -91,7 +91,7 @@ def changed_files(
             committed_changes = self._git_binary._invoke_unsandboxed(
                 self._create_git_cmdline(committed_cmd)
             )
-            files.update(committed_changes.split())
+            files.update(committed_changes.splitlines())
         if include_untracked:
             untracked_cmd = [
                 "ls-files",
@@ -102,14 +102,14 @@ def changed_files(
             untracked = self._git_binary._invoke_unsandboxed(
                 self._create_git_cmdline(untracked_cmd)
             )
-            files.update(untracked.split())
+            files.update(untracked.splitlines())
         # git will report changed files relative to the worktree: re-relativize to relative_to
         return {self._fix_git_relative_path(f, relative_to) for f in files}
 
     def changes_in(self, diffspec: str, relative_to: PurePath | str | None = None) -> set[str]:
         relative_to = PurePath(relative_to) if relative_to is not None else self.worktree
         cmd = ["diff-tree", "--no-commit-id", "--name-only", "-r", diffspec]
-        files = self._git_binary._invoke_unsandboxed(self._create_git_cmdline(cmd)).split()
+        files = self._git_binary._invoke_unsandboxed(self._create_git_cmdline(cmd)).splitlines()
         return {self._fix_git_relative_path(f.strip(), relative_to) for f in files}
 
     def _create_git_cmdline(self, args: Iterable[str]) -> list[str]:


Test patch:
diff --git a/src/python/pants/vcs/git_test.py b/src/python/pants/vcs/git_test.py
--- a/src/python/pants/vcs/git_test.py
+++ b/src/python/pants/vcs/git_test.py
@@ -139,6 +139,9 @@ def test_integration(worktree: Path, readme_file: Path, git: MutatingGitWorktree
     assert {"README"} == git.changed_files()
     assert {"README", "INSTALL"} == git.changed_files(include_untracked=True)
 
+    (worktree / "WITH SPACE").write_text("space in path")
+    assert {"README", "INSTALL", "WITH SPACE"} == git.changed_files(include_untracked=True)
+
     # Confirm that files outside of a given relative_to path are ignored
     assert set() == git.changed_files(relative_to="non-existent")