351 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			351 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package git
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/go-git/go-git/v5"
 | |
| 	"github.com/go-git/go-git/v5/config"
 | |
| 	"github.com/go-git/go-git/v5/plumbing"
 | |
| )
 | |
| 
 | |
| type Gitter interface {
 | |
| 	Clone(repo string) error
 | |
| 	FetchAll(repo string) error
 | |
| 	Checkout(repo, branchOrCommit string) error
 | |
| 	RepoDir(repo string) string
 | |
| }
 | |
| 
 | |
| type libGitter struct {
 | |
| 	folder string
 | |
| }
 | |
| 
 | |
| func (g libGitter) Clone(repo string) error {
 | |
| 	fold := filepath.Join(g.folder, dirifyRepo(repo))
 | |
| 	exists, err := pathExists(fold)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if exists {
 | |
| 		return nil
 | |
| 	}
 | |
| 	_, err = git.PlainClone(fold, false, &git.CloneOptions{
 | |
| 		URL:      repo,
 | |
| 		Progress: os.Stdout,
 | |
| 	})
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (g libGitter) FetchAll(repo string) error {
 | |
| 	repos, err := git.PlainOpen(filepath.Join(g.folder, dirifyRepo(repo)))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	remotes, err := repos.Remotes()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	err = remotes[0].Fetch(&git.FetchOptions{
 | |
| 		RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"},
 | |
| 		Progress: os.Stdout,
 | |
| 		Depth:    1,
 | |
| 	})
 | |
| 	if err != nil && err != git.NoErrAlreadyUpToDate {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (g libGitter) Checkout(repo, branchOrCommit string) error {
 | |
| 	if branchOrCommit == "latest" {
 | |
| 		branchOrCommit = "master"
 | |
| 	}
 | |
| 	repos, err := git.PlainOpen(filepath.Join(g.folder, dirifyRepo(repo)))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	worktree, err := repos.Worktree()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	isCommit := func(s string) bool {
 | |
| 		return strings.ContainsAny(s, "0123456789") && len(s) == 40
 | |
| 	}
 | |
| 	if isCommit(branchOrCommit) {
 | |
| 		err = worktree.Checkout(&git.CheckoutOptions{
 | |
| 			Hash:  plumbing.NewHash(branchOrCommit),
 | |
| 			Force: true,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	} else {
 | |
| 		err = worktree.Checkout(&git.CheckoutOptions{
 | |
| 			Branch: plumbing.NewBranchReferenceName(branchOrCommit),
 | |
| 			Force:  true,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (g libGitter) RepoDir(repo string) string {
 | |
| 	return filepath.Join(g.folder, dirifyRepo(repo))
 | |
| }
 | |
| 
 | |
| type binaryGitter struct {
 | |
| 	folder string
 | |
| }
 | |
| 
 | |
| func (g binaryGitter) Clone(repo string) error {
 | |
| 	fold := filepath.Join(g.folder, dirifyRepo(repo), ".git")
 | |
| 	exists, err := pathExists(fold)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if exists {
 | |
| 		return nil
 | |
| 	}
 | |
| 	fold = filepath.Join(g.folder, dirifyRepo(repo))
 | |
| 	cmd := exec.Command("git", "clone", repo, ".")
 | |
| 
 | |
| 	err = os.MkdirAll(fold, 0777)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	cmd.Dir = fold
 | |
| 	_, err = cmd.Output()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (g binaryGitter) FetchAll(repo string) error {
 | |
| 	cmd := exec.Command("git", "fetch", "--all")
 | |
| 	cmd.Dir = filepath.Join(g.folder, dirifyRepo(repo))
 | |
| 	outp, err := cmd.CombinedOutput()
 | |
| 	if err != nil {
 | |
| 		return errors.New(string(outp))
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (g binaryGitter) Checkout(repo, branchOrCommit string) error {
 | |
| 	if branchOrCommit == "latest" {
 | |
| 		branchOrCommit = "master"
 | |
| 	}
 | |
| 	cmd := exec.Command("git", "checkout", "-f", branchOrCommit)
 | |
| 	cmd.Dir = filepath.Join(g.folder, dirifyRepo(repo))
 | |
| 	outp, err := cmd.CombinedOutput()
 | |
| 	if err != nil {
 | |
| 		return errors.New(string(outp))
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (g binaryGitter) RepoDir(repo string) string {
 | |
| 	return filepath.Join(g.folder, dirifyRepo(repo))
 | |
| }
 | |
| 
 | |
| func NewGitter(folder string) Gitter {
 | |
| 	if commandExists("git") {
 | |
| 		return binaryGitter{folder}
 | |
| 	}
 | |
| 	return libGitter{folder}
 | |
| }
 | |
| 
 | |
| func commandExists(cmd string) bool {
 | |
| 	_, err := exec.LookPath(cmd)
 | |
| 	return err == nil
 | |
| }
 | |
| 
 | |
| func dirifyRepo(s string) string {
 | |
| 	s = strings.ReplaceAll(s, "https://", "")
 | |
| 	s = strings.ReplaceAll(s, "/", "-")
 | |
| 	return s
 | |
| }
 | |
| 
 | |
| // exists returns whether the given file or directory exists
 | |
| func pathExists(path string) (bool, error) {
 | |
| 	_, err := os.Stat(path)
 | |
| 	if err == nil {
 | |
| 		return true, nil
 | |
| 	}
 | |
| 	if os.IsNotExist(err) {
 | |
| 		return false, nil
 | |
| 	}
 | |
| 	return true, err
 | |
| }
 | |
| 
 | |
| // GetRepoRoot determines the repo root from a full path.
 | |
| // Returns empty string and no error if not found
 | |
| func GetRepoRoot(fullPath string) (string, error) {
 | |
| 	// traverse parent directories
 | |
| 	prev := fullPath
 | |
| 	for {
 | |
| 		current := prev
 | |
| 		exists, err := pathExists(filepath.Join(current, ".git"))
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		if exists {
 | |
| 			return current, nil
 | |
| 		}
 | |
| 		prev = filepath.Dir(current)
 | |
| 		// reached top level, see:
 | |
| 		// https://play.golang.org/p/rDgVdk3suzb
 | |
| 		if current == prev {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 	return "", nil
 | |
| }
 | |
| 
 | |
| const defaultRepo = "github.com/micro/services"
 | |
| 
 | |
| // Source is not just git related @todo move
 | |
| type Source struct {
 | |
| 	// is it a local folder intended for a local runtime?
 | |
| 	Local bool
 | |
| 	// absolute path to service folder in local mode
 | |
| 	FullPath string
 | |
| 	// path of folder to repo root
 | |
| 	// be it local or github repo
 | |
| 	Folder string
 | |
| 	// github ref
 | |
| 	Ref string
 | |
| 	// for cloning purposes
 | |
| 	// blank for local
 | |
| 	Repo string
 | |
| 	// dir to repo root
 | |
| 	// blank for non local
 | |
| 	LocalRepoRoot string
 | |
| }
 | |
| 
 | |
| // Name to be passed to RPC call runtime.Create Update Delete
 | |
| // eg: `helloworld/api`, `crufter/myrepo/helloworld/api`, `localfolder`
 | |
| func (s *Source) RuntimeName() string {
 | |
| 	if s.Repo == "github.com/micro/services" || s.Repo == "" {
 | |
| 		return s.Folder
 | |
| 	}
 | |
| 	return fmt.Sprintf("%v/%v", strings.ReplaceAll(s.Repo, "github.com/", ""), s.Folder)
 | |
| }
 | |
| 
 | |
| // Source to be passed to RPC call runtime.Create Update Delete
 | |
| // eg: `helloworld`, `github.com/crufter/myrepo/helloworld`, `/path/to/localrepo/localfolder`
 | |
| func (s *Source) RuntimeSource() string {
 | |
| 	if s.Local {
 | |
| 		return s.FullPath
 | |
| 	}
 | |
| 	if s.Repo == "github.com/micro/services" || s.Repo == "" {
 | |
| 		return s.Folder
 | |
| 	}
 | |
| 	return fmt.Sprintf("%v/%v", s.Repo, s.Folder)
 | |
| }
 | |
| 
 | |
| // ParseSource parses a `micro run/update/kill` source.
 | |
| func ParseSource(source string) (*Source, error) {
 | |
| 	// If github is not present, we got a shorthand for `micro/services`
 | |
| 	if !strings.Contains(source, "github.com") {
 | |
| 		source = "github.com/micro/services/" + source
 | |
| 	}
 | |
| 	if !strings.Contains(source, "@") {
 | |
| 		source += "@latest"
 | |
| 	}
 | |
| 	ret := &Source{}
 | |
| 	refs := strings.Split(source, "@")
 | |
| 	ret.Ref = refs[1]
 | |
| 	parts := strings.Split(refs[0], "/")
 | |
| 	ret.Repo = strings.Join(parts[0:3], "/")
 | |
| 	if len(parts) > 1 {
 | |
| 		ret.Folder = strings.Join(parts[3:], "/")
 | |
| 	}
 | |
| 
 | |
| 	return ret, nil
 | |
| }
 | |
| 
 | |
| // ParseSourceLocal detects and handles local pathes too
 | |
| // workdir should be used only from the CLI @todo better interface for this function.
 | |
| // PathExistsFunc exists only for testing purposes, to make the function side effect free.
 | |
| func ParseSourceLocal(workDir, source string, pathExistsFunc ...func(path string) (bool, error)) (*Source, error) {
 | |
| 	var pexists func(string) (bool, error)
 | |
| 	if len(pathExistsFunc) == 0 {
 | |
| 		pexists = pathExists
 | |
| 	} else {
 | |
| 		pexists = pathExistsFunc[0]
 | |
| 	}
 | |
| 	var localFullPath string
 | |
| 	if len(workDir) > 0 {
 | |
| 		localFullPath = filepath.Join(workDir, source)
 | |
| 	} else {
 | |
| 		localFullPath = source
 | |
| 	}
 | |
| 	if exists, err := pexists(localFullPath); err == nil && exists {
 | |
| 		localRepoRoot, err := GetRepoRoot(localFullPath)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		var folder string
 | |
| 		// If the local repo root is a top level folder, we are not in a git repo.
 | |
| 		// In this case, we should take the last folder as folder name.
 | |
| 		if localRepoRoot == "" {
 | |
| 			folder = filepath.Base(localFullPath)
 | |
| 		} else {
 | |
| 			folder = strings.ReplaceAll(localFullPath, localRepoRoot+string(filepath.Separator), "")
 | |
| 		}
 | |
| 
 | |
| 		return &Source{
 | |
| 			Local:         true,
 | |
| 			Folder:        folder,
 | |
| 			FullPath:      localFullPath,
 | |
| 			LocalRepoRoot: localRepoRoot,
 | |
| 			Ref:           "latest", // @todo consider extracting branch from git here
 | |
| 		}, nil
 | |
| 	}
 | |
| 	return ParseSource(source)
 | |
| }
 | |
| 
 | |
| // CheckoutSource for the local runtime server
 | |
| // folder is the folder to check out the source code to
 | |
| // Modifies source path to set it to checked out repo absolute path locally.
 | |
| func CheckoutSource(folder string, source *Source) error {
 | |
| 	// if it's a local folder, do nothing
 | |
| 	if exists, err := pathExists(source.FullPath); err == nil && exists {
 | |
| 		return nil
 | |
| 	}
 | |
| 	gitter := NewGitter(folder)
 | |
| 	repo := source.Repo
 | |
| 	if !strings.Contains(repo, "https://") {
 | |
| 		repo = "https://" + repo
 | |
| 	}
 | |
| 	// Always clone, it's idempotent and only clones if needed
 | |
| 	err := gitter.Clone(repo)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	source.FullPath = filepath.Join(gitter.RepoDir(source.Repo), source.Folder)
 | |
| 	return gitter.Checkout(repo, source.Ref)
 | |
| }
 | |
| 
 | |
| // code below is not used yet
 | |
| 
 | |
| var nameExtractRegexp = regexp.MustCompile(`((micro|web)\.Name\(")(.*)("\))`)
 | |
| 
 | |
| func extractServiceName(fileContent []byte) string {
 | |
| 	hits := nameExtractRegexp.FindAll(fileContent, 1)
 | |
| 	if len(hits) == 0 {
 | |
| 		return ""
 | |
| 	}
 | |
| 	hit := string(hits[0])
 | |
| 	return strings.Split(hit, "\"")[1]
 | |
| }
 |