Browse code

Add godeps commit verification

Verify commits to ensure that any changes to Godeps are validated
as proper UPSTREAM commits.

Dan Mace authored on 2015/12/03 05:07:07
Showing 11 changed files
... ...
@@ -56,6 +56,7 @@ verify:
56 56
 else
57 57
 verify: build
58 58
 endif
59
+	hack/verify-upstream-commits.sh
59 60
 	hack/verify-gofmt.sh
60 61
 	hack/verify-govet.sh
61 62
 	hack/verify-generated-deep-copies.sh
62 63
new file mode 100644
... ...
@@ -0,0 +1,36 @@
0
+package main
1
+
2
+import (
3
+	"flag"
4
+	"fmt"
5
+	"os"
6
+	"strings"
7
+
8
+	"github.com/openshift/origin/cmd/rebasehelpers/util"
9
+)
10
+
11
+func main() {
12
+	var start, end string
13
+	flag.StringVar(&start, "start", "master", "The start of the revision range for analysis")
14
+	flag.StringVar(&end, "end", "HEAD", "The end of the revision range for analysis")
15
+	flag.Parse()
16
+
17
+	commits, err := util.CommitsBetween(start, end)
18
+	if err != nil {
19
+		os.Stderr.WriteString(fmt.Sprintf("ERROR: couldn't find commits from %s..%s: %v\n", start, end, err))
20
+		os.Exit(1)
21
+	}
22
+
23
+	errs := []string{}
24
+	for _, validate := range AllValidators {
25
+		err := validate(commits)
26
+		if err != nil {
27
+			errs = append(errs, err.Error())
28
+		}
29
+	}
30
+
31
+	if len(errs) > 0 {
32
+		os.Stderr.WriteString(strings.Join(errs, "\n\n"))
33
+		os.Exit(2)
34
+	}
35
+}
0 36
new file mode 100644
... ...
@@ -0,0 +1,184 @@
0
+package main
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"regexp"
6
+	"text/template"
7
+
8
+	"github.com/openshift/origin/cmd/rebasehelpers/util"
9
+)
10
+
11
+var CommitSummaryErrorTemplate = `
12
+The following UPSTREAM commits have invalid summaries:
13
+
14
+{{ range .Commits }}  [{{ .Sha }}] {{ .Summary }}
15
+{{ end }}
16
+UPSTREAM commit summaries should look like:
17
+
18
+  UPSTREAM: [non-kube-account/repo[/path]: ]<PR number>|'<carry>'|'<drop>': description
19
+
20
+UPSTREAM commits which revert previous UPSTREAM commits should look like:
21
+
22
+  UPSTREAM: revert: <sha>: <normal upstream format>
23
+
24
+UPSTREAM commits are validated against the following regular expression:
25
+
26
+  {{ .Pattern }}
27
+
28
+Examples of valid summaries:
29
+
30
+  UPSTREAM: 12345: An origin change
31
+  UPSTREAM: docker/docker: 12345: A docker change
32
+  UPSTREAM: <carry>: A carried kube change
33
+  UPSTREAM: <drop>: A dropped kube change
34
+  UPSTREAM: revert: abcd123: docker/docker: 12345: A docker change
35
+
36
+`
37
+
38
+var AllValidators = []func([]util.Commit) error{
39
+	ValidateUpstreamCommitsWithoutGodepsChanges,
40
+	ValidateUpstreamCommitModifiesSingleGodepsRepo,
41
+	ValidateUpstreamCommitSummaries,
42
+	ValidateUpstreamCommitModifiesOnlyGodeps,
43
+	ValidateUpstreamCommitModifiesOnlyDeclaredGodepRepo,
44
+}
45
+
46
+// ValidateUpstreamCommitsWithoutGodepsChanges returns an error if any
47
+// upstream commits have no Godeps changes.
48
+func ValidateUpstreamCommitsWithoutGodepsChanges(commits []util.Commit) error {
49
+	problemCommits := []util.Commit{}
50
+	for _, commit := range commits {
51
+		if commit.HasGodepsChanges() && !commit.DeclaresUpstreamChange() {
52
+			problemCommits = append(problemCommits, commit)
53
+		}
54
+	}
55
+	if len(problemCommits) > 0 {
56
+		label := "The following commits contain Godeps changes but aren't declared as UPSTREAM:"
57
+		msg := renderGodepFilesError(label, problemCommits, RenderOnlyGodepsFiles)
58
+		return fmt.Errorf(msg)
59
+	}
60
+	return nil
61
+}
62
+
63
+// ValidateUpstreamCommitModifiesSingleGodepsRepo returns an error if any
64
+// upstream commits have changes that span more than one Godeps repo.
65
+func ValidateUpstreamCommitModifiesSingleGodepsRepo(commits []util.Commit) error {
66
+	problemCommits := []util.Commit{}
67
+	for _, commit := range commits {
68
+		godepsChanges, err := commit.GodepsReposChanged()
69
+		if err != nil {
70
+			return err
71
+		}
72
+		if len(godepsChanges) > 1 {
73
+			problemCommits = append(problemCommits, commit)
74
+		}
75
+	}
76
+	if len(problemCommits) > 0 {
77
+		label := "The following UPSTREAM commits modify more than one repo in their changelist"
78
+		msg := renderGodepFilesError(label, problemCommits, RenderOnlyGodepsFiles)
79
+		return fmt.Errorf(msg)
80
+	}
81
+	return nil
82
+}
83
+
84
+// ValidateUpstreamCommitSummaries ensures that any commits which declare to
85
+// be upstream match the regular expressions for UPSTREAM summaries.
86
+func ValidateUpstreamCommitSummaries(commits []util.Commit) error {
87
+	problemCommits := []util.Commit{}
88
+	for _, commit := range commits {
89
+		if commit.DeclaresUpstreamChange() && !commit.MatchesUpstreamSummaryPattern() {
90
+			problemCommits = append(problemCommits, commit)
91
+		}
92
+	}
93
+	if len(problemCommits) > 0 {
94
+		tmpl, _ := template.New("problems").Parse(CommitSummaryErrorTemplate)
95
+		data := struct {
96
+			Pattern *regexp.Regexp
97
+			Commits []util.Commit
98
+		}{
99
+			Pattern: util.UpstreamSummaryPattern,
100
+			Commits: problemCommits,
101
+		}
102
+		buffer := &bytes.Buffer{}
103
+		tmpl.Execute(buffer, data)
104
+		return fmt.Errorf(buffer.String())
105
+	}
106
+	return nil
107
+}
108
+
109
+// ValidateUpstreamCommitModifiesOnlyGodeps ensures that any Godeps commits
110
+// modify ONLY Godeps files.
111
+func ValidateUpstreamCommitModifiesOnlyGodeps(commits []util.Commit) error {
112
+	problemCommits := []util.Commit{}
113
+	for _, commit := range commits {
114
+		if commit.HasGodepsChanges() && commit.HasNonGodepsChanges() {
115
+			problemCommits = append(problemCommits, commit)
116
+		}
117
+	}
118
+	if len(problemCommits) > 0 {
119
+		label := "The following UPSTREAM commits modify files outside Godeps"
120
+		msg := renderGodepFilesError(label, problemCommits, RenderAllFiles)
121
+		return fmt.Errorf(msg)
122
+	}
123
+	return nil
124
+}
125
+
126
+// ValidateUpstreamCommitModifiesOnlyDeclaredGodepRepo ensures that an
127
+// upstream commit only modifies the Godep repo the summary declares.
128
+func ValidateUpstreamCommitModifiesOnlyDeclaredGodepRepo(commits []util.Commit) error {
129
+	problemCommits := []util.Commit{}
130
+	for _, commit := range commits {
131
+		if commit.DeclaresUpstreamChange() {
132
+			declaredRepo, err := commit.DeclaredUpstreamRepo()
133
+			if err != nil {
134
+				return err
135
+			}
136
+			reposChanged, err := commit.GodepsReposChanged()
137
+			if err != nil {
138
+				return err
139
+			}
140
+			for _, changedRepo := range reposChanged {
141
+				if changedRepo != declaredRepo {
142
+					fmt.Printf("changedRepo=%s, declaredRepo=%s\n", changedRepo, declaredRepo)
143
+					problemCommits = append(problemCommits, commit)
144
+				}
145
+			}
146
+		}
147
+	}
148
+	if len(problemCommits) > 0 {
149
+		label := "The following UPSTREAM commits modify Godeps repos other than the repo the commit declares"
150
+		msg := renderGodepFilesError(label, problemCommits, RenderAllFiles)
151
+		return fmt.Errorf(msg)
152
+	}
153
+	return nil
154
+}
155
+
156
+type CommitFilesRenderOption int
157
+
158
+const (
159
+	RenderNoFiles CommitFilesRenderOption = iota
160
+	RenderOnlyGodepsFiles
161
+	RenderOnlyNonGodepsFiles
162
+	RenderAllFiles
163
+)
164
+
165
+// renderGodepFilesError formats commits and their file lists into readable
166
+// output prefixed with label.
167
+func renderGodepFilesError(label string, commits []util.Commit, opt CommitFilesRenderOption) string {
168
+	msg := fmt.Sprintf("%s:\n\n", label)
169
+	for _, commit := range commits {
170
+		msg += fmt.Sprintf("[%s] %s\n", commit.Sha, commit.Summary)
171
+		if opt == RenderNoFiles {
172
+			continue
173
+		}
174
+		for _, file := range commit.Files {
175
+			if opt == RenderAllFiles ||
176
+				(opt == RenderOnlyGodepsFiles && file.HasGodepsChanges()) ||
177
+				(opt == RenderOnlyNonGodepsFiles && !file.HasGodepsChanges()) {
178
+				msg += fmt.Sprintf("  - %s\n", file)
179
+			}
180
+		}
181
+	}
182
+	return msg
183
+}
0 184
new file mode 100644
... ...
@@ -0,0 +1,379 @@
0
+package main
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/openshift/origin/cmd/rebasehelpers/util"
6
+)
7
+
8
+func TestValidateUpstreamCommitsWithoutGodepsChanges(t *testing.T) {
9
+	tests := []struct {
10
+		name          string
11
+		commits       []util.Commit
12
+		errorExpected bool
13
+	}{
14
+		{
15
+			name: "test 1",
16
+			commits: []util.Commit{
17
+				{
18
+					Sha:     "aaa0000",
19
+					Summary: "commit 1",
20
+					Files:   []util.File{"file1", "pkg/file2"},
21
+				},
22
+				{
23
+					Sha:     "aaa0001",
24
+					Summary: "commit 2",
25
+					Files:   []util.File{"Godeps/file1", "pkg/file2"},
26
+				},
27
+			},
28
+			errorExpected: true,
29
+		},
30
+		{
31
+			name: "test 2",
32
+			commits: []util.Commit{
33
+				{
34
+					Sha:     "aaa0000",
35
+					Summary: "commit 1",
36
+					Files:   []util.File{"file1", "pkg/file2"},
37
+				},
38
+				{
39
+					Sha:     "aaa0001",
40
+					Summary: "UPSTREAM: commit 2",
41
+					Files:   []util.File{"Godeps/file1", "pkg/file2"},
42
+				},
43
+			},
44
+			errorExpected: false,
45
+		},
46
+	}
47
+	for _, test := range tests {
48
+		t.Logf("evaluating test %q", test.name)
49
+		err := ValidateUpstreamCommitsWithoutGodepsChanges(test.commits)
50
+		if err != nil {
51
+			if test.errorExpected {
52
+				t.Logf("got expected error:\n%s", err)
53
+				continue
54
+			} else {
55
+				t.Fatalf("unexpected error:\n%s", err)
56
+			}
57
+		} else {
58
+			if test.errorExpected {
59
+				t.Fatalf("expected an error, got none")
60
+			}
61
+		}
62
+	}
63
+}
64
+
65
+func TestValidateUpstreamCommitModifiesSingleGodepsRepo(t *testing.T) {
66
+	tests := []struct {
67
+		name          string
68
+		commits       []util.Commit
69
+		errorExpected bool
70
+	}{
71
+		{
72
+			name: "test 1",
73
+			commits: []util.Commit{
74
+				{
75
+					Sha:     "aaa0000",
76
+					Summary: "commit 1",
77
+					Files:   []util.File{"file1", "pkg/file2"},
78
+				},
79
+				{
80
+					Sha:     "aaa0001",
81
+					Summary: "commit 2",
82
+					Files: []util.File{
83
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
84
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
85
+					},
86
+				},
87
+			},
88
+			errorExpected: false,
89
+		},
90
+		{
91
+			name: "test 2",
92
+			commits: []util.Commit{
93
+				{
94
+					Sha:     "aaa0000",
95
+					Summary: "commit 1",
96
+					Files:   []util.File{"file1", "pkg/file2"},
97
+				},
98
+				{
99
+					Sha:     "aaa0001",
100
+					Summary: "UPSTREAM: commit 2",
101
+					Files: []util.File{
102
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
103
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
104
+						"Godeps/_workspace/src/github.com/coreos/etcd/file",
105
+					},
106
+				},
107
+				{
108
+					Sha:     "aaa0002",
109
+					Summary: "UPSTREAM: commit 3",
110
+					Files: []util.File{
111
+						"Godeps/_workspace/src/k8s.io/heapster/file1",
112
+						"Godeps/_workspace/src/github.com/coreos/etcd/file1",
113
+					},
114
+				},
115
+			},
116
+			errorExpected: true,
117
+		},
118
+	}
119
+	for _, test := range tests {
120
+		t.Logf("evaluating test %q", test.name)
121
+		err := ValidateUpstreamCommitModifiesSingleGodepsRepo(test.commits)
122
+		if err != nil {
123
+			if test.errorExpected {
124
+				t.Logf("got expected error:\n%s", err)
125
+				continue
126
+			} else {
127
+				t.Fatalf("unexpected error:\n%s", err)
128
+			}
129
+		} else {
130
+			if test.errorExpected {
131
+				t.Fatalf("expected an error, got none")
132
+			}
133
+		}
134
+	}
135
+}
136
+
137
+func TestValidateUpstreamCommitModifiesOnlyGodeps(t *testing.T) {
138
+	tests := []struct {
139
+		name          string
140
+		commits       []util.Commit
141
+		errorExpected bool
142
+	}{
143
+		{
144
+			name: "test 1",
145
+			commits: []util.Commit{
146
+				{
147
+					Sha:     "aaa0000",
148
+					Summary: "commit 1",
149
+					Files:   []util.File{"file1", "pkg/file2"},
150
+				},
151
+				{
152
+					Sha:     "aaa0001",
153
+					Summary: "UPSTREAM: commit 2",
154
+					Files: []util.File{
155
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
156
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
157
+						"pkg/some_file",
158
+					},
159
+				},
160
+			},
161
+			errorExpected: true,
162
+		},
163
+		{
164
+			name: "test 2",
165
+			commits: []util.Commit{
166
+				{
167
+					Sha:     "aaa0000",
168
+					Summary: "commit 1",
169
+					Files:   []util.File{"file1", "pkg/file2"},
170
+				},
171
+				{
172
+					Sha:     "aaa0001",
173
+					Summary: "UPSTREAM: commit 2",
174
+					Files: []util.File{
175
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
176
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
177
+					},
178
+				},
179
+			},
180
+			errorExpected: false,
181
+		},
182
+	}
183
+	for _, test := range tests {
184
+		t.Logf("evaluating test %q", test.name)
185
+		err := ValidateUpstreamCommitModifiesOnlyGodeps(test.commits)
186
+		if err != nil {
187
+			if test.errorExpected {
188
+				t.Logf("got expected error:\n%s", err)
189
+				continue
190
+			} else {
191
+				t.Fatalf("unexpected error:\n%s", err)
192
+			}
193
+		} else {
194
+			if test.errorExpected {
195
+				t.Fatalf("expected an error, got none")
196
+			}
197
+		}
198
+	}
199
+}
200
+
201
+func TestValidateUpstreamCommitSummaries(t *testing.T) {
202
+	tests := []struct {
203
+		summary string
204
+		valid   bool
205
+	}{
206
+		{valid: true, summary: "UPSTREAM: 12345: a change"},
207
+		{valid: true, summary: "UPSTREAM: k8s.io/heapster: 12345: a change"},
208
+		{valid: true, summary: "UPSTREAM: <carry>: a change"},
209
+		{valid: true, summary: "UPSTREAM: <drop>: a change"},
210
+		{valid: true, summary: "UPSTREAM: github.com/coreos/etcd: <carry>: a change"},
211
+		{valid: true, summary: "UPSTREAM: github.com/coreos/etcd: <drop>: a change"},
212
+		{valid: true, summary: "UPSTREAM: revert: abcd123: 12345: a change"},
213
+		{valid: true, summary: "UPSTREAM: revert: abcd123: k8s.io/heapster: 12345: a change"},
214
+		{valid: true, summary: "UPSTREAM: revert: abcd123: <carry>: a change"},
215
+		{valid: true, summary: "UPSTREAM: revert: abcd123: <drop>: a change"},
216
+		{valid: true, summary: "UPSTREAM: revert: abcd123: github.com/coreos/etcd: <carry>: a change"},
217
+		{valid: true, summary: "UPSTREAM: revert: abcd123: github.com/coreos/etcd: <drop>: a change"},
218
+		{valid: false, summary: "UPSTREAM: whoopsie daisy"},
219
+	}
220
+	for _, test := range tests {
221
+		commit := util.Commit{Summary: test.summary, Sha: "abcd000"}
222
+		err := ValidateUpstreamCommitSummaries([]util.Commit{commit})
223
+		if err != nil {
224
+			if test.valid {
225
+				t.Fatalf("unexpected error:\n%s", err)
226
+			} else {
227
+				t.Logf("got expected error:\n%s", err)
228
+			}
229
+		} else {
230
+			if !test.valid {
231
+				t.Fatalf("expected an error, got none; summary: %s", test.summary)
232
+			}
233
+		}
234
+	}
235
+}
236
+
237
+func TestValidateUpstreamCommitModifiesOnlyDeclaredGodepRepo(t *testing.T) {
238
+	tests := []struct {
239
+		name          string
240
+		commits       []util.Commit
241
+		errorExpected bool
242
+	}{
243
+		{
244
+			name: "test 1",
245
+			commits: []util.Commit{
246
+				{
247
+					Sha:     "aaa0000",
248
+					Summary: "commit 1",
249
+					Files:   []util.File{"file1", "pkg/file2"},
250
+				},
251
+				{
252
+					Sha:     "aaa0001",
253
+					Summary: "UPSTREAM: github.com/coreos/etcd: 12345: a change",
254
+					Files: []util.File{
255
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
256
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
257
+						"Godeps/_workspace/src/github.com/coreos/etcd/file1",
258
+					},
259
+				},
260
+			},
261
+			errorExpected: true,
262
+		},
263
+		{
264
+			name: "test 2",
265
+			commits: []util.Commit{
266
+				{
267
+					Sha:     "aaa0001",
268
+					Summary: "UPSTREAM: github.com/coreos/etcd: 12345: a change",
269
+					Files: []util.File{
270
+						"Godeps/_workspace/src/github.com/coreos/etcd/file1",
271
+						"Godeps/_workspace/src/github.com/coreos/etcd/file2",
272
+					},
273
+				},
274
+			},
275
+			errorExpected: false,
276
+		},
277
+		{
278
+			name: "test three segments",
279
+			commits: []util.Commit{
280
+				{
281
+					Sha:     "aaa0001",
282
+					Summary: "UPSTREAM: github.com/coreos/etcd: 12345: a change",
283
+					Files: []util.File{
284
+						"Godeps/_workspace/src/github.com/coreos/etcd/a/file1",
285
+						"Godeps/_workspace/src/github.com/coreos/etcd/b/file2",
286
+					},
287
+				},
288
+			},
289
+			errorExpected: false,
290
+		},
291
+		{
292
+			name: "test 3",
293
+			commits: []util.Commit{
294
+				{
295
+					Sha:     "aaa0001",
296
+					Summary: "UPSTREAM: 12345: a change",
297
+					Files: []util.File{
298
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
299
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
300
+					},
301
+				},
302
+			},
303
+			errorExpected: false,
304
+		},
305
+		{
306
+			name: "test 4",
307
+			commits: []util.Commit{
308
+				{
309
+					Sha:     "aaa0001",
310
+					Summary: "UPSTREAM: 12345: a change",
311
+					Files: []util.File{
312
+						"Godeps/_workspace/src/github.com/coreos/etcd/file1",
313
+						"Godeps/_workspace/src/github.com/coreos/etcd/file2",
314
+					},
315
+				},
316
+			},
317
+			errorExpected: true,
318
+		},
319
+		{
320
+			name: "test 5",
321
+			commits: []util.Commit{
322
+				{
323
+					Sha:     "aaa0001",
324
+					Summary: "UPSTREAM: revert: abcd000: 12345: a change",
325
+					Files: []util.File{
326
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
327
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
328
+					},
329
+				},
330
+			},
331
+			errorExpected: false,
332
+		},
333
+		{
334
+			name: "test 6",
335
+			commits: []util.Commit{
336
+				{
337
+					Sha:     "aaa0001",
338
+					Summary: "UPSTREAM: revert: abcd000: github.com/coreos/etcd: 12345: a change",
339
+					Files: []util.File{
340
+						"Godeps/_workspace/src/k8s.io/kubernetes/file1",
341
+						"Godeps/_workspace/src/k8s.io/kubernetes/file2",
342
+					},
343
+				},
344
+			},
345
+			errorExpected: true,
346
+		},
347
+		{
348
+			name: "test 7",
349
+			commits: []util.Commit{
350
+				{
351
+					Sha:     "aaa0001",
352
+					Summary: "UPSTREAM: revert: abcd000: github.com/coreos/etcd: 12345: a change",
353
+					Files: []util.File{
354
+						"Godeps/_workspace/src/github.com/coreos/etcd/file1",
355
+						"Godeps/_workspace/src/github.com/coreos/etcd/file2",
356
+					},
357
+				},
358
+			},
359
+			errorExpected: false,
360
+		},
361
+	}
362
+	for _, test := range tests {
363
+		t.Logf("evaluating test %q", test.name)
364
+		err := ValidateUpstreamCommitModifiesOnlyDeclaredGodepRepo(test.commits)
365
+		if err != nil {
366
+			if test.errorExpected {
367
+				t.Logf("got expected error:\n%s", err)
368
+				continue
369
+			} else {
370
+				t.Fatalf("unexpected error:\n%s", err)
371
+			}
372
+		} else {
373
+			if test.errorExpected {
374
+				t.Fatalf("expected an error, got none")
375
+			}
376
+		}
377
+	}
378
+}
0 379
new file mode 100644
... ...
@@ -0,0 +1,218 @@
0
+package util
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"os"
6
+	"os/exec"
7
+	"regexp"
8
+	"strings"
9
+)
10
+
11
+var UpstreamSummaryPattern = regexp.MustCompile(`UPSTREAM: (revert: [a-f0-9]{7,}: )?(([\w\.-]+\/[\w-]+)(\/[\w-]+)?: )?([0-9]{4,}:|<carry>:|<drop>:)`)
12
+
13
+// supportedHosts maps source hosts to the number of path segments that
14
+// represent the account/repo for that host. This is necessary because we
15
+// can't tell just by looking at an import path whether the repo is identified
16
+// by the first 2 or 3 path segments.
17
+//
18
+// If dependencies are introduced from new hosts, they'll need to be added
19
+// here.
20
+var SupportedHosts = map[string]int{
21
+	"bitbucket.org":     3,
22
+	"code.google.com":   3,
23
+	"github.com":        3,
24
+	"golang.org":        3,
25
+	"google.golang.org": 2,
26
+	"gopkg.in":          2,
27
+	"k8s.io":            2,
28
+	"speter.net":        2,
29
+}
30
+
31
+type Commit struct {
32
+	Sha     string
33
+	Summary string
34
+	Files   []File
35
+}
36
+
37
+func (c Commit) DeclaresUpstreamChange() bool {
38
+	return strings.HasPrefix(strings.ToLower(c.Summary), "upstream")
39
+}
40
+
41
+func (c Commit) MatchesUpstreamSummaryPattern() bool {
42
+	return UpstreamSummaryPattern.MatchString(c.Summary)
43
+}
44
+
45
+func (c Commit) DeclaredUpstreamRepo() (string, error) {
46
+	if !c.DeclaresUpstreamChange() {
47
+		return "", fmt.Errorf("commit declares no upstream changes")
48
+	}
49
+	if !c.MatchesUpstreamSummaryPattern() {
50
+		return "", fmt.Errorf("commit doesn't match the upstream commit summary pattern")
51
+	}
52
+	groups := UpstreamSummaryPattern.FindStringSubmatch(c.Summary)
53
+	repo := groups[3]
54
+	if len(repo) == 0 {
55
+		repo = "k8s.io/kubernetes"
56
+	}
57
+	// Do a simple special casing for repos which use 3 path segments to
58
+	// identify the repo. The only repos ever seen use either 2 or 3 so don't
59
+	// bother trying to make it too generic.
60
+	segments := -1
61
+	for host, count := range SupportedHosts {
62
+		if strings.HasPrefix(repo, host) {
63
+			segments = count
64
+			break
65
+		}
66
+	}
67
+	if segments == -1 {
68
+		fmt.Printf("commit modifies unsupported repo %q\n", repo)
69
+		return "", fmt.Errorf("commit modifies unsupported repo %q", repo)
70
+	}
71
+	fmt.Printf("repo=%s, segments=%d\n", repo, segments)
72
+	if segments == 3 {
73
+		repo += groups[4]
74
+	}
75
+	return repo, nil
76
+}
77
+
78
+func (c Commit) HasGodepsChanges() bool {
79
+	for _, file := range c.Files {
80
+		if file.HasGodepsChanges() {
81
+			return true
82
+		}
83
+	}
84
+	return false
85
+}
86
+
87
+func (c Commit) HasNonGodepsChanges() bool {
88
+	for _, file := range c.Files {
89
+		if !file.HasGodepsChanges() {
90
+			return true
91
+		}
92
+	}
93
+	return false
94
+}
95
+
96
+func (c Commit) GodepsReposChanged() ([]string, error) {
97
+	repos := map[string]struct{}{}
98
+	for _, file := range c.Files {
99
+		if !file.HasGodepsChanges() {
100
+			continue
101
+		}
102
+		repo, err := file.GodepsRepoChanged()
103
+		if err != nil {
104
+			return nil, fmt.Errorf("problem with file %q in commit %s: %s", file, c.Sha, err)
105
+		}
106
+		repos[repo] = struct{}{}
107
+	}
108
+	changed := []string{}
109
+	for repo := range repos {
110
+		changed = append(changed, repo)
111
+	}
112
+	return changed, nil
113
+}
114
+
115
+type File string
116
+
117
+func (f File) HasGodepsChanges() bool {
118
+	return strings.HasPrefix(string(f), "Godeps")
119
+}
120
+
121
+func (f File) GodepsRepoChanged() (string, error) {
122
+	if !f.HasGodepsChanges() {
123
+		return "", fmt.Errorf("file doesn't appear to be a Godeps change")
124
+	}
125
+	// Find the _workspace path segment index.
126
+	workspaceIdx := -1
127
+	parts := strings.Split(string(f), string(os.PathSeparator))
128
+	for i, part := range parts {
129
+		if part == "_workspace" {
130
+			workspaceIdx = i
131
+			break
132
+		}
133
+	}
134
+	// Godeps path struture assumption: Godeps/_workspace/src/...
135
+	if workspaceIdx == -1 || len(parts) < (workspaceIdx+3) {
136
+		return "", fmt.Errorf("file doesn't appear to be a Godeps workspace path")
137
+	}
138
+	// Deal with repos which could be identified by either 2 or 3 path segments.
139
+	host := parts[workspaceIdx+2]
140
+	segments := -1
141
+	for supportedHost, count := range SupportedHosts {
142
+		if host == supportedHost {
143
+			segments = count
144
+			break
145
+		}
146
+	}
147
+	if segments == -1 {
148
+		return "", fmt.Errorf("file modifies an unsupported repo host %q", host)
149
+	}
150
+	switch segments {
151
+	case 2:
152
+		return fmt.Sprintf("%s/%s", host, parts[workspaceIdx+3]), nil
153
+	case 3:
154
+		return fmt.Sprintf("%s/%s/%s", host, parts[workspaceIdx+3], parts[workspaceIdx+4]), nil
155
+	}
156
+	return "", fmt.Errorf("file modifies an unsupported repo host %q", host)
157
+}
158
+
159
+func CommitsBetween(a, b string) ([]Commit, error) {
160
+	commits := []Commit{}
161
+	stdout, _, err := run("git", "log", "--oneline", fmt.Sprintf("%s..%s", a, b))
162
+	if err != nil {
163
+		return nil, fmt.Errorf("error executing git log: %s", err)
164
+	}
165
+	for _, log := range strings.Split(stdout, "\n") {
166
+		if len(log) == 0 {
167
+			continue
168
+		}
169
+		commit, err := NewCommitFromOnelineLog(log)
170
+		if err != nil {
171
+			return nil, err
172
+		}
173
+		commits = append(commits, commit)
174
+	}
175
+	return commits, nil
176
+}
177
+
178
+func NewCommitFromOnelineLog(log string) (Commit, error) {
179
+	var commit Commit
180
+	parts := strings.Split(log, " ")
181
+	if len(parts) < 2 {
182
+		return commit, fmt.Errorf("invalid log entry: %s", log)
183
+	}
184
+	commit.Sha = parts[0]
185
+	commit.Summary = strings.Join(parts[1:], " ")
186
+	files, err := filesInCommit(commit.Sha)
187
+	if err != nil {
188
+		return commit, err
189
+	}
190
+	commit.Files = files
191
+	return commit, nil
192
+}
193
+
194
+func filesInCommit(sha string) ([]File, error) {
195
+	files := []File{}
196
+	stdout, _, err := run("git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha)
197
+	if err != nil {
198
+		return nil, err
199
+	}
200
+	for _, filename := range strings.Split(stdout, "\n") {
201
+		if len(filename) == 0 {
202
+			continue
203
+		}
204
+		files = append(files, File(filename))
205
+	}
206
+	return files, nil
207
+}
208
+
209
+func run(args ...string) (string, string, error) {
210
+	cmd := exec.Command(args[0], args[1:]...)
211
+	var stdout bytes.Buffer
212
+	var stderr bytes.Buffer
213
+	cmd.Stdout = &stdout
214
+	cmd.Stderr = &stderr
215
+	err := cmd.Run()
216
+	return stdout.String(), stderr.String(), err
217
+}
... ...
@@ -638,3 +638,11 @@ os::build::gen-docs() {
638 638
 
639 639
   echo "Assets generated in ${dest}"
640 640
 }
641
+
642
+# os::build::find-binary locates a locally built binary for the current
643
+# platform and returns the path to the binary.
644
+os::build::find-binary() {
645
+  local bin="$1"
646
+  local path=$( (ls -t _output/local/bin/$(os::build::host_platform)/${bin}) 2>/dev/null || true | head -1 )
647
+  echo "$path"
648
+}
... ...
@@ -18,7 +18,7 @@ fi
18 18
 "${OS_ROOT}/hack/build-go.sh" cmd/genbashcomp
19 19
 
20 20
 # Find binary
21
-genbashcomp=$( (ls -t _output/local/bin/${platform}/genbashcomp) 2>/dev/null || true | head -1 )
21
+genbashcomp="$(os::build::find-binary genbashcomp)"
22 22
 
23 23
 if [[ ! "$genbashcomp" ]]; then
24 24
   {
... ...
@@ -12,7 +12,7 @@ source "${OS_ROOT}/hack/common.sh"
12 12
 "${OS_ROOT}/hack/build-go.sh" cmd/gendocs
13 13
 
14 14
 # Find binary
15
-gendocs=$( (ls -t _output/local/bin/$(os::build::host_platform)/gendocs) 2>/dev/null || true | head -1 )
15
+gendocs="$(os::build::find-binary gendocs)"
16 16
 
17 17
 if [[ -z "$gendocs" ]]; then
18 18
   {
... ...
@@ -20,7 +20,7 @@ else
20 20
   echo "$buildout" | sed 's/^/   /'
21 21
 fi
22 22
 
23
-genconversion="${OS_ROOT}/_output/local/bin/$(os::build::host_platform)/genconversion"
23
+genconversion="$(os::build::find-binary genconversion)"
24 24
 
25 25
 echo "   Verifying genconversion binary..."
26 26
 if [[ ! -x "$genconversion" ]]; then
... ...
@@ -20,7 +20,7 @@ else
20 20
   echo "$buildout" | sed 's/^/   /'
21 21
 fi
22 22
 
23
-gendeepcopy="${OS_ROOT}/_output/local/bin/$(os::build::host_platform)/gendeepcopy"
23
+gendeepcopy="$(os::build::find-binary gendeepcopy)"
24 24
 
25 25
 echo "   Verifying gendeepcopy binary..."
26 26
 if [[ ! -x "$gendeepcopy" ]]; then
27 27
new file mode 100755
... ...
@@ -0,0 +1,16 @@
0
+#!/bin/bash
1
+
2
+set -o errexit
3
+set -o nounset
4
+set -o pipefail
5
+
6
+OS_ROOT=$(dirname "${BASH_SOURCE}")/..
7
+source "${OS_ROOT}/hack/common.sh"
8
+
9
+"${OS_ROOT}/hack/build-go.sh" cmd/rebasehelpers/commitchecker
10
+
11
+# Find binary
12
+commitchecker="$(os::build::find-binary commitchecker)"
13
+echo "===== Verifying UPSTREAM Commits ====="
14
+$commitchecker
15
+echo "SUCCESS: All commits are valid."