initial commit
This commit is contained in:
221
pkg/vcs/git.go
Normal file
221
pkg/vcs/git.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/config"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
|
||||
"gopkg.in/src-d/go-git.v4/storage/memory"
|
||||
)
|
||||
|
||||
const remoteName = "origin"
|
||||
|
||||
type gitVCS struct {
|
||||
log logger
|
||||
module string
|
||||
auth Auth
|
||||
}
|
||||
|
||||
func NewGit(l logger, module string, auth Auth) VCS {
|
||||
return &gitVCS{log: l, module: module, auth: auth}
|
||||
}
|
||||
|
||||
func (g *gitVCS) List(ctx context.Context) ([]Version, error) {
|
||||
g.log("gitVCS.List", "module", g.module)
|
||||
repo, err := g.repo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remote, err := repo.Remote(remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth, err := g.authMethod()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs, err := remote.List(&git.ListOptions{Auth: auth})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []Version{}
|
||||
masterHash := ""
|
||||
for _, ref := range refs {
|
||||
name := ref.Name()
|
||||
if name == plumbing.Master {
|
||||
masterHash = ref.Hash().String()
|
||||
} else if name.IsTag() && strings.HasPrefix(name.String(), "refs/tags/v") {
|
||||
list = append(list, Version(strings.TrimPrefix(name.String(), "refs/tags/")))
|
||||
}
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
if masterHash == "" {
|
||||
return nil, errors.New("no tags and no master branch found")
|
||||
}
|
||||
short := masterHash[:12]
|
||||
t, err := g.Timestamp(ctx, Version("v0.0.0-20060102150405-"+short))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = []Version{Version(fmt.Sprintf("v0.0.0-%s-%s", t.Format("20060102150405"), short))}
|
||||
}
|
||||
|
||||
g.log("gitVCS.List", "module", g.module, "list", list)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (g *gitVCS) Timestamp(ctx context.Context, version Version) (time.Time, error) {
|
||||
g.log("gitVCS.Timestamp", "module", g.module, "version", version)
|
||||
ci, err := g.commit(ctx, version)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
g.log("gitVCS.Timestamp", "module", g.module, "version", version, "timestamp", ci.Committer.When)
|
||||
return ci.Committer.When, nil
|
||||
}
|
||||
|
||||
func isVendoredPackage(name string) bool {
|
||||
var i int
|
||||
if strings.HasPrefix(name, "vendor/") {
|
||||
i += len("vendor/")
|
||||
} else if j := strings.Index(name, "/vendor/"); j >= 0 {
|
||||
i += len("/vendor/")
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(name[i:], "/")
|
||||
}
|
||||
|
||||
func (g *gitVCS) Zip(ctx context.Context, version Version) (io.ReadCloser, error) {
|
||||
g.log("gitVCS.Zip", "module", g.module, "version", version)
|
||||
ci, err := g.commit(ctx, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, err := ci.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
zw := zip.NewWriter(b)
|
||||
tree.Files().ForEach(func(f *object.File) error {
|
||||
// go mod strips vendored directories from the zip, and we do the same
|
||||
// to match the checksums in the go.sum
|
||||
if isVendoredPackage(f.Name) {
|
||||
return nil
|
||||
}
|
||||
w, err := zw.Create(filepath.Join(g.module+"@"+string(version), f.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, err := f.Reader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
io.Copy(w, r)
|
||||
return nil
|
||||
})
|
||||
zw.Close()
|
||||
return ioutil.NopCloser(bytes.NewBuffer(b.Bytes())), nil
|
||||
}
|
||||
|
||||
func (g *gitVCS) repo(ctx context.Context) (*git.Repository, error) {
|
||||
repo, err := git.Init(memory.NewStorage(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
schema := "https://"
|
||||
if g.auth.Key != "" {
|
||||
schema = "ssh://"
|
||||
}
|
||||
repoRoot := g.module
|
||||
if meta, err := MetaImports(ctx, g.module); err == nil {
|
||||
repoRoot = meta
|
||||
}
|
||||
_, err = repo.CreateRemote(&config.RemoteConfig{
|
||||
Name: remoteName,
|
||||
URLs: []string{schema + repoRoot + ".git"},
|
||||
})
|
||||
return repo, err
|
||||
}
|
||||
|
||||
func (g *gitVCS) commit(ctx context.Context, version Version) (*object.Commit, error) {
|
||||
repo, err := g.repo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth, err := g.authMethod()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = repo.FetchContext(ctx, &git.FetchOptions{
|
||||
RemoteName: remoteName,
|
||||
Auth: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version = Version(strings.TrimSuffix(string(version), "+incompatible"))
|
||||
hash := version.Hash()
|
||||
if version.IsSemVer() {
|
||||
tags, err := repo.Tags()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags.ForEach(func(t *plumbing.Reference) error {
|
||||
if t.Name().String() == "refs/tags/"+string(version) {
|
||||
hash = t.Hash().String()
|
||||
annotated, err := repo.TagObject(t.Hash())
|
||||
if err == nil {
|
||||
hash = annotated.Target.String()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
commits, err := repo.CommitObjects()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits.ForEach(func(ci *object.Commit) error {
|
||||
if strings.HasPrefix(ci.Hash.String(), version.Hash()) {
|
||||
hash = ci.Hash.String()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
g.log("gitVCS.commit", "module", g.module, "version", version, "hash", hash)
|
||||
return repo.CommitObject(plumbing.NewHash(hash))
|
||||
}
|
||||
|
||||
func (g *gitVCS) authMethod() (transport.AuthMethod, error) {
|
||||
if g.auth.Key != "" {
|
||||
return ssh.NewPublicKeysFromFile("git", g.auth.Key, "")
|
||||
} else if g.auth.Username != "" {
|
||||
return &http.BasicAuth{Username: g.auth.Username, Password: g.auth.Password}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
90
pkg/vcs/vcs.go
Normal file
90
pkg/vcs/vcs.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type logger = func(v ...interface{})
|
||||
|
||||
type Version string
|
||||
|
||||
var reSemVer = regexp.MustCompile(`^v\d+\.\d+\.\d+$`)
|
||||
|
||||
func (v Version) IsSemVer() bool { return reSemVer.MatchString(string(v)) }
|
||||
func (v Version) Hash() string {
|
||||
fields := strings.Split(string(v), "-")
|
||||
if len(fields) != 3 {
|
||||
return ""
|
||||
}
|
||||
return fields[2]
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
Timestamp(ctx context.Context, version Version) (time.Time, error)
|
||||
Zip(ctx context.Context, version Version) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
type VCS interface {
|
||||
List(ctx context.Context) ([]Version, error)
|
||||
Module
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Username string
|
||||
Password string
|
||||
Key string
|
||||
}
|
||||
|
||||
func NoAuth() Auth { return Auth{} }
|
||||
func Password(username, password string) Auth { return Auth{Username: username, Password: password} }
|
||||
func Key(key string) Auth { return Auth{Key: key} }
|
||||
|
||||
func MetaImports(ctx context.Context, module string) (string, error) {
|
||||
if strings.HasPrefix(module, "github.com/") || strings.HasPrefix(module, "bitbucket.org/") {
|
||||
return module, nil
|
||||
}
|
||||
// TODO: use context
|
||||
res, err := http.Get("https://" + module + "?go-get=1")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
html := struct {
|
||||
HTML string `xml:"html"`
|
||||
Head struct {
|
||||
Meta []struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Content string `xml:"content,attr"`
|
||||
} `xml:"meta"`
|
||||
} `xml:"head"`
|
||||
}{}
|
||||
dec := xml.NewDecoder(res.Body)
|
||||
dec.Strict = false
|
||||
dec.AutoClose = xml.HTMLAutoClose
|
||||
dec.Entity = xml.HTMLEntity
|
||||
if err := dec.Decode(&html); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, meta := range html.Head.Meta {
|
||||
if meta.Name == "go-import" {
|
||||
if f := strings.Fields(meta.Content); len(f) == 3 {
|
||||
if f[0] != module {
|
||||
return "", errors.New("prefix does not match the module")
|
||||
}
|
||||
url := f[2]
|
||||
if i := strings.Index(url, "://"); i >= 0 {
|
||||
url = url[i+3:]
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("go-import meta tag not found")
|
||||
}
|
||||
Reference in New Issue
Block a user