We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/python/pants/init/specs_calculator.py`
Content:
```
1 # Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
2 # Licensed under the Apache License, Version 2.0 (see LICENSE).
3 
4 import logging
5 from typing import cast
6 
7 from pants.base.specs import AddressLiteralSpec, FileLiteralSpec, RawSpecs, Specs
8 from pants.base.specs_parser import SpecsParser
9 from pants.core.util_rules.environments import determine_bootstrap_environment
10 from pants.core.util_rules.system_binaries import GitBinary
11 from pants.engine.addresses import AddressInput
12 from pants.engine.environment import EnvironmentName
13 from pants.engine.internals.scheduler import SchedulerSession
14 from pants.engine.internals.selectors import Params
15 from pants.engine.rules import QueryRule
16 from pants.option.options import Options
17 from pants.option.options_bootstrapper import OptionsBootstrapper
18 from pants.util.frozendict import FrozenDict
19 from pants.vcs.changed import ChangedAddresses, ChangedOptions, ChangedRequest
20 from pants.vcs.git import GitWorktreeRequest, MaybeGitWorktree
21 
22 logger = logging.getLogger(__name__)
23 
24 
25 class InvalidSpecConstraint(Exception):
26     """Raised when invalid constraints are given via specs and arguments like --changed*."""
27 
28 
29 def calculate_specs(
30     options_bootstrapper: OptionsBootstrapper,
31     options: Options,
32     session: SchedulerSession,
33     working_dir: str,
34 ) -> Specs:
35     """Determine the specs for a given Pants run."""
36     global_options = options.for_global_scope()
37     unmatched_cli_globs = global_options.unmatched_cli_globs
38     specs = SpecsParser(working_dir=working_dir).parse_specs(
39         options.specs,
40         description_of_origin="CLI arguments",
41         unmatched_glob_behavior=unmatched_cli_globs,
42     )
43 
44     changed_options = ChangedOptions.from_options(options.for_scope("changed"))
45     logger.debug("specs are: %s", specs)
46     logger.debug("changed_options are: %s", changed_options)
47 
48     if specs and changed_options.provided:
49         changed_name = "--changed-since" if changed_options.since else "--changed-diffspec"
50         specs_description = specs.arguments_provided_description()
51         assert specs_description is not None
52         raise InvalidSpecConstraint(
53             f"You used `{changed_name}` at the same time as using {specs_description}. You can "
54             f"only use `{changed_name}` or use normal arguments."
55         )
56 
57     if not changed_options.provided:
58         return specs
59 
60     bootstrap_environment = determine_bootstrap_environment(session)
61 
62     (git_binary,) = session.product_request(GitBinary, [Params(bootstrap_environment)])
63     (maybe_git_worktree,) = session.product_request(
64         MaybeGitWorktree, [Params(GitWorktreeRequest(), git_binary, bootstrap_environment)]
65     )
66     if not maybe_git_worktree.git_worktree:
67         raise InvalidSpecConstraint(
68             "The `--changed-*` options are only available if Git is used for the repository."
69         )
70 
71     changed_files = tuple(changed_options.changed_files(maybe_git_worktree.git_worktree))
72     file_literal_specs = tuple(FileLiteralSpec(f) for f in changed_files)
73 
74     changed_request = ChangedRequest(changed_files, changed_options.dependents)
75     (changed_addresses,) = session.product_request(
76         ChangedAddresses,
77         [Params(changed_request, options_bootstrapper, bootstrap_environment)],
78     )
79     logger.debug("changed addresses: %s", changed_addresses)
80 
81     address_literal_specs = []
82     for address in cast(ChangedAddresses, changed_addresses):
83         address_input = AddressInput.parse(address.spec, description_of_origin="`--changed-since`")
84         address_literal_specs.append(
85             AddressLiteralSpec(
86                 path_component=address_input.path_component,
87                 target_component=address_input.target_component,
88                 generated_component=address_input.generated_component,
89                 parameters=FrozenDict(address_input.parameters),
90             )
91         )
92 
93     return Specs(
94         includes=RawSpecs(
95             # We need both address_literals and file_literals to cover all our edge cases, including
96             # target-aware vs. target-less goals, e.g. `list` vs `count-loc`.
97             address_literals=tuple(address_literal_specs),
98             file_literals=file_literal_specs,
99             unmatched_glob_behavior=unmatched_cli_globs,
100             filter_by_global_options=True,
101             from_change_detection=True,
102             description_of_origin="`--changed-since`",
103         ),
104         ignores=RawSpecs(description_of_origin="`--changed-since`"),
105     )
106 
107 
108 def rules():
109     return [
110         QueryRule(ChangedAddresses, [ChangedRequest, EnvironmentName]),
111         QueryRule(GitBinary, [EnvironmentName]),
112         QueryRule(MaybeGitWorktree, [GitWorktreeRequest, GitBinary, EnvironmentName]),
113     ]
114
```

Path: `src/python/pants/vcs/git.py`
Content:
```
1 # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
2 # Licensed under the Apache License, Version 2.0 (see LICENSE).
3 
4 from __future__ import annotations
5 
6 import dataclasses
7 import logging
8 import os
9 from dataclasses import dataclass
10 from os import PathLike
11 from pathlib import Path, PurePath
12 from typing import Any, Iterable
13 
14 from pants.core.util_rules.system_binaries import GitBinary, GitBinaryException, MaybeGitBinary
15 from pants.engine.engine_aware import EngineAwareReturnType
16 from pants.engine.rules import collect_rules, rule
17 from pants.util.contextutil import pushd
18 
19 logger = logging.getLogger(__name__)
20 
21 
22 class GitWorktree(EngineAwareReturnType):
23     """Implements a safe wrapper for un-sandboxed access to Git in the user's working copy.
24 
25     This type (and any wrappers) should be marked `EngineAwareReturnType.cacheable=False`, because
26     it internally uses un-sandboxed APIs, and `@rules` which produce it should re-run in each
27     session. It additionally implements a default `__eq__` in order to prevent early-cutoff in the
28     graph, and force any consumers of the type to re-run.
29     """
30 
31     worktree: PurePath
32     _gitdir: PurePath
33     _git_binary: GitBinary
34 
35     def __init__(
36         self,
37         binary: GitBinary,
38         worktree: PathLike[str] | None = None,
39         gitdir: PathLike[str] | None = None,
40     ) -> None:
41         """Creates a git object that assumes the git repository is in the cwd by default.
42 
43         binary:    The git binary to use.
44         worktree:  The path to the git repository working tree directory (typically '.').
45         gitdir:    The path to the repository's git metadata directory (typically '.git').
46         """
47         self.worktree = Path(worktree or os.getcwd()).resolve()
48         self._gitdir = Path(gitdir).resolve() if gitdir else (self.worktree / ".git")
49         self._git_binary = binary
50 
51     def cacheable(self) -> bool:
52         return False
53 
54     @property
55     def current_rev_identifier(self):
56         return "HEAD"
57 
58     @property
59     def commit_id(self):
60         return self._git_binary._invoke_unsandboxed(self._create_git_cmdline(["rev-parse", "HEAD"]))
61 
62     @property
63     def branch_name(self) -> str | None:
64         branch = self._git_binary._invoke_unsandboxed(
65             self._create_git_cmdline(["rev-parse", "--abbrev-ref", "HEAD"])
66         )
67         return None if branch == "HEAD" else branch
68 
69     def _fix_git_relative_path(self, worktree_path: str, relative_to: PurePath | str) -> str:
70         return str((self.worktree / worktree_path).relative_to(relative_to))
71 
72     def changed_files(
73         self,
74         from_commit: str | None = None,
75         include_untracked: bool = False,
76         relative_to: PurePath | str | None = None,
77     ) -> set[str]:
78         relative_to = PurePath(relative_to) if relative_to is not None else self.worktree
79         rel_suffix = ["--", str(relative_to)]
80         uncommitted_changes = self._git_binary._invoke_unsandboxed(
81             self._create_git_cmdline(
82                 ["diff", "--name-only", "HEAD"] + rel_suffix,
83             )
84         )
85 
86         files = set(uncommitted_changes.splitlines())
87         if from_commit:
88             # Grab the diff from the merge-base to HEAD using ... syntax.  This ensures we have just
89             # the changes that have occurred on the current branch.
90             committed_cmd = ["diff", "--name-only", from_commit + "...HEAD"] + rel_suffix
91             committed_changes = self._git_binary._invoke_unsandboxed(
92                 self._create_git_cmdline(committed_cmd)
93             )
94             files.update(committed_changes.split())
95         if include_untracked:
96             untracked_cmd = [
97                 "ls-files",
98                 "--other",
99                 "--exclude-standard",
100                 "--full-name",
101             ] + rel_suffix
102             untracked = self._git_binary._invoke_unsandboxed(
103                 self._create_git_cmdline(untracked_cmd)
104             )
105             files.update(untracked.split())
106         # git will report changed files relative to the worktree: re-relativize to relative_to
107         return {self._fix_git_relative_path(f, relative_to) for f in files}
108 
109     def changes_in(self, diffspec: str, relative_to: PurePath | str | None = None) -> set[str]:
110         relative_to = PurePath(relative_to) if relative_to is not None else self.worktree
111         cmd = ["diff-tree", "--no-commit-id", "--name-only", "-r", diffspec]
112         files = self._git_binary._invoke_unsandboxed(self._create_git_cmdline(cmd)).split()
113         return {self._fix_git_relative_path(f.strip(), relative_to) for f in files}
114 
115     def _create_git_cmdline(self, args: Iterable[str]) -> list[str]:
116         return [f"--git-dir={self._gitdir}", f"--work-tree={self.worktree}", *args]
117 
118     def __eq__(self, other: Any) -> bool:
119         # NB: See the class doc regarding equality.
120         return id(self) == id(other)
121 
122 
123 @dataclass(frozen=True)
124 class MaybeGitWorktree(EngineAwareReturnType):
125     git_worktree: GitWorktree | None = None
126 
127     def cacheable(self) -> bool:
128         return False
129 
130 
131 @dataclasses.dataclass(frozen=True)
132 class GitWorktreeRequest:
133     gitdir: PathLike[str] | None = None
134     subdir: PathLike[str] | None = None
135 
136 
137 @rule
138 async def get_git_worktree(
139     git_worktree_request: GitWorktreeRequest,
140     maybe_git_binary: MaybeGitBinary,
141 ) -> MaybeGitWorktree:
142     if not maybe_git_binary.git_binary:
143         return MaybeGitWorktree()
144 
145     git_binary = maybe_git_binary.git_binary
146     cmd = ["rev-parse", "--show-toplevel"]
147 
148     try:
149         if git_worktree_request.subdir:
150             with pushd(str(git_worktree_request.subdir)):
151                 output = git_binary._invoke_unsandboxed(cmd)
152         else:
153             output = git_binary._invoke_unsandboxed(cmd)
154     except GitBinaryException as e:
155         logger.info(f"No git repository at {os.getcwd()}: {e!r}")
156         return MaybeGitWorktree()
157 
158     git_worktree = GitWorktree(
159         binary=git_binary,
160         gitdir=git_worktree_request.gitdir,
161         worktree=PurePath(output),
162     )
163 
164     logger.debug(
165         f"Detected git repository at {git_worktree.worktree} on branch {git_worktree.branch_name}"
166     )
167     return MaybeGitWorktree(git_worktree=git_worktree)
168 
169 
170 def rules():
171     return [*collect_rules()]
172
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
             # 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 @@
             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 @@
             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]:

{"golden_diff": "diff --git a/src/python/pants/init/specs_calculator.py b/src/python/pants/init/specs_calculator.py\n--- a/src/python/pants/init/specs_calculator.py\n+++ b/src/python/pants/init/specs_calculator.py\n@@ -4,6 +4,7 @@\n import logging\n from typing import cast\n \n+from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior\n from pants.base.specs import AddressLiteralSpec, FileLiteralSpec, RawSpecs, Specs\n from pants.base.specs_parser import SpecsParser\n from pants.core.util_rules.environments import determine_bootstrap_environment\n@@ -96,7 +97,11 @@\n             # target-aware vs. target-less goals, e.g. `list` vs `count-loc`.\n             address_literals=tuple(address_literal_specs),\n             file_literals=file_literal_specs,\n-            unmatched_glob_behavior=unmatched_cli_globs,\n+            # The globs here are synthesized from VCS data by the `changed` mechanism.\n+            # As such it does not make sense to apply user-facing matching errors to them.\n+            # In particular, they can legitimately not match anything, if entire git\n+            # subtrees were deleted for example.\n+            unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,\n             filter_by_global_options=True,\n             from_change_detection=True,\n             description_of_origin=\"`--changed-since`\",\ndiff --git a/src/python/pants/vcs/git.py b/src/python/pants/vcs/git.py\n--- a/src/python/pants/vcs/git.py\n+++ b/src/python/pants/vcs/git.py\n@@ -91,7 +91,7 @@\n             committed_changes = self._git_binary._invoke_unsandboxed(\n                 self._create_git_cmdline(committed_cmd)\n             )\n-            files.update(committed_changes.split())\n+            files.update(committed_changes.splitlines())\n         if include_untracked:\n             untracked_cmd = [\n                 \"ls-files\",\n@@ -102,14 +102,14 @@\n             untracked = self._git_binary._invoke_unsandboxed(\n                 self._create_git_cmdline(untracked_cmd)\n             )\n-            files.update(untracked.split())\n+            files.update(untracked.splitlines())\n         # git will report changed files relative to the worktree: re-relativize to relative_to\n         return {self._fix_git_relative_path(f, relative_to) for f in files}\n \n     def changes_in(self, diffspec: str, relative_to: PurePath | str | None = None) -> set[str]:\n         relative_to = PurePath(relative_to) if relative_to is not None else self.worktree\n         cmd = [\"diff-tree\", \"--no-commit-id\", \"--name-only\", \"-r\", diffspec]\n-        files = self._git_binary._invoke_unsandboxed(self._create_git_cmdline(cmd)).split()\n+        files = self._git_binary._invoke_unsandboxed(self._create_git_cmdline(cmd)).splitlines()\n         return {self._fix_git_relative_path(f.strip(), relative_to) for f in files}\n \n     def _create_git_cmdline(self, args: Iterable[str]) -> list[str]:\n", "issue": "Deleted files cause `pants tailor` with `--changed-since` to fail\n**Describe the bug**\r\n\r\nI use the following command in CI to validate the monorepo, as recommended by the docs:\r\n\r\n```shell\r\n> pants \\\r\n\t  --changed-since=origin/main \\\r\n\t  tailor --check \\\r\n\t  update-build-files --check \\\r\n\t  lint\r\n```\r\n\r\nHowever, 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:\r\n\r\n```shell\r\n16:40:57.91 [ERROR] 1 Exception encountered:\r\n\r\nEngine traceback:\r\n  in `tailor` goal\r\n\r\nIntrinsicError: Unmatched glob from `--changed-since`: \"aws/projects/my_project_name/*\"\r\n\r\nDo 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.\r\n\r\n\r\nExited with code exit status 1\r\n```\r\n\r\nIf files are deleted, yes, they are changed, but they shouldn't throw an error.\r\n\r\n**Pants version**\r\n\r\n2.19.0\r\n\r\n**OS**\r\n\r\nLinux (CircleCI Ubuntu executor)\r\n\r\n**Additional info**\r\n\r\nN/A\r\n\n", "before_files": [{"content": "# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\nimport logging\nfrom typing import cast\n\nfrom pants.base.specs import AddressLiteralSpec, FileLiteralSpec, RawSpecs, Specs\nfrom pants.base.specs_parser import SpecsParser\nfrom pants.core.util_rules.environments import determine_bootstrap_environment\nfrom pants.core.util_rules.system_binaries import GitBinary\nfrom pants.engine.addresses import AddressInput\nfrom pants.engine.environment import EnvironmentName\nfrom pants.engine.internals.scheduler import SchedulerSession\nfrom pants.engine.internals.selectors import Params\nfrom pants.engine.rules import QueryRule\nfrom pants.option.options import Options\nfrom pants.option.options_bootstrapper import OptionsBootstrapper\nfrom pants.util.frozendict import FrozenDict\nfrom pants.vcs.changed import ChangedAddresses, ChangedOptions, ChangedRequest\nfrom pants.vcs.git import GitWorktreeRequest, MaybeGitWorktree\n\nlogger = logging.getLogger(__name__)\n\n\nclass InvalidSpecConstraint(Exception):\n    \"\"\"Raised when invalid constraints are given via specs and arguments like --changed*.\"\"\"\n\n\ndef calculate_specs(\n    options_bootstrapper: OptionsBootstrapper,\n    options: Options,\n    session: SchedulerSession,\n    working_dir: str,\n) -> Specs:\n    \"\"\"Determine the specs for a given Pants run.\"\"\"\n    global_options = options.for_global_scope()\n    unmatched_cli_globs = global_options.unmatched_cli_globs\n    specs = SpecsParser(working_dir=working_dir).parse_specs(\n        options.specs,\n        description_of_origin=\"CLI arguments\",\n        unmatched_glob_behavior=unmatched_cli_globs,\n    )\n\n    changed_options = ChangedOptions.from_options(options.for_scope(\"changed\"))\n    logger.debug(\"specs are: %s\", specs)\n    logger.debug(\"changed_options are: %s\", changed_options)\n\n    if specs and changed_options.provided:\n        changed_name = \"--changed-since\" if changed_options.since else \"--changed-diffspec\"\n        specs_description = specs.arguments_provided_description()\n        assert specs_description is not None\n        raise InvalidSpecConstraint(\n            f\"You used `{changed_name}` at the same time as using {specs_description}. You can \"\n            f\"only use `{changed_name}` or use normal arguments.\"\n        )\n\n    if not changed_options.provided:\n        return specs\n\n    bootstrap_environment = determine_bootstrap_environment(session)\n\n    (git_binary,) = session.product_request(GitBinary, [Params(bootstrap_environment)])\n    (maybe_git_worktree,) = session.product_request(\n        MaybeGitWorktree, [Params(GitWorktreeRequest(), git_binary, bootstrap_environment)]\n    )\n    if not maybe_git_worktree.git_worktree:\n        raise InvalidSpecConstraint(\n            \"The `--changed-*` options are only available if Git is used for the repository.\"\n        )\n\n    changed_files = tuple(changed_options.changed_files(maybe_git_worktree.git_worktree))\n    file_literal_specs = tuple(FileLiteralSpec(f) for f in changed_files)\n\n    changed_request = ChangedRequest(changed_files, changed_options.dependents)\n    (changed_addresses,) = session.product_request(\n        ChangedAddresses,\n        [Params(changed_request, options_bootstrapper, bootstrap_environment)],\n    )\n    logger.debug(\"changed addresses: %s\", changed_addresses)\n\n    address_literal_specs = []\n    for address in cast(ChangedAddresses, changed_addresses):\n        address_input = AddressInput.parse(address.spec, description_of_origin=\"`--changed-since`\")\n        address_literal_specs.append(\n            AddressLiteralSpec(\n                path_component=address_input.path_component,\n                target_component=address_input.target_component,\n                generated_component=address_input.generated_component,\n                parameters=FrozenDict(address_input.parameters),\n            )\n        )\n\n    return Specs(\n        includes=RawSpecs(\n            # We need both address_literals and file_literals to cover all our edge cases, including\n            # target-aware vs. target-less goals, e.g. `list` vs `count-loc`.\n            address_literals=tuple(address_literal_specs),\n            file_literals=file_literal_specs,\n            unmatched_glob_behavior=unmatched_cli_globs,\n            filter_by_global_options=True,\n            from_change_detection=True,\n            description_of_origin=\"`--changed-since`\",\n        ),\n        ignores=RawSpecs(description_of_origin=\"`--changed-since`\"),\n    )\n\n\ndef rules():\n    return [\n        QueryRule(ChangedAddresses, [ChangedRequest, EnvironmentName]),\n        QueryRule(GitBinary, [EnvironmentName]),\n        QueryRule(MaybeGitWorktree, [GitWorktreeRequest, GitBinary, EnvironmentName]),\n    ]\n", "path": "src/python/pants/init/specs_calculator.py"}, {"content": "# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport logging\nimport os\nfrom dataclasses import dataclass\nfrom os import PathLike\nfrom pathlib import Path, PurePath\nfrom typing import Any, Iterable\n\nfrom pants.core.util_rules.system_binaries import GitBinary, GitBinaryException, MaybeGitBinary\nfrom pants.engine.engine_aware import EngineAwareReturnType\nfrom pants.engine.rules import collect_rules, rule\nfrom pants.util.contextutil import pushd\n\nlogger = logging.getLogger(__name__)\n\n\nclass GitWorktree(EngineAwareReturnType):\n    \"\"\"Implements a safe wrapper for un-sandboxed access to Git in the user's working copy.\n\n    This type (and any wrappers) should be marked `EngineAwareReturnType.cacheable=False`, because\n    it internally uses un-sandboxed APIs, and `@rules` which produce it should re-run in each\n    session. It additionally implements a default `__eq__` in order to prevent early-cutoff in the\n    graph, and force any consumers of the type to re-run.\n    \"\"\"\n\n    worktree: PurePath\n    _gitdir: PurePath\n    _git_binary: GitBinary\n\n    def __init__(\n        self,\n        binary: GitBinary,\n        worktree: PathLike[str] | None = None,\n        gitdir: PathLike[str] | None = None,\n    ) -> None:\n        \"\"\"Creates a git object that assumes the git repository is in the cwd by default.\n\n        binary:    The git binary to use.\n        worktree:  The path to the git repository working tree directory (typically '.').\n        gitdir:    The path to the repository's git metadata directory (typically '.git').\n        \"\"\"\n        self.worktree = Path(worktree or os.getcwd()).resolve()\n        self._gitdir = Path(gitdir).resolve() if gitdir else (self.worktree / \".git\")\n        self._git_binary = binary\n\n    def cacheable(self) -> bool:\n        return False\n\n    @property\n    def current_rev_identifier(self):\n        return \"HEAD\"\n\n    @property\n    def commit_id(self):\n        return self._git_binary._invoke_unsandboxed(self._create_git_cmdline([\"rev-parse\", \"HEAD\"]))\n\n    @property\n    def branch_name(self) -> str | None:\n        branch = self._git_binary._invoke_unsandboxed(\n            self._create_git_cmdline([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"])\n        )\n        return None if branch == \"HEAD\" else branch\n\n    def _fix_git_relative_path(self, worktree_path: str, relative_to: PurePath | str) -> str:\n        return str((self.worktree / worktree_path).relative_to(relative_to))\n\n    def changed_files(\n        self,\n        from_commit: str | None = None,\n        include_untracked: bool = False,\n        relative_to: PurePath | str | None = None,\n    ) -> set[str]:\n        relative_to = PurePath(relative_to) if relative_to is not None else self.worktree\n        rel_suffix = [\"--\", str(relative_to)]\n        uncommitted_changes = self._git_binary._invoke_unsandboxed(\n            self._create_git_cmdline(\n                [\"diff\", \"--name-only\", \"HEAD\"] + rel_suffix,\n            )\n        )\n\n        files = set(uncommitted_changes.splitlines())\n        if from_commit:\n            # Grab the diff from the merge-base to HEAD using ... syntax.  This ensures we have just\n            # the changes that have occurred on the current branch.\n            committed_cmd = [\"diff\", \"--name-only\", from_commit + \"...HEAD\"] + rel_suffix\n            committed_changes = self._git_binary._invoke_unsandboxed(\n                self._create_git_cmdline(committed_cmd)\n            )\n            files.update(committed_changes.split())\n        if include_untracked:\n            untracked_cmd = [\n                \"ls-files\",\n                \"--other\",\n                \"--exclude-standard\",\n                \"--full-name\",\n            ] + rel_suffix\n            untracked = self._git_binary._invoke_unsandboxed(\n                self._create_git_cmdline(untracked_cmd)\n            )\n            files.update(untracked.split())\n        # git will report changed files relative to the worktree: re-relativize to relative_to\n        return {self._fix_git_relative_path(f, relative_to) for f in files}\n\n    def changes_in(self, diffspec: str, relative_to: PurePath | str | None = None) -> set[str]:\n        relative_to = PurePath(relative_to) if relative_to is not None else self.worktree\n        cmd = [\"diff-tree\", \"--no-commit-id\", \"--name-only\", \"-r\", diffspec]\n        files = self._git_binary._invoke_unsandboxed(self._create_git_cmdline(cmd)).split()\n        return {self._fix_git_relative_path(f.strip(), relative_to) for f in files}\n\n    def _create_git_cmdline(self, args: Iterable[str]) -> list[str]:\n        return [f\"--git-dir={self._gitdir}\", f\"--work-tree={self.worktree}\", *args]\n\n    def __eq__(self, other: Any) -> bool:\n        # NB: See the class doc regarding equality.\n        return id(self) == id(other)\n\n\n@dataclass(frozen=True)\nclass MaybeGitWorktree(EngineAwareReturnType):\n    git_worktree: GitWorktree | None = None\n\n    def cacheable(self) -> bool:\n        return False\n\n\n@dataclasses.dataclass(frozen=True)\nclass GitWorktreeRequest:\n    gitdir: PathLike[str] | None = None\n    subdir: PathLike[str] | None = None\n\n\n@rule\nasync def get_git_worktree(\n    git_worktree_request: GitWorktreeRequest,\n    maybe_git_binary: MaybeGitBinary,\n) -> MaybeGitWorktree:\n    if not maybe_git_binary.git_binary:\n        return MaybeGitWorktree()\n\n    git_binary = maybe_git_binary.git_binary\n    cmd = [\"rev-parse\", \"--show-toplevel\"]\n\n    try:\n        if git_worktree_request.subdir:\n            with pushd(str(git_worktree_request.subdir)):\n                output = git_binary._invoke_unsandboxed(cmd)\n        else:\n            output = git_binary._invoke_unsandboxed(cmd)\n    except GitBinaryException as e:\n        logger.info(f\"No git repository at {os.getcwd()}: {e!r}\")\n        return MaybeGitWorktree()\n\n    git_worktree = GitWorktree(\n        binary=git_binary,\n        gitdir=git_worktree_request.gitdir,\n        worktree=PurePath(output),\n    )\n\n    logger.debug(\n        f\"Detected git repository at {git_worktree.worktree} on branch {git_worktree.branch_name}\"\n    )\n    return MaybeGitWorktree(git_worktree=git_worktree)\n\n\ndef rules():\n    return [*collect_rules()]\n", "path": "src/python/pants/vcs/git.py"}], "after_files": [{"content": "# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\nimport logging\nfrom typing import cast\n\nfrom pants.base.glob_match_error_behavior import GlobMatchErrorBehavior\nfrom pants.base.specs import AddressLiteralSpec, FileLiteralSpec, RawSpecs, Specs\nfrom pants.base.specs_parser import SpecsParser\nfrom pants.core.util_rules.environments import determine_bootstrap_environment\nfrom pants.core.util_rules.system_binaries import GitBinary\nfrom pants.engine.addresses import AddressInput\nfrom pants.engine.environment import EnvironmentName\nfrom pants.engine.internals.scheduler import SchedulerSession\nfrom pants.engine.internals.selectors import Params\nfrom pants.engine.rules import QueryRule\nfrom pants.option.options import Options\nfrom pants.option.options_bootstrapper import OptionsBootstrapper\nfrom pants.util.frozendict import FrozenDict\nfrom pants.vcs.changed import ChangedAddresses, ChangedOptions, ChangedRequest\nfrom pants.vcs.git import GitWorktreeRequest, MaybeGitWorktree\n\nlogger = logging.getLogger(__name__)\n\n\nclass InvalidSpecConstraint(Exception):\n    \"\"\"Raised when invalid constraints are given via specs and arguments like --changed*.\"\"\"\n\n\ndef calculate_specs(\n    options_bootstrapper: OptionsBootstrapper,\n    options: Options,\n    session: SchedulerSession,\n    working_dir: str,\n) -> Specs:\n    \"\"\"Determine the specs for a given Pants run.\"\"\"\n    global_options = options.for_global_scope()\n    unmatched_cli_globs = global_options.unmatched_cli_globs\n    specs = SpecsParser(working_dir=working_dir).parse_specs(\n        options.specs,\n        description_of_origin=\"CLI arguments\",\n        unmatched_glob_behavior=unmatched_cli_globs,\n    )\n\n    changed_options = ChangedOptions.from_options(options.for_scope(\"changed\"))\n    logger.debug(\"specs are: %s\", specs)\n    logger.debug(\"changed_options are: %s\", changed_options)\n\n    if specs and changed_options.provided:\n        changed_name = \"--changed-since\" if changed_options.since else \"--changed-diffspec\"\n        specs_description = specs.arguments_provided_description()\n        assert specs_description is not None\n        raise InvalidSpecConstraint(\n            f\"You used `{changed_name}` at the same time as using {specs_description}. You can \"\n            f\"only use `{changed_name}` or use normal arguments.\"\n        )\n\n    if not changed_options.provided:\n        return specs\n\n    bootstrap_environment = determine_bootstrap_environment(session)\n\n    (git_binary,) = session.product_request(GitBinary, [Params(bootstrap_environment)])\n    (maybe_git_worktree,) = session.product_request(\n        MaybeGitWorktree, [Params(GitWorktreeRequest(), git_binary, bootstrap_environment)]\n    )\n    if not maybe_git_worktree.git_worktree:\n        raise InvalidSpecConstraint(\n            \"The `--changed-*` options are only available if Git is used for the repository.\"\n        )\n\n    changed_files = tuple(changed_options.changed_files(maybe_git_worktree.git_worktree))\n    file_literal_specs = tuple(FileLiteralSpec(f) for f in changed_files)\n\n    changed_request = ChangedRequest(changed_files, changed_options.dependents)\n    (changed_addresses,) = session.product_request(\n        ChangedAddresses,\n        [Params(changed_request, options_bootstrapper, bootstrap_environment)],\n    )\n    logger.debug(\"changed addresses: %s\", changed_addresses)\n\n    address_literal_specs = []\n    for address in cast(ChangedAddresses, changed_addresses):\n        address_input = AddressInput.parse(address.spec, description_of_origin=\"`--changed-since`\")\n        address_literal_specs.append(\n            AddressLiteralSpec(\n                path_component=address_input.path_component,\n                target_component=address_input.target_component,\n                generated_component=address_input.generated_component,\n                parameters=FrozenDict(address_input.parameters),\n            )\n        )\n\n    return Specs(\n        includes=RawSpecs(\n            # We need both address_literals and file_literals to cover all our edge cases, including\n            # target-aware vs. target-less goals, e.g. `list` vs `count-loc`.\n            address_literals=tuple(address_literal_specs),\n            file_literals=file_literal_specs,\n            # The globs here are synthesized from VCS data by the `changed` mechanism.\n            # As such it does not make sense to apply user-facing matching errors to them.\n            # In particular, they can legitimately not match anything, if entire git\n            # subtrees were deleted for example.\n            unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,\n            filter_by_global_options=True,\n            from_change_detection=True,\n            description_of_origin=\"`--changed-since`\",\n        ),\n        ignores=RawSpecs(description_of_origin=\"`--changed-since`\"),\n    )\n\n\ndef rules():\n    return [\n        QueryRule(ChangedAddresses, [ChangedRequest, EnvironmentName]),\n        QueryRule(GitBinary, [EnvironmentName]),\n        QueryRule(MaybeGitWorktree, [GitWorktreeRequest, GitBinary, EnvironmentName]),\n    ]\n", "path": "src/python/pants/init/specs_calculator.py"}, {"content": "# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport logging\nimport os\nfrom dataclasses import dataclass\nfrom os import PathLike\nfrom pathlib import Path, PurePath\nfrom typing import Any, Iterable\n\nfrom pants.core.util_rules.system_binaries import GitBinary, GitBinaryException, MaybeGitBinary\nfrom pants.engine.engine_aware import EngineAwareReturnType\nfrom pants.engine.rules import collect_rules, rule\nfrom pants.util.contextutil import pushd\n\nlogger = logging.getLogger(__name__)\n\n\nclass GitWorktree(EngineAwareReturnType):\n    \"\"\"Implements a safe wrapper for un-sandboxed access to Git in the user's working copy.\n\n    This type (and any wrappers) should be marked `EngineAwareReturnType.cacheable=False`, because\n    it internally uses un-sandboxed APIs, and `@rules` which produce it should re-run in each\n    session. It additionally implements a default `__eq__` in order to prevent early-cutoff in the\n    graph, and force any consumers of the type to re-run.\n    \"\"\"\n\n    worktree: PurePath\n    _gitdir: PurePath\n    _git_binary: GitBinary\n\n    def __init__(\n        self,\n        binary: GitBinary,\n        worktree: PathLike[str] | None = None,\n        gitdir: PathLike[str] | None = None,\n    ) -> None:\n        \"\"\"Creates a git object that assumes the git repository is in the cwd by default.\n\n        binary:    The git binary to use.\n        worktree:  The path to the git repository working tree directory (typically '.').\n        gitdir:    The path to the repository's git metadata directory (typically '.git').\n        \"\"\"\n        self.worktree = Path(worktree or os.getcwd()).resolve()\n        self._gitdir = Path(gitdir).resolve() if gitdir else (self.worktree / \".git\")\n        self._git_binary = binary\n\n    def cacheable(self) -> bool:\n        return False\n\n    @property\n    def current_rev_identifier(self):\n        return \"HEAD\"\n\n    @property\n    def commit_id(self):\n        return self._git_binary._invoke_unsandboxed(self._create_git_cmdline([\"rev-parse\", \"HEAD\"]))\n\n    @property\n    def branch_name(self) -> str | None:\n        branch = self._git_binary._invoke_unsandboxed(\n            self._create_git_cmdline([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"])\n        )\n        return None if branch == \"HEAD\" else branch\n\n    def _fix_git_relative_path(self, worktree_path: str, relative_to: PurePath | str) -> str:\n        return str((self.worktree / worktree_path).relative_to(relative_to))\n\n    def changed_files(\n        self,\n        from_commit: str | None = None,\n        include_untracked: bool = False,\n        relative_to: PurePath | str | None = None,\n    ) -> set[str]:\n        relative_to = PurePath(relative_to) if relative_to is not None else self.worktree\n        rel_suffix = [\"--\", str(relative_to)]\n        uncommitted_changes = self._git_binary._invoke_unsandboxed(\n            self._create_git_cmdline(\n                [\"diff\", \"--name-only\", \"HEAD\"] + rel_suffix,\n            )\n        )\n\n        files = set(uncommitted_changes.splitlines())\n        if from_commit:\n            # Grab the diff from the merge-base to HEAD using ... syntax.  This ensures we have just\n            # the changes that have occurred on the current branch.\n            committed_cmd = [\"diff\", \"--name-only\", from_commit + \"...HEAD\"] + rel_suffix\n            committed_changes = self._git_binary._invoke_unsandboxed(\n                self._create_git_cmdline(committed_cmd)\n            )\n            files.update(committed_changes.splitlines())\n        if include_untracked:\n            untracked_cmd = [\n                \"ls-files\",\n                \"--other\",\n                \"--exclude-standard\",\n                \"--full-name\",\n            ] + rel_suffix\n            untracked = self._git_binary._invoke_unsandboxed(\n                self._create_git_cmdline(untracked_cmd)\n            )\n            files.update(untracked.splitlines())\n        # git will report changed files relative to the worktree: re-relativize to relative_to\n        return {self._fix_git_relative_path(f, relative_to) for f in files}\n\n    def changes_in(self, diffspec: str, relative_to: PurePath | str | None = None) -> set[str]:\n        relative_to = PurePath(relative_to) if relative_to is not None else self.worktree\n        cmd = [\"diff-tree\", \"--no-commit-id\", \"--name-only\", \"-r\", diffspec]\n        files = self._git_binary._invoke_unsandboxed(self._create_git_cmdline(cmd)).splitlines()\n        return {self._fix_git_relative_path(f.strip(), relative_to) for f in files}\n\n    def _create_git_cmdline(self, args: Iterable[str]) -> list[str]:\n        return [f\"--git-dir={self._gitdir}\", f\"--work-tree={self.worktree}\", *args]\n\n    def __eq__(self, other: Any) -> bool:\n        # NB: See the class doc regarding equality.\n        return id(self) == id(other)\n\n\n@dataclass(frozen=True)\nclass MaybeGitWorktree(EngineAwareReturnType):\n    git_worktree: GitWorktree | None = None\n\n    def cacheable(self) -> bool:\n        return False\n\n\n@dataclasses.dataclass(frozen=True)\nclass GitWorktreeRequest:\n    gitdir: PathLike[str] | None = None\n    subdir: PathLike[str] | None = None\n\n\n@rule\nasync def get_git_worktree(\n    git_worktree_request: GitWorktreeRequest,\n    maybe_git_binary: MaybeGitBinary,\n) -> MaybeGitWorktree:\n    if not maybe_git_binary.git_binary:\n        return MaybeGitWorktree()\n\n    git_binary = maybe_git_binary.git_binary\n    cmd = [\"rev-parse\", \"--show-toplevel\"]\n\n    try:\n        if git_worktree_request.subdir:\n            with pushd(str(git_worktree_request.subdir)):\n                output = git_binary._invoke_unsandboxed(cmd)\n        else:\n            output = git_binary._invoke_unsandboxed(cmd)\n    except GitBinaryException as e:\n        logger.info(f\"No git repository at {os.getcwd()}: {e!r}\")\n        return MaybeGitWorktree()\n\n    git_worktree = GitWorktree(\n        binary=git_binary,\n        gitdir=git_worktree_request.gitdir,\n        worktree=PurePath(output),\n    )\n\n    logger.debug(\n        f\"Detected git repository at {git_worktree.worktree} on branch {git_worktree.branch_name}\"\n    )\n    return MaybeGitWorktree(git_worktree=git_worktree)\n\n\ndef rules():\n    return [*collect_rules()]\n", "path": "src/python/pants/vcs/git.py"}]}