github.com/sc0rp1us/gb@v0.4.1-0.20160319180011-4ba8cf1baa5a/vendor/repo.go (about) 1 package vendor 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/url" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "strings" 14 15 "github.com/constabulary/gb/fileutils" 16 ) 17 18 // RemoteRepo describes a remote dvcs repository. 19 type RemoteRepo interface { 20 21 // Checkout checks out a specific branch, tag, or revision. 22 // The interpretation of these three values is impementation 23 // specific. 24 Checkout(branch, tag, revision string) (WorkingCopy, error) 25 26 // URL returns the URL the clone was taken from. It should 27 // only be called after Clone. 28 URL() string 29 } 30 31 // WorkingCopy represents a local copy of a remote dvcs repository. 32 type WorkingCopy interface { 33 34 // Dir is the root of this working copy. 35 Dir() string 36 37 // Revision returns the revision of this working copy. 38 Revision() (string, error) 39 40 // Branch returns the branch to which this working copy belongs. 41 Branch() (string, error) 42 43 // Destroy removes the working copy and cleans path to the working copy. 44 Destroy() error 45 } 46 47 var ( 48 ghregex = regexp.MustCompile(`^(?P<root>github\.com/([A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`) 49 bbregex = regexp.MustCompile(`^(?P<root>bitbucket\.org/(?P<bitname>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`) 50 lpregex = regexp.MustCompile(`^launchpad.net/([A-Za-z0-9-._]+)(/[A-Za-z0-9-._]+)?(/.+)?`) 51 gcregex = regexp.MustCompile(`^(?P<root>code\.google\.com/[pr]/(?P<project>[a-z0-9\-]+)(\.(?P<subrepo>[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`) 52 genericre = regexp.MustCompile(`^(?P<root>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/~]*?)\.(?P<vcs>bzr|git|hg|svn))([/A-Za-z0-9_.\-]+)*$`) 53 ) 54 55 // DeduceRemoteRepo takes a potential import path and returns a RemoteRepo 56 // representing the remote location of the source of an import path. 57 // Remote repositories can be bare import paths, or urls including a checkout scheme. 58 // If deduction would cause traversal of an insecure host, a message will be 59 // printed and the travelsal path will be ignored. 60 func DeduceRemoteRepo(path string, insecure bool) (RemoteRepo, string, error) { 61 u, err := url.Parse(path) 62 if err != nil { 63 return nil, "", fmt.Errorf("%q is not a valid import path", path) 64 } 65 66 var schemes []string 67 if u.Scheme != "" { 68 schemes = append(schemes, u.Scheme) 69 } 70 71 path = u.Host + u.Path 72 if !regexp.MustCompile(`^([A-Za-z0-9-]+)(\.[A-Za-z0-9-]+)+(/[A-Za-z0-9-_.~]+)*$`).MatchString(path) { 73 return nil, "", fmt.Errorf("%q is not a valid import path", path) 74 } 75 76 switch { 77 case ghregex.MatchString(path): 78 v := ghregex.FindStringSubmatch(path) 79 url := &url.URL{ 80 Host: "github.com", 81 Path: v[2], 82 } 83 repo, err := Gitrepo(url, insecure, schemes...) 84 return repo, v[0][len(v[1]):], err 85 case bbregex.MatchString(path): 86 v := bbregex.FindStringSubmatch(path) 87 url := &url.URL{ 88 Host: "bitbucket.org", 89 Path: v[2], 90 } 91 repo, err := Gitrepo(url, insecure, schemes...) 92 if err == nil { 93 return repo, v[0][len(v[1]):], nil 94 } 95 repo, err = Hgrepo(url, insecure) 96 if err == nil { 97 return repo, v[0][len(v[1]):], nil 98 } 99 return nil, "", fmt.Errorf("unknown repository type") 100 case gcregex.MatchString(path): 101 v := gcregex.FindStringSubmatch(path) 102 url := &url.URL{ 103 Host: "code.google.com", 104 Path: "p/" + v[2], 105 } 106 repo, err := Hgrepo(url, insecure, schemes...) 107 if err == nil { 108 return repo, v[0][len(v[1]):], nil 109 } 110 repo, err = Gitrepo(url, insecure, schemes...) 111 if err == nil { 112 return repo, v[0][len(v[1]):], nil 113 } 114 return nil, "", fmt.Errorf("unknown repository type") 115 case lpregex.MatchString(path): 116 v := lpregex.FindStringSubmatch(path) 117 v = append(v, "", "") 118 if v[2] == "" { 119 // launchpad.net/project" 120 repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%v", v[1])) 121 return repo, "", err 122 } 123 // launchpad.net/project/series" 124 repo, err := Bzrrepo(fmt.Sprintf("https://launchpad.net/%s/%s", v[1], v[2])) 125 return repo, v[3], err 126 } 127 128 // try the general syntax 129 if genericre.MatchString(path) { 130 v := genericre.FindStringSubmatch(path) 131 switch v[5] { 132 case "git": 133 x := strings.SplitN(v[1], "/", 2) 134 url := &url.URL{ 135 Host: x[0], 136 Path: x[1], 137 } 138 repo, err := Gitrepo(url, insecure, schemes...) 139 return repo, v[6], err 140 case "hg": 141 x := strings.SplitN(v[1], "/", 2) 142 url := &url.URL{ 143 Host: x[0], 144 Path: x[1], 145 } 146 repo, err := Hgrepo(url, insecure, schemes...) 147 return repo, v[6], err 148 case "bzr": 149 repo, err := Bzrrepo("https://" + v[1]) 150 return repo, v[6], err 151 default: 152 return nil, "", fmt.Errorf("unknown repository type: %q", v[5]) 153 154 } 155 } 156 157 // no idea, try to resolve as a vanity import 158 importpath, vcs, reporoot, err := ParseMetadata(path, insecure) 159 if err != nil { 160 return nil, "", err 161 } 162 u, err = url.Parse(reporoot) 163 if err != nil { 164 return nil, "", err 165 } 166 extra := path[len(importpath):] 167 switch vcs { 168 case "git": 169 u.Path = u.Path[1:] 170 repo, err := Gitrepo(u, insecure, u.Scheme) 171 return repo, extra, err 172 case "hg": 173 u.Path = u.Path[1:] 174 repo, err := Hgrepo(u, insecure, u.Scheme) 175 return repo, extra, err 176 case "bzr": 177 repo, err := Bzrrepo(reporoot) 178 return repo, extra, err 179 default: 180 return nil, "", fmt.Errorf("unknown repository type: %q", vcs) 181 } 182 } 183 184 // Gitrepo returns a RemoteRepo representing a remote git repository. 185 func Gitrepo(url *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) { 186 if len(schemes) == 0 { 187 schemes = []string{"ssh", "https", "git", "http"} 188 } 189 u, err := probeGitUrl(url, insecure, schemes) 190 if err != nil { 191 return nil, err 192 } 193 return &gitrepo{ 194 url: u, 195 }, nil 196 } 197 198 func probeGitUrl(u *url.URL, insecure bool, schemes []string) (string, error) { 199 git := func(url *url.URL) error { 200 out, err := run("git", "ls-remote", url.String(), "HEAD") 201 if err != nil { 202 return err 203 } 204 205 if !bytes.Contains(out, []byte("HEAD")) { 206 return fmt.Errorf("not a git repo") 207 } 208 return nil 209 } 210 return probe(git, u, insecure, schemes...) 211 } 212 213 func probeHgUrl(u *url.URL, insecure bool, schemes []string) (string, error) { 214 hg := func(url *url.URL) error { 215 _, err := run("hg", "identify", url.String()) 216 return err 217 } 218 return probe(hg, u, insecure, schemes...) 219 } 220 221 func probeBzrUrl(u string) error { 222 bzr := func(url *url.URL) error { 223 _, err := run("bzr", "info", url.String()) 224 return err 225 } 226 url, err := url.Parse(u) 227 if err != nil { 228 return err 229 } 230 _, err = probe(bzr, url, false, "https") 231 return err 232 } 233 234 // probe calls the supplied vcs function to probe a variety of url constructions. 235 // If vcs returns non nil, it is assumed that the url is not a valid repo. 236 func probe(vcs func(*url.URL) error, vcsUrl *url.URL, insecure bool, schemes ...string) (string, error) { 237 var unsuccessful []string 238 for _, scheme := range schemes { 239 240 // make copy of url and apply scheme 241 vcsUrl := *vcsUrl 242 vcsUrl.Scheme = scheme 243 244 switch vcsUrl.Scheme { 245 case "ssh": 246 vcsUrl.User = url.User("git") 247 if err := vcs(&vcsUrl); err == nil { 248 return vcsUrl.String(), nil 249 } 250 case "https": 251 if err := vcs(&vcsUrl); err == nil { 252 return vcsUrl.String(), nil 253 } 254 255 case "http", "git": 256 if !insecure { 257 fmt.Println("skipping insecure protocol:", vcsUrl.String()) 258 continue 259 } 260 if err := vcs(&vcsUrl); err == nil { 261 return vcsUrl.String(), nil 262 } 263 default: 264 return "", fmt.Errorf("unsupported scheme: %v", vcsUrl.Scheme) 265 } 266 unsuccessful = append(unsuccessful, vcsUrl.String()) 267 } 268 return "", fmt.Errorf("vcs probe failed, tried: %s", strings.Join(unsuccessful, ",")) 269 } 270 271 // gitrepo is a git RemoteRepo. 272 type gitrepo struct { 273 274 // remote repository url, see man 1 git-clone 275 url string 276 } 277 278 func (g *gitrepo) URL() string { 279 return g.url 280 } 281 282 // Checkout fetchs the remote branch, tag, or revision. If more than one is 283 // supplied, an error is returned. If the branch is blank, 284 // then the default remote branch will be used. If the branch is "HEAD", an 285 // error will be returned. 286 func (g *gitrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) { 287 if branch == "HEAD" { 288 return nil, fmt.Errorf("cannot update %q as it has been previously fetched with -tag or -revision. Please use gb vendor delete then fetch again.", g.url) 289 } 290 if !atMostOne(branch, tag, revision) { 291 return nil, fmt.Errorf("only one of branch, tag or revision may be supplied") 292 } 293 dir, err := mktmp() 294 if err != nil { 295 return nil, err 296 } 297 wc := workingcopy{ 298 path: dir, 299 } 300 301 args := []string{ 302 "clone", 303 "-q", // silence progress report to stderr 304 g.url, 305 dir, 306 } 307 if branch != "" { 308 args = append(args, "--branch", branch) 309 } 310 311 if _, err := run("git", args...); err != nil { 312 wc.Destroy() 313 return nil, err 314 } 315 316 if revision != "" || tag != "" { 317 if err := runOutPath(os.Stderr, dir, "git", "checkout", "-q", oneOf(revision, tag)); err != nil { 318 wc.Destroy() 319 return nil, err 320 } 321 } 322 323 return &GitClone{wc}, nil 324 } 325 326 type workingcopy struct { 327 path string 328 } 329 330 func (w workingcopy) Dir() string { return w.path } 331 332 func (w workingcopy) Destroy() error { 333 if err := fileutils.RemoveAll(w.path); err != nil { 334 return err 335 } 336 parent := filepath.Dir(w.path) 337 return cleanPath(parent) 338 } 339 340 // GitClone is a git WorkingCopy. 341 type GitClone struct { 342 workingcopy 343 } 344 345 func (g *GitClone) Revision() (string, error) { 346 rev, err := runPath(g.path, "git", "rev-parse", "HEAD") 347 return strings.TrimSpace(string(rev)), err 348 } 349 350 func (g *GitClone) Branch() (string, error) { 351 rev, err := runPath(g.path, "git", "rev-parse", "--abbrev-ref", "HEAD") 352 return strings.TrimSpace(string(rev)), err 353 } 354 355 // Hgrepo returns a RemoteRepo representing a remote git repository. 356 func Hgrepo(u *url.URL, insecure bool, schemes ...string) (RemoteRepo, error) { 357 if len(schemes) == 0 { 358 schemes = []string{"https", "http"} 359 } 360 url, err := probeHgUrl(u, insecure, schemes) 361 if err != nil { 362 return nil, err 363 } 364 return &hgrepo{ 365 url: url, 366 }, nil 367 } 368 369 // hgrepo is a Mercurial repo. 370 type hgrepo struct { 371 372 // remote repository url, see man 1 hg 373 url string 374 } 375 376 func (h *hgrepo) URL() string { return h.url } 377 378 func (h *hgrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) { 379 if !atMostOne(tag, revision) { 380 return nil, fmt.Errorf("only one of tag or revision may be supplied") 381 } 382 dir, err := mktmp() 383 if err != nil { 384 return nil, err 385 } 386 args := []string{ 387 "clone", 388 h.url, 389 dir, 390 "--noninteractive", 391 } 392 393 if branch != "" { 394 args = append(args, "--branch", branch) 395 } 396 if err := runOut(os.Stderr, "hg", args...); err != nil { 397 fileutils.RemoveAll(dir) 398 return nil, err 399 } 400 if revision != "" { 401 if err := runOut(os.Stderr, "hg", "--cwd", dir, "update", "-r", revision); err != nil { 402 fileutils.RemoveAll(dir) 403 return nil, err 404 } 405 } 406 407 return &HgClone{ 408 workingcopy{ 409 path: dir, 410 }, 411 }, nil 412 } 413 414 // HgClone is a mercurial WorkingCopy. 415 type HgClone struct { 416 workingcopy 417 } 418 419 func (h *HgClone) Revision() (string, error) { 420 rev, err := run("hg", "--cwd", h.path, "id", "-i") 421 return strings.TrimSpace(string(rev)), err 422 } 423 424 func (h *HgClone) Branch() (string, error) { 425 rev, err := run("hg", "--cwd", h.path, "branch") 426 return strings.TrimSpace(string(rev)), err 427 } 428 429 // Bzrrepo returns a RemoteRepo representing a remote bzr repository. 430 func Bzrrepo(url string) (RemoteRepo, error) { 431 if err := probeBzrUrl(url); err != nil { 432 return nil, err 433 } 434 return &bzrrepo{ 435 url: url, 436 }, nil 437 } 438 439 // bzrrepo is a bzr RemoteRepo. 440 type bzrrepo struct { 441 442 // remote repository url 443 url string 444 } 445 446 func (b *bzrrepo) URL() string { 447 return b.url 448 } 449 450 func (b *bzrrepo) Checkout(branch, tag, revision string) (WorkingCopy, error) { 451 if !atMostOne(tag, revision) { 452 return nil, fmt.Errorf("only one of tag or revision may be supplied") 453 } 454 dir, err := mktmp() 455 if err != nil { 456 return nil, err 457 } 458 wc := filepath.Join(dir, "wc") 459 if err := runOut(os.Stderr, "bzr", "branch", b.url, wc); err != nil { 460 fileutils.RemoveAll(dir) 461 return nil, err 462 } 463 464 return &BzrClone{ 465 workingcopy{ 466 path: wc, 467 }, 468 }, nil 469 } 470 471 // BzrClone is a bazaar WorkingCopy. 472 type BzrClone struct { 473 workingcopy 474 } 475 476 func (b *BzrClone) Revision() (string, error) { 477 return "1", nil 478 } 479 480 func (b *BzrClone) Branch() (string, error) { 481 return "master", nil 482 } 483 484 func cleanPath(path string) error { 485 if files, _ := ioutil.ReadDir(path); len(files) > 0 || filepath.Base(path) == "src" { 486 return nil 487 } 488 parent := filepath.Dir(path) 489 if err := fileutils.RemoveAll(path); err != nil { 490 return err 491 } 492 return cleanPath(parent) 493 } 494 495 func mkdir(path string) error { 496 return os.MkdirAll(path, 0755) 497 } 498 499 func mktmp() (string, error) { 500 return ioutil.TempDir("", "gb-vendor-") 501 } 502 503 func run(c string, args ...string) ([]byte, error) { 504 var buf bytes.Buffer 505 err := runOut(&buf, c, args...) 506 return buf.Bytes(), err 507 } 508 509 func runOut(w io.Writer, c string, args ...string) error { 510 cmd := exec.Command(c, args...) 511 cmd.Stdin = nil 512 cmd.Stdout = w 513 cmd.Stderr = os.Stderr 514 return cmd.Run() 515 } 516 517 func runPath(path string, c string, args ...string) ([]byte, error) { 518 var buf bytes.Buffer 519 err := runOutPath(&buf, path, c, args...) 520 return buf.Bytes(), err 521 } 522 523 func runOutPath(w io.Writer, path string, c string, args ...string) error { 524 cmd := exec.Command(c, args...) 525 cmd.Dir = path 526 cmd.Stdin = nil 527 cmd.Stdout = w 528 cmd.Stderr = os.Stderr 529 return cmd.Run() 530 } 531 532 // atMostOne returns true if no more than one string supplied is not empty. 533 func atMostOne(args ...string) bool { 534 var c int 535 for _, arg := range args { 536 if arg != "" { 537 c++ 538 } 539 } 540 return c < 2 541 } 542 543 // oneof returns the first non empty string 544 func oneOf(args ...string) string { 545 for _, arg := range args { 546 if arg != "" { 547 return arg 548 } 549 } 550 return "" 551 }