forked from toolshed/tagcmp
Init
This commit is contained in:
commit
b4367c8019
102
README.md
Normal file
102
README.md
Normal file
@ -0,0 +1,102 @@
|
||||
# tagcmp
|
||||
|
||||
Comparison operations for image tags. Because registries aren't doing this for
|
||||
us 🙄 This library is helpful if you're aiming to use only "stable" and
|
||||
"semver-like" tags want to be able to do things like compare them, find which
|
||||
tags are more recent, sort them and other types of comparisons.
|
||||
|
||||
A best-effort implementation which follows the wisdom of [Renovate].
|
||||
|
||||
> Docker doesn't really have versioning, instead it supports "tags" and these
|
||||
> are usually used by Docker image authors as a form of versioning ... It's
|
||||
> pretty "wild west" for tagging and not always compliant with SemVer.
|
||||
|
||||
The [Renovate implementation], which allows image tags to be automatically
|
||||
upgraded, is the only show in town, apparently. This library follows that
|
||||
implementation quite closely.
|
||||
|
||||
[renovate]: https://github.com/renovatebot/renovate/blob/main/lib/versioning/docker/readme.md
|
||||
[renovate implementation]: https://github.com/renovatebot/renovate/tree/main/lib/datasource/docker
|
||||
|
||||
## Public API
|
||||
|
||||
> TODO
|
||||
|
||||
## Types of versions supported
|
||||
|
||||
```golang
|
||||
// semver
|
||||
"5",
|
||||
"2.6",
|
||||
"4.3.5",
|
||||
|
||||
// semver with 'v'
|
||||
"v1",
|
||||
"v2.3",
|
||||
"v1.0.2",
|
||||
|
||||
// semver with suffix
|
||||
"6-alpine",
|
||||
"6.2-alpine",
|
||||
"6.2.1-alpine",
|
||||
|
||||
// semver with sufixx and 'v'
|
||||
"v6-alpine",
|
||||
"v6.2-alpine",
|
||||
"v6.2.1-alpine",
|
||||
"v6.2.1-alpine",
|
||||
|
||||
// semver with multiple suffix values
|
||||
"v6.2.1-alpine-foo",
|
||||
```
|
||||
|
||||
## Types of versions not supported
|
||||
|
||||
> Please note, we could support some of these versions if people really need
|
||||
> them to be supported. Some tags are using a unique format which we could
|
||||
> support by implementing a very specific parser for (e.g. `ParseMinioTag`,
|
||||
> `ParseZncTag`). For now, this library tries to provide a `Parse` function
|
||||
> which handles more general cases. Please open an issue, change sets are
|
||||
> welcome.
|
||||
|
||||
```golang
|
||||
// empty
|
||||
"",
|
||||
|
||||
// patametrized
|
||||
"${MAILU_VERSION:-master}",
|
||||
"${PHP_VERSION}-fpm-alpine3.13",
|
||||
|
||||
// commit hash like
|
||||
"0a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d",
|
||||
|
||||
// numeric
|
||||
"20191109",
|
||||
"e02267d",
|
||||
|
||||
// not semver
|
||||
"3.0.6.0",
|
||||
"r1295",
|
||||
"version-r1070",
|
||||
|
||||
// prerelease
|
||||
"3.7.0b1",
|
||||
"3.8.0b1-alpine",
|
||||
|
||||
// multiple versions
|
||||
"5.36-backdrop-php7.4",
|
||||
"v1.0.5_3.4.0",
|
||||
"v1.0.5_3.4.0_openid-sso",
|
||||
|
||||
// tz based
|
||||
"RELEASE.2021-04-22T15-44-28Z",
|
||||
|
||||
// only text
|
||||
"alpine",
|
||||
"latest",
|
||||
"master",
|
||||
|
||||
// multiple - delimters
|
||||
"apache-debian-1.8-prod",
|
||||
"version-znc-1.8.2",
|
||||
```
|
308
tagcmp.go
Normal file
308
tagcmp.go
Normal file
@ -0,0 +1,308 @@
|
||||
// Package tagcmp provides image tag comparison operations.
|
||||
package tagcmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Major string `json:",omitempty"` // major semver part
|
||||
Minor string `json:",omitempty"` // minor semver part
|
||||
MissingMinor bool // whether or not the minor semver part was left out
|
||||
Patch string `json:",omitempty"` // patch semver part
|
||||
MissingPatch bool // whether or not he patch semver part was left out
|
||||
Suffix string // tag suffix (e.g. "-alpine")
|
||||
UsesV bool // whether or not the tag uses the "v" prefix
|
||||
}
|
||||
|
||||
// ByTag sorts tags
|
||||
type ByTag []Tag
|
||||
|
||||
func (t ByTag) Len() int { return len(t) }
|
||||
func (t ByTag) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||
func (t ByTag) Less(i, j int) bool {
|
||||
return t[i].IsLessThan(t[j])
|
||||
}
|
||||
|
||||
// IsGreaterThan tests if a tag is greater than another. There are some
|
||||
// tag-isms to take into account here, shorter is bigger (i.e. 2.1 > 2.1.1 ==
|
||||
// true, 2 > 2.1 == true).
|
||||
func (t Tag) IsGreaterThan(tag Tag) bool {
|
||||
// shorter is bigger, i.e. 2.1 > 2.1.1
|
||||
if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
|
||||
return true
|
||||
}
|
||||
if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
// ignore errors since Parse already handled
|
||||
mj1, _ := strconv.Atoi(t.Major)
|
||||
mj2, _ := strconv.Atoi(tag.Major)
|
||||
if mj1 > mj2 {
|
||||
return true
|
||||
}
|
||||
if mj2 > mj1 {
|
||||
return false
|
||||
}
|
||||
|
||||
mn1, _ := strconv.Atoi(t.Minor)
|
||||
mn2, _ := strconv.Atoi(tag.Minor)
|
||||
if mn1 > mn2 {
|
||||
return true
|
||||
}
|
||||
if mn2 > mn1 {
|
||||
return false
|
||||
}
|
||||
|
||||
p1, _ := strconv.Atoi(t.Patch)
|
||||
p2, _ := strconv.Atoi(tag.Patch)
|
||||
if p1 > p2 {
|
||||
return true
|
||||
}
|
||||
if p2 > p1 {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsLessThan tests if a tag is less than another. There are some tag-isms to
|
||||
// take into account here, shorter is bigger (i.e. 2.1 < 2.1.1 == false, 2 <
|
||||
// 2.1 == false).
|
||||
func (t Tag) IsLessThan(tag Tag) bool {
|
||||
if t.IsGreaterThan(tag) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals tests Tag equality
|
||||
func (t Tag) Equals(tag Tag) bool {
|
||||
if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
// ignore errors since Parse already handled
|
||||
mj1, _ := strconv.Atoi(t.Major)
|
||||
mj2, _ := strconv.Atoi(tag.Major)
|
||||
if mj1 != mj2 {
|
||||
return false
|
||||
}
|
||||
|
||||
mn1, _ := strconv.Atoi(t.Minor)
|
||||
mn2, _ := strconv.Atoi(tag.Minor)
|
||||
if mn1 != mn2 {
|
||||
return false
|
||||
}
|
||||
|
||||
p1, _ := strconv.Atoi(t.Patch)
|
||||
p2, _ := strconv.Atoi(tag.Patch)
|
||||
if p1 != p2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// String formats a Tag correctly in string representation
|
||||
func (t Tag) String() string {
|
||||
var repr string
|
||||
|
||||
if t.UsesV {
|
||||
repr += "v"
|
||||
}
|
||||
|
||||
repr += t.Major
|
||||
|
||||
if !t.MissingMinor {
|
||||
repr += fmt.Sprintf(".%s", t.Minor)
|
||||
}
|
||||
|
||||
if !t.MissingPatch {
|
||||
repr += fmt.Sprintf(".%s", t.Patch)
|
||||
}
|
||||
|
||||
if t.Suffix != "" {
|
||||
repr += fmt.Sprintf("-%s", t.Suffix)
|
||||
}
|
||||
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.Suffix != "" && tag.Suffix == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.Suffix != "" && tag.Suffix != "" {
|
||||
if t.Suffix != tag.Suffix {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if t.MissingMinor && !tag.MissingMinor || tag.MissingMinor && !t.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.MissingPatch && !tag.MissingPatch || tag.MissingPatch && !t.MissingPatch {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// CommitHashPattern matches commit-like hash tags
|
||||
var CommitHashPattern = "^[a-f0-9]{7,40}$"
|
||||
|
||||
// DotPattern matches tags which contain multiple versions
|
||||
var DotPattern = "([0-9]+)\\.([0-9]+)"
|
||||
|
||||
// EmptyPattern matches when tags are missing
|
||||
var EmptyPattern = "^$"
|
||||
|
||||
// ParametrizedPattern matches when tags are parametrized
|
||||
var ParametrizedPattern = "\\${.+}"
|
||||
|
||||
// StringPattern matches when tags are only made up of alphabetic characters
|
||||
var StringPattern = "^[a-zA-Z]+$"
|
||||
|
||||
// patternMatches determines if a tag matches unsupported patterns
|
||||
func patternMatches(tag string) error {
|
||||
unsupported := []string{
|
||||
CommitHashPattern,
|
||||
EmptyPattern,
|
||||
ParametrizedPattern,
|
||||
StringPattern,
|
||||
}
|
||||
|
||||
for _, pattern := range unsupported {
|
||||
if match, _ := regexp.Match(pattern, []byte(tag)); match {
|
||||
return fmt.Errorf("'%s' is not supported (%s)", tag, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// patternCounts determines if tags match unsupported patterns by counting occurences of matches
|
||||
func patternCounts(tag string) error {
|
||||
v := regexp.MustCompile(DotPattern)
|
||||
if m := v.FindAllStringIndex(tag, -1); len(m) > 1 {
|
||||
return fmt.Errorf("'%s' is not supported (%s)", tag, DotPattern)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseVersionPart converts a semver version part to an integer
|
||||
func parseVersionPart(part string) (int, error) {
|
||||
p, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return p, 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
|
||||
// therefore this is a best-effort implementation. Examples of tags this
|
||||
// function can parse are: "5", "5.2", "v4", "v5.3.6", "4-alpine",
|
||||
// "v3.2.1-debian".
|
||||
func Parse(tag string) (Tag, error) {
|
||||
if err := patternMatches(tag); err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
if err := patternCounts(tag); err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
usesV := false
|
||||
if string(tag[0]) == "v" {
|
||||
tag = strings.TrimPrefix(tag, "v")
|
||||
usesV = true
|
||||
}
|
||||
|
||||
var suffix string
|
||||
splits := strings.SplitN(tag, "-", 2)
|
||||
if len(splits) > 1 {
|
||||
tag = splits[0]
|
||||
suffix = splits[1]
|
||||
}
|
||||
|
||||
var major, minor, patch string
|
||||
var missingMinor, missingPatch bool
|
||||
parts := strings.Split(tag, ".")
|
||||
switch {
|
||||
case len(parts) == 1:
|
||||
if _, err := parseVersionPart(parts[0]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
|
||||
}
|
||||
major = parts[0]
|
||||
missingMinor = true
|
||||
missingPatch = true
|
||||
case len(parts) == 2:
|
||||
if _, err := parseVersionPart(parts[0]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
|
||||
}
|
||||
major = parts[0]
|
||||
|
||||
if _, err := parseVersionPart(parts[1]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
|
||||
}
|
||||
minor = parts[1]
|
||||
missingPatch = true
|
||||
case len(parts) == 3:
|
||||
if _, err := parseVersionPart(parts[0]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
|
||||
}
|
||||
major = parts[0]
|
||||
|
||||
if _, err := parseVersionPart(parts[1]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
|
||||
}
|
||||
minor = parts[1]
|
||||
|
||||
if _, err := parseVersionPart(parts[2]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse patch part of '%s': '%s'", tag, parts[2])
|
||||
}
|
||||
patch = parts[2]
|
||||
default:
|
||||
return Tag{}, fmt.Errorf("couldn't parse semver of '%s", tag)
|
||||
}
|
||||
|
||||
parsedTag := Tag{
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
MissingMinor: missingMinor,
|
||||
Patch: patch,
|
||||
MissingPatch: missingPatch,
|
||||
UsesV: usesV,
|
||||
Suffix: suffix,
|
||||
}
|
||||
|
||||
return parsedTag, nil
|
||||
}
|
||||
|
||||
// IsParseable determines if a tag is supported by this library
|
||||
func IsParseable(tag string) bool {
|
||||
if _, err := Parse(tag); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
702
tagcmp_test.go
Normal file
702
tagcmp_test.go
Normal file
@ -0,0 +1,702 @@
|
||||
package tagcmp_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/tagcmp"
|
||||
)
|
||||
|
||||
var giteaTags = []string{
|
||||
"latest",
|
||||
"1",
|
||||
"1-linux-amd64",
|
||||
"1-linux-amd64-rootless",
|
||||
"1-linux-arm64",
|
||||
"1-linux-arm64-rootless",
|
||||
"1-rootless",
|
||||
"1.0",
|
||||
"1.0.0",
|
||||
"1.0.1",
|
||||
"1.0.2",
|
||||
"1.1",
|
||||
"1.1.0",
|
||||
"1.1.1",
|
||||
"1.1.2",
|
||||
"1.1.3",
|
||||
"1.1.4",
|
||||
"1.10",
|
||||
"1.10-linux-amd64",
|
||||
"1.10-linux-arm64",
|
||||
"1.10.0",
|
||||
"1.10.0-linux-amd64",
|
||||
"1.10.0-linux-arm64",
|
||||
"1.10.0-rc1",
|
||||
"1.10.0-rc1-linux-amd64",
|
||||
"1.10.0-rc1-linux-arm64",
|
||||
"1.10.0-rc2",
|
||||
"1.10.0-rc2-linux-amd64",
|
||||
"1.10.0-rc2-linux-arm64",
|
||||
"1.10.1",
|
||||
"1.10.1-linux-amd64",
|
||||
"1.10.1-linux-arm64",
|
||||
"1.10.2",
|
||||
"1.10.2-linux-amd64",
|
||||
"1.10.2-linux-arm64",
|
||||
"1.10.3",
|
||||
"1.10.3-linux-amd64",
|
||||
"1.10.3-linux-arm64",
|
||||
"1.10.4",
|
||||
"1.10.4-linux-amd64",
|
||||
"1.10.4-linux-arm64",
|
||||
"1.10.5",
|
||||
"1.10.5-linux-amd64",
|
||||
"1.10.5-linux-arm64",
|
||||
"1.10.6",
|
||||
"1.10.6-linux-amd64",
|
||||
"1.10.6-linux-arm64",
|
||||
"1.11",
|
||||
"1.11-linux-amd64",
|
||||
"1.11-linux-arm64",
|
||||
"1.11.0",
|
||||
"1.11.0-linux-amd64",
|
||||
"1.11.0-linux-arm64",
|
||||
"1.11.0-rc1",
|
||||
"1.11.0-rc1-linux-amd64",
|
||||
"1.11.0-rc1-linux-arm64",
|
||||
"1.11.0-rc2",
|
||||
"1.11.0-rc2-linux-amd64",
|
||||
"1.11.0-rc2-linux-arm64",
|
||||
"1.11.1",
|
||||
"1.11.1-linux-amd64",
|
||||
"1.11.1-linux-arm64",
|
||||
"1.11.2",
|
||||
"1.11.2-linux-amd64",
|
||||
"1.11.2-linux-arm64",
|
||||
"1.11.3",
|
||||
"1.11.3-linux-amd64",
|
||||
"1.11.3-linux-arm64",
|
||||
"1.11.4",
|
||||
"1.11.4-linux-amd64",
|
||||
"1.11.4-linux-arm64",
|
||||
"1.11.5",
|
||||
"1.11.5-linux-amd64",
|
||||
"1.11.5-linux-arm64",
|
||||
"1.11.6",
|
||||
"1.11.6-linux-amd64",
|
||||
"1.11.6-linux-arm64",
|
||||
"1.11.7",
|
||||
"1.11.7-linux-amd64",
|
||||
"1.11.7-linux-arm64",
|
||||
"1.11.8",
|
||||
"1.11.8-linux-amd64",
|
||||
"1.11.8-linux-arm64",
|
||||
"1.12",
|
||||
"1.12-linux-amd64",
|
||||
"1.12-linux-arm64",
|
||||
"1.12.0",
|
||||
"1.12.0-linux-amd64",
|
||||
"1.12.0-linux-arm64",
|
||||
"1.12.0-rc1",
|
||||
"1.12.0-rc1-linux-amd64",
|
||||
"1.12.0-rc1-linux-arm64",
|
||||
"1.12.0-rc2",
|
||||
"1.12.0-rc2-linux-amd64",
|
||||
"1.12.0-rc2-linux-arm64",
|
||||
"1.12.1",
|
||||
"1.12.1-linux-amd64",
|
||||
"1.12.1-linux-arm64",
|
||||
"1.12.2",
|
||||
"1.12.2-linux-amd64",
|
||||
"1.12.2-linux-arm64",
|
||||
"1.12.3",
|
||||
"1.12.3-linux-amd64",
|
||||
"1.12.3-linux-arm64",
|
||||
"1.12.4",
|
||||
"1.12.4-linux-amd64",
|
||||
"1.12.4-linux-arm64",
|
||||
"1.12.5",
|
||||
"1.12.5-linux-amd64",
|
||||
"1.12.5-linux-arm64",
|
||||
"1.12.6",
|
||||
"1.12.6-linux-amd64",
|
||||
"1.12.6-linux-arm64",
|
||||
"1.13",
|
||||
"1.13-linux-amd64",
|
||||
"1.13-linux-arm64",
|
||||
"1.13.0",
|
||||
"1.13.0-linux-amd64",
|
||||
"1.13.0-linux-arm64",
|
||||
"1.13.0-rc1",
|
||||
"1.13.0-rc1-linux-amd64",
|
||||
"1.13.0-rc1-linux-arm64",
|
||||
"1.13.0-rc2",
|
||||
"1.13.0-rc2-linux-amd64",
|
||||
"1.13.0-rc2-linux-arm64",
|
||||
"1.13.1",
|
||||
"1.13.1-linux-amd64",
|
||||
"1.13.1-linux-arm64",
|
||||
"1.13.2",
|
||||
"1.13.2-linux-amd64",
|
||||
"1.13.2-linux-arm64",
|
||||
"1.13.3",
|
||||
"1.13.3-linux-amd64",
|
||||
"1.13.3-linux-arm64",
|
||||
"1.13.4",
|
||||
"1.13.4-linux-amd64",
|
||||
"1.13.4-linux-arm64",
|
||||
"1.13.5",
|
||||
"1.13.5-linux-amd64",
|
||||
"1.13.5-linux-arm64",
|
||||
"1.13.6",
|
||||
"1.13.6-linux-amd64",
|
||||
"1.13.6-linux-arm64",
|
||||
"1.13.7",
|
||||
"1.13.7-linux-amd64",
|
||||
"1.13.7-linux-arm64",
|
||||
"1.14",
|
||||
"1.14-linux-amd64",
|
||||
"1.14-linux-amd64-rootless",
|
||||
"1.14-linux-arm64",
|
||||
"1.14-linux-arm64-rootless",
|
||||
"1.14-rootless",
|
||||
"1.14.0",
|
||||
"1.14.0-linux-amd64",
|
||||
"1.14.0-linux-amd64-rootless",
|
||||
"1.14.0-linux-arm64",
|
||||
"1.14.0-linux-arm64-rootless",
|
||||
"1.14.0-rc1",
|
||||
"1.14.0-rc1-linux-amd64",
|
||||
"1.14.0-rc1-linux-amd64-rootless",
|
||||
"1.14.0-rc1-linux-arm64",
|
||||
"1.14.0-rc1-linux-arm64-rootless",
|
||||
"1.14.0-rc1-rootless",
|
||||
"1.14.0-rc2",
|
||||
"1.14.0-rc2-linux-amd64",
|
||||
"1.14.0-rc2-linux-amd64-rootless",
|
||||
"1.14.0-rc2-linux-arm64",
|
||||
"1.14.0-rc2-linux-arm64-rootless",
|
||||
"1.14.0-rc2-rootless",
|
||||
"1.14.0-rootless",
|
||||
"1.14.1",
|
||||
"1.14.1-linux-amd64",
|
||||
"1.14.1-linux-amd64-rootless",
|
||||
"1.14.1-linux-arm64",
|
||||
"1.14.1-linux-arm64-rootless",
|
||||
"1.14.1-rootless",
|
||||
"1.14.2",
|
||||
"1.14.2-linux-amd64",
|
||||
"1.14.2-linux-amd64-rootless",
|
||||
"1.14.2-linux-arm64",
|
||||
"1.14.2-linux-arm64-rootless",
|
||||
"1.14.2-rootless",
|
||||
"1.14.3",
|
||||
"1.14.3-linux-amd64",
|
||||
"1.14.3-linux-amd64-rootless",
|
||||
"1.14.3-linux-arm64",
|
||||
"1.14.3-linux-arm64-rootless",
|
||||
"1.14.3-rootless",
|
||||
"1.14.4",
|
||||
"1.14.4-linux-amd64",
|
||||
"1.14.4-linux-amd64-rootless",
|
||||
"1.14.4-linux-arm64",
|
||||
"1.14.4-linux-arm64-rootless",
|
||||
"1.14.4-rootless",
|
||||
"1.14.5",
|
||||
"1.14.5-linux-amd64",
|
||||
"1.14.5-linux-amd64-rootless",
|
||||
"1.14.5-linux-arm64",
|
||||
"1.14.5-linux-arm64-rootless",
|
||||
"1.14.5-rootless",
|
||||
"1.14.6",
|
||||
"1.14.6-linux-amd64",
|
||||
"1.14.6-linux-amd64-rootless",
|
||||
"1.14.6-linux-arm64",
|
||||
"1.14.6-linux-arm64-rootless",
|
||||
"1.14.6-rootless",
|
||||
"1.15.0-rc1",
|
||||
"1.15.0-rc1-linux-amd64",
|
||||
"1.15.0-rc1-linux-amd64-rootless",
|
||||
"1.15.0-rc1-linux-arm64",
|
||||
"1.15.0-rc1-linux-arm64-rootless",
|
||||
"1.15.0-rc1-rootless",
|
||||
"1.15.0-rc2",
|
||||
"1.15.0-rc2-linux-amd64",
|
||||
"1.15.0-rc2-linux-amd64-rootless",
|
||||
"1.15.0-rc2-linux-arm64",
|
||||
"1.15.0-rc2-linux-arm64-rootless",
|
||||
"1.15.0-rc2-rootless",
|
||||
"1.15.0-rc3",
|
||||
"1.15.0-rc3-linux-amd64",
|
||||
"1.15.0-rc3-linux-amd64-rootless",
|
||||
"1.15.0-rc3-linux-arm64",
|
||||
"1.15.0-rc3-linux-arm64-rootless",
|
||||
"1.15.0-rc3-rootless",
|
||||
"1.2",
|
||||
"1.2.0",
|
||||
"1.2.0-rc1",
|
||||
"1.2.0-rc2",
|
||||
"1.2.0-rc3",
|
||||
"1.2.0-rc4",
|
||||
"1.2.1",
|
||||
"1.2.2",
|
||||
"1.2.3",
|
||||
"1.3",
|
||||
"1.3.0",
|
||||
"1.3.0-rc1",
|
||||
"1.3.0-rc2",
|
||||
"1.3.1",
|
||||
"1.3.2",
|
||||
"1.3.3",
|
||||
"1.4",
|
||||
"1.4.0",
|
||||
"1.4.0-rc1",
|
||||
"1.4.0-rc2",
|
||||
"1.4.0-rc3",
|
||||
"1.4.1",
|
||||
"1.4.2",
|
||||
"1.4.3",
|
||||
"1.5",
|
||||
"1.5.0",
|
||||
"1.5.0-rc1",
|
||||
"1.5.0-rc2",
|
||||
"1.5.1",
|
||||
"1.5.2",
|
||||
"1.5.3",
|
||||
"1.6",
|
||||
"1.6.0",
|
||||
"1.6.0-rc1",
|
||||
"1.6.0-rc2",
|
||||
"1.6.1",
|
||||
"1.6.2",
|
||||
"1.6.3",
|
||||
"1.6.4",
|
||||
"1.7",
|
||||
"1.7.0",
|
||||
"1.7.0-rc1",
|
||||
"1.7.0-rc2",
|
||||
"1.7.0-rc3",
|
||||
"1.7.1",
|
||||
"1.7.2",
|
||||
"1.7.3",
|
||||
"1.7.4",
|
||||
"1.7.5",
|
||||
"1.7.6",
|
||||
"1.8",
|
||||
"1.8.0",
|
||||
"1.8.0-rc1",
|
||||
"1.8.0-rc2",
|
||||
"1.8.0-rc3",
|
||||
"1.8.1",
|
||||
"1.8.2",
|
||||
"1.8.3",
|
||||
"1.9",
|
||||
"1.9-linux-amd64",
|
||||
"1.9-linux-arm64",
|
||||
"1.9.0",
|
||||
"1.9.1",
|
||||
"1.9.2",
|
||||
"1.9.2-linux-amd64",
|
||||
"1.9.2-linux-arm64",
|
||||
"1.9.3",
|
||||
"1.9.3-linux-amd64",
|
||||
"1.9.3-linux-arm64",
|
||||
"1.9.4",
|
||||
"1.9.4-linux-amd64",
|
||||
"1.9.4-linux-arm64",
|
||||
"1.9.5",
|
||||
"1.9.5-linux-amd64",
|
||||
"1.9.5-linux-arm64",
|
||||
"1.9.6",
|
||||
"1.9.6-linux-amd64",
|
||||
"1.9.6-linux-arm64",
|
||||
"dev",
|
||||
"dev-linux-amd64",
|
||||
"dev-linux-amd64-rootless",
|
||||
"dev-linux-arm64",
|
||||
"dev-linux-arm64-rootless",
|
||||
"dev-rootless",
|
||||
"latest-rootless",
|
||||
"linux-amd64",
|
||||
"linux-amd64-rootless",
|
||||
"linux-arm64",
|
||||
"linux-arm64-rootless",
|
||||
}
|
||||
|
||||
var supported = []string{
|
||||
// semver
|
||||
"5",
|
||||
"2.6",
|
||||
"4.3.5",
|
||||
|
||||
// semver with 'v'
|
||||
"v1",
|
||||
"v2.3",
|
||||
"v1.0.2",
|
||||
|
||||
// semver with suffix
|
||||
"6-alpine",
|
||||
"6.2-alpine",
|
||||
"6.2.1-alpine",
|
||||
|
||||
// semver with sufix and 'v'
|
||||
"v6-alpine",
|
||||
"v6.2-alpine",
|
||||
"v6.2.1-alpine",
|
||||
"v6.2.1-alpine",
|
||||
|
||||
// semver with multiple suffix values
|
||||
"v6.2.1-alpine-foo",
|
||||
}
|
||||
|
||||
var unsupported = []string{
|
||||
// empty
|
||||
"",
|
||||
|
||||
// patametrized
|
||||
"${MAILU_VERSION:-master}",
|
||||
"${PHP_VERSION}-fpm-alpine3.13",
|
||||
|
||||
// commit hash like
|
||||
"0a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d",
|
||||
|
||||
// numeric
|
||||
"20191109",
|
||||
"e02267d",
|
||||
|
||||
// not semver
|
||||
"3.0.6.0",
|
||||
"r1295",
|
||||
"version-r1070",
|
||||
|
||||
// prerelease
|
||||
"3.7.0b1",
|
||||
"3.8.0b1-alpine",
|
||||
|
||||
// multiple versions
|
||||
"5.36-backdrop-php7.4",
|
||||
"v1.0.5_3.4.0",
|
||||
"v1.0.5_3.4.0_openid-sso",
|
||||
|
||||
// tz based
|
||||
"RELEASE.2021-04-22T15-44-28Z",
|
||||
|
||||
// only text
|
||||
"alpine",
|
||||
"latest",
|
||||
"master",
|
||||
|
||||
// multiple - delimters
|
||||
"apache-debian-1.8-prod",
|
||||
"version-znc-1.8.2",
|
||||
}
|
||||
|
||||
func TestParseUnsupported(t *testing.T) {
|
||||
for _, tag := range unsupported {
|
||||
if _, err := tagcmp.Parse(tag); err == nil {
|
||||
t.Errorf("'%s' was parsed but it is unsupported", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSupported(t *testing.T) {
|
||||
for _, tag := range supported {
|
||||
if _, err := tagcmp.Parse(tag); err != nil {
|
||||
t.Errorf("'%s' was not parsed but it is supported", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRetainZeroes(t *testing.T) {
|
||||
tag, err := tagcmp.Parse("18.04")
|
||||
if err != nil {
|
||||
t.Errorf("'18.04' should have parsed but didn't: %s", err)
|
||||
}
|
||||
if tag.Minor != "04" {
|
||||
t.Errorf("parsing '18.04' didn't retain zeroes: %s", tag.Minor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectsV(t *testing.T) {
|
||||
tag1, err := tagcmp.Parse("v2")
|
||||
if !tag1.UsesV {
|
||||
t.Error("'v2' uses 'v' but wasn't detected as such")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("'v2' should have parsed but didn't: %s", err)
|
||||
}
|
||||
|
||||
tag2, err := tagcmp.Parse("2")
|
||||
if tag2.UsesV {
|
||||
t.Error("'v2' doesn't use 'v' but wasn't detected as such")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("'2' should have parsed but didn't: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingParts(t *testing.T) {
|
||||
tag1, err := tagcmp.Parse("v2")
|
||||
if !tag1.MissingMinor || !tag1.MissingPatch {
|
||||
t.Error("'v2' parsed without realising there are missing parts")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("'v2' should have parsed but didn't: %s", err)
|
||||
}
|
||||
|
||||
tag2, err := tagcmp.Parse("v2.3")
|
||||
if !tag2.MissingPatch {
|
||||
t.Error("'v2.3' parsed without realising there are missing parts")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("'v2.3' should have parsed but didn't: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMajorPart(t *testing.T) {
|
||||
majors := map[string]string{
|
||||
"1.2.3": "1",
|
||||
"18.04": "18",
|
||||
"10.1": "10",
|
||||
"3": "3",
|
||||
}
|
||||
|
||||
for m := range majors {
|
||||
if p, _ := tagcmp.Parse(m); p.Major != majors[m] {
|
||||
t.Errorf("'%s' didn't parse major part correctly: %s", m, p.Major)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinorPart(t *testing.T) {
|
||||
minors := map[string]string{
|
||||
"1.2.3": "2",
|
||||
"18.04": "04",
|
||||
"10.1": "1",
|
||||
"3": "",
|
||||
}
|
||||
|
||||
for m := range minors {
|
||||
if p, _ := tagcmp.Parse(m); p.Minor != minors[m] {
|
||||
t.Errorf("'%s' didn't parse minor part correctly: %s", m, p.Minor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchPart(t *testing.T) {
|
||||
patches := map[string]string{
|
||||
"1.2.3": "3",
|
||||
"18.04": "",
|
||||
"10.1": "",
|
||||
"3": "",
|
||||
}
|
||||
|
||||
for m := range patches {
|
||||
if p, _ := tagcmp.Parse(m); p.Patch != patches[m] {
|
||||
t.Errorf("'%s' didn't parse patch part correctly: %s", m, p.Patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGreaterThan(t *testing.T) {
|
||||
pairs := []struct {
|
||||
t1 string
|
||||
t2 string
|
||||
expected bool
|
||||
}{
|
||||
{"1.2.3", "1.2", false},
|
||||
{"18.04", "18.1", true},
|
||||
{"10.1", "10.1.2", true},
|
||||
{"3", "2", true},
|
||||
{"1.2.3", "1.2.3", 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)
|
||||
}
|
||||
res := p1.IsGreaterThan(p2)
|
||||
if res != p.expected {
|
||||
t.Errorf("(%s).IsGreatherThan(%s) gave %t but expected %t", p.t1, p.t2, res, p.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestIsLessThan(t *testing.T) {
|
||||
pairs := []struct {
|
||||
t1 string
|
||||
t2 string
|
||||
expected bool
|
||||
}{
|
||||
{"1.2.3", "2.0", true},
|
||||
{"18.04", "18.1", false},
|
||||
{"10.1", "10.0.4", false},
|
||||
{"3", "4.0", false},
|
||||
{"1.2", "1.3.4", false},
|
||||
{"0.0.1", "0.2.1", true},
|
||||
{"0.0.1", "2.0.0", true},
|
||||
{"1.3.9", "1.4.8", true},
|
||||
{"v0.2.1", "v16.0.1", true},
|
||||
}
|
||||
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)
|
||||
}
|
||||
res := p1.IsLessThan(p2)
|
||||
if res != p.expected {
|
||||
t.Errorf("(%s).IsLessThan(%s) gave %t but expected %t", p.t1, p.t2, res, p.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEquals(t *testing.T) {
|
||||
pairs := []struct {
|
||||
t1 string
|
||||
t2 string
|
||||
expected bool
|
||||
}{
|
||||
{"1.2.3", "1.2.3", true},
|
||||
{"18.04", "18.4", true},
|
||||
{"10.0", "10.0.4", false},
|
||||
{"3", "4.0", false},
|
||||
{"1.2", "1.2.3", 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)
|
||||
}
|
||||
res := p1.Equals(p2)
|
||||
if res != p.expected {
|
||||
t.Errorf("(%s).Equals(%s) gave %t but expected %t", p.t1, p.t2, res, p.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCompatible(t *testing.T) {
|
||||
pairs := []struct {
|
||||
t1 string
|
||||
t2 string
|
||||
expected bool
|
||||
}{
|
||||
{"v1", "v2", true},
|
||||
{"v1", "v2.4", false},
|
||||
{"1", "2", true},
|
||||
{"2.3.4", "v2.3.4", false},
|
||||
{"1.2.3", "1.2.6", true},
|
||||
{"1.2.3", "1.2.0", true},
|
||||
{"5-alpine", "6-alpine", true},
|
||||
{"5-alpine", "6.5-alpine", 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)
|
||||
}
|
||||
res := p1.IsCompatible(p2)
|
||||
if res != p.expected {
|
||||
t.Errorf("(%s).IsCompatible(%s) gave %t but expected %t", p.t1, p.t2, res, p.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
rawTags := []string{
|
||||
"v1.4.8",
|
||||
"v1.3.9",
|
||||
"v2.0.0",
|
||||
"v16.0.1",
|
||||
"v0.0.1",
|
||||
"v0.2.1",
|
||||
"v5.9.1",
|
||||
}
|
||||
|
||||
var tags []tagcmp.Tag
|
||||
for _, rawTag := range rawTags {
|
||||
tag, err := tagcmp.Parse(rawTag)
|
||||
if err != nil {
|
||||
t.Errorf("'%s' should have parsed but didn't: %s", tag, err)
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
sort.Sort(tagcmp.ByTag(tags))
|
||||
|
||||
expected := []string{
|
||||
"v0.0.1",
|
||||
"v0.2.1",
|
||||
"v1.3.9",
|
||||
"v1.4.8",
|
||||
"v2.0.0",
|
||||
"v5.9.1",
|
||||
"v16.0.1",
|
||||
}
|
||||
|
||||
for idx, tag := range tags {
|
||||
if tag.String() != expected[idx] {
|
||||
t.Errorf("'%s' sorted out of order, saw '%s', expected '%s'", tag, tags, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
for _, tag := range supported {
|
||||
p, err := tagcmp.Parse(tag)
|
||||
if err != nil {
|
||||
t.Errorf("'%s' was not parsed but it is supported", tag)
|
||||
}
|
||||
if p.String() != tag {
|
||||
t.Errorf("String() of '%s' didn't render properly: %s", tag, p.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGiteaFilterCompatible(t *testing.T) {
|
||||
expected := []string{
|
||||
"1.14.0-rootless",
|
||||
"1.14.1-rootless",
|
||||
"1.14.2-rootless",
|
||||
"1.14.3-rootless",
|
||||
"1.14.4-rootless",
|
||||
"1.14.5-rootless",
|
||||
"1.14.6-rootless",
|
||||
}
|
||||
|
||||
tag, err := tagcmp.Parse("1.14.0-rootless")
|
||||
if err != nil {
|
||||
t.Errorf("'1.14.0-rootless' should have parsed but didn't: %s", err)
|
||||
}
|
||||
|
||||
var filtered []tagcmp.Tag
|
||||
for _, giteaTag := range giteaTags {
|
||||
// not interested in unsupported tags right now
|
||||
p, _ := tagcmp.Parse(giteaTag)
|
||||
if tag.IsCompatible(p) {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(tagcmp.ByTag(filtered))
|
||||
|
||||
for idx, tag := range filtered {
|
||||
if tag.String() != expected[idx] {
|
||||
t.Errorf("'%s' out of order or incompatible, saw '%s', expected '%s'", tag, filtered, expected)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user