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]
|
|
}
|