|  | package util | 
|  |  | 
|  | import ( | 
|  | "encoding/xml" | 
|  | "fmt" | 
|  | "go/build" | 
|  | "io" | 
|  | "net/http" | 
|  | "net/url" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "regexp" | 
|  | "strings" | 
|  |  | 
|  | "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) | 
|  | } | 
|  | } | 
|  |  | 
|  | // GetRootFromPackage retrives the top level package from a name. | 
|  | // | 
|  | // 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 GetRootFromPackage(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 = getRootFromGoGet(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 getRootFromGoGet(pkg string) string { | 
|  |  | 
|  | p, found := checkRemotePackageCache(pkg) | 
|  | if found { | 
|  | return p | 
|  | } | 
|  |  | 
|  | 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 { | 
|  | addToRemotePackageCache(pkg, pkg) | 
|  | return pkg | 
|  | } | 
|  | defer resp.Body.Close() | 
|  |  | 
|  | nu, err := parseImportFromBody(u, resp.Body) | 
|  | if err != nil { | 
|  | addToRemotePackageCache(pkg, pkg) | 
|  | return pkg | 
|  | } else if nu == "" { | 
|  | addToRemotePackageCache(pkg, pkg) | 
|  | return pkg | 
|  | } | 
|  |  | 
|  | addToRemotePackageCache(pkg, nu) | 
|  | return nu | 
|  | } | 
|  |  | 
|  | // The caching is not concurrency safe but should be made to be that way. | 
|  | // This implementation is far too much of a hack... rewrite needed. | 
|  | var remotePackageCache = make(map[string]string) | 
|  |  | 
|  | func checkRemotePackageCache(pkg string) (string, bool) { | 
|  | for k, v := range remotePackageCache { | 
|  | if pkg == k { | 
|  | return v, true | 
|  | } | 
|  | } | 
|  |  | 
|  | return pkg, false | 
|  | } | 
|  |  | 
|  | func addToRemotePackageCache(pkg, v string) { | 
|  | remotePackageCache[pkg] = v | 
|  | } | 
|  |  | 
|  | 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 { | 
|  | // If we hit the end of the markup and don't have anything | 
|  | // we return an error. | 
|  | err = vcs.ErrCannotDetectVCS | 
|  | } | 
|  | 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 the prefix supplied by the remote system isn't a prefix to the | 
|  | // url we're fetching return continue looking for more go-imports. | 
|  | // 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]) { | 
|  | continue | 
|  | } else { | 
|  | u = f[0] | 
|  | return | 
|  | } | 
|  |  | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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:    "hub.jazz.net", | 
|  | pattern: `^(?P<rootpkg>hub\.jazz\.net/git/[a-z0-9]+/[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_.\-]+)*$`, | 
|  | }, | 
|  | } | 
|  |  | 
|  | // BuildCtxt is a convenience wrapper for not having to import go/build | 
|  | // anywhere else | 
|  | type BuildCtxt struct { | 
|  | build.Context | 
|  | } | 
|  |  | 
|  | // PackageName attempts to determine the name of the base package. | 
|  | // | 
|  | // If resolution fails, this will return "main". | 
|  | func (b *BuildCtxt) PackageName(base string) string { | 
|  | cwd, err := os.Getwd() | 
|  | if err != nil { | 
|  | return "main" | 
|  | } | 
|  |  | 
|  | pkg, err := b.Import(base, cwd, 0) | 
|  | if err != nil { | 
|  | // There may not be any top level Go source files but the project may | 
|  | // still be within the GOPATH. | 
|  | if strings.HasPrefix(base, b.GOPATH) { | 
|  | p := strings.TrimPrefix(base, b.GOPATH) | 
|  | return strings.Trim(p, string(os.PathSeparator)) | 
|  | } | 
|  | } | 
|  |  | 
|  | return pkg.ImportPath | 
|  | } | 
|  |  | 
|  | // GetBuildContext returns a build context from go/build. When the $GOROOT | 
|  | // variable is not set in the users environment it sets the context's root | 
|  | // path to the path returned by 'go env GOROOT'. | 
|  | // | 
|  | // TODO: This should be moved to the `dependency` package. | 
|  | func GetBuildContext() (*BuildCtxt, error) { | 
|  | buildContext := &BuildCtxt{build.Default} | 
|  | if goRoot := os.Getenv("GOROOT"); len(goRoot) == 0 { | 
|  | out, err := exec.Command("go", "env", "GOROOT").Output() | 
|  | if goRoot = strings.TrimSpace(string(out)); len(goRoot) == 0 || err != nil { | 
|  | return nil, fmt.Errorf("Please set the $GOROOT environment " + | 
|  | "variable to use this command\n") | 
|  | } | 
|  | buildContext.GOROOT = goRoot | 
|  | } | 
|  | return buildContext, nil | 
|  | } | 
|  |  | 
|  | // NormalizeName takes a package name and normalizes it to the top level package. | 
|  | // | 
|  | // For example, golang.org/x/crypto/ssh becomes golang.org/x/crypto. 'ssh' is | 
|  | // returned as extra data. | 
|  | // | 
|  | // FIXME: Is this deprecated? | 
|  | func NormalizeName(name string) (string, string) { | 
|  | // Fastpath check if a name in the GOROOT. There is an issue when a pkg | 
|  | // is in the GOROOT and GetRootFromPackage tries to look it up because it | 
|  | // expects remote names. | 
|  | b, err := GetBuildContext() | 
|  | if err == nil { | 
|  | p := filepath.Join(b.GOROOT, "src", name) | 
|  | if _, err := os.Stat(p); err == nil { | 
|  | return filepath.ToSlash(name), "" | 
|  | } | 
|  | } | 
|  |  | 
|  | name = filepath.ToSlash(name) | 
|  | root := GetRootFromPackage(name) | 
|  | extra := strings.TrimPrefix(name, root) | 
|  | if len(extra) > 0 && extra != "/" { | 
|  | extra = strings.TrimPrefix(extra, "/") | 
|  | } else { | 
|  | // If extra is / (which is what it would be here) we want to return "" | 
|  | extra = "" | 
|  | } | 
|  |  | 
|  | return root, extra | 
|  | } |