v0.10.0 ce48ee3

ci.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ports
4
5import (
6	"context"
7	"errors"
8	"strconv"
9	"time"
10
11	"bnema.dev/ermite/internal/domain"
12)
13
14// CIRunDraft is what CreateRun stores: a run with its source, its jobs and
15// their checks, built once the store has chosen the ids.
16type CIRunDraft struct {
17	Run    domain.CIRun
18	Source domain.CIRunSource
19	Jobs   []domain.CIJob
20	Checks []domain.CICheck
21}
22
23// CIWrite is a set of records written in one atomic batch. A run or a job with
24// Version 0 must not exist yet; any other must be unchanged since it was read.
25// Checks are derived from the jobs and upserted without a precondition.
26// Apply returns domain.ErrConflict otherwise and domain.ErrNotFound when the
27// repository no longer exists.
28//
29// RepoTokens are job repository tokens created with the batch (they must not
30// exist yet), RevokeJobs the jobs whose repository tokens are deleted by it,
31// and Resolutions the action resolutions of a run attempt to keep. They are
32// written under the lock of the run that owns them.
33//
34// Annotations replace, for each (execution of a job, step) they name, all the
35// annotations stored for it (none deletes them); ClearAnnotations deletes all
36// the annotations of the executions of jobs they name, steps included. Both
37// are written with the batch, so a call is applied whole or not at all.
38type CIWrite struct {
39	Runs             []domain.CIRun
40	Jobs             []domain.CIJob
41	Checks           []domain.CICheck
42	RepoTokens       []domain.CIRepoToken
43	RevokeJobs       []CIJobRef
44	Resolutions      []domain.CIActionResolution
45	Annotations      []CIStepAnnotations
46	ClearAnnotations []CILogRef
47}
48
49// CIStepAnnotations are all the annotations of one step of one execution of a
50// job, in the order the step produced them.
51type CIStepAnnotations struct {
52	Repo domain.RepoName
53	Exec CILogRef
54	Step int32
55	// Items are the annotations; none removes those of the step.
56	Items []domain.CIAnnotation
57}
58
59// CIJobRef names one execution of a job.
60type CIJobRef struct {
61	RunID      int64
62	RunAttempt int32
63	JobID      string
64}
65
66// CIRepoTokenStore reads the job repository tokens, for the authentication of
67// a request. They are written by CIStore.Apply. *kvstore.CI implements it.
68type CIRepoTokenStore interface {
69	// FindRepoTokenByHash returns the token whose secret has that hash, or
70	// domain.ErrNotFound.
71	FindRepoTokenByHash(ctx context.Context, hash domain.TokenHash) (domain.CIRepoToken, error)
72	// GetRepoToken returns the token with that id, or domain.ErrNotFound.
73	GetRepoToken(ctx context.Context, id string) (domain.CIRepoToken, error)
74}
75
76// CIStore keeps CI runs, jobs and checks. The store derives its own indexes
77// (the queue of ready jobs, the active jobs, the open runs, the job tokens)
78// from the records it writes.
79type CIStore interface {
80	// CreateRun takes the next run id and the next number of the workflow in
81	// the repository (both never reused), calls build with them and stores
82	// the draft it returns, in one batch that fails with domain.ErrNotFound
83	// once the repository is deleted. The records come back with their
84	// versions.
85	CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id, number int64) CIRunDraft) (CIRunDraft, error)
86	// Apply writes the records in one batch and returns the version they all
87	// have afterwards.
88	Apply(ctx context.Context, w CIWrite) (version uint64, err error)
89	// GetRun returns the run, or domain.ErrNotFound.
90	GetRun(ctx context.Context, repo domain.RepoName, id int64) (domain.CIRun, error)
91	// GetActionResolution returns the resolution kept for the key in the run
92	// attempt, or domain.ErrNotFound.
93	GetActionResolution(ctx context.Context, runID int64, runAttempt int32, key string) (domain.CIActionResolution, error)
94	// GetRunSource returns what the run was planned from, or
95	// domain.ErrNotFound. It is how a run is found from its id alone.
96	GetRunSource(ctx context.Context, runID int64) (domain.CIRunSource, error)
97	// ListRuns returns the runs of the repository with an id below before (0
98	// for no bound), newest first, at most limit.
99	ListRuns(ctx context.Context, repo domain.RepoName, before int64, limit int) ([]domain.CIRun, error)
100	// RunIDs returns the ids of the runs of the repository.
101	RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error)
102	// ListOpenRuns returns the runs that have not completed.
103	ListOpenRuns(ctx context.Context) ([]domain.CIRun, error)
104	// JobAnnotations returns the annotations of one execution of a job
105	// (CIJob.Attempt), ordered by step then by the order the step produced
106	// them. A job without annotation gives an empty list.
107	JobAnnotations(ctx context.Context, runID int64, attempt int32, jobID string) ([]domain.CIAnnotation, error)
108	// GetJob returns a job of a run at an attempt of the run, or
109	// domain.ErrNotFound.
110	GetJob(ctx context.Context, runID int64, runAttempt int32, jobID string) (domain.CIJob, error)
111	// ListJobs returns the jobs of a run at an attempt, ordered by index then
112	// combination.
113	ListJobs(ctx context.Context, runID int64, runAttempt int32) ([]domain.CIJob, error)
114	// FindJobByToken returns the job whose current token has that hash and
115	// whose token is still valid at now, or domain.ErrNotFound.
116	FindJobByToken(ctx context.Context, hash domain.TokenHash, now time.Time) (domain.CIJob, error)
117	// ListReadyJobs returns the jobs that wait for a runner, oldest first, at
118	// most limit.
119	ListReadyJobs(ctx context.Context, limit int) ([]domain.CIJob, error)
120	// ListActiveJobs returns the jobs given to a runner and not over.
121	ListActiveJobs(ctx context.Context) ([]domain.CIJob, error)
122	// ListChecks returns the checks of a commit, at most limit; more is true
123	// when there are more.
124	ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) (checks []domain.CICheck, more bool, err error)
125	// Sweep removes the runs completed before the time, with their jobs,
126	// checks and sources (not their numbers), and returns their ids so that
127	// the logs can go too.
128	Sweep(ctx context.Context, before time.Time) ([]int64, error)
129	// CreateRunner stores the runner and the index of its token. A name in use
130	// is domain.ErrAlreadyExists, or with replace the old runner is removed in
131	// the same batch and returned.
132	CreateRunner(ctx context.Context, r domain.CIRunner, replace bool) (removed []domain.CIRunner, err error)
133	// GetRunner returns the runner, or domain.ErrNotFound.
134	GetRunner(ctx context.Context, id string) (domain.CIRunner, error)
135	// FindRunnerByToken returns the runner whose token has that hash, or
136	// domain.ErrNotFound.
137	FindRunnerByToken(ctx context.Context, hash domain.TokenHash) (domain.CIRunner, error)
138	// ListRunners returns every runner ordered by name.
139	ListRunners(ctx context.Context) ([]domain.CIRunner, error)
140	// RemoveRunner removes the runner and its token, or returns
141	// domain.ErrNotFound.
142	RemoveRunner(ctx context.Context, id string) (domain.CIRunner, error)
143}
144
145// CIRunCleaner is what repository deletion needs from CI: the runs of the
146// repository before its records go, and the removal of their logs and
147// summaries after. *usecase.CI implements it.
148type CIRunCleaner interface {
149	RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error)
150	RemoveLogs(ctx context.Context, runIDs []int64) error
151}
152
153// CIChecksReader is the read side of the checks, for the merge rule and the
154// pages that show them. *usecase.CI implements it.
155type CIChecksReader interface {
156	// CommitChecks returns the checks of a commit, without access control (the
157	// caller has checked it). More than 1,000 is domain.ErrResourceExhausted,
158	// not a part of them: a rule must not decide on some of the checks.
159	CommitChecks(ctx context.Context, repo domain.RepoName, sha string) ([]domain.CICheck, error)
160}
161
162// CILogRef names one execution of a job: its log, its summaries and its
163// annotations are kept under it.
164type CILogRef struct {
165	RunID   int64
166	JobID   string
167	Attempt int32
168}
169
170// ErrCILogLimit is returned by CILogStore.Append when the lines do not fit in
171// the log limit; nothing was appended.
172var ErrCILogLimit = errors.New("log limit reached")
173
174// ErrCILogGap wraps domain.ErrOutOfRange when an offset is above the count.
175type ErrCILogGap struct{ Count int64 }
176
177// Error implements error.
178func (e *ErrCILogGap) Error() string {
179	return "out of range: the log holds lines up to the offset of " + strconv.FormatInt(e.Count, 10)
180}
181
182// Is makes errors.Is(err, domain.ErrOutOfRange) true.
183func (e *ErrCILogGap) Is(target error) bool { return target == domain.ErrOutOfRange }
184
185// CILogStore keeps the logs of the jobs: append-only files, one line per log
186// line, never in the metadata store. Lines are numbered from 0.
187type CILogStore interface {
188	// Append appends the lines at offset (the number of the first line), with
189	// the idempotence of AppendLog: lines already stored are skipped, a gap is
190	// an *ErrCILogGap. It returns the number of lines after the call and how
191	// many of the request were new. A log that would exceed limit bytes gets
192	// nothing and ErrCILogLimit.
193	Append(ctx context.Context, ref CILogRef, offset int64, lines []domain.CILogLine, limit int64) (next int64, added int, err error)
194	// AppendForced appends lines whatever the limit (the last line of a log
195	// that reached it).
196	AppendForced(ctx context.Context, ref CILogRef, lines []domain.CILogLine) (next int64, err error)
197	// Count returns the number of lines held, 0 for a log that does not exist.
198	Count(ctx context.Context, ref CILogRef) (int64, error)
199	// Read returns lines from the one numbered from, at most maxLines lines and
200	// about maxBytes of text. It returns domain.ErrNotFound when the log does
201	// not exist and domain.ErrOutOfRange when from is above the count.
202	Read(ctx context.Context, ref CILogRef, from int64, maxLines, maxBytes int) ([]domain.CILogLine, error)
203	// Changed returns a channel that is closed at the next append to the log.
204	Changed(ref CILogRef) <-chan struct{}
205	// Reset empties the log.
206	Reset(ctx context.Context, ref CILogRef) error
207	// RemoveRuns deletes the logs of the runs.
208	RemoveRuns(ctx context.Context, runIDs []int64) error
209	// RunIDs lists the runs that have a log directory, for the orphan sweep.
210	RunIDs(ctx context.Context) ([]int64, error)
211}
212
213// CISummaryStore keeps the job summaries (the Markdown a step wrote to
214// GITHUB_STEP_SUMMARY): one file per step of an execution of a job, never in
215// the metadata store.
216type CISummaryStore interface {
217	// Put stores the summary of a step, replacing the one it has; the file
218	// appears whole or not at all. An empty markdown removes the summary.
219	Put(ctx context.Context, ref CILogRef, step int32, markdown string) error
220	// List returns the summaries of the steps that have one, by step number,
221	// at most limit; more is true when there are others. With maxBytes above
222	// 0 the files are sized (stat) before they are read, and a summary that
223	// would take the total of the Markdown read past maxBytes is returned
224	// Omitted, unread, as are all the ones after it: a call never reads more
225	// than maxBytes.
226	List(ctx context.Context, ref CILogRef, limit int, maxBytes int64) (summaries []domain.CIStepSummary, more bool, err error)
227	// RemoveJob deletes the summaries of one execution of a job.
228	RemoveJob(ctx context.Context, ref CILogRef) error
229	// RemoveRuns deletes the summaries of the runs.
230	RemoveRuns(ctx context.Context, runIDs []int64) error
231	// RunIDs lists the runs that have a summary directory, for the orphan
232	// sweep.
233	RunIDs(ctx context.Context) ([]int64, error)
234}
235
236// WorkflowTrigger starts the workflows that match pushed refs. OnPush only
237// queues the work and returns: planning happens in the background and its
238// errors never reach the push.
239type WorkflowTrigger interface {
240	OnPush(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, pusher domain.Principal)
241	// OnMerge is OnPush for the move of a target branch by a merge. GitHub shows
242	// the owner as the actor of the push of a merge, whoever merged; but the
243	// rights the run's actions are resolved with are those of merger, the
244	// principal that merged, never the owner's.
245	OnMerge(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, merger domain.Principal)
246}
247
248// CIGit is what planning needs from Git beyond RepoReader and MergeGit.
249type CIGit interface {
250	// ChangedPaths returns the paths changed from base to head (a zero base
251	// means the empty tree), in the order of the diff and without renames
252	// detection, at most max; truncated is true when there were more.
253	ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, max int) (paths []string, truncated bool, err error)
254	// CommitsOffBranches returns the commits reachable from head and from no
255	// branch (refs/heads/*; tags do not count), newest first, at most limit
256	// (1 to MaxCommitLimit); more is true when there are others. It is empty
257	// for a commit that a branch already has.
258	CommitsOffBranches(ctx context.Context, repo domain.RepoName, head domain.ObjectID, limit int) (commits []domain.Commit, more bool, err error)
259}
260
261// --- inbound ---
262
263// RegisterRunnerParams describe a runner to register.
264type RegisterRunnerParams struct {
265	Name      string
266	Labels    []string
267	Version   string
268	Isolation domain.CIIsolation
269	Arch, OS  string
270	Replace   bool
271}
272
273// RegisteredRunner is a new runner with its secret, shown once.
274type RegisteredRunner struct {
275	Runner domain.CIRunner
276	Token  string
277}
278
279// RunnerJob is a job given to a runner.
280type RunnerJob struct {
281	Job   domain.CIJob
282	Token string
283	// GithubToken is the job repository token, the "github.token" of the job.
284	GithubToken  string
285	Repo         domain.RepoName
286	Sha, Ref     string
287	WorkflowPath string
288	WorkflowYAML string
289}
290
291// FetchJobParams selects the job a runner asks for.
292type FetchJobParams struct {
293	Labels []string
294	PollID string
295}
296
297// FetchJobResult is the answer of a poll.
298type FetchJobResult struct {
299	Job *RunnerJob
300	// RetryAfter is how long the runner waits before polling again.
301	RetryAfter time.Duration
302}
303
304// StepUpdate is the state of one step as a runner reports it.
305type StepUpdate struct {
306	Number     int32
307	Name       string
308	State      domain.CIStepState
309	Conclusion domain.CIConclusion
310	// Outcome is how the step ended before continue-on-error; none when the
311	// runner does not report it.
312	Outcome     domain.CIConclusion
313	StartedAt   time.Time
314	CompletedAt time.Time
315}
316
317// CompleteJobParams end a job.
318type CompleteJobParams struct {
319	JobID          string
320	Conclusion     domain.CIConclusion
321	Outputs        map[string]string
322	RunnerShutdown bool
323	Steps          []StepUpdate
324	// Annotations replace those of each step they name, as in UpdateJob.
325	Annotations []domain.CIAnnotation
326}
327
328// CompleteJobResult is the stored result of a job.
329type CompleteJobResult struct {
330	Conclusion domain.CIConclusion
331	Error      string
332	Applied    bool
333}
334
335// ResolvedAction is the resolution of one "uses" of ResolveActions: where to
336// download the action, or Error (then only Ref is set).
337type ResolvedAction struct {
338	Ref           string
339	NameWithOwner string
340	Path          string
341	ResolvedSHA   string
342	TarballURL    string
343	Token         string
344	Source        domain.CIActionSource
345	Error         string
346}
347
348// RemoteRef is a ref of a remote repository, as `git ls-remote` lists it.
349type RemoteRef struct {
350	// Name is the full ref name ("refs/heads/main", "refs/tags/v1").
351	Name string
352	// ID is the object the ref points to (the tag object of an annotated tag).
353	ID string
354	// Peeled is the commit an annotated tag points to; empty for other refs.
355	Peeled string
356}
357
358// GitRemote lists the refs of a repository on github.com, with no
359// credentials. It returns domain.ErrNotFound for a repository that does not
360// exist or needs credentials (GitHub answers alike), and an error matching
361// domain.ErrUnavailable when it cannot tell (network, timeout, too large an
362// answer).
363type GitRemote interface {
364	ListRefs(ctx context.Context, owner, repo string) ([]RemoteRef, error)
365}
366
367// JobSignal is one message of WatchJob.
368type JobSignal struct {
369	Keepalive bool
370	// Cancel is set for a cancel request.
371	Cancel *JobCancel
372	// Ended is set when the job has ended.
373	Ended *domain.JobEndedError
374}
375
376// JobCancel is a cancel request for a running job.
377type JobCancel struct {
378	Reason      domain.CICancelReason
379	Message     string
380	RequestedAt time.Time
381}
382
383// RunnerService is what runners call. The secret is the bearer secret of the
384// call: the runner token for FetchJob, the job token for the other calls.
385// A bad secret is domain.ErrUnauthenticated; a job token of another job than
386// the one named is domain.ErrForbidden. *usecase.CI implements it.
387type RunnerService interface {
388	Register(ctx context.Context, p domain.Principal, params RegisterRunnerParams) (RegisteredRunner, error)
389	FetchJob(ctx context.Context, secret string, params FetchJobParams) (FetchJobResult, error)
390	// UpdateJob applies the steps and the annotations whole or not at all: for
391	// each step number present in annotations, the annotations of that step are
392	// replaced (also for a step already completed, or not in steps).
393	UpdateJob(ctx context.Context, secret, jobID string, steps []StepUpdate, annotations []domain.CIAnnotation) error
394	AppendLog(ctx context.Context, secret, jobID string, step int32, offset int64, lines []domain.CILogLine) (next int64, err error)
395	Heartbeat(ctx context.Context, secret, jobID string) error
396	// WatchJob calls ready once the job token is valid and the stream is
397	// registered, before any message: the adapter flushes the response headers
398	// then, because a client's call returns only when they arrive.
399	WatchJob(ctx context.Context, secret, jobID string, ready func(), send func(JobSignal) error) error
400	CompleteJob(ctx context.Context, secret string, params CompleteJobParams) (CompleteJobResult, error)
401	// ResolveActions resolves the "uses" of the job's steps, one answer per
402	// reference in the same order. A reference that cannot be resolved is not
403	// an error: its answer carries Error. A local ("./") or Docker
404	// ("docker://") reference, or more than 100, is domain.ErrInvalid.
405	ResolveActions(ctx context.Context, secret, jobID string, uses []string) ([]ResolvedAction, error)
406	// UploadSummary stores the summary of a step, replacing the one it has; an
407	// empty markdown removes it. More than 1 MiB, invalid UTF-8 or a step
408	// number outside 1 to 1,000 is domain.ErrInvalid.
409	UploadSummary(ctx context.Context, secret, jobID string, step int32, markdown string) error
410}
411
412// JobView is a job as owners see it.
413type JobView struct {
414	domain.CIJob
415	// Lines is the number of lines of its log now.
416	Lines int64
417	// RunnerGone is set when the runner of the job was removed since.
418	RunnerGone bool
419}
420
421// StepRanges returns where the lines of every step are in the log.
422func (v JobView) StepRanges() []domain.StepRange { return domain.StepRanges(v.Steps, v.Lines) }
423
424// ListRunsParams select a page of runs.
425type ListRunsParams struct {
426	PageSize  int
427	PageToken string
428	// Workflow restricts the result to a workflow file: its path or its file
429	// name. Branch restricts it to the runs of a branch, by short name. Status
430	// restricts it to the runs of that status. Empty means no restriction.
431	Workflow string
432	Branch   string
433	Status   domain.CIStatus
434}
435
436// RunPage is a page of runs, newest first.
437type RunPage struct {
438	Runs          []domain.CIRun
439	NextPageToken string
440}
441
442// RunDetail is a run with its jobs.
443type RunDetail struct {
444	Run           domain.CIRun
445	Jobs          []JobView
446	LatestAttempt int32
447}
448
449// LogBatch is a message of a log stream: consecutive lines from Offset.
450type LogBatch struct {
451	Offset int64
452	Lines  []JobLogLine
453}
454
455// JobLogLine is a log line with its step.
456type JobLogLine struct {
457	Step int32
458	domain.CILogLine
459}
460
461// GetJobLogParams select the log to stream.
462type GetJobLogParams struct {
463	JobID string
464	// Attempt is the attempt of the job, 0 for the latest.
465	Attempt int32
466	Offset  int64
467	Follow  bool
468	// Ready, when not nil, is called once the access is checked and the
469	// stream is counted, before any batch: the adapter flushes the response
470	// headers then (a follow on a job with no line yet would keep the client's
471	// call waiting for the first one).
472	Ready func()
473	// Client identifies where the call comes from, for the limit of streams
474	// per client; empty counts against the total only.
475	Client string
476}
477
478// CheckView is a check with what owners need to read it.
479type CheckView struct {
480	domain.CICheck
481	// WaitingForRunner is set for a queued check whose job no registered
482	// runner can take (no runner has all its labels).
483	WaitingForRunner bool
484}
485
486// ChecksResult are the checks of a commit.
487type ChecksResult struct {
488	Checks    []CheckView
489	Truncated bool
490}
491
492// RunnerPage is a page of runners.
493type RunnerPage struct {
494	Runners       []RunnerView
495	NextPageToken string
496}
497
498// RunnerView is a runner with what is known of it now.
499type RunnerView struct {
500	domain.CIRunner
501	LastSeen time.Time
502	Busy     bool
503}
504
505// JobAnnotation is an annotation with the job it belongs to.
506type JobAnnotation struct {
507	JobID      string
508	Annotation domain.CIAnnotation
509}
510
511// AnnotationList are the annotations of a run, at most
512// domain.CIMaxListedAnnotations.
513type AnnotationList struct {
514	Annotations []JobAnnotation
515	Truncated   bool
516}
517
518// SummaryList are the summaries of a job, at most domain.CIMaxListedSummaries.
519type SummaryList struct {
520	Summaries []domain.CIStepSummary
521	Truncated bool
522}
523
524// RunService is the owner side of CI. *usecase.CI implements it.
525type RunService interface {
526	ListRuns(ctx context.Context, p domain.Principal, repo domain.RepoName, params ListRunsParams) (RunPage, error)
527	GetRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32) (RunDetail, error)
528	GetJob(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (JobView, error)
529	GetJobLog(ctx context.Context, p domain.Principal, repo domain.RepoName, params GetJobLogParams, send func(LogBatch) error) error
530	CancelRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64) (domain.CIRun, error)
531	RerunRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool) (domain.CIRun, error)
532	ListChecks(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ChecksResult, error)
533	// ListAnnotations returns the annotations of a run attempt (0 for the
534	// latest), of one job when jobID is not empty. Each job gives those of the
535	// attempt in which it executed.
536	ListAnnotations(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32, jobID string) (AnnotationList, error)
537	// GetStepSummaries returns the summaries of a job at an attempt (0 for the
538	// latest), by step number. maxBytes above 0 bounds the Markdown read: the
539	// summaries past it come back Omitted, without their Markdown (the API
540	// passes 0, no bound).
541	GetStepSummaries(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, maxBytes int64) (SummaryList, error)
542	ListRunners(ctx context.Context, p domain.Principal, size int, token string) (RunnerPage, error)
543	RemoveRunner(ctx context.Context, p domain.Principal, id string) error
544}

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 44

↑↓ moveenter openesc close