File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/repo/repo.go
345 lines · 10kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
// Package repo turns an owner and a name into a bare repository, owned by the URL. Chapter 11.
133728e barerepo 1mo
2
package repo
133728e barerepo 1mo
3
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"context"
133728e barerepo 1mo
6
"errors"
133728e barerepo 1mo
7
"fmt"
133728e barerepo 1mo
8
"os"
133728e barerepo 1mo
9
"path/filepath"
133728e barerepo 1mo
10
"strconv"
133728e barerepo 1mo
11
"strings"
133728e barerepo 1mo
12
"time"
133728e barerepo 1mo
13
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
15
)
133728e barerepo 1mo
16
133728e barerepo 1mo
17
// Dir resolves a path under root, validating the names and the result, because either alone has failed.
133728e barerepo 1mo
18
func Dir(root, owner, name string) (string, error) {
133728e barerepo 1mo
19
// The trash is a sibling of the accounts under root, so an account named trash would own repositories Walk skips and the window erases. 44.4.
133728e barerepo 1mo
20
if !gitx.ValidName(owner) || !gitx.ValidRepoName(name) || owner == TrashDir {
133728e barerepo 1mo
21
return "", fmt.Errorf("no such repository")
133728e barerepo 1mo
22
}
133728e barerepo 1mo
23
absRoot, err := filepath.Abs(root)
133728e barerepo 1mo
24
if err != nil {
133728e barerepo 1mo
25
return "", err
133728e barerepo 1mo
26
}
133728e barerepo 1mo
27
dir := filepath.Join(absRoot, owner, name+".git")
133728e barerepo 1mo
28
if !strings.HasPrefix(dir, absRoot+string(os.PathSeparator)) {
133728e barerepo 1mo
29
return "", fmt.Errorf("no such repository")
133728e barerepo 1mo
30
}
133728e barerepo 1mo
31
return dir, nil
133728e barerepo 1mo
32
}
133728e barerepo 1mo
33
133728e barerepo 1mo
34
// Exists reports whether the repository is on disk.
133728e barerepo 1mo
35
func Exists(root, owner, name string) bool {
133728e barerepo 1mo
36
dir, err := Dir(root, owner, name)
133728e barerepo 1mo
37
if err != nil {
133728e barerepo 1mo
38
return false
133728e barerepo 1mo
39
}
133728e barerepo 1mo
40
st, err := os.Stat(filepath.Join(dir, "HEAD"))
133728e barerepo 1mo
41
return err == nil && st.Mode().IsRegular()
133728e barerepo 1mo
42
}
133728e barerepo 1mo
43
133728e barerepo 1mo
44
// Create makes the bare repository and the hooks, and no commit, because empty means empty.
133728e barerepo 1mo
45
func Create(ctx context.Context, root, owner, name, defaultBranch, serverBin string) (string, error) {
133728e barerepo 1mo
46
dir, err := Dir(root, owner, name)
133728e barerepo 1mo
47
if err != nil {
133728e barerepo 1mo
48
return "", err
133728e barerepo 1mo
49
}
133728e barerepo 1mo
50
if Exists(root, owner, name) {
133728e barerepo 1mo
51
return "", errors.New("that repository already exists")
133728e barerepo 1mo
52
}
133728e barerepo 1mo
53
if defaultBranch == "" {
133728e barerepo 1mo
54
defaultBranch = "master"
133728e barerepo 1mo
55
}
133728e barerepo 1mo
56
if !gitx.ValidRef("refs/heads/" + defaultBranch) {
133728e barerepo 1mo
57
return "", fmt.Errorf("%q is not a usable branch name", defaultBranch)
133728e barerepo 1mo
58
}
133728e barerepo 1mo
59
if err := os.MkdirAll(filepath.Dir(dir), 0o750); err != nil {
133728e barerepo 1mo
60
return "", err
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
// An explicit initial branch, because git's default is a local setting to not inherit.
133728e barerepo 1mo
63
if _, err := gitx.Run(ctx, "", "init", "--bare", "--initial-branch", defaultBranch, "--", dir); err != nil {
133728e barerepo 1mo
64
os.RemoveAll(dir)
133728e barerepo 1mo
65
return "", err
133728e barerepo 1mo
66
}
133728e barerepo 1mo
67
if _, err := gitx.Run(ctx, dir, "symbolic-ref", "HEAD", "refs/heads/"+defaultBranch); err != nil {
133728e barerepo 1mo
68
os.RemoveAll(dir)
133728e barerepo 1mo
69
return "", err
133728e barerepo 1mo
70
}
133728e barerepo 1mo
71
if err := InstallHooks(dir, serverBin); err != nil {
133728e barerepo 1mo
72
os.RemoveAll(dir)
133728e barerepo 1mo
73
return "", err
133728e barerepo 1mo
74
}
133728e barerepo 1mo
75
return dir, nil
133728e barerepo 1mo
76
}
133728e barerepo 1mo
77
133728e barerepo 1mo
78
// hooks are two lines each, so upgrading the binary upgrades every repository at once.
133728e barerepo 1mo
79
var hooks = []string{"pre-receive", "post-receive", "proc-receive"}
133728e barerepo 1mo
80
133728e barerepo 1mo
81
// InstallHooks replaces the scripts and deletes the samples, so the directory holds what runs.
133728e barerepo 1mo
82
func InstallHooks(dir, serverBin string) error {
133728e barerepo 1mo
83
hookDir := filepath.Join(dir, "hooks")
133728e barerepo 1mo
84
if err := os.MkdirAll(hookDir, 0o750); err != nil {
133728e barerepo 1mo
85
return err
133728e barerepo 1mo
86
}
133728e barerepo 1mo
87
entries, err := os.ReadDir(hookDir)
133728e barerepo 1mo
88
if err != nil {
133728e barerepo 1mo
89
return err
133728e barerepo 1mo
90
}
133728e barerepo 1mo
91
for _, e := range entries {
133728e barerepo 1mo
92
if strings.HasSuffix(e.Name(), ".sample") || e.Name() == "update" {
133728e barerepo 1mo
93
os.Remove(filepath.Join(hookDir, e.Name()))
133728e barerepo 1mo
94
}
133728e barerepo 1mo
95
}
133728e barerepo 1mo
96
if serverBin == "" {
133728e barerepo 1mo
97
serverBin, err = os.Executable()
133728e barerepo 1mo
98
if err != nil {
133728e barerepo 1mo
99
return err
133728e barerepo 1mo
100
}
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
for _, h := range hooks {
133728e barerepo 1mo
103
body := fmt.Sprintf("#!/bin/sh\nexec %s hook %s\n", serverBin, h)
133728e barerepo 1mo
104
path := filepath.Join(hookDir, h)
133728e barerepo 1mo
105
tmp := path + ".tmp"
133728e barerepo 1mo
106
if err := os.WriteFile(tmp, []byte(body), 0o750); err != nil {
133728e barerepo 1mo
107
return err
133728e barerepo 1mo
108
}
133728e barerepo 1mo
109
if err := os.Rename(tmp, path); err != nil {
133728e barerepo 1mo
110
return err
133728e barerepo 1mo
111
}
133728e barerepo 1mo
112
}
133728e barerepo 1mo
113
// A prefix, not a glob, and replaced not added, or reinstalling stacks copies of it.
133728e barerepo 1mo
114
if _, err := gitx.Run(context.Background(), dir,
133728e barerepo 1mo
115
"config", "--replace-all", "receive.procReceiveRefs", "refs/proposals"); err != nil {
133728e barerepo 1mo
116
return err
133728e barerepo 1mo
117
}
133728e barerepo 1mo
118
return nil
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
133728e barerepo 1mo
121
// HeadBranch reads HEAD every time, because assuming master or main is wrong for half the world.
133728e barerepo 1mo
122
func HeadBranch(ctx context.Context, dir string) (string, error) {
133728e barerepo 1mo
123
// HEAD is a file, and every repository page needs this, so do not spend a process on it.
133728e barerepo 1mo
124
if name, err := gitx.HeadBranch(dir); err == nil {
133728e barerepo 1mo
125
return name, nil
133728e barerepo 1mo
126
}
133728e barerepo 1mo
127
out, err := gitx.Run(ctx, dir, "symbolic-ref", "--short", "HEAD")
133728e barerepo 1mo
128
if err != nil {
133728e barerepo 1mo
129
return "", err
133728e barerepo 1mo
130
}
133728e barerepo 1mo
131
return strings.TrimSpace(out), nil
133728e barerepo 1mo
132
}
133728e barerepo 1mo
133
133728e barerepo 1mo
134
// IsEmpty reports whether the repository has no refs yet.
133728e barerepo 1mo
135
func IsEmpty(ctx context.Context, dir string) bool {
133728e barerepo 1mo
136
return !gitx.AnyRef(dir)
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
133728e barerepo 1mo
139
// Move relocates the directory and rewrites nothing inside it, because that is not a transfer. 44.3.
133728e barerepo 1mo
140
func Move(root, oldOwner, oldName, newOwner, newName string) error {
133728e barerepo 1mo
141
from, err := Dir(root, oldOwner, oldName)
133728e barerepo 1mo
142
if err != nil {
133728e barerepo 1mo
143
return err
133728e barerepo 1mo
144
}
133728e barerepo 1mo
145
to, err := Dir(root, newOwner, newName)
133728e barerepo 1mo
146
if err != nil {
133728e barerepo 1mo
147
return err
133728e barerepo 1mo
148
}
133728e barerepo 1mo
149
if _, err := os.Stat(to); err == nil {
133728e barerepo 1mo
150
return fmt.Errorf("%s/%s already exists on disk", newOwner, newName)
133728e barerepo 1mo
151
}
133728e barerepo 1mo
152
if err := os.MkdirAll(filepath.Dir(to), 0o750); err != nil {
133728e barerepo 1mo
153
return err
133728e barerepo 1mo
154
}
133728e barerepo 1mo
155
return os.Rename(from, to)
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
133728e barerepo 1mo
158
// TrashDir is where a delete waits, because barerepo has no channel to answer a support request. 44.4.
133728e barerepo 1mo
159
const TrashDir = "trash"
133728e barerepo 1mo
160
133728e barerepo 1mo
161
// Trash moves a repository out of service, keeping it for the window.
133728e barerepo 1mo
162
func Trash(root, owner, name string, at time.Time) (string, error) {
133728e barerepo 1mo
163
from, err := Dir(root, owner, name)
133728e barerepo 1mo
164
if err != nil {
133728e barerepo 1mo
165
return "", err
133728e barerepo 1mo
166
}
133728e barerepo 1mo
167
absRoot, err := filepath.Abs(root)
133728e barerepo 1mo
168
if err != nil {
133728e barerepo 1mo
169
return "", err
133728e barerepo 1mo
170
}
133728e barerepo 1mo
171
to := filepath.Join(absRoot, TrashDir,
133728e barerepo 1mo
172
fmt.Sprintf("%d-%s-%s.git", at.Unix(), owner, name))
133728e barerepo 1mo
173
if err := os.MkdirAll(filepath.Dir(to), 0o750); err != nil {
133728e barerepo 1mo
174
return "", err
133728e barerepo 1mo
175
}
133728e barerepo 1mo
176
return to, os.Rename(from, to)
133728e barerepo 1mo
177
}
133728e barerepo 1mo
178
133728e barerepo 1mo
179
// EmptyTrash erases what is past the window, which chapter 44.4 puts at 30 days.
133728e barerepo 1mo
180
func EmptyTrash(root string, olderThan time.Duration, now time.Time) (int, error) {
133728e barerepo 1mo
181
dir := filepath.Join(root, TrashDir)
133728e barerepo 1mo
182
entries, err := os.ReadDir(dir)
133728e barerepo 1mo
183
if err != nil {
133728e barerepo 1mo
184
return 0, nil
133728e barerepo 1mo
185
}
133728e barerepo 1mo
186
cutoff := now.Add(-olderThan).Unix()
133728e barerepo 1mo
187
erased := 0
133728e barerepo 1mo
188
for _, e := range entries {
133728e barerepo 1mo
189
stamp, _, ok := strings.Cut(e.Name(), "-")
133728e barerepo 1mo
190
if !ok {
133728e barerepo 1mo
191
continue
133728e barerepo 1mo
192
}
133728e barerepo 1mo
193
at, err := strconv.ParseInt(stamp, 10, 64)
133728e barerepo 1mo
194
if err != nil || at >= cutoff {
133728e barerepo 1mo
195
continue
133728e barerepo 1mo
196
}
133728e barerepo 1mo
197
if err := os.RemoveAll(filepath.Join(dir, e.Name())); err != nil {
133728e barerepo 1mo
198
return erased, err
133728e barerepo 1mo
199
}
133728e barerepo 1mo
200
erased++
133728e barerepo 1mo
201
}
133728e barerepo 1mo
202
return erased, nil
133728e barerepo 1mo
203
}
133728e barerepo 1mo
204
133728e barerepo 1mo
205
// InTrash reports a name still in its window, where a push fails rather than creates. Appendix D.
133728e barerepo 1mo
206
func InTrash(root, owner, name string) bool {
133728e barerepo 1mo
207
entries, err := os.ReadDir(filepath.Join(root, TrashDir))
133728e barerepo 1mo
208
if err != nil {
133728e barerepo 1mo
209
return false
133728e barerepo 1mo
210
}
133728e barerepo 1mo
211
// A hyphen is a name character, so a suffix match reads evil/my-bot-x as bot/x and holds a name nobody deleted, and only the stamp varies.
133728e barerepo 1mo
212
want := owner + "-" + name + ".git"
133728e barerepo 1mo
213
for _, e := range entries {
133728e barerepo 1mo
214
if _, rest, ok := strings.Cut(e.Name(), "-"); ok && rest == want {
133728e barerepo 1mo
215
return true
133728e barerepo 1mo
216
}
133728e barerepo 1mo
217
}
133728e barerepo 1mo
218
return false
218 of 345 lines · the whole file
history · rawbarerepo 0.1.0