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

barerepo / server / internal/hook/hook.go
854 lines · 29kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
// Package hook is what git runs around a push, and its stderr is the pusher's terminal.
133728e barerepo 1mo
2
package hook
133728e barerepo 1mo
3
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"bufio"
133728e barerepo 1mo
6
"bytes"
133728e barerepo 1mo
7
"context"
133728e barerepo 1mo
8
"errors"
133728e barerepo 1mo
9
"fmt"
133728e barerepo 1mo
10
"io"
133728e barerepo 1mo
11
"net/url"
133728e barerepo 1mo
12
"os"
133728e barerepo 1mo
13
"strings"
133728e barerepo 1mo
14
"time"
133728e barerepo 1mo
15
133728e barerepo 1mo
16
"github.com/barerepo/server/internal/config"
133728e barerepo 1mo
17
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
18
"github.com/barerepo/server/internal/proposal"
133728e barerepo 1mo
19
"github.com/barerepo/server/internal/repo"
133728e barerepo 1mo
20
"github.com/barerepo/server/internal/repocfg"
133728e barerepo 1mo
21
"github.com/barerepo/server/internal/run"
133728e barerepo 1mo
22
"github.com/barerepo/server/internal/search"
133728e barerepo 1mo
23
"github.com/barerepo/server/internal/store"
133728e barerepo 1mo
24
"github.com/barerepo/server/internal/thread"
133728e barerepo 1mo
25
"github.com/barerepo/server/internal/workflow"
133728e barerepo 1mo
26
)
133728e barerepo 1mo
27
133728e barerepo 1mo
28
// ErrRejected means the reason is already in the pusher's terminal, so the caller adds nothing.
133728e barerepo 1mo
29
var ErrRejected = errors.New("rejected")
133728e barerepo 1mo
30
133728e barerepo 1mo
31
// reject explains and refuses, in that order.
133728e barerepo 1mo
32
func reject(out io.Writer, format string, args ...any) error {
133728e barerepo 1mo
33
fmt.Fprintf(out, format+"\n", args...)
133728e barerepo 1mo
34
return ErrRejected
133728e barerepo 1mo
35
}
133728e barerepo 1mo
36
133728e barerepo 1mo
37
// Update is one line of a hook's stdin: <old-sha> <new-sha> <refname>.
133728e barerepo 1mo
38
type Update struct {
133728e barerepo 1mo
39
Old, New, Ref string
133728e barerepo 1mo
40
}
133728e barerepo 1mo
41
133728e barerepo 1mo
42
// Zero is the all-zero object id git uses for "did not exist" and "deleted".
133728e barerepo 1mo
43
const Zero = "0000000000000000000000000000000000000000"
133728e barerepo 1mo
44
133728e barerepo 1mo
45
func (u Update) Creating() bool { return strings.Trim(u.Old, "0") == "" }
133728e barerepo 1mo
46
func (u Update) Deleting() bool { return strings.Trim(u.New, "0") == "" }
133728e barerepo 1mo
47
133728e barerepo 1mo
48
// Env is what ssh and http set before git-receive-pack, and git passes it to the hooks.
133728e barerepo 1mo
49
type Env struct {
133728e barerepo 1mo
50
Account string // who is pushing. empty is impossible on a write.
133728e barerepo 1mo
51
Owner string
133728e barerepo 1mo
52
Name string
133728e barerepo 1mo
53
Dir string
133728e barerepo 1mo
54
URL string // where this repository is on the web
133728e barerepo 1mo
55
Created bool // this push created the repository
133728e barerepo 1mo
56
// Config is where the server read its configuration, which a stripped hook cannot find alone.
133728e barerepo 1mo
57
Config string
133728e barerepo 1mo
58
}
133728e barerepo 1mo
59
133728e barerepo 1mo
60
func EnvFromOS() Env {
133728e barerepo 1mo
61
dir, _ := os.Getwd()
133728e barerepo 1mo
62
return Env{
133728e barerepo 1mo
63
Account: os.Getenv("BAREREPO_ACCOUNT"),
133728e barerepo 1mo
64
Owner: os.Getenv("BAREREPO_OWNER"),
133728e barerepo 1mo
65
Name: os.Getenv("BAREREPO_NAME"),
133728e barerepo 1mo
66
URL: os.Getenv("BAREREPO_URL"),
133728e barerepo 1mo
67
Created: os.Getenv("BAREREPO_CREATED") == "1",
133728e barerepo 1mo
68
Config: os.Getenv("BAREREPO_CONFIG"),
133728e barerepo 1mo
69
Dir: dir,
133728e barerepo 1mo
70
}
133728e barerepo 1mo
71
}
133728e barerepo 1mo
72
133728e barerepo 1mo
73
// Vars renders an Env for exec.Cmd. git passes these through to the hooks.
133728e barerepo 1mo
74
func (e Env) Vars() []string {
133728e barerepo 1mo
75
created := "0"
133728e barerepo 1mo
76
if e.Created {
133728e barerepo 1mo
77
created = "1"
133728e barerepo 1mo
78
}
133728e barerepo 1mo
79
return []string{
133728e barerepo 1mo
80
"BAREREPO_ACCOUNT=" + e.Account,
133728e barerepo 1mo
81
"BAREREPO_OWNER=" + e.Owner,
133728e barerepo 1mo
82
"BAREREPO_NAME=" + e.Name,
133728e barerepo 1mo
83
"BAREREPO_URL=" + e.URL,
133728e barerepo 1mo
84
"BAREREPO_CREATED=" + created,
133728e barerepo 1mo
85
"BAREREPO_CONFIG=" + e.Config,
133728e barerepo 1mo
86
}
133728e barerepo 1mo
87
}
133728e barerepo 1mo
88
133728e barerepo 1mo
89
// ReadUpdates parses a hook's stdin.
133728e barerepo 1mo
90
func ReadUpdates(r io.Reader) ([]Update, error) {
133728e barerepo 1mo
91
var out []Update
133728e barerepo 1mo
92
sc := bufio.NewScanner(r)
133728e barerepo 1mo
93
for sc.Scan() {
133728e barerepo 1mo
94
f := strings.Fields(sc.Text())
133728e barerepo 1mo
95
if len(f) != 3 {
133728e barerepo 1mo
96
return nil, fmt.Errorf("malformed hook input")
133728e barerepo 1mo
97
}
133728e barerepo 1mo
98
out = append(out, Update{Old: f[0], New: f[1], Ref: f[2]})
133728e barerepo 1mo
99
}
133728e barerepo 1mo
100
return out, sc.Err()
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
133728e barerepo 1mo
103
// PreReceive decides the whole push, all or nothing, because a half push is harder to reason about.
133728e barerepo 1mo
104
func PreReceive(ctx context.Context, e Env, ups []Update, out io.Writer) error {
133728e barerepo 1mo
105
// Chapter 11: typos create repositories, so print the URL where the mistake is visible.
133728e barerepo 1mo
106
if e.Created {
133728e barerepo 1mo
107
fmt.Fprintf(out, "\ncreated %s/%s. it is private.\n", e.Owner, e.Name)
133728e barerepo 1mo
108
if e.URL != "" {
133728e barerepo 1mo
109
fmt.Fprintf(out, " %s\n", e.URL)
133728e barerepo 1mo
110
}
133728e barerepo 1mo
111
fmt.Fprintf(out, " if that name is a typo, delete it on the config page.\n\n")
133728e barerepo 1mo
112
}
133728e barerepo 1mo
113
133728e barerepo 1mo
114
cfg, cfgErr := repocfg.Load(ctx, e.Dir)
133728e barerepo 1mo
115
if cfgErr != nil {
133728e barerepo 1mo
116
// A malformed file must not lock anyone out. Chapter 14.
133728e barerepo 1mo
117
fmt.Fprintf(out, "warning: %s does not parse\n", repocfg.Path)
133728e barerepo 1mo
118
fmt.Fprintf(out, "warning: %v\n", cfgErr)
133728e barerepo 1mo
119
if cfg.FellBackTo != "" {
133728e barerepo 1mo
120
fmt.Fprintf(out, "warning: the settings from %s are still in force\n", short(cfg.FellBackTo))
133728e barerepo 1mo
121
} else {
133728e barerepo 1mo
122
fmt.Fprintf(out, "warning: no earlier version parses either, so the defaults are in force\n")
133728e barerepo 1mo
123
}
133728e barerepo 1mo
124
}
133728e barerepo 1mo
125
133728e barerepo 1mo
126
// Judged once, and the owner is exempt because unarchiving arrives by push. Chapter 21.3.
133728e barerepo 1mo
127
if cfg.Repo.Archived && e.Account != e.Owner {
133728e barerepo 1mo
128
return reject(out, "this repository is archived.\nthe owner can unarchive it in %s", repocfg.Path)
133728e barerepo 1mo
129
}
133728e barerepo 1mo
130
133728e barerepo 1mo
131
head, err := repo.HeadBranch(ctx, e.Dir)
133728e barerepo 1mo
132
if err != nil {
133728e barerepo 1mo
133
// An empty repository's HEAD points nowhere until the first push gives it something.
133728e barerepo 1mo
134
head = ""
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
133728e barerepo 1mo
137
// A typo is best reported by the push that makes it, not by whoever pushes next. Chapter 14.
133728e barerepo 1mo
138
for _, u := range ups {
133728e barerepo 1mo
139
if head == "" || u.Ref != "refs/heads/"+head || u.New == Zero {
133728e barerepo 1mo
140
continue
133728e barerepo 1mo
141
}
133728e barerepo 1mo
142
if err := repocfg.ParseAt(ctx, e.Dir, u.New); err != nil {
133728e barerepo 1mo
143
fmt.Fprintf(out, "warning: this push leaves %s so it does not parse\n", repocfg.Path)
133728e barerepo 1mo
144
fmt.Fprintf(out, "warning: %v\n", err)
133728e barerepo 1mo
145
fmt.Fprintf(out, "warning: the settings in force do not change until it parses again\n")
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
}
133728e barerepo 1mo
148
133728e barerepo 1mo
149
// The owner may write any ref they own, or chapter 40.3's `git push --mirror` cannot restore.
133728e barerepo 1mo
150
restoring := e.Account != "" && e.Account == e.Owner
133728e barerepo 1mo
151
133728e barerepo 1mo
152
// Chapter 20.2's push limit is one number for the whole push, so every ref's objects are summed and counted once, or a tag beside its branch is charged twice.
133728e barerepo 1mo
153
counted := map[string]bool{}
133728e barerepo 1mo
154
// A commit reachable from two refs in one push is read once, the same as the byte count above.
133728e barerepo 1mo
155
signedSeen := map[string]bool{}
133728e barerepo 1mo
156
// A first commit has no parent to rebase onto, and the printed command has to be the one that works.
133728e barerepo 1mo
157
signedRoot := map[string]bool{}
133728e barerepo 1mo
158
var adding int64
133728e barerepo 1mo
159
133728e barerepo 1mo
160
for _, u := range ups {
133728e barerepo 1mo
161
if !gitx.ValidRef(u.Ref) {
133728e barerepo 1mo
162
return reject(out, "%s is not a usable ref name", u.Ref)
133728e barerepo 1mo
163
}
133728e barerepo 1mo
164
// Chapter 20.2: turning LFS off does not hold without a limit, and the limit names the file.
133728e barerepo 1mo
165
if Limits.MaxBlobMB > 0 || Limits.MaxPushMB > 0 {
133728e barerepo 1mo
166
adds := inspect(ctx, e.Dir, u, Limits.MaxBlobMB, counted)
133728e barerepo 1mo
167
if len(adds.Over) > 0 {
133728e barerepo 1mo
168
return reject(out, "%s", tooBig(adds.Over, Limits.MaxBlobMB))
133728e barerepo 1mo
169
}
133728e barerepo 1mo
170
adding += adds.Bytes
133728e barerepo 1mo
171
if Limits.MaxPushMB > 0 && adding > int64(Limits.MaxPushMB)<<20 {
133728e barerepo 1mo
172
return reject(out, "%s", tooMuch(adding, Limits.MaxPushMB))
133728e barerepo 1mo
173
}
133728e barerepo 1mo
174
}
133728e barerepo 1mo
175
133728e barerepo 1mo
176
if restoring && !strings.HasPrefix(u.Ref, "refs/heads/") {
133728e barerepo 1mo
177
// Branches still meet the rules below, which protect the owner from themselves.
133728e barerepo 1mo
178
continue
133728e barerepo 1mo
179
}
133728e barerepo 1mo
180
switch {
133728e barerepo 1mo
181
case u.Ref == proposal.NewRef || proposal.Number(u.Ref) > 0:
133728e barerepo 1mo
182
// proc-receive owns this namespace, so only ask whether the account may propose.
133728e barerepo 1mo
183
if !cfg.MayPropose(e.Owner, e.Account) {
133728e barerepo 1mo
184
return reject(out, "this repository does not accept proposals from you")
133728e barerepo 1mo
185
}
133728e barerepo 1mo
186
// Chapter 27: rule 5 is an open door, and the cap is how it is defended without closing.
133728e barerepo 1mo
187
if u.Ref == proposal.NewRef && Limits.MaxOpenProposals > 0 {
133728e barerepo 1mo
188
open := thread.OpenProposalsBy(ctx, e.Dir, e.Account)
133728e barerepo 1mo
189
if open >= Limits.MaxOpenProposals {
133728e barerepo 1mo
190
return reject(out,
133728e barerepo 1mo
191
"you have %d proposals open on %s/%s. the limit is %d.\n"+
133728e barerepo 1mo
192
"land or close one, then push this again.",
133728e barerepo 1mo
193
open, e.Owner, e.Name, Limits.MaxOpenProposals)
133728e barerepo 1mo
194
}
133728e barerepo 1mo
195
}
133728e barerepo 1mo
196
133728e barerepo 1mo
197
case strings.HasPrefix(u.Ref, "refs/heads/"), strings.HasPrefix(u.Ref, "refs/tags/"):
133728e barerepo 1mo
198
// An official repository distributes what it holds, so every commit that lands in one is signed. Nothing else on the server is affected.
133728e barerepo 1mo
199
if cfg.Access.RequireSigned && !u.Deleting() {
133728e barerepo 1mo
200
if bad := unsignedIn(ctx, e.Dir, u, signedSeen, signedRoot); len(bad) > 0 {
133728e barerepo 1mo
201
return refuseUnsigned(out, u.Ref, bad, signedRoot)
133728e barerepo 1mo
202
}
133728e barerepo 1mo
203
}
133728e barerepo 1mo
204
if !cfg.MayPush(e.Owner, e.Account) {
133728e barerepo 1mo
205
// The push-rejected page's wording, so the terminal and the page never differ.
133728e barerepo 1mo
206
fmt.Fprintf(out, "you pushed to %s\n", u.Ref)
133728e barerepo 1mo
207
fmt.Fprintf(out, "%s [access] push = %s · you are %s\n",
133728e barerepo 1mo
208
repocfg.Path, repocfg.List(cfg.Access.Push), repocfg.Who(e.Account))
133728e barerepo 1mo
209
fmt.Fprintf(out, "\npush here instead. it needs no permission.\n")
209 of 854 lines · the whole file
history · rawbarerepo 0.1.0