gate-cache 0.3.0__tar.gz → 0.5.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.3.0
3
+ Version: 0.5.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>
@@ -31,6 +31,7 @@ Classifier: Programming Language :: Python :: 3.13
31
31
  Classifier: Programming Language :: Python :: 3.14
32
32
  Requires-Dist: cyclopts>=5.2.0,<6.0.0
33
33
  Requires-Dist: nclutils>=3.4.4,<4.0.0
34
+ Requires-Dist: platformdirs>=4.13.0,<5.0.0
34
35
  Requires-Python: >=3.11
35
36
  Description-Content-Type: text/markdown
36
37
 
@@ -40,17 +41,17 @@ Skip a command that already passed on the same code.
40
41
 
41
42
  A lint or test command often runs many times on code that did not change. It runs when you finish the work, in a pre-commit hook, after a rebase that changed nothing, and in a pre-push hook. When an agent works in the repository, it runs the same command again. If the command takes minutes, the repeat runs take most of the time between finished code and a pushed branch.
42
43
 
43
- gate-cache remembers which commands passed on which code. It wraps a command and gives it a name. When the command passes, gate-cache writes a marker for that name and for the exact content of the tracked files. The next time the same name runs on the same content, gate-cache finds the marker, skips the command, and exits 0 at once.
44
+ gate-cache remembers which commands passed on which code. It wraps a command, and the command line is the name of the gate. When the command passes, gate-cache writes a marker for that name and for the exact content of the files in the working copy that git does not ignore. The next time the same name runs on the same content, gate-cache finds the marker, skips the command, and exits 0 at once.
44
45
 
45
46
  ```console
46
- $ gate-cache --name pytest -- uv run pytest
47
+ $ gate-cache -- uv run pytest
47
48
  ============================= 412 passed in 94.20s =============================
48
49
 
49
- $ gate-cache --name pytest -- uv run pytest
50
- gate-cache: pytest already passed on tree aaff74984ccc, skipping
50
+ $ gate-cache -- uv run pytest
51
+ gate-cache: uv run pytest already passed on tree aaff74984ccc, skipping
51
52
  ```
52
53
 
53
- A change to a tracked file changes the content, so no marker matches and the command runs again. A CI runner that starts from a fresh clone has no markers, so it runs every command.
54
+ A change to any file that git does not ignore changes the content, so no marker matches and the command runs again. A CI runner that starts from a fresh clone has no markers, so it runs every command.
54
55
 
55
56
  ## Install
56
57
 
@@ -64,18 +65,26 @@ The prek and pre-commit hooks install gate-cache themselves, so the hooks work w
64
65
 
65
66
  ## Run a command through gate-cache
66
67
 
67
- The name goes after `--name`, and the command goes after `--`:
68
+ The command goes after `--`:
69
+
70
+ ```sh
71
+ gate-cache -- uv run pytest
72
+ gate-cache -- uv run ruff check .
73
+ ```
74
+
75
+ The command and its arguments are the name of the gate. Each name has its own markers, so a pass of `uv run pytest -k unit` never skips `uv run pytest`.
76
+
77
+ To choose the name yourself, put `--name` before `--`:
68
78
 
69
79
  ```sh
70
80
  gate-cache --name pytest -- uv run pytest
71
- gate-cache --name ruff -- uv run ruff check .
72
81
  ```
73
82
 
74
- Each name has its own markers. A command run under one name never skips because of a marker for another name.
83
+ With `--name`, the name alone identifies the gate, and the arguments of the command are not part of it. Give a name when two different command lines do the same check and must share markers, or when a gate depends on [inputs that git does not track](#inputs-that-git-does-not-track).
75
84
 
76
85
  gate-cache exits with the exit code of the command, or with 0 when it skips the command. It writes a marker only when the command passes, so a command that failed runs again the next time.
77
86
 
78
- When the working copy has uncommitted edits or files that git does not track, the content on disk is not a commit-ready snapshot. gate-cache then runs the command and writes no marker.
87
+ The tree includes uncommitted edits and untracked files that git does not ignore. A gate that passes on uncommitted work therefore skips in the commit hook when you commit the same content. gate-cache computes the tree from a private copy of the index, so it never stages files or changes your index.
79
88
 
80
89
  ## Run it as a hook
81
90
 
@@ -88,18 +97,18 @@ repos:
88
97
  hooks:
89
98
  - id: gate-cache
90
99
  name: pytest
91
- args: [--name, pytest, --, uv, run, pytest]
100
+ args: [--, uv, run, pytest]
92
101
  files: ^(src/|tests/)
93
102
  - id: gate-cache
94
103
  name: ruff
95
- args: [--name, ruff, --, uv, run, ruff, check, .]
104
+ args: [--, uv, run, ruff, check, .]
96
105
  ```
97
106
 
98
107
  The hook runs at commit time and at push time. At commit time, gate-cache matches the content that you commit.
99
108
 
100
109
  The hook runs in its own virtual environment, and the wrapped command inherits that environment. A command started with `uv run`, or a project command such as `just test`, runs in the project environment. `uv run` prints a warning that `VIRTUAL_ENV` does not match the project. The warning has no effect on the run.
101
110
 
102
- Hooks, agents, and your terminal all read and write the same markers. When one of them runs `gate-cache --name pytest -- uv run pytest` and it passes, the others skip that name on the same content.
111
+ Hooks, agents, and your terminal all read and write the same markers. When one of them runs `gate-cache -- uv run pytest` and it passes, the others skip `uv run pytest` on the same content.
103
112
 
104
113
  ## Examples
105
114
 
@@ -109,38 +118,53 @@ These examples show gate-cache inside the task runner of a project. When a recip
109
118
 
110
119
  ```just
111
120
  test:
112
- gate-cache --name test -- uv run pytest
121
+ gate-cache -- uv run pytest
113
122
 
114
123
  lint:
115
- gate-cache --name lint -- uv run ruff check .
124
+ gate-cache -- uv run ruff check .
116
125
  ```
117
126
 
118
127
  After `just test` passes, the next `just test` on the same content skips pytest.
119
128
 
120
129
  ### Recipes with arguments
121
130
 
122
- gate-cache identifies a command by its name alone, not by its arguments. If `just test -k unit` and `just test` ran under the same name, a pass of the unit tests would also skip the full suite. Giving each set of arguments its own name, or caching only the bare form, keeps the two apart.
123
-
124
- This recipe caches `just test` and runs `just test -k unit` without the cache:
131
+ A recipe that passes its arguments to the command needs no `--name`. Each set of arguments gives a different command line, so each set has its own markers:
125
132
 
126
133
  ```just
127
134
  test *args:
128
- #!/usr/bin/env bash
129
- set -euo pipefail
130
- if [[ -z "{{ args }}" ]]; then
131
- exec gate-cache --name test -- uv run pytest
132
- fi
133
- exec uv run pytest {{ args }}
135
+ gate-cache -- uv run pytest {{ args }}
134
136
  ```
135
137
 
138
+ `just test` runs under the name `uv run pytest`, and `just test -k unit` runs under the name `uv run pytest -k unit`. A pass of the unit tests never skips the full suite.
139
+
140
+ If the recipe gives `--name`, the arguments are not part of the name. Put them in the name, or a pass of `just test -k unit` also skips `just test`.
141
+
142
+ ### Inputs that git does not track
143
+
144
+ A marker does not cover ignored files, environment variables, or tool versions. To make a gate depend on one of these inputs, put its value in the name. When the value changes, the name changes, so no marker matches and the command runs again:
145
+
146
+ ```just
147
+ # Run again when the uv version changes
148
+ test:
149
+ gate-cache --name "test-uv$(uv --version | cut -d' ' -f2)" -- uv run pytest
150
+
151
+ # Run again when the ignored .env file changes
152
+ integration:
153
+ gate-cache --name "integration-$(git hash-object .env)" -- uv run pytest tests/integration
154
+ ```
155
+
156
+ `git hash-object` prints a hash of the file, so the name does not hold the secrets in `.env`. Do not put a secret value in a name, because the marker and `gate-cache status` record the name as plain text. Without `--name`, the command line is the name, so give `--name` to a command that takes a secret as an argument.
157
+
158
+ The shell of the recipe expands `$(...)` before gate-cache starts. A hook `entry` or `args` list does not go through a shell, so put the command in a recipe and call the recipe from the hook. To look up the gate with `gate-cache status --name`, give the expanded name, such as `test-uv0.13.0`.
159
+
136
160
  ### Makefile
137
161
 
138
162
  ```make
139
163
  test:
140
- gate-cache --name test -- uv run pytest
164
+ gate-cache -- uv run pytest
141
165
 
142
166
  lint:
143
- gate-cache --name lint -- uv run ruff check .
167
+ gate-cache -- uv run ruff check .
144
168
  ```
145
169
 
146
170
  ### A hook that calls the recipe
@@ -186,17 +210,17 @@ GATE_CACHE_DISABLE=1 just test
186
210
  ```console
187
211
  $ gate-cache status
188
212
  tree: aaff74984cccd156a469afa7d9ab10e4777beb24
189
- passed: pytest
190
- missed: ruff (exit code 1)
213
+ passed: uv run pytest
214
+ missed: uv run ruff check . (exit code 1)
191
215
  ```
192
216
 
193
217
  A name is missed when its command failed, or when it exited 0 but changed the files. A later pass of the same name removes it from the `missed` line.
194
218
 
195
- With `--name`, it reports on one name:
219
+ With `--name`, it reports on one name. For a gate that ran without `--name`, give the command line as the name:
196
220
 
197
221
  ```console
198
- $ gate-cache status --name ruff
199
- name: ruff
222
+ $ gate-cache status --name "uv run ruff check ."
223
+ name: uv run ruff check .
200
224
  tree: aaff74984cccd156a469afa7d9ab10e4777beb24
201
225
  marker: /path/to/repo/.git/gate-cache/7d94ca2f57ec0a77f60d328fb405d339fb0c2a645991b97c042ac54403104e9e
202
226
  result: miss
@@ -205,7 +229,7 @@ $ gate-cache status --name ruff
205
229
 
206
230
  The `last run` line shows only when the name ran on this content without a pass.
207
231
 
208
- `status` runs nothing. With `--name`, it exits 0 when a marker matches and 1 when none does. Without `--name`, it exits 0 when at least one name passed. When gate-cache cannot match the content, for example because of uncommitted edits, the `tree` line gives the reason.
232
+ `status` runs nothing. With `--name`, it exits 0 when a marker matches and 1 when none does. Without `--name`, it exits 0 when at least one name passed. When gate-cache cannot match the content, for example during a merge conflict, the `tree` line gives the reason.
209
233
 
210
234
  With `--json`, `status` writes one JSON object to stdout instead of text. The exit codes do not change:
211
235
 
@@ -215,18 +239,18 @@ $ gate-cache status --json
215
239
  "enabled": true,
216
240
  "tree": "aaff74984cccd156a469afa7d9ab10e4777beb24",
217
241
  "reason": null,
218
- "passed": ["pytest"],
242
+ "passed": ["uv run pytest"],
219
243
  "missed": [
220
- {"name": "ruff", "exit_code": 1, "reason": "exit code 1", "timestamp": "2026-10-12T12:31:55Z"}
244
+ {"name": "uv run ruff check .", "exit_code": 1, "reason": "exit code 1", "timestamp": "2026-10-12T12:31:55Z"}
221
245
  ]
222
246
  }
223
247
  ```
224
248
 
225
- `reason` tells why gate-cache cannot match the content, and is `null` when it can. When caching is off, `enabled` is `false` and `reason` names the variable. With `--name`, the object has the `name`, `marker`, `result`, and `last_run` keys in place of `passed` and `missed`.
249
+ `reason` tells why gate-cache cannot match the content, and is `null` when it can. When caching is off, `enabled` is `false`, `reason` names the variable, and the other keys are empty or `null`. With `--name`, the object has the `name`, `marker`, `result`, and `last_run` keys in place of `passed` and `missed`.
226
250
 
227
251
  ## Start fresh
228
252
 
229
- A marker records only the content of the tracked files. A new tool version, a changed environment variable, or a change to a service does not change that content, so the old markers still match.
253
+ A marker records only the content of the files that git does not ignore. A new tool version, a changed environment variable, or a change to a service does not change that content, so the old markers still match. To make a gate run again when one of these inputs changes, put the input in the name, as [Inputs that git does not track](#inputs-that-git-does-not-track) shows.
230
254
 
231
255
  `gate-cache clear` deletes every marker for the repository, and the records of missed runs:
232
256
 
@@ -4,17 +4,17 @@ Skip a command that already passed on the same code.
4
4
 
5
5
  A lint or test command often runs many times on code that did not change. It runs when you finish the work, in a pre-commit hook, after a rebase that changed nothing, and in a pre-push hook. When an agent works in the repository, it runs the same command again. If the command takes minutes, the repeat runs take most of the time between finished code and a pushed branch.
6
6
 
7
- gate-cache remembers which commands passed on which code. It wraps a command and gives it a name. When the command passes, gate-cache writes a marker for that name and for the exact content of the tracked files. The next time the same name runs on the same content, gate-cache finds the marker, skips the command, and exits 0 at once.
7
+ gate-cache remembers which commands passed on which code. It wraps a command, and the command line is the name of the gate. When the command passes, gate-cache writes a marker for that name and for the exact content of the files in the working copy that git does not ignore. The next time the same name runs on the same content, gate-cache finds the marker, skips the command, and exits 0 at once.
8
8
 
9
9
  ```console
10
- $ gate-cache --name pytest -- uv run pytest
10
+ $ gate-cache -- uv run pytest
11
11
  ============================= 412 passed in 94.20s =============================
12
12
 
13
- $ gate-cache --name pytest -- uv run pytest
14
- gate-cache: pytest already passed on tree aaff74984ccc, skipping
13
+ $ gate-cache -- uv run pytest
14
+ gate-cache: uv run pytest already passed on tree aaff74984ccc, skipping
15
15
  ```
16
16
 
17
- A change to a tracked file changes the content, so no marker matches and the command runs again. A CI runner that starts from a fresh clone has no markers, so it runs every command.
17
+ A change to any file that git does not ignore changes the content, so no marker matches and the command runs again. A CI runner that starts from a fresh clone has no markers, so it runs every command.
18
18
 
19
19
  ## Install
20
20
 
@@ -28,18 +28,26 @@ The prek and pre-commit hooks install gate-cache themselves, so the hooks work w
28
28
 
29
29
  ## Run a command through gate-cache
30
30
 
31
- The name goes after `--name`, and the command goes after `--`:
31
+ The command goes after `--`:
32
+
33
+ ```sh
34
+ gate-cache -- uv run pytest
35
+ gate-cache -- uv run ruff check .
36
+ ```
37
+
38
+ The command and its arguments are the name of the gate. Each name has its own markers, so a pass of `uv run pytest -k unit` never skips `uv run pytest`.
39
+
40
+ To choose the name yourself, put `--name` before `--`:
32
41
 
33
42
  ```sh
34
43
  gate-cache --name pytest -- uv run pytest
35
- gate-cache --name ruff -- uv run ruff check .
36
44
  ```
37
45
 
38
- Each name has its own markers. A command run under one name never skips because of a marker for another name.
46
+ With `--name`, the name alone identifies the gate, and the arguments of the command are not part of it. Give a name when two different command lines do the same check and must share markers, or when a gate depends on [inputs that git does not track](#inputs-that-git-does-not-track).
39
47
 
40
48
  gate-cache exits with the exit code of the command, or with 0 when it skips the command. It writes a marker only when the command passes, so a command that failed runs again the next time.
41
49
 
42
- When the working copy has uncommitted edits or files that git does not track, the content on disk is not a commit-ready snapshot. gate-cache then runs the command and writes no marker.
50
+ The tree includes uncommitted edits and untracked files that git does not ignore. A gate that passes on uncommitted work therefore skips in the commit hook when you commit the same content. gate-cache computes the tree from a private copy of the index, so it never stages files or changes your index.
43
51
 
44
52
  ## Run it as a hook
45
53
 
@@ -52,18 +60,18 @@ repos:
52
60
  hooks:
53
61
  - id: gate-cache
54
62
  name: pytest
55
- args: [--name, pytest, --, uv, run, pytest]
63
+ args: [--, uv, run, pytest]
56
64
  files: ^(src/|tests/)
57
65
  - id: gate-cache
58
66
  name: ruff
59
- args: [--name, ruff, --, uv, run, ruff, check, .]
67
+ args: [--, uv, run, ruff, check, .]
60
68
  ```
61
69
 
62
70
  The hook runs at commit time and at push time. At commit time, gate-cache matches the content that you commit.
63
71
 
64
72
  The hook runs in its own virtual environment, and the wrapped command inherits that environment. A command started with `uv run`, or a project command such as `just test`, runs in the project environment. `uv run` prints a warning that `VIRTUAL_ENV` does not match the project. The warning has no effect on the run.
65
73
 
66
- Hooks, agents, and your terminal all read and write the same markers. When one of them runs `gate-cache --name pytest -- uv run pytest` and it passes, the others skip that name on the same content.
74
+ Hooks, agents, and your terminal all read and write the same markers. When one of them runs `gate-cache -- uv run pytest` and it passes, the others skip `uv run pytest` on the same content.
67
75
 
68
76
  ## Examples
69
77
 
@@ -73,38 +81,53 @@ These examples show gate-cache inside the task runner of a project. When a recip
73
81
 
74
82
  ```just
75
83
  test:
76
- gate-cache --name test -- uv run pytest
84
+ gate-cache -- uv run pytest
77
85
 
78
86
  lint:
79
- gate-cache --name lint -- uv run ruff check .
87
+ gate-cache -- uv run ruff check .
80
88
  ```
81
89
 
82
90
  After `just test` passes, the next `just test` on the same content skips pytest.
83
91
 
84
92
  ### Recipes with arguments
85
93
 
86
- gate-cache identifies a command by its name alone, not by its arguments. If `just test -k unit` and `just test` ran under the same name, a pass of the unit tests would also skip the full suite. Giving each set of arguments its own name, or caching only the bare form, keeps the two apart.
87
-
88
- This recipe caches `just test` and runs `just test -k unit` without the cache:
94
+ A recipe that passes its arguments to the command needs no `--name`. Each set of arguments gives a different command line, so each set has its own markers:
89
95
 
90
96
  ```just
91
97
  test *args:
92
- #!/usr/bin/env bash
93
- set -euo pipefail
94
- if [[ -z "{{ args }}" ]]; then
95
- exec gate-cache --name test -- uv run pytest
96
- fi
97
- exec uv run pytest {{ args }}
98
+ gate-cache -- uv run pytest {{ args }}
98
99
  ```
99
100
 
101
+ `just test` runs under the name `uv run pytest`, and `just test -k unit` runs under the name `uv run pytest -k unit`. A pass of the unit tests never skips the full suite.
102
+
103
+ If the recipe gives `--name`, the arguments are not part of the name. Put them in the name, or a pass of `just test -k unit` also skips `just test`.
104
+
105
+ ### Inputs that git does not track
106
+
107
+ A marker does not cover ignored files, environment variables, or tool versions. To make a gate depend on one of these inputs, put its value in the name. When the value changes, the name changes, so no marker matches and the command runs again:
108
+
109
+ ```just
110
+ # Run again when the uv version changes
111
+ test:
112
+ gate-cache --name "test-uv$(uv --version | cut -d' ' -f2)" -- uv run pytest
113
+
114
+ # Run again when the ignored .env file changes
115
+ integration:
116
+ gate-cache --name "integration-$(git hash-object .env)" -- uv run pytest tests/integration
117
+ ```
118
+
119
+ `git hash-object` prints a hash of the file, so the name does not hold the secrets in `.env`. Do not put a secret value in a name, because the marker and `gate-cache status` record the name as plain text. Without `--name`, the command line is the name, so give `--name` to a command that takes a secret as an argument.
120
+
121
+ The shell of the recipe expands `$(...)` before gate-cache starts. A hook `entry` or `args` list does not go through a shell, so put the command in a recipe and call the recipe from the hook. To look up the gate with `gate-cache status --name`, give the expanded name, such as `test-uv0.13.0`.
122
+
100
123
  ### Makefile
101
124
 
102
125
  ```make
103
126
  test:
104
- gate-cache --name test -- uv run pytest
127
+ gate-cache -- uv run pytest
105
128
 
106
129
  lint:
107
- gate-cache --name lint -- uv run ruff check .
130
+ gate-cache -- uv run ruff check .
108
131
  ```
109
132
 
110
133
  ### A hook that calls the recipe
@@ -150,17 +173,17 @@ GATE_CACHE_DISABLE=1 just test
150
173
  ```console
151
174
  $ gate-cache status
152
175
  tree: aaff74984cccd156a469afa7d9ab10e4777beb24
153
- passed: pytest
154
- missed: ruff (exit code 1)
176
+ passed: uv run pytest
177
+ missed: uv run ruff check . (exit code 1)
155
178
  ```
156
179
 
157
180
  A name is missed when its command failed, or when it exited 0 but changed the files. A later pass of the same name removes it from the `missed` line.
158
181
 
159
- With `--name`, it reports on one name:
182
+ With `--name`, it reports on one name. For a gate that ran without `--name`, give the command line as the name:
160
183
 
161
184
  ```console
162
- $ gate-cache status --name ruff
163
- name: ruff
185
+ $ gate-cache status --name "uv run ruff check ."
186
+ name: uv run ruff check .
164
187
  tree: aaff74984cccd156a469afa7d9ab10e4777beb24
165
188
  marker: /path/to/repo/.git/gate-cache/7d94ca2f57ec0a77f60d328fb405d339fb0c2a645991b97c042ac54403104e9e
166
189
  result: miss
@@ -169,7 +192,7 @@ $ gate-cache status --name ruff
169
192
 
170
193
  The `last run` line shows only when the name ran on this content without a pass.
171
194
 
172
- `status` runs nothing. With `--name`, it exits 0 when a marker matches and 1 when none does. Without `--name`, it exits 0 when at least one name passed. When gate-cache cannot match the content, for example because of uncommitted edits, the `tree` line gives the reason.
195
+ `status` runs nothing. With `--name`, it exits 0 when a marker matches and 1 when none does. Without `--name`, it exits 0 when at least one name passed. When gate-cache cannot match the content, for example during a merge conflict, the `tree` line gives the reason.
173
196
 
174
197
  With `--json`, `status` writes one JSON object to stdout instead of text. The exit codes do not change:
175
198
 
@@ -179,18 +202,18 @@ $ gate-cache status --json
179
202
  "enabled": true,
180
203
  "tree": "aaff74984cccd156a469afa7d9ab10e4777beb24",
181
204
  "reason": null,
182
- "passed": ["pytest"],
205
+ "passed": ["uv run pytest"],
183
206
  "missed": [
184
- {"name": "ruff", "exit_code": 1, "reason": "exit code 1", "timestamp": "2026-10-12T12:31:55Z"}
207
+ {"name": "uv run ruff check .", "exit_code": 1, "reason": "exit code 1", "timestamp": "2026-10-12T12:31:55Z"}
185
208
  ]
186
209
  }
187
210
  ```
188
211
 
189
- `reason` tells why gate-cache cannot match the content, and is `null` when it can. When caching is off, `enabled` is `false` and `reason` names the variable. With `--name`, the object has the `name`, `marker`, `result`, and `last_run` keys in place of `passed` and `missed`.
212
+ `reason` tells why gate-cache cannot match the content, and is `null` when it can. When caching is off, `enabled` is `false`, `reason` names the variable, and the other keys are empty or `null`. With `--name`, the object has the `name`, `marker`, `result`, and `last_run` keys in place of `passed` and `missed`.
190
213
 
191
214
  ## Start fresh
192
215
 
193
- A marker records only the content of the tracked files. A new tool version, a changed environment variable, or a change to a service does not change that content, so the old markers still match.
216
+ A marker records only the content of the files that git does not ignore. A new tool version, a changed environment variable, or a change to a service does not change that content, so the old markers still match. To make a gate run again when one of these inputs changes, put the input in the name, as [Inputs that git does not track](#inputs-that-git-does-not-track) shows.
194
217
 
195
218
  `gate-cache clear` deletes every marker for the repository, and the records of missed runs:
196
219
 
@@ -8,12 +8,13 @@ classifiers = [
8
8
  dependencies = [
9
9
  "cyclopts>=5.2.0,<6.0.0",
10
10
  "nclutils>=3.4.4,<4.0.0",
11
+ "platformdirs>=4.13.0,<5.0.0",
11
12
  ]
12
13
  description = "Skip lint and test gates that already passed on the exact same git tree."
13
14
  name = "gate-cache"
14
15
  readme = "README.md"
15
16
  requires-python = ">=3.11"
16
- version = "0.3.0"
17
+ version = "0.5.0"
17
18
 
18
19
  [[project.authors]]
19
20
  name = "Nate Landau"
@@ -6,13 +6,17 @@
6
6
  "Programming Language :: Python :: 3.13",
7
7
  "Programming Language :: Python :: 3.14",
8
8
  ]
9
- dependencies = ["cyclopts>=5.2.0,<6.0.0", "nclutils>=3.4.4,<4.0.0"]
9
+ dependencies = [
10
+ "cyclopts>=5.2.0,<6.0.0",
11
+ "nclutils>=3.4.4,<4.0.0",
12
+ "platformdirs>=4.13.0,<5.0.0",
13
+ ]
10
14
  description = "Skip lint and test gates that already passed on the exact same git tree."
11
15
  license = { file = "LICENSE" }
12
16
  name = "gate-cache"
13
17
  readme = "README.md"
14
18
  requires-python = ">=3.11"
15
- version = "0.3.0"
19
+ version = "0.5.0"
16
20
 
17
21
  [project.scripts]
18
22
  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.3.0"
3
+ __version__ = "0.5.0"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import shlex
5
6
  import sys
6
7
  from typing import Annotated
7
8
 
@@ -17,14 +18,17 @@ _SUBCOMMANDS = {"status", "clear"}
17
18
  _INFO_FLAGS = {"-h", "--help", "--version"}
18
19
 
19
20
  _HELP = """\
20
- Skip a lint or test command that already passed on the same git tree. gate-cache runs
21
- the command, records a pass keyed on the git tree of the tracked files (the index, when
22
- the working copy matches it), and skips the command next time the tree is identical.
23
- With unstaged changes or untracked files the command runs without caching and nothing
24
- is recorded.
25
-
26
- Both --name and -- are required, and only --name may come before --. Everything after
27
- -- is the command, verbatim.
21
+ Skip a lint or test command that already passed on the same git tree. gate-cache
22
+ runs the command, records a pass keyed on the git tree of the working copy
23
+ (tracked and untracked, non-ignored files), and skips the command next time the
24
+ tree is identical. A pass on uncommitted work therefore skips when the same
25
+ content is committed.
26
+
27
+ -- is required, and only --name may come before it. Everything after -- is the
28
+ command, verbatim. Without --name, the gate name is the command itself, so each
29
+ set of arguments has its own records. Give --name to share records between
30
+ commands that check the same thing, or to key on inputs that the tree does not
31
+ cover.
28
32
 
29
33
  Exit codes:
30
34
  run the command's own exit code, or 0 when a recorded pass lets it skip.
@@ -40,11 +44,19 @@ same way.
40
44
  Set GATE_CACHE_MAX_AGE_DAYS to a positive whole number to change how many days a
41
45
  recorded pass lasts (default 30).
42
46
 
43
- Only file content is keyed. Never wrap checks that depend on anything else,
44
- such as commit-message or branch-name checks.
47
+ Only the name and the file content are keyed. The working directory is not, so
48
+ a command whose meaning depends on it needs a distinct --name. To also key on an
49
+ ignored file, an environment variable, or a tool version, put its value in
50
+ --name. A changed value gives a new name, so the command runs again. Never put a
51
+ secret in a name, because markers and status store it as plain text. Without
52
+ --name the command line is the name, so give --name to a command that takes a
53
+ secret argument. Never wrap checks that depend on the commit message or the
54
+ branch name.
45
55
 
46
56
  Typical use:
57
+ gate-cache -- uv run pytest -x
47
58
  gate-cache --name pytest -- uv run pytest -x
59
+ gate-cache --name "test-$(git hash-object .env)" -- uv run pytest
48
60
  """
49
61
 
50
62
  app = App(
@@ -53,7 +65,7 @@ app = App(
53
65
  help=_HELP,
54
66
  help_format="plaintext",
55
67
  usage=(
56
- "Usage: gate-cache --name NAME -- COMMAND [ARGS...]\n"
68
+ "Usage: gate-cache [--name NAME] -- COMMAND [ARGS...]\n"
57
69
  " gate-cache status [--name NAME] [--json]\n"
58
70
  " gate-cache clear"
59
71
  ),
@@ -68,14 +80,14 @@ def run(
68
80
  ],
69
81
  /,
70
82
  *,
71
- name: str,
83
+ name: str | None = None,
72
84
  ) -> int:
73
85
  """Run COMMAND unless it already passed on the current git tree.
74
86
 
75
87
  On a skip, prints one line to stderr and exits 0 without running anything. Otherwise
76
88
  runs the command and exits with its exit code. The command runs without caching
77
89
  (and nothing is recorded) when GATE_CACHE_DISABLE or CI turns caching off, the tree
78
- has unstaged changes or untracked files, or this is not a git repository. A pass is recorded only
90
+ is unknown (for example an unmerged index), or this is not a git repository. A pass is recorded only
79
91
  when the command exits 0 and the run leaves the tree unchanged.
80
92
 
81
93
  Parameters
@@ -84,11 +96,15 @@ def run(
84
96
  The command and its arguments, given after `--`. Example: -- uv run pytest -x
85
97
  name
86
98
  Gate name. Records match only when the name and the tree both match.
99
+ Defaults to the command and its arguments, quoted as a shell would.
87
100
  """
88
101
  if not command:
89
102
  say("no command given after --")
90
103
  return _USAGE_ERROR
91
- return run_gate(name, command)
104
+ if name == "":
105
+ say("--name needs a value, e.g. gate-cache --name pytest -- uv run pytest")
106
+ return _USAGE_ERROR
107
+ return run_gate(shlex.join(command) if name is None else name, command)
92
108
 
93
109
 
94
110
  @app.command
@@ -147,7 +163,7 @@ def _usage_error(tokens: list[str]) -> str | None:
147
163
  index += 1
148
164
  else:
149
165
  break
150
- return "put -- before the command, e.g. gate-cache --name pytest -- uv run pytest"
166
+ return "put -- before the command, e.g. gate-cache -- uv run pytest"
151
167
 
152
168
 
153
169
  def main() -> None: