Skip to content

Commit b7530de

Browse files
authored
Add version.Delta for classifying version changes (#27)
Signed-off-by: Kimmo Lehto <[email protected]>
1 parent 18aa613 commit b7530de

File tree

3 files changed

+179
-0
lines changed

3 files changed

+179
-0
lines changed

delta.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package version
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// Delta represents the differences between two versions.
8+
type Delta struct {
9+
a, b *Version
10+
MajorUpgrade bool
11+
MinorUpgrade bool
12+
PatchUpgrade bool
13+
K0sUpgrade bool
14+
Equal bool
15+
Downgrade bool
16+
PrereleaseOnly bool
17+
BuildMetadataChange bool
18+
Consecutive bool
19+
}
20+
21+
// NewDelta analyzes the differences between two versions and returns a Delta.
22+
func NewDelta(a, b *Version) Delta {
23+
if a == nil || b == nil {
24+
panic("NewDelta called with a nil Version")
25+
}
26+
27+
cmp := a.Compare(b)
28+
majorEqual, minorEqual, patchEqual := a.segmentEqual(b, 0), a.segmentEqual(b, 1), a.segmentEqual(b, 2)
29+
lessThan := cmp < 0
30+
31+
d := Delta{
32+
a: a,
33+
b: b,
34+
MajorUpgrade: lessThan && a.segments[0] < b.segments[0],
35+
MinorUpgrade: lessThan && majorEqual && a.segments[1] < b.segments[1],
36+
PatchUpgrade: lessThan && majorEqual && minorEqual && a.segments[2] < b.segments[2],
37+
Equal: cmp == 0,
38+
Downgrade: cmp > 0,
39+
K0sUpgrade: majorEqual && minorEqual && patchEqual && a.pre == b.pre && a.isK0s && b.isK0s && a.k0s < b.k0s,
40+
PrereleaseOnly: lessThan && a.Patch() == b.Patch() && (a.pre != "" || b.pre != ""),
41+
BuildMetadataChange: a.meta != b.meta,
42+
}
43+
44+
switch {
45+
case d.PatchUpgrade:
46+
d.Consecutive = b.segments[2]-a.segments[2] == 1
47+
case d.MinorUpgrade:
48+
d.Consecutive = b.segments[1]-a.segments[1] == 1 && b.segments[2] == 0
49+
case d.MajorUpgrade:
50+
d.Consecutive = b.segments[0]-a.segments[0] == 1 && b.segments[1] == 0 && b.segments[2] == 0
51+
case d.K0sUpgrade:
52+
d.Consecutive = b.k0s-a.k0s == 1
53+
}
54+
55+
return d
56+
}
57+
58+
func (d Delta) conseq() string {
59+
if d.Consecutive {
60+
return "consecutive"
61+
}
62+
return "non-consecutive"
63+
}
64+
65+
// String returns a human-readable representation of the Delta.
66+
func (d Delta) String() string {
67+
if d.Downgrade {
68+
return fmt.Sprintf("%s is a downgrade from %s", d.b, d.a)
69+
}
70+
if d.MajorUpgrade {
71+
return fmt.Sprintf("a %s major upgrade from %s to %s", d.conseq(), d.a.Major(), d.b.Major())
72+
}
73+
if d.MinorUpgrade {
74+
return fmt.Sprintf("a %s minor upgrade from %s to %s", d.conseq(), d.a.Minor(), d.b.Minor())
75+
}
76+
if d.PrereleaseOnly {
77+
if d.b.pre == "" {
78+
return fmt.Sprintf("an upgrade from a %s pre-release to stable", d.a.Patch())
79+
}
80+
return fmt.Sprintf("an upgrade between pre-release versions of %s", d.a.Patch())
81+
}
82+
if d.PatchUpgrade {
83+
return fmt.Sprintf("a %s patch upgrade to %s", d.conseq(), d.b)
84+
}
85+
86+
if d.K0sUpgrade {
87+
return fmt.Sprintf("a %s k0s upgrade to k0s build %d", d.conseq(), d.b.k0s)
88+
}
89+
90+
if d.BuildMetadataChange {
91+
return fmt.Sprintf("build metadata changes from %q to %q", d.a.meta, d.b.meta)
92+
}
93+
94+
return "no change"
95+
}

delta_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package version_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/k0sproject/version"
8+
)
9+
10+
func TestDelta(t *testing.T) {
11+
tests := []struct {
12+
a, b string
13+
expect string
14+
}{
15+
{"v1.0.0", "v1.0.1", "a consecutive patch upgrade to v1.0.1"},
16+
{"v1.0.1", "v1.0.3", "a non-consecutive patch upgrade to v1.0.3"},
17+
{"v1.0.0", "v1.1.0", "a consecutive minor upgrade from v1.0 to v1.1"},
18+
{"v1.0.0", "v2.0.0", "a consecutive major upgrade from v1 to v2"},
19+
{"v1.0.1", "v1.0.0", "v1.0.0 is a downgrade from v1.0.1"},
20+
{"v1.0.0-alpha", "v1.0.0", "an upgrade from a v1.0.0 pre-release to stable"},
21+
{"v1.0.0-alpha.1", "v1.0.0-alpha.2", "an upgrade between pre-release versions of v1.0.0"},
22+
{"v1.0.0+build1", "v1.0.0+build2", "build metadata changes from \"build1\" to \"build2\""},
23+
{"v1.0.0", "v1.0.0", "no change"},
24+
{"v1.0.0-rc.1+k0s.1", "v1.0.0-rc.1+k0s.1", "no change"},
25+
{"v1.1.1", "v2.1.0", "a non-consecutive major upgrade from v1 to v2"},
26+
{"v1.1.1", "v1.2.0", "a consecutive minor upgrade from v1.1 to v1.2"},
27+
{"v1.1.1+k0s.0", "v1.1.1+k0s.2", "a non-consecutive k0s upgrade to k0s build 2"},
28+
{"v1.1.1+k0s.0", "v1.1.1+k0s.1", "a consecutive k0s upgrade to k0s build 1"},
29+
{"v1.1.1+k0s.0", "v1.3", "a non-consecutive minor upgrade from v1.1 to v1.3"},
30+
{"v1.1.1+k0s.0", "v2", "a consecutive major upgrade from v1 to v2"},
31+
}
32+
33+
for _, test := range tests {
34+
t.Run("delta from "+test.a+" to "+test.b, func(t *testing.T) {
35+
a, err := version.NewVersion(test.a)
36+
NoError(t, err)
37+
b, err := version.NewVersion(test.b)
38+
NoError(t, err)
39+
delta := version.NewDelta(a, b)
40+
if result := delta.String(); result != test.expect {
41+
t.Errorf("expected: %q, got: %q", test.expect, result)
42+
}
43+
})
44+
}
45+
}
46+
47+
func ExampleDelta() {
48+
a, _ := version.NewVersion("v1.0.0")
49+
b, _ := version.NewVersion("v1.2.1")
50+
delta := version.NewDelta(a, b)
51+
fmt.Printf("patch upgrade: %t\n", delta.PatchUpgrade)
52+
fmt.Println(delta.String())
53+
// Output:
54+
// patch upgrade: false
55+
// a non-consecutive minor upgrade from v1.0 to v1.2
56+
}

version.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,34 @@ func (v *Version) Satisfies(constraint Constraints) bool {
413413
return constraint.Check(v)
414414
}
415415

416+
// Delta returns a comparison to the given version
417+
func (v *Version) Delta(b *Version) Delta {
418+
return NewDelta(v, b)
419+
}
420+
421+
// segmentEqual checks if the segments at the specified index are equal between two versions.
422+
func (v *Version) segmentEqual(b *Version, index int) bool {
423+
if v == nil || b == nil || index < 0 || index >= maxSegments {
424+
return false
425+
}
426+
return v.segments[index] == b.segments[index]
427+
}
428+
429+
// Major returns a string like "v2" from a version like 2.0.0
430+
func (v *Version) Major() string {
431+
return fmt.Sprintf("v%d", v.segments[0])
432+
}
433+
434+
// Minor returns a string like "v2.3" from a version like 2.3.0
435+
func (v *Version) Minor() string {
436+
return fmt.Sprintf("v%d.%d", v.segments[0], v.segments[1])
437+
}
438+
439+
// Patch returns a string like "v2.3.4" from a version like 2.3.4-rc.1
440+
func (v *Version) Patch() string {
441+
return fmt.Sprintf("v%d.%d.%d", v.segments[0], v.segments[1], v.segments[2])
442+
}
443+
416444
// MustParse is like NewVersion but panics if the version cannot be parsed.
417445
// It simplifies safe initialization of global variables.
418446
func MustParse(v string) *Version {

0 commit comments

Comments
 (0)