eng/tools/internal/modinfo/modinfo.go (151 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. package modinfo import ( "fmt" "path/filepath" "regexp" "sort" "strconv" "strings" "github.com/Azure/azure-sdk-for-go/eng/tools/internal/delta" "github.com/Azure/azure-sdk-for-go/eng/tools/internal/dirs" "github.com/Azure/azure-sdk-for-go/eng/tools/internal/exports" "github.com/Azure/azure-sdk-for-go/eng/tools/internal/report" "github.com/Masterminds/semver" ) var ( verSuffixRegex = regexp.MustCompile(`v\d+$`) ) // HasVersionSuffix returns true if the specified path has a version suffix in the form vN. func HasVersionSuffix(path string) bool { return verSuffixRegex.MatchString(path) } // FindVersionSuffix returns the version suffix or the empty string. func FindVersionSuffix(path string) string { return verSuffixRegex.FindString(path) } // GetModuleSubdirs returns all subdirectories under path that correspond to module major versions. // The subdirectories are sorted according to semantic version. func GetModuleSubdirs(path string) ([]string, error) { subdirs, err := dirs.GetSubdirs(path) if err != nil { return nil, err } modDirs := []string{} for _, subdir := range subdirs { matched := HasVersionSuffix(subdir) if matched { modDirs = append(modDirs, subdir) } } sortModuleTagsBySemver(modDirs) return modDirs, nil } // IncrementModuleVersion increments the passed in module major version by one. // E.g. a provided value of "v2" will return "v3". If ver is "" the return value is "v2". func IncrementModuleVersion(ver string) string { if ver == "" { return "v2" } v, err := strconv.ParseInt(ver[1:], 10, 32) if err != nil { panic(err) } return fmt.Sprintf("v%d", v+1) } // sorts module tags based on their semantic versions. // this is necessary because lexically sorted is not sufficient // due to v10.0.0 appearing before v2.0.0 func sortModuleTagsBySemver(modDirs []string) { sort.SliceStable(modDirs, func(i, j int) bool { lv, err := semver.NewVersion(modDirs[i]) if err != nil { panic(err) } rv, err := semver.NewVersion(modDirs[j]) if err != nil { panic(err) } return lv.LessThan(rv) }) } // CreateModuleNameFromPath creates a module name from the provided path. func CreateModuleNameFromPath(pkgDir string) (string, error) { // e.g. /work/src/github.com/Azure/azure-sdk-for-go/services/foo/2019-01-01/foo // returns github.com/Azure/azure-sdk-for-go/services/foo/2019-01-01/foo repoRoot := filepath.Join("github.com", "Azure", "azure-sdk-for-go") i := strings.Index(pkgDir, repoRoot) if i < 0 { return "", fmt.Errorf("didn't find '%s' in '%s'", repoRoot, pkgDir) } return strings.Replace(pkgDir[i:], "\\", "/", -1), nil } // Provider provides information about a module staged for release. type Provider interface { DestDir() string NewExports() bool BreakingChanges() bool VersionSuffix() bool NewModule() bool GenerateReport() report.Package } type module struct { lhs exports.Content rhs exports.Content dest string } // GetModuleInfo collects information about a module staged for release. // baseline is the directory for the current module // staged is the directory for the module staged for release func GetModuleInfo(baseline, staged string) (Provider, error) { // TODO: verify staged is a child of baseline lhs, err := exports.Get(baseline) if err != nil { // if baseline has no content then this is a v1 package if ei, ok := err.(exports.LoadPackageErrorInfo); !ok || len(ei.Packages()) != 0 { return nil, fmt.Errorf("failed to get exports for package '%s': %s", baseline, err) } } rhs, err := exports.Get(staged) if err != nil { return nil, fmt.Errorf("failed to get exports for package '%s': %s", staged, err) } mod := module{ lhs: lhs, rhs: rhs, dest: baseline, } // calculate the destination directory // if there are breaking changes calculate the new directory if mod.BreakingChanges() { dest := filepath.Dir(staged) v := 2 if verSuffixRegex.MatchString(baseline) { // baseline has a version, get the number and increment it s := string(baseline[len(baseline)-1]) v, err = strconv.Atoi(s) if err != nil { return nil, fmt.Errorf("failed to convert '%s' to int: %v", s, err) } v++ } mod.dest = filepath.Join(dest, fmt.Sprintf("v%d", v)) } return mod, nil } // DestDir returns the fully qualified module destination directory. func (m module) DestDir() string { return m.dest } // NewExports returns true if the module contains any additive changes. func (m module) NewExports() bool { if m.lhs.IsEmpty() { return true } adds := delta.GetExports(m.lhs, m.rhs) return !adds.IsEmpty() } // BreakingChanges returns true if the module contains breaking changes. func (m module) BreakingChanges() bool { if m.lhs.IsEmpty() { return false } // check for changed content if len(delta.GetConstTypeChanges(m.lhs, m.rhs)) > 0 || len(delta.GetFuncSigChanges(m.lhs, m.rhs)) > 0 || len(delta.GetInterfaceMethodSigChanges(m.lhs, m.rhs)) > 0 || len(delta.GetStructFieldChanges(m.lhs, m.rhs)) > 0 { return true } // check for removed content if removed := delta.GetExports(m.rhs, m.lhs); !removed.IsEmpty() { return true } return false } // VersionSuffix returns true if the module path contains a version suffix. func (m module) VersionSuffix() bool { return verSuffixRegex.MatchString(m.dest) } // NewModule returns true if the module is new, i.e. v1.0.0. func (m module) NewModule() bool { return m.lhs.IsEmpty() } // GenerateReport generates a package report for the module. func (m module) GenerateReport() report.Package { return report.Generate(m.lhs, m.rhs, nil) } // IsValidModuleVersion returns true if the provided string is a valid module version (e.g. v1.2.3). func IsValidModuleVersion(v string) bool { r := regexp.MustCompile(`^v\d+\.\d+\.\d+$`) return r.MatchString(v) }