Merge pull request #444 from Masterminds/feat/reworked-init
Rework create/init
diff --git a/action/config_wizard.go b/action/config_wizard.go
new file mode 100644
index 0000000..c00c3d7
--- /dev/null
+++ b/action/config_wizard.go
@@ -0,0 +1,362 @@
+package action
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/Masterminds/glide/cache"
+ "github.com/Masterminds/glide/cfg"
+ "github.com/Masterminds/glide/msg"
+ gpath "github.com/Masterminds/glide/path"
+ "github.com/Masterminds/semver"
+ "github.com/Masterminds/vcs"
+)
+
+// ConfigWizard reads configuration from a glide.yaml file and attempts to suggest
+// improvements. The wizard is interactive.
+func ConfigWizard(base string) {
+ _, err := gpath.Glide()
+ glidefile := gpath.GlideFile
+ if err != nil {
+ msg.Info("Unable to find a glide.yaml file. Would you like to create one now? Yes (Y) or No (N)")
+ bres := msg.PromptUntilYorN()
+ if bres {
+ // Guess deps
+ conf := guessDeps(base, false)
+ // Write YAML
+ if err := conf.WriteFile(glidefile); err != nil {
+ msg.Die("Could not save %s: %s", glidefile, err)
+ }
+ } else {
+ msg.Err("Unable to find configuration file. Please create configuration information to continue.")
+ }
+ }
+
+ conf := EnsureConfig()
+
+ err = cache.Setup()
+ if err != nil {
+ msg.Die("Problem setting up cache: %s", err)
+ }
+
+ msg.Info("Looking for dependencies to make suggestions on")
+ msg.Info("--> Scanning for dependencies not using verion ranges")
+ msg.Info("--> Scanning for dependencies using commit ids")
+ var deps []*cfg.Dependency
+ for _, dep := range conf.Imports {
+ if wizardLookInto(dep) {
+ deps = append(deps, dep)
+ }
+ }
+
+ msg.Info("Gathering information on each dependency")
+ msg.Info("--> This may take a moment. Especially on a codebase with many dependencies")
+ msg.Info("--> Gathering release information for dependencies")
+ msg.Info("--> Looking for dependency imports where versions are commit ids")
+ for _, dep := range deps {
+ wizardFindVersions(dep)
+ }
+
+ var changes int
+ for _, dep := range deps {
+ var remote string
+ if dep.Repository != "" {
+ remote = dep.Repository
+ } else {
+ remote = "https://" + dep.Name
+ }
+
+ // First check, ask if the tag should be used instead of the commit id for it.
+ cur := cache.MemCurrent(remote)
+ if cur != "" && cur != dep.Reference {
+ wizardSugOnce()
+ var dres bool
+ asked, use, val := wizardOnce("current")
+ if !use {
+ dres = wizardAskCurrent(cur, dep)
+ }
+ if !asked {
+ as := wizardRemember()
+ wizardSetOnce("current", as, dres)
+ }
+
+ if asked && use {
+ dres = val.(bool)
+ }
+
+ if dres {
+ msg.Info("Updating %s to use the tag %s instead of commit id %s", dep.Name, cur, dep.Reference)
+ dep.Reference = cur
+ changes++
+ }
+ }
+
+ // Second check, if no version is being used and there's a semver release ask about latest.
+ memlatest := cache.MemLatest(remote)
+ if dep.Reference == "" && memlatest != "" {
+ wizardSugOnce()
+ var dres bool
+ asked, use, val := wizardOnce("latest")
+ if !use {
+ dres = wizardAskLatest(memlatest, dep)
+ }
+ if !asked {
+ as := wizardRemember()
+ wizardSetOnce("latest", as, dres)
+ }
+
+ if asked && use {
+ dres = val.(bool)
+ }
+
+ if dres {
+ msg.Info("Updating %s to use the release %s instead of no release", dep.Name, memlatest)
+ dep.Reference = memlatest
+ changes++
+ }
+ }
+
+ // Third check, if the version is semver offer to use a range instead.
+ sv, err := semver.NewVersion(dep.Reference)
+ if err == nil {
+ wizardSugOnce()
+ var res string
+ asked, use, val := wizardOnce("range")
+ if !use {
+ res = wizardAskRange(sv, dep)
+ }
+ if !asked {
+ as := wizardRemember()
+ wizardSetOnce("range", as, res)
+ }
+
+ if asked && use {
+ res = val.(string)
+ }
+
+ if res == "m" {
+ r := "^" + sv.String()
+ msg.Info("Updating %s to use the range %s instead of commit id %s", dep.Name, r, dep.Reference)
+ dep.Reference = r
+ changes++
+ } else if res == "p" {
+ r := "~" + sv.String()
+ msg.Info("Updating %s to use the range %s instead of commit id %s", dep.Name, r, dep.Reference)
+ dep.Reference = r
+ changes++
+ }
+ }
+ }
+
+ if changes > 0 {
+ msg.Info("Configuration changes have been made. Would you like to write these")
+ msg.Info("changes to your configuration file? Yes (Y) or No (N)")
+ dres := msg.PromptUntilYorN()
+ if dres {
+ msg.Info("Writing updates to configuration file (%s)", glidefile)
+ 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.:")
+ msg.Info("--> For more information on versions and ranges see https://glide.sh/docs/versions/")
+ msg.Info("--> For details on additional metadata see https://glide.sh/docs/glide.yaml/")
+ } else {
+ msg.Warn("Change not written to configuration file")
+ }
+ } else {
+ msg.Info("No proposed changes found. Have a nice day.")
+ }
+}
+
+var wizardOnceVal = make(map[string]interface{})
+var wizardOnceDo = make(map[string]bool)
+var wizardOnceAsked = make(map[string]bool)
+
+var wizardSuggeseOnce bool
+
+func wizardSugOnce() {
+ if !wizardSuggeseOnce {
+ msg.Info("Here are some suggestions...")
+ }
+ wizardSuggeseOnce = true
+}
+
+// Returns if it's you should prompt, if not prompt if you should use stored value,
+// and stored value if it has one.
+func wizardOnce(name string) (bool, bool, interface{}) {
+ return wizardOnceAsked[name], wizardOnceDo[name], wizardOnceVal[name]
+}
+
+func wizardSetOnce(name string, prompt bool, val interface{}) {
+ wizardOnceAsked[name] = true
+ wizardOnceDo[name] = prompt
+ wizardOnceVal[name] = val
+}
+
+func wizardRemember() bool {
+ msg.Info("Would you like to remember the previous decision and apply it to future")
+ msg.Info("dependencies? Yes (Y) or No (N)")
+ return msg.PromptUntilYorN()
+}
+
+func wizardAskRange(ver *semver.Version, d *cfg.Dependency) string {
+ vstr := ver.String()
+ msg.Info("The package %s appears to use semantic versions (http://semver.org).", d.Name)
+ msg.Info("Would you like 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" {
+ return "m"
+ } else if res == "p" || res == "patch" {
+ return "p"
+ }
+
+ return "s"
+}
+
+func wizardAskCurrent(cur string, d *cfg.Dependency) bool {
+ msg.Info("The package %s is currently set to use the version %s.", d.Name, d.Reference)
+ msg.Info("There is an equivalent semantic version (http://semver.org) release of %s. Would", cur)
+ msg.Info("you like to use that instead? Yes (Y) or No (N)")
+ return msg.PromptUntilYorN()
+}
+
+func wizardAskLatest(latest string, d *cfg.Dependency) bool {
+ msg.Info("The package %s appears to have Semantic Version releases (http://semver.org). ", d.Name)
+ msg.Info("The latestrelease is %s. You are currently not using a release. Would you like", latest)
+ msg.Info("to use this release? Yes (Y) or No (N)")
+ return msg.PromptUntilYorN()
+}
+
+func wizardLookInto(d *cfg.Dependency) bool {
+ _, err := semver.NewConstraint(d.Reference)
+
+ // The existing version is already a valid semver constraint so we skip suggestions.
+ if err == nil {
+ return false
+ }
+
+ return true
+}
+
+// Note, this really needs a simpler name.
+var createGitParseVersion = regexp.MustCompile(`(?m-s)(?:tags)/(\S+)$`)
+
+func wizardFindVersions(d *cfg.Dependency) {
+ l, err := cache.Location()
+ if err != nil {
+ msg.Debug("Problem detecting cache location: %s", err)
+ }
+ var remote string
+ if d.Repository != "" {
+ remote = d.Repository
+ } else {
+ remote = "https://" + d.Name
+ }
+
+ 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)
+ }
+
+ var useLocal bool
+ if _, err = os.Stat(local); err == nil {
+ useLocal = true
+ }
+
+ // 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 !useLocal && repo.Vcs() == vcs.Git {
+ out, err2 := exec.Command("git", "ls-remote", remote).CombinedOutput()
+ if err2 == nil {
+ cache.MemTouch(remote)
+ cc = false
+ lines := strings.Split(string(out), "\n")
+ for _, i := range lines {
+ ti := strings.TrimSpace(i)
+ if found := createGitParseVersion.FindString(ti); found != "" {
+ tg := strings.TrimPrefix(strings.TrimSuffix(found, "^{}"), "tags/")
+ cache.MemPut(remote, tg)
+ if d.Reference != "" && strings.HasPrefix(ti, d.Reference) {
+ cache.MemSetCurrent(remote, tg)
+ }
+ }
+ }
+ }
+ }
+
+ if cc {
+ cache.Lock(key)
+ cache.MemTouch(remote)
+ 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.Tags()
+ if err != nil {
+ msg.Debug("Problem getting tags: %s", err)
+ } else {
+ for _, v := range tgs {
+ cache.MemPut(remote, v)
+ }
+ }
+ if d.Reference != "" && repo.IsReference(d.Reference) {
+ tgs, err = repo.TagsFromCommit(d.Reference)
+ if err != nil {
+ msg.Debug("Problem getting tags for commit: %s", err)
+ } else {
+ if len(tgs) > 0 {
+ for _, v := range tgs {
+ if !(repo.Vcs() == vcs.Hg && v == "tip") {
+ cache.MemSetCurrent(remote, v)
+ }
+ }
+ }
+ }
+ }
+ cache.Unlock(key)
+ }
+}
+
+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 ""
+}
diff --git a/action/create.go b/action/create.go
index b66257c..4d1c0bb 100644
--- a/action/create.go
+++ b/action/create.go
@@ -25,7 +25,7 @@
// 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, nonInteractive bool) {
glidefile := gpath.GlideFile
// Guard against overwrites.
guardYAML(glidefile)
@@ -33,9 +33,28 @@
// Guess deps
conf := guessDeps(base, skipImport)
// Write YAML
+ msg.Info("Writing configuration file (%s)", glidefile)
if err := conf.WriteFile(glidefile); err != nil {
msg.Die("Could not save %s: %s", glidefile, err)
}
+
+ var res bool
+ if !nonInteractive {
+ msg.Info("Would you like Glide to help you find ways to improve your glide.yaml configuration?")
+ msg.Info("If you want to revisit this step you can use the config-wizard command at any time.")
+ msg.Info("Yes (Y) or No (N)?")
+ res = msg.PromptUntilYorN()
+ if res {
+ ConfigWizard(base)
+ }
+ }
+
+ if !res {
+ 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/")
+ msg.Info("--> Running the config-wizard command to improve the versions in your configuration")
+ }
}
// guardYAML fails if the given file already exists.
@@ -76,6 +95,13 @@
guessImportDeps(base, config)
}
+ importLen := len(config.Imports)
+ if importLen == 0 {
+ msg.Info("Scanning code to look for dependencies")
+ } else {
+ msg.Info("Scanning code to look for dependencies not found in import")
+ }
+
// Resolve dependencies by looking at the tree.
r, err := dependency.NewResolver(base)
if err != nil {
@@ -102,7 +128,7 @@
root, subpkg := util.NormalizeName(n)
if !config.HasDependency(root) && root != config.Name {
- msg.Info("Found reference to %s\n", n)
+ msg.Info("--> Found reference to %s\n", n)
d := &cfg.Dependency{
Name: root,
}
@@ -115,13 +141,17 @@
subpkg = strings.TrimPrefix(subpkg, "/")
d := config.Imports.Get(root)
if !d.HasSubpackage(subpkg) {
- msg.Info("Adding sub-package %s to %s\n", subpkg, root)
+ msg.Info("--> Adding sub-package %s to %s\n", subpkg, root)
d.Subpackages = append(d.Subpackages, subpkg)
}
}
}
}
+ if len(config.Imports) == importLen && importLen != 0 {
+ msg.Info("--> Code scanning found no additional imports")
+ }
+
return config
}
@@ -146,7 +176,12 @@
}
for _, i := range deps {
- msg.Info("Found imported reference to %s\n", i.Name)
+ if i.Reference == "" {
+ msg.Info("--> Found imported reference to %s", i.Name)
+ } else {
+ msg.Info("--> Found imported reference to %s at revision %s", i.Name, i.Reference)
+ }
+
config.Imports = append(config.Imports, i)
}
}
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/cache/memory.go b/cache/memory.go
new file mode 100644
index 0000000..3809331
--- /dev/null
+++ b/cache/memory.go
@@ -0,0 +1,139 @@
+package cache
+
+import (
+ "sync"
+
+ "github.com/Masterminds/glide/msg"
+ "github.com/Masterminds/semver"
+)
+
+// Provide an in memory cache of imported project information.
+
+var defaultMemCache = newMemCache()
+
+// MemPut put a version into the in memory cache for a name.
+// This will silently ignore non-semver and make sure the latest
+// is stored.
+func MemPut(name, version string) {
+ defaultMemCache.put(name, version)
+}
+
+// MemTouched returns true if the cache was touched for a name.
+func MemTouched(name string) bool {
+ return defaultMemCache.touched(name)
+}
+
+// MemTouch notes if a name has been looked at.
+func MemTouch(name string) {
+ defaultMemCache.touch(name)
+}
+
+// MemLatest returns the latest, that is most recent, semver release. This
+// may be a blank string if no put value
+func MemLatest(name string) string {
+ return defaultMemCache.getLatest(name)
+}
+
+// MemSetCurrent is used to set the current version in use.
+func MemSetCurrent(name, version string) {
+ defaultMemCache.setCurrent(name, version)
+}
+
+// MemCurrent is used to get the current version in use.
+func MemCurrent(name string) string {
+ return defaultMemCache.current(name)
+}
+
+// An in memory cache.
+type memCache struct {
+ sync.RWMutex
+ latest map[string]string
+ t map[string]bool
+ versions map[string][]string
+ c map[string]string
+}
+
+func newMemCache() *memCache {
+ return &memCache{
+ latest: make(map[string]string),
+ t: make(map[string]bool),
+ versions: make(map[string][]string),
+ c: make(map[string]string),
+ }
+}
+
+func (m *memCache) setCurrent(name, version string) {
+ m.Lock()
+ defer m.Unlock()
+
+ if m.c[name] == "" {
+ m.c[name] = version
+ } else {
+ // If we already have a version try to see if the new or old one is
+ // semver and use that one.
+ _, err := semver.NewVersion(m.c[name])
+ if err != nil {
+ _, err2 := semver.NewVersion(version)
+ if err2 == nil {
+ m.c[name] = version
+ }
+ }
+ }
+}
+
+func (m *memCache) current(name string) string {
+ m.RLock()
+ defer m.RUnlock()
+ return m.c[name]
+}
+
+func (m *memCache) put(name, version string) {
+ m.Lock()
+ defer m.Unlock()
+ m.t[name] = true
+ sv, err := semver.NewVersion(version)
+ if err != nil {
+ msg.Debug("Ignoring %s version %s: %s", name, version, err)
+ return
+ }
+
+ latest, found := m.latest[name]
+ if found {
+ lv, err := semver.NewVersion(latest)
+ if err != nil {
+ if sv.GreaterThan(lv) {
+ m.latest[name] = version
+ }
+ }
+ } else {
+ m.latest[name] = version
+ }
+
+ found = false
+ for _, v := range m.versions[name] {
+ if v == version {
+ found = true
+ }
+ }
+ if !found {
+ m.versions[name] = append(m.versions[name], version)
+ }
+}
+
+func (m *memCache) touch(name string) {
+ m.Lock()
+ defer m.Unlock()
+ m.t[name] = true
+}
+
+func (m *memCache) touched(name string) bool {
+ m.RLock()
+ defer m.RUnlock()
+ return m.t[name]
+}
+
+func (m *memCache) getLatest(name string) string {
+ m.RLock()
+ defer m.RUnlock()
+ return m.latest[name]
+}
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..5bd2894 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,24 @@
Name: "skip-import",
Usage: "When initializing skip importing from other package managers.",
},
+ cli.BoolFlag{
+ Name: "non-interactive",
+ Usage: "Disable interactive prompts.",
+ },
},
Action: func(c *cli.Context) {
- action.Create(".", c.Bool("skip-import"))
+ action.Create(".", c.Bool("skip-import"), c.Bool("non-interactive"))
+ },
+ },
+ {
+ Name: "config-wizard",
+ ShortName: "cw",
+ Usage: "Wizard that makes optional suggestions to improve config in a glide.yaml file.",
+ Description: `Glide will analyze a projects glide.yaml file and the imported
+ projects to find ways the glide.yaml file can potentially be improved. It
+ will then interactively make suggestions that you can skip or accept.`,
+ Action: func(c *cli.Context) {
+ action.ConfigWizard(".")
},
},
{
@@ -664,6 +683,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..34213fd 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,54 @@
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)
+}
+
+// PromptUntilYorN provides a prompt until the user chooses yes or no. This is
+// not case sensitive and they can input other options such as Y or N.
+// In the response Yes is bool true and No is bool false.
+func (m *Messenger) PromptUntilYorN() bool {
+ res, err := m.PromptUntil([]string{"y", "yes", "n", "no"})
+ if err != nil {
+ m.Die("Error processing response: %s", err)
+ }
+
+ if res == "y" || res == "yes" {
+ return true
+ }
+
+ return false
+}
+
+// PromptUntilYorN provides a prompt until the user chooses yes or no. This is
+// not case sensitive and they can input other options such as Y or N.
+// In the response Yes is bool true and No is bool false.
+// Uses the default setup.
+func PromptUntilYorN() bool {
+ return Default.PromptUntilYorN()
+}