Skip to content

g1t/crates/contracts/src/events.rs

747 lines27,116 bytesCodeBlame
1//! Events published on the bus, and the events service that carries them.
2//! Mirrors `packages/contracts/src/events.ts`.
3
4use serde::Serialize;
5
6/// What a publisher supplies; the bus fills in the id and time.
7#[derive(Debug, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct NewEvent<T: Serialize> {
10 #[serde(rename = "type")]
11 pub kind: &'static str,
12 /// The service that published it.
13 pub source: &'static str,
14 /// The repo the event concerns.
15 pub repo_id: Option<String>,
16 /// The user or agent that caused it, if any.
17 pub actor: Option<String>,
18 pub data: T,
19}
20
21#[derive(Debug, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct RepoCreated {
24 pub repo_id: String,
25 pub namespace: String,
26 pub name: String,
27 pub is_private: bool,
28}
29
30/// The payload of `repo.collaborator_added`, `repo.collaborator_removed`
31/// and `repo.collaborator_role_changed`: a person's own role on one
32/// repository (see `access`). `role` is the role they have now (null once
33/// removed); `previous_role` what they had before (null when added).
34#[derive(Debug, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct RepoCollaborator {
37 pub repo_id: String,
38 pub namespace: String,
39 pub name: String,
40 pub username: String,
41 pub role: Option<crate::access::RepoRole>,
42 pub previous_role: Option<crate::access::RepoRole>,
43}
44
45#[derive(Debug, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub struct RepoForked {
48 pub repo_id: String,
49 pub source_repo_id: String,
50 pub pull_id: String,
51}
52
53/// One branch or tag moved by a push. `after` is the commit it points to now.
54#[derive(Debug, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct GitPush {
57 pub repo_id: String,
58 /// The full ref, such as `refs/heads/main`.
59 #[serde(rename = "ref")]
60 pub git_ref: String,
61 /// Where it pointed before; absent for a new branch or tag.
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub before: Option<String>,
64 pub after: String,
65 /// Whether the ref is the repository's default branch.
66 pub default_branch: bool,
67 /// Set when the push was too large to scan for secrets before it was
68 /// stored, and was let through: the security service scans
69 /// `before..after` after it landed. Absent otherwise.
70 #[serde(skip_serializing_if = "std::ops::Not::not")]
71 pub unscanned: bool,
72}
73
74/// The payload of `issue.opened`, `issue.updated`, `issue.assigned`,
75/// `issue.closed` and `issue.reopened`; each uses the fields that apply to it.
76#[derive(Debug, Default, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct IssueEvent {
79 pub issue_id: String,
80 pub repo_id: String,
81 pub number: u32,
82 /// Who opened it: g1t, for one its agent filed while at work.
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub author: Option<crate::credentials::Principal>,
85 /// For an issue g1t's agent filed: the person it was working for.
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub requested_by: Option<crate::credentials::Principal>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub title: Option<String>,
90 /// On close: `completed` or `not_planned`.
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub reason: Option<&'static str>,
93 /// On close: the number of the pull request whose merge closed it.
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub resolved_by: Option<u32>,
96 /// On `issue.assigned`: the people it is now assigned to.
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub assignees: Option<Vec<String>>,
99 /// On `issue.assigned`: those of them who were not before.
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub added: Option<Vec<String>>,
102}
103
104/// The payload of `pull.opened`, `pull.ready`, `pull.updated` (its head
105/// moved), `pull.closed`, `pull.merged`, `pull.assigned`,
106/// `pull.review_requested` and `pull.review_request_removed` (reviewers
107/// asked, or no longer), `pull.stalled` (g1t stopped seeing it through
108/// until a person steps in) and `pull.resumed` (it picked back up); each
109/// uses the fields that apply to it.
110#[derive(Debug, Default, Serialize)]
111#[serde(rename_all = "camelCase")]
112pub struct PullEvent {
113 pub pull_id: String,
114 pub repo_id: String,
115 pub number: u32,
116 /// Who opened it: g1t, for a change g1t made.
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub author: Option<crate::credentials::Principal>,
119 /// For a change g1t made: the person who asked for it.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub requested_by: Option<crate::credentials::Principal>,
122 /// The number of the issue it is for.
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub issue: Option<u32>,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub agent: Option<String>,
127 /// On merge: the commit the branch now points to. On update and when
128 /// marked ready: the head of the change.
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub commit: Option<String>,
131 /// On close: the pull request that was merged instead.
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub superseded_by: Option<u32>,
134 /// How sure g1t is of a g1t agent's change, once it has worked that out.
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub confidence: Option<crate::work::Confidence>,
137 /// On `pull.assigned`: the people it is now assigned to.
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub assignees: Option<Vec<String>>,
140 /// On `pull.assigned`: those newly assigned.
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub added: Option<Vec<String>>,
143 /// On `pull.review_requested`: the reviewers newly asked; on
144 /// `pull.review_request_removed`, those no longer asked.
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub reviewers: Option<Vec<String>>,
147 /// On `pull.stalled`: why g1t stopped, and what would start it again.
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub detail: Option<String>,
150}
151
152/// `deployment.succeeded` and `deployment.failed`: a build of a project
153/// finished, for production or for one pull request's preview.
154#[derive(Debug, Serialize)]
155#[serde(rename_all = "camelCase")]
156pub struct DeploymentEvent {
157 pub deployment_id: String,
158 pub project_id: String,
159 pub repo_id: String,
160 pub workspace: String,
161 /// The project's slug.
162 pub project: String,
163 /// `production` or `preview`.
164 pub kind: String,
165 pub branch: Option<String>,
166 /// For a preview: its pull request.
167 pub number: Option<u32>,
168 pub commit: String,
169 /// Where the deployment is on the site, such as
170 /// `/acme/rocket/deployments/dpl_1`.
171 pub path: String,
172 /// For a failure: what went wrong.
173 pub error: Option<String>,
174 /// For a success: whether the deployment before it, of the same app, failed.
175 pub recovered: bool,
176 /// Who started it, by username, or `g1t`.
177 pub triggered_by: String,
178}
179
180/// `checks.completed`: a run of an issue's acceptance checks against a pull
181/// request finished.
182#[derive(Debug, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct ChecksEvent {
185 pub pull_id: String,
186 pub repo_id: String,
187 pub number: u32,
188 /// `passed`, `failed` or `errored`.
189 pub status: &'static str,
190 /// The commit that was checked.
191 pub commit: String,
192}
193
194/// `workflow.completed`: a GitHub Actions run finished.
195#[derive(Debug, Serialize)]
196#[serde(rename_all = "camelCase")]
197pub struct WorkflowEvent {
198 pub run_id: String,
199 pub repo_id: String,
200 /// The workflow's name, and its file.
201 pub workflow: String,
202 pub path: String,
203 /// The run's number among the workflow's runs.
204 pub number: u64,
205 /// The GitHub event that started it, such as `push`.
206 pub event: String,
207 /// `success`, `failure`, `cancelled` or `skipped`.
208 pub conclusion: String,
209 #[serde(rename = "ref")]
210 pub git_ref: String,
211 pub sha: String,
212 /// The pull request it ran for, if any.
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub pull: Option<u32>,
215}
216
217/// `review.completed`: a g1t agent finished reviewing a pull request, or
218/// could not.
219#[derive(Debug, Serialize)]
220#[serde(rename_all = "camelCase")]
221pub struct ReviewEvent {
222 pub pull_id: String,
223 pub repo_id: String,
224 pub number: u32,
225 /// `approve` or `request_changes`; absent when no review was written.
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub verdict: Option<&'static str>,
228}
229
230/// `comment.created`. `number` is the issue or pull request commented on.
231#[derive(Debug, Serialize)]
232#[serde(rename_all = "camelCase")]
233pub struct CommentCreated {
234 pub comment_id: String,
235 pub repo_id: String,
236 pub number: u32,
237 /// Set when the comment is on a pull request.
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub pull_id: Option<String>,
240 /// Set when the comment is a review: approve or request changes.
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub verdict: Option<crate::work::Verdict>,
243}
244
245#[derive(Debug, Serialize)]
246#[serde(rename_all = "camelCase")]
247pub struct SessionAppended {
248 pub pull_id: String,
249 pub repo_id: String,
250 pub number: u32,
251 pub count: u32,
252}
253
254/// An event as stored in the log and delivered to subscribers. `data` is
255/// left as JSON; each reader decodes the types it cares about.
256#[derive(Clone, Debug, Serialize, serde::Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct Event {
259 /// Sorts by the time the event was published.
260 pub id: String,
261 #[serde(rename = "type")]
262 pub kind: String,
263 /// The service that published it.
264 pub source: String,
265 /// RFC 3339.
266 pub time: String,
267 /// The repo the event concerns.
268 pub repo_id: Option<String>,
269 /// The user or agent that caused it, if any.
270 pub actor: Option<String>,
271 pub data: serde_json::Value,
272}
273
274/// `publish`, as a publisher sends it. Returns nothing.
275#[derive(Debug, Serialize)]
276pub struct Publish<T: Serialize> {
277 pub events: Vec<NewEvent<T>>,
278}
279
280/// `publish`, as the events service reads it.
281#[derive(Debug, serde::Deserialize)]
282pub struct PublishArgs {
283 pub events: Vec<Published>,
284}
285
286/// A [`NewEvent`] of any type, as received.
287#[derive(Debug, serde::Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct Published {
290 #[serde(rename = "type")]
291 pub kind: String,
292 pub source: String,
293 #[serde(default)]
294 pub repo_id: Option<String>,
295 #[serde(default)]
296 pub actor: Option<String>,
297 pub data: serde_json::Value,
298}
299
300/// `list`: events from the log, newest first. Returns `Vec<Event>`.
301#[derive(Debug, Default, Serialize, serde::Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct ListArgs {
304 #[serde(default)]
305 pub repo_id: Option<String>,
306 /// Only these types; all types when empty.
307 #[serde(default)]
308 pub types: Vec<String>,
309 /// Only events older than this event id.
310 #[serde(default)]
311 pub before: Option<String>,
312 #[serde(default)]
313 pub limit: Option<u32>,
314}
315
316/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
317/// Every service that stores a slug moves its rows to the workspace's
318/// *current* slug (ask identity by `workspace_id`), so that a repeated or
319/// late delivery after a second rename still lands in the right place.
320#[derive(Clone, Debug, Serialize, serde::Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct WorkspaceRenamed {
323 pub workspace_id: String,
324 pub from: String,
325 pub to: String,
326}
327
328impl WorkspaceRenamed {
329 /// The slugs whose rows move to `current`: the two this rename names,
330 /// minus `current` itself. Moving rows keyed by either converges on the
331 /// current slug whatever order renames are delivered in.
332 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
333 let mut slugs: Vec<String> = Vec::new();
334 for slug in [&self.from, &self.to] {
335 if slug != current && !slugs.contains(slug) {
336 slugs.push(slug.clone());
337 }
338 }
339 slugs
340 }
341}
342
343/// `repo.updated`: a repository's description, topics or visibility
344/// changed. `visibility_changed` says whether it went public or private,
345/// which `repo.visibility_changed` also announces on its own.
346#[derive(Debug, Serialize, serde::Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct RepoUpdated {
349 pub repo_id: String,
350 pub namespace: String,
351 pub name: String,
352 pub is_private: bool,
353 #[serde(default)]
354 pub visibility_changed: bool,
355}
356
357/// `repo.visibility_changed`: a repository went public or private.
358#[derive(Debug, Serialize, serde::Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct RepoVisibilityChanged {
361 pub repo_id: String,
362 pub is_private: bool,
363}
364
365/// `repo.renamed`: a repository's name changed within its workspace,
366/// keeping its id and its git store key. Like `repo.transferred`, a path
367/// change: every service that keeps rows under the repository's path moves
368/// them to its *current* path (ask repos `path_by_id`), so a repeated or
369/// late delivery after a second rename or a transfer still lands in the
370/// right place. `g1t_kit::transfer::on_event` handles both.
371#[derive(Clone, Debug, Serialize, serde::Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct RepoRenamed {
374 pub repo_id: String,
375 /// The workspace it is in.
376 pub namespace: String,
377 /// Its old name.
378 pub from: String,
379 /// Its new name.
380 pub to: String,
381}
382
383impl RepoRenamed {
384 /// The paths whose rows move to `current` (`namespace/name`): the two
385 /// this rename names, minus `current`.
386 pub fn stale_paths(&self, current: &str) -> Vec<String> {
387 let mut paths: Vec<String> = Vec::new();
388 for name in [&self.from, &self.to] {
389 let path = format!("{}/{name}", self.namespace);
390 if path != current && !paths.contains(&path) {
391 paths.push(path);
392 }
393 }
394 paths
395 }
396}
397
398/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
399/// git refuses it, but it can be restored until `purge_after`, so services
400/// stop what runs for it (agents, workflows, deployments, indexing,
401/// webhook deliveries) and hide it, and keep what they hold until
402/// `repo.purged`. `repo.restored` brings it back.
403#[derive(Clone, Debug, Serialize, serde::Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct RepoDeleted {
406 pub repo_id: String,
407 #[serde(default)]
408 pub namespace: String,
409 #[serde(default)]
410 pub name: String,
411 #[serde(default)]
412 pub is_private: bool,
413 /// RFC 3339: when it is purged unless restored first.
414 #[serde(default)]
415 pub purge_after: String,
416 /// It went with its workspace (`workspace.deleting`). Services that
417 /// handle the workspace as a whole (deployments pauses its apps rather
418 /// than taking them down) leave this one to that.
419 #[serde(default)]
420 pub with_workspace: bool,
421}
422
423/// `repo.restored`: a deleted repository is back, at its path, as it was.
424/// Services start again what `repo.deleted` stopped: index it, deploy its
425/// production, show it.
426#[derive(Clone, Debug, Serialize, serde::Deserialize)]
427#[serde(rename_all = "camelCase")]
428pub struct RepoRestored {
429 pub repo_id: String,
430 pub namespace: String,
431 pub name: String,
432 pub is_private: bool,
433 /// It came back with its workspace (`workspace.restored`).
434 #[serde(default)]
435 pub with_workspace: bool,
436}
437
438/// `repo.purged`: a deleted repository is gone for good, its git data
439/// with it. Services drop every row they keep for it by `repo_id`, except
440/// history that belongs to its workspace: ledgers, invoices and the audit
441/// log. Its path is free for a new repository.
442#[derive(Clone, Debug, Serialize, serde::Deserialize)]
443#[serde(rename_all = "camelCase")]
444pub struct RepoPurged {
445 pub repo_id: String,
446 pub namespace: String,
447 pub name: String,
448}
449
450/// `repo.archived` and `repo.unarchived`: a repository became read-only,
451/// or writable again. While archived, pushes are refused, issues and pull
452/// requests are locked, and agents and workflows do not run for it; its
453/// deployments keep serving.
454#[derive(Clone, Debug, Serialize, serde::Deserialize)]
455#[serde(rename_all = "camelCase")]
456pub struct RepoArchived {
457 pub repo_id: String,
458 pub namespace: String,
459 pub name: String,
460 pub archived: bool,
461}
462
463/// `repo.default_branch_changed`: the branch everything lands on is now
464/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
465/// requests into it now target `to`) rather than another branch chosen.
466#[derive(Clone, Debug, Serialize, serde::Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct RepoDefaultBranchChanged {
469 pub repo_id: String,
470 pub from: String,
471 pub to: String,
472 #[serde(default)]
473 pub renamed: bool,
474}
475
476/// `branch.renamed`: a branch was renamed. Pull requests from or into
477/// `from` follow it to `to`, and web addresses that name `from` redirect.
478#[derive(Clone, Debug, Serialize, serde::Deserialize)]
479#[serde(rename_all = "camelCase")]
480pub struct BranchRenamed {
481 pub repo_id: String,
482 pub from: String,
483 pub to: String,
484 /// Whether it is the default branch.
485 #[serde(default)]
486 pub default_branch: bool,
487}
488
489/// `repo.transferred`: a repository moved from one workspace to another,
490/// keeping its id and its name. Every service that keeps a repository
491/// under its path (`namespace/name`) or its workspace's slug moves those
492/// rows to the repository's *current* path (ask repos `path_by_id`), so a
493/// repeated or late delivery after a second transfer still lands in the
494/// right place. What was charged or recorded before the transfer stays
495/// with the workspace it happened in.
496#[derive(Clone, Debug, Serialize, serde::Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct RepoTransferred {
499 pub repo_id: String,
500 pub name: String,
501 /// The workspace it left.
502 pub from: String,
503 /// The workspace it went to.
504 pub to: String,
505}
506
507impl RepoTransferred {
508 /// The paths whose rows move to `current` (`namespace/name`): the two
509 /// this transfer names, minus `current`. Moving rows keyed by either
510 /// converges whatever order transfers are delivered in.
511 pub fn stale_paths(&self, current: &str) -> Vec<String> {
512 let mut paths: Vec<String> = Vec::new();
513 for namespace in [&self.from, &self.to] {
514 let path = format!("{namespace}/{}", self.name);
515 if path != current && !paths.contains(&path) {
516 paths.push(path);
517 }
518 }
519 paths
520 }
521}
522
523/// `workspace.deleted`: a workspace is gone. Services drop what they keep
524/// for it alone (its webhooks, integrations, secrets, memory, guardrails,
525/// agent queue) and keep what is history: ledgers, invoices and the audit
526/// log stay under its slug, which is never given to another workspace.
527#[derive(Clone, Debug, Serialize, serde::Deserialize)]
528#[serde(rename_all = "camelCase")]
529pub struct WorkspaceDeleted {
530 pub workspace_id: String,
531 pub slug: String,
532}
533
534/// `workspace.deleting`: an owner deleted a workspace, and it can be
535/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
536/// meantime. Services hide what they keep for it and stop what runs for it,
537/// keeping their rows: repos deletes its repositories softly (each with a
538/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
539/// apps, search drops it from results. `workspace.restored` undoes exactly
540/// that; once `purge_after` passes, `workspace.deleted` follows and
541/// services purge as for any deleted workspace.
542#[derive(Clone, Debug, Serialize, serde::Deserialize)]
543#[serde(rename_all = "camelCase")]
544pub struct WorkspaceDeleting {
545 pub workspace_id: String,
546 pub slug: String,
547 /// The username of the owner who deleted it.
548 pub by: String,
549 /// RFC 3339: when it is purged unless restored first.
550 pub purge_after: String,
551}
552
553/// `workspace.restored`: staff brought a deleted workspace back, with its
554/// members and tokens. Services undo what they did on `workspace.deleting`,
555/// and only that: a repository deleted on its own before stays deleted.
556#[derive(Clone, Debug, Serialize, serde::Deserialize)]
557#[serde(rename_all = "camelCase")]
558pub struct WorkspaceRestored {
559 pub workspace_id: String,
560 pub slug: String,
561}
562
563/// `user.updated`: an account was made, or changed what its profile shows
564/// (name, bio, avatar). Nothing private: ask identity for the profile.
565#[derive(Debug, Serialize, serde::Deserialize)]
566#[serde(rename_all = "camelCase")]
567pub struct UserUpdated {
568 pub username: String,
569}
570
571/// `user.email_added`, `user.email_verified`, `user.email_removed` and
572/// `user.primary_email_changed`: an account's addresses changed. Never the
573/// address itself; ask identity, as the person, for that.
574#[derive(Debug, Serialize, serde::Deserialize)]
575#[serde(rename_all = "camelCase")]
576pub struct UserEmailChanged {
577 pub user_id: String,
578 /// Whether g1t staff made the change.
579 #[serde(default)]
580 pub by_staff: bool,
581}
582
583/// `workspace.updated`: a workspace was made, or its name, description or
584/// icon changed. Ask identity for it by slug.
585#[derive(Debug, Serialize, serde::Deserialize)]
586#[serde(rename_all = "camelCase")]
587pub struct WorkspaceUpdated {
588 pub workspace_id: String,
589 pub slug: String,
590}
591
592/// `invite.created`: someone (or staff) made an invite. Never the code or
593/// the address it is for.
594#[derive(Debug, Serialize, serde::Deserialize)]
595#[serde(rename_all = "camelCase")]
596pub struct InviteCreated {
597 pub invite_id: String,
598 /// The account that made it; null when staff did.
599 pub inviter_id: Option<String>,
600 /// The workspace it joins.
601 pub workspace_id: Option<String>,
602 /// Whether it is bound to one email address.
603 pub bound: bool,
604}
605
606/// `invite.redeemed`: an invite was used, by a new account or by an
607/// existing one joining a workspace.
608#[derive(Debug, Serialize, serde::Deserialize)]
609#[serde(rename_all = "camelCase")]
610pub struct InviteRedeemed {
611 pub invite_id: String,
612 pub user_id: String,
613 pub inviter_id: Option<String>,
614 pub workspace_id: Option<String>,
615 /// Whether it made the account.
616 pub created_account: bool,
617}
618
619/// `waitlist.requested`: someone asked for access. Ask identity's staff
620/// methods for the entry; the address is not in the event.
621#[derive(Debug, Serialize, serde::Deserialize)]
622#[serde(rename_all = "camelCase")]
623pub struct WaitlistRequested {
624 pub entry_id: String,
625}
626
627/// The payload of `package.published`, `package.version_deleted`,
628/// `package.deleted` and `package.visibility_changed`; each uses the fields
629/// that apply to it. `repo_id` is the repository the package is linked to.
630#[derive(Clone, Debug, Default, Serialize)]
631#[serde(rename_all = "camelCase")]
632pub struct PackageEvent {
633 pub package_id: String,
634 pub workspace: String,
635 pub ecosystem: String,
636 pub name: String,
637 pub repo_id: Option<String>,
638 /// The version published or deleted: for a container image, its
639 /// manifest's digest.
640 #[serde(skip_serializing_if = "Option::is_none")]
641 pub version: Option<String>,
642 #[serde(skip_serializing_if = "Option::is_none")]
643 pub digest: Option<String>,
644 #[serde(skip_serializing_if = "Option::is_none")]
645 pub size: Option<u64>,
646 /// On publish: the tags that now point to the version.
647 #[serde(skip_serializing_if = "Option::is_none")]
648 pub tags: Option<Vec<String>>,
649 /// On `package.visibility_changed`: `public` or `private`.
650 #[serde(skip_serializing_if = "Option::is_none")]
651 pub visibility: Option<String>,
652}
653
654/// `queue.changed`: a repository's merge queue gained, lost or settled an
655/// entry, so the next batch may be ready to test.
656#[derive(Debug, Serialize)]
657#[serde(rename_all = "camelCase")]
658pub struct QueueChanged {
659 pub repo_id: String,
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn a_push_says_it_was_unscanned_only_when_it_was() {
668 let push = |unscanned| GitPush {
669 repo_id: "rep_1".into(),
670 git_ref: "refs/heads/import".into(),
671 before: None,
672 after: "abc".into(),
673 default_branch: false,
674 unscanned,
675 };
676 let quiet = serde_json::to_value(push(false)).unwrap();
677 assert!(quiet.get("unscanned").is_none());
678 let flagged = serde_json::to_value(push(true)).unwrap();
679 assert_eq!(flagged["unscanned"], true);
680 assert_eq!(flagged["ref"], "refs/heads/import");
681 }
682
683 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
684 WorkspaceRenamed {
685 workspace_id: "wsp_1".into(),
686 from: from.into(),
687 to: to.into(),
688 }
689 }
690
691 #[test]
692 fn stale_slugs_leave_out_the_current_one() {
693 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
694 // Delivered after a second rename, b → c: both move to c.
695 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
696 // Renamed back: a → b → a.
697 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
698 }
699
700 fn transferred(from: &str, to: &str) -> RepoTransferred {
701 RepoTransferred {
702 repo_id: "rep_1".into(),
703 name: "rocket".into(),
704 from: from.into(),
705 to: to.into(),
706 }
707 }
708
709 #[test]
710 fn stale_paths_leave_out_the_current_one() {
711 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
712 // Delivered after a second transfer, b → c: both move to c.
713 assert_eq!(
714 transferred("a", "b").stale_paths("c/rocket"),
715 vec!["a/rocket", "b/rocket"]
716 );
717 // Transferred back: a → b → a.
718 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
719 }
720
721 #[test]
722 fn a_transfer_reads_as_published() {
723 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
724 let event: RepoTransferred = serde_json::from_value(data).unwrap();
725 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
726 }
727
728 #[test]
729 fn a_rename_names_both_paths_in_its_workspace() {
730 let renamed = RepoRenamed {
731 repo_id: "rep_1".into(),
732 namespace: "acme".into(),
733 from: "old".into(),
734 to: "new".into(),
735 };
736 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
737 // Delivered after a transfer: both names in acme move.
738 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
739 }
740
741 #[test]
742 fn reads_the_published_payload() {
743 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
744 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
745 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
746 }
747}