Merging from master
diff --git a/action/create.go b/action/create.go
index b66257c..fa72e6b 100644
--- a/action/create.go
+++ b/action/create.go
@@ -2,20 +2,31 @@
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.
@@ -25,17 +36,21 @@
// 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 bool) {
+func Create(base string, skipImport, noInteract, skipVerSug bool) {
glidefile := gpath.GlideFile
// Guard against overwrites.
guardYAML(glidefile)
// Guess deps
- conf := guessDeps(base, skipImport)
+ 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.
@@ -54,7 +69,7 @@
//
// 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 bool) *cfg.Config {
+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)
@@ -70,12 +85,18 @@
// 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 {
- guessImportDeps(base, config)
+ 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 {
@@ -97,27 +118,76 @@
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 {
- msg.Info("Found reference to %s\n", n)
- d := &cfg.Dependency{
- Name: root,
+ 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)
}
- if len(subpkg) > 0 {
- d.Subpackages = []string{subpkg}
+
+ 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) {
- msg.Info("Adding sub-package %s to %s\n", subpkg, root)
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)
+ }
+ }
}
}
}
@@ -125,7 +195,117 @@
return config
}
-func guessImportDeps(base string, config *cfg.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)
@@ -135,7 +315,7 @@
if d, ok := guessImportGodep(absBase); ok {
msg.Info("Importing Godep configuration")
- msg.Warn("Godep uses commit id versions. Consider using Semantic Versions with Glide")
+ 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")
@@ -143,12 +323,41 @@
} 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
}
- for _, i := range deps {
- msg.Info("Found imported reference to %s\n", i.Name)
- config.Imports = append(config.Imports, i)
+ 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) {
@@ -177,3 +386,165 @@
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]
+}
diff --git a/action/debug.go b/action/debug.go
index 7b8eea6..6af9d26 100644
--- a/action/debug.go
+++ b/action/debug.go
@@ -7,6 +7,15 @@
// Debug sets the debugging flags across components.
func Debug(on bool) {
msg.Default.IsDebugging = on
+
+ if on == true {
+ msg.Default.IsVerbose = on
+ }
+}
+
+// Verbose sets the verbose flags across components.
+func Verbose(on bool) {
+ msg.Default.IsVerbose = on
}
// Quiet sets the quiet flags across components.
diff --git a/gb/gb.go b/gb/gb.go
index 1f3b208..7837a42 100644
--- a/gb/gb.go
+++ b/gb/gb.go
@@ -26,6 +26,7 @@
}
msg.Info("Found GB manifest file in %s", gpath.StripBasepath(dir))
+ msg.Info("--> Parsing GB metadata...")
buf := []*cfg.Dependency{}
file, err := os.Open(path)
if err != nil {
diff --git a/glide.go b/glide.go
index 0e1e355..2e01e1c 100644
--- a/glide.go
+++ b/glide.go
@@ -92,8 +92,12 @@
Usage: "Quiet (no info or debug messages)",
},
cli.BoolFlag{
+ Name: "verbose",
+ Usage: "Print detailed informational messages",
+ },
+ cli.BoolFlag{
Name: "debug",
- Usage: "Print Debug messages (verbose)",
+ Usage: "Print debug verbose informational messages",
},
cli.StringFlag{
Name: "home",
@@ -146,9 +150,17 @@
Name: "skip-import",
Usage: "When initializing skip importing from other package managers.",
},
+ cli.BoolFlag{
+ Name: "non-interactive",
+ Usage: "Disable interactive prompts.",
+ },
+ cli.BoolFlag{
+ Name: "skip-version-suggestions",
+ Usage: "When imported commit ids are found that don't map to versions skip suggesting a version.",
+ },
},
Action: func(c *cli.Context) {
- action.Create(".", c.Bool("skip-import"))
+ action.Create(".", c.Bool("skip-import"), c.Bool("non-interactive"), c.Bool("skip-version-suggestions"))
},
},
{
@@ -664,6 +676,7 @@
// so it can be used by any Glide command.
func startup(c *cli.Context) error {
action.Debug(c.Bool("debug"))
+ action.Verbose(c.Bool("verbose"))
action.NoColor(c.Bool("no-color"))
action.Quiet(c.Bool("quiet"))
action.Init(c.String("yaml"), c.String("home"))
diff --git a/godep/godep.go b/godep/godep.go
index a7d0525..71291b3 100644
--- a/godep/godep.go
+++ b/godep/godep.go
@@ -55,6 +55,7 @@
return []*cfg.Dependency{}, nil
}
msg.Info("Found Godeps.json file in %s", gpath.StripBasepath(dir))
+ msg.Info("--> Parsing Godeps metadata...")
buf := []*cfg.Dependency{}
diff --git a/gom/gom.go b/gom/gom.go
index 76305e4..51910b0 100644
--- a/gom/gom.go
+++ b/gom/gom.go
@@ -26,6 +26,7 @@
}
msg.Info("Found Gomfile in %s", gpath.StripBasepath(dir))
+ msg.Info("--> Parsing Gomfile metadata...")
buf := []*cfg.Dependency{}
goms, err := parseGomfile(path)
diff --git a/gpm/gpm.go b/gpm/gpm.go
index 45e316f..e58a81d 100644
--- a/gpm/gpm.go
+++ b/gpm/gpm.go
@@ -31,6 +31,7 @@
return []*cfg.Dependency{}, nil
}
msg.Info("Found Godeps file in %s", gpath.StripBasepath(dir))
+ msg.Info("--> Parsing GPM metadata...")
buf := []*cfg.Dependency{}
diff --git a/msg/msg.go b/msg/msg.go
index af49ecf..bb8b919 100644
--- a/msg/msg.go
+++ b/msg/msg.go
@@ -1,6 +1,7 @@
package msg
import (
+ "bufio"
"fmt"
"io"
"os"
@@ -16,9 +17,12 @@
// Quiet, if true, suppresses chatty levels, like Info.
Quiet bool
- // IsDebugging, if true, shows verbose levels, like Debug.
+ // IsDebugging, if true, shows Debug.
IsDebugging bool
+ // IsVerbose, if true, shows detailed informational messages.
+ IsVerbose bool
+
// NoColor, if true, will not use color in the output.
NoColor bool
@@ -28,6 +32,9 @@
// Stderr is the location where this prints logs.
Stderr io.Writer
+ // Stdin is the location where input is read.
+ Stdin io.Reader
+
// PanicOnDie if true Die() will panic instead of exiting.
PanicOnDie bool
@@ -43,9 +50,11 @@
m := &Messenger{
Quiet: false,
IsDebugging: false,
+ IsVerbose: false,
NoColor: false,
Stdout: os.Stdout,
Stderr: os.Stderr,
+ Stdin: os.Stdin,
PanicOnDie: false,
ecode: 1,
}
@@ -76,7 +85,7 @@
return
}
prefix := "[DEBUG]\t"
- Msg(prefix+msg, args...)
+ m.Msg(prefix+msg, args...)
}
// Debug logs debug information using the Default Messenger
@@ -84,6 +93,19 @@
Default.Debug(msg, args...)
}
+// Verbose logs detailed information
+func (m *Messenger) Verbose(msg string, args ...interface{}) {
+ if m.Quiet || !m.IsVerbose {
+ return
+ }
+ m.Info(msg, args...)
+}
+
+// Verbose detailed information using the Default Messenger
+func Verbose(msg string, args ...interface{}) {
+ Default.Verbose(msg, args...)
+}
+
// Warn logs a warning
func (m *Messenger) Warn(msg string, args ...interface{}) {
prefix := m.Color(Yellow, "[WARN]\t")
@@ -238,3 +260,30 @@
func Color(code, msg string) string {
return Default.Color(code, msg)
}
+
+// PromptUntil provides a prompt until one of the passed in strings has been
+// entered and return is hit. Note, the comparisons are case insensitive meaning
+// Y == y. The returned value is the one from the passed in options (same case).
+func (m *Messenger) PromptUntil(opts []string) (string, error) {
+ reader := bufio.NewReader(os.Stdin)
+ for {
+ text, err := reader.ReadString('\n')
+ if err != nil {
+ return "", err
+ }
+
+ for _, c := range opts {
+ if strings.EqualFold(c, strings.TrimSpace(text)) {
+ return c, nil
+ }
+ }
+ }
+}
+
+// PromptUntil provides a prompt until one of the passed in strings has been
+// entered and return is hit. Note, the comparisons are case insensitive meaning
+// Y == y. The returned value is the one from the passed in options (same case).
+// Uses the default setup.
+func PromptUntil(opts []string) (string, error) {
+ return Default.PromptUntil(opts)
+}