1
2
3
4
5 package main
6
7 import (
8 "fmt"
9 "strings"
10
11 "golang.org/x/exp/apidiff"
12 "golang.org/x/mod/module"
13 "golang.org/x/mod/semver"
14 "golang.org/x/tools/go/packages"
15 )
16
17
18
19 type report struct {
20
21
22
23 base moduleInfo
24
25
26
27
28 release moduleInfo
29
30
31
32 packages []packageReport
33
34
35 versionInvalid *versionMessage
36
37
38
39 haveCompatibleChanges bool
40
41
42
43 haveIncompatibleChanges bool
44
45
46
47 haveBaseErrors bool
48
49
50
51 haveReleaseErrors bool
52 }
53
54
55
56
57
58 func (r *report) String() string {
59 buf := &strings.Builder{}
60 for _, p := range r.packages {
61 buf.WriteString(p.String())
62 }
63
64 if !r.canVerifyReleaseVersion() {
65 return buf.String()
66 }
67
68 if len(r.release.diagnostics) > 0 {
69 buf.WriteString("# diagnostics\n")
70 for _, d := range r.release.diagnostics {
71 fmt.Fprintln(buf, d)
72 }
73 buf.WriteByte('\n')
74 }
75
76 buf.WriteString("# summary\n")
77 baseVersion := r.base.version
78 if r.base.modPath != r.release.modPath {
79 baseVersion = r.base.modPath + "@" + baseVersion
80 }
81 if r.base.versionInferred {
82 fmt.Fprintf(buf, "Inferred base version: %s\n", baseVersion)
83 } else if r.base.versionQuery != "" {
84 fmt.Fprintf(buf, "Base version: %s (%s)\n", baseVersion, r.base.versionQuery)
85 }
86
87 if r.versionInvalid != nil {
88 fmt.Fprintln(buf, r.versionInvalid)
89 } else if r.release.versionInferred {
90 if r.release.tagPrefix == "" {
91 fmt.Fprintf(buf, "Suggested version: %s\n", r.release.version)
92 } else {
93 fmt.Fprintf(buf, "Suggested version: %[1]s (with tag %[2]s%[1]s)\n", r.release.version, r.release.tagPrefix)
94 }
95 } else if r.release.version != "" {
96 if r.release.tagPrefix == "" {
97 fmt.Fprintf(buf, "%s is a valid semantic version for this release.\n", r.release.version)
98
99 if semver.Compare(r.release.version, "v0.0.0-99999999999999-zzzzzzzzzzzz") < 0 {
100 fmt.Fprintf(buf, `Note: %s sorts lower in MVS than pseudo-versions, which may be
101 unexpected for users. So, it may be better to choose a different suffix.`, r.release.version)
102 }
103 } else {
104 fmt.Fprintf(buf, "%[1]s (with tag %[2]s%[1]s) is a valid semantic version for this release\n", r.release.version, r.release.tagPrefix)
105 }
106 }
107
108 if r.versionInvalid == nil && r.haveBaseErrors {
109 fmt.Fprintln(buf, "Errors were found in the base version. Some API changes may be omitted.")
110 }
111
112 return buf.String()
113 }
114
115 func (r *report) addPackage(p packageReport) {
116 r.packages = append(r.packages, p)
117 if len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
118
119
120
121
122 for _, c := range p.Changes {
123 if !c.Compatible && len(p.releaseErrors) == 0 {
124 r.haveIncompatibleChanges = true
125 } else if c.Compatible && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
126 r.haveCompatibleChanges = true
127 }
128 }
129 }
130 if len(p.baseErrors) > 0 {
131 r.haveBaseErrors = true
132 }
133 if len(p.releaseErrors) > 0 {
134 r.haveReleaseErrors = true
135 }
136 }
137
138
139
140
141 func (r *report) validateReleaseVersion() {
142 if r.release.version == "" {
143 panic("validateVersion called without version")
144 }
145 setNotValid := func(format string, args ...interface{}) {
146 r.versionInvalid = &versionMessage{
147 message: fmt.Sprintf("%s is not a valid semantic version for this release.", r.release.version),
148 reason: fmt.Sprintf(format, args...),
149 }
150 }
151
152 if r.haveReleaseErrors {
153 if r.haveReleaseErrors {
154 setNotValid("Errors were found in one or more packages.")
155 return
156 }
157 }
158
159
160
161
162 _, suffix, ok := module.SplitPathVersion(r.release.modPath)
163 if !ok {
164 setNotValid("%s: could not find version suffix in module path", r.release.modPath)
165 return
166 }
167 if suffix != "" {
168 if suffix[0] != '/' && suffix[0] != '.' {
169 setNotValid("%s: unknown module path version suffix: %q", r.release.modPath, suffix)
170 return
171 }
172 pathMajor := suffix[1:]
173 major := semver.Major(r.release.version)
174 if pathMajor != major {
175 setNotValid(`The major version %s does not match the major version suffix
176 in the module path: %s`, major, r.release.modPath)
177 return
178 }
179 } else if major := semver.Major(r.release.version); major != "v0" && major != "v1" {
180 setNotValid(`The module path does not end with the major version suffix /%s,
181 which is required for major versions v2 or greater.`, major)
182 return
183 }
184
185 for _, v := range r.base.existingVersions {
186 if semver.Compare(v, r.release.version) == 0 {
187 setNotValid("version %s already exists", v)
188 }
189 }
190
191
192 if semver.Major(r.base.version) == "v0" || r.base.modPath != r.release.modPath {
193 return
194 }
195 if r.haveIncompatibleChanges {
196 setNotValid("There are incompatible changes.")
197 return
198 }
199 if r.haveCompatibleChanges && semver.MajorMinor(r.base.version) == semver.MajorMinor(r.release.version) {
200 setNotValid(`There are compatible changes, but the minor version is not incremented
201 over the base version (%s).`, r.base.version)
202 return
203 }
204
205 if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, r.release.version) > 0 {
206 setNotValid(`Module indirectly depends on a higher version of itself (%s).
207 `, r.release.highestTransitiveVersion)
208 }
209 }
210
211
212
213 func (r *report) suggestReleaseVersion() {
214 setNotValid := func(format string, args ...interface{}) {
215 r.versionInvalid = &versionMessage{
216 message: "Cannot suggest a release version.",
217 reason: fmt.Sprintf(format, args...),
218 }
219 }
220 setVersion := func(v string) {
221 r.release.version = v
222 r.release.versionInferred = true
223 }
224
225 if r.base.modPath != r.release.modPath {
226 setNotValid("Base module path is different from release.")
227 return
228 }
229
230 if r.haveReleaseErrors || r.haveBaseErrors {
231 setNotValid("Errors were found.")
232 return
233 }
234
235 var major, minor, patch, pre string
236 if r.base.version != "none" {
237 minVersion := r.base.version
238 if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, minVersion) > 0 {
239 setNotValid("Module indirectly depends on a higher version of itself (%s) than the base version (%s).", r.release.highestTransitiveVersion, r.base.version)
240 return
241 }
242
243 var err error
244 major, minor, patch, pre, _, err = parseVersion(minVersion)
245 if err != nil {
246 panic(fmt.Sprintf("could not parse base version: %v", err))
247 }
248 }
249
250 if r.haveIncompatibleChanges && r.base.version != "none" && pre == "" && major != "0" {
251 setNotValid("Incompatible changes were detected.")
252 return
253
254
255 }
256
257
258
259
260
261 var latestForBaseMajor string
262 for _, v := range r.base.existingVersions {
263 if semver.Major(v) != semver.Major(r.base.version) {
264 continue
265 }
266 if latestForBaseMajor == "" || semver.Compare(latestForBaseMajor, v) < 0 {
267 latestForBaseMajor = v
268 }
269 }
270 if latestForBaseMajor != "" && latestForBaseMajor != r.base.version {
271 setNotValid(fmt.Sprintf("Can only suggest a release version when compared against the most recent version of this major: %s.", latestForBaseMajor))
272 return
273 }
274
275 if r.base.version == "none" {
276 if _, pathMajor, ok := module.SplitPathVersion(r.release.modPath); !ok {
277 panic(fmt.Sprintf("could not parse module path %q", r.release.modPath))
278 } else if pathMajor == "" {
279 setVersion("v0.1.0")
280 } else {
281 setVersion(pathMajor[1:] + ".0.0")
282 }
283 return
284 }
285
286 if pre != "" {
287
288 } else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") || r.requirementsChanged() {
289 minor = incDecimal(minor)
290 patch = "0"
291 } else {
292 patch = incDecimal(patch)
293 }
294 setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch))
295 return
296 }
297
298
299
300 func (r *report) canVerifyReleaseVersion() bool {
301
302
303
304
305 basePath := strings.TrimSuffix(r.base.modPath, r.base.modPathMajor)
306 releasePath := strings.TrimSuffix(r.release.modPath, r.release.modPathMajor)
307 return basePath == releasePath
308 }
309
310
311
312
313
314
315
316
317
318
319
320
321 func (r *report) requirementsChanged() bool {
322 if r.base.goModFile == nil {
323
324 return true
325 }
326
327
328
329 baseReqs := make(map[string]string)
330 for _, r := range r.base.goModFile.Require {
331 baseReqs[r.Mod.Path] = r.Mod.Version
332 }
333
334 for _, r := range r.release.goModFile.Require {
335 if _, ok := baseReqs[r.Mod.Path]; !ok {
336
337
338 return true
339 }
340 if semver.Compare(semver.MajorMinor(r.Mod.Version), semver.MajorMinor(baseReqs[r.Mod.Path])) > 0 {
341
342 return true
343 }
344 }
345
346 if r.release.goModFile.Go != nil && r.base.goModFile.Go != nil {
347 if r.release.goModFile.Go.Version > r.base.goModFile.Go.Version {
348
349 return true
350 }
351 }
352
353 return false
354 }
355
356
357
358 func (r *report) isSuccessful() bool {
359 return len(r.release.diagnostics) == 0 && r.versionInvalid == nil
360 }
361
362 type versionMessage struct {
363 message, reason string
364 }
365
366 func (m versionMessage) String() string {
367 return m.message + "\n" + m.reason + "\n"
368 }
369
370
371 func incDecimal(decimal string) string {
372
373 digits := []byte(decimal)
374 i := len(digits) - 1
375 for ; i >= 0 && digits[i] == '9'; i-- {
376 digits[i] = '0'
377 }
378 if i >= 0 {
379 digits[i]++
380 } else {
381
382 digits[0] = '1'
383 digits = append(digits, '0')
384 }
385 return string(digits)
386 }
387
388 type packageReport struct {
389 apidiff.Report
390 path string
391 baseErrors, releaseErrors []packages.Error
392 }
393
394 func (p *packageReport) String() string {
395 if len(p.Changes) == 0 && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
396 return ""
397 }
398 buf := &strings.Builder{}
399 fmt.Fprintf(buf, "# %s\n", p.path)
400 if len(p.baseErrors) > 0 {
401 fmt.Fprintf(buf, "## errors in base version:\n")
402 for _, e := range p.baseErrors {
403 fmt.Fprintln(buf, e)
404 }
405 buf.WriteByte('\n')
406 }
407 if len(p.releaseErrors) > 0 {
408 fmt.Fprintf(buf, "## errors in release version:\n")
409 for _, e := range p.releaseErrors {
410 fmt.Fprintln(buf, e)
411 }
412 buf.WriteByte('\n')
413 }
414 if len(p.Changes) > 0 {
415 var compatible, incompatible []apidiff.Change
416 for _, c := range p.Changes {
417 if c.Compatible {
418 compatible = append(compatible, c)
419 } else {
420 incompatible = append(incompatible, c)
421 }
422 }
423 if len(incompatible) > 0 {
424 fmt.Fprintf(buf, "## incompatible changes\n")
425 for _, c := range incompatible {
426 fmt.Fprintln(buf, c.Message)
427 }
428 }
429 if len(compatible) > 0 {
430 fmt.Fprintf(buf, "## compatible changes\n")
431 for _, c := range compatible {
432 fmt.Fprintln(buf, c.Message)
433 }
434 }
435 buf.WriteByte('\n')
436 }
437 return buf.String()
438 }
439
440
441
442
443
444 func parseVersion(vers string) (major, minor, patch, pre, meta string, err error) {
445 if !strings.HasPrefix(vers, "v") {
446 return "", "", "", "", "", fmt.Errorf("version %q does not start with 'v'", vers)
447 }
448 base := vers[1:]
449 if i := strings.IndexByte(base, '+'); i >= 0 {
450 meta = base[i+1:]
451 base = base[:i]
452 }
453 if i := strings.IndexByte(base, '-'); i >= 0 {
454 pre = base[i+1:]
455 base = base[:i]
456 }
457 parts := strings.Split(base, ".")
458 if len(parts) != 3 {
459 return "", "", "", "", "", fmt.Errorf("version %q should have three numbers", vers)
460 }
461 major, minor, patch = parts[0], parts[1], parts[2]
462 return major, minor, patch, pre, meta, nil
463 }
464
View as plain text