v0.2.0 41cf2ec

convert.go RawHistoryFolder

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}

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