v0.2.0 41cf2ec

ports.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package ports declares the interfaces the use cases need from the outside
4// world. Adapters implement them; cmd wires them.
5package ports
6
7import (
8	"context"
9	"io"
10	"time"
11
12	"bnema.dev/ermite/internal/domain"
13)
14
15//go:generate go tool mockery
16
17// RepoMetaStore keeps repository metadata. It is the source of truth for
18// everything ermite owns about a repository except its Git data.
19type RepoMetaStore interface {
20	// Create stores repo. It returns domain.ErrAlreadyExists if a repository
21	// with that name exists, including when a concurrent Create wins.
22	Create(ctx context.Context, repo domain.Repo) error
23	// Get returns the repository, or domain.ErrNotFound.
24	Get(ctx context.Context, name domain.RepoName) (domain.Repo, error)
25	// List returns every repository ordered by name.
26	List(ctx context.Context) ([]domain.Repo, error)
27	// Update replaces the stored repository. It returns domain.ErrNotFound if
28	// there is none and domain.ErrConflict if another write changed the
29	// stored repository since it was read.
30	Update(ctx context.Context, repo domain.Repo) error
31	// Delete removes the repository and everything keyed to it: its merge
32	// requests, their comments and read positions, and its number sequence.
33	// A repository created again under the same name starts empty, with its
34	// numbers starting at 1. Deleting a missing one is not an error.
35	Delete(ctx context.Context, name domain.RepoName) error
36}
37
38// StagedID identifies a repository being created. It is opaque to callers.
39type StagedID string
40
41// RepoDirs is the result of listing the repository directories.
42type RepoDirs struct {
43	// Names are the directories that hold a valid repository name.
44	Names []domain.RepoName
45	// Invalid are the entries that cannot be a repository directory.
46	Invalid []string
47}
48
49// GitRepos manages bare repository directories.
50type GitRepos interface {
51	// Stage creates an empty bare repository outside the public directory
52	// and returns its identifier.
53	Stage(ctx context.Context) (StagedID, error)
54	// Publish moves a staged repository to its final place under name. It
55	// returns domain.ErrAlreadyExists if a directory for name exists; the
56	// staged repository is left in place for Discard.
57	Publish(ctx context.Context, id StagedID, name domain.RepoName) error
58	// Discard removes a staged repository. A missing one is not an error.
59	Discard(ctx context.Context, id StagedID) error
60	// PurgeStaging removes every staged repository left by a previous run.
61	PurgeStaging(ctx context.Context) error
62	// List returns the repository directories.
63	List(ctx context.Context) (RepoDirs, error)
64	// Trash moves the repository directory to the trash, where it can be
65	// recovered by hand. It returns domain.ErrNotFound if there is none.
66	Trash(ctx context.Context, name domain.RepoName) error
67	// DefaultBranch returns the branch HEAD points to (without refs/heads/)
68	// and whether that branch exists. It returns domain.ErrNotFound if there
69	// is no such repository.
70	DefaultBranch(ctx context.Context, name domain.RepoName) (branch string, exists bool, err error)
71	// HasBranch reports whether refs/heads/<branch> exists. A name that
72	// cannot be a branch does not exist. It returns domain.ErrNotFound if
73	// there is no such repository.
74	HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error)
75	// HeadBranch returns the short name of the branch HEAD points to, whether
76	// or not that branch exists, and "" when HEAD is detached. It reads no
77	// refs, so it is cheap enough for a listing. It returns
78	// domain.ErrNotFound if there is no such repository.
79	HeadBranch(ctx context.Context, name domain.RepoName) (string, error)
80	// SetDefaultBranch points HEAD to the branch, which need not exist yet.
81	SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error
82	// Maintain refreshes the commit-graph of the repository.
83	Maintain(ctx context.Context, name domain.RepoName) error
84}
85
86// CommitQuery selects a page of history.
87type CommitQuery struct {
88	// Revision is where the history starts; the zero value is HEAD.
89	Revision domain.Revision
90	// Path restricts the history to the commits that changed this file or
91	// directory; the root means the whole history.
92	Path domain.TreePath
93	// Skip is the number of commits of the listing to leave out. A caller that
94	// pages passes the commit it got in CommitPage.Head as Revision and the
95	// number of commits already read, so that every page walks the same
96	// history even when the revision moves.
97	Skip int
98	// Limit is the number of commits wanted, 1 to MaxCommitLimit.
99	Limit int
100}
101
102// MaxCommitLimit is the most commits one CommitQuery may ask for.
103const MaxCommitLimit = 1000
104
105// CommitPage is a page of history, newest first.
106type CommitPage struct {
107	// Head is the commit the revision resolved to.
108	Head domain.ObjectID
109	// Commits has at most Limit commits.
110	Commits []domain.Commit
111	// HasMore tells that commits follow the last one.
112	HasMore bool
113}
114
115// RepoReader reads the Git data of repositories: refs, trees, blobs, history
116// and diffs. A repository that has no directory gives domain.ErrNotFound; so
117// do an unknown revision, an empty repository (which has no commit) and a
118// path that does not exist at the revision.
119type RepoReader interface {
120	// ListRefs returns HEAD, then the branches, then the tags, the last two
121	// ordered by name. HEAD is always listed: when its branch does not exist
122	// yet its ShortName and ids are empty.
123	ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error)
124	// ResolveRevision turns a revision into the commit it designates. A name
125	// that is both a branch and a tag means the branch.
126	ResolveRevision(ctx context.Context, repo domain.RepoName, rev domain.Revision) (domain.ResolvedRevision, error)
127	// ResolveRefPath splits a path such as "feature/x/dir/file" into the
128	// longest leading part that names a branch or a tag and the rest. When no
129	// ref matches, the first segment may be a commit id (full, or abbreviated
130	// to MinAbbrevLen or more). It returns domain.ErrNotFound if nothing
131	// matches.
132	ResolveRefPath(ctx context.Context, repo domain.RepoName, path string) (domain.RefPath, error)
133	// GetTree lists the directory at path, at most domain.MaxTreeEntries
134	// entries (Tree.Truncated tells). With includeLastCommit, the first
135	// domain.MaxLastCommitEntries entries get their last commit. It returns
136	// domain.ErrNotDirectory if path names something else.
137	GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)
138	// OpenBlob returns the description of the file at path and a reader of its
139	// content, which streams without holding the whole file in memory. The
140	// caller must close the reader. It returns domain.ErrNotFile if path names
141	// something else.
142	OpenBlob(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error)
143	// ListCommits returns a page of history.
144	ListCommits(ctx context.Context, repo domain.RepoName, q CommitQuery) (CommitPage, error)
145	// GetCommit returns a commit with its statistics and its diff against the
146	// first parent, bounded by limits.
147	GetCommit(ctx context.Context, repo domain.RepoName, rev domain.Revision, limits domain.DiffLimits) (domain.CommitDetail, error)
148}
149
150// HTTPBackend tells how to run `git http-backend` for one request.
151type HTTPBackend struct {
152	// Path is the git-http-backend executable.
153	Path string
154	// Dir is the working directory of the process.
155	Dir string
156	// Env is the complete environment of the process: the isolated Git
157	// environment, GIT_PROJECT_ROOT and the Git configuration of the request
158	// (GIT_CONFIG_COUNT/KEY/VALUE). The caller may append to it.
159	Env []string
160}
161
162// GitBackend provides the Git side of smart HTTP.
163type GitBackend interface {
164	// HTTPBackend returns how to run git http-backend. For a push it also
165	// enables http.receivepack and sets REMOTE_USER; a read request cannot
166	// reach receive-pack.
167	HTTPBackend(ctx context.Context, push bool) (HTTPBackend, error)
168}
169
170// TokenStore keeps the stored tokens and the index from secret hash to token.
171// The secret itself is never stored.
172type TokenStore interface {
173	// Create stores the token and its hash index in one write. It returns
174	// domain.ErrAlreadyExists if the id or the hash is taken.
175	Create(ctx context.Context, token domain.Token) error
176	// Get returns the token with that id, or domain.ErrNotFound.
177	Get(ctx context.Context, id string) (domain.Token, error)
178	// FindByHash returns the token whose secret has that hash, or
179	// domain.ErrNotFound.
180	FindByHash(ctx context.Context, hash domain.TokenHash) (domain.Token, error)
181	// List returns every token, oldest first.
182	List(ctx context.Context) ([]domain.Token, error)
183	// Revoke removes the token and its hash index in one write, or returns
184	// domain.ErrNotFound.
185	Revoke(ctx context.Context, id string) error
186	// Touch records that the token was used at the given time. It returns
187	// domain.ErrNotFound if the token is gone.
188	Touch(ctx context.Context, id string, at time.Time) error
189}
190
191// MergeRequestStore keeps the merge requests of the repositories, their
192// comments and the read position of the reviewer. A repository has no entry
193// until its first merge request. Create, SaveComment and SetRead return
194// domain.ErrNotFound when the repository does not exist, so that deleting a
195// repository leaves nothing for one created again under its name.
196type MergeRequestStore interface {
197	// Create stores the merge request under the next number of its
198	// repository, which it sets, and returns it. Numbers start at 1 and are
199	// never reused, even by a concurrent Create.
200	Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error)
201	// Get returns the merge request, or domain.ErrNotFound. Its Version
202	// tells Update which state it was read at.
203	Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error)
204	// List returns every merge request of the repository, highest number
205	// first.
206	List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
207	// Update replaces the stored merge request. It returns domain.ErrNotFound
208	// if there is none and domain.ErrConflict if another write changed it
209	// since mr was read.
210	Update(ctx context.Context, mr domain.MergeRequest) error
211	// ListComments returns the comments of the merge request, oldest first.
212	ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error)
213	// GetComment returns one comment, or domain.ErrNotFound.
214	GetComment(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error)
215	// SaveComment writes a comment and the merge request it belongs to in one
216	// batch: a new comment (Version 0) must not exist yet, an existing one
217	// must be unchanged since it was read, and so must mr. It returns
218	// domain.ErrConflict otherwise. The caller sets mr.CommentSeq and the
219	// comment's ID for a new comment, and mr.UpdatedAt.
220	SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error
221	// GetRead returns the head the reviewer last marked read, or the zero
222	// ObjectID when there is none.
223	GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error)
224	// SetRead records the head the reviewer has read. It returns
225	// domain.ErrNotFound when the repository does not exist.
226	SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error
227}
228
229// MergeGit is what merge requests need from the Git repositories beyond
230// RepoReader. Every method returns domain.ErrNotFound when the repository has
231// no directory.
232type MergeGit interface {
233	// BranchHead returns the commit refs/heads/<branch> points to, or
234	// domain.ErrNotFound when there is no such branch.
235	BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)
236	// HasCommit reports whether the repository holds that commit.
237	HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)
238	// MergeBase returns the best common ancestor of two commits, or
239	// domain.ErrNotFound when they share no history.
240	MergeBase(ctx context.Context, repo domain.RepoName, a, b domain.ObjectID) (domain.ObjectID, error)
241	// IsAncestor reports whether ancestor is reachable from descendant, a
242	// commit being its own ancestor.
243	IsAncestor(ctx context.Context, repo domain.RepoName, ancestor, descendant domain.ObjectID) (bool, error)
244	// ListRange returns the commits reachable from head and not from exclude
245	// ("exclude..head"), newest first, at most limit; more is true when the
246	// range holds more. A zero exclude means the whole history of head.
247	ListRange(ctx context.Context, repo domain.RepoName, exclude, head domain.ObjectID, limit int) (commits []domain.Commit, more bool, err error)
248	// DiffCommits returns the statistics and the diff from commit base to
249	// commit head (renames detected), bounded by limits.
250	DiffCommits(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error)
251	// CommitTree returns the tree of a commit.
252	CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)
253	// WriteCommit stores a commit object, given as the text of the object,
254	// and returns its id.
255	WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)
256	// UpdateBranch moves refs/heads/<branch> from old to new. It returns
257	// domain.ErrConflict when the branch no longer points to old.
258	UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, new domain.ObjectID) error
259}
260
261// CommitSigner signs the commits ermite creates (ADR 0022). The private key
262// never leaves it.
263type CommitSigner interface {
264	// PublicKey returns the public key and its fingerprint.
265	PublicKey() domain.SigningPublicKey
266	// Sign signs the text of a commit object and returns the armored SSH
267	// signature (namespace "git") to put in its gpgsig header.
268	Sign(payload []byte) (string, error)
269}
270
271// SessionStore keeps what the browser sessions of the owner (ADR 0018) need
272// to survive a restart: the key that signs them and the epoch that voids them.
273type SessionStore interface {
274	// Key returns the 32-byte secret that signs the session cookies. The first
275	// call on an empty store generates and stores it; a restart keeps it.
276	Key(ctx context.Context) ([]byte, error)
277	// Epoch returns the current epoch, 0 until the first BumpEpoch. A session
278	// is valid only while its epoch is the current one.
279	Epoch(ctx context.Context) (uint64, error)
280	// BumpEpoch increments the epoch, which ends every session, and returns
281	// the new value.
282	BumpEpoch(ctx context.Context) (uint64, error)
283}
284
285// EventBus fans events out to in-process subscribers.
286type EventBus interface {
287	// Publish hands the event to every subscriber without blocking. A
288	// subscriber that cannot keep up loses events and receives a
289	// domain.EventGap.
290	Publish(ctx context.Context, ev domain.Event)
291	// Subscribe returns a channel of events published from now on. The
292	// channel is closed when ctx ends.
293	Subscribe(ctx context.Context) <-chan domain.Event
294}
295
296// Clock tells the time.
297type Clock interface {
298	Now() time.Time
299}

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 43

↑↓ moveenter openesc close