detached aa7340c

ports.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package ports declares every interface that crosses a layer, and the
4// parameter and result types of the use case interfaces.
5//
6// Outbound ports (ports.go) are what the use cases need from the outside
7// world: adapters implement them. Inbound ports (services.go) are the use
8// cases as the adapters that serve requests see them: the types of package usecase
9// implement them. Adapters and use cases depend on this package and on domain,
10// never on each other; internal/app wires the implementations. All the mocks are generated in
11// ports/mocks.
12package ports
13
14import (
15	"context"
16	"fmt"
17	"io"
18	"time"
19
20	"bnema.dev/ermite/internal/domain"
21)
22
23//go:generate go tool mockery
24
25// RepoMetaStore keeps repository metadata. It is the source of truth for
26// everything ermite owns about a repository except its Git data.
27type RepoMetaStore interface {
28	// Create stores repo. It returns domain.ErrAlreadyExists if a repository
29	// with that name exists, including when a concurrent Create wins.
30	Create(ctx context.Context, repo domain.Repo) error
31	// Get returns the repository, or domain.ErrNotFound.
32	Get(ctx context.Context, name domain.RepoName) (domain.Repo, error)
33	// List returns every repository ordered by name.
34	List(ctx context.Context) ([]domain.Repo, error)
35	// Update replaces the stored repository. It returns domain.ErrNotFound if
36	// there is none and domain.ErrConflict if another write changed the
37	// stored repository since it was read.
38	Update(ctx context.Context, repo domain.Repo) error
39	// Delete removes the repository and everything keyed to it: its merge
40	// requests, their comments and read positions, and its number sequence.
41	// A repository created again under the same name starts empty, with its
42	// numbers starting at 1. Deleting a missing one is not an error.
43	Delete(ctx context.Context, name domain.RepoName) error
44}
45
46// StagedID identifies a repository being created. It is opaque to callers.
47type StagedID string
48
49// RepoDirs is the result of listing the repository directories.
50type RepoDirs struct {
51	// Names are the directories that hold a valid repository name.
52	Names []domain.RepoName
53	// Invalid are the entries that cannot be a repository directory.
54	Invalid []string
55}
56
57// GitRepos manages bare repository directories.
58type GitRepos interface {
59	// Stage creates an empty bare repository outside the public directory
60	// and returns its identifier.
61	Stage(ctx context.Context) (StagedID, error)
62	// Publish moves a staged repository to its final place under name. It
63	// returns domain.ErrAlreadyExists if a directory for name exists; the
64	// staged repository is left in place for Discard.
65	Publish(ctx context.Context, id StagedID, name domain.RepoName) error
66	// Discard removes a staged repository. A missing one is not an error.
67	Discard(ctx context.Context, id StagedID) error
68	// PurgeStaging removes every staged repository left by a previous run.
69	PurgeStaging(ctx context.Context) error
70	// List returns the repository directories.
71	List(ctx context.Context) (RepoDirs, error)
72	// Trash moves the repository directory to the trash, where it can be
73	// recovered by hand. It returns domain.ErrNotFound if there is none.
74	Trash(ctx context.Context, name domain.RepoName) error
75	// DefaultBranch returns the branch HEAD points to (without refs/heads/)
76	// and whether that branch exists. It returns domain.ErrNotFound if there
77	// is no such repository.
78	DefaultBranch(ctx context.Context, name domain.RepoName) (branch string, exists bool, err error)
79	// HasBranch reports whether refs/heads/<branch> exists. A name that
80	// cannot be a branch does not exist. It returns domain.ErrNotFound if
81	// there is no such repository.
82	HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error)
83	// HeadBranch returns the short name of the branch HEAD points to, whether
84	// or not that branch exists, and "" when HEAD is detached. It reads no
85	// refs, so it is cheap enough for a listing. It returns
86	// domain.ErrNotFound if there is no such repository.
87	HeadBranch(ctx context.Context, name domain.RepoName) (string, error)
88	// SetDefaultBranch points HEAD to the branch, which need not exist yet.
89	SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error
90	// Maintain refreshes the commit-graph of the repository.
91	Maintain(ctx context.Context, name domain.RepoName) error
92}
93
94// RepoFreshener is implemented by a RepoReader that keeps repositories open
95// between reads. The use cases that write to a repository (a push, the
96// maintenance that follows it) call Refresh so that the reader picks the change
97// up at once instead of when its own timer says so. It is optional: reads
98// stay correct without it.
99type RepoFreshener interface {
100	// Refresh tells the reader that the repository changed on disk. It does
101	// not fail: a reader that cannot refresh finds out on its next read.
102	Refresh(ctx context.Context, name domain.RepoName)
103}
104
105// CommitQuery selects a page of history.
106type CommitQuery struct {
107	// Revision is where the history starts; the zero value is HEAD.
108	Revision domain.Revision
109	// Path restricts the history to the commits that changed this file or
110	// directory; the root means the whole history.
111	Path domain.TreePath
112	// Skip is the number of commits of the listing to leave out. A caller that
113	// pages passes the commit it got in CommitPage.Head as Revision and the
114	// number of commits already read, so that every page walks the same
115	// history even when the revision moves.
116	Skip int
117	// Limit is the number of commits wanted, 1 to MaxCommitLimit.
118	Limit int
119}
120
121// MaxCommitTimes bounds RepoReader.CommitTimes.
122const MaxCommitTimes = 100_000
123
124// MaxCommitLimit is the most commits one CommitQuery may ask for.
125const MaxCommitLimit = 1000
126
127// Validate checks the page size and the offset of the query. It returns an
128// error matching domain.ErrInvalidPageSize.
129func (q CommitQuery) Validate() error {
130	if q.Limit < 1 || q.Limit > MaxCommitLimit || q.Skip < 0 {
131		return fmt.Errorf("list commits: %w: limit %d, skip %d", domain.ErrInvalidPageSize, q.Limit, q.Skip)
132	}
133	return nil
134}
135
136// CommitPage is a page of history, newest first.
137type CommitPage struct {
138	// Head is the commit the revision resolved to.
139	Head domain.ObjectID
140	// Commits has at most Limit commits.
141	Commits []domain.Commit
142	// HasMore tells that commits follow the last one.
143	HasMore bool
144}
145
146// RepoReader reads the Git data of repositories: refs, trees, blobs, history
147// and diffs. A repository that has no directory gives domain.ErrNotFound; so
148// do an unknown revision, an empty repository (which has no commit) and a
149// path that does not exist at the revision.
150type RepoReader interface {
151	// ListRefs returns HEAD, then the branches, then the tags, the last two
152	// ordered by name. HEAD is always listed: when its branch does not exist
153	// yet its ShortName and ids are empty.
154	ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error)
155	// ResolveRevision turns a revision into the commit it designates. A name
156	// that is both a branch and a tag means the branch.
157	ResolveRevision(ctx context.Context, repo domain.RepoName, rev domain.Revision) (domain.ResolvedRevision, error)
158	// ResolveRefPath splits a path such as "feature/x/dir/file" into the
159	// longest leading part that names a branch or a tag and the rest. When no
160	// ref matches, the first segment may be a commit id (full, or abbreviated
161	// to MinAbbrevLen or more). It returns domain.ErrNotFound if nothing
162	// matches.
163	ResolveRefPath(ctx context.Context, repo domain.RepoName, path string) (domain.RefPath, error)
164	// GetTree lists the directory at path, at most domain.MaxTreeEntries
165	// entries (Tree.Truncated tells). With includeLastCommit, the first
166	// domain.MaxLastCommitEntries entries get their last commit. It returns
167	// domain.ErrNotDirectory if path names something else.
168	GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)
169	// ListPaths lists the file and symlink paths of rev recursively in tree
170	// order, at most maxPaths paths and maxBytes bytes of paths (PathList.Truncated
171	// tells), skipping submodules and paths that are not addressable
172	// (domain.IsAddressablePath).
173	ListPaths(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths, maxBytes int) (domain.PathList, error)
174	// OpenBlob returns the description of the file at path and a reader of its
175	// content, which streams without holding the whole file in memory. The
176	// caller must close the reader. It returns domain.ErrNotFile if path names
177	// something else.
178	OpenBlob(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error)
179	// ListCommits returns a page of history.
180	ListCommits(ctx context.Context, repo domain.RepoName, q CommitQuery) (CommitPage, error)
181	// GetCommit returns a commit with its statistics and its diff against the
182	// first parent, bounded by limits.
183	GetCommit(ctx context.Context, repo domain.RepoName, rev domain.Revision, limits domain.DiffLimits) (domain.CommitDetail, error)
184	// CountCommits counts the commits reachable from head.
185	CountCommits(ctx context.Context, repo domain.RepoName, head domain.ObjectID) (int, error)
186	// CommitTimes are the committer dates of the commits reachable from head
187	// committed at or after since, in no particular order; at most
188	// MaxCommitTimes, truncated tells there were more.
189	CommitTimes(ctx context.Context, repo domain.RepoName, head domain.ObjectID, since time.Time) (times []time.Time, truncated bool, err error)
190	// LatestTag is the most recently created tag (the commit date for a
191	// lightweight tag); ok is false when the repository has no tag.
192	LatestTag(ctx context.Context, repo domain.RepoName) (tag domain.TagInfo, ok bool, err error)
193}
194
195// HTTPBackend tells how to run `git http-backend` for one request.
196type HTTPBackend struct {
197	// Path is the git-http-backend executable.
198	Path string
199	// Dir is the working directory of the process.
200	Dir string
201	// Env is the complete environment of the process: the isolated Git
202	// environment, GIT_PROJECT_ROOT and the Git configuration of the request
203	// (GIT_CONFIG_COUNT/KEY/VALUE). The caller may append to it.
204	Env []string
205}
206
207// GitBackend provides the Git side of smart HTTP.
208type GitBackend interface {
209	// HTTPBackend returns how to run git http-backend. For a push it also
210	// enables http.receivepack and sets REMOTE_USER; a read request cannot
211	// reach receive-pack.
212	HTTPBackend(ctx context.Context, push bool) (HTTPBackend, error)
213}
214
215// GitArchive writes the archive of a commit.
216type GitArchive interface {
217	// Archive writes the tree of the commit as a gzip-compressed tar to w, all
218	// the paths below the directory prefix. It writes nothing and returns
219	// domain.ErrNotFound when the repository or the commit does not exist.
220	Archive(ctx context.Context, repo domain.RepoName, commit domain.ObjectID, prefix string, w io.Writer) error
221}
222
223// TokenStore keeps the stored tokens and the index from secret hash to token.
224// The secret itself is never stored.
225type TokenStore interface {
226	// Create stores the token and its hash index in one write. It returns
227	// domain.ErrAlreadyExists if the id or the hash is taken.
228	Create(ctx context.Context, token domain.Token) error
229	// Get returns the token with that id, or domain.ErrNotFound.
230	Get(ctx context.Context, id string) (domain.Token, error)
231	// FindByHash returns the token whose secret has that hash, or
232	// domain.ErrNotFound.
233	FindByHash(ctx context.Context, hash domain.TokenHash) (domain.Token, error)
234	// List returns every token, oldest first.
235	List(ctx context.Context) ([]domain.Token, error)
236	// Revoke removes the token and its hash index in one write, or returns
237	// domain.ErrNotFound.
238	Revoke(ctx context.Context, id string) error
239	// Touch records that the token was used at the given time. It returns
240	// domain.ErrNotFound if the token is gone.
241	Touch(ctx context.Context, id string, at time.Time) error
242}
243
244// IssueStore keeps the issues of the repositories and their comments. Issues
245// share the number sequence of the merge requests of their repository, so that
246// "#n" names one record. Create and SaveComment return domain.ErrNotFound when
247// the repository does not exist, so that deleting a repository leaves nothing
248// for one created again under its name. *kvstore.Issues implements it.
249type IssueStore interface {
250	// Create stores the issue under the next number of its repository (the
251	// one merge requests draw from), which it sets, and returns it. A number
252	// is never given twice, to an issue or to a merge request, even by a
253	// concurrent Create.
254	Create(ctx context.Context, issue domain.Issue) (domain.Issue, error)
255	// Get returns the issue, or domain.ErrNotFound. Its Version tells Update
256	// which state it was read at.
257	Get(ctx context.Context, repo domain.RepoName, number int64) (domain.Issue, error)
258	// List returns a page of the issues of the repository in the given state
259	// (any when empty), highest number first.
260	List(ctx context.Context, repo domain.RepoName, params ListIssuesParams) (IssuePage, error)
261	// ListOpenAll returns the open issues of every repository, reading only
262	// the open ones (through the same index). The order is unspecified.
263	ListOpenAll(ctx context.Context) ([]domain.Issue, error)
264	// Update replaces the stored issue and keeps the index of the open issues
265	// in step. It returns domain.ErrNotFound if there is none and
266	// domain.ErrConflict if another write changed it since issue was read.
267	Update(ctx context.Context, issue domain.Issue) error
268	// SaveComment writes a comment and the issue it belongs to in one batch: a
269	// new comment (Version 0) must not exist yet, an existing one must be
270	// unchanged since it was read, and so must issue. It returns
271	// domain.ErrConflict otherwise. The caller sets issue.CommentSeq, the
272	// comment's ID for a new comment, and issue.UpdatedAt.
273	SaveComment(ctx context.Context, issue domain.Issue, c domain.IssueComment) error
274	// ListComments returns the comments of the issue, oldest first.
275	ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueComment, error)
276}
277
278// PendingIssueStore keeps the issues submitted through the public form until
279// the owner decides. They have no number: Approve gives one. Put and Approve
280// return domain.ErrNotFound when the repository does not exist, so that
281// deleting a repository leaves nothing for one created again under its name.
282// *kvstore.PendingIssues implements it.
283type PendingIssueStore interface {
284	// Put stores the pending issue; its id must be new.
285	Put(ctx context.Context, p domain.PendingIssue) error
286	// Get returns the pending issue, or domain.ErrNotFound.
287	Get(ctx context.Context, repo domain.RepoName, id domain.PendingIssueID) (domain.PendingIssue, error)
288	// List returns a page of the pending issues of the repository, or of
289	// every repository when repo is zero, oldest first. A PageSize of 0
290	// means every one.
291	List(ctx context.Context, repo domain.RepoName, page ListPendingParams) (PendingIssuePage, error)
292	// Count returns how many pending issues there are in all repositories,
293	// reading at most domain.MaxPendingIssues+1 of them.
294	Count(ctx context.Context) (int, error)
295	// Approve takes the next number of the repository (the one merge requests
296	// and issues share), writes the issue domain.ApprovedIssue(pending, now)
297	// with that number, indexes it as open and deletes the pending record,
298	// in one batch. It returns domain.ErrNotFound when either is missing.
299	Approve(ctx context.Context, repo domain.RepoName, id domain.PendingIssueID, now time.Time) (domain.Issue, error)
300	// Delete removes the pending issue, or returns domain.ErrNotFound.
301	Delete(ctx context.Context, repo domain.RepoName, id domain.PendingIssueID) error
302}
303
304// IssueKeyStore keeps the server key of the public issue form, from which the
305// use cases derive their subkeys.
306type IssueKeyStore interface {
307	// Key returns the 32-byte secret. The first call on an empty store
308	// generates and stores it; a restart keeps it.
309	Key(ctx context.Context) ([]byte, error)
310}
311
312// IssueBanStore keeps the bans of submitters, by the hash of their client key.
313type IssueBanStore interface {
314	// Ban bans the hash until the given time, replacing an earlier ban.
315	Ban(ctx context.Context, hash []byte, until time.Time) error
316	// Banned reports whether the hash is banned at now. An expired ban counts
317	// as not banned and is deleted.
318	Banned(ctx context.Context, hash []byte, now time.Time) (bool, error)
319}
320
321// MergeRequestStore keeps the merge requests of the repositories, their
322// comments and the read position of the reviewer. A repository has no entry
323// until its first merge request. Create, SaveComment and SetRead return
324// domain.ErrNotFound when the repository does not exist, so that deleting a
325// repository leaves nothing for one created again under its name.
326type MergeRequestStore interface {
327	// Create stores the merge request under the next number of its
328	// repository, which it sets, and returns it. Numbers start at 1 and are
329	// never reused, even by a concurrent Create.
330	Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error)
331	// Get returns the merge request, or domain.ErrNotFound. Its Version
332	// tells Update which state it was read at.
333	Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error)
334	// List returns every merge request of the repository, highest number
335	// first.
336	List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
337	// ListOpen returns the open merge requests of the repository, highest
338	// number first, reading only the open ones (through an index).
339	ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
340	// ListOpenAll returns the open merge requests of every repository,
341	// reading only the open ones (through the same index). The order is
342	// unspecified.
343	ListOpenAll(ctx context.Context) ([]domain.MergeRequest, error)
344	// CountOpen returns the number of open merge requests of the repository
345	// from the index of keys: no request is read.
346	CountOpen(ctx context.Context, repo domain.RepoName) (int, error)
347	// OpenBySource returns the numbers of the open merge requests whose source
348	// branch is one of branches (short names), from an index of keys: a push
349	// looks up the branches it moved, whatever the number of merge requests the
350	// repository ever had. No request is read. It stops after limit numbers (a
351	// limit of 0 or less means none) and reports truncated when there are more,
352	// without counting them; the context is checked between keys, and a
353	// cancelled one ends the lookup with its error and what was found so far.
354	OpenBySource(ctx context.Context, repo domain.RepoName, branches []string, limit int) (numbers []int64, truncated bool, err error)
355	// OpenByTarget is OpenBySource for the open merge requests whose target
356	// branch is one of branches.
357	OpenByTarget(ctx context.Context, repo domain.RepoName, branches []string, limit int) (numbers []int64, truncated bool, err error)
358	// Update replaces the stored merge request. It returns domain.ErrNotFound
359	// if there is none and domain.ErrConflict if another write changed it
360	// since mr was read.
361	Update(ctx context.Context, mr domain.MergeRequest) error
362	// ListComments returns the comments of the merge request, oldest first.
363	ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error)
364	// GetComment returns one comment, or domain.ErrNotFound.
365	GetComment(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error)
366	// SaveComment writes a comment and the merge request it belongs to in one
367	// batch: a new comment (Version 0) must not exist yet, an existing one
368	// must be unchanged since it was read, and so must mr. It returns
369	// domain.ErrConflict otherwise. The caller sets mr.CommentSeq and the
370	// comment's ID for a new comment, and mr.UpdatedAt.
371	SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error
372	// GetRead returns the head the reviewer last marked read, or the zero
373	// ObjectID when there is none.
374	GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error)
375	// SetRead records the head the reviewer has read. It returns
376	// domain.ErrNotFound when the repository does not exist, and
377	// domain.ErrConflict when concurrent writes to the repository keep it
378	// from landing.
379	SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error
380	// GetMergeability returns the stored mergeability of an open merge request,
381	// or domain.ErrNotFound when there is none.
382	GetMergeability(ctx context.Context, repo domain.RepoName, number int64) (domain.Mergeability, error)
383	// PutMergeability stores the mergeability. A Version of 0 requires that
384	// none is stored, otherwise the stored one must be unchanged since it was
385	// read (domain.ErrConflict). It returns domain.ErrNotFound when the merge
386	// request is not stored or not open, so that a late write never resurrects
387	// a record.
388	PutMergeability(ctx context.Context, m domain.Mergeability) error
389	// DeleteMergeability removes the stored mergeability; no error when absent.
390	DeleteMergeability(ctx context.Context, repo domain.RepoName, number int64) error
391}
392
393// MergeGit is what merge requests need from the Git repositories beyond
394// RepoReader. Every method returns domain.ErrNotFound when the repository has
395// no directory.
396type MergeGit interface {
397	// BranchHead returns the commit refs/heads/<branch> points to, or
398	// domain.ErrNotFound when there is no such branch.
399	BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)
400	// HasCommit reports whether the repository holds that commit.
401	HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)
402	// MergeBase returns the best common ancestor of two commits, or
403	// domain.ErrNotFound when they share no history.
404	MergeBase(ctx context.Context, repo domain.RepoName, a, b domain.ObjectID) (domain.ObjectID, error)
405	// IsAncestor reports whether ancestor is reachable from descendant, a
406	// commit being its own ancestor.
407	IsAncestor(ctx context.Context, repo domain.RepoName, ancestor, descendant domain.ObjectID) (bool, error)
408	// ListRange returns the commits reachable from head and not from exclude
409	// ("exclude..head"), newest first, at most limit; more is true when the
410	// range holds more. A zero exclude means the whole history of head.
411	ListRange(ctx context.Context, repo domain.RepoName, exclude, head domain.ObjectID, limit int) (commits []domain.Commit, more bool, err error)
412	// DiffCommits returns the statistics and the diff from commit base to
413	// commit head (renames detected), bounded by limits.
414	DiffCommits(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error)
415	// CommitTree returns the tree of a commit.
416	CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)
417	// WriteCommit stores a commit object, given as the text of the object,
418	// and returns its id.
419	WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)
420	// UpdateBranch moves refs/heads/<branch> from old to new. It returns
421	// domain.ErrConflict when the branch no longer points to old.
422	UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, new domain.ObjectID) error
423	// DeleteBranch deletes refs/heads/<branch> if it still points to old. It
424	// returns domain.ErrConflict when the branch moved or is gone.
425	DeleteBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID) error
426	// MergeTree merges head into base without creating a commit. When the
427	// merge is not clean (or the commits share no history) Clean is false and
428	// ConflictPaths lists every conflicted path, unbounded: the caller bounds
429	// them (MergeTreeResult.Bounded).
430	MergeTree(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID) (domain.MergeTreeResult, error)
431	// CountBehind counts the commits of target that head lacks, stopping at
432	// limit: the raw count, at most limit. The caller interprets it
433	// (domain.BoundBehind).
434	CountBehind(ctx context.Context, repo domain.RepoName, head, target domain.ObjectID, limit int) (n int, err error)
435}
436
437// MergeabilityTrigger queues mergeability checks. It only queues the work and
438// returns.
439type MergeabilityTrigger interface {
440	// OnBranchesMoved queues the open merge requests whose source or target
441	// branch was moved or deleted by updates.
442	OnBranchesMoved(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate)
443	// OnOpened queues a newly opened merge request.
444	OnOpened(ctx context.Context, mr domain.MergeRequest)
445}
446
447// MergeabilityReader reads the mergeability of open merge requests.
448type MergeabilityReader interface {
449	// Read returns the stored mergeability for mr and targetTip, or a
450	// "checking" one (and queues a check) when there is none or it is stale.
451	Read(ctx context.Context, mr domain.MergeRequest, targetTip domain.ObjectID) domain.Mergeability
452}
453
454// CommitSigner signs the commits ermite creates (ADR 0022). The private key
455// never leaves it.
456type CommitSigner interface {
457	// PublicKey returns the public key and its fingerprint.
458	PublicKey() domain.SigningPublicKey
459	// Sign signs the text of a commit object and returns the armored SSH
460	// signature (namespace "git") to put in its gpgsig header.
461	Sign(payload []byte) (string, error)
462}
463
464// SessionStore keeps what the browser sessions of the owner (ADR 0018) need
465// to survive a restart: the key that signs them and the epoch that voids them.
466type SessionStore interface {
467	// Key returns the 32-byte secret that signs the session cookies. The first
468	// call on an empty store generates and stores it; a restart keeps it.
469	Key(ctx context.Context) ([]byte, error)
470	// Epoch returns the current epoch, 0 until the first BumpEpoch. A session
471	// is valid only while its epoch is the current one.
472	Epoch(ctx context.Context) (uint64, error)
473	// BumpEpoch increments the epoch, which ends every session, and returns
474	// the new value.
475	BumpEpoch(ctx context.Context) (uint64, error)
476}
477
478// PasskeyStore keeps the owner's passkeys (ADR 0026) and the WebAuthn user
479// handle they belong to.
480type PasskeyStore interface {
481	// UserHandle returns the 32-byte WebAuthn user handle of the owner. The
482	// first call on an empty store generates and stores it; it never changes.
483	UserHandle(ctx context.Context) ([]byte, error)
484	// List returns every passkey, oldest first.
485	List(ctx context.Context) ([]domain.Passkey, error)
486	// Get returns the passkey with that credential id, or domain.ErrNotFound.
487	Get(ctx context.Context, id []byte) (domain.Passkey, error)
488	// Create stores the passkey. It returns domain.ErrAlreadyExists if the
489	// credential id is taken. It does not bound the number of passkeys: the
490	// use case checks domain.MaxPasskeys.
491	Create(ctx context.Context, p domain.Passkey) error
492	// Update replaces the stored passkey (sign count, backup state, last
493	// use), or returns domain.ErrNotFound.
494	Update(ctx context.Context, p domain.Passkey) error
495	// Delete removes the passkey, or returns domain.ErrNotFound.
496	Delete(ctx context.Context, id []byte) error
497}
498
499// PasskeyVerifier runs the WebAuthn ceremonies (ADR 0026). It hides the
500// WebAuthn library: the ceremony data it hands out is opaque to the caller,
501// which keeps it between the begin and the finish of a ceremony and passes it
502// back untouched. Every verification failure is domain.ErrUnauthenticated,
503// wrapped with a reason meant for logs, not for callers.
504type PasskeyVerifier interface {
505	// BeginRegistration starts the registration of a passkey for the owner.
506	// options is the JSON of the credential creation options for the browser
507	// ({"publicKey": {...}}); existing passkeys are excluded.
508	BeginRegistration(userHandle []byte, existing []domain.Passkey) (options []byte, ceremony []byte, err error)
509	// FinishRegistration checks the browser's response (the JSON of
510	// PublicKeyCredential.toJSON()) and returns the new passkey, without a name
511	// or creation time.
512	FinishRegistration(userHandle []byte, ceremony, response []byte) (domain.Passkey, error)
513	// BeginLogin starts a discoverable login. options is the JSON of the
514	// credential request options for the browser ({"publicKey": {...}}).
515	BeginLogin() (options []byte, ceremony []byte, err error)
516	// FinishLogin checks the browser's response and returns the stored passkey
517	// updated with the sign count and backup state the response reports. lookup
518	// returns domain.ErrNotFound for an unknown credential id. A counter that
519	// does not advance is not an error here: the returned SignCount is then the
520	// stored one, and the caller, which holds the lock that orders the logins,
521	// refuses it.
522	FinishLogin(userHandle []byte, ceremony, response []byte, lookup func(id []byte) (domain.Passkey, error)) (domain.Passkey, error)
523}
524
525// EventBus fans events out to in-process subscribers.
526type EventBus interface {
527	// Publish hands the event to every subscriber without blocking. A
528	// subscriber that cannot keep up loses events and receives a
529	// domain.EventGap.
530	Publish(ctx context.Context, ev domain.Event)
531	// Subscribe returns a channel of events published from now on. The
532	// channel is closed when ctx ends.
533	Subscribe(ctx context.Context) <-chan domain.Event
534}
535
536// Clock tells the time.
537type Clock interface {
538	Now() time.Time
539}

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