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