v0.1.0 3f267b5

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	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}

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 2

Tags 45

↑↓ moveenter openesc close