-
Notifications
You must be signed in to change notification settings - Fork 46
/
semver.go
224 lines (183 loc) · 5.87 KB
/
semver.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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
Package semver provides functionality to parse and process semantic versions, as they are used in multiple components of Constellation.
The official [semantic versioning specification] disallows leading "v" prefixes.
However, the Constellation config uses the "v" prefix for versions to make version strings more recognizable.
This package bridges the gap between Go's semver pkg (doesn't allow "v" prefix) and the Constellation config (requires "v" prefix).
[semantic versioning specification]: https://semver.org/
*/
package semver
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"golang.org/x/mod/semver"
)
// Sort sorts a list of semantic version strings using [ByVersion].
func Sort(list []Semver) {
sort.Sort(byVersion(list))
}
// Semver represents a semantic version.
type Semver struct {
major int
minor int
patch int
prerelease string
}
// New returns a Version from a string.
func New(version string) (Semver, error) {
// ensure that semver has "v" prefix
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
if !semver.IsValid(version) {
return Semver{}, fmt.Errorf("invalid semver: %s", version)
}
version = semver.Canonical(version)
var major, minor, patch int
_, pre, _ := strings.Cut(version, "-")
_, err := fmt.Sscanf(version, "v%d.%d.%d", &major, &minor, &patch)
if err != nil {
return Semver{}, fmt.Errorf("parsing semver parts: %w", err)
}
return Semver{
major: major,
minor: minor,
patch: patch,
prerelease: pre,
}, nil
}
// NewFromInt constructs a new Semver from three integers and prerelease string: MAJOR.MINOR.PATCH-PRERELEASE.
func NewFromInt(major, minor, patch int, prerelease string) Semver {
return Semver{
major: major,
minor: minor,
patch: patch,
prerelease: prerelease,
}
}
// NewSlice returns a slice of Semver from a slice of strings.
func NewSlice(in []string) ([]Semver, error) {
var out []Semver
for _, version := range in {
semVersion, err := New(version)
if err != nil {
return nil, fmt.Errorf("parsing version %s: %w", version, err)
}
out = append(out, semVersion)
}
return out, nil
}
// ToStrings converts a slice of Semver to a slice of strings.
func ToStrings(in []Semver) []string {
var out []string
for _, v := range in {
out = append(out, v.String())
}
return out
}
// Major returns the major version of the object.
func (v Semver) Major() int {
return v.major
}
// Minor returns the minor version of the object.
func (v Semver) Minor() int {
return v.minor
}
// Patch returns the patch version of the object.
func (v Semver) Patch() int {
return v.patch
}
// Prerelease returns the prerelease section of the object.
func (v Semver) Prerelease() string {
return v.prerelease
}
// String returns the string representation of the version.
func (v Semver) String() string {
version := fmt.Sprintf("v%d.%d.%d", v.major, v.minor, v.patch)
if v.prerelease != "" {
return fmt.Sprintf("%s-%s", version, v.prerelease)
}
return version
}
// Compare compares two versions. It relies on the semver.Compare function internally.
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
func (v Semver) Compare(other Semver) int {
return semver.Compare(v.String(), other.String())
}
// MajorMinorEqual returns if the major and minor version of two versions are equal.
func (v Semver) MajorMinorEqual(other Semver) bool {
return v.major == other.major && v.minor == other.minor
}
// IsUpgradeTo returns if a version is an upgrade to another version.
// It checks if the version of v is greater than the version of other and allows a drift of at most one minor version.
func (v Semver) IsUpgradeTo(other Semver) error {
if v.Compare(other) <= 0 {
return compatibility.NewInvalidUpgradeError(other.String(), v.String(), errors.New("current version newer than or equal to new version"))
}
if v.major != other.major {
return compatibility.NewInvalidUpgradeError(other.String(), v.String(), compatibility.ErrMajorMismatch)
}
if v.minor-other.minor > 1 {
return compatibility.NewInvalidUpgradeError(other.String(), v.String(), compatibility.ErrMinorDrift)
}
return nil
}
// NextMinor returns the next minor version in the format "vMAJOR.MINOR+1".
func (v Semver) NextMinor() string {
return fmt.Sprintf("v%d.%d", v.major, v.minor+1)
}
// MarshalYAML implements the yaml.Marshaller interface.
func (v Semver) MarshalYAML() (any, error) {
return v.String(), nil
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (v *Semver) UnmarshalYAML(unmarshal func(any) error) error {
var raw string
if err := unmarshal(&raw); err != nil {
return fmt.Errorf("unmarshalling to string: %w", err)
}
version, err := New(raw)
if err != nil {
return fmt.Errorf("parsing semantic version: %w", err)
}
*v = version
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (v Semver) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, v.String())), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (v *Semver) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
version, err := New(s)
if err != nil {
return err
}
*v = version
return nil
}
// byVersion implements [sort.Interface] for sorting semantic version strings.
// Copied from Go's semver pkg with minimal modification.
// https://cs.opensource.google/go/x/mod/+/master:semver/semver.go
type byVersion []Semver
func (vs byVersion) Len() int { return len(vs) }
func (vs byVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
func (vs byVersion) Less(i, j int) bool {
cmp := vs[i].Compare(vs[j])
if cmp != 0 {
return cmp < 0
}
// if versions are equal, sort by lexicographic order
return vs[i].String() < vs[j].String()
}