diff --git a/docs/content/configuration/filter.md b/docs/content/configuration/filter.md index 9cdb20b7..f0708a97 100644 --- a/docs/content/configuration/filter.md +++ b/docs/content/configuration/filter.md @@ -68,3 +68,26 @@ D enterprise.github.com/company In the above example, any module not in the rules will be excluded. All modules from `enterprise.github.com/company` are fetched directly from the source. The `github.com/gomods/athens` module will be stored in the proxy storage, but only for version `v0.4.1` and any patch versions under `v0.1` and `v0.2` minor versions. + +### Versions Filter Modifiers + +Athens provides advanced filter modifiers to cover cases such as API compatibility or when a given dependency changes its license from a given versions. The modifiers are intended to be used in the pattern list of the filter file. + +
+- +# external dependency approved list ++ github.com/gomods/athens+ +The currently supported modifiers are + +* `~1.2.3` will enable all patch versions from 1.2.3 and above (e.g. 1.2.3, 1.2.4, 1.2.5) + * Formally, `1.2.x` where `x >= 3` + +* `^1.2.3` will enable all patch and minor versions from 1.2.3 and above (e.g. 1.2.4, 1.3.0 and 1.4.5) + * Formally, `1.x.y` where `x >= 2` and `y >= 3` + +* `<1.2.3` will enable all versions lower than 1.2.3 (e.g. 1.2.2, 1.0.0 and 0.58.9) + * Formally, `x.y.z` where `x <= 1`, `y < = 2` and `z < 3` + +This kind of modifiers will work only if a three parts semantic version is specified. For example, `~4.5.6` will work while `~4.5` won't. \ No newline at end of file diff --git a/pkg/module/filter.go b/pkg/module/filter.go index 63005c09..5c02e817 100644 --- a/pkg/module/filter.go +++ b/pkg/module/filter.go @@ -10,7 +10,8 @@ import ( ) var ( - pathSeparator = "/" + pathSeparator = "/" + versionSeparator = "." ) // Filter is a filter of modules @@ -42,7 +43,7 @@ func NewFilter(filterFilePath string) (*Filter, error) { } // AddRule adds rule for specified path -func (f *Filter) AddRule(path string, versions []string, rule FilterRule) { +func (f *Filter) AddRule(path string, qualifiers []string, rule FilterRule) { f.ensurePath(path) segments := getPathSegments(path) @@ -62,7 +63,7 @@ func (f *Filter) AddRule(path string, versions []string, rule FilterRule) { last := segments[len(segments)-1] rn := latest.next[last] rn.rule = rule - rn.vers = versions + rn.qualifiers = qualifiers latest.next[last] = rn } @@ -102,9 +103,9 @@ func (f *Filter) getAssociatedRule(version string, path ...string) FilterRule { } rn = rn.next[p] // default to true if no version filter, false otherwise - match := len(rn.vers) == 0 - for _, ver := range rn.vers { - if strings.HasPrefix(version, ver) { + match := len(rn.qualifiers) == 0 + for _, q := range rn.qualifiers { + if matches(version, q) { match = true break } @@ -172,30 +173,115 @@ func initFromConfig(filePath string) (*Filter, error) { f.AddRule("", nil, rule) continue } - var vers []string + var qual []string if len(split) == 3 { - vers = strings.Split(split[2], ",") - for i := range vers { - vers[i] = strings.TrimRight(vers[i], "*") - if vers[i][len(vers[i])-1] != '.' && strings.Count(vers[i], ".") < 2 { - vers[i] += "." + qual = strings.Split(split[2], ",") + for i := range qual { + qual[i] = strings.TrimRight(qual[i], "*") + if qual[i][len(qual[i])-1] != '.' && strings.Count(qual[i], ".") < 2 { + qual[i] += "." } } } path := strings.TrimSpace(split[1]) - f.AddRule(path, vers, rule) + f.AddRule(path, qual, rule) } return f, nil } +// matches checks if the given version matches the given qualifier. +// Qualifiers can be: +// - plain versions +// - v1.2.3 enables v1.2.3 +// - ~1.2.3: enables 1.2.x which are at least 1.2.3 +// - ^1.2.3: enables 1.x.x which are at least 1.2.3 +// - <1.2.3: enables everything lower than 1.2.3 includes 1.2.2 and 0.58.9 as well +func matches(version, qualifier string) bool { + if len(qualifier) < 2 || len(version) < 1 { + return false + } + + prefix := qualifier[0] + first := qualifier[1] + + // v1.2.3 means we accept every version starting with v1.2.3 + // handle this special case first, then go for ~v1.2.3 and similar + if prefix == 'v' && first >= '0' && first <= '9' { // a number + return strings.HasPrefix(version, qualifier) + } + + v, err := getVersionSegments(version[1:]) + if err != nil { + return false + } + + q, err := getVersionSegments(qualifier[2:]) + if err != nil { + return false + } + + if len(v) != len(q) { + return false + } + // no semver + if len(v) != 3 || len(q) != 3 { + return false + } + + switch prefix { + case '~': + if v[0] == q[0] && v[1] == q[1] && v[2] >= q[2] { + return true + } + return false + case '^': + if v[0] == q[0] && v[1] > q[1] { + return true + } + if v[0] == q[0] && v[1] == q[1] && v[2] >= q[2] { + return true + } + return false + case '<': + if v[0] < q[0] { + return true + } + if v[0] == q[0] && v[1] < q[1] { + return true + } + if v[0] == q[0] && v[1] == q[1] && v[2] <= q[2] { + return true + } + return false + } + return false +} + func getPathSegments(path string) []string { + return getSegments(path, pathSeparator) +} + +func getVersionSegments(path string) ([]int, error) { + vv := getSegments(path, versionSeparator) + res := make([]int, len(vv)) + for i, v := range vv { + n, err := strconv.Atoi(v) + if err != nil { + return nil, err + } + res[i] = n + } + return res, nil +} + +func getSegments(path, separator string) []string { path = strings.TrimSpace(path) - path = strings.Trim(path, pathSeparator) + path = strings.Trim(path, separator) if path == "" { return []string{} } - return strings.Split(path, pathSeparator) + return strings.Split(path, separator) } func newRule(r FilterRule) ruleNode { diff --git a/pkg/module/filter_rule.go b/pkg/module/filter_rule.go index b2210f1c..89732b94 100644 --- a/pkg/module/filter_rule.go +++ b/pkg/module/filter_rule.go @@ -1,7 +1,7 @@ package module type ruleNode struct { - next map[string]ruleNode - rule FilterRule - vers []string + next map[string]ruleNode + rule FilterRule + qualifiers []string } diff --git a/pkg/module/filter_test.go b/pkg/module/filter_test.go index 26d363aa..d44360a9 100644 --- a/pkg/module/filter_test.go +++ b/pkg/module/filter_test.go @@ -133,6 +133,75 @@ func (t *FilterTests) Test_versionFilter() { r.Equal(Include, f.Rule("github.com/a/b/c/d", "v1.3.4")) } +func (t *FilterTests) Test_versionFilterMinor() { + r := t.Require() + filter := tempFilterFile(t.T()) + defer os.Remove(filter) + + f, err := NewFilter(filter) + r.NoError(err) + f.AddRule("", nil, Exclude) + f.AddRule("github.com/a/b", []string{"~v1.2.3", "~v2.3.40"}, Include) + r.Equal(Include, f.Rule("github.com/a/b", "v1.2.3")) + r.Equal(Include, f.Rule("github.com/a/b", "v1.2.5")) + r.Equal(Exclude, f.Rule("github.com/a/b", "v1.2.2")) + r.Equal(Exclude, f.Rule("github.com/a/b", "v1.3.3")) + r.Equal(Include, f.Rule("github.com/a/b", "v2.3.45")) + r.Equal(Exclude, f.Rule("github.com/a/b", "v2.2.45")) + r.Equal(Exclude, f.Rule("github.com/a/b", "v2.3.20")) +} + +func (t *FilterTests) Test_versionFilterMiddle() { + r := t.Require() + filter := tempFilterFile(t.T()) + defer os.Remove(filter) + + f, err := NewFilter(filter) + r.NoError(err) + f.AddRule("", nil, Exclude) + f.AddRule("github.com/a/b", []string{"^v1.2.3", "^v2.3.40"}, Include) + r.Equal(Include, f.Rule("github.com/a/b", "v1.2.3")) + r.Equal(Include, f.Rule("github.com/a/b", "v1.2.5")) + r.Equal(Include, f.Rule("github.com/a/b", "v1.4.2")) + r.Equal(Exclude, f.Rule("github.com/a/b", "v1.2.1")) + r.Equal(Include, f.Rule("github.com/a/b", "v2.3.45")) + r.Equal(Include, f.Rule("github.com/a/b", "v2.4.1")) + r.Equal(Exclude, f.Rule("github.com/a/b", "v2.2.45")) +} + +func (t *FilterTests) Test_versionFilterLess() { + r := t.Require() + filter := tempFilterFile(t.T()) + defer os.Remove(filter) + + f, err := NewFilter(filter) + r.NoError(err) + f.AddRule("", nil, Exclude) + f.AddRule("github.com/a/b", []string{"