133728e barerepo 1mo
1
package e2e
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"net/http"
133728e barerepo 1mo
5
"os/exec"
133728e barerepo 1mo
6
"path/filepath"
133728e barerepo 1mo
7
"strings"
133728e barerepo 1mo
8
"sync"
133728e barerepo 1mo
9
"testing"
133728e barerepo 1mo
12
// seed makes a repository with one commit and returns the working copy.
133728e barerepo 1mo
13
func seed(t *testing.T, in *instance, token, owner, name string) string {
133728e barerepo 1mo
14
t.Helper()
133728e barerepo 1mo
15
work := t.TempDir()
133728e barerepo 1mo
16
run(t, work, "git", "init", "-q", "-b", "master")
133728e barerepo 1mo
17
write(t, work, "config.go", "package main\n")
133728e barerepo 1mo
18
write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n")
133728e barerepo 1mo
19
run(t, work, "git", "add", "-A")
133728e barerepo 1mo
20
run(t, work, "git", "commit", "-qm", "first")
133728e barerepo 1mo
21
run(t, work, "git", "push", "-q", in.url(token, "/"+owner+"/"+name), "master")
133728e barerepo 1mo
22
return work
133728e barerepo 1mo
25
// clone checks out a repository as somebody else.
133728e barerepo 1mo
26
func clone(t *testing.T, in *instance, token, path string) string {
133728e barerepo 1mo
27
t.Helper()
133728e barerepo 1mo
28
dir := filepath.Join(t.TempDir(), "c")
133728e barerepo 1mo
29
run(t, "", "git", "clone", "-q", in.url(token, path), dir)
133728e barerepo 1mo
30
return dir
133728e barerepo 1mo
33
func commit(t *testing.T, dir, body, message string) {
133728e barerepo 1mo
34
t.Helper()
133728e barerepo 1mo
35
write(t, dir, "config.go", body)
133728e barerepo 1mo
36
run(t, dir, "git", "commit", "-qam", message)
133728e barerepo 1mo
39
// Chapter 45.2. Each case is one of the hook tests it lists.
133728e barerepo 1mo
40
func TestHooks(t *testing.T) {
133728e barerepo 1mo
41
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
42
t.Skip("git is not installed")
133728e barerepo 1mo
44
in := newInstance(t)
133728e barerepo 1mo
45
john := in.account("john")
133728e barerepo 1mo
46
lisa := in.account("lisa")
133728e barerepo 1mo
47
mark := in.account("mark")
133728e barerepo 1mo
48
seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
50
t.Run("push to master without access", func(t *testing.T) {
133728e barerepo 1mo
51
c := clone(t, in, lisa, "/john/johnbot")
133728e barerepo 1mo
52
commit(t, c, "package main // lisa\n", "lisa edits")
133728e barerepo 1mo
53
out, err := try(t, c, "git", "push", in.url(lisa, "/john/johnbot"), "master")
133728e barerepo 1mo
54
if err == nil {
133728e barerepo 1mo
55
t.Fatal("the push was accepted")
133728e barerepo 1mo
57
// Name the way forward, because a rejected push is where a contributor decides to stop.
133728e barerepo 1mo
58
if !strings.Contains(out, "refs/proposals/new") {
133728e barerepo 1mo
59
t.Errorf("the rejection does not name the proposal command:\n%s", out)
133728e barerepo 1mo
61
if !strings.Contains(out, "[access] push") {
133728e barerepo 1mo
62
t.Errorf("the rejection does not name the rule that refused it:\n%s", out)
133728e barerepo 1mo
64
})
133728e barerepo 1mo
66
t.Run("push to refs/proposals/new", func(t *testing.T) {
133728e barerepo 1mo
67
c := clone(t, in, lisa, "/john/johnbot")
133728e barerepo 1mo
68
commit(t, c, "package main // proposal\n", "a proposal")
133728e barerepo 1mo
69
out := run(t, c, "git", "push", in.url(lisa, "/john/johnbot"), "HEAD:refs/proposals/new")
133728e barerepo 1mo
70
if !strings.Contains(out, "refs/proposals/") {
133728e barerepo 1mo
71
t.Fatalf("no number was allocated:\n%s", out)
133728e barerepo 1mo
73
if !strings.Contains(out, in.http.URL) {
133728e barerepo 1mo
74
t.Errorf("the push output has no url to open:\n%s", out)
133728e barerepo 1mo
76
if strings.Contains(out, "refs/proposals/new\n") {
133728e barerepo 1mo
77
t.Error("refs/proposals/new was created; it is reserved")
133728e barerepo 1mo
79
})
133728e barerepo 1mo
81
t.Run("two pushes at the same moment", func(t *testing.T) {
133728e barerepo 1mo
82
// Two different numbers, which is the whole reason the counter is a compare-and-swap.
133728e barerepo 1mo
83
var wg sync.WaitGroup
133728e barerepo 1mo
84
outs := make([]string, 2)
133728e barerepo 1mo
85
start := make(chan struct{})
133728e barerepo 1mo
86
for i := range outs {
133728e barerepo 1mo
87
c := clone(t, in, mark, "/john/johnbot")
133728e barerepo 1mo
88
commit(t, c, "package main // race "+string(rune('a'+i))+"\n", "race")
133728e barerepo 1mo
89
wg.Add(1)
133728e barerepo 1mo
90
go func(i int, dir string) {
133728e barerepo 1mo
91
defer wg.Done()
133728e barerepo 1mo
92
<-start
133728e barerepo 1mo
93
out, _ := try(t, dir, "git", "push", in.url(mark, "/john/johnbot"), "HEAD:refs/proposals/new")
133728e barerepo 1mo
94
outs[i] = out
133728e barerepo 1mo
95
}(i, c)
133728e barerepo 1mo
97
close(start)
133728e barerepo 1mo
98
wg.Wait()
133728e barerepo 1mo
100
got := map[string]bool{}
133728e barerepo 1mo
101
for i, out := range outs {
133728e barerepo 1mo
102
ref := proposalRef(out)
133728e barerepo 1mo
103
if ref == "" {
133728e barerepo 1mo
104
t.Fatalf("push %d allocated nothing:\n%s", i, out)
133728e barerepo 1mo
105
}
133728e barerepo 1mo
106
if got[ref] {
133728e barerepo 1mo
107
t.Fatalf("both pushes got %s", ref)
133728e barerepo 1mo
108
}
133728e barerepo 1mo
109
got[ref] = true
133728e barerepo 1mo
110
}
133728e barerepo 1mo
111
})
133728e barerepo 1mo
113
t.Run("push to another account's proposal", func(t *testing.T) {
133728e barerepo 1mo
114
c := clone(t, in, lisa, "/john/johnbot")
133728e barerepo 1mo
115
commit(t, c, "package main // mine\n", "mine")
133728e barerepo 1mo
116
out := run(t, c, "git", "push", in.url(lisa, "/john/johnbot"), "HEAD:refs/proposals/new")
133728e barerepo 1mo
117
ref := proposalRef(out)
133728e barerepo 1mo
119
other := clone(t, in, mark, "/john/johnbot")
133728e barerepo 1mo
120
commit(t, other, "package main // yours\n", "taking it over")
133728e barerepo 1mo
121
out, err := try(t, other, "git", "push", "-f", in.url(mark, "/john/johnbot"), "HEAD:"+ref)
133728e barerepo 1mo
122
if err == nil {
133728e barerepo 1mo
123
t.Fatal("somebody else's proposal was overwritten")
133728e barerepo 1mo
124
}
133728e barerepo 1mo
125
if !strings.Contains(out, "belongs to lisa") {
133728e barerepo 1mo
126
t.Errorf("the rejection does not say whose it is:\n%s", out)
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
})
133728e barerepo 1mo
130
t.Run("merging closes the thread", func(t *testing.T) {
133728e barerepo 1mo
131
c := clone(t, in, lisa, "/john/johnbot")
133728e barerepo 1mo
132
commit(t, c, "package main // to merge\n", "worth merging")
133728e barerepo 1mo
133
out := run(t, c, "git", "push", in.url(lisa, "/john/johnbot"), "HEAD:refs/proposals/new")
133728e barerepo 1mo
134
ref := proposalRef(out)
133728e barerepo 1mo
135
n := strings.TrimPrefix(ref, "refs/proposals/")
133728e barerepo 1mo
137
owner := clone(t, in, john, "/john/johnbot")
133728e barerepo 1mo
138
run(t, owner, "git", "fetch", "-q", in.url(john, "/john/johnbot"), ref+":prop")
133728e barerepo 1mo
139
run(t, owner, "git", "merge", "-q", "--no-edit", "prop")
133728e barerepo 1mo
140
out = run(t, owner, "git", "push", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
141
if !strings.Contains(out, "closed as merged") {
133728e barerepo 1mo
142
t.Errorf("the thread did not close itself:\n%s", out)
133728e barerepo 1mo
143
}
133728e barerepo 1mo
144
dir := repoDir(t, in, "john", "johnbot")
133728e barerepo 1mo
145
meta := run(t, dir, "git", "cat-file", "blob", "refs/notes/threads/"+n+":meta")
133728e barerepo 1mo
146
if !strings.Contains(meta, "state: merged") {
133728e barerepo 1mo
147
t.Errorf("thread %s is not merged:\n%s", n, meta)
133728e barerepo 1mo
148
}
133728e barerepo 1mo
149
})
133728e barerepo 1mo
151
t.Run("squash merging leaves it open", func(t *testing.T) {
133728e barerepo 1mo
152
// A squash changes the hashes, so the server says so rather than guessing. 36.7.
133728e barerepo 1mo
153
c := clone(t, in, lisa, "/john/johnbot")
133728e barerepo 1mo
154
commit(t, c, "package main // squash me\n", "squash me")
133728e barerepo 1mo
155
out := run(t, c, "git", "push", in.url(lisa, "/john/johnbot"), "HEAD:refs/proposals/new")
133728e barerepo 1mo
156
n := strings.TrimPrefix(proposalRef(out), "refs/proposals/")
133728e barerepo 1mo
158
owner := clone(t, in, john, "/john/johnbot")
133728e barerepo 1mo
159
run(t, owner, "git", "fetch", "-q", in.url(john, "/john/johnbot"), "refs/proposals/"+n+":prop")
133728e barerepo 1mo
160
run(t, owner, "git", "merge", "-q", "--squash", "prop")
133728e barerepo 1mo
161
run(t, owner, "git", "commit", "-qm", "squashed")
133728e barerepo 1mo
162
run(t, owner, "git", "push", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
164
dir := repoDir(t, in, "john", "johnbot")
133728e barerepo 1mo
165
meta := run(t, dir, "git", "cat-file", "blob", "refs/notes/threads/"+n+":meta")
133728e barerepo 1mo
166
if !strings.Contains(meta, "state: open") {
133728e barerepo 1mo
167
t.Errorf("a squash merge closed thread %s, which the server cannot know:\n%s", n, meta)
133728e barerepo 1mo
168
}
133728e barerepo 1mo
169
})
133728e barerepo 1mo
172
// proposalRef reads the allocated ref out of git's push output.
133728e barerepo 1mo
173
func proposalRef(out string) string {
133728e barerepo 1mo
174
for _, line := range strings.Split(out, "\n") {
133728e barerepo 1mo
175
i := strings.Index(line, "refs/proposals/")
133728e barerepo 1mo
176
if i < 0 {
133728e barerepo 1mo
177
continue
133728e barerepo 1mo
178
}
133728e barerepo 1mo
179
ref := strings.Fields(line[i:])[0]
133728e barerepo 1mo
180
if ref != "refs/proposals/new" {
133728e barerepo 1mo
181
return ref
133728e barerepo 1mo
182
}
133728e barerepo 1mo
183
}
133728e barerepo 1mo
184
return ""
133728e barerepo 1mo
187
// A commit author is whatever the pusher typed, so it never decides who writes a proposal ref.
133728e barerepo 1mo
188
func TestCommitAuthorIsNotIdentity(t *testing.T) {
133728e barerepo 1mo
189
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
190
t.Skip("git is not installed")
133728e barerepo 1mo
191
}
133728e barerepo 1mo
192
in := newInstance(t)
133728e barerepo 1mo
193
john := in.account("john")
133728e barerepo 1mo
194
lisa := in.account("lisa")
133728e barerepo 1mo
195
mark := in.account("mark")
133728e barerepo 1mo
196
seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
198
c := clone(t, in, lisa, "/john/johnbot")
133728e barerepo 1mo
199
commit(t, c, "package main // lisa's work\n", "lisa's work")
133728e barerepo 1mo
200
ref := proposalRef(run(t, c, "git", "push", in.url(lisa, "/john/johnbot"), "HEAD:refs/proposals/new"))
133728e barerepo 1mo
202
// mark claims to be lisa in the only place a commit records a name.
133728e barerepo 1mo
203
thief := clone(t, in, mark, "/john/johnbot")
133728e barerepo 1mo
204
run(t, thief, "git", "config", "user.name", "lisa")
133728e barerepo 1mo
205
run(t, thief, "git", "config", "user.email", "lisa@localhost")
133728e barerepo 1mo
206
write(t, thief, "config.go", "package main // taken\n")
133728e barerepo 1mo
207
run(t, thief, "git", "commit", "-qam", "taken")
133728e barerepo 1mo
209
out, err := try(t, thief, "git", "push", "-f", in.url(mark, "/john/johnbot"), "HEAD:"+ref)
133728e barerepo 1mo
210
if err == nil {
133728e barerepo 1mo
211
t.Fatal("a commit signed with somebody else's name took over their proposal")