v0.1.0 3f267b5

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. Deleting a missing one is not an error.
32	Delete(ctx context.Context, name domain.RepoName) error
33}
34
35// StagedID identifies a repository being created. It is opaque to callers.
36type StagedID string
37
38// RepoDirs is the result of listing the repository directories.
39type RepoDirs struct {
40	// Names are the directories that hold a valid repository name.
41	Names []domain.RepoName
42	// Invalid are the entries that cannot be a repository directory.
43	Invalid []string
44}
45
46// GitRepos manages bare repository directories.
47type GitRepos interface {
48	// Stage creates an empty bare repository outside the public directory
49	// and returns its identifier.
50	Stage(ctx context.Context) (StagedID, error)
51	// Publish moves a staged repository to its final place under name. It
52	// returns domain.ErrAlreadyExists if a directory for name exists; the
53	// staged repository is left in place for Discard.
54	Publish(ctx context.Context, id StagedID, name domain.RepoName) error
55	// Discard removes a staged repository. A missing one is not an error.
56	Discard(ctx context.Context, id StagedID) error
57	// PurgeStaging removes every staged repository left by a previous run.
58	PurgeStaging(ctx context.Context) error
59	// List returns the repository directories.
60	List(ctx context.Context) (RepoDirs, error)
61	// Trash moves the repository directory to the trash, where it can be
62	// recovered by hand. It returns domain.ErrNotFound if there is none.
63	Trash(ctx context.Context, name domain.RepoName) error
64	// DefaultBranch returns the branch HEAD points to (without refs/heads/)
65	// and whether that branch exists. It returns domain.ErrNotFound if there
66	// is no such repository.
67	DefaultBranch(ctx context.Context, name domain.RepoName) (branch string, exists bool, err error)
68	// HasBranch reports whether refs/heads/<branch> exists. A name that
69	// cannot be a branch does not exist. It returns domain.ErrNotFound if
70	// there is no such repository.
71	HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error)
72	// HeadBranch returns the short name of the branch HEAD points to, whether
73	// or not that branch exists, and "" when HEAD is detached. It reads no
74	// refs, so it is cheap enough for a listing. It returns
75	// domain.ErrNotFound if there is no such repository.
76	HeadBranch(ctx context.Context, name domain.RepoName) (string, error)
77	// SetDefaultBranch points HEAD to the branch, which need not exist yet.
78	SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error
79	// Maintain refreshes the commit-graph of the repository.
80	Maintain(ctx context.Context, name domain.RepoName) error
81}
82
83// CommitQuery selects a page of history.
84type CommitQuery struct {
85	// Revision is where the history starts; the zero value is HEAD.
86	Revision domain.Revision
87	// Path restricts the history to the commits that changed this file or
88	// directory; the root means the whole history.
89	Path domain.TreePath
90	// Skip is the number of commits of the listing to leave out. A caller that
91	// pages passes the commit it got in CommitPage.Head as Revision and the
92	// number of commits already read, so that every page walks the same
93	// history even when the revision moves.
94	Skip int
95	// Limit is the number of commits wanted, 1 to MaxCommitLimit.
96	Limit int
97}
98
99// MaxCommitLimit is the most commits one CommitQuery may ask for.
100const MaxCommitLimit = 1000
101
102// CommitPage is a page of history, newest first.
103type CommitPage struct {
104	// Head is the commit the revision resolved to.
105	Head domain.ObjectID
106	// Commits has at most Limit commits.
107	Commits []domain.Commit
108	// HasMore tells that commits follow the last one.
109	HasMore bool
110}
111
112// RepoReader reads the Git data of repositories: refs, trees, blobs, history
113// and diffs. A repository that has no directory gives domain.ErrNotFound; so
114// do an unknown revision, an empty repository (which has no commit) and a
115// path that does not exist at the revision.
116type RepoReader interface {
117	// ListRefs returns HEAD, then the branches, then the tags, the last two
118	// ordered by name. HEAD is always listed: when its branch does not exist
119	// yet its ShortName and ids are empty.
120	ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error)
121	// ResolveRevision turns a revision into the commit it designates. A name
122	// that is both a branch and a tag means the branch.
123	ResolveRevision(ctx context.Context, repo domain.RepoName, rev domain.Revision) (domain.ResolvedRevision, error)
124	// ResolveRefPath splits a path such as "feature/x/dir/file" into the
125	// longest leading part that names a branch or a tag and the rest. When no
126	// ref matches, the first segment may be a commit id (full, or abbreviated
127	// to MinAbbrevLen or more). It returns domain.ErrNotFound if nothing
128	// matches.
129	ResolveRefPath(ctx context.Context, repo domain.RepoName, path string) (domain.RefPath, error)
130	// GetTree lists the directory at path, at most domain.MaxTreeEntries
131	// entries (Tree.Truncated tells). With includeLastCommit, the first
132	// domain.MaxLastCommitEntries entries get their last commit. It returns
133	// domain.ErrNotDirectory if path names something else.
134	GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)
135	// OpenBlob returns the description of the file at path and a reader of its
136	// content, which streams without holding the whole file in memory. The
137	// caller must close the reader. It returns domain.ErrNotFile if path names
138	// something else.
139	OpenBlob(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error)
140	// ListCommits returns a page of history.
141	ListCommits(ctx context.Context, repo domain.RepoName, q CommitQuery) (CommitPage, error)
142	// GetCommit returns a commit with its statistics and its diff against the
143	// first parent, bounded by limits.
144	GetCommit(ctx context.Context, repo domain.RepoName, rev domain.Revision, limits domain.DiffLimits) (domain.CommitDetail, error)
145}
146
147// HTTPBackend tells how to run `git http-backend` for one request.
148type HTTPBackend struct {
149	// Path is the git-http-backend executable.
150	Path string
151	// Dir is the working directory of the process.
152	Dir string
153	// Env is the complete environment of the process: the isolated Git
154	// environment, GIT_PROJECT_ROOT and the Git configuration of the request
155	// (GIT_CONFIG_COUNT/KEY/VALUE). The caller may append to it.
156	Env []string
157}
158
159// GitBackend provides the Git side of smart HTTP.
160type GitBackend interface {
161	// HTTPBackend returns how to run git http-backend. For a push it also
162	// enables http.receivepack and sets REMOTE_USER; a read request cannot
163	// reach receive-pack.
164	HTTPBackend(ctx context.Context, push bool) (HTTPBackend, error)
165}
166
167// TokenStore keeps the stored tokens and the index from secret hash to token.
168// The secret itself is never stored.
169type TokenStore interface {
170	// Create stores the token and its hash index in one write. It returns
171	// domain.ErrAlreadyExists if the id or the hash is taken.
172	Create(ctx context.Context, token domain.Token) error
173	// Get returns the token with that id, or domain.ErrNotFound.
174	Get(ctx context.Context, id string) (domain.Token, error)
175	// FindByHash returns the token whose secret has that hash, or
176	// domain.ErrNotFound.
177	FindByHash(ctx context.Context, hash domain.TokenHash) (domain.Token, error)
178	// List returns every token, oldest first.
179	List(ctx context.Context) ([]domain.Token, error)
180	// Revoke removes the token and its hash index in one write, or returns
181	// domain.ErrNotFound.
182	Revoke(ctx context.Context, id string) error
183	// Touch records that the token was used at the given time. It returns
184	// domain.ErrNotFound if the token is gone.
185	Touch(ctx context.Context, id string, at time.Time) error
186}
187
188// EventBus fans events out to in-process subscribers.
189type EventBus interface {
190	// Publish hands the event to every subscriber without blocking. A
191	// subscriber that cannot keep up loses events and receives a
192	// domain.EventGap.
193	Publish(ctx context.Context, ev domain.Event)
194	// Subscribe returns a channel of events published from now on. The
195	// channel is closed when ctx ends.
196	Subscribe(ctx context.Context) <-chan domain.Event
197}
198
199// Clock tells the time.
200type Clock interface {
201	Now() time.Time
202}

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 3

Tags 43

↑↓ moveenter openesc close