blob: 028d8d8adc24f71bd58a3eda3d4bd50239c27e9f [file] [log] [blame]
package cmd
import (
"encoding/xml"
"fmt"
"sort"
//"log"
"io"
"net/http"
"net/url"
"os"
"path"
"regexp"
"runtime"
"strings"
"github.com/Masterminds/cookoo"
"github.com/Masterminds/semver"
v "github.com/Masterminds/vcs"
)
func init() {
// Precompile the regular expressions used to check VCS locations.
for _, v := range vcsList {
v.regex = regexp.MustCompile(v.pattern)
}
// Uncomment the line below and the log import to see the output
// from the vcs commands executed for each project.
//v.Logger = log.New(os.Stdout, "go-vcs", log.LstdFlags)
}
// GetAll gets zero or more repos.
//
// This takes a package name, normalizes it, finds the repo, and installs it.
// It's the workhorse behind `glide get`.
//
// Params:
// - packages ([]string): Package names to get.
// - verbose (bool): default false
//
// Returns:
// - []*Dependency: A list of constructed dependencies.
func GetAll(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
names := p.Get("packages", []string{}).([]string)
cfg := p.Get("conf", nil).(*Config)
insecure := p.Get("insecure", false).(bool)
Info("Preparing to install %d package.", len(names))
deps := []*Dependency{}
for _, name := range names {
cwd, err := VendorPath(c)
if err != nil {
return nil, err
}
root := getRepoRootFromPackage(name)
if len(root) == 0 {
return nil, fmt.Errorf("Package name is required for %q.", name)
}
if cfg.HasDependency(root) {
Warn("Package %q is already in glide.yaml. Skipping", root)
continue
}
dest := path.Join(cwd, root)
var repoURL string
if insecure {
repoURL = "http://" + root
} else {
repoURL = "https://" + root
}
repo, err := v.NewRepo(repoURL, dest)
if err != nil {
Error("Could not construct repo for %q: %s", name, err)
return false, err
}
dep := &Dependency{
Name: root,
}
// When retriving from an insecure location set the repo to the
// insecure location.
if insecure {
dep.Repository = "http://" + root
}
subpkg := strings.TrimPrefix(name, root)
if len(subpkg) > 0 && subpkg != "/" {
dep.Subpackages = []string{subpkg}
}
if err := repo.Get(); err != nil {
return dep, err
}
cfg.Imports = append(cfg.Imports, dep)
deps = append(deps, dep)
}
return deps, nil
}
// GetImports iterates over the imported packages and gets them.
func GetImports(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
cfg := p.Get("conf", nil).(*Config)
cwd, err := VendorPath(c)
if err != nil {
Error("Failed to prepare vendor directory: %s", err)
return false, err
}
if len(cfg.Imports) == 0 {
Info("No dependencies found. Nothing downloaded.\n")
return false, nil
}
for _, dep := range cfg.Imports {
if err := VcsGet(dep, cwd); err != nil {
Warn("Skipped getting %s: %v\n", dep.Name, err)
}
}
return true, nil
}
// UpdateImports iterates over the imported packages and updates them.
//
// Params:
//
// - force (bool): force packages to update (default false)
// - conf (*Config): The configuration
// - packages([]string): The packages to update. Default is all.
func UpdateImports(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
cfg := p.Get("conf", nil).(*Config)
force := p.Get("force", true).(bool)
plist := p.Get("packages", []string{}).([]string)
pkgs := list2map(plist)
restrict := len(pkgs) > 0
cwd, err := VendorPath(c)
if err != nil {
return false, err
}
if len(cfg.Imports) == 0 {
Info("No dependencies found. Nothing updated.\n")
return false, nil
}
for _, dep := range cfg.Imports {
if restrict && !pkgs[dep.Name] {
Debug("===> Skipping %q", dep.Name)
continue
}
// Hack: The updateCache global keeps us from re-updating the same
// dependencies when we're recursing. We cache here to prevent
// flattening from causing unnecessary updates.
updateCache[dep.Name] = true
if err := VcsUpdate(dep, cwd, force); err != nil {
Warn("Update failed for %s: %s\n", dep.Name, err)
}
}
return true, nil
}
// SetReference is a command to set the VCS reference (commit id, tag, etc) for
// a project.
func SetReference(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
cfg := p.Get("conf", nil).(*Config)
cwd, err := VendorPath(c)
if err != nil {
return false, err
}
if len(cfg.Imports) == 0 {
Info("No references set.\n")
return false, nil
}
for _, dep := range cfg.Imports {
if err := VcsVersion(dep, cwd); err != nil {
Warn("Failed to set version on %s to %s: %s\n", dep.Name, dep.Reference, err)
}
}
return true, nil
}
// filterArchOs indicates a dependency should be filtered out because it is
// the wrong GOOS or GOARCH.
func filterArchOs(dep *Dependency) bool {
found := false
if len(dep.Arch) > 0 {
for _, a := range dep.Arch {
if a == runtime.GOARCH {
found = true
}
}
// If it's not found, it should be filtered out.
if !found {
return true
}
}
found = false
if len(dep.Os) > 0 {
for _, o := range dep.Os {
if o == runtime.GOOS {
found = true
}
}
if !found {
return true
}
}
return false
}
// VcsExists checks if the directory has a local VCS checkout.
func VcsExists(dep *Dependency, dest string) bool {
repo, err := dep.GetRepo(dest)
if err != nil {
return false
}
return repo.CheckLocal()
}
// VcsGet figures out how to fetch a dependency, and then gets it.
//
// VcsGet installs into the dest.
func VcsGet(dep *Dependency, dest string) error {
repo, err := dep.GetRepo(dest)
if err != nil {
return err
}
return repo.Get()
}
// VcsUpdate updates to a particular checkout based on the VCS setting.
func VcsUpdate(dep *Dependency, vend string, force bool) error {
Info("Fetching updates for %s.\n", dep.Name)
if filterArchOs(dep) {
Info("%s is not used for %s/%s.\n", dep.Name, runtime.GOOS, runtime.GOARCH)
return nil
}
dest := path.Join(vend, dep.Name)
// If destination doesn't exist we need to perform an initial checkout.
if _, err := os.Stat(dest); os.IsNotExist(err) {
if err = VcsGet(dep, dest); err != nil {
Warn("Unable to checkout %s\n", dep.Name)
return err
}
} else {
// At this point we have a directory for the package.
// When the directory is not empty and has no VCS directory it's
// a vendored files situation.
empty, err := isDirectoryEmpty(dest)
if err != nil {
return err
}
_, err = v.DetectVcsFromFS(dest)
if empty == false && err == v.ErrCannotDetectVCS {
Warn("%s appears to be a vendored package. Unable to update. Consider the '--update-vendored' flag.\n", dep.Name)
} else {
repo, err := dep.GetRepo(dest)
// Tried to checkout a repo to a path that does not work. Either the
// type or endpoint has changed. Force is being passed in so the old
// location can be removed and replaced with the new one.
// Warning, any changes in the old location will be deleted.
// TODO: Put dirty checking in on the existing local checkout.
if (err == v.ErrWrongVCS || err == v.ErrWrongRemote) && force == true {
var newRemote string
if len(dep.Repository) > 0 {
newRemote = dep.Repository
} else {
newRemote = "https://" + dep.Name
}
Warn("Replacing %s with contents from %s\n", dep.Name, newRemote)
rerr := os.RemoveAll(dest)
if rerr != nil {
return rerr
}
if err = VcsGet(dep, dest); err != nil {
Warn("Unable to checkout %s\n", dep.Name)
return err
}
} else if err != nil {
return err
} else {
// Check if the current version is a tag or commit id. If it is
// and that version is already checked out we can skip updating
// which is faster than going out to the Internet to perform
// an update.
if dep.Reference != "" {
version, err := repo.Version()
if err != nil {
return err
}
ib, err := isBranch(dep.Reference, repo)
if err != nil {
return err
}
// If the current version equals the ref and it's not a
// branch it's a tag or commit id so we can skip
// performing an update.
if version == dep.Reference && !ib {
Info("%s is already set to version %s. Skipping update.", dep.Name, dep.Reference)
return nil
}
}
if err := repo.Update(); err != nil {
Warn("Download failed.\n")
return err
}
}
}
}
return nil
}
// VcsVersion set the VCS version for a checkout.
func VcsVersion(dep *Dependency, vend string) error {
// If there is no refernece configured there is nothing to set.
if dep.Reference == "" {
return nil
}
cwd := path.Join(vend, dep.Name)
// When the directory is not empty and has no VCS directory it's
// a vendored files situation.
empty, err := isDirectoryEmpty(cwd)
if err != nil {
return err
}
_, err = v.DetectVcsFromFS(cwd)
if empty == false && err == v.ErrCannotDetectVCS {
Warn("%s appears to be a vendored package. Unable to set new version. Consider the '--update-vendored' flag.\n", dep.Name)
} else {
repo, err := dep.GetRepo(cwd)
if err != nil {
return err
}
ver := dep.Reference
// Referenes in Git can begin with a ^ which is similar to semver.
// If there is a ^ prefix we assume it's a semver constraint rather than
// part of the git/VCS commit id.
if repo.IsReference(ver) && !strings.HasPrefix(ver, "^") {
Info("Setting version for %s to %s.\n", dep.Name, ver)
} else {
// Create the constraing first to make sure it's valid before
// working on the repo.
constraint, err := semver.NewConstraint(ver)
// Make sure the constriant is valid. At this point it's not a valid
// reference so if it's not a valid constrint we can exit early.
if err != nil {
Warn("The reference '%s' is not valid\n", ver)
return err
}
// Get the tags and branches (in that order)
refs, err := getAllVcsRefs(repo)
if err != nil {
return err
}
// Convert and filter the list to semver.Version instances
semvers := getSemVers(refs)
// Sort semver list
sort.Sort(sort.Reverse(semver.Collection(semvers)))
found := false
for _, v := range semvers {
if constraint.Check(v) {
found = true
// If the constrint passes get the original reference
ver = v.Original()
break
}
}
if found {
Info("Detected semantic version. Setting version for %s to %s.\n", dep.Name, ver)
} else {
Warn("Unable to find semantic version for constraint %s %s\n", dep.Name, ver)
}
}
if err := repo.UpdateVersion(ver); err != nil {
Error("Failed to set version to %s: %s\n", dep.Reference, err)
return err
}
}
return nil
}
// VcsLastCommit gets the last commit ID from the given dependency.
func VcsLastCommit(dep *Dependency, vend string) (string, error) {
cwd := path.Join(vend, dep.Name)
repo, err := dep.GetRepo(cwd)
if err != nil {
return "", err
}
if repo.CheckLocal() == false {
return "", fmt.Errorf("%s is not a VCS repo\n", dep.Name)
}
version, err := repo.Version()
if err != nil {
return "", err
}
return version, nil
}
// From a package name find the root repo. For example,
// the package github.com/Masterminds/cookoo/io has a root repo
// at github.com/Masterminds/cookoo
func getRepoRootFromPackage(pkg string) string {
for _, v := range vcsList {
m := v.regex.FindStringSubmatch(pkg)
if m == nil {
continue
}
if m[1] != "" {
return m[1]
}
}
// There are cases where a package uses the special go get magic for
// redirects. If we've not discovered the location already try that.
pkg = getRepoRootFromGoGet(pkg)
return pkg
}
// Pages like https://golang.org/x/net provide an html document with
// meta tags containing a location to work with. The go tool uses
// a meta tag with the name go-import which is what we use here.
// godoc.org also has one call go-source that we do not need to use.
// The value of go-import is in the form "prefix vcs repo". The prefix
// should match the vcsURL and the repo is a location that can be
// checked out. Note, to get the html document you you need to add
// ?go-get=1 to the url.
func getRepoRootFromGoGet(pkg string) string {
vcsURL := "https://" + pkg
u, err := url.Parse(vcsURL)
if err != nil {
return pkg
}
if u.RawQuery == "" {
u.RawQuery = "go-get=1"
} else {
u.RawQuery = u.RawQuery + "+go-get=1"
}
checkURL := u.String()
resp, err := http.Get(checkURL)
if err != nil {
return pkg
}
defer resp.Body.Close()
nu, err := parseImportFromBody(u, resp.Body)
if err != nil {
return pkg
} else if nu == "" {
return pkg
}
return nu
}
func parseImportFromBody(ur *url.URL, r io.ReadCloser) (u string, err error) {
d := xml.NewDecoder(r)
d.CharsetReader = charsetReader
d.Strict = false
var t xml.Token
for {
t, err = d.Token()
if err != nil {
if err == io.EOF {
err = nil
}
return
}
if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
return
}
if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
return
}
e, ok := t.(xml.StartElement)
if !ok || !strings.EqualFold(e.Name.Local, "meta") {
continue
}
if attrValue(e.Attr, "name") != "go-import" {
continue
}
if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 {
// If this the second time a go-import statement has been detected
// return an error. There should only be one import statement per
// html file. We don't simply return the first found in order to
// detect pages including more than one.
if u != "" {
u = ""
err = v.ErrCannotDetectVCS
return
}
// If the prefix supplied by the remote system isn't a prefix to the
// url we're fetching return an error. This will work for exact
// matches and prefixes. For example, golang.org/x/net as a prefix
// will match for golang.org/x/net and golang.org/x/net/context.
vcsURL := ur.Host + ur.Path
if !strings.HasPrefix(vcsURL, f[0]) {
err = v.ErrCannotDetectVCS
return
}
u = f[0]
}
}
}
func charsetReader(charset string, input io.Reader) (io.Reader, error) {
switch strings.ToLower(charset) {
case "ascii":
return input, nil
default:
return nil, fmt.Errorf("can't decode XML document using charset %q", charset)
}
}
func attrValue(attrs []xml.Attr, name string) string {
for _, a := range attrs {
if strings.EqualFold(a.Name.Local, name) {
return a.Value
}
}
return ""
}
type vcsInfo struct {
host string
pattern string
regex *regexp.Regexp
}
var vcsList = []*vcsInfo{
{
host: "github.com",
pattern: `^(?P<rootpkg>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
},
{
host: "bitbucket.org",
pattern: `^(?P<rootpkg>bitbucket\.org/([A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
},
{
host: "launchpad.net",
pattern: `^(?P<rootpkg>launchpad\.net/(([A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)?|~[A-Za-z0-9_.\-]+/(\+junk|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
},
{
host: "git.launchpad.net",
pattern: `^(?P<rootpkg>git\.launchpad\.net/(([A-Za-z0-9_.\-]+)|~[A-Za-z0-9_.\-]+/(\+git|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))$`,
},
{
host: "go.googlesource.com",
pattern: `^(?P<rootpkg>go\.googlesource\.com/[A-Za-z0-9_.\-]+/?)$`,
},
// TODO: Once Google Code becomes fully deprecated this can be removed.
{
host: "code.google.com",
pattern: `^(?P<rootpkg>code\.google\.com/[pr]/([a-z0-9\-]+)(\.([a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`,
},
// Alternative Google setup for SVN. This is the previous structure but it still works... until Google Code goes away.
{
pattern: `^(?P<rootpkg>[a-z0-9_\-.]+\.googlecode\.com/svn(/.*)?)$`,
},
// Alternative Google setup. This is the previous structure but it still works... until Google Code goes away.
{
pattern: `^(?P<rootpkg>[a-z0-9_\-.]+\.googlecode\.com/(git|hg))(/.*)?$`,
},
// If none of the previous detect the type they will fall to this looking for the type in a generic sense
// by the extension to the path.
{
pattern: `^(?P<rootpkg>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/]*?)\.(bzr|git|hg|svn))(/[A-Za-z0-9_.\-]+)*$`,
},
}