blob: fa72e6bde4c7c4124afa0486156c43a774734140 [file] [log] [blame]
package action
import (
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"github.com/Masterminds/glide/cache"
"github.com/Masterminds/glide/cfg"
"github.com/Masterminds/glide/dependency"
"github.com/Masterminds/glide/gb"
"github.com/Masterminds/glide/godep"
"github.com/Masterminds/glide/gom"
"github.com/Masterminds/glide/gpm"
"github.com/Masterminds/glide/msg"
gpath "github.com/Masterminds/glide/path"
"github.com/Masterminds/glide/util"
"github.com/Masterminds/semver"
"github.com/Masterminds/vcs"
)
// TODO(mattfarina): This file could really use to be cleaned up. It's functional
// but not all that elegant. This mess of code really needs someone to come
// along and clean it up. Pretty please.
// Create creates/initializes a new Glide repository.
//
// This will fail if a glide.yaml already exists.
//
// By default, this will scan the present source code directory for dependencies.
//
// If skipImport is set to true, this will not attempt to import from an existing
// GPM, Godep, or GB project if one should exist. However, it will still attempt
// to read the local source to determine required packages.
func Create(base string, skipImport, noInteract, skipVerSug bool) {
glidefile := gpath.GlideFile
// Guard against overwrites.
guardYAML(glidefile)
// Guess deps
conf := guessDeps(base, skipImport, noInteract, skipVerSug)
// Write YAML
msg.Info("Writing glide.yaml file")
if err := conf.WriteFile(glidefile); err != nil {
msg.Die("Could not save %s: %s", glidefile, err)
}
msg.Info("You can now edit the glide.yaml file. Consider:")
msg.Info("--> Using versions and ranges. See https://glide.sh/docs/versions/")
msg.Info("--> Adding additional metadata. See https://glide.sh/docs/glide.yaml/")
}
// guardYAML fails if the given file already exists.
//
// This prevents an important file from being overwritten.
func guardYAML(filename string) {
if _, err := os.Stat(filename); err == nil {
msg.Die("Cowardly refusing to overwrite existing YAML.")
}
}
// guessDeps attempts to resolve all of the dependencies for a given project.
//
// base is the directory to start with.
// skipImport will skip running the automatic imports.
//
// FIXME: This function is likely a one-off that has a more standard alternative.
// It's also long and could use a refactor.
func guessDeps(base string, skipImport, noInteract, skipVerSug bool) *cfg.Config {
buildContext, err := util.GetBuildContext()
if err != nil {
msg.Die("Failed to build an import context: %s", err)
}
name := buildContext.PackageName(base)
msg.Info("Generating a YAML configuration file and guessing the dependencies")
config := new(cfg.Config)
// Get the name of the top level package
config.Name = name
// Import by looking at other package managers and looking over the
// entire directory structure.
var deps cfg.Dependencies
// Attempt to import from other package managers.
if !skipImport {
deps = guessImportDeps(base)
if len(deps) == 0 {
msg.Info("No dependencies found to import")
}
}
msg.Info("Scanning code to look for dependencies")
// Resolve dependencies by looking at the tree.
r, err := dependency.NewResolver(base)
if err != nil {
msg.Die("Error creating a dependency resolver: %s", err)
}
h := &dependency.DefaultMissingPackageHandler{Missing: []string{}, Gopath: []string{}}
r.Handler = h
sortable, err := r.ResolveLocal(false)
if err != nil {
msg.Die("Error resolving local dependencies: %s", err)
}
sort.Strings(sortable)
vpath := r.VendorDir
if !strings.HasSuffix(vpath, "/") {
vpath = vpath + string(os.PathSeparator)
}
var count int
var all string
var allOnce bool
for _, pa := range sortable {
n := strings.TrimPrefix(pa, vpath)
root, subpkg := util.NormalizeName(n)
if !config.HasDependency(root) && root != config.Name {
count++
d := deps.Get(root)
if d == nil {
d = &cfg.Dependency{
Name: root,
}
msg.Info("--> Found reference to %s", n)
} else {
msg.Info("--> Found imported reference to %s", n)
}
all, allOnce = guessAskVersion(noInteract, all, allOnce, skipVerSug, d)
if subpkg != "" {
if !d.HasSubpackage(subpkg) {
d.Subpackages = append(d.Subpackages, subpkg)
}
msg.Verbose("--> Noting sub-package %s to %s", subpkg, root)
}
config.Imports = append(config.Imports, d)
} else if config.HasDependency(root) {
if len(subpkg) > 0 {
subpkg = strings.TrimPrefix(subpkg, "/")
d := config.Imports.Get(root)
if !d.HasSubpackage(subpkg) {
d.Subpackages = append(d.Subpackages, subpkg)
}
msg.Verbose("--> Noting sub-package %s to %s", subpkg, root)
}
}
}
if !skipImport && len(deps) > count {
var res string
if noInteract {
res = "y"
} else {
msg.Info("%d unused imported dependencies found. These are likely transitive dependencies ", len(deps)-count)
msg.Info("(dependencies of your dependencies). Would you like to track them in your")
msg.Info("glide.yaml file? Note, Glide will automatically scan your codebase to detect")
msg.Info("the complete dependency tree and import the complete tree. If your dependencies")
msg.Info("do not track dependency version information some version information may be lost.")
msg.Info("Yes (Y) or No (N)?")
res, err = msg.PromptUntil([]string{"y", "yes", "n", "no"})
if err != nil {
msg.Die("Error processing response: %s", err)
}
}
if res == "y" || res == "yes" {
msg.Info("Including additional imports in the glide.yaml file")
for _, dep := range deps {
found := config.Imports.Get(dep.Name)
if found == nil {
config.Imports = append(config.Imports, dep)
if dep.Reference != "" {
all, allOnce = guessAskVersion(noInteract, all, allOnce, skipVerSug, dep)
msg.Info("--> Adding %s at version %s", dep.Name, dep.Reference)
} else {
msg.Info("--> Adding %s", dep.Name)
}
}
}
}
}
return config
}
func guessAskVersion(noInteract bool, all string, allonce, skipVerSug bool, d *cfg.Dependency) (string, bool) {
if !noInteract && !skipVerSug && d.Reference == "" {
// If scanning has not happened already, for example this isn't an
// import, try it now.
var loc string
if d.Repository != "" {
loc = d.Repository
} else {
loc = "https://" + d.Name
}
if !guessVersionCache.checked(loc) {
createGuessVersion(loc, d.Reference)
}
semv := guessVersionCache.get(loc)
if semv != "" {
msg.Info("The package %s appears to have Semantic Version releases. The latest", d.Name)
msg.Info("release is %s. Would you like to use this release? Yes (Y) or No (N)", semv)
res, err2 := msg.PromptUntil([]string{"y", "yes", "n", "no"})
if err2 != nil {
msg.Die("Error processing response: %s", err2)
}
if res == "y" || res == "yes" {
d.Reference = semv
}
}
}
if !noInteract && d.Reference != "" {
var changedVer bool
ver, err := semver.NewVersion(d.Reference)
if err != nil && !skipVerSug {
var loc string
if d.Repository != "" {
loc = d.Repository
} else {
loc = "https://" + d.Name
}
semv := guessVersionCache.get(loc)
if semv != "" {
msg.Info("The package %s appears to have Semantic Version releases but the imported data", d.Name)
msg.Info("is not using them. The latest release is %s but the imported data is using %s.", semv, d.Reference)
msg.Info("Would you like to use the latest release version instead? Yes (Y) or No (N)")
res, err2 := msg.PromptUntil([]string{"y", "yes", "n", "no"})
if err2 != nil {
msg.Die("Error processing response: %s", err2)
}
if res == "y" || res == "yes" {
d.Reference = semv
changedVer = true
}
}
}
if changedVer {
ver, err = semver.NewVersion(d.Reference)
}
if err == nil {
if all == "" {
vstr := ver.String()
msg.Info("Imported dependency %s (%s) appears to use semantic versions (http://semver.org).", d.Name, d.Reference)
msg.Info("Would you like Glide to track the latest minor or patch releases (major.minor.path)?")
msg.Info("Tracking minor version releases would use '>= %s, < %d.0.0' ('^%s'). Tracking patch version", vstr, ver.Major()+1, vstr)
msg.Info("releases would use '>= %s, < %d.%d.0' ('~%s'). For more information on Glide versions", vstr, ver.Major(), ver.Minor()+1, vstr)
msg.Info("and ranges see https://glide.sh/docs/versions")
msg.Info("Minor (M), Patch (P), or Skip Ranges (S)?")
res, err := msg.PromptUntil([]string{"minor", "m", "patch", "p", "skip ranges", "s"})
if err != nil {
msg.Die("Error processing response: %s", err)
}
if res == "m" || res == "minor" {
d.Reference = "^" + vstr
} else if res == "p" || res == "patch" {
d.Reference = "~" + vstr
}
if !allonce {
msg.Info("Would you like to same response (%s) for future dependencies? Yes (Y) or No (N)", res)
res2, err := msg.PromptUntil([]string{"y", "yes", "n", "no"})
if err != nil {
msg.Die("Error processing response: %s", err)
}
if res2 == "yes" || res2 == "y" {
return res, true
}
return "", true
}
} else {
if all == "m" || all == "minor" {
d.Reference = "^" + ver.String()
} else if all == "p" || all == "patch" {
d.Reference = "~" + ver.String()
}
}
return all, allonce
}
return all, allonce
}
return all, allonce
}
func guessImportDeps(base string) cfg.Dependencies {
msg.Info("Attempting to import from other package managers (use --skip-import to skip)")
deps := []*cfg.Dependency{}
absBase, err := filepath.Abs(base)
if err != nil {
msg.Die("Failed to resolve location of %s: %s", base, err)
}
if d, ok := guessImportGodep(absBase); ok {
msg.Info("Importing Godep configuration")
msg.Warn("--> Godep uses commit id versions. Consider using Semantic Versions with Glide")
deps = d
} else if d, ok := guessImportGPM(absBase); ok {
msg.Info("Importing GPM configuration")
deps = d
} else if d, ok := guessImportGB(absBase); ok {
msg.Info("Importing GB configuration")
deps = d
} else if d, ok := guessImportGom(absBase); ok {
msg.Info("Importing GB configuration")
deps = d
}
if len(deps) > 0 {
msg.Info("--> Attempting to detect versions from imported commit ids")
}
var wg sync.WaitGroup
for _, i := range deps {
wg.Add(1)
go func(dep *cfg.Dependency) {
var remote string
if dep.Repository != "" {
remote = dep.Repository
} else {
remote = "https://" + dep.Name
}
ver := createGuessVersion(remote, dep.Reference)
if ver != dep.Reference {
msg.Verbose("--> Found imported reference to %s at version %s", dep.Name, ver)
dep.Reference = ver
}
msg.Debug("--> Found imported reference to %s at revision %s", dep.Name, dep.Reference)
wg.Done()
}(i)
}
wg.Wait()
return deps
}
func guessImportGodep(dir string) ([]*cfg.Dependency, bool) {
d, err := godep.Parse(dir)
if err != nil || len(d) == 0 {
return []*cfg.Dependency{}, false
}
return d, true
}
func guessImportGPM(dir string) ([]*cfg.Dependency, bool) {
d, err := gpm.Parse(dir)
if err != nil || len(d) == 0 {
return []*cfg.Dependency{}, false
}
return d, true
}
func guessImportGB(dir string) ([]*cfg.Dependency, bool) {
d, err := gb.Parse(dir)
if err != nil || len(d) == 0 {
return []*cfg.Dependency{}, false
}
return d, true
}
func guessImportGom(dir string) ([]*cfg.Dependency, bool) {
d, err := gom.Parse(dir)
if err != nil || len(d) == 0 {
return []*cfg.Dependency{}, false
}
return d, true
}
// Note, this really needs a simpler name.
var createGitParseVersion = regexp.MustCompile(`(?m-s)(?:tags)/(\S+)$`)
var guessVersionCache = newVersionCache()
func createGuessVersion(remote, id string) string {
err := cache.Setup()
if err != nil {
msg.Debug("Problem setting up cache: %s", err)
}
l, err := cache.Location()
if err != nil {
msg.Debug("Problem detecting cache location: %s", err)
}
key, err := cache.Key(remote)
if err != nil {
msg.Debug("Problem generating cache key for %s: %s", remote, err)
}
local := filepath.Join(l, "src", key)
repo, err := vcs.NewRepo(remote, local)
if err != nil {
msg.Debug("Problem getting repo instance: %s", err)
}
// Git endpoints allow for querying without fetching the codebase locally.
// We try that first to avoid fetching right away. Is this premature
// optimization?
cc := true
if repo.Vcs() == vcs.Git {
out, err := exec.Command("git", "ls-remote", remote).CombinedOutput()
if err == nil {
guessVersionCache.touchChecked(remote)
cc = false
lines := strings.Split(string(out), "\n")
res := ""
// TODO(mattfarina): Detect if the found version is semver and use
// that one instead of the first found.
for _, i := range lines {
ti := strings.TrimSpace(i)
if found := createGitParseVersion.FindString(ti); found != "" {
tg := strings.TrimPrefix(strings.TrimSuffix(found, "^{}"), "tags/")
if strings.HasPrefix(ti, id) {
res = tg
}
ver, err := semver.NewVersion(tg)
if err == nil {
if guessVersionCache.get(remote) == "" {
guessVersionCache.put(remote, tg)
} else {
ver2, err := semver.NewVersion(guessVersionCache.get(remote))
if err == nil {
if ver.GreaterThan(ver2) {
guessVersionCache.put(remote, tg)
}
}
}
}
}
}
if res != "" {
return res
}
}
}
if cc {
cache.Lock(key)
if _, err = os.Stat(local); os.IsNotExist(err) {
repo.Get()
branch := findCurrentBranch(repo)
c := cache.RepoInfo{DefaultBranch: branch}
err = cache.SaveRepoData(key, c)
if err != nil {
msg.Debug("Error saving cache repo details: %s", err)
}
} else {
repo.Update()
}
tgs, err := repo.TagsFromCommit(id)
if err != nil {
msg.Debug("Problem getting tags for commit: %s", err)
}
cache.Unlock(key)
if len(tgs) > 0 {
return tgs[0]
}
}
return id
}
func findCurrentBranch(repo vcs.Repo) string {
msg.Debug("Attempting to find current branch for %s", repo.Remote())
// Svn and Bzr don't have default branches.
if repo.Vcs() == vcs.Svn || repo.Vcs() == vcs.Bzr {
return ""
}
if repo.Vcs() == vcs.Git || repo.Vcs() == vcs.Hg {
ver, err := repo.Current()
if err != nil {
msg.Debug("Unable to find current branch for %s, error: %s", repo.Remote(), err)
return ""
}
return ver
}
return ""
}
type versionCache struct {
sync.RWMutex
cache map[string]string
cd map[string]bool
}
func newVersionCache() *versionCache {
return &versionCache{
cache: make(map[string]string),
cd: make(map[string]bool),
}
}
func (v *versionCache) put(name, version string) {
v.Lock()
defer v.Unlock()
v.cache[name] = version
v.cd[name] = true
}
func (v *versionCache) checked(name string) bool {
v.RLock()
defer v.RUnlock()
return v.cd[name]
}
func (v *versionCache) touchChecked(name string) {
v.Lock()
defer v.Unlock()
v.cd[name] = true
}
func (v *versionCache) get(name string) string {
v.RLock()
defer v.RUnlock()
return v.cache[name]
}