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 }