aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2019-03-28 06:08:03 +0700
committerShulhan <ms@kilabit.info>2019-03-30 03:33:31 +0700
commit1fa2f800c62ba5cd54827773a9e86ea104e6ddad (patch)
tree5586217091dc99cadf55938f34da66d61971cdc1
parent2ae4f88570a21ade28dd4c6c72240173c25d2a85 (diff)
downloadbeku-1fa2f800c62ba5cd54827773a9e86ea104e6ddad.tar.xz
all: use copy of "golang.org/x/tools/go/vcs"
The indirect dependencies of package "golang.org/x/tools/go/vcs" is overwhelming. Using "go get" on this package alone will pull up all dependency of "golang.org/x/tools". To minimize unneeded download of unneeded packages we copy the package vcs to our own repository including their license file.
-rw-r--r--go.mod5
-rw-r--r--go.sum10
-rw-r--r--go/vcs/LICENSE27
-rw-r--r--go/vcs/discovery.go76
-rw-r--r--go/vcs/env.go39
-rw-r--r--go/vcs/http.go80
-rw-r--r--go/vcs/vcs.go759
-rw-r--r--go/vcs/vcs_test.go280
-rw-r--r--package.go4
9 files changed, 1269 insertions, 11 deletions
diff --git a/go.mod b/go.mod
index d8e95ae..6c9d079 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,3 @@
module github.com/shuLhan/beku
-require (
- github.com/shuLhan/share v0.2.0
- golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
-)
+require github.com/shuLhan/share v0.4.0
diff --git a/go.sum b/go.sum
index 2bca9eb..421927e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/shuLhan/share v0.2.0 h1:xm12WiZ0L9Yj5fNUgxKXfpxl4SJ9VuRdRwbQ5cACeik=
-github.com/shuLhan/share v0.2.0/go.mod h1:SjjW0kmafz7R07V8GP8D9YvTj0jQ4A5LoLVYjC+920U=
-golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+github.com/shuLhan/share v0.4.0 h1:bp5SlzcF7u1feOc4BvWHHVtUXecaq26Lyn8rROMbQ1k=
+github.com/shuLhan/share v0.4.0/go.mod h1:n3BWSX6aGXiCmpc89DsfnhNnax89yhKLHFu6WC9ODes=
+golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/go/vcs/LICENSE b/go/vcs/LICENSE
new file mode 100644
index 0000000..6a66aea
--- /dev/null
+++ b/go/vcs/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/go/vcs/discovery.go b/go/vcs/discovery.go
new file mode 100644
index 0000000..f431dc1
--- /dev/null
+++ b/go/vcs/discovery.go
@@ -0,0 +1,76 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io"
+ "strings"
+)
+
+// charsetReader returns a reader for the given charset. Currently
+// it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful
+// error which is printed by go get, so the user can find why the package
+// wasn't downloaded if the encoding is not supported. Note that, in
+// order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters
+// greater than 0x7f are not rejected).
+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)
+ }
+}
+
+// parseMetaGoImports returns meta imports from the HTML in r.
+// Parsing ends at the end of the <head> section or the beginning of the <body>.
+func parseMetaGoImports(r io.Reader) (imports []metaImport, 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 || len(imports) > 0 {
+ 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 {
+ imports = append(imports, metaImport{
+ Prefix: f[0],
+ VCS: f[1],
+ RepoRoot: f[2],
+ })
+ }
+ }
+}
+
+// attrValue returns the attribute value for the case-insensitive key
+// `name', or the empty string if nothing is found.
+func attrValue(attrs []xml.Attr, name string) string {
+ for _, a := range attrs {
+ if strings.EqualFold(a.Name.Local, name) {
+ return a.Value
+ }
+ }
+ return ""
+}
diff --git a/go/vcs/env.go b/go/vcs/env.go
new file mode 100644
index 0000000..e846f5b
--- /dev/null
+++ b/go/vcs/env.go
@@ -0,0 +1,39 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+import (
+ "os"
+ "strings"
+)
+
+// envForDir returns a copy of the environment
+// suitable for running in the given directory.
+// The environment is the current process's environment
+// but with an updated $PWD, so that an os.Getwd in the
+// child will be faster.
+func envForDir(dir string) []string {
+ env := os.Environ()
+ // Internally we only use rooted paths, so dir is rooted.
+ // Even if dir is not rooted, no harm done.
+ return mergeEnvLists([]string{"PWD=" + dir}, env)
+}
+
+// mergeEnvLists merges the two environment lists such that
+// variables with the same name in "in" replace those in "out".
+func mergeEnvLists(in, out []string) []string {
+NextVar:
+ for _, inkv := range in {
+ k := strings.SplitAfterN(inkv, "=", 2)[0]
+ for i, outkv := range out {
+ if strings.HasPrefix(outkv, k) {
+ out[i] = inkv
+ continue NextVar
+ }
+ }
+ out = append(out, inkv)
+ }
+ return out
+}
diff --git a/go/vcs/http.go b/go/vcs/http.go
new file mode 100644
index 0000000..9618818
--- /dev/null
+++ b/go/vcs/http.go
@@ -0,0 +1,80 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+)
+
+// httpClient is the default HTTP client, but a variable so it can be
+// changed by tests, without modifying http.DefaultClient.
+var httpClient = http.DefaultClient
+
+// httpGET returns the data from an HTTP GET request for the given URL.
+func httpGET(url string) ([]byte, error) {
+ resp, err := httpClient.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("%s: %s", url, resp.Status)
+ }
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %v", url, err)
+ }
+ return b, nil
+}
+
+// httpsOrHTTP returns the body of either the importPath's
+// https resource or, if unavailable, the http resource.
+func httpsOrHTTP(importPath string) (urlStr string, body io.ReadCloser, err error) {
+ fetch := func(scheme string) (urlStr string, res *http.Response, err error) {
+ u, err := url.Parse(scheme + "://" + importPath)
+ if err != nil {
+ return "", nil, err
+ }
+ u.RawQuery = "go-get=1"
+ urlStr = u.String()
+ if Verbose {
+ log.Printf("Fetching %s", urlStr)
+ }
+ res, err = httpClient.Get(urlStr)
+ return
+ }
+ closeBody := func(res *http.Response) {
+ if res != nil {
+ res.Body.Close()
+ }
+ }
+ urlStr, res, err := fetch("https")
+ if err != nil || res.StatusCode != 200 {
+ if Verbose {
+ if err != nil {
+ log.Printf("https fetch failed.")
+ } else {
+ log.Printf("ignoring https fetch with status code %d", res.StatusCode)
+ }
+ }
+ closeBody(res)
+ urlStr, res, err = fetch("http")
+ }
+ if err != nil {
+ closeBody(res)
+ return "", nil, err
+ }
+ // Note: accepting a non-200 OK here, so people can serve a
+ // meta import in their http 404 page.
+ if Verbose {
+ log.Printf("Parsing meta tags from %s (status code %d)", urlStr, res.StatusCode)
+ }
+ return urlStr, res.Body, nil
+}
diff --git a/go/vcs/vcs.go b/go/vcs/vcs.go
new file mode 100644
index 0000000..dc89c34
--- /dev/null
+++ b/go/vcs/vcs.go
@@ -0,0 +1,759 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package vcs exposes functions for resolving import paths
+// and using version control systems, which can be used to
+// implement behavior similar to the standard "go get" command.
+//
+// This package is a copy of internal code in package cmd/go/internal/get,
+// modified to make the identifiers exported. It's provided here
+// for developers who want to write tools with similar semantics.
+// It needs to be manually kept in sync with upstream when changes are
+// made to cmd/go/internal/get; see https://golang.org/issue/11490.
+//
+package vcs
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// Verbose enables verbose operation logging.
+var Verbose bool
+
+// ShowCmd controls whether VCS commands are printed.
+var ShowCmd bool
+
+// A Cmd describes how to use a version control system
+// like Mercurial, Git, or Subversion.
+type Cmd struct {
+ Name string
+ Cmd string // name of binary to invoke command
+
+ CreateCmd string // command to download a fresh copy of a repository
+ DownloadCmd string // command to download updates into an existing repository
+
+ TagCmd []TagCmd // commands to list tags
+ TagLookupCmd []TagCmd // commands to lookup tags before running tagSyncCmd
+ TagSyncCmd string // command to sync to specific tag
+ TagSyncDefault string // command to sync to default tag
+
+ LogCmd string // command to list repository changelogs in an XML format
+
+ Scheme []string
+ PingCmd string
+}
+
+// A TagCmd describes a command to list available tags
+// that can be passed to Cmd.TagSyncCmd.
+type TagCmd struct {
+ Cmd string // command to list tags
+ Pattern string // regexp to extract tags from list
+}
+
+// vcsList lists the known version control systems
+var vcsList = []*Cmd{
+ vcsHg,
+ vcsGit,
+ vcsSvn,
+ vcsBzr,
+}
+
+// ByCmd returns the version control system for the given
+// command name (hg, git, svn, bzr).
+func ByCmd(cmd string) *Cmd {
+ for _, vcs := range vcsList {
+ if vcs.Cmd == cmd {
+ return vcs
+ }
+ }
+ return nil
+}
+
+// vcsHg describes how to use Mercurial.
+var vcsHg = &Cmd{
+ Name: "Mercurial",
+ Cmd: "hg",
+
+ CreateCmd: "clone -U {repo} {dir}",
+ DownloadCmd: "pull",
+
+ // We allow both tag and branch names as 'tags'
+ // for selecting a version. This lets people have
+ // a go.release.r60 branch and a go1 branch
+ // and make changes in both, without constantly
+ // editing .hgtags.
+ TagCmd: []TagCmd{
+ {"tags", `^(\S+)`},
+ {"branches", `^(\S+)`},
+ },
+ TagSyncCmd: "update -r {tag}",
+ TagSyncDefault: "update default",
+
+ LogCmd: "log --encoding=utf-8 --limit={limit} --template={template}",
+
+ Scheme: []string{"https", "http", "ssh"},
+ PingCmd: "identify {scheme}://{repo}",
+}
+
+// vcsGit describes how to use Git.
+var vcsGit = &Cmd{
+ Name: "Git",
+ Cmd: "git",
+
+ CreateCmd: "clone {repo} {dir}",
+ DownloadCmd: "pull --ff-only",
+
+ TagCmd: []TagCmd{
+ // tags/xxx matches a git tag named xxx
+ // origin/xxx matches a git branch named xxx on the default remote repository
+ {"show-ref", `(?:tags|origin)/(\S+)$`},
+ },
+ TagLookupCmd: []TagCmd{
+ {"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
+ },
+ TagSyncCmd: "checkout {tag}",
+ TagSyncDefault: "checkout master",
+
+ Scheme: []string{"git", "https", "http", "git+ssh"},
+ PingCmd: "ls-remote {scheme}://{repo}",
+}
+
+// vcsBzr describes how to use Bazaar.
+var vcsBzr = &Cmd{
+ Name: "Bazaar",
+ Cmd: "bzr",
+
+ CreateCmd: "branch {repo} {dir}",
+
+ // Without --overwrite bzr will not pull tags that changed.
+ // Replace by --overwrite-tags after http://pad.lv/681792 goes in.
+ DownloadCmd: "pull --overwrite",
+
+ TagCmd: []TagCmd{{"tags", `^(\S+)`}},
+ TagSyncCmd: "update -r {tag}",
+ TagSyncDefault: "update -r revno:-1",
+
+ Scheme: []string{"https", "http", "bzr", "bzr+ssh"},
+ PingCmd: "info {scheme}://{repo}",
+}
+
+// vcsSvn describes how to use Subversion.
+var vcsSvn = &Cmd{
+ Name: "Subversion",
+ Cmd: "svn",
+
+ CreateCmd: "checkout {repo} {dir}",
+ DownloadCmd: "update",
+
+ // There is no tag command in subversion.
+ // The branch information is all in the path names.
+
+ LogCmd: "log --xml --limit={limit}",
+
+ Scheme: []string{"https", "http", "svn", "svn+ssh"},
+ PingCmd: "info {scheme}://{repo}",
+}
+
+func (v *Cmd) String() string {
+ return v.Name
+}
+
+// run runs the command line cmd in the given directory.
+// keyval is a list of key, value pairs. run expands
+// instances of {key} in cmd into value, but only after
+// splitting cmd into individual arguments.
+// If an error occurs, run prints the command line and the
+// command's combined stdout+stderr to standard error.
+// Otherwise run discards the command's output.
+func (v *Cmd) run(dir string, cmd string, keyval ...string) error {
+ _, err := v.run1(dir, cmd, keyval, true)
+ return err
+}
+
+// runVerboseOnly is like run but only generates error output to standard error in verbose mode.
+func (v *Cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error {
+ _, err := v.run1(dir, cmd, keyval, false)
+ return err
+}
+
+// runOutput is like run but returns the output of the command.
+func (v *Cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) {
+ return v.run1(dir, cmd, keyval, true)
+}
+
+// run1 is the generalized implementation of run and runOutput.
+func (v *Cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) {
+ m := make(map[string]string)
+ for i := 0; i < len(keyval); i += 2 {
+ m[keyval[i]] = keyval[i+1]
+ }
+ args := strings.Fields(cmdline)
+ for i, arg := range args {
+ args[i] = expand(m, arg)
+ }
+
+ _, err := exec.LookPath(v.Cmd)
+ if err != nil {
+ fmt.Fprintf(os.Stderr,
+ "go: missing %s command. See http://golang.org/s/gogetcmd\n",
+ v.Name)
+ return nil, err
+ }
+
+ cmd := exec.Command(v.Cmd, args...)
+ cmd.Dir = dir
+ cmd.Env = envForDir(cmd.Dir)
+ if ShowCmd {
+ fmt.Printf("cd %s\n", dir)
+ fmt.Printf("%s %s\n", v.Cmd, strings.Join(args, " "))
+ }
+ var buf bytes.Buffer
+ cmd.Stdout = &buf
+ cmd.Stderr = &buf
+ err = cmd.Run()
+ out := buf.Bytes()
+ if err != nil {
+ if verbose || Verbose {
+ fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.Cmd, strings.Join(args, " "))
+ os.Stderr.Write(out)
+ }
+ return nil, err
+ }
+ return out, nil
+}
+
+// Ping pings the repo to determine if scheme used is valid.
+// This repo must be pingable with this scheme and VCS.
+func (v *Cmd) Ping(scheme, repo string) error {
+ return v.runVerboseOnly(".", v.PingCmd, "scheme", scheme, "repo", repo)
+}
+
+// Create creates a new copy of repo in dir.
+// The parent of dir must exist; dir must not.
+func (v *Cmd) Create(dir, repo string) error {
+ return v.run(".", v.CreateCmd, "dir", dir, "repo", repo)
+}
+
+// CreateAtRev creates a new copy of repo in dir at revision rev.
+// The parent of dir must exist; dir must not.
+// rev must be a valid revision in repo.
+func (v *Cmd) CreateAtRev(dir, repo, rev string) error {
+ if err := v.Create(dir, repo); err != nil {
+ return err
+ }
+ return v.run(dir, v.TagSyncCmd, "tag", rev)
+}
+
+// Download downloads any new changes for the repo in dir.
+// dir must be a valid VCS repo compatible with v.
+func (v *Cmd) Download(dir string) error {
+ return v.run(dir, v.DownloadCmd)
+}
+
+// Tags returns the list of available tags for the repo in dir.
+// dir must be a valid VCS repo compatible with v.
+func (v *Cmd) Tags(dir string) ([]string, error) {
+ var tags []string
+ for _, tc := range v.TagCmd {
+ out, err := v.runOutput(dir, tc.Cmd)
+ if err != nil {
+ return nil, err
+ }
+ re := regexp.MustCompile(`(?m-s)` + tc.Pattern)
+ for _, m := range re.FindAllStringSubmatch(string(out), -1) {
+ tags = append(tags, m[1])
+ }
+ }
+ return tags, nil
+}
+
+// TagSync syncs the repo in dir to the named tag, which is either a
+// tag returned by Tags or the empty string (the default tag).
+// dir must be a valid VCS repo compatible with v and the tag must exist.
+func (v *Cmd) TagSync(dir, tag string) error {
+ if v.TagSyncCmd == "" {
+ return nil
+ }
+ if tag != "" {
+ for _, tc := range v.TagLookupCmd {
+ out, err := v.runOutput(dir, tc.Cmd, "tag", tag)
+ if err != nil {
+ return err
+ }
+ re := regexp.MustCompile(`(?m-s)` + tc.Pattern)
+ m := re.FindStringSubmatch(string(out))
+ if len(m) > 1 {
+ tag = m[1]
+ break
+ }
+ }
+ }
+ if tag == "" && v.TagSyncDefault != "" {
+ return v.run(dir, v.TagSyncDefault)
+ }
+ return v.run(dir, v.TagSyncCmd, "tag", tag)
+}
+
+// Log logs the changes for the repo in dir.
+// dir must be a valid VCS repo compatible with v.
+func (v *Cmd) Log(dir, logTemplate string) ([]byte, error) {
+ if err := v.Download(dir); err != nil {
+ return []byte{}, err
+ }
+
+ const N = 50 // how many revisions to grab
+ return v.runOutput(dir, v.LogCmd, "limit", strconv.Itoa(N), "template", logTemplate)
+}
+
+// LogAtRev logs the change for repo in dir at the rev revision.
+// dir must be a valid VCS repo compatible with v.
+// rev must be a valid revision for the repo in dir.
+func (v *Cmd) LogAtRev(dir, rev, logTemplate string) ([]byte, error) {
+ if err := v.Download(dir); err != nil {
+ return []byte{}, err
+ }
+
+ // Append revision flag to LogCmd.
+ logAtRevCmd := v.LogCmd + " --rev=" + rev
+ return v.runOutput(dir, logAtRevCmd, "limit", strconv.Itoa(1), "template", logTemplate)
+}
+
+// A vcsPath describes how to convert an import path into a
+// version control system and repository name.
+type vcsPath struct {
+ prefix string // prefix this description applies to
+ re string // pattern for import path
+ repo string // repository to use (expand with match of re)
+ vcs string // version control system to use (expand with match of re)
+ check func(match map[string]string) error // additional checks
+ ping bool // ping for scheme to use to download repo
+
+ regexp *regexp.Regexp // cached compiled form of re
+}
+
+// FromDir inspects dir and its parents to determine the
+// version control system and code repository to use.
+// On return, root is the import path
+// corresponding to the root of the repository.
+func FromDir(dir, srcRoot string) (vcs *Cmd, root string, err error) {
+ // Clean and double-check that dir is in (a subdirectory of) srcRoot.
+ dir = filepath.Clean(dir)
+ srcRoot = filepath.Clean(srcRoot)
+ if len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {
+ return nil, "", fmt.Errorf("directory %q is outside source root %q", dir, srcRoot)
+ }
+
+ var vcsRet *Cmd
+ var rootRet string
+
+ origDir := dir
+ for len(dir) > len(srcRoot) {
+ for _, vcs := range vcsList {
+ if _, err := os.Stat(filepath.Join(dir, "."+vcs.Cmd)); err == nil {
+ root := filepath.ToSlash(dir[len(srcRoot)+1:])
+ // Record first VCS we find, but keep looking,
+ // to detect mistakes like one kind of VCS inside another.
+ if vcsRet == nil {
+ vcsRet = vcs
+ rootRet = root
+ continue
+ }
+ // Allow .git inside .git, which can arise due to submodules.
+ if vcsRet == vcs && vcs.Cmd == "git" {
+ continue
+ }
+ // Otherwise, we have one VCS inside a different VCS.
+ return nil, "", fmt.Errorf("directory %q uses %s, but parent %q uses %s",
+ filepath.Join(srcRoot, rootRet), vcsRet.Cmd, filepath.Join(srcRoot, root), vcs.Cmd)
+ }
+ }
+
+ // Move to parent.
+ ndir := filepath.Dir(dir)
+ if len(ndir) >= len(dir) {
+ // Shouldn't happen, but just in case, stop.
+ break
+ }
+ dir = ndir
+ }
+
+ if vcsRet != nil {
+ return vcsRet, rootRet, nil
+ }
+
+ return nil, "", fmt.Errorf("directory %q is not using a known version control system", origDir)
+}
+
+// RepoRoot represents a version control system, a repo, and a root of
+// where to put it on disk.
+type RepoRoot struct {
+ VCS *Cmd
+
+ // Repo is the repository URL, including scheme.
+ Repo string
+
+ // Root is the import path corresponding to the root of the
+ // repository.
+ Root string
+}
+
+// RepoRootForImportPath analyzes importPath to determine the
+// version control system, and code repository to use.
+func RepoRootForImportPath(importPath string, verbose bool) (*RepoRoot, error) {
+ rr, err := RepoRootForImportPathStatic(importPath, "")
+ if err == errUnknownSite {
+ rr, err = RepoRootForImportDynamic(importPath, verbose)
+
+ // RepoRootForImportDynamic returns error detail
+ // that is irrelevant if the user didn't intend to use a
+ // dynamic import in the first place.
+ // Squelch it.
+ if err != nil {
+ if Verbose {
+ log.Printf("import %q: %v", importPath, err)
+ }
+ err = fmt.Errorf("unrecognized import path %q", importPath)
+ }
+ }
+
+ if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.Root, "...") {
+ // Do not allow wildcards in the repo root.
+ rr = nil
+ err = fmt.Errorf("cannot expand ... in %q", importPath)
+ }
+ return rr, err
+}
+
+var errUnknownSite = errors.New("dynamic lookup required to find mapping")
+
+// RepoRootForImportPathStatic attempts to map importPath to a
+// RepoRoot using the commonly-used VCS hosting sites in vcsPaths
+// (github.com/user/dir), or from a fully-qualified importPath already
+// containing its VCS type (foo.com/repo.git/dir)
+//
+// If scheme is non-empty, that scheme is forced.
+func RepoRootForImportPathStatic(importPath, scheme string) (*RepoRoot, error) {
+ if strings.Contains(importPath, "://") {
+ return nil, fmt.Errorf("invalid import path %q", importPath)
+ }
+ for _, srv := range vcsPaths {
+ if !strings.HasPrefix(importPath, srv.prefix) {
+ continue
+ }
+ m := srv.regexp.FindStringSubmatch(importPath)
+ if m == nil {
+ if srv.prefix != "" {
+ return nil, fmt.Errorf("invalid %s import path %q", srv.prefix, importPath)
+ }
+ continue
+ }
+
+ // Build map of named subexpression matches for expand.
+ match := map[string]string{
+ "prefix": srv.prefix,
+ "import": importPath,
+ }
+ for i, name := range srv.regexp.SubexpNames() {
+ if name != "" && match[name] == "" {
+ match[name] = m[i]
+ }
+ }
+ if srv.vcs != "" {
+ match["vcs"] = expand(match, srv.vcs)
+ }
+ if srv.repo != "" {
+ match["repo"] = expand(match, srv.repo)
+ }
+ if srv.check != nil {
+ if err := srv.check(match); err != nil {
+ return nil, err
+ }
+ }
+ vcs := ByCmd(match["vcs"])
+ if vcs == nil {
+ return nil, fmt.Errorf("unknown version control system %q", match["vcs"])
+ }
+ if srv.ping {
+ if scheme != "" {
+ match["repo"] = scheme + "://" + match["repo"]
+ } else {
+ for _, scheme := range vcs.Scheme {
+ if vcs.Ping(scheme, match["repo"]) == nil {
+ match["repo"] = scheme + "://" + match["repo"]
+ break
+ }
+ }
+ }
+ }
+ rr := &RepoRoot{
+ VCS: vcs,
+ Repo: match["repo"],
+ Root: match["root"],
+ }
+ return rr, nil
+ }
+ return nil, errUnknownSite
+}
+
+// RepoRootForImportDynamic finds a *RepoRoot for a custom domain that's not
+// statically known by RepoRootForImportPathStatic.
+//
+// This handles custom import paths like "name.tld/pkg/foo" or just "name.tld".
+func RepoRootForImportDynamic(importPath string, verbose bool) (*RepoRoot, error) {
+ slash := strings.Index(importPath, "/")
+ if slash < 0 {
+ slash = len(importPath)
+ }
+ host := importPath[:slash]
+ if !strings.Contains(host, ".") {
+ return nil, errors.New("import path doesn't contain a hostname")
+ }
+ urlStr, body, err := httpsOrHTTP(importPath)
+ if err != nil {
+ return nil, fmt.Errorf("http/https fetch: %v", err)
+ }
+ defer body.Close()
+ imports, err := parseMetaGoImports(body)
+ if err != nil {
+ return nil, fmt.Errorf("parsing %s: %v", importPath, err)
+ }
+ metaImport, err := matchGoImport(imports, importPath)
+ if err != nil {
+ if err != errNoMatch {
+ return nil, fmt.Errorf("parse %s: %v", urlStr, err)
+ }
+ return nil, fmt.Errorf("parse %s: no go-import meta tags", urlStr)
+ }
+ if verbose {
+ log.Printf("get %q: found meta tag %#v at %s", importPath, metaImport, urlStr)
+ }
+ // If the import was "uni.edu/bob/project", which said the
+ // prefix was "uni.edu" and the RepoRoot was "evilroot.com",
+ // make sure we don't trust Bob and check out evilroot.com to
+ // "uni.edu" yet (possibly overwriting/preempting another
+ // non-evil student). Instead, first verify the root and see
+ // if it matches Bob's claim.
+ if metaImport.Prefix != importPath {
+ if verbose {
+ log.Printf("get %q: verifying non-authoritative meta tag", importPath)
+ }
+ urlStr0 := urlStr
+ urlStr, body, err = httpsOrHTTP(metaImport.Prefix)
+ if err != nil {
+ return nil, fmt.Errorf("fetch %s: %v", urlStr, err)
+ }
+ imports, err := parseMetaGoImports(body)
+ if err != nil {
+ return nil, fmt.Errorf("parsing %s: %v", importPath, err)
+ }
+ if len(imports) == 0 {
+ return nil, fmt.Errorf("fetch %s: no go-import meta tag", urlStr)
+ }
+ metaImport2, err := matchGoImport(imports, importPath)
+ if err != nil || metaImport != metaImport2 {
+ return nil, fmt.Errorf("%s and %s disagree about go-import for %s", urlStr0, urlStr, metaImport.Prefix)
+ }
+ }
+
+ if err := validateRepoRoot(metaImport.RepoRoot); err != nil {
+ return nil, fmt.Errorf("%s: invalid repo root %q: %v", urlStr, metaImport.RepoRoot, err)
+ }
+ rr := &RepoRoot{
+ VCS: ByCmd(metaImport.VCS),
+ Repo: metaImport.RepoRoot,
+ Root: metaImport.Prefix,
+ }
+ if rr.VCS == nil {
+ return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, metaImport.VCS)
+ }
+ return rr, nil
+}
+
+// validateRepoRoot returns an error if repoRoot does not seem to be
+// a valid URL with scheme.
+func validateRepoRoot(repoRoot string) error {
+ url, err := url.Parse(repoRoot)
+ if err != nil {
+ return err
+ }
+ if url.Scheme == "" {
+ return errors.New("no scheme")
+ }
+ return nil
+}
+
+// metaImport represents the parsed <meta name="go-import"
+// content="prefix vcs reporoot" /> tags from HTML files.
+type metaImport struct {
+ Prefix, VCS, RepoRoot string
+}
+
+// errNoMatch is returned from matchGoImport when there's no applicable match.
+var errNoMatch = errors.New("no import match")
+
+// pathPrefix reports whether sub is a prefix of s,
+// only considering entire path components.
+func pathPrefix(s, sub string) bool {
+ // strings.HasPrefix is necessary but not sufficient.
+ if !strings.HasPrefix(s, sub) {
+ return false
+ }
+ // The remainder after the prefix must either be empty or start with a slash.
+ rem := s[len(sub):]
+ return rem == "" || rem[0] == '/'
+}
+
+// matchGoImport returns the metaImport from imports matching importPath.
+// An error is returned if there are multiple matches.
+// errNoMatch is returned if none match.
+func matchGoImport(imports []metaImport, importPath string) (_ metaImport, err error) {
+ match := -1
+ for i, im := range imports {
+ if !pathPrefix(importPath, im.Prefix) {
+ continue
+ }
+
+ if match != -1 {
+ err = fmt.Errorf("multiple meta tags match import path %q", importPath)
+ return
+ }
+ match = i
+ }
+ if match == -1 {
+ err = errNoMatch
+ return
+ }
+ return imports[match], nil
+}
+
+// expand rewrites s to replace {k} with match[k] for each key k in match.
+func expand(match map[string]string, s string) string {
+ for k, v := range match {
+ s = strings.Replace(s, "{"+k+"}", v, -1)
+ }
+ return s
+}
+
+// vcsPaths lists the known vcs paths.
+var vcsPaths = []*vcsPath{
+ // Github
+ {
+ prefix: "github.com/",
+ re: `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[\p{L}0-9_.\-]+)*$`,
+ vcs: "git",
+ repo: "https://{root}",
+ check: noVCSSuffix,
+ },
+
+ // Bitbucket
+ {
+ prefix: "bitbucket.org/",
+ re: `^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
+ repo: "https://{root}",
+ check: bitbucketVCS,
+ },
+
+ // Launchpad
+ {
+ prefix: "launchpad.net/",
+ re: `^(?P<root>launchpad\.net/((?P<project>[A-Za-z0-9_.\-]+)(?P<series>/[A-Za-z0-9_.\-]+)?|~[A-Za-z0-9_.\-]+/(\+junk|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
+ vcs: "bzr",
+ repo: "https://{root}",
+ check: launchpadVCS,
+ },
+
+ // Git at OpenStack
+ {
+ prefix: "git.openstack.org",
+ re: `^(?P<root>git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(\.git)?(/[A-Za-z0-9_.\-]+)*$`,
+ vcs: "git",
+ repo: "https://{root}",
+ check: noVCSSuffix,
+ },
+
+ // General syntax for any server.
+ {
+ re: `^(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/]*?)\.(?P<vcs>bzr|git|hg|svn))(/[A-Za-z0-9_.\-]+)*$`,
+ ping: true,
+ },
+}
+
+func init() {
+ // fill in cached regexps.
+ // Doing this eagerly discovers invalid regexp syntax
+ // without having to run a command that needs that regexp.
+ for _, srv := range vcsPaths {
+ srv.regexp = regexp.MustCompile(srv.re)
+ }
+}
+
+// noVCSSuffix checks that the repository name does not
+// end in .foo for any version control system foo.
+// The usual culprit is ".git".
+func noVCSSuffix(match map[string]string) error {
+ repo := match["repo"]
+ for _, vcs := range vcsList {
+ if strings.HasSuffix(repo, "."+vcs.Cmd) {
+ return fmt.Errorf("invalid version control suffix in %s path", match["prefix"])
+ }
+ }
+ return nil
+}
+
+// bitbucketVCS determines the version control system for a
+// Bitbucket repository, by using the Bitbucket API.
+func bitbucketVCS(match map[string]string) error {
+ if err := noVCSSuffix(match); err != nil {
+ return err
+ }
+
+ var resp struct {
+ SCM string `json:"scm"`
+ }
+ url := expand(match, "https://api.bitbucket.org/2.0/repositories/{bitname}?fields=scm")
+ data, err := httpGET(url)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(data, &resp); err != nil {
+ return fmt.Errorf("decoding %s: %v", url, err)
+ }
+
+ if ByCmd(resp.SCM) != nil {
+ match["vcs"] = resp.SCM
+ if resp.SCM == "git" {
+ match["repo"] += ".git"
+ }
+ return nil
+ }
+
+ return fmt.Errorf("unable to detect version control system for bitbucket.org/ path")
+}
+
+// launchpadVCS solves the ambiguity for "lp.net/project/foo". In this case,
+// "foo" could be a series name registered in Launchpad with its own branch,
+// and it could also be the name of a directory within the main project
+// branch one level up.
+func launchpadVCS(match map[string]string) error {
+ if match["project"] == "" || match["series"] == "" {
+ return nil
+ }
+ _, err := httpGET(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format"))
+ if err != nil {
+ match["root"] = expand(match, "launchpad.net/{project}")
+ match["repo"] = expand(match, "https://{root}")
+ }
+ return nil
+}
diff --git a/go/vcs/vcs_test.go b/go/vcs/vcs_test.go
new file mode 100644
index 0000000..d18ff18
--- /dev/null
+++ b/go/vcs/vcs_test.go
@@ -0,0 +1,280 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vcs
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+// Test that RepoRootForImportPath creates the correct RepoRoot for a given importPath.
+// TODO(cmang): Add tests for SVN and BZR.
+func TestRepoRootForImportPath(t *testing.T) {
+ if runtime.GOOS == "android" {
+ t.Skipf("incomplete source tree on %s", runtime.GOOS)
+ }
+
+ tests := []struct {
+ path string
+ want *RepoRoot
+ }{
+ {
+ "github.com/golang/groupcache",
+ &RepoRoot{
+ VCS: vcsGit,
+ Repo: "https://github.com/golang/groupcache",
+ },
+ },
+ // Unicode letters in directories (issue 18660).
+ {
+ "github.com/user/unicode/испытание",
+ &RepoRoot{
+ VCS: vcsGit,
+ Repo: "https://github.com/user/unicode",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ got, err := RepoRootForImportPath(test.path, false)
+ if err != nil {
+ t.Errorf("RepoRootForImportPath(%q): %v", test.path, err)
+ continue
+ }
+ want := test.want
+ if got.VCS.Name != want.VCS.Name || got.Repo != want.Repo {
+ t.Errorf("RepoRootForImportPath(%q) = VCS(%s) Repo(%s), want VCS(%s) Repo(%s)", test.path, got.VCS, got.Repo, want.VCS, want.Repo)
+ }
+ }
+}
+
+// Test that FromDir correctly inspects a given directory and returns the right VCS and root.
+func TestFromDir(t *testing.T) {
+ tempDir, err := ioutil.TempDir("", "vcstest")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ for j, vcs := range vcsList {
+ dir := filepath.Join(tempDir, "example.com", vcs.Name, "."+vcs.Cmd)
+ if j&1 == 0 {
+ err := os.MkdirAll(dir, 0755)
+ if err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ err := os.MkdirAll(filepath.Dir(dir), 0755)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f, err := os.Create(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.Close()
+ }
+
+ want := RepoRoot{
+ VCS: vcs,
+ Root: path.Join("example.com", vcs.Name),
+ }
+ var got RepoRoot
+ got.VCS, got.Root, err = FromDir(dir, tempDir)
+ if err != nil {
+ t.Errorf("FromDir(%q, %q): %v", dir, tempDir, err)
+ continue
+ }
+ if got.VCS.Name != want.VCS.Name || got.Root != want.Root {
+ t.Errorf("FromDir(%q, %q) = VCS(%s) Root(%s), want VCS(%s) Root(%s)", dir, tempDir, got.VCS, got.Root, want.VCS, want.Root)
+ }
+ }
+}
+
+var parseMetaGoImportsTests = []struct {
+ in string
+ out []metaImport
+}{
+ {
+ `<meta name="go-import" content="foo/bar git https://github.com/rsc/foo/bar">`,
+ []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar"}},
+ },
+ {
+ `<meta name="go-import" content="foo/bar git https://github.com/rsc/foo/bar">
+ <meta name="go-import" content="baz/quux git http://github.com/rsc/baz/quux">`,
+ []metaImport{
+ {"foo/bar", "git", "https://github.com/rsc/foo/bar"},
+ {"baz/quux", "git", "http://github.com/rsc/baz/quux"},
+ },
+ },
+ {
+ `<head>
+ <meta name="go-import" content="foo/bar git https://github.com/rsc/foo/bar">
+ </head>`,
+ []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar"}},
+ },
+ {
+ `<meta name="go-import" content="foo/bar git https://github.com/rsc/foo/bar">
+ <body>`,
+ []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar"}},
+ },
+}
+
+func TestParseMetaGoImports(t *testing.T) {
+ for i, tt := range parseMetaGoImportsTests {
+ out, err := parseMetaGoImports(strings.NewReader(tt.in))
+ if err != nil {
+ t.Errorf("test#%d: %v", i, err)
+ continue
+ }
+ if !reflect.DeepEqual(out, tt.out) {
+ t.Errorf("test#%d:\n\thave %q\n\twant %q", i, out, tt.out)
+ }
+ }
+}
+
+func TestValidateRepoRoot(t *testing.T) {
+ tests := []struct {
+ root string
+ ok bool
+ }{
+ {
+ root: "",
+ ok: false,
+ },
+ {
+ root: "http://",
+ ok: true,
+ },
+ {
+ root: "git+ssh://",
+ ok: true,
+ },
+ {
+ root: "http#://",
+ ok: false,
+ },
+ {
+ root: "-config",
+ ok: false,
+ },
+ {
+ root: "-config://",
+ ok: false,
+ },
+ }
+
+ for _, test := range tests {
+ err := validateRepoRoot(test.root)
+ ok := err == nil
+ if ok != test.ok {
+ want := "error"
+ if test.ok {
+ want = "nil"
+ }
+ t.Errorf("validateRepoRoot(%q) = %q, want %s", test.root, err, want)
+ }
+ }
+}
+
+func TestMatchGoImport(t *testing.T) {
+ tests := []struct {
+ imports []metaImport
+ path string
+ mi metaImport
+ err error
+ }{
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/foo",
+ mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/foo/",
+ mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ {Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/foo",
+ mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ {Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/fooa",
+ mi: metaImport{Prefix: "example.com/user/fooa", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/foo/bar",
+ err: errors.New("should not be allowed to create nested repo"),
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/foo/bar/baz",
+ err: errors.New("should not be allowed to create nested repo"),
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/foo/bar/baz/qux",
+ err: errors.New("should not be allowed to create nested repo"),
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com/user/foo/bar/baz/",
+ err: errors.New("should not be allowed to create nested repo"),
+ },
+ {
+ imports: []metaImport{
+ {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ {Prefix: "example.com/user/foo/bar", VCS: "git", RepoRoot: "https://example.com/repo/target"},
+ },
+ path: "example.com",
+ err: errors.New("pathologically short path"),
+ },
+ }
+
+ for _, test := range tests {
+ mi, err := matchGoImport(test.imports, test.path)
+ if mi != test.mi {
+ t.Errorf("unexpected metaImport; got %v, want %v", mi, test.mi)
+ }
+
+ got := err
+ want := test.err
+ if (got == nil) != (want == nil) {
+ t.Errorf("unexpected error; got %v, want %v", got, want)
+ }
+ }
+}
diff --git a/package.go b/package.go
index be58628..d9c072d 100644
--- a/package.go
+++ b/package.go
@@ -14,12 +14,12 @@ import (
"sort"
"strings"
- "golang.org/x/tools/go/vcs"
-
"github.com/shuLhan/share/lib/debug"
"github.com/shuLhan/share/lib/git"
"github.com/shuLhan/share/lib/ini"
libio "github.com/shuLhan/share/lib/io"
+
+ "github.com/shuLhan/beku/go/vcs"
)
const (