v0.30.0 c899179

mirror.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ports
4
5import (
6	"bnema.dev/ermite/internal/domain"
7	"context"
8	"crypto/ed25519"
9	"time"
10)
11
12// MirrorStore keeps the mirror configuration, state and snapshots (kvstore implements it).
13type MirrorStore interface {
14	GetGlobal(ctx context.Context) (domain.MirrorGlobalConfig, error) // zero value, nil error when unset
15	PutGlobal(ctx context.Context, g domain.MirrorGlobalConfig) error
16	GetRepoConfig(ctx context.Context, repo domain.RepoName) (domain.MirrorRepoConfig, error) // domain.ErrNotFound when none
17	PutRepoConfig(ctx context.Context, c domain.MirrorRepoConfig) error                       // ErrNotFound when the repository is gone (guardRepoKey)
18	DeleteRepoConfig(ctx context.Context, repo domain.RepoName) error                         // missing is not an error
19	ListRepoConfigs(ctx context.Context) ([]domain.MirrorRepoConfig, error)                   // by repo name; with ListStatus, what ListStatus of the service reads
20	GetStatus(ctx context.Context, repo domain.RepoName) (domain.MirrorStatus, error)         // ErrNotFound when none
21	PutStatus(ctx context.Context, s domain.MirrorStatus) error
22	ListStatus(ctx context.Context) ([]domain.MirrorStatus, error)                        // stored statuses only
23	GetSnapshot(ctx context.Context, repo domain.RepoName) (domain.MirrorSnapshot, error) // ErrNotFound when none
24	PutSnapshot(ctx context.Context, repo domain.RepoName, s domain.MirrorSnapshot) error
25}
26
27// MirrorKeyStore keeps the sealed seed of the mirror key. The use case seals/opens it with
28// SecretSealer (AAD domain.MirrorKeyPath), as OIDC does.
29type MirrorKeyStore interface {
30	// GetMirrorKey returns the sealed seed and its creation time, or ErrNotFound.
31	GetMirrorKey(ctx context.Context) (sealed []byte, createdAt time.Time, err error)
32	// PutMirrorKey replaces the key; ifAbsent makes it fail with ErrAlreadyExists when one exists.
33	PutMirrorKey(ctx context.Context, sealed []byte, createdAt time.Time, ifAbsent bool) error
34}
35
36// MirrorPush is one push request to the transport.
37type MirrorPush struct {
38	Repo       domain.RepoName
39	URL        string
40	Refspecs   []string           // "<ref>:<ref>", no "+", from domain.MirrorRefspecs
41	PrivateKey ed25519.PrivateKey // written to a 0600 file for the duration of the push
42}
43type MirrorRefStatus string
44
45const (
46	MirrorRefOK       MirrorRefStatus = "ok"         // porcelain ' ' or '*'
47	MirrorRefUpToDate MirrorRefStatus = "up_to_date" // '='
48	MirrorRefRejected MirrorRefStatus = "rejected"   // '!'
49)
50
51type MirrorRefOutcome struct {
52	Ref    string
53	Status MirrorRefStatus
54	Reason string
55}
56type MirrorPushResult struct {
57	Refs     []MirrorRefOutcome
58	Duration time.Duration
59}
60
61// MirrorPusher runs `git push --porcelain` (gitpush implements it). A failed push returns a
62// *domain.MirrorError (classified); ctx cancellation is returned as is. A result with a rejected
63// ref and err == nil never happens: a rejection is a MirrorError of class diverged.
64type MirrorPusher interface {
65	Push(ctx context.Context, req MirrorPush) (MirrorPushResult, error)
66}
67
68// MirrorTrigger is what Pushes.Record tells the mirrors. *usecase.Mirrors implements it.
69type MirrorTrigger interface {
70	OnPush(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate) // never blocks
71}
72
73// MirrorWarner is what the post-receive hook and the web ask. *usecase.Mirrors implements it.
74type MirrorWarner interface {
75	// Warning is non-empty when the mirror of repo is failing or paused ("mirror to github.com:x/y is paused: <reason>")
76	// and p has repo:admin on repo: the remote and the reason are for admins only.
77	Warning(ctx context.Context, p domain.Principal, repo domain.RepoName) string
78	// Badge is the state and reason for a viewer: reason is "" unless p may administer the repository.
79	Badge(ctx context.Context, p domain.Principal, repo domain.RepoName) (domain.MirrorState, string)
80}
81type MirrorSyncResult struct {
82	Repo   domain.RepoName
83	Status domain.MirrorStatus
84	Queued bool
85	NoOp   bool
86	Err    error // SyncAll only: why this repository failed; the other repositories still ran
87}
88
89// MirrorKeyRotation is the outcome of RotateKey; Previous is zero when no key existed.
90type MirrorKeyRotation struct {
91	Key      domain.MirrorPublicKey
92	Previous domain.MirrorPublicKey
93}
94
95// MirrorService is the inbound port of the API. Authorization: key show needs a non-anonymous
96// principal; key rotate, global config and ListStatus need HasOwnerRights(); per-repo config and
97// Sync need Allows(ScopeRepoAdmin, repo); GetStatus needs read access to the repository.
98type MirrorService interface {
99	ShowKey(ctx context.Context, p domain.Principal) (domain.MirrorPublicKey, error) // generates lazily
100	RotateKey(ctx context.Context, p domain.Principal) (MirrorKeyRotation, error)
101	GetGlobalConfig(ctx context.Context, p domain.Principal) (domain.MirrorGlobalConfig, error)
102	SetGlobalConfig(ctx context.Context, p domain.Principal, patch MirrorGlobalPatch) (domain.MirrorGlobalConfig, error)
103	SetRepoMirror(ctx context.Context, p domain.Principal, c domain.MirrorRepoConfig) (domain.MirrorTarget, error)
104	RemoveRepoMirror(ctx context.Context, p domain.Principal, repo domain.RepoName) error
105	SetRepoExcluded(ctx context.Context, p domain.Principal, repo domain.RepoName, excluded bool) (domain.MirrorTarget, error)
106	GetStatus(ctx context.Context, p domain.Principal, repo domain.RepoName) (MirrorStatusView, error)
107	ListStatus(ctx context.Context, p domain.Principal) ([]MirrorStatusView, error)
108	Sync(ctx context.Context, p domain.Principal, repo domain.RepoName, force bool) (MirrorSyncResult, error)
109	// SyncAll skips disabled mirrors and continues past failures, reported per repository in Err.
110	SyncAll(ctx context.Context, p domain.Principal, force bool) ([]MirrorSyncResult, error)
111}
112type MirrorGlobalPatch struct {
113	Enabled     *bool
114	URLTemplate *string
115	Cron        *string
116	AllBranches *bool
117}
118type MirrorStatusView struct {
119	Target    domain.MirrorTarget
120	Status    domain.MirrorStatus
121	NextRunAt time.Time
122}

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