1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6 "encoding/base64"
7 "fmt"
8 "slices"
9 "time"
10
11 "google.golang.org/protobuf/types/known/timestamppb"
12
13 pb "bnema.dev/ermite-api/gen/ermite"
14 "bnema.dev/ermite/internal/domain"
15 "bnema.dev/ermite/internal/ports"
16)
17
18// This file maps domain values to the messages of the contract and back.
19// Messages are DTOs: the domain never leaks into them, nor they into the
20// domain.
21
22func timestamp(t time.Time) *timestamppb.Timestamp {
23 if t.IsZero() {
24 return nil
25 }
26 return timestamppb.New(t)
27}
28
29func goTime(ts *timestamppb.Timestamp) time.Time {
30 if ts == nil {
31 return time.Time{}
32 }
33 return ts.AsTime()
34}
35
36func visibilityToPB(v domain.Visibility) pb.Visibility {
37 switch v {
38 case domain.VisibilityPublic:
39 return pb.Visibility_VISIBILITY_PUBLIC
40 case domain.VisibilityPrivate:
41 return pb.Visibility_VISIBILITY_PRIVATE
42 }
43 return pb.Visibility_VISIBILITY_UNSPECIFIED
44}
45
46// visibilityFromPB maps a visibility of a request. Unspecified is the empty
47// domain.Visibility, which the use cases treat as "default".
48func visibilityFromPB(v pb.Visibility) (domain.Visibility, error) {
49 switch v {
50 case pb.Visibility_VISIBILITY_UNSPECIFIED:
51 return "", nil
52 case pb.Visibility_VISIBILITY_PUBLIC:
53 return domain.VisibilityPublic, nil
54 case pb.Visibility_VISIBILITY_PRIVATE:
55 return domain.VisibilityPrivate, nil
56 }
57 return "", fmt.Errorf("%w: %v", domain.ErrInvalidVisibility, v)
58}
59
60func repoToPB(v ports.RepoView) *pb.Repo {
61 settings := v.EffectiveSettings()
62 return &pb.Repo{
63 Name: v.Name.String(),
64 Description: v.Description,
65 Topics: v.Topics,
66 Visibility: visibilityToPB(v.Visibility),
67 DefaultBranch: v.DefaultBranch,
68 CreatedAt: timestamp(v.CreatedAt),
69 PushedAt: timestamp(v.PushedAt),
70 Settings: settingsToPB(settings),
71 }
72}
73
74func settingsToPB(s domain.RepoSettings) *pb.RepoSettings {
75 out := &pb.RepoSettings{AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)}
76 for _, m := range s.AllowedMergeModes {
77 out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m))
78 }
79 return out
80}
81
82func mergeModeToPB(m domain.MergeMode) pb.MergeMode {
83 switch m {
84 case domain.MergeModeFastForward:
85 return pb.MergeMode_MERGE_MODE_FAST_FORWARD
86 case domain.MergeModeSquash:
87 return pb.MergeMode_MERGE_MODE_SQUASH
88 case domain.MergeModeMerge:
89 return pb.MergeMode_MERGE_MODE_MERGE_COMMIT
90 }
91 return pb.MergeMode_MERGE_MODE_UNSPECIFIED
92}
93
94// mergeModeFromPB maps a mode of a merge request: unspecified is the empty
95// domain.MergeMode, which the use case reads as fast-forward.
96func mergeModeFromPB(m pb.MergeMode) (domain.MergeMode, error) {
97 switch m {
98 case pb.MergeMode_MERGE_MODE_UNSPECIFIED:
99 return "", nil
100 case pb.MergeMode_MERGE_MODE_FAST_FORWARD:
101 return domain.MergeModeFastForward, nil
102 case pb.MergeMode_MERGE_MODE_SQUASH:
103 return domain.MergeModeSquash, nil
104 case pb.MergeMode_MERGE_MODE_MERGE_COMMIT:
105 return domain.MergeModeMerge, nil
106 }
107 return "", fmt.Errorf("%w: merge mode %v", domain.ErrInvalid, m)
108}
109
110// mergeModesFromPB maps the allowed modes of the settings, where unspecified
111// is rejected.
112func mergeModesFromPB(in []pb.MergeMode) ([]domain.MergeMode, error) {
113 out := make([]domain.MergeMode, 0, len(in))
114 for _, m := range in {
115 mode, err := mergeModeFromPB(m)
116 if err != nil {
117 return nil, err
118 }
119 if mode == "" {
120 return nil, fmt.Errorf("%w: MERGE_MODE_UNSPECIFIED is not a merge mode", domain.ErrInvalid)
121 }
122 out = append(out, mode)
123 }
124 return out, nil
125}
126
127var scopeToPB = map[domain.Scope]pb.Scope{
128 domain.ScopeMRReview: pb.Scope_SCOPE_MR_REVIEW,
129 domain.ScopeRepoRead: pb.Scope_SCOPE_REPO_READ,
130 domain.ScopeRepoWrite: pb.Scope_SCOPE_REPO_WRITE,
131 domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN,
132 domain.ScopeMRWrite: pb.Scope_SCOPE_MR_WRITE,
133 domain.ScopeMRMerge: pb.Scope_SCOPE_MR_MERGE,
134 domain.ScopeMRForceMerge: pb.Scope_SCOPE_MR_FORCE_MERGE,
135 domain.ScopeCIWrite: pb.Scope_SCOPE_CI_WRITE,
136 domain.ScopeSecretsWrite: pb.Scope_SCOPE_SECRETS_WRITE,
137 domain.ScopeRunnerRegister: pb.Scope_SCOPE_RUNNER_REGISTER,
138 domain.ScopeTokenAdmin: pb.Scope_SCOPE_TOKEN_ADMIN,
139 domain.ScopeIssueWrite: pb.Scope_SCOPE_ISSUE_WRITE,
140 domain.ScopeReleaseWrite: pb.Scope_SCOPE_RELEASE_WRITE,
141}
142
143func scopeFromPB(s pb.Scope) (domain.Scope, error) {
144 for d, p := range scopeToPB {
145 if p == s {
146 return d, nil
147 }
148 }
149 return "", fmt.Errorf("%w: %v", domain.ErrInvalidScope, s)
150}
151
152func tokenToPB(t domain.Token) *pb.Token {
153 out := &pb.Token{
154 Id: t.ID,
155 Label: t.Label,
156 ExpiresAt: timestamp(t.ExpiresAt),
157 CreatedAt: timestamp(t.CreatedAt),
158 LastUsedAt: timestamp(t.LastUsedAt),
159 }
160 for _, s := range t.Scopes {
161 out.Scopes = append(out.Scopes, scopeToPB[s])
162 }
163 for _, r := range t.Repos {
164 out.Repos = append(out.Repos, r.String())
165 }
166 out.BranchPrefixes = slices.Clone(t.BranchPrefixes)
167 return out
168}
169
170func refKindToPB(k domain.RefKind) pb.RefKind {
171 switch k {
172 case domain.RefKindBranch:
173 return pb.RefKind_REF_KIND_BRANCH
174 case domain.RefKindTag:
175 return pb.RefKind_REF_KIND_TAG
176 case domain.RefKindHead:
177 return pb.RefKind_REF_KIND_HEAD
178 }
179 return pb.RefKind_REF_KIND_UNSPECIFIED
180}
181
182func refKindFromPB(k pb.RefKind) (domain.RefKind, error) {
183 switch k {
184 case pb.RefKind_REF_KIND_BRANCH:
185 return domain.RefKindBranch, nil
186 case pb.RefKind_REF_KIND_TAG:
187 return domain.RefKindTag, nil
188 case pb.RefKind_REF_KIND_HEAD:
189 return domain.RefKindHead, nil
190 }
191 return "", fmt.Errorf("%w: %v", domain.ErrInvalidRefKind, k)
192}
193
194func refToPB(r domain.Ref) *pb.Ref {
195 return &pb.Ref{
196 Name: r.Name,
197 ShortName: r.ShortName,
198 Kind: refKindToPB(r.Kind),
199 Id: r.ID.String(),
200 CommitId: r.CommitID.String(),
201 }
202}
203
204func entryKindToPB(k domain.EntryKind) pb.EntryKind {
205 switch k {
206 case domain.EntryFile:
207 return pb.EntryKind_ENTRY_KIND_FILE
208 case domain.EntryExecutable:
209 return pb.EntryKind_ENTRY_KIND_EXECUTABLE
210 case domain.EntryDirectory:
211 return pb.EntryKind_ENTRY_KIND_DIRECTORY
212 case domain.EntrySymlink:
213 return pb.EntryKind_ENTRY_KIND_SYMLINK
214 case domain.EntrySubmodule:
215 return pb.EntryKind_ENTRY_KIND_SUBMODULE
216 }
217 return pb.EntryKind_ENTRY_KIND_UNSPECIFIED
218}
219
220func signatureToPB(s domain.Signature) *pb.Signature {
221 return &pb.Signature{Name: s.Name, Email: s.Email, Time: timestamp(s.Time)}
222}
223
224func commitToPB(c domain.Commit) *pb.Commit {
225 out := &pb.Commit{
226 Id: c.ID.String(),
227 Author: signatureToPB(c.Author),
228 Committer: signatureToPB(c.Committer),
229 Summary: c.Summary,
230 Message: c.Message,
231 }
232 for _, p := range c.Parents {
233 out.ParentIds = append(out.ParentIds, p.String())
234 }
235 if c.Verification.Status != domain.StatusUnchecked {
236 out.Verification = verificationToPB(c.Verification)
237 }
238 return out
239}
240
241func verificationToPB(v domain.CommitVerification) *pb.CommitVerification {
242 return &pb.CommitVerification{
243 Format: signatureFormatToPB(v.Format),
244 Status: signatureStatusToPB(v.Status),
245 Fingerprint: v.Fingerprint,
246 KeyId: v.KeyID,
247 KeyTitle: v.KeyTitle,
248 }
249}
250
251func signatureFormatToPB(f domain.SignatureFormat) pb.SignatureFormat {
252 switch f {
253 case domain.FormatSSH:
254 return pb.SignatureFormat_SIGNATURE_FORMAT_SSH
255 case domain.FormatGPG:
256 return pb.SignatureFormat_SIGNATURE_FORMAT_GPG
257 case domain.FormatX509:
258 return pb.SignatureFormat_SIGNATURE_FORMAT_X509
259 case domain.FormatNone:
260 return pb.SignatureFormat_SIGNATURE_FORMAT_UNSPECIFIED
261 }
262 return pb.SignatureFormat_SIGNATURE_FORMAT_UNSPECIFIED
263}
264
265// signatureFormatFromPB maps the format of a key to add: only SSH and GPG keys
266// can be trusted.
267func signatureFormatFromPB(f pb.SignatureFormat) (domain.SignatureFormat, error) {
268 switch f {
269 case pb.SignatureFormat_SIGNATURE_FORMAT_SSH:
270 return domain.FormatSSH, nil
271 case pb.SignatureFormat_SIGNATURE_FORMAT_GPG:
272 return domain.FormatGPG, nil
273 case pb.SignatureFormat_SIGNATURE_FORMAT_UNSPECIFIED, pb.SignatureFormat_SIGNATURE_FORMAT_X509:
274 }
275 return domain.FormatNone, fmt.Errorf("%w: key format must be ssh or gpg", domain.ErrInvalid)
276}
277
278func signatureStatusToPB(s domain.SignatureStatus) pb.SignatureStatus {
279 switch s {
280 case domain.StatusUnsigned:
281 return pb.SignatureStatus_SIGNATURE_STATUS_UNSIGNED
282 case domain.StatusVerified:
283 return pb.SignatureStatus_SIGNATURE_STATUS_VERIFIED
284 case domain.StatusUnknownKey:
285 return pb.SignatureStatus_SIGNATURE_STATUS_UNKNOWN_KEY
286 case domain.StatusBadSignature:
287 return pb.SignatureStatus_SIGNATURE_STATUS_BAD_SIGNATURE
288 case domain.StatusEmailMismatch:
289 return pb.SignatureStatus_SIGNATURE_STATUS_EMAIL_MISMATCH
290 case domain.StatusExpired:
291 return pb.SignatureStatus_SIGNATURE_STATUS_EXPIRED
292 case domain.StatusRevoked:
293 return pb.SignatureStatus_SIGNATURE_STATUS_REVOKED
294 case domain.StatusUnsupported:
295 return pb.SignatureStatus_SIGNATURE_STATUS_UNSUPPORTED
296 case domain.StatusUnchecked:
297 return pb.SignatureStatus_SIGNATURE_STATUS_UNSPECIFIED
298 }
299 return pb.SignatureStatus_SIGNATURE_STATUS_UNSPECIFIED
300}
301
302func trustedKeyToPB(k domain.TrustedKey) *pb.TrustedKey {
303 return &pb.TrustedKey{
304 Id: k.ID,
305 Format: signatureFormatToPB(k.Format),
306 Title: k.Title,
307 PublicKey: k.PublicKey,
308 Emails: slices.Clone(k.Emails),
309 CreatedAt: timestamp(k.CreatedAt),
310 Builtin: k.Builtin,
311 }
312}
313
314func treeEntryToPB(e domain.TreeEntry) *pb.TreeEntry {
315 out := &pb.TreeEntry{
316 Name: e.Name,
317 Path: e.Path,
318 Kind: entryKindToPB(e.Kind),
319 Mode: e.Mode,
320 Id: e.ID.String(),
321 Size: e.Size,
322 }
323 if e.LastCommit != nil {
324 out.LastCommit = commitToPB(*e.LastCommit)
325 }
326 return out
327}
328
329func blobInfoToPB(b domain.BlobInfo) *pb.BlobInfo {
330 return &pb.BlobInfo{
331 Id: b.ID.String(),
332 Path: b.Path,
333 Size: b.Size,
334 Binary: b.Binary,
335 MediaType: b.MediaType,
336 Mode: b.Mode,
337 }
338}
339
340func fileStatusToPB(s domain.FileStatus) pb.FileStatus {
341 switch s {
342 case domain.FileAdded:
343 return pb.FileStatus_FILE_STATUS_ADDED
344 case domain.FileDeleted:
345 return pb.FileStatus_FILE_STATUS_DELETED
346 case domain.FileModified:
347 return pb.FileStatus_FILE_STATUS_MODIFIED
348 case domain.FileRenamed:
349 return pb.FileStatus_FILE_STATUS_RENAMED
350 case domain.FileCopied:
351 return pb.FileStatus_FILE_STATUS_COPIED
352 case domain.FileTypeChanged:
353 return pb.FileStatus_FILE_STATUS_TYPE_CHANGED
354 }
355 return pb.FileStatus_FILE_STATUS_UNSPECIFIED
356}
357
358func lineKindToPB(k domain.LineKind) pb.LineKind {
359 switch k {
360 case domain.LineContext:
361 return pb.LineKind_LINE_KIND_CONTEXT
362 case domain.LineAddition:
363 return pb.LineKind_LINE_KIND_ADDITION
364 case domain.LineDeletion:
365 return pb.LineKind_LINE_KIND_DELETION
366 }
367 return pb.LineKind_LINE_KIND_UNSPECIFIED
368}
369
370func hunkToPB(h domain.Hunk) *pb.Hunk {
371 out := &pb.Hunk{
372 OldStart: int32(h.OldStart), OldLines: int32(h.OldLines),
373 NewStart: int32(h.NewStart), NewLines: int32(h.NewLines),
374 Header: h.Header,
375 Lines: make([]*pb.Line, 0, len(h.Lines)),
376 }
377 for _, l := range h.Lines {
378 out.Lines = append(out.Lines, &pb.Line{
379 Kind: lineKindToPB(l.Kind),
380 OldNumber: int32(l.OldNumber),
381 NewNumber: int32(l.NewNumber),
382 Text: l.Text,
383 NoNewlineAtEof: l.NoNewlineAtEOF,
384 })
385 }
386 return out
387}
388
389func fileDiffToPB(f domain.FileDiff) *pb.FileDiff {
390 out := &pb.FileDiff{
391 OldPath: f.OldPath, NewPath: f.NewPath,
392 Status: fileStatusToPB(f.Status),
393 OldMode: f.OldMode, NewMode: f.NewMode,
394 OldId: f.OldID.String(), NewId: f.NewID.String(),
395 Binary: f.Binary,
396 Additions: int32(f.Additions),
397 Deletions: int32(f.Deletions),
398 Similarity: int32(f.Similarity),
399 Truncated: f.Omitted != domain.OmitNone,
400 Omitted: fileOmissionToPB(f.Omitted),
401 }
402 for _, h := range f.Hunks {
403 out.Hunks = append(out.Hunks, hunkToPB(h))
404 }
405 return out
406}
407
408func fileOmissionToPB(o domain.FileOmission) pb.FileOmission {
409 switch o {
410 case domain.OmitGenerated:
411 return pb.FileOmission_FILE_OMISSION_GENERATED
412 case domain.OmitLarge:
413 return pb.FileOmission_FILE_OMISSION_LARGE
414 case domain.OmitBudget:
415 return pb.FileOmission_FILE_OMISSION_BUDGET
416 case domain.OmitUnavailable:
417 return pb.FileOmission_FILE_OMISSION_UNAVAILABLE
418 }
419 return pb.FileOmission_FILE_OMISSION_UNSPECIFIED
420}
421
422func diffStatsToPB(s domain.DiffStats) *pb.DiffStats {
423 return &pb.DiffStats{
424 FilesChanged: int32(s.FilesChanged),
425 Additions: int32(s.Additions),
426 Deletions: int32(s.Deletions),
427 }
428}
429
430func diffToPB(d domain.Diff) *pb.Diff {
431 out := &pb.Diff{Truncated: d.Truncated}
432 for _, f := range d.Files {
433 out.Truncated = out.Truncated || f.Omitted != domain.OmitNone
434 out.Files = append(out.Files, fileDiffToPB(f))
435 }
436 return out
437}
438
439func commitDetailToPB(d domain.CommitDetail) *pb.GetCommitResponse {
440 return &pb.GetCommitResponse{
441 Commit: commitToPB(d.Commit),
442 Stats: diffStatsToPB(d.Stats),
443 Diff: diffToPB(d.Diff),
444 }
445}
446
447// oidToPB turns the zero object id of a ref update (no object) into "".
448func oidToPB(oid string) string {
449 if oid == domain.ZeroOID {
450 return ""
451 }
452 return oid
453}
454
455func refUpdatesToPB(updates []domain.RefUpdate) []*pb.RefUpdate {
456 out := make([]*pb.RefUpdate, 0, len(updates))
457 for _, u := range updates {
458 out = append(out, &pb.RefUpdate{Ref: u.Ref, OldId: oidToPB(u.Old), NewId: oidToPB(u.New)})
459 }
460 return out
461}
462
463// watchEventToPB maps one item of a watch; nil when the event has no
464// representation in the contract.
465func watchEventToPB(we ports.WatchEvent) *pb.Event {
466 if we.Gap {
467 return &pb.Event{Kind: &pb.Event_Gap{Gap: &pb.Gap{}}}
468 }
469 out := &pb.Event{Id: we.ID}
470 switch ev := we.Event.(type) {
471 case domain.RepoCreated:
472 out.Time = timestamp(ev.At)
473 out.Kind = &pb.Event_RepoCreated{RepoCreated: &pb.RepoCreated{Repo: repoOrName(we, ev.Repo, ev.Visibility)}}
474 case domain.RepoUpdated:
475 out.Time = timestamp(ev.At)
476 out.Kind = &pb.Event_RepoUpdated{RepoUpdated: &pb.RepoUpdated{Repo: repoOrName(we, ev.Repo, "")}}
477 case domain.RepoDeleted:
478 out.Time = timestamp(ev.At)
479 out.Kind = &pb.Event_RepoDeleted{RepoDeleted: &pb.RepoDeleted{Repo: ev.Repo.String()}}
480 case domain.PushEvent:
481 out.Time = timestamp(ev.At)
482 out.Kind = &pb.Event_Pushed{Pushed: &pb.Pushed{Repo: ev.Repo.String(), RefUpdates: refUpdatesToPB(ev.Updates)}}
483 case domain.MergeRequestOpened:
484 out.Time = timestamp(ev.At)
485 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
486 case domain.MergeRequestMerged:
487 out.Time = timestamp(ev.At)
488 out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest), Auto: ev.Auto}}
489 case domain.MergeRequestAutoMergeEnabled:
490 out.Time = timestamp(ev.At)
491 out.Kind = &pb.Event_MergeRequestAutoMergeEnabled{MergeRequestAutoMergeEnabled: &pb.MergeRequestAutoMergeEnabled{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
492 case domain.MergeRequestAutoMergeCancelled:
493 out.Time = timestamp(ev.At)
494 out.Kind = &pb.Event_MergeRequestAutoMergeCancelled{MergeRequestAutoMergeCancelled: &pb.MergeRequestAutoMergeCancelled{MergeRequest: mergeRequestToPB(ev.MergeRequest), Reason: ev.Reason}}
495 case domain.MergeRequestClosed:
496 out.Time = timestamp(ev.At)
497 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
498 case domain.CommentAdded:
499 out.Time = timestamp(ev.At)
500 out.Kind = &pb.Event_CommentAdded{CommentAdded: &pb.CommentAdded{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
501 case domain.IssueOpened:
502 out.Time = timestamp(ev.At)
503 out.Kind = &pb.Event_IssueOpened{IssueOpened: &pb.IssueOpened{Issue: issueToPB(ev.Issue)}}
504 case domain.IssueClosed:
505 out.Time = timestamp(ev.At)
506 out.Kind = &pb.Event_IssueClosed{IssueClosed: &pb.IssueClosed{Issue: issueToPB(ev.Issue)}}
507 case domain.IssueReopened:
508 out.Time = timestamp(ev.At)
509 out.Kind = &pb.Event_IssueReopened{IssueReopened: &pb.IssueReopened{Issue: issueToPB(ev.Issue)}}
510 case domain.IssueCommented:
511 out.Time = timestamp(ev.At)
512 out.Kind = &pb.Event_IssueCommented{IssueCommented: &pb.IssueCommented{Repo: ev.Repo.String(), Number: ev.Number, Comment: issueCommentToPB(ev.Comment)}}
513 case domain.IssueSubmitted:
514 out.Time = timestamp(ev.At)
515 out.Kind = &pb.Event_IssueSubmitted{IssueSubmitted: &pb.IssueSubmitted{Repo: ev.Repo.String(), Id: ev.ID.String()}}
516 case domain.CommentEdited:
517 out.Time = timestamp(ev.At)
518 out.Kind = &pb.Event_CommentEdited{CommentEdited: &pb.CommentEdited{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
519 case domain.CommentDeleted:
520 out.Time = timestamp(ev.At)
521 out.Kind = &pb.Event_CommentDeleted{CommentDeleted: &pb.CommentDeleted{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
522 case domain.CommentResolved:
523 out.Time = timestamp(ev.At)
524 out.Kind = &pb.Event_CommentResolved{CommentResolved: &pb.CommentResolved{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
525 case domain.MergeRequestMergeability:
526 out.Time = timestamp(ev.At)
527 out.Kind = &pb.Event_MergeRequestMergeability{MergeRequestMergeability: &pb.MergeRequestMergeability{Repo: ev.Repo.String(), Number: ev.Number, Mergeability: mergeabilityToPB(ev.Mergeability)}}
528 case domain.RunUpdated:
529 out.Time = timestamp(ev.At)
530 out.Kind = runEventToPB(ev)
531 case domain.JobUpdated:
532 out.Time = timestamp(ev.At)
533 out.Kind = jobEventToPB(ev)
534 case domain.MirrorFailed:
535 out.Time = timestamp(ev.At)
536 out.Kind = &pb.Event_MirrorFailed{MirrorFailed: &pb.MirrorFailed{Repo: ev.Repo.String(), State: mirrorStateToPB(ev.State), FailureClass: string(ev.Class)}}
537 case domain.MirrorRecovered:
538 out.Time = timestamp(ev.At)
539 out.Kind = &pb.Event_MirrorRecovered{MirrorRecovered: &pb.MirrorRecovered{Repo: ev.Repo.String()}}
540 default:
541 // Release lifecycle conversions live alongside the event service.
542 // Web-only events have no representation in the API.
543 return releaseEventToPB(we)
544 }
545 return out
546}
547
548func repoOrName(we ports.WatchEvent, name domain.RepoName, vis domain.Visibility) *pb.Repo {
549 if we.Repo != nil {
550 return repoToPB(*we.Repo)
551 }
552 return &pb.Repo{Name: name.String(), Visibility: visibilityToPB(vis)}
553}
554
555// passkeysToPB converts passkeys; the credential id is base64url without
556// padding, the form RemovePasskey takes back. Key material never leaves.
557func passkeysToPB(list []domain.Passkey) []*pb.Passkey {
558 out := make([]*pb.Passkey, 0, len(list))
559 for _, p := range list {
560 out = append(out, &pb.Passkey{
561 Id: base64.RawURLEncoding.EncodeToString(p.ID),
562 Name: p.Name,
563 CreatedAt: timestamp(p.CreatedAt),
564 LastUsedAt: timestamp(p.LastUsedAt),
565 Synced: p.BackupState,
566 })
567 }
568 return out
569}
570
571func releaseToPB(r domain.Release) *pb.Release {
572 return &pb.Release{
573 Id: r.ID, Repo: r.Repo.String(), Tag: r.Tag, Title: r.Title, Notes: r.Notes,
574 Draft: r.Draft, Prerelease: r.Prerelease, TargetCommitish: r.TargetCommitish, Author: r.Author,
575 CreatedAt: timestamp(r.CreatedAt), PublishedAt: timestamp(r.PublishedAt), UpdatedAt: timestamp(r.UpdatedAt),
576 }
577}
578
579func assetToPB(a domain.ReleaseAsset) *pb.ReleaseAsset {
580 return &pb.ReleaseAsset{
581 Id: a.ID, ReleaseId: a.ReleaseID, Name: a.Name, Label: a.Label, ContentType: a.ContentType,
582 Size: a.Size, Digest: a.Digest, Uploader: a.Uploader,
583 CreatedAt: timestamp(a.CreatedAt), UpdatedAt: timestamp(a.UpdatedAt),
584 }
585}
586
587func assetsToPB(assets []domain.ReleaseAsset) []*pb.ReleaseAsset {
588 out := make([]*pb.ReleaseAsset, 0, len(assets))
589 for _, a := range assets {
590 out = append(out, assetToPB(a))
591 }
592 return out
593}