github.com/cedriclam/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 }