Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add version.Delta for classifying version changes #27

Merged
merged 1 commit into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions delta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package version

import (
"fmt"
)

// Delta represents the differences between two versions.
type Delta struct {
a, b *Version
MajorUpgrade bool
MinorUpgrade bool
PatchUpgrade bool
K0sUpgrade bool
Equal bool
Downgrade bool
PrereleaseOnly bool
BuildMetadataChange bool
Consecutive bool
}

// NewDelta analyzes the differences between two versions and returns a Delta.
func NewDelta(a, b *Version) Delta {
if a == nil || b == nil {
panic("NewDelta called with a nil Version")
}

cmp := a.Compare(b)
majorEqual, minorEqual, patchEqual := a.segmentEqual(b, 0), a.segmentEqual(b, 1), a.segmentEqual(b, 2)
lessThan := cmp < 0

d := Delta{
a: a,
b: b,
MajorUpgrade: lessThan && a.segments[0] < b.segments[0],
MinorUpgrade: lessThan && majorEqual && a.segments[1] < b.segments[1],
PatchUpgrade: lessThan && majorEqual && minorEqual && a.segments[2] < b.segments[2],
Equal: cmp == 0,
Downgrade: cmp > 0,
K0sUpgrade: majorEqual && minorEqual && patchEqual && a.pre == b.pre && a.isK0s && b.isK0s && a.k0s < b.k0s,
PrereleaseOnly: lessThan && a.Patch() == b.Patch() && (a.pre != "" || b.pre != ""),
BuildMetadataChange: a.meta != b.meta,
}

switch {
case d.PatchUpgrade:
d.Consecutive = b.segments[2]-a.segments[2] == 1
case d.MinorUpgrade:
d.Consecutive = b.segments[1]-a.segments[1] == 1 && b.segments[2] == 0
case d.MajorUpgrade:
d.Consecutive = b.segments[0]-a.segments[0] == 1 && b.segments[1] == 0 && b.segments[2] == 0
case d.K0sUpgrade:
d.Consecutive = b.k0s-a.k0s == 1
}

return d
}

func (d Delta) conseq() string {
if d.Consecutive {
return "consecutive"
}
return "non-consecutive"
}

// String returns a human-readable representation of the Delta.
func (d Delta) String() string {
if d.Downgrade {
return fmt.Sprintf("%s is a downgrade from %s", d.b, d.a)
}
if d.MajorUpgrade {
return fmt.Sprintf("a %s major upgrade from %s to %s", d.conseq(), d.a.Major(), d.b.Major())
}
if d.MinorUpgrade {
return fmt.Sprintf("a %s minor upgrade from %s to %s", d.conseq(), d.a.Minor(), d.b.Minor())
}
if d.PrereleaseOnly {
if d.b.pre == "" {
return fmt.Sprintf("an upgrade from a %s pre-release to stable", d.a.Patch())
}
return fmt.Sprintf("an upgrade between pre-release versions of %s", d.a.Patch())
}
if d.PatchUpgrade {
return fmt.Sprintf("a %s patch upgrade to %s", d.conseq(), d.b)
}

if d.K0sUpgrade {
return fmt.Sprintf("a %s k0s upgrade to k0s build %d", d.conseq(), d.b.k0s)
}

if d.BuildMetadataChange {
return fmt.Sprintf("build metadata changes from %q to %q", d.a.meta, d.b.meta)
}

return "no change"
}
56 changes: 56 additions & 0 deletions delta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package version_test

import (
"fmt"
"testing"

"github.com/k0sproject/version"
)

func TestDelta(t *testing.T) {
tests := []struct {
a, b string
expect string
}{
{"v1.0.0", "v1.0.1", "a consecutive patch upgrade to v1.0.1"},
{"v1.0.1", "v1.0.3", "a non-consecutive patch upgrade to v1.0.3"},
{"v1.0.0", "v1.1.0", "a consecutive minor upgrade from v1.0 to v1.1"},
{"v1.0.0", "v2.0.0", "a consecutive major upgrade from v1 to v2"},
{"v1.0.1", "v1.0.0", "v1.0.0 is a downgrade from v1.0.1"},
{"v1.0.0-alpha", "v1.0.0", "an upgrade from a v1.0.0 pre-release to stable"},
{"v1.0.0-alpha.1", "v1.0.0-alpha.2", "an upgrade between pre-release versions of v1.0.0"},
{"v1.0.0+build1", "v1.0.0+build2", "build metadata changes from \"build1\" to \"build2\""},
{"v1.0.0", "v1.0.0", "no change"},
{"v1.0.0-rc.1+k0s.1", "v1.0.0-rc.1+k0s.1", "no change"},
{"v1.1.1", "v2.1.0", "a non-consecutive major upgrade from v1 to v2"},
{"v1.1.1", "v1.2.0", "a consecutive minor upgrade from v1.1 to v1.2"},
{"v1.1.1+k0s.0", "v1.1.1+k0s.2", "a non-consecutive k0s upgrade to k0s build 2"},
{"v1.1.1+k0s.0", "v1.1.1+k0s.1", "a consecutive k0s upgrade to k0s build 1"},
{"v1.1.1+k0s.0", "v1.3", "a non-consecutive minor upgrade from v1.1 to v1.3"},
{"v1.1.1+k0s.0", "v2", "a consecutive major upgrade from v1 to v2"},
}

for _, test := range tests {
t.Run("delta from "+test.a+" to "+test.b, func(t *testing.T) {
a, err := version.NewVersion(test.a)
NoError(t, err)
b, err := version.NewVersion(test.b)
NoError(t, err)
delta := version.NewDelta(a, b)
if result := delta.String(); result != test.expect {
t.Errorf("expected: %q, got: %q", test.expect, result)
}
})
}
}

func ExampleDelta() {
a, _ := version.NewVersion("v1.0.0")
b, _ := version.NewVersion("v1.2.1")
delta := version.NewDelta(a, b)
fmt.Printf("patch upgrade: %t\n", delta.PatchUpgrade)
fmt.Println(delta.String())
// Output:
// patch upgrade: false
// a non-consecutive minor upgrade from v1.0 to v1.2
}
28 changes: 28 additions & 0 deletions version.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,34 @@ func (v *Version) Satisfies(constraint Constraints) bool {
return constraint.Check(v)
}

// Delta returns a comparison to the given version
func (v *Version) Delta(b *Version) Delta {
return NewDelta(v, b)
}

// segmentEqual checks if the segments at the specified index are equal between two versions.
func (v *Version) segmentEqual(b *Version, index int) bool {
if v == nil || b == nil || index < 0 || index >= maxSegments {
return false
}
return v.segments[index] == b.segments[index]
}

// Major returns a string like "v2" from a version like 2.0.0
func (v *Version) Major() string {
return fmt.Sprintf("v%d", v.segments[0])
}

// Minor returns a string like "v2.3" from a version like 2.3.0
func (v *Version) Minor() string {
return fmt.Sprintf("v%d.%d", v.segments[0], v.segments[1])
}

// Patch returns a string like "v2.3.4" from a version like 2.3.4-rc.1
func (v *Version) Patch() string {
return fmt.Sprintf("v%d.%d.%d", v.segments[0], v.segments[1], v.segments[2])
}

// MustParse is like NewVersion but panics if the version cannot be parsed.
// It simplifies safe initialization of global variables.
func MustParse(v string) *Version {
Expand Down
Loading