github.com/bilus/oya@v0.0.3-0.20190301162104-da4acbd394c6/pkg/repo/github.go (about)

     1  package repo
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/bilus/oya/pkg/oyafile"
    12  	"github.com/bilus/oya/pkg/pack"
    13  	"github.com/bilus/oya/pkg/semver"
    14  	"github.com/bilus/oya/pkg/types"
    15  	log "github.com/sirupsen/logrus"
    16  	"gopkg.in/src-d/go-billy.v4/memfs"
    17  	git "gopkg.in/src-d/go-git.v4"
    18  	"gopkg.in/src-d/go-git.v4/plumbing"
    19  	"gopkg.in/src-d/go-git.v4/plumbing/object"
    20  	"gopkg.in/src-d/go-git.v4/plumbing/transport"
    21  	"gopkg.in/src-d/go-git.v4/storage/memory"
    22  )
    23  
    24  // GithubRepo represents all versions of an Oya pack stored in a git repository on Github.com.
    25  type GithubRepo struct {
    26  	repoUris   []string
    27  	basePath   string
    28  	packPath   string
    29  	importPath types.ImportPath
    30  }
    31  
    32  // AvailableVersions returns a sorted list of remotely available pack versions.
    33  func (l *GithubRepo) AvailableVersions() ([]semver.Version, error) {
    34  	versions := make([]semver.Version, 0)
    35  
    36  	r, err := l.clone()
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  	tags, err := r.Tags()
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	err = tags.ForEach(
    45  		func(t *plumbing.Reference) error {
    46  			n := t.Name()
    47  			if n.IsTag() {
    48  				version, ok := l.parseRef(n.Short())
    49  				if ok {
    50  					versions = append(versions, version)
    51  				}
    52  			}
    53  			return nil
    54  		},
    55  	)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	semver.Sort(versions)
    60  	return versions, nil
    61  }
    62  
    63  func (l *GithubRepo) clone() (*git.Repository, error) {
    64  	var lastErr error
    65  	for _, uri := range l.repoUris {
    66  		fs := memfs.New()
    67  		storer := memory.NewStorage()
    68  		repo, err := git.Clone(storer, fs, &git.CloneOptions{
    69  			URL: uri,
    70  		})
    71  		if err == nil {
    72  			return repo, nil
    73  		}
    74  		lastErr = err
    75  
    76  	}
    77  	return nil, lastErr
    78  }
    79  
    80  // LatestVersion returns the latest available pack version based on tags in the remote Github repo.
    81  func (l *GithubRepo) LatestVersion() (pack.Pack, error) {
    82  	versions, err := l.AvailableVersions()
    83  	if err != nil {
    84  		return pack.Pack{}, err
    85  	}
    86  	if len(versions) == 0 {
    87  		return pack.Pack{}, ErrNoTaggedVersions{ImportPath: l.importPath}
    88  	}
    89  	latestVersion := versions[len(versions)-1]
    90  	return l.Version(latestVersion)
    91  }
    92  
    93  // Version returns the specified version of the pack.
    94  // NOTE: It doesn't check if it's available remotely. This may change.
    95  // It is used when loading Oyafiles so we probably shouldn't do it or use a different function there.
    96  func (l *GithubRepo) Version(version semver.Version) (pack.Pack, error) {
    97  	// BUG(bilus): Check if version exists?
    98  	return pack.New(l, version)
    99  }
   100  
   101  // ImportPath returns the pack's import path, e.g. github.com/tooploox/oya-packs/docker.
   102  func (l *GithubRepo) ImportPath() types.ImportPath {
   103  	return l.importPath
   104  }
   105  
   106  // InstallPath returns the local path for the specific pack version.
   107  func (l *GithubRepo) InstallPath(version semver.Version, installDir string) string {
   108  	path := filepath.Join(installDir, l.basePath, l.packPath)
   109  	return fmt.Sprintf("%v@%v", path, version.String())
   110  }
   111  
   112  func (l *GithubRepo) checkout(version semver.Version) (*object.Commit, error) {
   113  	r, err := l.clone()
   114  	if err != nil {
   115  		return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()}
   116  	}
   117  	tree, err := r.Worktree()
   118  	if err != nil {
   119  		return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()}
   120  	}
   121  	err = tree.Checkout(&git.CheckoutOptions{
   122  		Branch: plumbing.NewTagReferenceName(l.makeRef(version)),
   123  	})
   124  	if err != nil {
   125  		return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()}
   126  	}
   127  	ref, err := r.Head()
   128  	if err != nil {
   129  		return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()}
   130  	}
   131  	return r.CommitObject(ref.Hash())
   132  }
   133  
   134  // Install downloads & copies the specified version of the path to the output directory,
   135  // preserving its import path.
   136  // For example, for /home/bilus/.oya output directory and import path github.com/bilus/foo,
   137  // the pack will be extracted to /home/bilus/.oya/github.com/bilus/foo.
   138  func (l *GithubRepo) Install(version semver.Version, installDir string) error {
   139  	commit, err := l.checkout(version)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	fIter, err := commit.Files()
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	sourceBasePath := l.packPath
   150  	targetPath := l.InstallPath(version, installDir)
   151  	log.Printf("Installing pack %v version %v into %q (git tag: %v)", l.ImportPath(), version, targetPath, l.makeRef(version))
   152  
   153  	return fIter.ForEach(func(f *object.File) error {
   154  		if outside, err := l.isOutsidePack(f.Name); outside || err != nil {
   155  			return err // May be nil if outside true.
   156  		}
   157  		relPath, err := filepath.Rel(sourceBasePath, f.Name)
   158  		if err != nil {
   159  			return err
   160  		}
   161  		targetPath := filepath.Join(targetPath, relPath)
   162  		return copyFile(f, targetPath)
   163  	})
   164  }
   165  
   166  func (l *GithubRepo) IsInstalled(version semver.Version, installDir string) (bool, error) {
   167  	fullPath := l.InstallPath(version, installDir)
   168  	_, err := os.Stat(fullPath)
   169  	if err != nil {
   170  		if os.IsNotExist(err) {
   171  			return false, nil
   172  		}
   173  		return false, err
   174  	}
   175  	return true, nil
   176  }
   177  
   178  func (l *GithubRepo) Reqs(version semver.Version) ([]pack.Pack, error) {
   179  	// BUG(bilus): This is a slow way to get requirements for a pack.
   180  	// It involves installing it out to a local directory.
   181  	// But it's also the simplest one. We can optimize by using HTTP
   182  	// access to pull in Oyafile and then parse the Require: section here.
   183  	// It means duplicating the logic including the assumption that the requires
   184  	// will always be stored in Oyafile, rather than a separate file along the lines
   185  	// of go.mod.
   186  
   187  	tempDir, err := ioutil.TempDir("", "oya")
   188  	defer os.RemoveAll(tempDir)
   189  
   190  	err = l.Install(version, tempDir)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	fullPath := l.InstallPath(version, tempDir)
   196  	o, found, err := oyafile.LoadFromDir(fullPath, fullPath)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	if !found {
   201  		return nil, ErrNoRootOyafile{l.importPath, version}
   202  	}
   203  
   204  	// BUG(bilus): This doesn't take Oyafile#Replacements into account.
   205  	// This probably doesn't matter because it's likely meaningless for
   206  	// packs accessed remotely but we may want to revisit it.
   207  
   208  	packs := make([]pack.Pack, len(o.Requires))
   209  	for i, require := range o.Requires {
   210  		repo, err := Open(require.ImportPath)
   211  		if err != nil {
   212  			return nil, err
   213  		}
   214  		pack, err := repo.Version(require.Version)
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  		packs[i] = pack
   219  	}
   220  
   221  	return packs, nil
   222  }
   223  
   224  func copyFile(f *object.File, targetPath string) error {
   225  	err := os.MkdirAll(filepath.Dir(targetPath), os.ModePerm)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	reader, err := f.Reader()
   230  	if err != nil {
   231  		return err
   232  	}
   233  	// BUG(bilus): Copy permissions.
   234  	writer, err := os.OpenFile(targetPath, os.O_RDWR|os.O_CREATE, 0666)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	_, err = io.Copy(writer, reader)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	err = writer.Sync()
   243  	if err != nil {
   244  		return err
   245  	}
   246  	mode, err := f.Mode.ToOSFileMode()
   247  	if err != nil {
   248  		return err
   249  	}
   250  	err = os.Chmod(targetPath, mode)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	return err
   255  }
   256  
   257  func parseImportPath(importPath types.ImportPath) (uris []string, basePath string, packPath string, err error) {
   258  	parts := strings.Split(string(importPath), "/")
   259  	if len(parts) < 3 {
   260  		return nil, "", "", ErrNotGithub{ImportPath: importPath}
   261  	}
   262  	basePath = strings.Join(parts[0:3], "/")
   263  	packPath = strings.Join(parts[3:], "/")
   264  	// Prefer https but fall back on ssh if cannot clone via https
   265  	// as would be the case for private repositories.
   266  	uris = []string{
   267  		fmt.Sprintf("https://%v.git", basePath),
   268  		fmt.Sprintf("git@%s:%s/%s.git", parts[0], parts[1], parts[2]),
   269  	}
   270  	return
   271  }
   272  
   273  func (l *GithubRepo) parseRef(tag string) (semver.Version, bool) {
   274  	if len(l.packPath) > 0 && strings.HasPrefix(tag, l.packPath) {
   275  		tag = tag[len(l.packPath)+1:] // e.g. "pack1/v1.0.0" => v1.0.0
   276  	}
   277  	version, err := semver.Parse(tag)
   278  	return version, err == nil
   279  }
   280  
   281  func (l *GithubRepo) makeRef(version semver.Version) string {
   282  	if len(l.packPath) > 0 {
   283  		return fmt.Sprintf("%v/%v", l.packPath, version.String())
   284  
   285  	} else {
   286  		return fmt.Sprintf("%v", version.String())
   287  	}
   288  }
   289  
   290  func (l *GithubRepo) isOutsidePack(relPath string) (bool, error) {
   291  	r, err := filepath.Rel(l.packPath, relPath)
   292  	if err != nil {
   293  		return false, err
   294  	}
   295  	return strings.Contains(r, ".."), nil
   296  }
   297  
   298  func toErrClone(url string, err error) error {
   299  	if err == transport.ErrAuthenticationRequired {
   300  		return ErrClone{RepoUrl: url, GitMsg: "Repository not found or private"}
   301  	}
   302  	return ErrClone{RepoUrl: url, GitMsg: err.Error()}
   303  }