/
fragment.go
199 lines (167 loc) · 5 KB
/
fragment.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package util
import (
"errors"
"fmt"
"io/ioutil"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/yaml"
)
type Fragment struct {
Entries []FragmentEntry `yaml:"entries"`
}
func (f *Fragment) Validate() error {
for i, e := range f.Entries {
if err := e.Validate(); err != nil {
return fmt.Errorf("entry[%d] invalid: %v", i, err)
}
}
return nil
}
type FragmentEntry struct {
Description string `json:"description"`
Kind EntryKind `json:"kind"`
Breaking bool `json:"breaking"`
Migration *EntryMigration `json:"migration,omitempty"`
PullRequest *uint `json:"pull_request_override,omitempty"`
PullRequestLink string `json:"-"`
}
func (e *FragmentEntry) Validate() error {
if err := e.Kind.Validate(); err != nil {
return fmt.Errorf("invalid kind: %v", err)
}
if len(e.Description) == 0 {
return errors.New("missing description")
}
if e.Breaking && e.Kind != Change && e.Kind != Removal {
return fmt.Errorf("breaking changes can only be kind %q or %q, got %q", Change, Removal, e.Kind)
}
if e.Breaking && e.Migration == nil {
return fmt.Errorf("breaking changes require migration sections")
}
if e.Migration != nil {
if err := e.Migration.Validate(); err != nil {
return fmt.Errorf("invalid migration: %v", err)
}
}
return nil
}
func (e FragmentEntry) pullRequestLink(repo string) string {
if e.PullRequest == nil {
return ""
}
return fmt.Sprintf("[#%d](https://%s/pull/%d)", *e.PullRequest, repo, *e.PullRequest)
}
type EntryKind string
const (
Addition EntryKind = "addition"
Change EntryKind = "change"
Removal EntryKind = "removal"
Deprecation EntryKind = "deprecation"
Bugfix EntryKind = "bugfix"
)
func (k EntryKind) Validate() error {
for _, t := range []EntryKind{Addition, Change, Removal, Deprecation, Bugfix} {
if k == t {
return nil
}
}
return fmt.Errorf("%q is not a supported kind", k)
}
type EntryMigration struct {
Header string `yaml:"header"`
Body string `yaml:"body"`
}
func (m EntryMigration) Validate() error {
if len(m.Header) == 0 {
return errors.New("header not specified")
}
if len(m.Body) == 0 {
return errors.New("body not specified")
}
return nil
}
func LoadEntries(fragmentsDir, repo string) ([]FragmentEntry, error) {
files, err := ioutil.ReadDir(fragmentsDir)
if err != nil {
return nil, fmt.Errorf("failed to read fragments directory: %w", err)
}
var entries []FragmentEntry
for _, fragFile := range files {
if fragFile.Name() == "00-template.yaml" {
continue
}
if fragFile.IsDir() {
log.Warnf("Skipping directory %q", fragFile.Name())
continue
}
if filepath.Ext(fragFile.Name()) != ".yaml" {
log.Warnf("Skipping non-YAML file %q", fragFile.Name())
continue
}
path := filepath.Join(fragmentsDir, fragFile.Name())
fragmentData, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read fragment file %q: %w", fragFile.Name(), err)
}
fragment := Fragment{}
if err := yaml.Unmarshal(fragmentData, &fragment); err != nil {
return nil, fmt.Errorf("failed to parse fragment file %q: %w", fragFile.Name(), err)
}
if err := fragment.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate fragment file %q: %w", fragFile.Name(), err)
}
prNum, err := prGetter.GetPullRequestNumberFor(path)
if err != nil {
log.Warn(err)
}
if prNum != 0 {
for i, e := range fragment.Entries {
if e.PullRequest == nil {
fragment.Entries[i].PullRequest = &prNum
}
}
}
for i, e := range fragment.Entries {
fragment.Entries[i].PullRequestLink = e.pullRequestLink(repo)
}
entries = append(entries, fragment.Entries...)
}
return entries, nil
}
var prGetter PullRequestNumberGetter = &gitPullRequestNumberGetter{}
type PullRequestNumberGetter interface {
GetPullRequestNumberFor(file string) (uint, error)
}
type gitPullRequestNumberGetter struct{}
func (g *gitPullRequestNumberGetter) GetPullRequestNumberFor(filename string) (uint, error) {
msg, err := g.getCommitMessage(filename)
if err != nil {
return 0, err
}
return g.parsePRNumber(msg)
}
func (g *gitPullRequestNumberGetter) getCommitMessage(filename string) (string, error) {
args := fmt.Sprintf("log --follow --pretty=format:%%s --diff-filter=A --find-renames=90%% %s", filename)
line, err := exec.Command("git", strings.Split(args, " ")...).CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to locate git commit for PR discovery: %v", err)
}
return string(line), nil
}
var numRegex = regexp.MustCompile(`\(#(\d+)\)$`)
func (g *gitPullRequestNumberGetter) parsePRNumber(msg string) (uint, error) {
matches := numRegex.FindAllStringSubmatch(msg, 1)
if len(matches) == 0 || len(matches[0]) < 2 {
return 0, fmt.Errorf("could not find PR number in commit message")
}
u64, err := strconv.ParseUint(matches[0][1], 10, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse PR number %q from commit message: %v", matches[0][1], err)
}
return uint(u64), nil
}