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

barerepo / server / internal/hook/procreceive.go
298 lines · 9.2kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package hook
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"io"
133728e barerepo 1mo
7
"strings"
133728e barerepo 1mo
8
"time"
133728e barerepo 1mo
9
133728e barerepo 1mo
10
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
11
"github.com/barerepo/server/internal/pktline"
133728e barerepo 1mo
12
"github.com/barerepo/server/internal/proposal"
133728e barerepo 1mo
13
"github.com/barerepo/server/internal/repocfg"
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/store"
133728e barerepo 1mo
15
"github.com/barerepo/server/internal/thread"
133728e barerepo 1mo
16
)
133728e barerepo 1mo
17
133728e barerepo 1mo
18
// ProcReceive turns a push to refs/proposals/new into refs/proposals/47, over pkt-line.
133728e barerepo 1mo
19
func ProcReceive(ctx context.Context, e Env, stdin io.Reader, stdout, out io.Writer) error {
133728e barerepo 1mo
20
r := pktline.NewReader(stdin)
133728e barerepo 1mo
21
w := pktline.NewWriter(stdout)
133728e barerepo 1mo
22
133728e barerepo 1mo
23
if err := negotiate(r, w); err != nil {
133728e barerepo 1mo
24
return err
133728e barerepo 1mo
25
}
133728e barerepo 1mo
26
commands, err := readCommands(r)
133728e barerepo 1mo
27
if err != nil {
133728e barerepo 1mo
28
return err
133728e barerepo 1mo
29
}
133728e barerepo 1mo
30
133728e barerepo 1mo
31
cfg, _ := repocfg.Load(ctx, e.Dir)
133728e barerepo 1mo
32
133728e barerepo 1mo
33
for _, c := range commands {
133728e barerepo 1mo
34
res := s.handle(ctx, e, cfg, c, out)
133728e barerepo 1mo
35
if res.err != "" {
133728e barerepo 1mo
36
if err := w.Write("ng " + c.Ref + " " + res.err); err != nil {
133728e barerepo 1mo
37
return err
133728e barerepo 1mo
38
}
133728e barerepo 1mo
39
continue
133728e barerepo 1mo
40
}
133728e barerepo 1mo
41
if err := w.Write("ok " + c.Ref); err != nil {
133728e barerepo 1mo
42
return err
133728e barerepo 1mo
43
}
133728e barerepo 1mo
44
// Say where the push landed, so the client names the proposal and not the magic ref.
133728e barerepo 1mo
45
if res.ref != "" && res.ref != c.Ref {
133728e barerepo 1mo
46
if err := w.Write("option refname " + res.ref); err != nil {
133728e barerepo 1mo
47
return err
133728e barerepo 1mo
48
}
133728e barerepo 1mo
49
}
133728e barerepo 1mo
50
if res.old != "" {
133728e barerepo 1mo
51
if err := w.Write("option old-oid " + res.old); err != nil {
133728e barerepo 1mo
52
return err
133728e barerepo 1mo
53
}
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
if res.forced {
133728e barerepo 1mo
56
if err := w.Write("option forced-update"); err != nil {
133728e barerepo 1mo
57
return err
133728e barerepo 1mo
58
}
133728e barerepo 1mo
59
}
133728e barerepo 1mo
60
}
133728e barerepo 1mo
61
return w.Flush()
133728e barerepo 1mo
62
}
133728e barerepo 1mo
63
133728e barerepo 1mo
64
// now is the one clock this file reads, so a test can hold it still.
133728e barerepo 1mo
65
var now = time.Now
133728e barerepo 1mo
66
133728e barerepo 1mo
67
// s exists only to group the handling; proc-receive has no state of its own.
133728e barerepo 1mo
68
var s procReceive
133728e barerepo 1mo
69
133728e barerepo 1mo
70
type procReceive struct{}
133728e barerepo 1mo
71
133728e barerepo 1mo
72
type command struct{ Old, New, Ref string }
133728e barerepo 1mo
73
133728e barerepo 1mo
74
func (c command) Creating() bool { return strings.Trim(c.Old, "0") == "" }
133728e barerepo 1mo
75
func (c command) Deleting() bool { return strings.Trim(c.New, "0") == "" }
133728e barerepo 1mo
76
133728e barerepo 1mo
77
type result struct {
133728e barerepo 1mo
78
ref string
133728e barerepo 1mo
79
old string
133728e barerepo 1mo
80
forced bool
133728e barerepo 1mo
81
err string
133728e barerepo 1mo
82
}
133728e barerepo 1mo
83
133728e barerepo 1mo
84
// negotiate agrees on protocol version 1.
133728e barerepo 1mo
85
func negotiate(r *pktline.Reader, w *pktline.Writer) error {
133728e barerepo 1mo
86
for {
133728e barerepo 1mo
87
line, err := r.Read()
133728e barerepo 1mo
88
if err == pktline.ErrFlush {
133728e barerepo 1mo
89
break
133728e barerepo 1mo
90
}
133728e barerepo 1mo
91
if err != nil {
133728e barerepo 1mo
92
return err
133728e barerepo 1mo
93
}
133728e barerepo 1mo
94
// The line is "version=1" and may carry capabilities after a NUL.
133728e barerepo 1mo
95
if v, _, _ := strings.Cut(line, "\x00"); v != "version=1" {
133728e barerepo 1mo
96
return fmt.Errorf("unsupported proc-receive version %q", v)
133728e barerepo 1mo
97
}
133728e barerepo 1mo
98
}
133728e barerepo 1mo
99
if err := w.Write("version=1"); err != nil {
133728e barerepo 1mo
100
return err
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
return w.Flush()
133728e barerepo 1mo
103
}
133728e barerepo 1mo
104
133728e barerepo 1mo
105
func readCommands(r *pktline.Reader) ([]command, error) {
133728e barerepo 1mo
106
var out []command
133728e barerepo 1mo
107
for {
133728e barerepo 1mo
108
line, err := r.Read()
133728e barerepo 1mo
109
if err == pktline.ErrFlush {
133728e barerepo 1mo
110
return out, nil
133728e barerepo 1mo
111
}
133728e barerepo 1mo
112
if err != nil {
133728e barerepo 1mo
113
return nil, err
133728e barerepo 1mo
114
}
133728e barerepo 1mo
115
f := strings.Fields(line)
133728e barerepo 1mo
116
if len(f) < 3 {
133728e barerepo 1mo
117
return nil, fmt.Errorf("malformed proc-receive command %q", line)
133728e barerepo 1mo
118
}
133728e barerepo 1mo
119
out = append(out, command{Old: f[0], New: f[1], Ref: f[2]})
133728e barerepo 1mo
120
}
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
133728e barerepo 1mo
123
// handle does the work for one command and says what to report.
133728e barerepo 1mo
124
func (procReceive) handle(ctx context.Context, e Env, cfg repocfg.Config, c command, out io.Writer) result {
133728e barerepo 1mo
125
switch {
133728e barerepo 1mo
126
case c.Ref == proposal.NewRef:
133728e barerepo 1mo
127
return openProposal(ctx, e, cfg, c, out)
133728e barerepo 1mo
128
case proposal.Number(c.Ref) > 0:
133728e barerepo 1mo
129
return updateProposal(ctx, e, cfg, c, out)
133728e barerepo 1mo
130
default:
133728e barerepo 1mo
131
return result{err: c.Ref + " is not a proposal ref"}
133728e barerepo 1mo
132
}
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
133728e barerepo 1mo
135
// openProposal allocates a number and creates the ref.
133728e barerepo 1mo
136
func openProposal(ctx context.Context, e Env, cfg repocfg.Config, c command, out io.Writer) result {
133728e barerepo 1mo
137
if c.Deleting() {
133728e barerepo 1mo
138
return result{err: "refs/proposals/new is not a ref, so it cannot be deleted"}
133728e barerepo 1mo
139
}
133728e barerepo 1mo
140
if !cfg.MayPropose(e.Owner, e.Account) {
133728e barerepo 1mo
141
return result{err: "this repository does not accept proposals from you"}
133728e barerepo 1mo
142
}
133728e barerepo 1mo
143
n, err := proposal.Allocate(ctx, e.Dir)
133728e barerepo 1mo
144
if err != nil {
133728e barerepo 1mo
145
return result{err: err.Error()}
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
ref := proposal.Ref(n)
133728e barerepo 1mo
148
if _, err := gitx.Run(ctx, e.Dir, "update-ref", ref, c.New, ""); err != nil {
133728e barerepo 1mo
149
return result{err: err.Error()}
133728e barerepo 1mo
150
}
133728e barerepo 1mo
151
133728e barerepo 1mo
152
// A proposal is a thread with a ref, so opening one opens the thread. Chapter 13.
133728e barerepo 1mo
153
subject, err := gitx.Run(ctx, e.Dir, "log", "--max-count=1", "--format=%s", c.New)
133728e barerepo 1mo
154
if err != nil {
133728e barerepo 1mo
155
subject = ""
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
meta := thread.Meta{
133728e barerepo 1mo
158
Title: strings.TrimSpace(subject),
133728e barerepo 1mo
159
State: thread.Open,
133728e barerepo 1mo
160
Ref: ref,
133728e barerepo 1mo
161
Author: e.Account,
133728e barerepo 1mo
162
Opened: now(),
133728e barerepo 1mo
163
}
133728e barerepo 1mo
164
if err := thread.Create(ctx, e.Dir, n, meta, "", thread.Comment{}); err != nil {
133728e barerepo 1mo
165
return result{err: err.Error()}
133728e barerepo 1mo
166
}
133728e barerepo 1mo
167
133728e barerepo 1mo
168
record(ctx, store.Event{
133728e barerepo 1mo
169
Kind: store.ProposalOpened, Actor: e.Account, Repo: e.Owner + "/" + e.Name,
133728e barerepo 1mo
170
Number: n, Ref: ref, Title: meta.Title,
133728e barerepo 1mo
171
})
133728e barerepo 1mo
172
took(ctx, e.Account, e.Owner+"/"+e.Name, n)
133728e barerepo 1mo
173
133728e barerepo 1mo
174
fmt.Fprintf(out, "\nproposal %d opened.\n", n)
133728e barerepo 1mo
175
if e.URL != "" {
133728e barerepo 1mo
176
fmt.Fprintf(out, " %s/thread/%d\n", e.URL, n)
133728e barerepo 1mo
177
}
133728e barerepo 1mo
178
fmt.Fprintf(out, " update it later with: git push -f origin HEAD:%s\n\n", ref)
133728e barerepo 1mo
179
return result{ref: ref, old: c.Old}
133728e barerepo 1mo
180
}
133728e barerepo 1mo
181
133728e barerepo 1mo
182
// updateProposal expects force, because chapter 12 updates a proposal by rewriting it.
133728e barerepo 1mo
183
func updateProposal(ctx context.Context, e Env, cfg repocfg.Config, c command, out io.Writer) result {
133728e barerepo 1mo
184
n := proposal.Number(c.Ref)
133728e barerepo 1mo
185
if c.Ref != proposal.Ref(n) {
133728e barerepo 1mo
186
return result{err: "a revision ref is written by the server, not by a push"}
133728e barerepo 1mo
187
}
133728e barerepo 1mo
188
// git hands proc-receive the client's old-oid and checks it against nothing, so read the ref: chapter 12's retained revision and the thread's record both hang off what was really replaced.
133728e barerepo 1mo
189
old := tipOf(ctx, e.Dir, c.Ref)
133728e barerepo 1mo
190
creating := strings.Trim(old, "0") == ""
133728e barerepo 1mo
191
133728e barerepo 1mo
192
// An owner pushing a mirror is restoring, and chapter 40.3 depends on it working.
133728e barerepo 1mo
193
if e.Account != "" && e.Account == e.Owner && creating {
133728e barerepo 1mo
194
if _, err := gitx.Run(ctx, e.Dir, "update-ref", c.Ref, c.New, ""); err != nil {
133728e barerepo 1mo
195
return result{err: err.Error()}
133728e barerepo 1mo
196
}
133728e barerepo 1mo
197
return result{ref: c.Ref, old: old}
133728e barerepo 1mo
198
}
133728e barerepo 1mo
199
author, err := authorOf(ctx, e.Dir, n)
133728e barerepo 1mo
200
if err != nil {
133728e barerepo 1mo
201
return result{err: err.Error()}
133728e barerepo 1mo
202
}
133728e barerepo 1mo
203
if author == "" {
133728e barerepo 1mo
204
// Nothing records who opened this ref, so only push access may touch it.
133728e barerepo 1mo
205
if !cfg.MayPush(e.Owner, e.Account) {
133728e barerepo 1mo
206
return result{err: fmt.Sprintf("proposal %d has no recorded author, so only somebody with push access may update it", n)}
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
author = e.Account
133728e barerepo 1mo
209
}
133728e barerepo 1mo
210
// Chapter 12: the author, plus anyone with push access.
133728e barerepo 1mo
211
if e.Account != author && !cfg.MayPush(e.Owner, e.Account) {
133728e barerepo 1mo
212
return result{err: fmt.Sprintf("proposal %d belongs to %s", n, author)}
133728e barerepo 1mo
213
}
133728e barerepo 1mo
214
133728e barerepo 1mo
215
// Retain the replaced tip, so comments anchored to it do not dangle. Chapters 12 and 43.5.
133728e barerepo 1mo
216
forced := false
133728e barerepo 1mo
217
expect := old
133728e barerepo 1mo
218
if creating {
133728e barerepo 1mo
219
// An empty old value means the ref must not exist, which is what a create asks for.
219 of 298 lines · the whole file
history · rawbarerepo 0.1.0