git-goose 0.0.1__py3-none-any.whl
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.
- git_goose-0.0.1.dist-info/LICENSE +29 -0
- git_goose-0.0.1.dist-info/METADATA +235 -0
- git_goose-0.0.1.dist-info/RECORD +30 -0
- git_goose-0.0.1.dist-info/WHEEL +5 -0
- git_goose-0.0.1.dist-info/top_level.txt +1 -0
- goose/__init__.py +0 -0
- goose/__main__.py +251 -0
- goose/_utils/__init__.py +0 -0
- goose/_utils/pydantic.py +10 -0
- goose/asyncio.py +19 -0
- goose/backend/__init__.py +0 -0
- goose/backend/base.py +76 -0
- goose/backend/index.py +20 -0
- goose/backend/node.py +190 -0
- goose/backend/python.py +185 -0
- goose/config.py +74 -0
- goose/context.py +34 -0
- goose/environment.py +259 -0
- goose/executable_unit.py +11 -0
- goose/filter.py +10 -0
- goose/git/__init__.py +0 -0
- goose/git/shared.py +48 -0
- goose/git/status.py +155 -0
- goose/manifest.py +160 -0
- goose/orphan_environments.py +20 -0
- goose/parallel.py +40 -0
- goose/paths.py +22 -0
- goose/process.py +26 -0
- goose/scheduler.py +154 -0
- goose/targets.py +103 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Anton Agestam
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: git-goose
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A picky and eager Git hook runner.
|
|
5
|
+
Author-email: Anton Agestam <git@antonagestam.se>
|
|
6
|
+
License: BSD 3-Clause
|
|
7
|
+
Project-URL: Source Repository, https://github.com/antonagestam/goose
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: <3.13,>3.12
|
|
13
|
+
Description-Content-Type: text/markdown; charset=UTF-8
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pydantic
|
|
16
|
+
Requires-Dist: PyYAML
|
|
17
|
+
Requires-Dist: nodeenv
|
|
18
|
+
Requires-Dist: identify
|
|
19
|
+
Requires-Dist: typer
|
|
20
|
+
Requires-Dist: rich
|
|
21
|
+
Requires-Dist: shellingham
|
|
22
|
+
|
|
23
|
+
<!-- prettier-ignore-start -->
|
|
24
|
+
<!-- https://github.com/prettier/prettier/issues/15479 -->
|
|
25
|
+
|
|
26
|
+
> [!NOTE]
|
|
27
|
+
> This project is in its infancy. Please try it out and report and help fix any issues or missing features, but expect a somewhat broken experience.
|
|
28
|
+
>
|
|
29
|
+
> Breaking CLI changes is to be expected without notice between arbitrary versions until version 1.0 is released.
|
|
30
|
+
|
|
31
|
+
<!-- prettier-ignore-end -->
|
|
32
|
+
|
|
33
|
+
<h1 align=center>goose<br>🦆🧪💻</h1>
|
|
34
|
+
|
|
35
|
+
<p align=center>A <i>picky</i> and <i>eager</i> <a href=https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks>Git hook</a> runner.</p>
|
|
36
|
+
|
|
37
|
+
- Reproducible builds.
|
|
38
|
+
- Dynamic parallelism.
|
|
39
|
+
- Small file-system footprint.
|
|
40
|
+
|
|
41
|
+
### Features
|
|
42
|
+
|
|
43
|
+
- Smart parallelism schedules hooks across CPUs while avoiding concurrent writes.
|
|
44
|
+
- Deterministic environments by using ecosystem-specific lock files.
|
|
45
|
+
- Environments are shared across hooks.
|
|
46
|
+
- Self-contained definitions means there's no need to push tool-specific configuration
|
|
47
|
+
upstream, or to maintain brittle mirroring schemes.
|
|
48
|
+
|
|
49
|
+
### Parallelism
|
|
50
|
+
|
|
51
|
+
Goose takes care to keep your CPUs as busy as possible, optimizing to have the full
|
|
52
|
+
suite of hooks finish as soon as possible. It does this by distributing units of work to
|
|
53
|
+
all available processing cores.
|
|
54
|
+
|
|
55
|
+
Parameterized hooks, or hooks that take files as command line arguments, are divided to
|
|
56
|
+
one unit of work per available core. Whenever a core becomes available for more work, a
|
|
57
|
+
new unit is chosen for execution.
|
|
58
|
+
|
|
59
|
+
The scheduler takes care to never run more than one mutating hook on the same file. It
|
|
60
|
+
does this by taking into account hooks marked as `read_only` and by comparing sets of
|
|
61
|
+
files a unit of work is assigned to. Two incompatible hooks can be simultaneously
|
|
62
|
+
working on two separate parts of the code-base.
|
|
63
|
+
|
|
64
|
+
### Deterministic environments
|
|
65
|
+
|
|
66
|
+
Goose uses lock files to facilitate deterministic results across developer environments
|
|
67
|
+
and CI. You specify dependencies in `goose.yaml`, and invoking `goose run` will produce
|
|
68
|
+
the appropriate lock files under a `.goose/` directory. The `.goose/` directory is meant
|
|
69
|
+
to be checked into git, so that future invocations of `goose run` can use the lock files
|
|
70
|
+
it contains to produce identical environments for hooks to run in.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
Create a `goose.yaml` file in the repository root.
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
environments:
|
|
78
|
+
- id: python
|
|
79
|
+
ecosystem:
|
|
80
|
+
language: python
|
|
81
|
+
version: "3.12"
|
|
82
|
+
dependencies:
|
|
83
|
+
- ruff
|
|
84
|
+
|
|
85
|
+
hooks:
|
|
86
|
+
- id: ruff
|
|
87
|
+
environment: python
|
|
88
|
+
command: ruff
|
|
89
|
+
args: [check, --force-exclude, --fix]
|
|
90
|
+
types: [python]
|
|
91
|
+
|
|
92
|
+
- id: ruff-format
|
|
93
|
+
environment: python
|
|
94
|
+
command: ruff
|
|
95
|
+
args: [format, --force-exclude]
|
|
96
|
+
types: [python]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Bootstrap environments, generate lock files, and install dependencies.
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
$ python -m goose upgrade
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Run all hooks over all files.
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
$ python -m goose run --select=all
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Commit configuration and lock files.
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
$ git add goose.yaml .goose
|
|
115
|
+
$ git commit -m 'Add goose configuration'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Upgrading hook versions
|
|
119
|
+
|
|
120
|
+
As pinning of hook versions is handled with lock files, there's no need to change
|
|
121
|
+
configuration to upgrade hook dependency versions, instead you just run the upgrade
|
|
122
|
+
command.
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
$ python -m goose upgrade
|
|
126
|
+
$ git add .goose
|
|
127
|
+
$ git commit -m 'Bump goose dependencies'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Example node hook
|
|
131
|
+
|
|
132
|
+
Goose currently supports Python and Node environments, here's an example using
|
|
133
|
+
[Prettier] to format Markdown files.
|
|
134
|
+
|
|
135
|
+
[Prettier]: https://prettier.io/
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
environments:
|
|
139
|
+
- id: node
|
|
140
|
+
ecosystem:
|
|
141
|
+
language: node
|
|
142
|
+
version: "21.7.1"
|
|
143
|
+
dependencies:
|
|
144
|
+
- prettier
|
|
145
|
+
|
|
146
|
+
hooks:
|
|
147
|
+
- id: prettier
|
|
148
|
+
environment: node
|
|
149
|
+
command: prettier
|
|
150
|
+
types: [markdown]
|
|
151
|
+
args:
|
|
152
|
+
- --write
|
|
153
|
+
- --ignore-unknown
|
|
154
|
+
- --parser=markdown
|
|
155
|
+
- --print-width=88
|
|
156
|
+
- --prose-wrap=always
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Read-only hooks
|
|
160
|
+
|
|
161
|
+
You will likely want to use a mix of pure linters, as well as formatters and
|
|
162
|
+
auto-fixers. Tools that don't mutate files can be more heavily parallelized by Goose,
|
|
163
|
+
because they can inspect overlapping sets of files simultaneously as other tools. To
|
|
164
|
+
enable this you set `read_only: true` in hook configuration.
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
environments:
|
|
168
|
+
- id: python
|
|
169
|
+
ecosystem:
|
|
170
|
+
language: python
|
|
171
|
+
version: "3.12"
|
|
172
|
+
dependencies:
|
|
173
|
+
- pre-commit-hooks
|
|
174
|
+
|
|
175
|
+
hooks:
|
|
176
|
+
- id: check-case-conflict
|
|
177
|
+
environment: python
|
|
178
|
+
command: check-case-conflict
|
|
179
|
+
read_only: true
|
|
180
|
+
|
|
181
|
+
- id: check-merge-conflict
|
|
182
|
+
environment: python
|
|
183
|
+
command: check-merge-conflict
|
|
184
|
+
read_only: true
|
|
185
|
+
types: [text]
|
|
186
|
+
|
|
187
|
+
- id: python-debug-statements
|
|
188
|
+
environment: python
|
|
189
|
+
command: debug-statement-hook
|
|
190
|
+
read_only: true
|
|
191
|
+
types: [python]
|
|
192
|
+
|
|
193
|
+
- id: detect-private-key
|
|
194
|
+
environment: python
|
|
195
|
+
command: detect-private-key
|
|
196
|
+
read_only: true
|
|
197
|
+
types: [text]
|
|
198
|
+
|
|
199
|
+
- id: end-of-file-fixer
|
|
200
|
+
environment: python
|
|
201
|
+
command: end-of-file-fixer
|
|
202
|
+
types: [text]
|
|
203
|
+
|
|
204
|
+
- id: trailing-whitespace-fixer
|
|
205
|
+
environment: python
|
|
206
|
+
command: trailing-whitespace-fixer
|
|
207
|
+
types: [text]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Hooks that do not specify `read_only: true` will never run simultaneously as other tools
|
|
211
|
+
over the same file.
|
|
212
|
+
|
|
213
|
+
### Non-parameterized hooks
|
|
214
|
+
|
|
215
|
+
Some tools don't support passing files, or just work better if given the responsibility
|
|
216
|
+
to parallelize work itself. One such tool is mypy. You can instruct goose to not pass
|
|
217
|
+
filenames to a hook (and as a consequence, also not spawn multiple parallel jobs for
|
|
218
|
+
this hook).
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
environments:
|
|
222
|
+
- id: mypy
|
|
223
|
+
ecosystem:
|
|
224
|
+
language: python
|
|
225
|
+
version: "3.12"
|
|
226
|
+
dependencies:
|
|
227
|
+
- mypy
|
|
228
|
+
|
|
229
|
+
hooks:
|
|
230
|
+
- id: mypy
|
|
231
|
+
environment: mypy
|
|
232
|
+
command: mypy
|
|
233
|
+
read_only: true
|
|
234
|
+
parameterize: false
|
|
235
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
goose/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
goose/__main__.py,sha256=k-FvWXMtgVJy8P8y-ZZLmZwX_fVWHqepDaMs3V5pePI,7050
|
|
3
|
+
goose/asyncio.py,sha256=bOhXmv7yaGqoqMGiBB0-HH3XcXfBZPQ28dObnafLJ0U,444
|
|
4
|
+
goose/config.py,sha256=O44J4w6bfA1KH8hxZlu_BudgTCH6EWk9-b5cxaZmc0g,2023
|
|
5
|
+
goose/context.py,sha256=GHTGkCyTrkL2v05ch7X3n12msSH-hvoIO0QP-lLQzMg,983
|
|
6
|
+
goose/environment.py,sha256=rckXiaUaM1KGdAHF3RwyAUJ1CsbYQKe0aO5ExJgcSN8,8182
|
|
7
|
+
goose/executable_unit.py,sha256=K1_GlBpCFOgQqrbmawe6gHkADUpKyMwARp_qbkF0YKM,241
|
|
8
|
+
goose/filter.py,sha256=HHpRGeA8dlZWX4Y-Xm0JtmWWCvVU7YTurlOnf-Ur5Jg,252
|
|
9
|
+
goose/manifest.py,sha256=yYwMqUwM7z8OdjT7Mm5grsfq-FBZRGxzggahL7fGqFo,4613
|
|
10
|
+
goose/orphan_environments.py,sha256=XQ1KhzTFqsrobamjgk4aoe_GpF53aKUVQD8yoamHC7U,638
|
|
11
|
+
goose/parallel.py,sha256=CDVpVU4YFJqsJEDQ2W36B7MvU4LcUcaTtioUuiFuMDw,1212
|
|
12
|
+
goose/paths.py,sha256=skxJJ1r2QZjA64q_X1A73z24SSftw-5ciXmwFYMnlUg,580
|
|
13
|
+
goose/process.py,sha256=HKc--KzGP-i61vzDwJz-vAeSH1kw2aeDVyGZXZJH2FQ,735
|
|
14
|
+
goose/scheduler.py,sha256=-z1qmMlp9xhGZgQBccKbWp6ylOBJP8US-L_1si6f5Gc,5268
|
|
15
|
+
goose/targets.py,sha256=Cji6Faah9psmBnyWnBf9o9giEhBjVHht1ZkAtp6NqU8,2692
|
|
16
|
+
goose/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
goose/_utils/pydantic.py,sha256=mkTODVaAT2XxcG7-GpERbDTBU6q2G2_IRS86Gj2ueNk,139
|
|
18
|
+
goose/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
goose/backend/base.py,sha256=-MH6aj9M4oRQuAQ88n0KeUWjQ0HCbSVJQiVUrTkq12c,1546
|
|
20
|
+
goose/backend/index.py,sha256=E7S8qm-rw_PlUATzGp_ZoqvHOuNS6WrZVTBzMkbSiK0,442
|
|
21
|
+
goose/backend/node.py,sha256=B9vKfbOrFZG5R5EyrfwuGoD7LLlqo2A4wFoxyNhuM3g,5166
|
|
22
|
+
goose/backend/python.py,sha256=bJvMjfsIbsrm7M9YHbkeTMqYzTpQ2BN4gpbhOrs7JeI,5087
|
|
23
|
+
goose/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
goose/git/shared.py,sha256=OzpGVKnigWdIuHghj31Vdno-8gEMpScnb1TSxpzGfjs,1238
|
|
25
|
+
goose/git/status.py,sha256=T-TAzmDm0vY6WOfpNY8CCmttI2aEmroHsZLQ9gunYLY,4254
|
|
26
|
+
git_goose-0.0.1.dist-info/LICENSE,sha256=TI5yLazmd6njfFDJi4xrvKB71AjMuNSXLZrrZ112hn0,1521
|
|
27
|
+
git_goose-0.0.1.dist-info/METADATA,sha256=jQM293oOk5zMl3qSuzWxZ_yZFYctWhV5NSdFnHm3XoU,6280
|
|
28
|
+
git_goose-0.0.1.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
|
|
29
|
+
git_goose-0.0.1.dist-info/top_level.txt,sha256=H7wC3j2XnCev-Pw5Zx1OB1QaR1u0kxIEmhfgWu7VcCE,6
|
|
30
|
+
git_goose-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
goose
|
goose/__init__.py
ADDED
|
File without changes
|
goose/__main__.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import enum
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Collection
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
from typing import Final
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from typing import TypeAlias
|
|
12
|
+
from typing import assert_never
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.live import Live
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.spinner import Spinner
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
from goose.backend.base import RunResult
|
|
23
|
+
from goose.config import HookConfig
|
|
24
|
+
|
|
25
|
+
from .asyncio import asyncio_entrypoint
|
|
26
|
+
from .context import gather_context
|
|
27
|
+
from .environment import NeedsFreeze
|
|
28
|
+
from .environment import prepare_environment
|
|
29
|
+
from .orphan_environments import probe_orphan_environments
|
|
30
|
+
from .scheduler import Scheduler
|
|
31
|
+
from .scheduler import UnitFinished
|
|
32
|
+
from .scheduler import UnitScheduled
|
|
33
|
+
from .targets import Selector
|
|
34
|
+
from .targets import get_targets
|
|
35
|
+
|
|
36
|
+
ConfigOption: TypeAlias = Annotated[
|
|
37
|
+
Path,
|
|
38
|
+
typer.Option(
|
|
39
|
+
"--config",
|
|
40
|
+
resolve_path=True,
|
|
41
|
+
readable=True,
|
|
42
|
+
exists=True,
|
|
43
|
+
dir_okay=False,
|
|
44
|
+
),
|
|
45
|
+
]
|
|
46
|
+
default_config: Final = Path("goose.yaml")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
cli = typer.Typer(pretty_exceptions_enable=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@cli.command()
|
|
53
|
+
@asyncio_entrypoint
|
|
54
|
+
async def upgrade(
|
|
55
|
+
config_path: ConfigOption = default_config,
|
|
56
|
+
) -> None:
|
|
57
|
+
ctx = gather_context(config_path)
|
|
58
|
+
await asyncio.gather(
|
|
59
|
+
*[
|
|
60
|
+
asyncio.create_task(prepare_environment(environment, upgrade=True))
|
|
61
|
+
for environment in ctx.environments.values()
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
print("All environments up-to-date", file=sys.stderr)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_unit_state(
|
|
68
|
+
state: RunResult | asyncio.Task[RunResult] | None,
|
|
69
|
+
spinner: Table,
|
|
70
|
+
) -> Text | Table:
|
|
71
|
+
match state:
|
|
72
|
+
case None:
|
|
73
|
+
return Text("[ ]", style="dim")
|
|
74
|
+
case asyncio.Task():
|
|
75
|
+
return spinner
|
|
76
|
+
case RunResult.ok:
|
|
77
|
+
return Text("[✓]", style="green")
|
|
78
|
+
case RunResult.error:
|
|
79
|
+
return Text("[✗]", style="red")
|
|
80
|
+
case RunResult.modified:
|
|
81
|
+
return Text("[✎]", style="red")
|
|
82
|
+
case no_match:
|
|
83
|
+
assert_never(no_match)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_hook_name(
|
|
87
|
+
hook: HookConfig,
|
|
88
|
+
states: Collection[RunResult | asyncio.Task[RunResult] | None],
|
|
89
|
+
) -> Text:
|
|
90
|
+
if RunResult.error in states:
|
|
91
|
+
return Text(hook.id, style="red")
|
|
92
|
+
if any(isinstance(state, asyncio.Task) for state in states):
|
|
93
|
+
return Text(hook.id)
|
|
94
|
+
if RunResult.ok in states:
|
|
95
|
+
return Text(hook.id, style="green")
|
|
96
|
+
return Text(hook.id, style="dim")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def generate_live_process_table(scheduler: Scheduler) -> Iterator[Panel]:
|
|
100
|
+
spinner = Table.grid()
|
|
101
|
+
spinner.add_row(
|
|
102
|
+
"[blue][[/blue]",
|
|
103
|
+
Spinner("dots4", style="blue"),
|
|
104
|
+
"[blue]][/blue]",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
hooks_table = Table(
|
|
109
|
+
show_header=False,
|
|
110
|
+
show_edge=False,
|
|
111
|
+
)
|
|
112
|
+
hooks_table.add_column("Hook")
|
|
113
|
+
hooks_table.add_column("Processes")
|
|
114
|
+
for hook, hook_units in scheduler.state().items():
|
|
115
|
+
process_table = Table.grid(padding=2)
|
|
116
|
+
process_table.add_row(
|
|
117
|
+
*(
|
|
118
|
+
format_unit_state(unit_state, spinner)
|
|
119
|
+
for unit, unit_state in hook_units.items()
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
hooks_table.add_row(
|
|
123
|
+
format_hook_name(hook, hook_units.values()),
|
|
124
|
+
process_table,
|
|
125
|
+
)
|
|
126
|
+
yield Panel(
|
|
127
|
+
hooks_table,
|
|
128
|
+
title="Running hooks",
|
|
129
|
+
border_style="magenta",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def display_live_table(scheduler: Scheduler) -> None:
|
|
134
|
+
live_table = generate_live_process_table(scheduler)
|
|
135
|
+
|
|
136
|
+
with Live(refresh_per_second=10) as live:
|
|
137
|
+
live.update(next(live_table), refresh=True)
|
|
138
|
+
async for _ in scheduler.until_complete():
|
|
139
|
+
live.update(next(live_table), refresh=True)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def print_summary(console: Console, scheduler: Scheduler) -> None:
|
|
143
|
+
any_error = False
|
|
144
|
+
any_modified = False
|
|
145
|
+
for units in scheduler.state().values():
|
|
146
|
+
for unit_state in units.values():
|
|
147
|
+
if unit_state is RunResult.error:
|
|
148
|
+
any_error = True
|
|
149
|
+
elif unit_state is RunResult.modified:
|
|
150
|
+
any_modified = True
|
|
151
|
+
if any_error and any_modified:
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
if any_error:
|
|
155
|
+
console.print("Some hooks errored.", style="red")
|
|
156
|
+
if any_modified:
|
|
157
|
+
console.print("Some hooks made changes.", style="red")
|
|
158
|
+
if any_error or any_modified:
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
console.print("All ok!", style="green")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@cli.command()
|
|
165
|
+
@asyncio_entrypoint
|
|
166
|
+
async def run(
|
|
167
|
+
selected_hook: Optional[str] = typer.Argument(default=None), # noqa
|
|
168
|
+
config_path: ConfigOption = default_config,
|
|
169
|
+
delete_orphan_environments: bool = False,
|
|
170
|
+
select: Selector = typer.Option(default="diff"),
|
|
171
|
+
) -> None:
|
|
172
|
+
console = Console(stderr=True)
|
|
173
|
+
ctx = gather_context(config_path)
|
|
174
|
+
|
|
175
|
+
probe_orphan_environments(ctx, delete=delete_orphan_environments)
|
|
176
|
+
|
|
177
|
+
prepare_tasks = [
|
|
178
|
+
asyncio.create_task(prepare_environment(environment))
|
|
179
|
+
for environment in ctx.environments.values()
|
|
180
|
+
]
|
|
181
|
+
done, pending = await asyncio.wait(
|
|
182
|
+
prepare_tasks,
|
|
183
|
+
return_when=asyncio.FIRST_EXCEPTION,
|
|
184
|
+
)
|
|
185
|
+
for task in done:
|
|
186
|
+
try:
|
|
187
|
+
task.result()
|
|
188
|
+
except NeedsFreeze:
|
|
189
|
+
console.print(
|
|
190
|
+
"Missing lock files, run `goose upgrade` first.",
|
|
191
|
+
style="red",
|
|
192
|
+
)
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
print("All environments ready", file=sys.stderr)
|
|
196
|
+
|
|
197
|
+
scheduler = Scheduler(
|
|
198
|
+
context=ctx,
|
|
199
|
+
targets=await get_targets(ctx.config, select),
|
|
200
|
+
selected_hook=selected_hook,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if sys.stdout.isatty():
|
|
204
|
+
await display_live_table(scheduler)
|
|
205
|
+
else:
|
|
206
|
+
async for event in scheduler.until_complete():
|
|
207
|
+
t = time.time_ns()
|
|
208
|
+
if isinstance(event, UnitScheduled):
|
|
209
|
+
print(
|
|
210
|
+
f"[{event.unit.hook.id}@{event.unit.id}] [{t}] Unit scheduled",
|
|
211
|
+
file=sys.stderr,
|
|
212
|
+
)
|
|
213
|
+
elif isinstance(event, UnitFinished):
|
|
214
|
+
print(
|
|
215
|
+
f"[{event.unit.hook.id}@{event.unit.id}] [{t}] Unit finished: {event.result.name}",
|
|
216
|
+
file=sys.stderr,
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
assert_never(event)
|
|
220
|
+
|
|
221
|
+
print_summary(console, scheduler)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
template = """\
|
|
225
|
+
#!/usr/bin/env bash
|
|
226
|
+
set -euo pipefail
|
|
227
|
+
python -m goose run '--config={config_path}'
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class GitHookType(enum.Enum):
|
|
232
|
+
pre_commit = "pre-commit"
|
|
233
|
+
pre_push = "pre-push"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@cli.command()
|
|
237
|
+
@asyncio_entrypoint
|
|
238
|
+
async def git_hook(
|
|
239
|
+
hook: GitHookType,
|
|
240
|
+
config_path: ConfigOption = default_config,
|
|
241
|
+
) -> None:
|
|
242
|
+
hooks_path = Path(".git/hooks")
|
|
243
|
+
assert hooks_path.exists()
|
|
244
|
+
assert hooks_path.is_dir()
|
|
245
|
+
hook_path = hooks_path / hook.value
|
|
246
|
+
hook_path.write_text(template.format(config_path=str(config_path)))
|
|
247
|
+
hook_path.chmod(0o755)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
cli()
|
goose/_utils/__init__.py
ADDED
|
File without changes
|
goose/_utils/pydantic.py
ADDED
goose/asyncio.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from collections.abc import Coroutine
|
|
5
|
+
from typing import ParamSpec
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
P = ParamSpec("P")
|
|
9
|
+
R = TypeVar("R")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def asyncio_entrypoint(
|
|
13
|
+
fn: Callable[P, Coroutine[None, None, R]],
|
|
14
|
+
) -> Callable[P, R]:
|
|
15
|
+
@functools.wraps(fn)
|
|
16
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
17
|
+
return asyncio.run(fn(*args, **kwargs))
|
|
18
|
+
|
|
19
|
+
return wrapper
|
|
File without changes
|
goose/backend/base.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from collections.abc import Awaitable
|
|
3
|
+
from collections.abc import Coroutine
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
from typing import final
|
|
8
|
+
|
|
9
|
+
from goose.config import EnvironmentConfig
|
|
10
|
+
from goose.executable_unit import ExecutableUnit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Bootstrap(Protocol):
|
|
14
|
+
def __call__(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
config: EnvironmentConfig,
|
|
18
|
+
env_path: Path,
|
|
19
|
+
) -> Awaitable[None]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Freeze(Protocol):
|
|
23
|
+
def __call__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
config: EnvironmentConfig,
|
|
27
|
+
env_path: Path,
|
|
28
|
+
lock_files_path: Path,
|
|
29
|
+
) -> Awaitable[None]: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Sync(Protocol):
|
|
33
|
+
def __call__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
config: EnvironmentConfig,
|
|
37
|
+
env_path: Path,
|
|
38
|
+
lock_files_path: Path,
|
|
39
|
+
) -> Awaitable[None]: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RunResult(enum.Enum):
|
|
43
|
+
ok = enum.auto()
|
|
44
|
+
error = enum.auto()
|
|
45
|
+
modified = enum.auto()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Run(Protocol):
|
|
49
|
+
def __call__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
config: EnvironmentConfig,
|
|
53
|
+
env_path: Path,
|
|
54
|
+
unit: ExecutableUnit,
|
|
55
|
+
) -> Coroutine[None, None, RunResult]: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@final
|
|
59
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
60
|
+
class Backend:
|
|
61
|
+
ecosystem: str
|
|
62
|
+
bootstrap: Bootstrap
|
|
63
|
+
"""
|
|
64
|
+
- Create environment if it does not exist.
|
|
65
|
+
- Install basic dependencies to enable freezing.
|
|
66
|
+
"""
|
|
67
|
+
freeze: Freeze
|
|
68
|
+
"""
|
|
69
|
+
- Update lock file.
|
|
70
|
+
"""
|
|
71
|
+
sync: Sync
|
|
72
|
+
"""
|
|
73
|
+
- Install missing dependencies.
|
|
74
|
+
- Uninstall obsolete dependencies.
|
|
75
|
+
"""
|
|
76
|
+
run: Run
|
goose/backend/index.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
from goose.config import EcosystemConfig
|
|
6
|
+
|
|
7
|
+
from . import node
|
|
8
|
+
from . import python
|
|
9
|
+
from .base import Backend
|
|
10
|
+
|
|
11
|
+
backends: Final[Mapping[str, Backend]] = MappingProxyType(
|
|
12
|
+
{
|
|
13
|
+
"node": node.backend,
|
|
14
|
+
"python": python.backend,
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_backend(ecosystem: EcosystemConfig) -> Backend:
|
|
20
|
+
return backends[ecosystem.language]
|