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 }