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 return &pb.Repo{
60 Name: v.Name.String(),
61 Description: v.Description,
62 Visibility: visibilityToPB(v.Visibility),
63 DefaultBranch: v.DefaultBranch,
64 CreatedAt: timestamp(v.CreatedAt),
65 PushedAt: timestamp(v.PushedAt),
66 }
67}
68
69var scopeToPB = map[domain.Scope]pb.Scope{
70 domain.ScopeRepoRead: pb.Scope_SCOPE_REPO_READ,
71 domain.ScopeRepoWrite: pb.Scope_SCOPE_REPO_WRITE,
72 domain.ScopeRepoAdmin: pb.Scope_SCOPE_REPO_ADMIN,
73 domain.ScopeMRWrite: pb.Scope_SCOPE_MR_WRITE,
74 domain.ScopeMRMerge: pb.Scope_SCOPE_MR_MERGE,
75 domain.ScopeCIWrite: pb.Scope_SCOPE_CI_WRITE,
76 domain.ScopeSecretsWrite: pb.Scope_SCOPE_SECRETS_WRITE,
77 domain.ScopeRunnerRegister: pb.Scope_SCOPE_RUNNER_REGISTER,
78 domain.ScopeTokenAdmin: pb.Scope_SCOPE_TOKEN_ADMIN,
79}
80
81func scopeFromPB(s pb.Scope) (domain.Scope, error) {
82 for d, p := range scopeToPB {
83 if p == s {
84 return d, nil
85 }
86 }
87 return "", fmt.Errorf("%w: %v", domain.ErrInvalidScope, s)
88}
89
90func tokenToPB(t domain.Token) *pb.Token {
91 out := &pb.Token{
92 Id: t.ID,
93 Label: t.Label,
94 ExpiresAt: timestamp(t.ExpiresAt),
95 CreatedAt: timestamp(t.CreatedAt),
96 LastUsedAt: timestamp(t.LastUsedAt),
97 }
98 for _, s := range t.Scopes {
99 out.Scopes = append(out.Scopes, scopeToPB[s])
100 }
101 for _, r := range t.Repos {
102 out.Repos = append(out.Repos, r.String())
103 }
104 return out
105}
106
107func refKindToPB(k domain.RefKind) pb.RefKind {
108 switch k {
109 case domain.RefKindBranch:
110 return pb.RefKind_REF_KIND_BRANCH
111 case domain.RefKindTag:
112 return pb.RefKind_REF_KIND_TAG
113 case domain.RefKindHead:
114 return pb.RefKind_REF_KIND_HEAD
115 }
116 return pb.RefKind_REF_KIND_UNSPECIFIED
117}
118
119func refKindFromPB(k pb.RefKind) (domain.RefKind, error) {
120 switch k {
121 case pb.RefKind_REF_KIND_BRANCH:
122 return domain.RefKindBranch, nil
123 case pb.RefKind_REF_KIND_TAG:
124 return domain.RefKindTag, nil
125 case pb.RefKind_REF_KIND_HEAD:
126 return domain.RefKindHead, nil
127 }
128 return "", fmt.Errorf("%w: %v", domain.ErrInvalidRefKind, k)
129}
130
131func refToPB(r domain.Ref) *pb.Ref {
132 return &pb.Ref{
133 Name: r.Name,
134 ShortName: r.ShortName,
135 Kind: refKindToPB(r.Kind),
136 Id: r.ID.String(),
137 CommitId: r.CommitID.String(),
138 }
139}
140
141func entryKindToPB(k domain.EntryKind) pb.EntryKind {
142 switch k {
143 case domain.EntryFile:
144 return pb.EntryKind_ENTRY_KIND_FILE
145 case domain.EntryExecutable:
146 return pb.EntryKind_ENTRY_KIND_EXECUTABLE
147 case domain.EntryDirectory:
148 return pb.EntryKind_ENTRY_KIND_DIRECTORY
149 case domain.EntrySymlink:
150 return pb.EntryKind_ENTRY_KIND_SYMLINK
151 case domain.EntrySubmodule:
152 return pb.EntryKind_ENTRY_KIND_SUBMODULE
153 }
154 return pb.EntryKind_ENTRY_KIND_UNSPECIFIED
155}
156
157func signatureToPB(s domain.Signature) *pb.Signature {
158 return &pb.Signature{Name: s.Name, Email: s.Email, Time: timestamp(s.Time)}
159}
160
161func commitToPB(c domain.Commit) *pb.Commit {
162 out := &pb.Commit{
163 Id: c.ID.String(),
164 Author: signatureToPB(c.Author),
165 Committer: signatureToPB(c.Committer),
166 Summary: c.Summary,
167 Message: c.Message,
168 }
169 for _, p := range c.Parents {
170 out.ParentIds = append(out.ParentIds, p.String())
171 }
172 return out
173}
174
175func treeEntryToPB(e domain.TreeEntry) *pb.TreeEntry {
176 out := &pb.TreeEntry{
177 Name: e.Name,
178 Path: e.Path,
179 Kind: entryKindToPB(e.Kind),
180 Mode: e.Mode,
181 Id: e.ID.String(),
182 Size: e.Size,
183 }
184 if e.LastCommit != nil {
185 out.LastCommit = commitToPB(*e.LastCommit)
186 }
187 return out
188}
189
190func blobInfoToPB(b domain.BlobInfo) *pb.BlobInfo {
191 return &pb.BlobInfo{
192 Id: b.ID.String(),
193 Path: b.Path,
194 Size: b.Size,
195 Binary: b.Binary,
196 MediaType: b.MediaType,
197 Mode: b.Mode,
198 }
199}
200
201func fileStatusToPB(s domain.FileStatus) pb.FileStatus {
202 switch s {
203 case domain.FileAdded:
204 return pb.FileStatus_FILE_STATUS_ADDED
205 case domain.FileDeleted:
206 return pb.FileStatus_FILE_STATUS_DELETED
207 case domain.FileModified:
208 return pb.FileStatus_FILE_STATUS_MODIFIED
209 case domain.FileRenamed:
210 return pb.FileStatus_FILE_STATUS_RENAMED
211 case domain.FileCopied:
212 return pb.FileStatus_FILE_STATUS_COPIED
213 case domain.FileTypeChanged:
214 return pb.FileStatus_FILE_STATUS_TYPE_CHANGED
215 }
216 return pb.FileStatus_FILE_STATUS_UNSPECIFIED
217}
218
219func lineKindToPB(k domain.LineKind) pb.LineKind {
220 switch k {
221 case domain.LineContext:
222 return pb.LineKind_LINE_KIND_CONTEXT
223 case domain.LineAddition:
224 return pb.LineKind_LINE_KIND_ADDITION
225 case domain.LineDeletion:
226 return pb.LineKind_LINE_KIND_DELETION
227 }
228 return pb.LineKind_LINE_KIND_UNSPECIFIED
229}
230
231func hunkToPB(h domain.Hunk) *pb.Hunk {
232 out := &pb.Hunk{
233 OldStart: int32(h.OldStart), OldLines: int32(h.OldLines),
234 NewStart: int32(h.NewStart), NewLines: int32(h.NewLines),
235 Header: h.Header,
236 Lines: make([]*pb.Line, 0, len(h.Lines)),
237 }
238 for _, l := range h.Lines {
239 out.Lines = append(out.Lines, &pb.Line{
240 Kind: lineKindToPB(l.Kind),
241 OldNumber: int32(l.OldNumber),
242 NewNumber: int32(l.NewNumber),
243 Text: l.Text,
244 NoNewlineAtEof: l.NoNewlineAtEOF,
245 })
246 }
247 return out
248}
249
250func fileDiffToPB(f domain.FileDiff) *pb.FileDiff {
251 out := &pb.FileDiff{
252 OldPath: f.OldPath, NewPath: f.NewPath,
253 Status: fileStatusToPB(f.Status),
254 OldMode: f.OldMode, NewMode: f.NewMode,
255 OldId: f.OldID.String(), NewId: f.NewID.String(),
256 Binary: f.Binary,
257 Additions: int32(f.Additions),
258 Deletions: int32(f.Deletions),
259 Similarity: int32(f.Similarity),
260 Truncated: f.Truncated,
261 }
262 for _, h := range f.Hunks {
263 out.Hunks = append(out.Hunks, hunkToPB(h))
264 }
265 return out
266}
267
268func commitDetailToPB(d domain.CommitDetail) *pb.GetCommitResponse {
269 out := &pb.GetCommitResponse{
270 Commit: commitToPB(d.Commit),
271 Stats: &pb.DiffStats{
272 FilesChanged: int32(d.Stats.FilesChanged),
273 Additions: int32(d.Stats.Additions),
274 Deletions: int32(d.Stats.Deletions),
275 },
276 Diff: &pb.Diff{Truncated: d.Diff.Truncated},
277 }
278 for _, f := range d.Diff.Files {
279 out.Diff.Files = append(out.Diff.Files, fileDiffToPB(f))
280 }
281 return out
282}
283
284// oidToPB turns the zero object id of a ref update (no object) into "".
285func oidToPB(oid string) string {
286 if oid == domain.ZeroOID {
287 return ""
288 }
289 return oid
290}
291
292func refUpdatesToPB(updates []domain.RefUpdate) []*pb.RefUpdate {
293 out := make([]*pb.RefUpdate, 0, len(updates))
294 for _, u := range updates {
295 out = append(out, &pb.RefUpdate{Ref: u.Ref, OldId: oidToPB(u.Old), NewId: oidToPB(u.New)})
296 }
297 return out
298}
299
300// watchEventToPB maps one item of a watch; nil when the event has no
301// representation in the contract.
302func watchEventToPB(we app.WatchEvent) *pb.Event {
303 if we.Gap {
304 return &pb.Event{Kind: &pb.Event_Gap{Gap: &pb.Gap{}}}
305 }
306 out := &pb.Event{Id: we.ID}
307 switch ev := we.Event.(type) {
308 case domain.RepoCreated:
309 out.Time = timestamp(ev.At)
310 out.Kind = &pb.Event_RepoCreated{RepoCreated: &pb.RepoCreated{Repo: repoOrName(we, ev.Repo, ev.Visibility)}}
311 case domain.RepoUpdated:
312 out.Time = timestamp(ev.At)
313 out.Kind = &pb.Event_RepoUpdated{RepoUpdated: &pb.RepoUpdated{Repo: repoOrName(we, ev.Repo, "")}}
314 case domain.RepoDeleted:
315 out.Time = timestamp(ev.At)
316 out.Kind = &pb.Event_RepoDeleted{RepoDeleted: &pb.RepoDeleted{Repo: ev.Repo.String()}}
317 case domain.PushEvent:
318 out.Time = timestamp(ev.At)
319 out.Kind = &pb.Event_Pushed{Pushed: &pb.Pushed{Repo: ev.Repo.String(), RefUpdates: refUpdatesToPB(ev.Updates)}}
320 default:
321 return nil
322 }
323 return out
324}
325
326func repoOrName(we app.WatchEvent, name domain.RepoName, vis domain.Visibility) *pb.Repo {
327 if we.Repo != nil {
328 return repoToPB(*we.Repo)
329 }
330 return &pb.Repo{Name: name.String(), Visibility: visibilityToPB(vis)}
331}