133728e barerepo 1mo
1
// Package transport writes chapters 11 and 18 once, because a rule on one transport is not a rule.
133728e barerepo 1mo
2
package transport
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"context"
133728e barerepo 1mo
6
"database/sql"
133728e barerepo 1mo
7
"errors"
133728e barerepo 1mo
8
"fmt"
133728e barerepo 1mo
9
"os"
133728e barerepo 1mo
10
"time"
133728e barerepo 1mo
12
"path/filepath"
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/config"
133728e barerepo 1mo
15
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
16
"github.com/barerepo/server/internal/repo"
133728e barerepo 1mo
17
"github.com/barerepo/server/internal/repocfg"
133728e barerepo 1mo
18
"github.com/barerepo/server/internal/store"
133728e barerepo 1mo
21
type Server struct {
133728e barerepo 1mo
22
Cfg config.Config
133728e barerepo 1mo
23
DB *store.DB
133728e barerepo 1mo
24
// Bin is the barerepo binary the hooks call. Empty means this executable.
133728e barerepo 1mo
25
Bin string
133728e barerepo 1mo
28
var (
133728e barerepo 1mo
29
// A reader without access gets this too, or "forbidden" would announce the private repository.
133728e barerepo 1mo
30
ErrNotFound = errors.New("no such repository")
133728e barerepo 1mo
31
ErrDenied = errors.New("denied")
133728e barerepo 1mo
34
// Redirect is a moved repository, kept forever, because a 404 breaks every clone. 44.2.
133728e barerepo 1mo
35
type Redirect struct{ Owner, Name string }
133728e barerepo 1mo
37
func (r Redirect) Error() string { return "moved to " + r.Owner + "/" + r.Name }
133728e barerepo 1mo
39
// Result is a resolved repository.
133728e barerepo 1mo
40
type Result struct {
133728e barerepo 1mo
41
Dir string
133728e barerepo 1mo
42
Owner string
133728e barerepo 1mo
43
Name string
133728e barerepo 1mo
44
Config repocfg.Config
133728e barerepo 1mo
45
Created bool // this call created it, per chapter 11
133728e barerepo 1mo
46
// Warning reaches the pusher's terminal, because a malformed config must not lock anyone out.
133728e barerepo 1mo
47
Warning string
133728e barerepo 1mo
50
// Intent exists because git over http splits a push in two, and only the second is a push.
133728e barerepo 1mo
51
type Intent int
133728e barerepo 1mo
53
const (
133728e barerepo 1mo
54
// Read is git-upload-pack: clone, fetch, ls-remote.
133728e barerepo 1mo
55
Read Intent = iota
133728e barerepo 1mo
56
// Announce must never create, or ls-remote and push --dry-run would claim names by accident.
133728e barerepo 1mo
57
Announce
133728e barerepo 1mo
58
// Write is the push itself, and is the only thing that may create.
133728e barerepo 1mo
59
Write
133728e barerepo 1mo
62
func (i Intent) write() bool { return i != Read }
133728e barerepo 1mo
64
// Open resolves a repository for account user, who is already authenticated.
133728e barerepo 1mo
65
func (s *Server) Open(ctx context.Context, owner, name, user string, intent Intent) (*Result, error) {
133728e barerepo 1mo
66
write := intent.write()
133728e barerepo 1mo
67
if to, err := s.redirect(ctx, owner, name); err != nil {
133728e barerepo 1mo
68
return nil, err
133728e barerepo 1mo
69
} else if to != nil {
133728e barerepo 1mo
70
return nil, *to
133728e barerepo 1mo
73
if !repo.Exists(s.Cfg.Paths.Repos, owner, name) {
133728e barerepo 1mo
74
switch intent {
133728e barerepo 1mo
75
case Read:
133728e barerepo 1mo
76
return nil, ErrNotFound
133728e barerepo 1mo
77
case Announce:
133728e barerepo 1mo
78
// Answer "no refs" instead of creating, and let the push that follows create it.
133728e barerepo 1mo
79
if err := s.mayCreate(ctx, owner, name, user); err != nil {
133728e barerepo 1mo
80
return nil, err
133728e barerepo 1mo
82
dir, err := s.emptyTemplate(ctx)
133728e barerepo 1mo
83
if err != nil {
133728e barerepo 1mo
84
return nil, err
133728e barerepo 1mo
86
return &Result{Dir: dir, Owner: owner, Name: name, Config: repocfg.Default()}, nil
133728e barerepo 1mo
88
return s.create(ctx, owner, name, user)
133728e barerepo 1mo
91
dir, err := repo.Dir(s.Cfg.Paths.Repos, owner, name)
133728e barerepo 1mo
92
if err != nil {
133728e barerepo 1mo
93
return nil, ErrNotFound
133728e barerepo 1mo
95
res := &Result{Dir: dir, Owner: owner, Name: name}
133728e barerepo 1mo
96
res.Config, err = repocfg.Load(ctx, dir)
133728e barerepo 1mo
97
if err != nil {
133728e barerepo 1mo
98
// Chapter 14: fall back and warn, so the file cannot lock the owner out of fixing it.
133728e barerepo 1mo
99
res.Warning = fmt.Sprintf("%s does not parse: %v", repocfg.Path, err)
133728e barerepo 1mo
100
if res.Config.FellBackTo != "" {
133728e barerepo 1mo
101
res.Warning += fmt.Sprintf("\nthe settings from %.7s are still in force", res.Config.FellBackTo)
133728e barerepo 1mo
102
} else {
133728e barerepo 1mo
103
res.Warning += "\nno earlier version parses either, so the defaults are in force"
133728e barerepo 1mo
104
}
133728e barerepo 1mo
105
}
133728e barerepo 1mo
107
if !res.Config.MayRead(owner, user) {
133728e barerepo 1mo
108
return nil, ErrNotFound
133728e barerepo 1mo
109
}
133728e barerepo 1mo
110
if write && res.Config.Repo.Archived && user != owner {
133728e barerepo 1mo
111
return nil, fmt.Errorf("%w: this repository is archived. the owner can unarchive it in %s",
133728e barerepo 1mo
112
ErrDenied, repocfg.Path)
133728e barerepo 1mo
113
}
133728e barerepo 1mo
114
// pre-receive decides which refs may be written, so reaching receive-pack grants nothing.
133728e barerepo 1mo
115
return res, nil
133728e barerepo 1mo
118
// mayCreate answers whether this push could create the repository, without creating it.
133728e barerepo 1mo
119
func (s *Server) mayCreate(ctx context.Context, owner, name, user string) error {
133728e barerepo 1mo
120
// Someone else's namespace answers the same whether the name is free or private, or the two messages list what alice keeps private. Chapter 18.
133728e barerepo 1mo
121
switch {
133728e barerepo 1mo
122
case user == "", user != owner:
133728e barerepo 1mo
123
return ErrNotFound
133728e barerepo 1mo
124
case !s.Cfg.Behavior.AllowPushToCreate:
133728e barerepo 1mo
125
return fmt.Errorf("%w: this server does not create repositories by push. make it at %s/new first",
133728e barerepo 1mo
126
ErrDenied, s.Cfg.Server.ExternalURL)
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
if _, err := s.DB.Account(ctx, owner); err != nil {
133728e barerepo 1mo
129
return ErrNotFound
133728e barerepo 1mo
130
}
133728e barerepo 1mo
131
if why := s.Claimed(ctx, owner, name); why != "" {
133728e barerepo 1mo
132
return fmt.Errorf("%w: %s", ErrDenied, why)
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
return nil
133728e barerepo 1mo
137
// Claimed says why a name cannot be taken, or nothing when it can. Chapters 21.2 and 44.4.
133728e barerepo 1mo
138
func (s *Server) Claimed(ctx context.Context, owner, name string) string {
133728e barerepo 1mo
139
// A renamed name is never freed, or the redirect points at a repository that is not the one. 21.2.
133728e barerepo 1mo
140
if to, err := s.redirect(ctx, owner, name); err == nil && to != nil {
133728e barerepo 1mo
141
return owner + "/" + name + " is now " + to.Owner + "/" + to.Name +
133728e barerepo 1mo
142
". the old name is kept forever, so nothing that points at it breaks."
133728e barerepo 1mo
143
}
133728e barerepo 1mo
144
// A deleted name is held while its data waits out the window, or a restore has nowhere to land.
133728e barerepo 1mo
145
if repo.InTrash(s.Cfg.Paths.Repos, owner, name) {
133728e barerepo 1mo
146
return owner + "/" + name + " was deleted. its name is held for 30 days, then it is free."
133728e barerepo 1mo
147
}
133728e barerepo 1mo
148
return ""
133728e barerepo 1mo
151
// emptyTemplate is one refless repository shared by every advertisement, so git states its own caps.
133728e barerepo 1mo
152
func (s *Server) emptyTemplate(ctx context.Context) (string, error) {
133728e barerepo 1mo
153
dir := filepath.Join(s.Cfg.Paths.Cache, "empty.git")
133728e barerepo 1mo
154
if _, err := os.Stat(filepath.Join(dir, "HEAD")); err == nil {
133728e barerepo 1mo
155
return dir, nil
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
if err := os.MkdirAll(s.Cfg.Paths.Cache, 0o750); err != nil {
133728e barerepo 1mo
158
return "", err
133728e barerepo 1mo
159
}
133728e barerepo 1mo
160
if _, err := gitx.Run(ctx, "", "init", "--bare", "--initial-branch", "master", "--", dir); err != nil {
133728e barerepo 1mo
161
return "", err
133728e barerepo 1mo
162
}
133728e barerepo 1mo
163
return dir, nil
133728e barerepo 1mo
166
// create implements push-to-create, chapter 11 and appendix D.
133728e barerepo 1mo
167
func (s *Server) create(ctx context.Context, owner, name, user string) (*Result, error) {
133728e barerepo 1mo
168
if err := s.mayCreate(ctx, owner, name, user); err != nil {
133728e barerepo 1mo
169
return nil, err
133728e barerepo 1mo
170
}
133728e barerepo 1mo
172
// HEAD is a placeholder until post-receive sees which branch actually arrived.
133728e barerepo 1mo
173
dir, err := repo.Create(ctx, s.Cfg.Paths.Repos, owner, name, "master", s.bin())
133728e barerepo 1mo
174
if err != nil {
133728e barerepo 1mo
175
return nil, err
133728e barerepo 1mo
176
}
133728e barerepo 1mo
177
if _, err := s.DB.ExecContext(ctx,
133728e barerepo 1mo
178
`INSERT INTO repos (owner, name, created_at) VALUES (?, ?, ?)`,
133728e barerepo 1mo
179
owner, name, time.Now().Unix()); err != nil {
133728e barerepo 1mo
180
os.RemoveAll(dir)
133728e barerepo 1mo
181
return nil, err
133728e barerepo 1mo
182
}
133728e barerepo 1mo
183
cfg := repocfg.Default()
133728e barerepo 1mo
184
// Chapter 11: publishing by accident is final, and hiding by accident is one config line.
133728e barerepo 1mo
185
cfg.Repo.Visibility = "private"
133728e barerepo 1mo
186
return &Result{Dir: dir, Owner: owner, Name: name, Config: cfg, Created: true}, nil
133728e barerepo 1mo
189
// redirect follows a rename or transfer. Chapter 44.2.
133728e barerepo 1mo
190
func (s *Server) redirect(ctx context.Context, owner, name string) (*Redirect, error) {
133728e barerepo 1mo
191
var to Redirect
133728e barerepo 1mo
192
err := s.DB.QueryRowContext(ctx,
133728e barerepo 1mo
193
`SELECT new_owner, new_name FROM redirects WHERE old_owner = ? AND old_name = ?`,
133728e barerepo 1mo
194
owner, name).Scan(&to.Owner, &to.Name)
133728e barerepo 1mo
195
if errors.Is(err, sql.ErrNoRows) {
133728e barerepo 1mo
196
return nil, nil
133728e barerepo 1mo
197
}
133728e barerepo 1mo
198
if err != nil {
133728e barerepo 1mo
199
return nil, err
133728e barerepo 1mo
200
}
133728e barerepo 1mo
201
return &to, nil
133728e barerepo 1mo
204
func (s *Server) bin() string {
133728e barerepo 1mo
205
if s.Bin != "" {
133728e barerepo 1mo
206
return s.Bin
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
if p, err := os.Executable(); err == nil {
133728e barerepo 1mo
209
return p
133728e barerepo 1mo
210
}
133728e barerepo 1mo
211
return "barerepo"