v0.34.0 7c1886f

convert.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close