gate-cache 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gate-cache
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Skip lint and test gates that already passed on the exact same git tree.
5
5
  Author: Nate Landau
6
6
  Author-email: Nate Landau <github@natenate.org>
@@ -127,7 +127,7 @@ A command that exits 0 but changes the tree does not get a record. Examples are
127
127
 
128
128
  A failing command, a signal, or Ctrl-C never writes a marker. If gate-cache gets `SIGTERM` or `SIGHUP` while the command runs, it sends the signal to the command, waits for the command to stop, and exits with the status of the command. A cache error never changes the exit code of the command. If gate-cache cannot write a marker, it prints a warning and keeps the exit code.
129
129
 
130
- Markers live in the git common directory, so all linked worktrees of a repository share them. After each new marker, gate-cache deletes markers older than 30 days.
130
+ Markers live in the git common directory, so all linked worktrees of a repository share them. After each new marker, gate-cache deletes markers older than 30 days. To change that limit, set `GATE_CACHE_MAX_AGE_DAYS`.
131
131
 
132
132
  ## Marker format
133
133
 
@@ -171,7 +171,9 @@ gate-cache has two commands in addition to the default run. Run `gate-cache --he
171
171
 
172
172
  ### status
173
173
 
174
- `gate-cache status` reports whether a gate would skip on the current tree. It runs nothing.
174
+ `gate-cache status` reports which gates would skip on the current tree. It runs nothing.
175
+
176
+ With `--name`, it reports on one gate:
175
177
 
176
178
  ```console
177
179
  $ gate-cache status --name pytest
@@ -181,7 +183,19 @@ $ gate-cache status --name pytest
181
183
  result: hit
182
184
  ```
183
185
 
184
- If the tree is unknown, the `tree` line gives the reason, such as `unknown: untracked file: notes.txt`. The command exits 0 on a hit and 1 otherwise.
186
+ The command exits 0 on a hit and 1 otherwise.
187
+
188
+ Without `--name`, it lists every gate that passed on the current tree:
189
+
190
+ ```console
191
+ $ gate-cache status
192
+ tree: aaff74984cccd156a469afa7d9ab10e4777beb24
193
+ gates: api-lint, pytest
194
+ ```
195
+
196
+ The command exits 0 when at least one gate matches. If no gate matches, it prints `No gates match the current tree` and exits 1. The list reads the JSON content of each marker, so it does not show a marker that another tool wrote without that content. Such a marker still makes its gate skip.
197
+
198
+ In both forms, if the tree is unknown, the `tree` line gives the reason, such as `unknown: untracked file: notes.txt`, and the command exits 1.
185
199
 
186
200
  ### clear
187
201
 
@@ -192,14 +206,29 @@ $ gate-cache clear
192
206
  gate-cache: removed 3 markers
193
207
  ```
194
208
 
195
- ## Turn off caching
209
+ ## Environment variables
196
210
 
197
- If `GATE_CACHE_DISABLE` has a value that is not empty and not `0`, gate-cache runs every command and never reads or writes markers. With caching off, `gate-cache status` exits 1.
211
+ Two environment variables control the cache.
212
+
213
+ | Variable | Effect | Default |
214
+ | ------------------------- | -------------------------------------------------------------- | ------- |
215
+ | `GATE_CACHE_DISABLE` | Any value that is not empty and not `0` turns off caching. | not set |
216
+ | `GATE_CACHE_MAX_AGE_DAYS` | The age in days at which gate-cache deletes a marker. | `30` |
217
+
218
+ With caching off, gate-cache runs every command and never reads or writes markers, and `gate-cache status` exits 1.
198
219
 
199
220
  ```sh
200
221
  GATE_CACHE_DISABLE=1 git push
201
222
  ```
202
223
 
224
+ The marker directory holds one small file for each gate that passed on each tree. After each new marker, gate-cache deletes every marker older than `GATE_CACHE_MAX_AGE_DAYS`, so the directory holds only the passes from that period. The age counts from when gate-cache wrote the marker. A skip does not reset it, so a marker that a gate hits every day still expires. A smaller value keeps the directory smaller and causes more runs on old trees.
225
+
226
+ ```sh
227
+ export GATE_CACHE_MAX_AGE_DAYS=7
228
+ ```
229
+
230
+ The value must be a positive whole number. If it is not, gate-cache prints a warning and uses 30 days.
231
+
203
232
  ## Supported runners and platforms
204
233
 
205
234
  gate-cache works with prek and with pre-commit 3.2 or later. It runs on macOS and Linux. Windows is not supported.
@@ -91,7 +91,7 @@ A command that exits 0 but changes the tree does not get a record. Examples are
91
91
 
92
92
  A failing command, a signal, or Ctrl-C never writes a marker. If gate-cache gets `SIGTERM` or `SIGHUP` while the command runs, it sends the signal to the command, waits for the command to stop, and exits with the status of the command. A cache error never changes the exit code of the command. If gate-cache cannot write a marker, it prints a warning and keeps the exit code.
93
93
 
94
- Markers live in the git common directory, so all linked worktrees of a repository share them. After each new marker, gate-cache deletes markers older than 30 days.
94
+ Markers live in the git common directory, so all linked worktrees of a repository share them. After each new marker, gate-cache deletes markers older than 30 days. To change that limit, set `GATE_CACHE_MAX_AGE_DAYS`.
95
95
 
96
96
  ## Marker format
97
97
 
@@ -135,7 +135,9 @@ gate-cache has two commands in addition to the default run. Run `gate-cache --he
135
135
 
136
136
  ### status
137
137
 
138
- `gate-cache status` reports whether a gate would skip on the current tree. It runs nothing.
138
+ `gate-cache status` reports which gates would skip on the current tree. It runs nothing.
139
+
140
+ With `--name`, it reports on one gate:
139
141
 
140
142
  ```console
141
143
  $ gate-cache status --name pytest
@@ -145,7 +147,19 @@ $ gate-cache status --name pytest
145
147
  result: hit
146
148
  ```
147
149
 
148
- If the tree is unknown, the `tree` line gives the reason, such as `unknown: untracked file: notes.txt`. The command exits 0 on a hit and 1 otherwise.
150
+ The command exits 0 on a hit and 1 otherwise.
151
+
152
+ Without `--name`, it lists every gate that passed on the current tree:
153
+
154
+ ```console
155
+ $ gate-cache status
156
+ tree: aaff74984cccd156a469afa7d9ab10e4777beb24
157
+ gates: api-lint, pytest
158
+ ```
159
+
160
+ The command exits 0 when at least one gate matches. If no gate matches, it prints `No gates match the current tree` and exits 1. The list reads the JSON content of each marker, so it does not show a marker that another tool wrote without that content. Such a marker still makes its gate skip.
161
+
162
+ In both forms, if the tree is unknown, the `tree` line gives the reason, such as `unknown: untracked file: notes.txt`, and the command exits 1.
149
163
 
150
164
  ### clear
151
165
 
@@ -156,14 +170,29 @@ $ gate-cache clear
156
170
  gate-cache: removed 3 markers
157
171
  ```
158
172
 
159
- ## Turn off caching
173
+ ## Environment variables
160
174
 
161
- If `GATE_CACHE_DISABLE` has a value that is not empty and not `0`, gate-cache runs every command and never reads or writes markers. With caching off, `gate-cache status` exits 1.
175
+ Two environment variables control the cache.
176
+
177
+ | Variable | Effect | Default |
178
+ | ------------------------- | -------------------------------------------------------------- | ------- |
179
+ | `GATE_CACHE_DISABLE` | Any value that is not empty and not `0` turns off caching. | not set |
180
+ | `GATE_CACHE_MAX_AGE_DAYS` | The age in days at which gate-cache deletes a marker. | `30` |
181
+
182
+ With caching off, gate-cache runs every command and never reads or writes markers, and `gate-cache status` exits 1.
162
183
 
163
184
  ```sh
164
185
  GATE_CACHE_DISABLE=1 git push
165
186
  ```
166
187
 
188
+ The marker directory holds one small file for each gate that passed on each tree. After each new marker, gate-cache deletes every marker older than `GATE_CACHE_MAX_AGE_DAYS`, so the directory holds only the passes from that period. The age counts from when gate-cache wrote the marker. A skip does not reset it, so a marker that a gate hits every day still expires. A smaller value keeps the directory smaller and causes more runs on old trees.
189
+
190
+ ```sh
191
+ export GATE_CACHE_MAX_AGE_DAYS=7
192
+ ```
193
+
194
+ The value must be a positive whole number. If it is not, gate-cache prints a warning and uses 30 days.
195
+
167
196
  ## Supported runners and platforms
168
197
 
169
198
  gate-cache works with prek and with pre-commit 3.2 or later. It runs on macOS and Linux. Windows is not supported.
@@ -13,7 +13,7 @@ description = "Skip lint and test gates that already passed on the exact same gi
13
13
  name = "gate-cache"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.11"
16
- version = "0.1.0"
16
+ version = "0.2.0"
17
17
 
18
18
  [[project.authors]]
19
19
  name = "Nate Landau"
@@ -12,7 +12,7 @@
12
12
  name = "gate-cache"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.11"
15
- version = "0.1.0"
15
+ version = "0.2.0"
16
16
 
17
17
  [project.scripts]
18
18
  gate-cache = "gate_cache.cli:main"
@@ -1,3 +1,3 @@
1
1
  """Skip lint and test gates that already passed on the exact same git tree."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -29,10 +29,13 @@ Both --name and -- are required, and only --name may come before --. Everything
29
29
  Exit codes:
30
30
  run the command's own exit code, or 0 when a recorded pass lets it skip.
31
31
  Usage errors exit 2.
32
- status 0 when a recorded pass exists for the current tree, 1 otherwise.
32
+ status 0 when the named gate, or any gate without --name, passed on the
33
+ current tree. 1 otherwise.
33
34
  clear 0 on success, 1 on failure.
34
35
 
35
36
  Set GATE_CACHE_DISABLE to any non-empty value other than 0 to always run the command.
37
+ Set GATE_CACHE_MAX_AGE_DAYS to a positive whole number to change how many days a
38
+ recorded pass lasts (default 30).
36
39
 
37
40
  Only file content is keyed. Never wrap checks that depend on anything else,
38
41
  such as commit-message or branch-name checks.
@@ -48,7 +51,7 @@ app = App(
48
51
  help_format="plaintext",
49
52
  usage=(
50
53
  "Usage: gate-cache --name NAME -- COMMAND [ARGS...]\n"
51
- " gate-cache status --name NAME\n"
54
+ " gate-cache status [--name NAME]\n"
52
55
  " gate-cache clear"
53
56
  ),
54
57
  result_action="return_int_as_exit_code_else_zero",
@@ -86,16 +89,19 @@ def run(
86
89
 
87
90
 
88
91
  @app.command
89
- def status(*, name: str) -> int:
90
- """Report whether the gate would skip, without running anything.
92
+ def status(*, name: str | None = None) -> int:
93
+ """Report whether gates would skip, without running anything.
91
94
 
92
- Prints the current tree hash (or why it is unknown), the marker path, and hit or
93
- miss to stdout. Exits 0 on a hit, 1 otherwise.
95
+ With --name, print the tree hash (or why it is unknown), the marker path,
96
+ and hit or miss. Exit 0 on a hit, 1 otherwise.
97
+
98
+ Without --name, list every gate that passed on the current tree.
99
+ Exit 0 when at least one gate matches, 1 otherwise.
94
100
 
95
101
  Parameters
96
102
  ----------
97
103
  name
98
- Gate name to look up.
104
+ Gate name to look up. Omit it to list every gate that passed on this tree.
99
105
  """
100
106
  return gate_status(name)
101
107
 
@@ -8,15 +8,23 @@ from typing import TYPE_CHECKING
8
8
  from nclutils import pp
9
9
 
10
10
  from gate_cache.console import say
11
- from gate_cache.markers import clear_markers, marker_path, prune_markers, write_marker
11
+ from gate_cache.markers import (
12
+ clear_markers,
13
+ marker_path,
14
+ passed_names,
15
+ prune_markers,
16
+ write_marker,
17
+ )
12
18
  from gate_cache.runner import run_child
13
- from gate_cache.snapshot import git_common_dir, take_snapshot
19
+ from gate_cache.snapshot import Snapshot, git_common_dir, take_snapshot
14
20
 
15
21
  if TYPE_CHECKING:
16
22
  from collections.abc import Mapping, Sequence
17
23
  from pathlib import Path
18
24
 
19
25
  DISABLE_ENV = "GATE_CACHE_DISABLE"
26
+ MAX_AGE_ENV = "GATE_CACHE_MAX_AGE_DAYS"
27
+ DEFAULT_MAX_AGE_DAYS = 30
20
28
  _DISABLED_REASON = f"caching disabled by {DISABLE_ENV}"
21
29
  _NO_COMMON_DIR_REASON = "could not locate git directory"
22
30
 
@@ -34,6 +42,35 @@ def cache_disabled(environ: Mapping[str, str] | None = None) -> bool:
34
42
  return value not in {"", "0"}
35
43
 
36
44
 
45
+ def marker_max_age_days(environ: Mapping[str, str] | None = None) -> int:
46
+ """Read how many days a marker lives before pruning deletes it.
47
+
48
+ An invalid value warns and falls back to the default, so a typo in the setting
49
+ can never fail a gate.
50
+
51
+ Args:
52
+ environ: Environment to read; defaults to the process environment.
53
+
54
+ Returns:
55
+ The positive whole number of days from GATE_CACHE_MAX_AGE_DAYS, or 30 when it
56
+ is unset, empty, or invalid.
57
+ """
58
+ value = (os.environ if environ is None else environ).get(MAX_AGE_ENV, "").strip()
59
+ if not value:
60
+ return DEFAULT_MAX_AGE_DAYS
61
+ try:
62
+ days = int(value)
63
+ except ValueError:
64
+ days = 0
65
+ if days < 1:
66
+ say(
67
+ f"warning: {MAX_AGE_ENV}={value!r} is not a positive whole number of days, "
68
+ f"using {DEFAULT_MAX_AGE_DAYS}"
69
+ )
70
+ return DEFAULT_MAX_AGE_DAYS
71
+ return days
72
+
73
+
37
74
  def _marker_exists(path: Path) -> bool:
38
75
  """Return True only when the marker is known to exist."""
39
76
  try:
@@ -55,7 +92,7 @@ def _record_pass(path: Path, *, tree: str, name: str, command: Sequence[str]) ->
55
92
  except OSError as error:
56
93
  say(f"warning: could not record pass: {error}")
57
94
  return
58
- prune_markers(path.parent)
95
+ prune_markers(path.parent, max_age_days=marker_max_age_days())
59
96
 
60
97
 
61
98
  def _record_if_unchanged(path: Path, *, tree: str, name: str, command: Sequence[str]) -> None:
@@ -106,20 +143,25 @@ def run_gate(name: str, command: Sequence[str]) -> int:
106
143
  return result.exit_code
107
144
 
108
145
 
109
- def gate_status(name: str) -> int:
110
- """Print whether a gate would skip, without running anything.
146
+ def gate_status(name: str | None = None) -> int:
147
+ """Print whether a gate would skip, or list every gate that would, without running.
111
148
 
112
149
  Args:
113
- name: Gate name to look up.
150
+ name: Gate name to look up. When None, list every gate with a recorded pass on
151
+ the current tree.
114
152
 
115
153
  Returns:
116
- 0 when a recorded pass exists for the current tree, 1 otherwise.
154
+ 0 when a recorded pass exists for the gate, or for at least one gate when
155
+ listing, and 1 otherwise.
117
156
  """
118
157
  if cache_disabled():
119
158
  say(_DISABLED_REASON)
120
159
  return 1
121
160
 
122
161
  snapshot = take_snapshot()
162
+ if name is None:
163
+ return _list_passed(snapshot)
164
+
123
165
  tree = snapshot.tree
124
166
  path: Path | None = None
125
167
  marker = "none"
@@ -143,6 +185,24 @@ def gate_status(name: str) -> int:
143
185
  return 0 if hit else 1
144
186
 
145
187
 
188
+ def _list_passed(snapshot: Snapshot) -> int:
189
+ """Print the gates with a recorded pass on the snapshot's tree."""
190
+ if snapshot.tree is None:
191
+ pp.kv({"tree": f"unknown: {snapshot.reason}"})
192
+ return 1
193
+ common_dir = git_common_dir()
194
+ if common_dir is None:
195
+ pp.kv({"tree": snapshot.tree, "gates": f"unknown: {_NO_COMMON_DIR_REASON}"})
196
+ return 1
197
+ names = passed_names(common_dir, snapshot.tree)
198
+ if not names:
199
+ pp.kv({"tree": snapshot.tree})
200
+ pp.console().print("No gates match the current tree", markup=False, highlight=False)
201
+ return 1
202
+ pp.kv({"tree": snapshot.tree, "gates": ", ".join(names)})
203
+ return 0
204
+
205
+
146
206
  def clear_cache() -> int:
147
207
  """Delete every marker for the current repository.
148
208
 
@@ -96,6 +96,45 @@ def write_marker(path: Path, *, tree: str, name: str, command: Sequence[str]) ->
96
96
  raise
97
97
 
98
98
 
99
+ def passed_names(common_dir: Path, tree: str) -> list[str]:
100
+ """List the gates with a recorded pass on a tree, for reporting what would skip.
101
+
102
+ Marker filenames are one-way hashes, so names come from each marker's JSON content.
103
+ A file whose content is missing, unreadable, or does not hash to its own filename is
104
+ left out. Markers written by other tools without that content still count for skips
105
+ but cannot be listed.
106
+
107
+ Args:
108
+ common_dir: Absolute path to the git common directory.
109
+ tree: Git tree hash to match.
110
+
111
+ Returns:
112
+ Sorted, unique gate names.
113
+ """
114
+ names: set[str] = set()
115
+ try:
116
+ entries = list(marker_dir(common_dir).iterdir())
117
+ except OSError:
118
+ return []
119
+ for entry in entries:
120
+ if entry.name.startswith(TEMP_PREFIX):
121
+ continue
122
+ try:
123
+ data = json.loads(entry.read_text(encoding="utf-8"))
124
+ except (OSError, UnicodeDecodeError, ValueError):
125
+ continue
126
+ if not isinstance(data, dict):
127
+ continue
128
+ name = data.get("name")
129
+ if (
130
+ data.get("tree") == tree
131
+ and isinstance(name, str)
132
+ and marker_key(tree, name) == entry.name
133
+ ):
134
+ names.add(name)
135
+ return sorted(names)
136
+
137
+
99
138
  def prune_markers(directory: Path, *, max_age_days: int = 30, now: float | None = None) -> None:
100
139
  """Delete markers older than the cutoff, ignoring every filesystem error.
101
140
 
File without changes