github.com/heyitsanthony/glide@v0.12.3/action/config_wizard.go (about)

     1  package action
     2  
     3  import (
     4  	"os"
     5  	"os/exec"
     6  	"path/filepath"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/Masterminds/glide/cache"
    11  	"github.com/Masterminds/glide/cfg"
    12  	"github.com/Masterminds/glide/msg"
    13  	gpath "github.com/Masterminds/glide/path"
    14  	"github.com/Masterminds/semver"
    15  	"github.com/Masterminds/vcs"
    16  )
    17  
    18  // ConfigWizard reads configuration from a glide.yaml file and attempts to suggest
    19  // improvements. The wizard is interactive.
    20  func ConfigWizard(base string) {
    21  	cache.SystemLock()
    22  	_, err := gpath.Glide()
    23  	glidefile := gpath.GlideFile
    24  	if err != nil {
    25  		msg.Info("Unable to find a glide.yaml file. Would you like to create one now? Yes (Y) or No (N)")
    26  		bres := msg.PromptUntilYorN()
    27  		if bres {
    28  			// Guess deps
    29  			conf := guessDeps(base, false)
    30  			// Write YAML
    31  			if err := conf.WriteFile(glidefile); err != nil {
    32  				msg.Die("Could not save %s: %s", glidefile, err)
    33  			}
    34  		} else {
    35  			msg.Err("Unable to find configuration file. Please create configuration information to continue.")
    36  		}
    37  	}
    38  
    39  	conf := EnsureConfig()
    40  
    41  	cache.Setup()
    42  
    43  	msg.Info("Looking for dependencies to make suggestions on")
    44  	msg.Info("--> Scanning for dependencies not using version ranges")
    45  	msg.Info("--> Scanning for dependencies using commit ids")
    46  	var deps []*cfg.Dependency
    47  	for _, dep := range conf.Imports {
    48  		if wizardLookInto(dep) {
    49  			deps = append(deps, dep)
    50  		}
    51  	}
    52  	for _, dep := range conf.DevImports {
    53  		if wizardLookInto(dep) {
    54  			deps = append(deps, dep)
    55  		}
    56  	}
    57  
    58  	msg.Info("Gathering information on each dependency")
    59  	msg.Info("--> This may take a moment. Especially on a codebase with many dependencies")
    60  	msg.Info("--> Gathering release information for dependencies")
    61  	msg.Info("--> Looking for dependency imports where versions are commit ids")
    62  	for _, dep := range deps {
    63  		wizardFindVersions(dep)
    64  	}
    65  
    66  	var changes int
    67  	for _, dep := range deps {
    68  		remote := dep.Remote()
    69  
    70  		// First check, ask if the tag should be used instead of the commit id for it.
    71  		cur := cache.MemCurrent(remote)
    72  		if cur != "" && cur != dep.Reference {
    73  			wizardSugOnce()
    74  			var dres bool
    75  			asked, use, val := wizardOnce("current")
    76  			if !use {
    77  				dres = wizardAskCurrent(cur, dep)
    78  			}
    79  			if !asked {
    80  				as := wizardRemember()
    81  				wizardSetOnce("current", as, dres)
    82  			}
    83  
    84  			if asked && use {
    85  				dres = val.(bool)
    86  			}
    87  
    88  			if dres {
    89  				msg.Info("Updating %s to use the tag %s instead of commit id %s", dep.Name, cur, dep.Reference)
    90  				dep.Reference = cur
    91  				changes++
    92  			}
    93  		}
    94  
    95  		// Second check, if no version is being used and there's a semver release ask about latest.
    96  		memlatest := cache.MemLatest(remote)
    97  		if dep.Reference == "" && memlatest != "" {
    98  			wizardSugOnce()
    99  			var dres bool
   100  			asked, use, val := wizardOnce("latest")
   101  			if !use {
   102  				dres = wizardAskLatest(memlatest, dep)
   103  			}
   104  			if !asked {
   105  				as := wizardRemember()
   106  				wizardSetOnce("latest", as, dres)
   107  			}
   108  
   109  			if asked && use {
   110  				dres = val.(bool)
   111  			}
   112  
   113  			if dres {
   114  				msg.Info("Updating %s to use the release %s instead of no release", dep.Name, memlatest)
   115  				dep.Reference = memlatest
   116  				changes++
   117  			}
   118  		}
   119  
   120  		// Third check, if the version is semver offer to use a range instead.
   121  		sv, err := semver.NewVersion(dep.Reference)
   122  		if err == nil {
   123  			wizardSugOnce()
   124  			var res string
   125  			asked, use, val := wizardOnce("range")
   126  			if !use {
   127  				res = wizardAskRange(sv, dep)
   128  			}
   129  			if !asked {
   130  				as := wizardRemember()
   131  				wizardSetOnce("range", as, res)
   132  			}
   133  
   134  			if asked && use {
   135  				res = val.(string)
   136  			}
   137  
   138  			if res == "m" {
   139  				r := "^" + sv.String()
   140  				msg.Info("Updating %s to use the range %s instead of commit id %s", dep.Name, r, dep.Reference)
   141  				dep.Reference = r
   142  				changes++
   143  			} else if res == "p" {
   144  				r := "~" + sv.String()
   145  				msg.Info("Updating %s to use the range %s instead of commit id %s", dep.Name, r, dep.Reference)
   146  				dep.Reference = r
   147  				changes++
   148  			}
   149  		}
   150  	}
   151  
   152  	if changes > 0 {
   153  		msg.Info("Configuration changes have been made. Would you like to write these")
   154  		msg.Info("changes to your configuration file? Yes (Y) or No (N)")
   155  		dres := msg.PromptUntilYorN()
   156  		if dres {
   157  			msg.Info("Writing updates to configuration file (%s)", glidefile)
   158  			if err := conf.WriteFile(glidefile); err != nil {
   159  				msg.Die("Could not save %s: %s", glidefile, err)
   160  			}
   161  			msg.Info("You can now edit the glide.yaml file.:")
   162  			msg.Info("--> For more information on versions and ranges see https://glide.sh/docs/versions/")
   163  			msg.Info("--> For details on additional metadata see https://glide.sh/docs/glide.yaml/")
   164  		} else {
   165  			msg.Warn("Change not written to configuration file")
   166  		}
   167  	} else {
   168  		msg.Info("No proposed changes found. Have a nice day.")
   169  	}
   170  }
   171  
   172  var wizardOnceVal = make(map[string]interface{})
   173  var wizardOnceDo = make(map[string]bool)
   174  var wizardOnceAsked = make(map[string]bool)
   175  
   176  var wizardSuggeseOnce bool
   177  
   178  func wizardSugOnce() {
   179  	if !wizardSuggeseOnce {
   180  		msg.Info("Here are some suggestions...")
   181  	}
   182  	wizardSuggeseOnce = true
   183  }
   184  
   185  // Returns if it's you should prompt, if not prompt if you should use stored value,
   186  // and stored value if it has one.
   187  func wizardOnce(name string) (bool, bool, interface{}) {
   188  	return wizardOnceAsked[name], wizardOnceDo[name], wizardOnceVal[name]
   189  }
   190  
   191  func wizardSetOnce(name string, prompt bool, val interface{}) {
   192  	wizardOnceAsked[name] = true
   193  	wizardOnceDo[name] = prompt
   194  	wizardOnceVal[name] = val
   195  }
   196  
   197  func wizardRemember() bool {
   198  	msg.Info("Would you like to remember the previous decision and apply it to future")
   199  	msg.Info("dependencies? Yes (Y) or No (N)")
   200  	return msg.PromptUntilYorN()
   201  }
   202  
   203  func wizardAskRange(ver *semver.Version, d *cfg.Dependency) string {
   204  	vstr := ver.String()
   205  	msg.Info("The package %s appears to use semantic versions (http://semver.org).", d.Name)
   206  	msg.Info("Would you like to track the latest minor or patch releases (major.minor.patch)?")
   207  	msg.Info("Tracking minor version releases would use '>= %s, < %d.0.0' ('^%s'). Tracking patch version", vstr, ver.Major()+1, vstr)
   208  	msg.Info("releases would use '>= %s, < %d.%d.0' ('~%s'). For more information on Glide versions", vstr, ver.Major(), ver.Minor()+1, vstr)
   209  	msg.Info("and ranges see https://glide.sh/docs/versions")
   210  	msg.Info("Minor (M), Patch (P), or Skip Ranges (S)?")
   211  	res, err := msg.PromptUntil([]string{"minor", "m", "patch", "p", "skip ranges", "s"})
   212  	if err != nil {
   213  		msg.Die("Error processing response: %s", err)
   214  	}
   215  	if res == "m" || res == "minor" {
   216  		return "m"
   217  	} else if res == "p" || res == "patch" {
   218  		return "p"
   219  	}
   220  
   221  	return "s"
   222  }
   223  
   224  func wizardAskCurrent(cur string, d *cfg.Dependency) bool {
   225  	msg.Info("The package %s is currently set to use the version %s.", d.Name, d.Reference)
   226  	msg.Info("There is an equivalent semantic version (http://semver.org) release of %s. Would", cur)
   227  	msg.Info("you like to use that instead? Yes (Y) or No (N)")
   228  	return msg.PromptUntilYorN()
   229  }
   230  
   231  func wizardAskLatest(latest string, d *cfg.Dependency) bool {
   232  	msg.Info("The package %s appears to have Semantic Version releases (http://semver.org). ", d.Name)
   233  	msg.Info("The latest release is %s. You are currently not using a release. Would you like", latest)
   234  	msg.Info("to use this release? Yes (Y) or No (N)")
   235  	return msg.PromptUntilYorN()
   236  }
   237  
   238  func wizardLookInto(d *cfg.Dependency) bool {
   239  	_, err := semver.NewConstraint(d.Reference)
   240  
   241  	// The existing version is already a valid semver constraint so we skip suggestions.
   242  	if err == nil {
   243  		return false
   244  	}
   245  
   246  	return true
   247  }
   248  
   249  // Note, this really needs a simpler name.
   250  var createGitParseVersion = regexp.MustCompile(`(?m-s)(?:tags)/(\S+)$`)
   251  
   252  func wizardFindVersions(d *cfg.Dependency) {
   253  	l := cache.Location()
   254  	remote := d.Remote()
   255  
   256  	key, err := cache.Key(remote)
   257  	if err != nil {
   258  		msg.Debug("Problem generating cache key for %s: %s", remote, err)
   259  		return
   260  	}
   261  
   262  	local := filepath.Join(l, "src", key)
   263  	repo, err := vcs.NewRepo(remote, local)
   264  	if err != nil {
   265  		msg.Debug("Problem getting repo instance: %s", err)
   266  		return
   267  	}
   268  
   269  	var useLocal bool
   270  	if _, err = os.Stat(local); err == nil {
   271  		useLocal = true
   272  	}
   273  
   274  	// Git endpoints allow for querying without fetching the codebase locally.
   275  	// We try that first to avoid fetching right away. Is this premature
   276  	// optimization?
   277  	cc := true
   278  	if !useLocal && repo.Vcs() == vcs.Git {
   279  		out, err2 := exec.Command("git", "ls-remote", remote).CombinedOutput()
   280  		if err2 == nil {
   281  			cache.MemTouch(remote)
   282  			cc = false
   283  			lines := strings.Split(string(out), "\n")
   284  			for _, i := range lines {
   285  				ti := strings.TrimSpace(i)
   286  				if found := createGitParseVersion.FindString(ti); found != "" {
   287  					tg := strings.TrimPrefix(strings.TrimSuffix(found, "^{}"), "tags/")
   288  					cache.MemPut(remote, tg)
   289  					if d.Reference != "" && strings.HasPrefix(ti, d.Reference) {
   290  						cache.MemSetCurrent(remote, tg)
   291  					}
   292  				}
   293  			}
   294  		}
   295  	}
   296  
   297  	if cc {
   298  		cache.Lock(key)
   299  		cache.MemTouch(remote)
   300  		if _, err = os.Stat(local); os.IsNotExist(err) {
   301  			repo.Get()
   302  			branch := findCurrentBranch(repo)
   303  			c := cache.RepoInfo{DefaultBranch: branch}
   304  			err = cache.SaveRepoData(key, c)
   305  			if err != nil {
   306  				msg.Debug("Error saving cache repo details: %s", err)
   307  			}
   308  		} else {
   309  			repo.Update()
   310  		}
   311  		tgs, err := repo.Tags()
   312  		if err != nil {
   313  			msg.Debug("Problem getting tags: %s", err)
   314  		} else {
   315  			for _, v := range tgs {
   316  				cache.MemPut(remote, v)
   317  			}
   318  		}
   319  		if d.Reference != "" && repo.IsReference(d.Reference) {
   320  			tgs, err = repo.TagsFromCommit(d.Reference)
   321  			if err != nil {
   322  				msg.Debug("Problem getting tags for commit: %s", err)
   323  			} else {
   324  				if len(tgs) > 0 {
   325  					for _, v := range tgs {
   326  						if !(repo.Vcs() == vcs.Hg && v == "tip") {
   327  							cache.MemSetCurrent(remote, v)
   328  						}
   329  					}
   330  				}
   331  			}
   332  		}
   333  		cache.Unlock(key)
   334  	}
   335  }
   336  
   337  func findCurrentBranch(repo vcs.Repo) string {
   338  	msg.Debug("Attempting to find current branch for %s", repo.Remote())
   339  	// Svn and Bzr don't have default branches.
   340  	if repo.Vcs() == vcs.Svn || repo.Vcs() == vcs.Bzr {
   341  		return ""
   342  	}
   343  
   344  	if repo.Vcs() == vcs.Git || repo.Vcs() == vcs.Hg {
   345  		ver, err := repo.Current()
   346  		if err != nil {
   347  			msg.Debug("Unable to find current branch for %s, error: %s", repo.Remote(), err)
   348  			return ""
   349  		}
   350  		return ver
   351  	}
   352  
   353  	return ""
   354  }