1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
// +build go1.9
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
package model
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
const previewSubdir = string(os.PathSeparator) + "preview" + string(os.PathSeparator)
var previewVer = regexp.MustCompile(`(?:v?\d{4}-\d{2}-\d{2}|v?\d+[\.\d+\.\d\-]*)(?:-preview|-beta)`)
// these predicates are used when walking the package directories.
// if a predicate returns true it means to include that package.
func acceptAllPredicate(name string) bool {
return true
}
func includePreviewPredicate(name string) bool {
// check if the path contains a /preview/ subdirectory
if strings.Contains(name, previewSubdir) {
return false
}
return !previewVer.MatchString(name)
}
type operationGroup struct {
provider string
arm bool
group string
api string
}
type operInfo struct {
version string
rawpath string
modver string
}
type latestTracker map[operationGroup]operInfo
func (tracker latestTracker) Upsert(path string, pi PathInfo) (operInfo, operationGroup, int) {
group := operationGroup{
provider: pi.Provider,
arm: pi.IsArm,
group: pi.Group,
api: pi.APIPkg,
}
prev, ok := tracker[group]
if !ok {
tracker[group] = operInfo{pi.Version, path, pi.ModVer}
return prev, group, 1
}
if le, _ := versionLE(prev.version, pi.Version); le {
tracker[group] = operInfo{pi.Version, path, pi.ModVer}
return prev, group, 0
}
return prev, group, -1
}
// skipFileCheck is for testing purposes
func (tracker latestTracker) ToListDefinition(skipFileCheck bool) (ListDefinition, error) {
listDef := ListDefinition{
Include: []string{},
}
for _, entry := range tracker {
if skipFileCheck {
listDef.Include = append(listDef.Include, entry.rawpath)
continue
}
absolute, err := filepath.Abs(entry.rawpath)
if err != nil {
return listDef, err
}
// ensure the directory actually contains files
entries, err := ioutil.ReadDir(absolute)
if err != nil {
return listDef, err
}
for _, entry := range entries {
if !entry.IsDir() {
listDef.Include = append(listDef.Include, absolute)
break
}
}
}
sort.Strings(listDef.Include)
return listDef, nil
}
// GetLatestPackages returns the collection of latest packages in terms of API and module version.
func GetLatestPackages(rootDir string, includePreview bool, verboseLog *log.Logger) (ListDefinition, error) {
predicate := includePreviewPredicate
if includePreview {
predicate = acceptAllPredicate
}
tracker := latestTracker{}
filepath.Walk(rootDir, func(currentPath string, info os.FileInfo, openErr error) error {
pi, err := DeconstructPath(currentPath)
if err != nil || !info.IsDir() {
return nil
} else if !predicate(currentPath) {
verboseLog.Printf("%q rejected by Predicate", currentPath)
return nil
}
prev, group, result := tracker.Upsert(currentPath, pi)
switch result {
case 1:
verboseLog.Printf("New group found %v using version %q modver %q", group, pi.Version, pi.ModVer)
case 0:
verboseLog.Printf("Updating group %v from version %q to %q modver %q", group, prev.version, pi.Version, pi.ModVer)
case -1:
verboseLog.Printf("Evaluated group %v version %q decided to stay with %q", group, pi.Version, prev.version)
}
return nil
})
return tracker.ToListDefinition(false)
}
// versionLE takes two version strings that share a format and returns true if the one on the
// left is less than or equal to the one on the right. If the two do not match in format, or
// are not in a well known format, this will return false and an error.
var versionLE = func() func(string, string) (bool, error) {
type strategyTuple struct {
match *regexp.Regexp
handler func([]string, []string) (bool, error)
}
// there are two strategies in the following order:
// The first handles Azure API Versions which have a date optionally followed by some tag.
// The second strategy compares two semvers.
// the order is important as the semver strategy is less specific than the API version strategy due to
// inconsistencies in the directory structure (e.g. v1, 6.2, v1.0 etc). given this we must always check
// the API version strategry first as the semver strategy can match an API version yielding incorrect results.
wellKnownStrategies := []strategyTuple{
{
match: regexp.MustCompile(`^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})(?:[\.\-](?P<tag>.+))?$`),
handler: func(leftMatch, rightMatch []string) (bool, error) {
var err error
for i := 1; i <= 3; i++ { // Start with index 1 because the element 0 is the entire match, not a group. End at 3 because there are three numeric groups.
if leftMatch[i] == rightMatch[i] {
continue
}
var leftNum, rightNum int
leftNum, err = strconv.Atoi(leftMatch[i])
if err != nil {
return false, err
}
rightNum, err = strconv.Atoi(rightMatch[i])
if err != nil {
return false, err
}
if leftNum < rightNum {
return true, nil
}
return false, nil
}
if leftTag, rightTag := leftMatch[4], rightMatch[4]; leftTag == "" && rightTag != "" { // match[4] is the tag portion of a date based API Version label
return false, nil
} else if leftTag != "" && rightTag != "" {
return leftTag <= rightTag, nil
}
return true, nil
},
},
{
match: regexp.MustCompile(`(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?-?(?P<tag>.*))?`),
handler: func(leftMatch, rightMatch []string) (bool, error) {
for i := 1; i <= 3; i++ {
if len(leftMatch[i]) == 0 || len(rightMatch[i]) == 0 {
return leftMatch[i] <= rightMatch[i], nil
}
numLeft, err := strconv.Atoi(leftMatch[i])
if err != nil {
return false, err
}
numRight, err := strconv.Atoi(rightMatch[i])
if err != nil {
return false, err
}
if numLeft < numRight {
return true, nil
}
if numLeft > numRight {
return false, nil
}
}
return leftMatch[4] <= rightMatch[4], nil
},
},
}
// This function finds a strategy which recognizes the versions passed to it, then applies that strategy.
return func(left, right string) (bool, error) {
if left == right {
return true, nil
}
for _, strategy := range wellKnownStrategies {
if leftMatch, rightMatch := strategy.match.FindAllStringSubmatch(left, 1), strategy.match.FindAllStringSubmatch(right, 1); len(leftMatch) > 0 && len(rightMatch) > 0 {
return strategy.handler(leftMatch[0], rightMatch[0])
}
}
return false, fmt.Errorf("Unable to find versioning strategy that could compare %q and %q", left, right)
}
}()
|