github.com/annwntech/go-micro/v2@v2.9.5/runtime/local/git/git.go (about)

     1  package git
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/go-git/go-git/v5"
    13  	"github.com/go-git/go-git/v5/config"
    14  	"github.com/go-git/go-git/v5/plumbing"
    15  )
    16  
    17  type Gitter interface {
    18  	Clone(repo string) error
    19  	FetchAll(repo string) error
    20  	Checkout(repo, branchOrCommit string) error
    21  	RepoDir(repo string) string
    22  }
    23  
    24  type libGitter struct {
    25  	folder string
    26  }
    27  
    28  func (g libGitter) Clone(repo string) error {
    29  	fold := filepath.Join(g.folder, dirifyRepo(repo))
    30  	exists, err := pathExists(fold)
    31  	if err != nil {
    32  		return err
    33  	}
    34  	if exists {
    35  		return nil
    36  	}
    37  	_, err = git.PlainClone(fold, false, &git.CloneOptions{
    38  		URL:      repo,
    39  		Progress: os.Stdout,
    40  	})
    41  	return err
    42  }
    43  
    44  func (g libGitter) FetchAll(repo string) error {
    45  	repos, err := git.PlainOpen(filepath.Join(g.folder, dirifyRepo(repo)))
    46  	if err != nil {
    47  		return err
    48  	}
    49  	remotes, err := repos.Remotes()
    50  	if err != nil {
    51  		return err
    52  	}
    53  
    54  	err = remotes[0].Fetch(&git.FetchOptions{
    55  		RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"},
    56  		Progress: os.Stdout,
    57  		Depth:    1,
    58  	})
    59  	if err != nil && err != git.NoErrAlreadyUpToDate {
    60  		return err
    61  	}
    62  	return nil
    63  }
    64  
    65  func (g libGitter) Checkout(repo, branchOrCommit string) error {
    66  	if branchOrCommit == "latest" {
    67  		branchOrCommit = "master"
    68  	}
    69  	repos, err := git.PlainOpen(filepath.Join(g.folder, dirifyRepo(repo)))
    70  	if err != nil {
    71  		return err
    72  	}
    73  	worktree, err := repos.Worktree()
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	if plumbing.IsHash(branchOrCommit) {
    79  		return worktree.Checkout(&git.CheckoutOptions{
    80  			Hash:  plumbing.NewHash(branchOrCommit),
    81  			Force: true,
    82  		})
    83  	}
    84  
    85  	return worktree.Checkout(&git.CheckoutOptions{
    86  		Branch: plumbing.NewBranchReferenceName(branchOrCommit),
    87  		Force:  true,
    88  	})
    89  }
    90  
    91  func (g libGitter) RepoDir(repo string) string {
    92  	return filepath.Join(g.folder, dirifyRepo(repo))
    93  }
    94  
    95  type binaryGitter struct {
    96  	folder string
    97  }
    98  
    99  func (g binaryGitter) Clone(repo string) error {
   100  	fold := filepath.Join(g.folder, dirifyRepo(repo), ".git")
   101  	exists, err := pathExists(fold)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	if exists {
   106  		return nil
   107  	}
   108  	fold = filepath.Join(g.folder, dirifyRepo(repo))
   109  	cmd := exec.Command("git", "clone", repo, ".")
   110  
   111  	err = os.MkdirAll(fold, 0777)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	cmd.Dir = fold
   116  	_, err = cmd.Output()
   117  	if err != nil {
   118  		return err
   119  	}
   120  	return err
   121  }
   122  
   123  func (g binaryGitter) FetchAll(repo string) error {
   124  	cmd := exec.Command("git", "fetch", "--all")
   125  	cmd.Dir = filepath.Join(g.folder, dirifyRepo(repo))
   126  	outp, err := cmd.CombinedOutput()
   127  	if err != nil {
   128  		return errors.New(string(outp))
   129  	}
   130  	return err
   131  }
   132  
   133  func (g binaryGitter) Checkout(repo, branchOrCommit string) error {
   134  	if branchOrCommit == "latest" {
   135  		branchOrCommit = "master"
   136  	}
   137  	cmd := exec.Command("git", "checkout", "-f", branchOrCommit)
   138  	cmd.Dir = filepath.Join(g.folder, dirifyRepo(repo))
   139  	outp, err := cmd.CombinedOutput()
   140  	if err != nil {
   141  		return errors.New(string(outp))
   142  	}
   143  	return nil
   144  }
   145  
   146  func (g binaryGitter) RepoDir(repo string) string {
   147  	return filepath.Join(g.folder, dirifyRepo(repo))
   148  }
   149  
   150  func NewGitter(folder string) Gitter {
   151  	if commandExists("git") {
   152  		return binaryGitter{folder}
   153  	}
   154  	return libGitter{folder}
   155  }
   156  
   157  func commandExists(cmd string) bool {
   158  	_, err := exec.LookPath(cmd)
   159  	return err == nil
   160  }
   161  
   162  func dirifyRepo(s string) string {
   163  	s = strings.ReplaceAll(s, "https://", "")
   164  	s = strings.ReplaceAll(s, "/", "-")
   165  	return s
   166  }
   167  
   168  // exists returns whether the given file or directory exists
   169  func pathExists(path string) (bool, error) {
   170  	_, err := os.Stat(path)
   171  	if err == nil {
   172  		return true, nil
   173  	}
   174  	if os.IsNotExist(err) {
   175  		return false, nil
   176  	}
   177  	return true, err
   178  }
   179  
   180  // GetRepoRoot determines the repo root from a full path.
   181  // Returns empty string and no error if not found
   182  func GetRepoRoot(fullPath string) (string, error) {
   183  	// traverse parent directories
   184  	prev := fullPath
   185  	for {
   186  		current := prev
   187  		exists, err := pathExists(filepath.Join(current, ".git"))
   188  		if err != nil {
   189  			return "", err
   190  		}
   191  		if exists {
   192  			return current, nil
   193  		}
   194  		prev = filepath.Dir(current)
   195  		// reached top level, see:
   196  		// https://play.golang.org/p/rDgVdk3suzb
   197  		if current == prev {
   198  			break
   199  		}
   200  	}
   201  	return "", nil
   202  }
   203  
   204  const defaultRepo = "github.com/micro/services"
   205  
   206  // Source is not just git related @todo move
   207  type Source struct {
   208  	// is it a local folder intended for a local runtime?
   209  	Local bool
   210  	// absolute path to service folder in local mode
   211  	FullPath string
   212  	// path of folder to repo root
   213  	// be it local or github repo
   214  	Folder string
   215  	// github ref
   216  	Ref string
   217  	// for cloning purposes
   218  	// blank for local
   219  	Repo string
   220  	// dir to repo root
   221  	// blank for non local
   222  	LocalRepoRoot string
   223  }
   224  
   225  // Name to be passed to RPC call runtime.Create Update Delete
   226  // eg: `helloworld/api`, `crufter/myrepo/helloworld/api`, `localfolder`
   227  func (s *Source) RuntimeName() string {
   228  	if s.Repo == "github.com/micro/services" || s.Repo == "" {
   229  		return s.Folder
   230  	}
   231  	return fmt.Sprintf("%v/%v", strings.ReplaceAll(s.Repo, "github.com/", ""), s.Folder)
   232  }
   233  
   234  // Source to be passed to RPC call runtime.Create Update Delete
   235  // eg: `helloworld`, `github.com/crufter/myrepo/helloworld`, `/path/to/localrepo/localfolder`
   236  func (s *Source) RuntimeSource() string {
   237  	if s.Local {
   238  		return s.FullPath
   239  	}
   240  	if s.Repo == "github.com/micro/services" || s.Repo == "" {
   241  		return s.Folder
   242  	}
   243  	return fmt.Sprintf("%v/%v", s.Repo, s.Folder)
   244  }
   245  
   246  // ParseSource parses a `micro run/update/kill` source.
   247  func ParseSource(source string) (*Source, error) {
   248  	// If github is not present, we got a shorthand for `micro/services`
   249  	if !strings.Contains(source, "github.com") {
   250  		source = "github.com/micro/services/" + source
   251  	}
   252  	if !strings.Contains(source, "@") {
   253  		source += "@latest"
   254  	}
   255  	ret := &Source{}
   256  	refs := strings.Split(source, "@")
   257  	ret.Ref = refs[1]
   258  	parts := strings.Split(refs[0], "/")
   259  	ret.Repo = strings.Join(parts[0:3], "/")
   260  	if len(parts) > 1 {
   261  		ret.Folder = strings.Join(parts[3:], "/")
   262  	}
   263  
   264  	return ret, nil
   265  }
   266  
   267  // ParseSourceLocal detects and handles local pathes too
   268  // workdir should be used only from the CLI @todo better interface for this function.
   269  // PathExistsFunc exists only for testing purposes, to make the function side effect free.
   270  func ParseSourceLocal(workDir, source string, pathExistsFunc ...func(path string) (bool, error)) (*Source, error) {
   271  	var pexists func(string) (bool, error)
   272  	if len(pathExistsFunc) == 0 {
   273  		pexists = pathExists
   274  	} else {
   275  		pexists = pathExistsFunc[0]
   276  	}
   277  	var localFullPath string
   278  	if len(workDir) > 0 {
   279  		localFullPath = filepath.Join(workDir, source)
   280  	} else {
   281  		localFullPath = source
   282  	}
   283  	if exists, err := pexists(localFullPath); err == nil && exists {
   284  		localRepoRoot, err := GetRepoRoot(localFullPath)
   285  		if err != nil {
   286  			return nil, err
   287  		}
   288  		var folder string
   289  		// If the local repo root is a top level folder, we are not in a git repo.
   290  		// In this case, we should take the last folder as folder name.
   291  		if localRepoRoot == "" {
   292  			folder = filepath.Base(localFullPath)
   293  		} else {
   294  			folder = strings.ReplaceAll(localFullPath, localRepoRoot+string(filepath.Separator), "")
   295  		}
   296  
   297  		return &Source{
   298  			Local:         true,
   299  			Folder:        folder,
   300  			FullPath:      localFullPath,
   301  			LocalRepoRoot: localRepoRoot,
   302  			Ref:           "latest", // @todo consider extracting branch from git here
   303  		}, nil
   304  	}
   305  	return ParseSource(source)
   306  }
   307  
   308  // CheckoutSource for the local runtime server
   309  // folder is the folder to check out the source code to
   310  // Modifies source path to set it to checked out repo absolute path locally.
   311  func CheckoutSource(folder string, source *Source) error {
   312  	// if it's a local folder, do nothing
   313  	if exists, err := pathExists(source.FullPath); err == nil && exists {
   314  		return nil
   315  	}
   316  	gitter := NewGitter(folder)
   317  	repo := source.Repo
   318  	if !strings.Contains(repo, "https://") {
   319  		repo = "https://" + repo
   320  	}
   321  	// Always clone, it's idempotent and only clones if needed
   322  	err := gitter.Clone(repo)
   323  	if err != nil {
   324  		return err
   325  	}
   326  	source.FullPath = filepath.Join(gitter.RepoDir(source.Repo), source.Folder)
   327  	return gitter.Checkout(repo, source.Ref)
   328  }
   329  
   330  // code below is not used yet
   331  
   332  var nameExtractRegexp = regexp.MustCompile(`((micro|web)\.Name\(")(.*)("\))`)
   333  
   334  func extractServiceName(fileContent []byte) string {
   335  	hits := nameExtractRegexp.FindAll(fileContent, 1)
   336  	if len(hits) == 0 {
   337  		return ""
   338  	}
   339  	hit := string(hits[0])
   340  	return strings.Split(hit, "\"")[1]
   341  }