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) +}