|  | 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 version 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) | 
|  | } | 
|  | } | 
|  | for _, dep := range conf.DevImports { | 
|  | 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.patch)?") | 
|  | 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 latest release 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) | 
|  | return | 
|  | } | 
|  | 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) | 
|  | return | 
|  | } | 
|  |  | 
|  | local := filepath.Join(l, "src", key) | 
|  | repo, err := vcs.NewRepo(remote, local) | 
|  | if err != nil { | 
|  | msg.Debug("Problem getting repo instance: %s", err) | 
|  | return | 
|  | } | 
|  |  | 
|  | 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 "" | 
|  | } |