gitd-client 0.14.0__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.
- gitd/__init__.py +88 -0
- gitd/changes.py +77 -0
- gitd/client.py +245 -0
- gitd/comparison.py +111 -0
- gitd/connection.py +128 -0
- gitd/contract.json +3189 -0
- gitd/contract.py +50 -0
- gitd/credentials_calls.py +69 -0
- gitd/endpoint_calls.py +52 -0
- gitd/endpoint_tokens.py +55 -0
- gitd/errors.py +36 -0
- gitd/kept_credentials.py +107 -0
- gitd/messages.py +92 -0
- gitd/methods.py +58 -0
- gitd/models.py +369 -0
- gitd/publishing_calls.py +47 -0
- gitd/publishing_settings.py +216 -0
- gitd/timestamps.py +22 -0
- gitd/warm_layers.py +34 -0
- gitd_client-0.14.0.dist-info/METADATA +92 -0
- gitd_client-0.14.0.dist-info/RECORD +22 -0
- gitd_client-0.14.0.dist-info/WHEEL +4 -0
gitd/models.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""What gitd answers, as plain dataclasses, each built from the message gitd sends. The
|
|
2
|
+
messages are made from the contract at run time, so their fields are typed here as Any."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import messages
|
|
12
|
+
from .publishing_settings import OpenedPullRequest, Provenance
|
|
13
|
+
from .timestamps import Timestamp
|
|
14
|
+
|
|
15
|
+
STRATEGY_PREFIX = "STRATEGY_"
|
|
16
|
+
ACTION_PREFIX = "ACTION_"
|
|
17
|
+
ACCESS_PREFIX = "ACCESS_"
|
|
18
|
+
REASON_PREFIX = "REASON_"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Strategy(StrEnum):
|
|
22
|
+
"""How a workspace is made: a layer on a shared template, a block clone, or every file written."""
|
|
23
|
+
|
|
24
|
+
OVERLAY = "overlay"
|
|
25
|
+
REFLINK = "reflink"
|
|
26
|
+
COPY = "copy"
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_message(cls, number: int) -> Strategy:
|
|
30
|
+
name = messages.STRATEGY.name(number).removeprefix(STRATEGY_PREFIX).lower()
|
|
31
|
+
return cls(name) if name in {member.value for member in cls} else cls.COPY
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def preference(cls, strategy: Strategy | None) -> int:
|
|
35
|
+
"""The number a request carries: the strategy asked for, or none, for gitd to choose."""
|
|
36
|
+
name = strategy.value.upper() if strategy else "UNSPECIFIED"
|
|
37
|
+
return messages.STRATEGY.number(STRATEGY_PREFIX + name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Access(StrEnum):
|
|
41
|
+
"""Whether a repository can be read: by anyone, with a credential gitd keeps, or not at all."""
|
|
42
|
+
|
|
43
|
+
PUBLIC = "public"
|
|
44
|
+
REACHABLE = "reachable"
|
|
45
|
+
DENIED = "denied"
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_message(cls, number: int) -> Access:
|
|
49
|
+
name = messages.ACCESS.name(number).removeprefix(ACCESS_PREFIX).lower()
|
|
50
|
+
return cls(name) if name in {member.value for member in cls} else cls.DENIED
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Progress:
|
|
55
|
+
"""How far a fetch or checkout has gone; repository is the submodule's URL, empty for the
|
|
56
|
+
workspace's own repository."""
|
|
57
|
+
|
|
58
|
+
phase: str
|
|
59
|
+
percent: int
|
|
60
|
+
repository: str
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_message(cls, progress: Any) -> Progress:
|
|
64
|
+
return cls(phase=progress.phase, percent=progress.percent, repository=progress.repository)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class Layers:
|
|
69
|
+
"""An overlay workspace's template (lower), its own layer (upper, work), and the warm
|
|
70
|
+
layer between them, when it stands on one."""
|
|
71
|
+
|
|
72
|
+
lower: str
|
|
73
|
+
upper: str
|
|
74
|
+
work: str
|
|
75
|
+
warm: str = ""
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_message(cls, layers: Any) -> Layers:
|
|
79
|
+
return cls(lower=layers.lower, upper=layers.upper, work=layers.work, warm=layers.warm)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class PendingSubmodule:
|
|
84
|
+
"""A submodule that could not be checked out, and what git said."""
|
|
85
|
+
|
|
86
|
+
path: str
|
|
87
|
+
url: str
|
|
88
|
+
reason: str
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_message(cls, submodule: Any) -> PendingSubmodule:
|
|
92
|
+
return cls(path=submodule.path, url=submodule.url, reason=submodule.failure.git_said)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class Workspace:
|
|
97
|
+
"""Where a workspace ended up. branch is empty when it is detached at a commit;
|
|
98
|
+
lfs_patterns are the files the repository keeps in git LFS, which are pointers here, as
|
|
99
|
+
gitd does not download them."""
|
|
100
|
+
|
|
101
|
+
path: str
|
|
102
|
+
commit: str
|
|
103
|
+
branch: str
|
|
104
|
+
strategy: Strategy
|
|
105
|
+
layers: Layers | None = None
|
|
106
|
+
pending_submodules: list[PendingSubmodule] = field(default_factory=list)
|
|
107
|
+
lfs_patterns: list[str] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_message(cls, path: str, result: Any) -> Workspace:
|
|
111
|
+
return cls(
|
|
112
|
+
path=path,
|
|
113
|
+
commit=result.commit,
|
|
114
|
+
branch=result.branch,
|
|
115
|
+
strategy=Strategy.from_message(result.strategy),
|
|
116
|
+
layers=Layers.from_message(result.layers) if result.HasField("layers") else None,
|
|
117
|
+
pending_submodules=[
|
|
118
|
+
PendingSubmodule.from_message(pending) for pending in result.pending_submodules
|
|
119
|
+
],
|
|
120
|
+
lfs_patterns=list(result.lfs_patterns),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class Submodule:
|
|
126
|
+
path: str
|
|
127
|
+
url: str
|
|
128
|
+
commit: str
|
|
129
|
+
checked_out: bool
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_message(cls, submodule: Any) -> Submodule:
|
|
133
|
+
return cls(
|
|
134
|
+
path=submodule.path,
|
|
135
|
+
url=submodule.url,
|
|
136
|
+
commit=submodule.commit,
|
|
137
|
+
checked_out=submodule.checked_out,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class Description:
|
|
143
|
+
"""What a workspace is on disk. modified says whether it has uncommitted changes;
|
|
144
|
+
lfs_patterns are the files kept in git LFS, which are pointers here."""
|
|
145
|
+
|
|
146
|
+
url: str
|
|
147
|
+
commit: str
|
|
148
|
+
branch: str
|
|
149
|
+
modified: bool
|
|
150
|
+
strategy: Strategy
|
|
151
|
+
submodules: list[Submodule] = field(default_factory=list)
|
|
152
|
+
lfs_patterns: list[str] = field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def from_message(cls, response: Any) -> Description:
|
|
156
|
+
return cls(
|
|
157
|
+
url=response.url,
|
|
158
|
+
commit=response.commit,
|
|
159
|
+
branch=response.branch,
|
|
160
|
+
modified=response.modified,
|
|
161
|
+
strategy=Strategy.from_message(response.strategy),
|
|
162
|
+
submodules=[Submodule.from_message(submodule) for submodule in response.submodules],
|
|
163
|
+
lfs_patterns=list(response.lfs_patterns),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class PublishedRepository:
|
|
169
|
+
"""A repository a publish pushed. path is the submodule's, empty for the workspace's own
|
|
170
|
+
repository; subjects are the new commits' subjects, newest first."""
|
|
171
|
+
|
|
172
|
+
url: str
|
|
173
|
+
path: str
|
|
174
|
+
commit: str
|
|
175
|
+
subjects: list[str]
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def from_message(cls, repository: Any) -> PublishedRepository:
|
|
179
|
+
return cls(
|
|
180
|
+
url=repository.url,
|
|
181
|
+
path=repository.path,
|
|
182
|
+
commit=repository.commit,
|
|
183
|
+
subjects=list(repository.subjects),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True)
|
|
188
|
+
class Refusal:
|
|
189
|
+
"""Why gitd refused a call: its reason (BRANCH_REFUSED, AUTH_REQUIRED, …) and what git said."""
|
|
190
|
+
|
|
191
|
+
reason: str
|
|
192
|
+
said: str
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def from_message(cls, failure: Any) -> Refusal:
|
|
196
|
+
return cls(reason=cls.reason_of(failure), said=failure.git_said)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def reason_of(failure: Any) -> str:
|
|
200
|
+
return messages.REASON.name(failure.reason).removeprefix(REASON_PREFIX)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True)
|
|
204
|
+
class SubmodulePullRequest:
|
|
205
|
+
"""The pull request of one submodule, at path in the workspace and of the repository at
|
|
206
|
+
url, or why it could not be opened."""
|
|
207
|
+
|
|
208
|
+
path: str
|
|
209
|
+
url: str
|
|
210
|
+
pull_request: OpenedPullRequest | None = None
|
|
211
|
+
refused: Refusal | None = None
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def from_message(cls, submodule: Any) -> SubmodulePullRequest:
|
|
215
|
+
return cls(
|
|
216
|
+
path=submodule.path,
|
|
217
|
+
url=submodule.url,
|
|
218
|
+
pull_request=(
|
|
219
|
+
OpenedPullRequest.from_message(submodule.pull_request)
|
|
220
|
+
if submodule.HasField("pull_request")
|
|
221
|
+
else None
|
|
222
|
+
),
|
|
223
|
+
refused=Refusal.from_message(submodule.failure) if submodule.HasField("failure") else None,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True)
|
|
228
|
+
class Published:
|
|
229
|
+
"""What a publish pushed: the workspace's repository first, then each submodule; the
|
|
230
|
+
pull request it opened or updated, or why it could not, the branch being published
|
|
231
|
+
either way; and each submodule's, when asked for. An update that brought nothing new
|
|
232
|
+
lists the workspace's repository with no subjects."""
|
|
233
|
+
|
|
234
|
+
branch: str
|
|
235
|
+
repositories: list[PublishedRepository]
|
|
236
|
+
pull_request: OpenedPullRequest | None = None
|
|
237
|
+
pull_request_refused: Refusal | None = None
|
|
238
|
+
submodule_pull_requests: list[SubmodulePullRequest] = field(default_factory=list)
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def from_message(cls, response: Any) -> Published:
|
|
242
|
+
pushed = [response.repository, *response.submodules] if response.HasField("repository") else []
|
|
243
|
+
return cls(
|
|
244
|
+
branch=response.branch,
|
|
245
|
+
repositories=[PublishedRepository.from_message(repository) for repository in pushed],
|
|
246
|
+
pull_request=(
|
|
247
|
+
OpenedPullRequest.from_message(response.pull_request)
|
|
248
|
+
if response.HasField("pull_request")
|
|
249
|
+
else None
|
|
250
|
+
),
|
|
251
|
+
pull_request_refused=(
|
|
252
|
+
Refusal.from_message(response.pull_request_failure)
|
|
253
|
+
if response.HasField("pull_request_failure")
|
|
254
|
+
else None
|
|
255
|
+
),
|
|
256
|
+
submodule_pull_requests=[
|
|
257
|
+
SubmodulePullRequest.from_message(submodule) for submodule in response.submodule_pull_requests
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass(frozen=True)
|
|
263
|
+
class Info:
|
|
264
|
+
"""Which gitd answers, and how it makes workspaces on its machine and why."""
|
|
265
|
+
|
|
266
|
+
version: str
|
|
267
|
+
git_version: str
|
|
268
|
+
pools: int
|
|
269
|
+
pools_bytes: int
|
|
270
|
+
templates: int
|
|
271
|
+
templates_bytes: int
|
|
272
|
+
strategy: Strategy
|
|
273
|
+
strategy_reason: str
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def from_message(cls, response: Any) -> Info:
|
|
277
|
+
return cls(
|
|
278
|
+
version=response.version,
|
|
279
|
+
git_version=response.git_version,
|
|
280
|
+
pools=response.pools,
|
|
281
|
+
pools_bytes=response.pools_bytes,
|
|
282
|
+
templates=response.templates,
|
|
283
|
+
templates_bytes=response.templates_bytes,
|
|
284
|
+
strategy=Strategy.from_message(response.strategy),
|
|
285
|
+
strategy_reason=response.strategy_reason,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass(frozen=True)
|
|
290
|
+
class Probe:
|
|
291
|
+
"""Whether a repository can be read, its default branch, and each branch's commit.
|
|
292
|
+
denied says why it cannot be read, when it cannot."""
|
|
293
|
+
|
|
294
|
+
access: Access
|
|
295
|
+
default_branch: str
|
|
296
|
+
branches: dict[str, str]
|
|
297
|
+
denied: str | None = None
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def from_message(cls, response: Any) -> Probe:
|
|
301
|
+
return cls(
|
|
302
|
+
access=Access.from_message(response.access),
|
|
303
|
+
default_branch=response.default_branch,
|
|
304
|
+
branches=dict(response.commits),
|
|
305
|
+
denied=response.failure.git_said if response.HasField("failure") else None,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass(frozen=True)
|
|
310
|
+
class ConfigurationChange:
|
|
311
|
+
"""A change to the credentials or publishing settings: connection_added,
|
|
312
|
+
connection_removed, settings_set or settings_removed; subject is the credential's id or
|
|
313
|
+
the settings' prefix."""
|
|
314
|
+
|
|
315
|
+
action: str
|
|
316
|
+
subject: str
|
|
317
|
+
url_prefix: str
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def from_message(cls, change: Any) -> ConfigurationChange:
|
|
321
|
+
action = messages.CHANGE_ACTION.name(change.action).removeprefix(ACTION_PREFIX).lower()
|
|
322
|
+
return cls(action=action, subject=change.subject, url_prefix=change.url_prefix)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass(frozen=True)
|
|
326
|
+
class AuditEntry:
|
|
327
|
+
"""One publish, done or refused, or one change to credentials or settings, as the audit
|
|
328
|
+
log keeps it. url is the workspace's repository, empty when the workspace could not be
|
|
329
|
+
opened; repositories is what was pushed, the workspace's repository first, empty when
|
|
330
|
+
refused; requested_by and the pull request are as the publish said and did."""
|
|
331
|
+
|
|
332
|
+
time: datetime
|
|
333
|
+
workspace: str
|
|
334
|
+
url: str
|
|
335
|
+
branch: str
|
|
336
|
+
repositories: list[PublishedRepository] = field(default_factory=list)
|
|
337
|
+
refused: Refusal | None = None
|
|
338
|
+
requested_by: str = ""
|
|
339
|
+
pull_request: OpenedPullRequest | None = None
|
|
340
|
+
pull_request_refused: Refusal | None = None
|
|
341
|
+
change: ConfigurationChange | None = None
|
|
342
|
+
provenance: Provenance | None = None
|
|
343
|
+
submodule_pull_requests: list[SubmodulePullRequest] = field(default_factory=list)
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def from_message(cls, entry: Any) -> AuditEntry:
|
|
347
|
+
pushed = [entry.repository, *entry.submodules] if entry.HasField("repository") else []
|
|
348
|
+
return cls(
|
|
349
|
+
time=Timestamp.to_datetime(entry.time),
|
|
350
|
+
workspace=entry.workspace,
|
|
351
|
+
url=entry.url,
|
|
352
|
+
branch=entry.branch,
|
|
353
|
+
repositories=[PublishedRepository.from_message(repository) for repository in pushed],
|
|
354
|
+
refused=Refusal.from_message(entry.refusal) if entry.HasField("refusal") else None,
|
|
355
|
+
requested_by=entry.requested_by,
|
|
356
|
+
pull_request=OpenedPullRequest.from_message(entry.pull_request)
|
|
357
|
+
if entry.HasField("pull_request")
|
|
358
|
+
else None,
|
|
359
|
+
pull_request_refused=(
|
|
360
|
+
Refusal.from_message(entry.pull_request_failure)
|
|
361
|
+
if entry.HasField("pull_request_failure")
|
|
362
|
+
else None
|
|
363
|
+
),
|
|
364
|
+
change=ConfigurationChange.from_message(entry.change) if entry.HasField("change") else None,
|
|
365
|
+
provenance=Provenance.from_message(entry.provenance) if entry.HasField("provenance") else None,
|
|
366
|
+
submodule_pull_requests=[
|
|
367
|
+
SubmodulePullRequest.from_message(submodule) for submodule in entry.submodule_pull_requests
|
|
368
|
+
],
|
|
369
|
+
)
|
gitd/publishing_calls.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""The calls that read and set the rules publishing follows: for a platform's own pages,
|
|
2
|
+
never for an agent."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from . import messages, methods
|
|
7
|
+
from .connection import Connection
|
|
8
|
+
from .publishing_settings import Preset, PublishSettings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Publishing:
|
|
12
|
+
"""The publishing settings, per repository prefix, as gitd.publishing.
|
|
13
|
+
|
|
14
|
+
gitd.publishing.set(PublishSettings(url_prefix="https://github.com/acme", preset="recommended",
|
|
15
|
+
branch_prefix="agents/"))
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, connection: Connection) -> None:
|
|
19
|
+
self.connection = connection
|
|
20
|
+
|
|
21
|
+
def settings(self) -> list[PublishSettings]:
|
|
22
|
+
"""Every setting, gitd.toml's included."""
|
|
23
|
+
response = self.connection.call(methods.GET_SETTINGS, messages.GetSettingsRequest())
|
|
24
|
+
return [PublishSettings.from_message(settings) for settings in response.settings]
|
|
25
|
+
|
|
26
|
+
def set(self, settings: PublishSettings) -> PublishSettings:
|
|
27
|
+
"""Keeps the settings of a prefix, replacing those it had, as gitd checked them."""
|
|
28
|
+
request = messages.SetSettingsRequest(settings=settings.to_message())
|
|
29
|
+
return PublishSettings.from_message(self.connection.call(methods.SET_SETTINGS, request).settings)
|
|
30
|
+
|
|
31
|
+
def remove(self, url_prefix: str) -> None:
|
|
32
|
+
"""Forgets the settings of a prefix set through the API."""
|
|
33
|
+
self.connection.call(methods.REMOVE_SETTINGS, messages.RemoveSettingsRequest(url_prefix=url_prefix))
|
|
34
|
+
|
|
35
|
+
def presets(self) -> list[Preset]:
|
|
36
|
+
"""Every preset as it applies: gitd's own, gitd.toml's and those set through the API."""
|
|
37
|
+
response = self.connection.call(methods.LIST_PRESETS, messages.ListPresetsRequest())
|
|
38
|
+
return [Preset.from_message(preset) for preset in response.presets]
|
|
39
|
+
|
|
40
|
+
def set_preset(self, preset: Preset) -> Preset:
|
|
41
|
+
"""Keeps a preset, replacing one of the same name; "recommended" replaces gitd's own."""
|
|
42
|
+
request = messages.SetPresetRequest(preset=preset.to_message())
|
|
43
|
+
return Preset.from_message(self.connection.call(methods.SET_PRESET, request).preset)
|
|
44
|
+
|
|
45
|
+
def remove_preset(self, name: str) -> None:
|
|
46
|
+
"""Forgets a preset set through the API; refused while settings take it."""
|
|
47
|
+
self.connection.call(methods.REMOVE_PRESET, messages.RemovePresetRequest(name=name))
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""The rules publishing follows, and what a publish says of its pull request, as plain
|
|
2
|
+
dataclasses that write the messages gitd takes and read those it answers."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from . import messages
|
|
11
|
+
|
|
12
|
+
OPENING_PREFIX = "OPEN_"
|
|
13
|
+
ORIGIN_PREFIX = "ORIGIN_"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Opening(StrEnum):
|
|
17
|
+
"""When publishes open pull requests: only when a publish asks, or always."""
|
|
18
|
+
|
|
19
|
+
WHEN_ASKED = "when_asked"
|
|
20
|
+
ALWAYS = "always"
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_message(cls, number: int) -> Opening:
|
|
24
|
+
name = messages.OPENING.name(number).removeprefix(OPENING_PREFIX).lower()
|
|
25
|
+
return cls(name) if name in {member.value for member in cls} else cls.WHEN_ASKED
|
|
26
|
+
|
|
27
|
+
def to_message(self) -> int:
|
|
28
|
+
return messages.OPENING.number(OPENING_PREFIX + self.value.upper())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PullRequestSettings:
|
|
33
|
+
"""How publishes open pull requests: when, as drafts or not, with which labels, and
|
|
34
|
+
whether the person a publish names as requester is asked to review."""
|
|
35
|
+
|
|
36
|
+
opening: Opening = Opening.WHEN_ASKED
|
|
37
|
+
draft: bool = False
|
|
38
|
+
labels: list[str] = field(default_factory=list)
|
|
39
|
+
request_review_from_requester: bool = False
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_message(cls, settings: Any) -> PullRequestSettings:
|
|
43
|
+
return cls(
|
|
44
|
+
opening=Opening.from_message(settings.open),
|
|
45
|
+
draft=settings.draft,
|
|
46
|
+
labels=list(settings.labels),
|
|
47
|
+
request_review_from_requester=settings.request_review_from_requester,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def to_message(self) -> Any:
|
|
51
|
+
return messages.PullRequestSettings(
|
|
52
|
+
open=self.opening.to_message(),
|
|
53
|
+
draft=self.draft,
|
|
54
|
+
labels=self.labels,
|
|
55
|
+
request_review_from_requester=self.request_review_from_requester,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class PublishSettings:
|
|
61
|
+
"""The rules of the repositories whose address starts with url_prefix (empty for every
|
|
62
|
+
repository): the name of a preset whose rules they take ("recommended" is gitd's own),
|
|
63
|
+
paths a publish may not change and paths it may not delete, as git's glob pathspecs,
|
|
64
|
+
paths those rules leave free, at most how many files it may change, the branch prefix,
|
|
65
|
+
and how pull requests are opened. from_file says gitd.toml declares them."""
|
|
66
|
+
|
|
67
|
+
url_prefix: str = ""
|
|
68
|
+
preset: str = ""
|
|
69
|
+
protected_paths: list[str] = field(default_factory=list)
|
|
70
|
+
kept_paths: list[str] = field(default_factory=list)
|
|
71
|
+
allowed_paths: list[str] = field(default_factory=list)
|
|
72
|
+
max_changed_files: int | None = None
|
|
73
|
+
branch_prefix: str = ""
|
|
74
|
+
pull_requests: PullRequestSettings | None = None
|
|
75
|
+
from_file: bool = False
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_message(cls, settings: Any) -> PublishSettings:
|
|
79
|
+
return cls(
|
|
80
|
+
url_prefix=settings.url_prefix,
|
|
81
|
+
preset=settings.preset,
|
|
82
|
+
protected_paths=list(settings.protected_paths),
|
|
83
|
+
kept_paths=list(settings.kept_paths),
|
|
84
|
+
allowed_paths=list(settings.allowed_paths),
|
|
85
|
+
max_changed_files=settings.max_changed_files if settings.HasField("max_changed_files") else None,
|
|
86
|
+
branch_prefix=settings.branch_prefix,
|
|
87
|
+
pull_requests=(
|
|
88
|
+
PullRequestSettings.from_message(settings.pull_requests)
|
|
89
|
+
if settings.HasField("pull_requests")
|
|
90
|
+
else None
|
|
91
|
+
),
|
|
92
|
+
from_file=settings.from_file,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def to_message(self) -> Any:
|
|
96
|
+
message: Any = messages.PublishSettings(
|
|
97
|
+
url_prefix=self.url_prefix,
|
|
98
|
+
preset=self.preset,
|
|
99
|
+
protected_paths=self.protected_paths,
|
|
100
|
+
kept_paths=self.kept_paths,
|
|
101
|
+
allowed_paths=self.allowed_paths,
|
|
102
|
+
branch_prefix=self.branch_prefix,
|
|
103
|
+
)
|
|
104
|
+
if self.max_changed_files is not None:
|
|
105
|
+
message.max_changed_files = self.max_changed_files
|
|
106
|
+
if self.pull_requests is not None:
|
|
107
|
+
message.pull_requests.CopyFrom(self.pull_requests.to_message())
|
|
108
|
+
return message
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class PresetOrigin(StrEnum):
|
|
112
|
+
"""Where a preset comes from: gitd's own, gitd.toml, or the API."""
|
|
113
|
+
|
|
114
|
+
BUILT_IN = "built_in"
|
|
115
|
+
FILE = "file"
|
|
116
|
+
API = "api"
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_message(cls, number: int) -> PresetOrigin:
|
|
120
|
+
name = messages.PRESET_ORIGIN.name(number).removeprefix(ORIGIN_PREFIX).lower()
|
|
121
|
+
return cls(name) if name in {member.value for member in cls} else cls.API
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class Preset:
|
|
126
|
+
"""A named set of rules settings take: paths a publish may not change, paths it may not
|
|
127
|
+
delete, and at most how many files it may change. One named "recommended" replaces
|
|
128
|
+
gitd's own. origin is read from gitd, never sent."""
|
|
129
|
+
|
|
130
|
+
name: str
|
|
131
|
+
protected_paths: list[str] = field(default_factory=list)
|
|
132
|
+
kept_paths: list[str] = field(default_factory=list)
|
|
133
|
+
max_changed_files: int | None = None
|
|
134
|
+
origin: PresetOrigin = PresetOrigin.API
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_message(cls, preset: Any) -> Preset:
|
|
138
|
+
return cls(
|
|
139
|
+
name=preset.name,
|
|
140
|
+
protected_paths=list(preset.protected_paths),
|
|
141
|
+
kept_paths=list(preset.kept_paths),
|
|
142
|
+
max_changed_files=preset.max_changed_files if preset.HasField("max_changed_files") else None,
|
|
143
|
+
origin=PresetOrigin.from_message(preset.origin),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def to_message(self) -> Any:
|
|
147
|
+
message: Any = messages.Preset(
|
|
148
|
+
name=self.name, protected_paths=self.protected_paths, kept_paths=self.kept_paths
|
|
149
|
+
)
|
|
150
|
+
if self.max_changed_files is not None:
|
|
151
|
+
message.max_changed_files = self.max_changed_files
|
|
152
|
+
return message
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class PullRequestOptions:
|
|
157
|
+
"""A publish's own say on its pull request: open one even when the settings open them
|
|
158
|
+
only when asked, its title (the newest commit's subject when empty), the text before
|
|
159
|
+
what gitd checked, the branch it merges into, and whether it is a draft. submodules
|
|
160
|
+
opens one too in each submodule's repository, with the same title, into the branch
|
|
161
|
+
.gitmodules names for it or its default branch: to be merged first, with a merge commit."""
|
|
162
|
+
|
|
163
|
+
open: bool = False
|
|
164
|
+
title: str = ""
|
|
165
|
+
body: str = ""
|
|
166
|
+
base: str = ""
|
|
167
|
+
draft: bool | None = None
|
|
168
|
+
submodules: bool = False
|
|
169
|
+
|
|
170
|
+
def to_message(self) -> Any:
|
|
171
|
+
message: Any = messages.PullRequestOptions(
|
|
172
|
+
open=self.open, title=self.title, body=self.body, base=self.base, submodules=self.submodules
|
|
173
|
+
)
|
|
174
|
+
if self.draft is not None:
|
|
175
|
+
message.draft = self.draft
|
|
176
|
+
return message
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True)
|
|
180
|
+
class Provenance:
|
|
181
|
+
"""Where a publish's work came from, as the platform says: gitd records it as told and
|
|
182
|
+
never writes it into a commit. Each field is one line of at most 200 characters;
|
|
183
|
+
task_url is an https address."""
|
|
184
|
+
|
|
185
|
+
agent: str = ""
|
|
186
|
+
model: str = ""
|
|
187
|
+
session: str = ""
|
|
188
|
+
task_url: str = ""
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_message(cls, provenance: Any) -> Provenance:
|
|
192
|
+
return cls(
|
|
193
|
+
agent=provenance.agent,
|
|
194
|
+
model=provenance.model,
|
|
195
|
+
session=provenance.session,
|
|
196
|
+
task_url=provenance.task_url,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def to_message(self) -> Any:
|
|
200
|
+
return messages.Provenance(
|
|
201
|
+
agent=self.agent, model=self.model, session=self.session, task_url=self.task_url
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class OpenedPullRequest:
|
|
207
|
+
"""A pull request gitd opened; updated says it was already open from the branch, and
|
|
208
|
+
gitd updated its title and body."""
|
|
209
|
+
|
|
210
|
+
url: str
|
|
211
|
+
number: int
|
|
212
|
+
updated: bool = False
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def from_message(cls, opened: Any) -> OpenedPullRequest:
|
|
216
|
+
return cls(url=opened.url, number=opened.number, updated=opened.updated)
|
gitd/timestamps.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Between the contract's google.protobuf.Timestamp and Python's datetime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
NANOSECONDS_PER_SECOND = 1_000_000_000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Timestamp:
|
|
12
|
+
"""A Timestamp message read as a datetime, or written from one, always in UTC."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def to_datetime(message: Any) -> datetime:
|
|
16
|
+
return datetime.fromtimestamp(message.seconds + message.nanos / NANOSECONDS_PER_SECOND, tz=UTC)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def fill(message: Any, moment: datetime) -> None:
|
|
20
|
+
seconds = moment.timestamp()
|
|
21
|
+
message.seconds = int(seconds // 1)
|
|
22
|
+
message.nanos = int(round((seconds % 1) * NANOSECONDS_PER_SECOND))
|
gitd/warm_layers.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Warm layers: what installing a repository's dependencies wrote, sealed from one
|
|
2
|
+
workspace for the next ones to stand on."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class WarmLayer:
|
|
13
|
+
"""A layer sealed from a workspace. Workspaces stand on it at commit only; changed_files
|
|
14
|
+
are the files it changes that git does not ignore, and users the workspaces on it now."""
|
|
15
|
+
|
|
16
|
+
url: str
|
|
17
|
+
name: str
|
|
18
|
+
commit: str
|
|
19
|
+
sealed_at: datetime
|
|
20
|
+
sealed_from: str
|
|
21
|
+
changed_files: int
|
|
22
|
+
users: int
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_message(cls, warm: Any) -> WarmLayer:
|
|
26
|
+
return cls(
|
|
27
|
+
url=warm.url,
|
|
28
|
+
name=warm.name,
|
|
29
|
+
commit=warm.commit,
|
|
30
|
+
sealed_at=datetime.fromtimestamp(warm.sealed_at, tz=UTC),
|
|
31
|
+
sealed_from=warm.sealed_from,
|
|
32
|
+
changed_files=warm.changed_files,
|
|
33
|
+
users=warm.users,
|
|
34
|
+
)
|