|  | package action | 
|  |  | 
|  | import ( | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "regexp" | 
|  | "sort" | 
|  | "strings" | 
|  | "sync" | 
|  |  | 
|  | "github.com/Masterminds/glide/cache" | 
|  | "github.com/Masterminds/glide/cfg" | 
|  | "github.com/Masterminds/glide/dependency" | 
|  | "github.com/Masterminds/glide/gb" | 
|  | "github.com/Masterminds/glide/godep" | 
|  | "github.com/Masterminds/glide/gom" | 
|  | "github.com/Masterminds/glide/gpm" | 
|  | "github.com/Masterminds/glide/msg" | 
|  | gpath "github.com/Masterminds/glide/path" | 
|  | "github.com/Masterminds/glide/util" | 
|  | "github.com/Masterminds/semver" | 
|  | "github.com/Masterminds/vcs" | 
|  | ) | 
|  |  | 
|  | // TODO(mattfarina): This file could really use to be cleaned up. It's functional | 
|  | // but not all that elegant. This mess of code really needs someone to come | 
|  | // along and clean it up. Pretty please. | 
|  |  | 
|  | // Create creates/initializes a new Glide repository. | 
|  | // | 
|  | // This will fail if a glide.yaml already exists. | 
|  | // | 
|  | // By default, this will scan the present source code directory for dependencies. | 
|  | // | 
|  | // If skipImport is set to true, this will not attempt to import from an existing | 
|  | // GPM, Godep, or GB project if one should exist. However, it will still attempt | 
|  | // to read the local source to determine required packages. | 
|  | func Create(base string, skipImport, noInteract, skipVerSug bool) { | 
|  | glidefile := gpath.GlideFile | 
|  | // Guard against overwrites. | 
|  | guardYAML(glidefile) | 
|  |  | 
|  | // Guess deps | 
|  | conf := guessDeps(base, skipImport, noInteract, skipVerSug) | 
|  | // Write YAML | 
|  | msg.Info("Writing glide.yaml file") | 
|  | if err := conf.WriteFile(glidefile); err != nil { | 
|  | msg.Die("Could not save %s: %s", glidefile, err) | 
|  | } | 
|  | msg.Info("You can now edit the glide.yaml file. Consider:") | 
|  | msg.Info("--> Using versions and ranges. See https://glide.sh/docs/versions/") | 
|  | msg.Info("--> Adding additional metadata. See https://glide.sh/docs/glide.yaml/") | 
|  | } | 
|  |  | 
|  | // guardYAML fails if the given file already exists. | 
|  | // | 
|  | // This prevents an important file from being overwritten. | 
|  | func guardYAML(filename string) { | 
|  | if _, err := os.Stat(filename); err == nil { | 
|  | msg.Die("Cowardly refusing to overwrite existing YAML.") | 
|  | } | 
|  | } | 
|  |  | 
|  | // guessDeps attempts to resolve all of the dependencies for a given project. | 
|  | // | 
|  | // base is the directory to start with. | 
|  | // skipImport will skip running the automatic imports. | 
|  | // | 
|  | // FIXME: This function is likely a one-off that has a more standard alternative. | 
|  | // It's also long and could use a refactor. | 
|  | func guessDeps(base string, skipImport, noInteract, skipVerSug bool) *cfg.Config { | 
|  | buildContext, err := util.GetBuildContext() | 
|  | if err != nil { | 
|  | msg.Die("Failed to build an import context: %s", err) | 
|  | } | 
|  | name := buildContext.PackageName(base) | 
|  |  | 
|  | msg.Info("Generating a YAML configuration file and guessing the dependencies") | 
|  |  | 
|  | config := new(cfg.Config) | 
|  |  | 
|  | // Get the name of the top level package | 
|  | config.Name = name | 
|  |  | 
|  | // Import by looking at other package managers and looking over the | 
|  | // entire directory structure. | 
|  | var deps cfg.Dependencies | 
|  |  | 
|  | // Attempt to import from other package managers. | 
|  | if !skipImport { | 
|  | deps = guessImportDeps(base) | 
|  | if len(deps) == 0 { | 
|  | msg.Info("No dependencies found to import") | 
|  | } | 
|  | } | 
|  |  | 
|  | msg.Info("Scanning code to look for dependencies") | 
|  |  | 
|  | // Resolve dependencies by looking at the tree. | 
|  | r, err := dependency.NewResolver(base) | 
|  | if err != nil { | 
|  | msg.Die("Error creating a dependency resolver: %s", err) | 
|  | } | 
|  |  | 
|  | h := &dependency.DefaultMissingPackageHandler{Missing: []string{}, Gopath: []string{}} | 
|  | r.Handler = h | 
|  |  | 
|  | sortable, err := r.ResolveLocal(false) | 
|  | if err != nil { | 
|  | msg.Die("Error resolving local dependencies: %s", err) | 
|  | } | 
|  |  | 
|  | sort.Strings(sortable) | 
|  |  | 
|  | vpath := r.VendorDir | 
|  | if !strings.HasSuffix(vpath, "/") { | 
|  | vpath = vpath + string(os.PathSeparator) | 
|  | } | 
|  |  | 
|  | var count int | 
|  | var all string | 
|  | var allOnce bool | 
|  | for _, pa := range sortable { | 
|  | n := strings.TrimPrefix(pa, vpath) | 
|  | root, subpkg := util.NormalizeName(n) | 
|  |  | 
|  | if !config.HasDependency(root) && root != config.Name { | 
|  | count++ | 
|  | d := deps.Get(root) | 
|  | if d == nil { | 
|  | d = &cfg.Dependency{ | 
|  | Name: root, | 
|  | } | 
|  | msg.Info("--> Found reference to %s", n) | 
|  | } else { | 
|  | msg.Info("--> Found imported reference to %s", n) | 
|  | } | 
|  |  | 
|  | all, allOnce = guessAskVersion(noInteract, all, allOnce, skipVerSug, d) | 
|  |  | 
|  | if subpkg != "" { | 
|  | if !d.HasSubpackage(subpkg) { | 
|  | d.Subpackages = append(d.Subpackages, subpkg) | 
|  | } | 
|  | msg.Verbose("--> Noting sub-package %s to %s", subpkg, root) | 
|  | } | 
|  |  | 
|  | config.Imports = append(config.Imports, d) | 
|  | } else if config.HasDependency(root) { | 
|  | if len(subpkg) > 0 { | 
|  | subpkg = strings.TrimPrefix(subpkg, "/") | 
|  | d := config.Imports.Get(root) | 
|  | if !d.HasSubpackage(subpkg) { | 
|  | d.Subpackages = append(d.Subpackages, subpkg) | 
|  | } | 
|  | msg.Verbose("--> Noting sub-package %s to %s", subpkg, root) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if !skipImport && len(deps) > count { | 
|  | var res string | 
|  | if noInteract { | 
|  | res = "y" | 
|  | } else { | 
|  | msg.Info("%d unused imported dependencies found. These are likely transitive dependencies ", len(deps)-count) | 
|  | msg.Info("(dependencies of your dependencies). Would you like to track them in your") | 
|  | msg.Info("glide.yaml file? Note, Glide will automatically scan your codebase to detect") | 
|  | msg.Info("the complete dependency tree and import the complete tree. If your dependencies") | 
|  | msg.Info("do not track dependency version information some version information may be lost.") | 
|  | msg.Info("Yes (Y) or No (N)?") | 
|  | res, err = msg.PromptUntil([]string{"y", "yes", "n", "no"}) | 
|  | if err != nil { | 
|  | msg.Die("Error processing response: %s", err) | 
|  | } | 
|  | } | 
|  | if res == "y" || res == "yes" { | 
|  | msg.Info("Including additional imports in the glide.yaml file") | 
|  | for _, dep := range deps { | 
|  | found := config.Imports.Get(dep.Name) | 
|  | if found == nil { | 
|  | config.Imports = append(config.Imports, dep) | 
|  | if dep.Reference != "" { | 
|  | all, allOnce = guessAskVersion(noInteract, all, allOnce, skipVerSug, dep) | 
|  | msg.Info("--> Adding %s at version %s", dep.Name, dep.Reference) | 
|  | } else { | 
|  | msg.Info("--> Adding %s", dep.Name) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return config | 
|  | } | 
|  |  | 
|  | func guessAskVersion(noInteract bool, all string, allonce, skipVerSug bool, d *cfg.Dependency) (string, bool) { | 
|  | if !noInteract && !skipVerSug && d.Reference == "" { | 
|  | // If scanning has not happened already, for example this isn't an | 
|  | // import, try it now. | 
|  | var loc string | 
|  | if d.Repository != "" { | 
|  | loc = d.Repository | 
|  | } else { | 
|  | loc = "https://" + d.Name | 
|  | } | 
|  | if !guessVersionCache.checked(loc) { | 
|  | createGuessVersion(loc, d.Reference) | 
|  | } | 
|  |  | 
|  | semv := guessVersionCache.get(loc) | 
|  | if semv != "" { | 
|  | msg.Info("The package %s appears to have Semantic Version releases. The latest", d.Name) | 
|  | msg.Info("release is %s. Would you like to use this release? Yes (Y) or No (N)", semv) | 
|  | res, err2 := msg.PromptUntil([]string{"y", "yes", "n", "no"}) | 
|  | if err2 != nil { | 
|  | msg.Die("Error processing response: %s", err2) | 
|  | } | 
|  |  | 
|  | if res == "y" || res == "yes" { | 
|  | d.Reference = semv | 
|  | } | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | if !noInteract && d.Reference != "" { | 
|  | var changedVer bool | 
|  | ver, err := semver.NewVersion(d.Reference) | 
|  | if err != nil && !skipVerSug { | 
|  | var loc string | 
|  | if d.Repository != "" { | 
|  | loc = d.Repository | 
|  | } else { | 
|  | loc = "https://" + d.Name | 
|  | } | 
|  |  | 
|  | semv := guessVersionCache.get(loc) | 
|  | if semv != "" { | 
|  | msg.Info("The package %s appears to have Semantic Version releases but the imported data", d.Name) | 
|  | msg.Info("is not using them. The latest release is %s but the imported data is using %s.", semv, d.Reference) | 
|  | msg.Info("Would you like to use the latest release version instead? Yes (Y) or No (N)") | 
|  | res, err2 := msg.PromptUntil([]string{"y", "yes", "n", "no"}) | 
|  | if err2 != nil { | 
|  | msg.Die("Error processing response: %s", err2) | 
|  | } | 
|  |  | 
|  | if res == "y" || res == "yes" { | 
|  | d.Reference = semv | 
|  | changedVer = true | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if changedVer { | 
|  | ver, err = semver.NewVersion(d.Reference) | 
|  | } | 
|  | if err == nil { | 
|  | if all == "" { | 
|  | vstr := ver.String() | 
|  | msg.Info("Imported dependency %s (%s) appears to use semantic versions (http://semver.org).", d.Name, d.Reference) | 
|  | msg.Info("Would you like Glide to track the latest minor or patch releases (major.minor.path)?") | 
|  | msg.Info("Tracking minor version releases would use '>= %s, < %d.0.0' ('^%s'). Tracking patch version", vstr, ver.Major()+1, vstr) | 
|  | msg.Info("releases would use '>= %s, < %d.%d.0' ('~%s'). For more information on Glide versions", vstr, ver.Major(), ver.Minor()+1, vstr) | 
|  | msg.Info("and ranges see https://glide.sh/docs/versions") | 
|  | msg.Info("Minor (M), Patch (P), or Skip Ranges (S)?") | 
|  | res, err := msg.PromptUntil([]string{"minor", "m", "patch", "p", "skip ranges", "s"}) | 
|  | if err != nil { | 
|  | msg.Die("Error processing response: %s", err) | 
|  | } | 
|  | if res == "m" || res == "minor" { | 
|  | d.Reference = "^" + vstr | 
|  | } else if res == "p" || res == "patch" { | 
|  | d.Reference = "~" + vstr | 
|  | } | 
|  |  | 
|  | if !allonce { | 
|  | msg.Info("Would you like to same response (%s) for future dependencies? Yes (Y) or No (N)", res) | 
|  | res2, err := msg.PromptUntil([]string{"y", "yes", "n", "no"}) | 
|  | if err != nil { | 
|  | msg.Die("Error processing response: %s", err) | 
|  | } | 
|  | if res2 == "yes" || res2 == "y" { | 
|  | return res, true | 
|  | } | 
|  |  | 
|  | return "", true | 
|  | } | 
|  |  | 
|  | } else { | 
|  | if all == "m" || all == "minor" { | 
|  | d.Reference = "^" + ver.String() | 
|  | } else if all == "p" || all == "patch" { | 
|  | d.Reference = "~" + ver.String() | 
|  | } | 
|  | } | 
|  |  | 
|  | return all, allonce | 
|  | } | 
|  |  | 
|  | return all, allonce | 
|  | } | 
|  |  | 
|  | return all, allonce | 
|  | } | 
|  |  | 
|  | func guessImportDeps(base string) cfg.Dependencies { | 
|  | msg.Info("Attempting to import from other package managers (use --skip-import to skip)") | 
|  | deps := []*cfg.Dependency{} | 
|  | absBase, err := filepath.Abs(base) | 
|  | if err != nil { | 
|  | msg.Die("Failed to resolve location of %s: %s", base, err) | 
|  | } | 
|  |  | 
|  | if d, ok := guessImportGodep(absBase); ok { | 
|  | msg.Info("Importing Godep configuration") | 
|  | msg.Warn("--> Godep uses commit id versions. Consider using Semantic Versions with Glide") | 
|  | deps = d | 
|  | } else if d, ok := guessImportGPM(absBase); ok { | 
|  | msg.Info("Importing GPM configuration") | 
|  | deps = d | 
|  | } else if d, ok := guessImportGB(absBase); ok { | 
|  | msg.Info("Importing GB configuration") | 
|  | deps = d | 
|  | } else if d, ok := guessImportGom(absBase); ok { | 
|  | msg.Info("Importing GB configuration") | 
|  | deps = d | 
|  | } | 
|  |  | 
|  | if len(deps) > 0 { | 
|  | msg.Info("--> Attempting to detect versions from imported commit ids") | 
|  | } | 
|  |  | 
|  | var wg sync.WaitGroup | 
|  |  | 
|  | for _, i := range deps { | 
|  | wg.Add(1) | 
|  | go func(dep *cfg.Dependency) { | 
|  | var remote string | 
|  | if dep.Repository != "" { | 
|  | remote = dep.Repository | 
|  | } else { | 
|  | remote = "https://" + dep.Name | 
|  | } | 
|  | ver := createGuessVersion(remote, dep.Reference) | 
|  | if ver != dep.Reference { | 
|  | msg.Verbose("--> Found imported reference to %s at version %s", dep.Name, ver) | 
|  | dep.Reference = ver | 
|  | } | 
|  |  | 
|  | msg.Debug("--> Found imported reference to %s at revision %s", dep.Name, dep.Reference) | 
|  |  | 
|  | wg.Done() | 
|  | }(i) | 
|  | } | 
|  |  | 
|  | wg.Wait() | 
|  |  | 
|  | return deps | 
|  | } | 
|  |  | 
|  | func guessImportGodep(dir string) ([]*cfg.Dependency, bool) { | 
|  | d, err := godep.Parse(dir) | 
|  | if err != nil || len(d) == 0 { | 
|  | return []*cfg.Dependency{}, false | 
|  | } | 
|  |  | 
|  | return d, true | 
|  | } | 
|  |  | 
|  | func guessImportGPM(dir string) ([]*cfg.Dependency, bool) { | 
|  | d, err := gpm.Parse(dir) | 
|  | if err != nil || len(d) == 0 { | 
|  | return []*cfg.Dependency{}, false | 
|  | } | 
|  |  | 
|  | return d, true | 
|  | } | 
|  |  | 
|  | func guessImportGB(dir string) ([]*cfg.Dependency, bool) { | 
|  | d, err := gb.Parse(dir) | 
|  | if err != nil || len(d) == 0 { | 
|  | return []*cfg.Dependency{}, false | 
|  | } | 
|  |  | 
|  | return d, true | 
|  | } | 
|  |  | 
|  | func guessImportGom(dir string) ([]*cfg.Dependency, bool) { | 
|  | d, err := gom.Parse(dir) | 
|  | if err != nil || len(d) == 0 { | 
|  | return []*cfg.Dependency{}, false | 
|  | } | 
|  |  | 
|  | return d, true | 
|  | } | 
|  |  | 
|  | // Note, this really needs a simpler name. | 
|  | var createGitParseVersion = regexp.MustCompile(`(?m-s)(?:tags)/(\S+)$`) | 
|  |  | 
|  | var guessVersionCache = newVersionCache() | 
|  |  | 
|  | func createGuessVersion(remote, id string) string { | 
|  | err := cache.Setup() | 
|  | if err != nil { | 
|  | msg.Debug("Problem setting up cache: %s", err) | 
|  | } | 
|  | l, err := cache.Location() | 
|  | if err != nil { | 
|  | msg.Debug("Problem detecting cache location: %s", err) | 
|  | } | 
|  | key, err := cache.Key(remote) | 
|  | if err != nil { | 
|  | msg.Debug("Problem generating cache key for %s: %s", remote, err) | 
|  | } | 
|  |  | 
|  | local := filepath.Join(l, "src", key) | 
|  | repo, err := vcs.NewRepo(remote, local) | 
|  | if err != nil { | 
|  | msg.Debug("Problem getting repo instance: %s", err) | 
|  | } | 
|  |  | 
|  | // Git endpoints allow for querying without fetching the codebase locally. | 
|  | // We try that first to avoid fetching right away. Is this premature | 
|  | // optimization? | 
|  | cc := true | 
|  | if repo.Vcs() == vcs.Git { | 
|  | out, err := exec.Command("git", "ls-remote", remote).CombinedOutput() | 
|  | if err == nil { | 
|  | guessVersionCache.touchChecked(remote) | 
|  | cc = false | 
|  | lines := strings.Split(string(out), "\n") | 
|  | res := "" | 
|  |  | 
|  | // TODO(mattfarina): Detect if the found version is semver and use | 
|  | // that one instead of the first found. | 
|  | for _, i := range lines { | 
|  | ti := strings.TrimSpace(i) | 
|  | if found := createGitParseVersion.FindString(ti); found != "" { | 
|  | tg := strings.TrimPrefix(strings.TrimSuffix(found, "^{}"), "tags/") | 
|  | if strings.HasPrefix(ti, id) { | 
|  | res = tg | 
|  | } | 
|  |  | 
|  | ver, err := semver.NewVersion(tg) | 
|  | if err == nil { | 
|  | if guessVersionCache.get(remote) == "" { | 
|  | guessVersionCache.put(remote, tg) | 
|  | } else { | 
|  | ver2, err := semver.NewVersion(guessVersionCache.get(remote)) | 
|  | if err == nil { | 
|  | if ver.GreaterThan(ver2) { | 
|  | guessVersionCache.put(remote, tg) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if res != "" { | 
|  | return res | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if cc { | 
|  | cache.Lock(key) | 
|  | if _, err = os.Stat(local); os.IsNotExist(err) { | 
|  | repo.Get() | 
|  | branch := findCurrentBranch(repo) | 
|  | c := cache.RepoInfo{DefaultBranch: branch} | 
|  | err = cache.SaveRepoData(key, c) | 
|  | if err != nil { | 
|  | msg.Debug("Error saving cache repo details: %s", err) | 
|  | } | 
|  | } else { | 
|  | repo.Update() | 
|  | } | 
|  |  | 
|  | tgs, err := repo.TagsFromCommit(id) | 
|  | if err != nil { | 
|  | msg.Debug("Problem getting tags for commit: %s", err) | 
|  | } | 
|  | cache.Unlock(key) | 
|  | if len(tgs) > 0 { | 
|  | return tgs[0] | 
|  | } | 
|  | } | 
|  |  | 
|  | return id | 
|  | } | 
|  |  | 
|  | func findCurrentBranch(repo vcs.Repo) string { | 
|  | msg.Debug("Attempting to find current branch for %s", repo.Remote()) | 
|  | // Svn and Bzr don't have default branches. | 
|  | if repo.Vcs() == vcs.Svn || repo.Vcs() == vcs.Bzr { | 
|  | return "" | 
|  | } | 
|  |  | 
|  | if repo.Vcs() == vcs.Git || repo.Vcs() == vcs.Hg { | 
|  | ver, err := repo.Current() | 
|  | if err != nil { | 
|  | msg.Debug("Unable to find current branch for %s, error: %s", repo.Remote(), err) | 
|  | return "" | 
|  | } | 
|  | return ver | 
|  | } | 
|  |  | 
|  | return "" | 
|  | } | 
|  |  | 
|  | type versionCache struct { | 
|  | sync.RWMutex | 
|  | cache map[string]string | 
|  | cd    map[string]bool | 
|  | } | 
|  |  | 
|  | func newVersionCache() *versionCache { | 
|  | return &versionCache{ | 
|  | cache: make(map[string]string), | 
|  | cd:    make(map[string]bool), | 
|  | } | 
|  | } | 
|  |  | 
|  | func (v *versionCache) put(name, version string) { | 
|  | v.Lock() | 
|  | defer v.Unlock() | 
|  | v.cache[name] = version | 
|  | v.cd[name] = true | 
|  | } | 
|  |  | 
|  | func (v *versionCache) checked(name string) bool { | 
|  | v.RLock() | 
|  | defer v.RUnlock() | 
|  | return v.cd[name] | 
|  | } | 
|  |  | 
|  | func (v *versionCache) touchChecked(name string) { | 
|  | v.Lock() | 
|  | defer v.Unlock() | 
|  | v.cd[name] = true | 
|  | } | 
|  |  | 
|  | func (v *versionCache) get(name string) string { | 
|  | v.RLock() | 
|  | defer v.RUnlock() | 
|  | return v.cache[name] | 
|  | } |