action/config_wizard.go   F
last analyzed

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 85
eloc 236
dl 0
loc 356
rs 2
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A action.wizardOnce 0 2 1
A action.wizardSetOnce 0 4 1
A action.wizardRemember 0 4 1
A action.wizardAskCurrent 0 5 1
A action.wizardAskLatest 0 5 1
A action.wizardLookInto 0 9 2
B action.findCurrentBranch 0 17 6
A action.wizardSugOnce 0 5 2
F action.ConfigWizard 0 149 38
F action.wizardFindVersions 0 82 26
B action.wizardAskRange 0 22 6
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
}
358