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 }