github.com/MealCraft/glide@v0.13.4/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("The choices are:")
   208  	msg.Info(" - Tracking minor version releases would use '>= %s, < %d.0.0' ('^%s')", vstr, ver.Major()+1, vstr)
   209  	msg.Info(" - Tracking patch version releases would use '>= %s, < %d.%d.0' ('~%s')", vstr, ver.Major(), ver.Minor()+1, vstr)
   210  	msg.Info(" - Skip using ranges\n")
   211  	msg.Info("For more information on Glide versions and ranges see https://glide.sh/docs/versions")
   212  	msg.Info("Minor (M), Patch (P), or Skip Ranges (S)?")
   213  
   214  	res, err := msg.PromptUntil([]string{"minor", "m", "patch", "p", "skip ranges", "s"})
   215  	if err != nil {
   216  		msg.Die("Error processing response: %s", err)
   217  	}
   218  	if res == "m" || res == "minor" {
   219  		return "m"
   220  	} else if res == "p" || res == "patch" {
   221  		return "p"
   222  	}
   223  
   224  	return "s"
   225  }
   226  
   227  func wizardAskCurrent(cur string, d *cfg.Dependency) bool {
   228  	msg.Info("The package %s is currently set to use the version %s.", d.Name, d.Reference)
   229  	msg.Info("There is an equivalent semantic version (http://semver.org) release of %s. Would", cur)
   230  	msg.Info("you like to use that instead? Yes (Y) or No (N)")
   231  	return msg.PromptUntilYorN()
   232  }
   233  
   234  func wizardAskLatest(latest string, d *cfg.Dependency) bool {
   235  	msg.Info("The package %s appears to have Semantic Version releases (http://semver.org). ", d.Name)
   236  	msg.Info("The latest release is %s. You are currently not using a release. Would you like", latest)
   237  	msg.Info("to use this release? Yes (Y) or No (N)")
   238  	return msg.PromptUntilYorN()
   239  }
   240  
   241  func wizardLookInto(d *cfg.Dependency) bool {
   242  	_, err := semver.NewConstraint(d.Reference)
   243  
   244  	// The existing version is already a valid semver constraint so we skip suggestions.
   245  	if err == nil {
   246  		return false
   247  	}
   248  
   249  	return true
   250  }
   251  
   252  // Note, this really needs a simpler name.
   253  var createGitParseVersion = regexp.MustCompile(`(?m-s)(?:tags)/(\S+)$`)
   254  
   255  func wizardFindVersions(d *cfg.Dependency) {
   256  	l := cache.Location()
   257  	remote := d.Remote()
   258  
   259  	key, err := cache.Key(remote)
   260  	if err != nil {
   261  		msg.Debug("Problem generating cache key for %s: %s", remote, err)
   262  		return
   263  	}
   264  
   265  	local := filepath.Join(l, "src", key)
   266  	repo, err := vcs.NewRepo(remote, local)
   267  	if err != nil {
   268  		msg.Debug("Problem getting repo instance: %s", err)
   269  		return
   270  	}
   271  
   272  	var useLocal bool
   273  	if _, err = os.Stat(local); err == nil {
   274  		useLocal = true
   275  	}
   276  
   277  	// Git endpoints allow for querying without fetching the codebase locally.
   278  	// We try that first to avoid fetching right away. Is this premature
   279  	// optimization?
   280  	cc := true
   281  	if !useLocal && repo.Vcs() == vcs.Git {
   282  		out, err2 := exec.Command("git", "ls-remote", remote).CombinedOutput()
   283  		if err2 == nil {
   284  			cache.MemTouch(remote)
   285  			cc = false
   286  			lines := strings.Split(string(out), "\n")
   287  			for _, i := range lines {
   288  				ti := strings.TrimSpace(i)
   289  				if found := createGitParseVersion.FindString(ti); found != "" {
   290  					tg := strings.TrimPrefix(strings.TrimSuffix(found, "^{}"), "tags/")
   291  					cache.MemPut(remote, tg)
   292  					if d.Reference != "" && strings.HasPrefix(ti, d.Reference) {
   293  						cache.MemSetCurrent(remote, tg)
   294  					}
   295  				}
   296  			}
   297  		}
   298  	}
   299  
   300  	if cc {
   301  		cache.Lock(key)
   302  		cache.MemTouch(remote)
   303  		if _, err = os.Stat(local); os.IsNotExist(err) {
   304  			repo.Get()
   305  			branch := findCurrentBranch(repo)
   306  			c := cache.RepoInfo{DefaultBranch: branch}
   307  			err = cache.SaveRepoData(key, c)
   308  			if err != nil {
   309  				msg.Debug("Error saving cache repo details: %s", err)
   310  			}
   311  		} else {
   312  			repo.Update()
   313  		}
   314  		tgs, err := repo.Tags()
   315  		if err != nil {
   316  			msg.Debug("Problem getting tags: %s", err)
   317  		} else {
   318  			for _, v := range tgs {
   319  				cache.MemPut(remote, v)
   320  			}
   321  		}
   322  		if d.Reference != "" && repo.IsReference(d.Reference) {
   323  			tgs, err = repo.TagsFromCommit(d.Reference)
   324  			if err != nil {
   325  				msg.Debug("Problem getting tags for commit: %s", err)
   326  			} else {
   327  				if len(tgs) > 0 {
   328  					for _, v := range tgs {
   329  						if !(repo.Vcs() == vcs.Hg && v == "tip") {
   330  							cache.MemSetCurrent(remote, v)
   331  						}
   332  					}
   333  				}
   334  			}
   335  		}
   336  		cache.Unlock(key)
   337  	}
   338  }
   339  
   340  func findCurrentBranch(repo vcs.Repo) string {
   341  	msg.Debug("Attempting to find current branch for %s", repo.Remote())
   342  	// Svn and Bzr don't have default branches.
   343  	if repo.Vcs() == vcs.Svn || repo.Vcs() == vcs.Bzr {
   344  		return ""
   345  	}
   346  
   347  	if repo.Vcs() == vcs.Git || repo.Vcs() == vcs.Hg {
   348  		ver, err := repo.Current()
   349  		if err != nil {
   350  			msg.Debug("Unable to find current branch for %s, error: %s", repo.Remote(), err)
   351  			return ""
   352  		}
   353  		return ver
   354  	}
   355  
   356  	return ""
   357  }