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.
- {gate_cache-0.1.0 → gate_cache-0.2.0}/PKG-INFO +35 -6
- {gate_cache-0.1.0 → gate_cache-0.2.0}/README.md +34 -5
- {gate_cache-0.1.0 → gate_cache-0.2.0}/pyproject.toml +1 -1
- {gate_cache-0.1.0 → gate_cache-0.2.0}/pyproject.toml.orig +1 -1
- {gate_cache-0.1.0 → gate_cache-0.2.0}/src/gate_cache/__init__.py +1 -1
- {gate_cache-0.1.0 → gate_cache-0.2.0}/src/gate_cache/cli.py +13 -7
- {gate_cache-0.1.0 → gate_cache-0.2.0}/src/gate_cache/gate.py +67 -7
- {gate_cache-0.1.0 → gate_cache-0.2.0}/src/gate_cache/markers.py +39 -0
- {gate_cache-0.1.0 → gate_cache-0.2.0}/LICENSE +0 -0
- {gate_cache-0.1.0 → gate_cache-0.2.0}/src/gate_cache/console.py +0 -0
- {gate_cache-0.1.0 → gate_cache-0.2.0}/src/gate_cache/runner.py +0 -0
- {gate_cache-0.1.0 → gate_cache-0.2.0}/src/gate_cache/snapshot.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: gate-cache
|
|
3
|
-
Version: 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
|
|
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
|
-
|
|
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
|
-
##
|
|
209
|
+
## Environment variables
|
|
196
210
|
|
|
197
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
173
|
+
## Environment variables
|
|
160
174
|
|
|
161
|
-
|
|
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.
|
|
@@ -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
|
|
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
|
|
92
|
+
def status(*, name: str | None = None) -> int:
|
|
93
|
+
"""Report whether gates would skip, without running anything.
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|