1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6 "fmt"
7 "time"
8
9 "google.golang.org/protobuf/types/known/timestamppb"
10
11 pb "bnema.dev/ermite-api/gen/ermite"
12 "bnema.dev/ermite/internal/app"
13 "bnema.dev/ermite/internal/domain"
14)
15
16// This file maps domain values to the messages of the contract and back.
17// Messages are DTOs: the domain never leaks into them, nor they into the
18// domain.
19
20func timestamp(t time.Time) *timestamppb.Timestamp {
21 if t.IsZero() {
22 return nil
23 }
24 return timestamppb.New(t)
25}
26
27func goTime(ts *timestamppb.Timestamp) time.Time {
28 if ts == nil {
29 return time.Time{}
30 }
31 return ts.AsTime()
32}
33
34func visibilityToPB(v domain.Visibility) pb.Visibility {
35 switch v {
36 case domain.VisibilityPublic:
37 return pb.Visibility_VISIBILITY_PUBLIC
38 case domain.VisibilityPrivate:
39 return pb.Visibility_VISIBILITY_PRIVATE
40 }
41 return pb.Visibility_VISIBILITY_UNSPECIFIED
42}
43
44// visibilityFromPB maps a visibility of a request. Unspecified is the empty
45// domain.Visibility, which the use cases treat as "default".
46func visibilityFromPB(v pb.Visibility) (domain.Visibility, error) {
47 switch v {
48 case pb.Visibility_VISIBILITY_UNSPECIFIED:
49 return "", nil
50 case pb.Visibility_VISIBILITY_PUBLIC:
51 return domain.VisibilityPublic, nil
52 case pb.Visibility_VISIBILITY_PRIVATE:
53 return domain.VisibilityPrivate, nil
54 }
55 return "", fmt.Errorf("%w: %v", domain.ErrInvalidVisibility, v)
56}
57
58func repoToPB(v app.RepoView) *pb.Repo {
59 settings := v.Settings
60 if settings.IsZero() {
61 settings = domain.DefaultRepoSettings()
62 }
63 return &pb.Repo{
64 Name: v.Name.String(),
65 Description: v.Description,
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{FailingChecksBlock: s.FailingChecksBlock}
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 }
89 return pb.MergeMode_MERGE_MODE_UNSPECIFIED
90}
91
92// mergeModeFromPB maps a mode of a merge request: unspecified is the empty
93// domain.MergeMode, which the use case reads as fast-forward.
94func mergeModeFromPB(m pb.MergeMode) (domain.MergeMode, error) {
95 switch m {
96 case pb.MergeMode_MERGE_MODE_UNSPECIFIED:
97 return "", nil
98 case pb.MergeMode_MERGE_MODE_FAST_FORWARD:
99 return domain.MergeModeFastForward, nil
100 case pb.MergeMode_MERGE_MODE_SQUASH:
101 return domain.MergeModeSquash, nil
102 }
103 return "", fmt.Errorf("%w: merge mode %v", domain.ErrInvalid, m)
104}
105
106// mergeModesFromPB maps the allowed modes of the settings, where unspecified
107// is rejected.
108func mergeModesFromPB(in []pb.MergeMode) ([]domain.MergeMode, error) {
109 out := make([]domain.MergeMode, 0, len(in))
110 for _, m := range in {
111 mode, err := mergeModeFromPB(m)
112 if err != nil {
113 return nil, err
114 }
115 if mode == "" {
116 return nil, fmt.Errorf("%w: MERGE_MODE_UNSPECIFIED is not a merge mode", domain.ErrInvalid)
117 }
118 out = append(out, mode)
119 }
120 return out, nil
121}
122
123var scopeToPB = map[domain.Scope]pb.Scope{
124 domain.ScopeRepoRead: pb.Scope_SCOPE_REPO_READ,
125 domain.ScopeRepoWrite: pb.Scope_SCOPE_REPO_WRITE,
126 domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN,
127 domain.ScopeMRWrite: pb.Scope_SCOPE_MR_WRITE,
128 domain.ScopeMRMerge: pb.Scope_SCOPE_MR_MERGE,
129 domain.ScopeCIWrite: pb.Scope_SCOPE_CI_WRITE,
130 domain.ScopeSecretsWrite: pb.Scope_SCOPE_SECRETS_WRITE,
131 domain.ScopeRunnerRegister: pb.Scope_SCOPE_RUNNER_REGISTER,
132 domain.ScopeTokenAdmin: pb.Scope_SCOPE_TOKEN_ADMIN,
133}
134
135func scopeFromPB(s pb.Scope) (domain.Scope, error) {
136 for d, p := range scopeToPB {
137 if p == s {
138 return d, nil
139 }
140 }
141 return "", fmt.Errorf("%w: %v", domain.ErrInvalidScope, s)
142}
143
144func tokenToPB(t domain.Token) *pb.Token {
145 out := &pb.Token{
146 Id: t.ID,
147 Label: t.Label,
148 ExpiresAt: timestamp(t.ExpiresAt),
149 CreatedAt: timestamp(t.CreatedAt),
150 LastUsedAt: timestamp(t.LastUsedAt),
151 }
152 for _, s := range t.Scopes {
153 out.Scopes = append(out.Scopes, scopeToPB[s])
154 }
155 for _, r := range t.Repos {
156 out.Repos = append(out.Repos, r.String())
157 }
158 return out
159}
160
161func refKindToPB(k domain.RefKind) pb.RefKind {
162 switch k {
163 case domain.RefKindBranch:
164 return pb.RefKind_REF_KIND_BRANCH
165 case domain.RefKindTag:
166 return pb.RefKind_REF_KIND_TAG
167 case domain.RefKindHead:
168 return pb.RefKind_REF_KIND_HEAD
169 }
170 return pb.RefKind_REF_KIND_UNSPECIFIED
171}
172
173func refKindFromPB(k pb.RefKind) (domain.RefKind, error) {
174 switch k {
175 case pb.RefKind_REF_KIND_BRANCH:
176 return domain.RefKindBranch, nil
177 case pb.RefKind_REF_KIND_TAG:
178 return domain.RefKindTag, nil
179 case pb.RefKind_REF_KIND_HEAD:
180 return domain.RefKindHead, nil
181 }
182 return "", fmt.Errorf("%w: %v", domain.ErrInvalidRefKind, k)
183}
184
185func refToPB(r domain.Ref) *pb.Ref {
186 return &pb.Ref{
187 Name: r.Name,
188 ShortName: r.ShortName,
189 Kind: refKindToPB(r.Kind),
190 Id: r.ID.String(),
191 CommitId: r.CommitID.String(),
192 }
193}
194
195func entryKindToPB(k domain.EntryKind) pb.EntryKind {
196 switch k {
197 case domain.EntryFile:
198 return pb.EntryKind_ENTRY_KIND_FILE
199 case domain.EntryExecutable:
200 return pb.EntryKind_ENTRY_KIND_EXECUTABLE
201 case domain.EntryDirectory:
202 return pb.EntryKind_ENTRY_KIND_DIRECTORY
203 case domain.EntrySymlink:
204 return pb.EntryKind_ENTRY_KIND_SYMLINK
205 case domain.EntrySubmodule:
206 return pb.EntryKind_ENTRY_KIND_SUBMODULE
207 }
208 return pb.EntryKind_ENTRY_KIND_UNSPECIFIED
209}
210
211func signatureToPB(s domain.Signature) *pb.Signature {
212 return &pb.Signature{Name: s.Name, Email: s.Email, Time: timestamp(s.Time)}
213}
214
215func commitToPB(c domain.Commit) *pb.Commit {
216 out := &pb.Commit{
217 Id: c.ID.String(),
218 Author: signatureToPB(c.Author),
219 Committer: signatureToPB(c.Committer),
220 Summary: c.Summary,
221 Message: c.Message,
222 }
223 for _, p := range c.Parents {
224 out.ParentIds = append(out.ParentIds, p.String())
225 }
226 return out
227}
228
229func treeEntryToPB(e domain.TreeEntry) *pb.TreeEntry {
230 out := &pb.TreeEntry{
231 Name: e.Name,
232 Path: e.Path,
233 Kind: entryKindToPB(e.Kind),
234 Mode: e.Mode,
235 Id: e.ID.String(),
236 Size: e.Size,
237 }
238 if e.LastCommit != nil {
239 out.LastCommit = commitToPB(*e.LastCommit)
240 }
241 return out
242}
243
244func blobInfoToPB(b domain.BlobInfo) *pb.BlobInfo {
245 return &pb.BlobInfo{
246 Id: b.ID.String(),
247 Path: b.Path,
248 Size: b.Size,
249 Binary: b.Binary,
250 MediaType: b.MediaType,
251 Mode: b.Mode,
252 }
253}
254
255func fileStatusToPB(s domain.FileStatus) pb.FileStatus {
256 switch s {
257 case domain.FileAdded:
258 return pb.FileStatus_FILE_STATUS_ADDED
259 case domain.FileDeleted:
260 return pb.FileStatus_FILE_STATUS_DELETED
261 case domain.FileModified:
262 return pb.FileStatus_FILE_STATUS_MODIFIED
263 case domain.FileRenamed:
264 return pb.FileStatus_FILE_STATUS_RENAMED
265 case domain.FileCopied:
266 return pb.FileStatus_FILE_STATUS_COPIED
267 case domain.FileTypeChanged:
268 return pb.FileStatus_FILE_STATUS_TYPE_CHANGED
269 }
270 return pb.FileStatus_FILE_STATUS_UNSPECIFIED
271}
272
273func lineKindToPB(k domain.LineKind) pb.LineKind {
274 switch k {
275 case domain.LineContext:
276 return pb.LineKind_LINE_KIND_CONTEXT
277 case domain.LineAddition:
278 return pb.LineKind_LINE_KIND_ADDITION
279 case domain.LineDeletion:
280 return pb.LineKind_LINE_KIND_DELETION
281 }
282 return pb.LineKind_LINE_KIND_UNSPECIFIED
283}
284
285func hunkToPB(h domain.Hunk) *pb.Hunk {
286 out := &pb.Hunk{
287 OldStart: int32(h.OldStart), OldLines: int32(h.OldLines),
288 NewStart: int32(h.NewStart), NewLines: int32(h.NewLines),
289 Header: h.Header,
290 Lines: make([]*pb.Line, 0, len(h.Lines)),
291 }
292 for _, l := range h.Lines {
293 out.Lines = append(out.Lines, &pb.Line{
294 Kind: lineKindToPB(l.Kind),
295 OldNumber: int32(l.OldNumber),
296 NewNumber: int32(l.NewNumber),
297 Text: l.Text,
298 NoNewlineAtEof: l.NoNewlineAtEOF,
299 })
300 }
301 return out
302}
303
304func fileDiffToPB(f domain.FileDiff) *pb.FileDiff {
305 out := &pb.FileDiff{
306 OldPath: f.OldPath, NewPath: f.NewPath,
307 Status: fileStatusToPB(f.Status),
308 OldMode: f.OldMode, NewMode: f.NewMode,
309 OldId: f.OldID.String(), NewId: f.NewID.String(),
310 Binary: f.Binary,
311 Additions: int32(f.Additions),
312 Deletions: int32(f.Deletions),
313 Similarity: int32(f.Similarity),
314 Truncated: f.Truncated,
315 }
316 for _, h := range f.Hunks {
317 out.Hunks = append(out.Hunks, hunkToPB(h))
318 }
319 return out
320}
321
322func diffStatsToPB(s domain.DiffStats) *pb.DiffStats {
323 return &pb.DiffStats{
324 FilesChanged: int32(s.FilesChanged),
325 Additions: int32(s.Additions),
326 Deletions: int32(s.Deletions),
327 }
328}
329
330func diffToPB(d domain.Diff) *pb.Diff {
331 out := &pb.Diff{Truncated: d.Truncated}
332 for _, f := range d.Files {
333 out.Files = append(out.Files, fileDiffToPB(f))
334 }
335 return out
336}
337
338func commitDetailToPB(d domain.CommitDetail) *pb.GetCommitResponse {
339 return &pb.GetCommitResponse{
340 Commit: commitToPB(d.Commit),
341 Stats: diffStatsToPB(d.Stats),
342 Diff: diffToPB(d.Diff),
343 }
344}
345
346// oidToPB turns the zero object id of a ref update (no object) into "".
347func oidToPB(oid string) string {
348 if oid == domain.ZeroOID {
349 return ""
350 }
351 return oid
352}
353
354func refUpdatesToPB(updates []domain.RefUpdate) []*pb.RefUpdate {
355 out := make([]*pb.RefUpdate, 0, len(updates))
356 for _, u := range updates {
357 out = append(out, &pb.RefUpdate{Ref: u.Ref, OldId: oidToPB(u.Old), NewId: oidToPB(u.New)})
358 }
359 return out
360}
361
362// watchEventToPB maps one item of a watch; nil when the event has no
363// representation in the contract.
364func watchEventToPB(we app.WatchEvent) *pb.Event {
365 if we.Gap {
366 return &pb.Event{Kind: &pb.Event_Gap{Gap: &pb.Gap{}}}
367 }
368 out := &pb.Event{Id: we.ID}
369 switch ev := we.Event.(type) {
370 case domain.RepoCreated:
371 out.Time = timestamp(ev.At)
372 out.Kind = &pb.Event_RepoCreated{RepoCreated: &pb.RepoCreated{Repo: repoOrName(we, ev.Repo, ev.Visibility)}}
373 case domain.RepoUpdated:
374 out.Time = timestamp(ev.At)
375 out.Kind = &pb.Event_RepoUpdated{RepoUpdated: &pb.RepoUpdated{Repo: repoOrName(we, ev.Repo, "")}}
376 case domain.RepoDeleted:
377 out.Time = timestamp(ev.At)
378 out.Kind = &pb.Event_RepoDeleted{RepoDeleted: &pb.RepoDeleted{Repo: ev.Repo.String()}}
379 case domain.PushEvent:
380 out.Time = timestamp(ev.At)
381 out.Kind = &pb.Event_Pushed{Pushed: &pb.Pushed{Repo: ev.Repo.String(), RefUpdates: refUpdatesToPB(ev.Updates)}}
382 case domain.MergeRequestOpened:
383 out.Time = timestamp(ev.At)
384 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
385 case domain.MergeRequestMerged:
386 out.Time = timestamp(ev.At)
387 out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
388 case domain.MergeRequestClosed:
389 out.Time = timestamp(ev.At)
390 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
391 case domain.CommentAdded:
392 out.Time = timestamp(ev.At)
393 out.Kind = &pb.Event_CommentAdded{CommentAdded: &pb.CommentAdded{Repo: ev.Repo.String(), Number: ev.Number, Comment: commentToPB(ev.Comment)}}
394 default:
395 return nil
396 }
397 return out
398}
399
400func repoOrName(we app.WatchEvent, name domain.RepoName, vis domain.Visibility) *pb.Repo {
401 if we.Repo != nil {
402 return repoToPB(*we.Repo)
403 }
404 return &pb.Repo{Name: name.String(), Visibility: visibilityToPB(vis)}
405}