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