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}