Skip to content

Commit

Permalink
feat: Move pkg diff to separate package
Browse files Browse the repository at this point in the history
  • Loading branch information
matbme committed Nov 29, 2023
1 parent 1d5867e commit c5fad59
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 120 deletions.
5 changes: 3 additions & 2 deletions core/handlers/handle_releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin"
"github.com/vanilla-os/differ/core"
"github.com/vanilla-os/differ/diff"
"github.com/vanilla-os/differ/types"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -107,7 +108,7 @@ func HandleGetReleaseDiff(c *gin.Context) {
var diff struct {
OldDigest string `json:"_old_digest"`
NewDigest string `json:"_new_digest"`
Added, Upgraded, Downgraded, Removed []types.PackageDiff
Added, Upgraded, Downgraded, Removed []diff.PackageDiff
}
err := sonic.Unmarshal(cacheDiff, &diff)
if err != nil {
Expand Down Expand Up @@ -142,7 +143,7 @@ func HandleGetReleaseDiff(c *gin.Context) {
added, upgraded, downgraded, removed := newRelease.DiffPackages(oldRelease)

cacheDiffEntry := struct {
Added, Upgraded, Downgraded, Removed []types.PackageDiff
Added, Upgraded, Downgraded, Removed []diff.PackageDiff
}{
Added: added,
Upgraded: upgraded,
Expand Down
141 changes: 141 additions & 0 deletions diff/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package diff

import (
"cmp"
"regexp"
"strconv"
)

type Package map[string]string

type PackageDiff struct {
Name string `json:"name"`
NewVersion string `json:"new_version,omitempty"`
PreviousVersion string `json:"previous_version,omitempty"`
}

// This monstruosity is an adaptation of the regex for semver (available in https://semver.org/).
// It SHOULD be able to capture every type of exoteric versioning scheme out there.
var versionRegex = regexp.MustCompile(`^(?:(?P<prefix>\d+):)?(?P<major>\d+[a-zA-Z]?)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>\d+))?(?:[-~](?P<prerelease>(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:[+.](?P<buildmetadata>[0-9a-zA-Z-+.]+(?:\.[0-9a-zA-Z-]+)*))?$`)

// compareVersions has the same behavior as cmp.Compare, but for package versions. It parses
// both version strings and checks for differences in major, minor, patch, pre-release, etc.
func CompareVersions(a, b string) int {
aMatchStr := versionRegex.FindStringSubmatch(a)
aMatch := make(map[string]string)
for i, name := range versionRegex.SubexpNames() {
if i != 0 && name != "" && aMatchStr[i] != "" {
aMatch[name] = aMatchStr[i]
}
}

bMatchStr := versionRegex.FindStringSubmatch(b)
bMatch := make(map[string]string)
for i, name := range versionRegex.SubexpNames() {
if i != 0 && name != "" && bMatchStr[i] != "" {
bMatch[name] = bMatchStr[i]
}
}

compResult := 0

compOrder := []string{"prefix", "major", "minor", "patch", "prerelease", "buildmetadata"}
for _, comp := range compOrder {
aValue, aOk := aMatch[comp]
bValue, bOk := bMatch[comp]
// If neither version has component or if they equal
if !aOk && !bOk {
continue
}
// If a has component but b doesn't, package was upgraded, unless it's prerelease
if aOk && !bOk {
if comp == "prerelease" {
compResult = -1
} else {
compResult = 1
}
break
}
// If b has component but a doesn't, package was downgraded
if !aOk && bOk {
compResult = -1
break
}

// If both have, do regular compare
aValueInt, aErr := strconv.Atoi(aValue)
bValueInt, bErr := strconv.Atoi(bValue)

var abComp int
if aErr == nil && bErr == nil {
abComp = cmp.Compare(aValueInt, bValueInt)
} else {
abComp = cmp.Compare(aValue, bValue)
}
if abComp == 0 {
continue
}
compResult = abComp
break
}

return compResult
}

// PackageDiff returns the difference in packages between two images, organized into
// four slices: Added, Upgraded, Downgraded, and Removed packages, respectively.
func DiffPackages(oldPackages, newPackages Package) ([]PackageDiff, []PackageDiff, []PackageDiff, []PackageDiff) {
c := make(chan struct {
PackageDiff
int
})

newPkgsCopy := make(Package, len(newPackages))
for k, v := range newPackages {
newPkgsCopy[k] = v
}

for pkg, oldVersion := range oldPackages {
if newVersion, ok := newPkgsCopy[pkg]; ok {
go func(diff PackageDiff) {
result := CompareVersions(diff.PreviousVersion, diff.NewVersion)
c <- struct {
PackageDiff
int
}{diff, result}
}(PackageDiff{pkg, newVersion, oldVersion})
} else {
go func(diff PackageDiff) {
c <- struct {
PackageDiff
int
}{diff, 2}
}(PackageDiff{pkg, oldVersion, ""})
}

// Clear package from copy so we can later check for removed packages
delete(newPkgsCopy, pkg)
}

removed := []PackageDiff{}
for pkg, version := range newPkgsCopy {
removed = append(removed, PackageDiff{pkg, "", version})
}

added := []PackageDiff{}
upgraded := []PackageDiff{}
downgraded := []PackageDiff{}
for i := 0; i < len(oldPackages); i++ {
pkg := <-c
switch pkg.int {
case -1:
downgraded = append(downgraded, pkg.PackageDiff)
case 1:
upgraded = append(upgraded, pkg.PackageDiff)
case 2:
added = append(added, pkg.PackageDiff)
}
}

return added, upgraded, downgraded, removed
}
3 changes: 3 additions & 0 deletions diff/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/vanilla-os/differ/diff

go 1.21.4
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module github.com/vanilla-os/differ

go 1.21
go 1.21.4

require (
github.com/eko/gocache/store/ristretto/v4 v4.2.1
github.com/gin-gonic/gin v1.9.1
github.com/vanilla-os/differ/diff v0.0.0-00010101000000-000000000000
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)
Expand All @@ -22,7 +23,7 @@ require (
)

require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/bytedance/sonic v1.10.2
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
Expand Down Expand Up @@ -57,3 +58,5 @@ require (
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/vanilla-os/differ/diff => ./diff
6 changes: 6 additions & 0 deletions go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
go 1.21.4

use (
.
./diff
)
10 changes: 10 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
120 changes: 8 additions & 112 deletions types/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ package types
*/

import (
"cmp"
"regexp"
"slices"
"strconv"
"time"

"github.com/vanilla-os/differ/diff"
"gorm.io/gorm"
)

Expand All @@ -23,12 +20,6 @@ type Package struct {
Version string `json:"version"`
}

type PackageDiff struct {
Name string `json:"name"`
OldVersion string `json:"old_version,omitempty"`
NewVersion string `json:"new_version,omitempty"`
}

type Release struct {
gorm.Model `json:"-"`
Digest string `json:"digest" gorm:"unique"`
Expand All @@ -37,111 +28,16 @@ type Release struct {
Packages []Package `json:"packages" gorm:"many2many:release_packages;"`
}

// This monstruosity is an adaptation of the regex for semver (available in https://semver.org/).
// It SHOULD be able to capture every type of exoteric versioning scheme out there.
var versionRegex = regexp.MustCompile(`^(?:(?P<prefix>\d+):)?(?P<major>\d+[a-zA-Z]?)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>\d+))?(?:[-~](?P<prerelease>(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:[+.](?P<buildmetadata>[0-9a-zA-Z-+.]+(?:\.[0-9a-zA-Z-]+)*))?$`)

// compareVersions has the same behavior as cmp.Compare, but for package versions. It parses
// both version strings and checks for differences in major, minor, patch, pre-release, etc.
func compareVersions(a, b string) int {
aMatchStr := versionRegex.FindStringSubmatch(a)
aMatch := make(map[string]string)
for i, name := range versionRegex.SubexpNames() {
if i != 0 && name != "" && aMatchStr[i] != "" {
aMatch[name] = aMatchStr[i]
}
}

bMatchStr := versionRegex.FindStringSubmatch(b)
bMatch := make(map[string]string)
for i, name := range versionRegex.SubexpNames() {
if i != 0 && name != "" && bMatchStr[i] != "" {
bMatch[name] = bMatchStr[i]
}
}

compResult := 0

compOrder := []string{"prefix", "major", "minor", "patch", "prerelease", "buildmetadata"}
for _, comp := range compOrder {
aValue, aOk := aMatch[comp]
bValue, bOk := bMatch[comp]
// If neither version has component or if they equal
if !aOk && !bOk {
continue
}
// If a has component but b doesn't, package was upgraded, unless it's prerelease
if aOk && !bOk {
if comp == "prerelease" {
compResult = -1
} else {
compResult = 1
}
break
}
// If b has component but a doesn't, package was downgraded
if !aOk && bOk {
compResult = -1
break
}

// If both have, do regular compare
aValueInt, aErr := strconv.Atoi(aValue)
bValueInt, bErr := strconv.Atoi(bValue)

var abComp int
if aErr == nil && bErr == nil {
abComp = cmp.Compare(aValueInt, bValueInt)
} else {
abComp = cmp.Compare(aValue, bValue)
}
if abComp == 0 {
continue
}
compResult = abComp
break
}

return compResult
}

// PackageDiff returns the difference in packages between two images, organized into
// four slices: Added, Upgraded, Downgraded, and Removed packages, respectively.
func (re *Release) DiffPackages(other *Release) ([]PackageDiff, []PackageDiff, []PackageDiff, []PackageDiff) {
added := []PackageDiff{}
upgraded := []PackageDiff{}
downgraded := []PackageDiff{}
removed := []PackageDiff{}

otherCopy := make([]Package, len(other.Packages))
copy(otherCopy, other.Packages)

func (re *Release) DiffPackages(other *Release) ([]diff.PackageDiff, []diff.PackageDiff, []diff.PackageDiff, []diff.PackageDiff) {
thisPackagesMap := make(diff.Package, len(re.Packages))
for _, pkg := range re.Packages {
pos := slices.IndexFunc(otherCopy, func(n Package) bool { return n.Name == pkg.Name })
if pos != -1 {
diff := PackageDiff{pkg.Name, otherCopy[pos].Version, pkg.Version}
switch compareVersions(pkg.Version, otherCopy[pos].Version) {
case -1:
downgraded = append(downgraded, diff)
case 1:
upgraded = append(upgraded, diff)
}

// Clear package from copy so we can later check for removed packages
otherCopy[pos] = Package{}
} else {
diff := PackageDiff{pkg.Name, "", pkg.Version}
added = append(removed, diff)
}
thisPackagesMap[pkg.Name] = pkg.Version
}

for _, opkg := range otherCopy {
dummy := Package{}
if opkg != dummy {
diff := PackageDiff{opkg.Name, opkg.Version, ""}
removed = append(removed, diff)
}
otherPackagesMap := make(diff.Package, len(other.Packages))
for _, pkg := range other.Packages {
otherPackagesMap[pkg.Name] = pkg.Version
}

return added, upgraded, downgraded, removed
return diff.DiffPackages(thisPackagesMap, otherPackagesMap)
}
10 changes: 6 additions & 4 deletions types/release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ package types
import (
"slices"
"testing"

"github.com/vanilla-os/differ/diff"
)

func TestDiffPackages(t *testing.T) {
Expand All @@ -33,16 +35,16 @@ func TestDiffPackages(t *testing.T) {

added, upgraded, downgraded, removed := sampleNew.DiffPackages(&sampleOld)

if !slices.Equal(added, []PackageDiff{{"pkg1", "", "1.0"}}) {
if !slices.Equal(added, []diff.PackageDiff{{Name: "pkg1", NewVersion: "1.0", PreviousVersion: ""}}) {
t.Fatalf("DiffPackages added = %v, expected {\"pkg1\", \"1.0\", \"\"}", added)
}
if !slices.Equal(upgraded, []PackageDiff{{"pkg3", "1.0", "2.0"}}) {
if !slices.Equal(upgraded, []diff.PackageDiff{{Name: "pkg3", NewVersion: "1.0", PreviousVersion: "2.0"}}) {
t.Fatalf("DiffPackages upgraded = %v, expected {\"pkg3\", \"1.0\", \"2.0\"}", upgraded)
}
if !slices.Equal(downgraded, []PackageDiff{{"pkg4", "2.0", "1.0"}}) {
if !slices.Equal(downgraded, []diff.PackageDiff{{Name: "pkg4", NewVersion: "2.0", PreviousVersion: "1.0"}}) {
t.Fatalf("DiffPackages downgraded = %v, expected {\"pkg4\", \"2.0\", \"1.0\"}", downgraded)
}
if !slices.Equal(removed, []PackageDiff{{"pkg2", "1.0", ""}}) {
if !slices.Equal(removed, []diff.PackageDiff{{Name: "pkg2", NewVersion: "", PreviousVersion: "1.0"}}) {
t.Fatalf("DiffPackages rmeoved = %v, expected {\"pkg2\", \"\", \"1.0\"}", removed)
}
}
Loading

0 comments on commit c5fad59

Please sign in to comment.