~bigbes/sr-ht-spec

ref: b6931bdad118348d1e174299d960b1d0a6ee2189 sr-ht-spec/service/space.go -rw-r--r-- 5.6 KiB
b6931bda — Eugene Blikh build: package spec.sr.ht as an apk and wire push->build->mirror 26 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package service

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
	"sourcecraft.dev/bigbes/sr-ht-spec/db"
	"sourcecraft.dev/bigbes/sr-ht-spec/gitx"
)

// Space is one space, resolved: its reference, its row and its repository.
//
// The repository is the space — "spaces exist as repos" — and the row is
// bookkeeping that makes listing and index staleness cheap. ID is the row's
// primary key, which every other table references the space by; it is zero only
// for a space whose repository exists but whose row does not, a state
// CreateSpace is written to avoid and OpenSpace refuses to invent.
type Space struct {
	Ref     core.SpaceRef
	ID      int
	Created time.Time
	Repo    *gitx.Repo
}

// ApprovedBranch is the branch a document must be reachable from to be
// approved. Read from the repository's HEAD, so there is exactly one place a
// space records it.
func (sp *Space) ApprovedBranch() string { return sp.Repo.ApprovedBranch() }

// CreateSpace creates a space: the bare repository first, then the row.
//
// The order is not arbitrary and it is the opposite of the proposal path's.
// A proposal must be row-first because its branch name derives from the row's
// serial id; a space has no such dependency, and git is authoritative for
// content, so the repository leads:
//
//   - repository, then row — a crash in between leaves content on disk that is
//     merely unlisted, and owner and name are recoverable from the directory
//     name alone.
//   - row, then repository — a crash in between leaves a phantom space that
//     lists fine and 404s on every read, and db/ exposes no way to delete it.
//
// If the row insert fails, the repository this call just created is removed
// again and the insert's error is returned. That is safe precisely because it
// is seconds old, empty apart from gitx's initial commit, and named nowhere
// yet: nothing can have pushed to it. A cleanup failure is reported alongside
// the original error rather than swallowed.
func (s *Service) CreateSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) {
	if err := core.ValidateOwner(ref.Owner); err != nil {
		return nil, err
	}
	if err := core.ValidateSpaceName(ref.Name); err != nil {
		return nil, err
	}

	repo, err := gitx.Create(ctx, s.cfg.Repos, ref, gitx.CreateOptions{
		Owner: gitx.Signature{
			Name:  s.cfg.Instance.OwnerName,
			Email: s.cfg.Instance.OwnerEmail,
			When:  s.now().UTC(),
		},
	})
	if err != nil {
		if errors.Is(err, gitx.ErrExists) {
			return nil, fmt.Errorf("%w: %w", ErrSpaceExists, err)
		}
		return nil, fmt.Errorf("service: create repository for %s: %w", ref, err)
	}

	row, err := s.store.CreateSpace(ctx, ref)
	if err != nil {
		rmErr := os.RemoveAll(repo.Dir())
		if rmErr != nil {
			return nil, fmt.Errorf("service: create row for %s: %w "+
				"(the repository at %s could not be removed either: %v — remove it by hand before retrying)",
				ref, err, repo.Dir(), rmErr)
		}
		if errors.Is(err, db.ErrSpaceExists) {
			return nil, fmt.Errorf("%w: %w", ErrSpaceExists, err)
		}
		return nil, fmt.Errorf("service: create row for %s: %w", ref, err)
	}

	return &Space{Ref: ref, ID: row.ID, Created: row.Created, Repo: repo}, nil
}

// OpenSpace resolves a space by reference: its row and its repository, both
// required.
//
// A row with no repository, or a repository with no row, is a half-created
// space rather than a space, and is reported as such. Neither half is invented:
// serving reads from a repository with no row would give every document a
// space_id of zero in the index, and returning a row whose repository is
// missing would answer "the space exists" to every question and fail on each
// individual document.
func (s *Service) OpenSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) {
	row, err := s.store.GetSpace(ctx, ref)
	if err != nil {
		if errors.Is(err, db.ErrNotFound) {
			return nil, fmt.Errorf("%w: space %s", ErrNotFound, ref)
		}
		return nil, fmt.Errorf("service: look up space %s: %w", ref, err)
	}
	repo, err := s.openRepo(ref)
	if err != nil {
		return nil, err
	}
	return &Space{Ref: ref, ID: row.ID, Created: row.Created, Repo: repo}, nil
}

// openRepo opens a space's bare repository, mapping gitx's absence onto this
// package's. Split out because the reconciler resolves repositories for spaces
// it already has rows for.
func (s *Service) openRepo(ref core.SpaceRef) (*gitx.Repo, error) {
	repo, err := gitx.Open(s.cfg.Repos, ref)
	if err != nil {
		if errors.Is(err, gitx.ErrNotFound) {
			return nil, fmt.Errorf("%w: repository for space %s at %s",
				ErrNotFound, ref, gitx.DiskPath(s.cfg.Repos, ref))
		}
		return nil, fmt.Errorf("service: open repository for %s: %w", ref, err)
	}
	return repo, nil
}

// ListSpaces returns every space, ordered by owner then name.
//
// There is one human on this instance and no visibility levels, so there is
// nothing to filter by — the list is the whole corpus, which is also exactly
// what the meta-project (a filter that excludes nothing) needs.
//
// Repositories are not opened. Listing is a page of names, and opening N bare
// repositories to render it would make the cheapest view in the service the
// most expensive; callers that need a repository call OpenSpace. Space.Repo is
// therefore nil in every element returned here.
func (s *Service) ListSpaces(ctx context.Context) ([]*Space, error) {
	rows, err := s.store.ListSpaces(ctx)
	if err != nil {
		return nil, fmt.Errorf("service: list spaces: %w", err)
	}
	out := make([]*Space, 0, len(rows))
	for _, row := range rows {
		out = append(out, &Space{Ref: row.Ref, ID: row.ID, Created: row.Created})
	}
	return out, nil
}