forked from coop-cloud/tagcmp
Compare commits
2 Commits
main
...
baa299faa8
Author | SHA1 | Date |
---|---|---|
knoflook | baa299faa8 | |
knoflook | 054056163a |
121
tagcmp.go
121
tagcmp.go
|
@ -19,6 +19,12 @@ type Tag struct {
|
|||
Metadata string // metadata: what's after + and after the first "-"
|
||||
}
|
||||
|
||||
type TagDelta struct {
|
||||
Major int // major semver difference
|
||||
Minor int // minor semver difference
|
||||
Patch int // patch semver difference
|
||||
}
|
||||
|
||||
// ByTagAsc sorts tags in ascending order where the last element is the latest tag.
|
||||
type ByTagAsc []Tag
|
||||
|
||||
|
@ -148,6 +154,12 @@ func (t Tag) String() string {
|
|||
return repr
|
||||
}
|
||||
|
||||
func (t TagDelta) String() string {
|
||||
var repr string
|
||||
repr = fmt.Sprintf("%d.%d.%d", t.Major, t.Minor, t.Patch)
|
||||
return repr
|
||||
}
|
||||
|
||||
// IsCompatible determines if two tags can be compared together
|
||||
func (t Tag) IsCompatible(tag Tag) bool {
|
||||
if t.UsesV && !tag.UsesV || tag.UsesV && !t.UsesV {
|
||||
|
@ -175,43 +187,30 @@ func (t Tag) IsCompatible(tag Tag) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// UpgradeElement returns a Tag object which is the difference between an old and new tag
|
||||
// UpgradeDelta returns a TagDelta object which is the difference between an old and new tag
|
||||
// It can contain negative numbers if comparing with an older tag.
|
||||
func (curTag Tag) UpgradeElement(newTag Tag) (Tag, error) {
|
||||
func (curTag Tag) UpgradeDelta(newTag Tag) (TagDelta, error) {
|
||||
if !curTag.IsCompatible(newTag) {
|
||||
return Tag{}, fmt.Errorf("%s and %s are not compatible with each other", curTag.String(), newTag.String())
|
||||
return TagDelta{}, fmt.Errorf("%s and %s are not compatible with each other", curTag.String(), newTag.String())
|
||||
}
|
||||
diff := curTag
|
||||
curMajor, err := strconv.Atoi(curTag.Major)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
diff := TagDelta{
|
||||
Major: 0,
|
||||
Minor: 0,
|
||||
Patch: 0,
|
||||
}
|
||||
newMajor, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
diff.Major = strconv.Itoa(newMajor - curMajor)
|
||||
// assuming tags are correctly formatted
|
||||
curMajor, _ := strconv.Atoi(curTag.Major)
|
||||
newMajor, _ := strconv.Atoi(newTag.Major)
|
||||
diff.Major = newMajor - curMajor
|
||||
if !curTag.MissingMinor {
|
||||
curMinor, err := strconv.Atoi(curTag.Minor)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
newMinor, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
diff.Minor = strconv.Itoa(newMinor - curMinor)
|
||||
curMinor, _ := strconv.Atoi(curTag.Minor)
|
||||
newMinor, _ := strconv.Atoi(newTag.Minor)
|
||||
diff.Minor = newMinor - curMinor
|
||||
}
|
||||
if !curTag.MissingPatch {
|
||||
curPatch, err := strconv.Atoi(curTag.Patch)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
newPatch, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
diff.Patch = strconv.Itoa(newPatch - curPatch)
|
||||
curPatch, _ := strconv.Atoi(curTag.Patch)
|
||||
newPatch, _ := strconv.Atoi(newTag.Patch)
|
||||
diff.Patch = newPatch - curPatch
|
||||
}
|
||||
|
||||
return diff, nil
|
||||
|
@ -219,35 +218,23 @@ func (curTag Tag) UpgradeElement(newTag Tag) (Tag, error) {
|
|||
|
||||
// UpgradeType takes exit from UpgradeElemene and returns a numeric representation of upgrade or downgrade
|
||||
// 1/-1: patch 2/-2: minor 4/-4: major 0: no change
|
||||
func UpgradeType(t Tag) int {
|
||||
var major, minor, patch int
|
||||
major, _ = strconv.Atoi(t.Major)
|
||||
if t.MissingMinor {
|
||||
minor = 0
|
||||
} else {
|
||||
minor, _ = strconv.Atoi(t.Minor)
|
||||
}
|
||||
if t.MissingPatch {
|
||||
patch = 0
|
||||
} else {
|
||||
patch, _ = strconv.Atoi(t.Patch)
|
||||
}
|
||||
if major > 0 {
|
||||
func (d TagDelta) UpgradeType() int {
|
||||
if d.Major > 0 {
|
||||
return 4
|
||||
}
|
||||
if major < 0 {
|
||||
if d.Major < 0 {
|
||||
return -4
|
||||
}
|
||||
if minor > 0 {
|
||||
if d.Minor > 0 {
|
||||
return 2
|
||||
}
|
||||
if minor < 0 {
|
||||
if d.Minor < 0 {
|
||||
return -2
|
||||
}
|
||||
if patch > 0 {
|
||||
if d.Patch > 0 {
|
||||
return 1
|
||||
}
|
||||
if patch < 0 {
|
||||
if d.Patch < 0 {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
|
@ -306,6 +293,42 @@ func parseVersionPart(part string) (int, error) {
|
|||
return p, nil
|
||||
}
|
||||
|
||||
// ParseDelta converts a tag difference in the format of X, X.Y or X.Y.Z where
|
||||
// X, Y, Z are positive or negative integers or 0
|
||||
func ParseDelta(delta string) (TagDelta, error) {
|
||||
tagDelta := TagDelta{
|
||||
Major: 0,
|
||||
Minor: 0,
|
||||
Patch: 0,
|
||||
}
|
||||
|
||||
splits := strings.Split(delta, ".")
|
||||
if len(splits) > 3 {
|
||||
return TagDelta{}, fmt.Errorf("'%s' has too much dots", delta)
|
||||
}
|
||||
major, err := strconv.Atoi(splits[0])
|
||||
if err != nil {
|
||||
return TagDelta{}, fmt.Errorf("Major part of '%s' is not an integer", delta)
|
||||
}
|
||||
tagDelta.Major = major
|
||||
|
||||
if len(splits) > 1 {
|
||||
minor, err := strconv.Atoi(splits[1])
|
||||
if err != nil {
|
||||
return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
|
||||
}
|
||||
tagDelta.Minor = minor
|
||||
}
|
||||
if len(splits) > 2 {
|
||||
patch, err := strconv.Atoi(splits[2])
|
||||
if err != nil {
|
||||
return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
|
||||
}
|
||||
tagDelta.Patch = patch
|
||||
}
|
||||
return tagDelta, nil
|
||||
}
|
||||
|
||||
// Parse converts an image tag into a structured data format. It aims to to
|
||||
// support the general case of tags which are "semver-like" and/or stable and
|
||||
// parseable by heuristics. Image tags follow no formal specification and
|
||||
|
|
182
tagcmp_test.go
182
tagcmp_test.go
|
@ -345,15 +345,38 @@ var supported = []string{
|
|||
"v6.2.1-alpine",
|
||||
"v6.2.1-alpine",
|
||||
|
||||
// semver with '+'
|
||||
"5+SDFJ_-2l4x.",
|
||||
"2.6+SDFJ_-2l4x.",
|
||||
"4.3.5+SDFJ_-2l4x.",
|
||||
|
||||
// semver with '+' and 'v'
|
||||
"v5+SDFJ_-2l4x.",
|
||||
"v2.6+SDFJ_-2l4x.",
|
||||
"v4.3.5+SDFJ_-2l4x.",
|
||||
|
||||
// semver with '+' and suffix
|
||||
"5-alpine+SDFJ_-2l4x.",
|
||||
"2.6-alpine+SDFJ_-2l4x.",
|
||||
"4.3.5-alpine+SDFJ_-2l4x.",
|
||||
|
||||
// semver with 'v', '+' and suffix
|
||||
"v5-alpine+SDFJ_-2l4x.",
|
||||
"v2.6-alpine+SDFJ_-2l4x.",
|
||||
"v4.3.5-alpine+SDFJ_-2l4x.",
|
||||
|
||||
// semver with multiple suffix values
|
||||
"v6.2.1-alpine-foo",
|
||||
|
||||
// semver with multiple suffix values and '+'
|
||||
"v6.2.1-alpine-foo+68BC1E",
|
||||
}
|
||||
|
||||
var unsupported = []string{
|
||||
// empty
|
||||
"",
|
||||
|
||||
// patametrized
|
||||
// parametrized
|
||||
"${MAILU_VERSION:-master}",
|
||||
"${PHP_VERSION}-fpm-alpine3.13",
|
||||
|
||||
|
@ -369,6 +392,9 @@ var unsupported = []string{
|
|||
"r1295",
|
||||
"version-r1070",
|
||||
|
||||
// too much dots
|
||||
"1.0.0.0.0",
|
||||
|
||||
// prerelease
|
||||
"3.7.0b1",
|
||||
"3.8.0b1-alpine",
|
||||
|
@ -389,6 +415,12 @@ var unsupported = []string{
|
|||
// multiple - delimters
|
||||
"apache-debian-1.8-prod",
|
||||
"version-znc-1.8.2",
|
||||
|
||||
// unparsable major/minor/patch parts:
|
||||
"a.0.0",
|
||||
"1.a",
|
||||
"1.a.0",
|
||||
"1.0.a",
|
||||
}
|
||||
|
||||
func TestParseUnsupported(t *testing.T) {
|
||||
|
@ -498,6 +530,22 @@ func TestPatchPart(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIsParsableSupported(t *testing.T) {
|
||||
for _, tag := range supported {
|
||||
if !tagcmp.IsParsable(tag) {
|
||||
t.Errorf("'%s' should be parsable but IsParsable returned false", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsParsableUnsupported(t *testing.T) {
|
||||
for _, tag := range unsupported {
|
||||
if tagcmp.IsParsable(tag) {
|
||||
t.Errorf("'%s' should not be parsable but IsParsable returned true", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGreaterThan(t *testing.T) {
|
||||
pairs := []struct {
|
||||
t1 string
|
||||
|
@ -505,6 +553,7 @@ func TestIsGreaterThan(t *testing.T) {
|
|||
expected bool
|
||||
}{
|
||||
{"1.2.3", "1.2", false},
|
||||
{"1.2.3", "1.2.4", false},
|
||||
{"18.04", "18.1", true},
|
||||
{"10.1", "10.1.2", true},
|
||||
{"3", "2", true},
|
||||
|
@ -564,10 +613,15 @@ func TestEquals(t *testing.T) {
|
|||
expected bool
|
||||
}{
|
||||
{"1.2.3", "1.2.3", true},
|
||||
{"1.2.3", "1.2", false},
|
||||
{"1.2.3", "1", false},
|
||||
{"18.04", "18.4", true},
|
||||
{"10.0", "10.0.4", false},
|
||||
{"3", "4.0", false},
|
||||
{"1.2", "1.2.3", false},
|
||||
{"3", "4", false},
|
||||
{"1.3", "1.4", false},
|
||||
{"3+FF812B", "3", false},
|
||||
}
|
||||
for _, p := range pairs {
|
||||
p1, err := tagcmp.Parse(p.t1)
|
||||
|
@ -730,7 +784,7 @@ func TestSortDesc(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
func TestTagString(t *testing.T) {
|
||||
for _, tag := range supported {
|
||||
p, err := tagcmp.Parse(tag)
|
||||
if err != nil {
|
||||
|
@ -775,3 +829,127 @@ func TestGiteaFilterCompatible(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeltaParse(t *testing.T) {
|
||||
supportedDeltas := []string{
|
||||
"1",
|
||||
"1.0",
|
||||
"1.0.0",
|
||||
"-1",
|
||||
"-1.0",
|
||||
"-1.0.0",
|
||||
"-1.0.5",
|
||||
"-1.2.5",
|
||||
"1.-2",
|
||||
}
|
||||
unsupportedDeltas := []string{
|
||||
"AAAAAAAAAAAAAAAA",
|
||||
"1.2.3.4",
|
||||
"1.ab.2",
|
||||
"1.2.a",
|
||||
}
|
||||
for _, delta := range supportedDeltas {
|
||||
if _, err := tagcmp.ParseDelta(delta); err != nil {
|
||||
t.Errorf("'%s' wasn't parsed but it is supported: %s", delta, err)
|
||||
}
|
||||
}
|
||||
for _, delta := range unsupportedDeltas {
|
||||
if _, err := tagcmp.ParseDelta(delta); err == nil {
|
||||
t.Errorf("'%s' was parsed but it is not supported", delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeltaString(t *testing.T) {
|
||||
supportedDeltas := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"1", "1.0.0"},
|
||||
{"1.0", "1.0.0"},
|
||||
{"1.0.0", "1.0.0"},
|
||||
{"-1", "-1.0.0"},
|
||||
{"-1.0", "-1.0.0"},
|
||||
{"-1.0.0", "-1.0.0"},
|
||||
{"-1.0.5", "-1.0.5"},
|
||||
{"-1.2.5", "-1.2.5"},
|
||||
{"1.-2", "1.-2.0"},
|
||||
}
|
||||
for _, test := range supportedDeltas {
|
||||
parsedDelta, err := tagcmp.ParseDelta(test.in)
|
||||
if err != nil {
|
||||
t.Errorf("'%s' was not parsed but it is supported", test.in)
|
||||
}
|
||||
if parsedDelta.String() != test.out {
|
||||
t.Errorf("String() of '%s' didn't render properly: %s", test.in, parsedDelta.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeDelta(t *testing.T) {
|
||||
pairs := []struct {
|
||||
t1 string
|
||||
t2 string
|
||||
expected string
|
||||
throwsErr bool
|
||||
}{
|
||||
{"v1.0.0", "1.0.0", "", true},
|
||||
{"1", "2", "1.0.0", false},
|
||||
{"1.0", "1.1", "0.1.0", false},
|
||||
{"1.1.0", "1.1.1", "0.0.1", false},
|
||||
{"2", "1", "-1.0.0", false},
|
||||
{"1.1", "1.0", "0.-1.0", false},
|
||||
{"1.1.1", "1.1.0", "0.0.-1", false},
|
||||
}
|
||||
for _, p := range pairs {
|
||||
p1, err := tagcmp.Parse(p.t1)
|
||||
if err != nil {
|
||||
t.Errorf("'%s' should have parsed", p.t1)
|
||||
}
|
||||
p2, err := tagcmp.Parse(p.t2)
|
||||
if err != nil {
|
||||
t.Errorf("'%s' should have parsed", p.t2)
|
||||
}
|
||||
pexpected, _ := tagcmp.ParseDelta(p.expected)
|
||||
res, err := p1.UpgradeDelta(p2)
|
||||
if p.throwsErr && (err == nil) {
|
||||
t.Errorf("(%s).UpgradeDelta(%s) didn't throw an error but should have", p.t1, p.t2)
|
||||
} else if !p.throwsErr && (err != nil) {
|
||||
t.Errorf("(%s).UpgradeDelta(%s) threw an error but shouldn't have", p.t1, p.t2)
|
||||
}
|
||||
if res != pexpected {
|
||||
t.Errorf("(%s).UpgradeDelta(%s) gave %s but expected %s", p.t1, p.t2, res.String(), p.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUpgradeType(t *testing.T) {
|
||||
testSet := []struct {
|
||||
in string
|
||||
out int
|
||||
}{
|
||||
{"1.0.0", 4},
|
||||
{"-1.0.0", -4},
|
||||
{"1.2.0", 4},
|
||||
{"-1.2.0", -4},
|
||||
{"0.1.0", 2},
|
||||
{"0.-1.0", -2},
|
||||
{"0.1.2", 2},
|
||||
{"0.-1.2", -2},
|
||||
{"0.0.1", 1},
|
||||
{"0.0.-1", -1},
|
||||
{"0.0.0", 0},
|
||||
{"-0.-0.-0", 0},
|
||||
}
|
||||
for _, test := range testSet {
|
||||
tagDelta, err := tagcmp.ParseDelta(test.in)
|
||||
if err != nil {
|
||||
t.Errorf("tagcmp.ParseDelta couldn't parse '%s': '%s'", test.in, err)
|
||||
}
|
||||
upType := tagDelta.UpgradeType()
|
||||
if upType != test.out {
|
||||
t.Errorf("(%s).UpgradeType() returned '%d', expected '%d'", test.in, upType, test.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue