github.com/comcast/canticle@v0.0.0-20161108184242-c53cface56e8/canticles/vcs.go (about) 1 package canticles 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "os/exec" 8 "path" 9 "regexp" 10 "strings" 11 "sync" 12 13 "golang.org/x/tools/go/vcs" 14 ) 15 16 // TODO: We should just rip out the reliance on tools/vcs. Most of it 17 // is so non functional it is just a headache. 18 19 // A VCS has the ability to create and change the revision of a 20 // package. A VCS is generall resolved using a RepoDiscovery. 21 type VCS interface { 22 Create(rev string) error 23 SetRev(rev string) error 24 GetRev() (string, error) 25 GetBranch() (string, error) 26 UpdateBranch(branch string) (updated bool, update string, err error) 27 GetSource() (string, error) 28 GetRoot() string 29 } 30 31 // GitAtVCS creates a VCS cmd that supports the "git@blah.com:" syntax 32 func GitAtVCS() *vcs.Cmd { 33 v := &vcs.Cmd{} 34 *v = *vcs.ByCmd("git") 35 v.CreateCmd = "clone {repo} {dir}" 36 v.PingCmd = "ls-remote {scheme}@{repo}" 37 v.Scheme = []string{"git"} 38 v.PingCmd = "ls-remote {scheme}@{repo}" 39 return v 40 } 41 42 // A VCSCmd is used to run a VCS command for a repo 43 type VCSCmd struct { 44 Name string 45 Cmd string 46 Args []string 47 ParseRegex *regexp.Regexp 48 } 49 50 // ExecWithArgs overriden from the default 51 func (vc *VCSCmd) ExecWithArgs(repo string, args []string) (string, error) { 52 LogVerbose("Running command: %s %v in dir %s", vc.Cmd, args, repo) 53 cmd := exec.Command(vc.Cmd, args...) 54 cmd.Dir = repo 55 result, err := cmd.CombinedOutput() 56 resultTrim := strings.TrimSpace(string(result)) 57 rev := vc.ParseRegex.FindSubmatch([]byte(resultTrim)) 58 switch { 59 case err != nil: 60 return "", fmt.Errorf("Error getting revision %s", result) 61 case result == nil: 62 return "", errors.New("Error vcs returned no info for revision") 63 case rev == nil: 64 return "", fmt.Errorf("Error parsing cmd result:\n%s", string(result)) 65 default: 66 return string(rev[1]), nil 67 } 68 } 69 70 // Exec executes this command with its arguments and parses them using 71 // regexp. Return an error if the command generates an error or we can 72 // not parse the results. 73 func (vc *VCSCmd) Exec(repo string) (string, error) { 74 return vc.ExecWithArgs(repo, vc.Args) 75 } 76 77 // ExecReplace replaces the value in this commands args with values 78 // from vals and executes the function. 79 func (vc *VCSCmd) ExecReplace(repo string, vals map[string]string) (string, error) { 80 replacements := make([]string, 0, len(vals)*2) 81 for k, v := range vals { 82 replacements = append(replacements, k, v) 83 } 84 replacer := strings.NewReplacer(replacements...) 85 args := make([]string, 0, len(vc.Args)) 86 for _, arg := range vc.Args { 87 args = append(args, replacer.Replace(arg)) 88 } 89 return vc.ExecWithArgs(repo, args) 90 } 91 92 var ( 93 // GitRevCmd attempts to pull the current git from a git 94 // repo. It will fail if the work tree is "dirty". 95 GitRevCmd = &VCSCmd{ 96 Name: "Git", 97 Cmd: "git", 98 Args: []string{"rev-parse", "HEAD"}, 99 ParseRegex: regexp.MustCompile(`(\S+)`), 100 } 101 // SvnRevCmd attempts to pull the current svnversion from a svn 102 // repo. 103 SvnRevCmd = &VCSCmd{ 104 Name: "Subversion", 105 Cmd: "svnversion", 106 ParseRegex: regexp.MustCompile(`^(\S+)$`), // svnversion doesn't have a bad exitcode if not in svndir 107 } 108 // BzrRevCmd attempts to pull the current revno from a Bazaar 109 // repo. 110 BzrRevCmd = &VCSCmd{ 111 Name: "Bazaar", 112 Cmd: "bzr", 113 Args: []string{"revno"}, 114 ParseRegex: regexp.MustCompile(`(\S+)`), 115 } 116 // HgRevCmd attempts to pull the current node from a Mercurial 117 // repo. 118 HgRevCmd = &VCSCmd{ 119 Name: "Mercurial", 120 Cmd: "hg", 121 Args: []string{"log", "--template", "{node}"}, 122 ParseRegex: regexp.MustCompile(`(\S+)`), 123 } 124 // RevCmds is a map of cmd (git, svn, etc.) to 125 // the cmd to parse its revision. 126 RevCmds = map[string]*VCSCmd{ 127 GitRevCmd.Name: GitRevCmd, 128 SvnRevCmd.Name: SvnRevCmd, 129 BzrRevCmd.Name: BzrRevCmd, 130 HgRevCmd.Name: HgRevCmd, 131 } 132 133 // GitRemoteCmd attempts to pull the origin of a git repo. 134 GitRemoteCmd = &VCSCmd{ 135 Name: "Git", 136 Cmd: "git", 137 Args: []string{"ls-remote", "--get-url", "origin"}, 138 ParseRegex: regexp.MustCompile(`^(.+)$`), 139 } 140 // SvnRemoteCmd attempts to pull the origin of a svn repo. 141 SvnRemoteCmd = &VCSCmd{ 142 Name: "Subversion", 143 Cmd: "svn", 144 Args: []string{"info"}, 145 ParseRegex: regexp.MustCompile(`^URL: (.+)$`), // svnversion doesn't have a bad exitcode if not in svndir 146 } 147 // HgRemoteCmd attempts to pull the current default paths from 148 // a Mercurial repo. 149 HgRemoteCmd = &VCSCmd{ 150 Name: "Mercurial", 151 Cmd: "hg", 152 Args: []string{"paths", "default"}, 153 ParseRegex: regexp.MustCompile(`(.+)`), 154 } 155 // RemoteCmds is a map of cmd (git, svn, etc.) to 156 // the cmd to parse its revision. 157 RemoteCmds = map[string]*VCSCmd{ 158 GitRemoteCmd.Name: GitRemoteCmd, 159 SvnRemoteCmd.Name: SvnRemoteCmd, 160 HgRemoteCmd.Name: HgRemoteCmd, 161 } 162 163 // GitBranchCmd is used to get the current branch (if present) 164 GitBranchCmd = &VCSCmd{ 165 Name: "Git", 166 Cmd: "git", 167 Args: []string{"symbolic-ref", "--short", "HEAD"}, 168 ParseRegex: regexp.MustCompile(`(.+)`), 169 } 170 // HgBranchCmd is used to get the current branch (if present) 171 HgBranchCmd = &VCSCmd{ 172 Name: "Mercurial", 173 Cmd: "hg", 174 Args: []string{"id", "-b"}, 175 ParseRegex: regexp.MustCompile(`(.+)`), 176 } 177 // SvnBranchCmd is used to get the current branch (if present) 178 SvnBranchCmd = &VCSCmd{ 179 Name: "Subversion", 180 Cmd: "svn", 181 Args: []string{"info"}, 182 ParseRegex: regexp.MustCompile(`^URL: (.+)$`), 183 } 184 // BzrBranchCmd is used to get the current branch (if present) 185 BzrBranchCmd = &VCSCmd{ 186 Name: "Bazaar", 187 Cmd: "bzr", 188 Args: []string{"version-info"}, 189 ParseRegex: regexp.MustCompile(`branch-nick: (.+)`), 190 } 191 // BranchCmds is a map of cmd (git, svn, etc.) to 192 // the cmd to parse the current branch 193 BranchCmds = map[string]*VCSCmd{ 194 GitBranchCmd.Name: GitBranchCmd, 195 SvnBranchCmd.Name: SvnBranchCmd, 196 HgBranchCmd.Name: HgBranchCmd, 197 BzrBranchCmd.Name: BzrBranchCmd, 198 } 199 ) 200 201 // An UpdateCMD is used to update a local copy of remote branches and 202 // tags. Not relevant for Bazaar and SVN. 203 var ( 204 // GitUpdateCmd is used to update local copy's of remote branches (if present) 205 GitUpdateCmd = &VCSCmd{ 206 Name: "Git", 207 Cmd: "git", 208 Args: []string{"fetch", "--all"}, 209 ParseRegex: regexp.MustCompile(`(.+)`), 210 } 211 // HgUpdateCmd is used used to update local copy's of remote branches (if present) 212 HgUpdateCmd = &VCSCmd{ 213 Name: "Mercurial", 214 Cmd: "hg", 215 Args: []string{"pull"}, 216 ParseRegex: regexp.MustCompile(`(.+)`), 217 } 218 // BranchCmds is a map of cmd (git, svn, etc.) to 219 // the cmd to parse the current branch 220 UpdateCmds = map[string]*VCSCmd{ 221 GitUpdateCmd.Name: GitUpdateCmd, 222 HgUpdateCmd.Name: HgUpdateCmd, 223 } 224 ) 225 226 // A TagSyncCmd is used to set the revision of a git repo to the specified tag or branch. 227 var ( 228 GitTagSyncCmd = &VCSCmd{ 229 Name: "Git", 230 Cmd: "git", 231 Args: []string{"checkout", "{tag}"}, 232 ParseRegex: regexp.MustCompile(`(.+)`), 233 } 234 HgTagSyncCmd = &VCSCmd{ 235 Name: "Mercurial", 236 Cmd: "hg", 237 Args: []string{"update", "-r", "{tag}"}, 238 ParseRegex: regexp.MustCompile(`(.+)`), 239 } 240 BzrTagSyncCmd = &VCSCmd{ 241 Name: "Bazaar", 242 Cmd: "bzr", 243 Args: []string{"update", "-r", "{tag}"}, 244 ParseRegex: regexp.MustCompile(`(Updated to .+|Tree is up)$`), 245 } 246 SvnTagSyncCmd = &VCSCmd{ 247 Name: "Subversion", 248 Cmd: "svn", 249 Args: []string{"update", "--accept", "postpone", "-r", "{tag}"}, 250 ParseRegex: regexp.MustCompile(`(Updated to .+|At revision)`), 251 } 252 TagSyncCmds = map[string]*VCSCmd{ 253 GitTagSyncCmd.Name: GitTagSyncCmd, 254 HgTagSyncCmd.Name: HgTagSyncCmd, 255 BzrTagSyncCmd.Name: BzrTagSyncCmd, 256 SvnTagSyncCmd.Name: SvnTagSyncCmd, 257 } 258 ) 259 260 // A BranchUpdateCmd is used to update a branch (assumed to be already 261 // checked out) against a remote source. These commands will fail if 262 // the git equivalent of a "fast forward merge" can not be completed. 263 // The svn and bzr commands are the same as the tagsync commands. 264 var ( 265 GitBranchUpdateCmd = &VCSCmd{ 266 Name: "Git", 267 Cmd: "git", 268 Args: []string{"pull", "--ff-only", "origin", "{branch}"}, 269 ParseRegex: regexp.MustCompile(`(Already|Updating .+)`), 270 } 271 HgBranchUpdateCmd = &VCSCmd{ 272 Name: "Mercurial", 273 Cmd: "hg", 274 Args: []string{"pull", "-u"}, 275 ParseRegex: regexp.MustCompile(`(added .+|no changes found)$`), 276 } 277 BranchUpdateCmds = map[string]*VCSCmd{ 278 GitBranchUpdateCmd.Name: GitBranchUpdateCmd, 279 HgBranchUpdateCmd.Name: HgBranchUpdateCmd, 280 BzrTagSyncCmd.Name: BzrTagSyncCmd, 281 SvnTagSyncCmd.Name: SvnTagSyncCmd, 282 } 283 BranchUpdatedRegexs = map[string]*regexp.Regexp{ 284 GitBranchUpdateCmd.Name: regexp.MustCompile(`(Updating .+)`), 285 HgBranchUpdateCmd.Name: regexp.MustCompile(`(added .+)`), 286 BzrTagSyncCmd.Name: regexp.MustCompile(`(Updated to .+)`), 287 SvnTagSyncCmd.Name: regexp.MustCompile(`(Updated to .+)`), 288 } 289 ) 290 291 func GetSvnBranches(path string) ([]string, error) { 292 return nil, errors.New("Not implemented") 293 } 294 295 func GetGitBranches(path string) ([]string, error) { 296 cmd := exec.Command("git", "show-ref") 297 cmd.Dir = path 298 result, err := cmd.CombinedOutput() 299 if err != nil { 300 return nil, err 301 } 302 lines := strings.Split(string(result), "\n") 303 var results []string 304 for _, line := range lines { 305 parts := strings.Split(line, " ") 306 if len(parts) > 1 { 307 refName := parts[1] 308 switch { 309 case strings.HasPrefix(refName, "refs/heads/"): 310 results = append(results, strings.TrimPrefix(refName, "refs/heads/")) 311 case strings.HasPrefix(refName, "refs/remotes/"): 312 // refs/remotes/origin/<branchname> 313 remoteRef := strings.SplitN(strings.TrimPrefix(refName, "refs/remotes/"), "/", 2) 314 results = append(results, remoteRef[1]) 315 } 316 } 317 } 318 return results, nil 319 } 320 321 func GetHgBranches(path string) ([]string, error) { 322 return nil, errors.New("Not implemented") 323 } 324 325 func GetBzrBranches(path string) ([]string, error) { 326 return nil, errors.New("Not implemented") 327 } 328 329 var BranchFuncs = map[string]func(string) ([]string, error){ 330 GitBranchCmd.Name: GetGitBranches, 331 SvnBranchCmd.Name: GetSvnBranches, 332 HgBranchCmd.Name: GetHgBranches, 333 BzrBranchCmd.Name: GetBzrBranches, 334 } 335 336 // A LocalVCS uses packages and version control systems available at a 337 // local srcpath to control a local destpath (it copies the files over). 338 type LocalVCS struct { 339 Package string 340 Root string 341 SrcPath string 342 Cmd *vcs.Cmd 343 CurrentRevCmd *VCSCmd // CurrentRevCommand to check the current revision for sourcepath. 344 RemoteCmd *VCSCmd // RemoteCmd to obtain the upstream (remote) for a repo 345 BranchCmd *VCSCmd // BranchCmd to obtains the current branch if on one 346 UpdateCmd *VCSCmd // UpdateCMD is used to pull remote updates but NOT update the local 347 BranchUpdateCmd *VCSCmd // BranchUpdateCmd is used to update a local branch with a remote 348 BranchUpdatedRegex *regexp.Regexp // The regex to examine if an update occured from a branch update cmd 349 SyncCmd *VCSCmd 350 Branches func(path string) ([]string, error) 351 } 352 353 // NewLocalVCS returns a a LocalVCS with CurrentRevCmd initialized 354 // from the cmd's name using RevCmds and RemoteCmd from RemoteCmds. 355 func NewLocalVCS(pkg, root, srcPath string, cmd *vcs.Cmd) *LocalVCS { 356 return &LocalVCS{ 357 Package: pkg, 358 Root: root, 359 SrcPath: srcPath, 360 Cmd: cmd, 361 CurrentRevCmd: RevCmds[cmd.Name], 362 RemoteCmd: RemoteCmds[cmd.Name], 363 BranchCmd: BranchCmds[cmd.Name], 364 UpdateCmd: UpdateCmds[cmd.Name], 365 Branches: BranchFuncs[cmd.Name], 366 BranchUpdateCmd: BranchUpdateCmds[cmd.Name], 367 BranchUpdatedRegex: BranchUpdatedRegexs[cmd.Name], 368 SyncCmd: TagSyncCmds[cmd.Name], 369 } 370 } 371 372 // Create will copy (using a dir copier) the package from srcpath to 373 // destpath and then call set. 374 func (lv *LocalVCS) Create(rev string) error { 375 return lv.SetRev(rev) 376 } 377 378 // SetRev will use the LocalVCS's Cmd.TagSync method to change the 379 // revision of a repo if rev is not the empty string and Cmd is not 380 // nil. 381 func (lv *LocalVCS) SetRev(rev string) error { 382 if lv.Cmd == nil || rev == "" { 383 return nil 384 } 385 src := PackageSource(lv.SrcPath, lv.Root) 386 // Update against remotes if we need too 387 if lv.UpdateCmd != nil { 388 if _, err := lv.UpdateCmd.Exec(src); err != nil { 389 return err 390 } 391 } 392 // For revisions we just want to check it out 393 if err := lv.TagSync(rev); err != nil { 394 return err 395 } 396 return nil 397 } 398 399 func (lv *LocalVCS) TagSync(rev string) error { 400 LogVerbose("Tag sync to: %s", rev) 401 if lv.SyncCmd == nil { 402 return nil 403 } 404 _, err := lv.SyncCmd.ExecReplace(PackageSource(lv.SrcPath, lv.Root), map[string]string{"{tag}": rev}) 405 if err == nil { 406 return nil 407 } 408 LogVerbose("Tag sync failed with err: %s", err.Error()) 409 return lv.Cmd.TagSync(PackageSource(lv.SrcPath, lv.Root), rev) 410 } 411 412 func (lv *LocalVCS) RevIsBranch(rev string) bool { 413 branches, err := lv.Branches(PackageSource(lv.SrcPath, lv.Root)) 414 if err != nil { 415 LogVerbose("Error getting branches %s", err.Error()) 416 return false 417 } 418 LogVerbose("Found branches %v", branches) 419 for _, br := range branches { 420 if rev == br { 421 return true 422 } 423 } 424 return false 425 } 426 427 // GetRev will return current revision of the local repo. If the 428 // local package is not under a VCS it will return nil, nil. If the 429 // vcs can not query the version it will return nil and an error. 430 func (lv *LocalVCS) GetRev() (string, error) { 431 if lv.CurrentRevCmd == nil || lv.Cmd == nil { 432 return "", nil 433 } 434 return lv.CurrentRevCmd.Exec(PackageSource(lv.SrcPath, lv.Root)) 435 436 } 437 438 // GetSource on a LocalVCS will attempt to determine the local repos 439 // upstream source. See the RemoteCmd for each VCS for behavior. 440 func (lv *LocalVCS) GetSource() (string, error) { 441 if lv.RemoteCmd == nil { 442 return "", nil 443 } 444 return lv.RemoteCmd.Exec(PackageSource(lv.SrcPath, lv.Root)) 445 } 446 447 // GetRoot on a LocalVCS will return PackageName for SrcPath 448 func (lv *LocalVCS) GetRoot() string { 449 return lv.Root 450 } 451 452 // GetBranch on a LocalVCS will return the branch (if any) for the 453 // current local repo. If none GetBranch will return an error. 454 func (lv *LocalVCS) GetBranch() (string, error) { 455 return lv.BranchCmd.Exec(PackageSource(lv.SrcPath, lv.Root)) 456 } 457 458 // UpdateBranch will return true if the local branch was updated, 459 // false if not. Error will be non nil if an error occured during the 460 // udpate. 461 func (lv *LocalVCS) UpdateBranch(branch string) (updated bool, update string, err error) { 462 if !lv.RevIsBranch(branch) { 463 return false, fmt.Sprintf("rev %s is not a branch", branch), nil 464 } 465 res, err := lv.BranchUpdateCmd.ExecReplace( 466 PackageSource(lv.SrcPath, lv.Root), 467 map[string]string{"{branch}": branch}, 468 ) 469 if lv.BranchUpdatedRegex.Match([]byte(res)) { 470 return true, res, err 471 } 472 return false, res, err 473 } 474 475 // VCSType represents a prefix to look for, a scheme to ping a path 476 // with and a VCS command to do the pinging. 477 type VCSType struct { 478 Prefix string 479 Scheme string 480 VCS *vcs.Cmd 481 } 482 483 // VCSTypes is the list of VCSType used by GuessVCS 484 var VCSTypes = []VCSType{ 485 {"git+ssh://", "git+ssh", vcs.ByCmd("git")}, 486 {"git://", "git", vcs.ByCmd("git")}, 487 {"git@", "git", GitAtVCS()}, 488 {"ssh://hg@", "ssh", vcs.ByCmd("hg")}, 489 {"svn://", "svn", vcs.ByCmd("svn")}, 490 {"bzr://", "bzr", vcs.ByCmd("bzr")}, 491 {"https://", "https", vcs.ByCmd("git")}, // not so sure this is a good idea 492 } 493 494 // GuessVCS attempts to guess the VCS given a url. This uses the 495 // VCSTypes array, checking for prefixes that match and attempting to 496 // ping the VCS with the given scheme 497 func GuessVCS(url string) *vcs.Cmd { 498 for _, vt := range VCSTypes { 499 if !strings.HasPrefix(url, vt.Prefix) { 500 continue 501 } 502 path := strings.TrimPrefix(url, vt.Scheme) 503 path = strings.TrimPrefix(path, "://") 504 path = strings.TrimPrefix(path, "@") 505 LogVerbose("Pinging path %s with scheme %s for vcs %s", path, vt.Scheme, vt.VCS.Name) 506 if err := vt.VCS.Ping(vt.Scheme, path); err != nil { 507 LogVerbose("Error pinging path %s with scheme %s", path, vt.Scheme) 508 continue 509 } 510 return vt.VCS 511 } 512 return nil 513 } 514 515 // PackageVCS wraps the underlying golang.org/x/tools/go/vcs to 516 // present the interface we need. It also implements the functionality 517 // necessary for SetRev to happen correctly. 518 type PackageVCS struct { 519 Repo *vcs.RepoRoot 520 Gopath string 521 } 522 523 // UpdateBranch will attempt to construct a local vcs and update that. 524 func (pv *PackageVCS) UpdateBranch(branch string) (updated bool, update string, err error) { 525 lv := NewLocalVCS(pv.Repo.Root, pv.Repo.Root, pv.Gopath, pv.Repo.VCS) 526 return lv.UpdateBranch(branch) 527 } 528 529 // Create clones the VCS into the location provided by Repo.Root 530 func (pv *PackageVCS) Create(rev string) error { 531 v := pv.Repo.VCS 532 dir := PackageSource(pv.Gopath, pv.Repo.Root) 533 if err := v.Create(dir, pv.Repo.Repo); err != nil { 534 return err 535 } 536 if rev == "" { 537 return nil 538 } 539 return pv.SetRev(rev) 540 } 541 542 // SetRev changes the revision of the Repo.Root to the value 543 // provided. This also modifies the git based vcs to be able to deal 544 // with non named revisions (sigh). 545 func (pv *PackageVCS) SetRev(rev string) error { 546 lv := NewLocalVCS(pv.Repo.Root, pv.Repo.Root, pv.Gopath, pv.Repo.VCS) 547 return lv.TagSync(rev) 548 } 549 550 // GetRev does not work on remote VCS's and will always return a not 551 // implemented error. 552 func (pv *PackageVCS) GetRev() (string, error) { 553 return "", errors.New("package VCS currently does not support GetRev") 554 } 555 556 // GetRoot will return pv.Repo.Root 557 func (pv *PackageVCS) GetRoot() string { 558 return pv.Repo.Root 559 } 560 561 // GetSource returns the pv.Repo.Repo 562 func (pv *PackageVCS) GetSource() (string, error) { 563 return pv.Repo.Repo, nil 564 } 565 566 // GetBranch does not work on remote VCS for now and will return an 567 // error. 568 func (pv *PackageVCS) GetBranch() (string, error) { 569 return "", errors.New("package VCS currently does not support GetBranch") 570 } 571 572 // A ResolutionFailureError contains status as to whether this is a resolution failure 573 // or of some other type 574 type ResolutionFailureError struct { 575 Err error 576 Pkg string 577 VCS string 578 } 579 580 // A NewResolutionFailureError with the pkg and vcs passed in 581 func NewResolutionFailureError(pkg, vcs string) *ResolutionFailureError { 582 return &ResolutionFailureError{ 583 Err: fmt.Errorf("pkg %s could not be resolved by vcs %s", pkg, vcs), 584 Pkg: pkg, 585 VCS: vcs, 586 } 587 } 588 589 // Error message attached to this vcs error 590 func (re ResolutionFailureError) Error() string { 591 return re.Err.Error() 592 } 593 594 // ResolutionFailureErr will return non nil if a RepoResolver could not 595 // resolve a VCS. 596 func ResolutionFailureErr(err error) *ResolutionFailureError { 597 if err == nil { 598 return nil 599 } 600 if re, ok := err.(*ResolutionFailureError); ok { 601 return re 602 } 603 return nil 604 } 605 606 // RepoResolver provides the mechanisms for resolving a VCS from an 607 // importpath and sourceUrl. 608 type RepoResolver interface { 609 ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) 610 } 611 612 // DefaultRepoResolver attempts to resolve a repo using the go 613 // vcs.RepoRootForImportPath semantics and guessing logic. 614 type DefaultRepoResolver struct { 615 Gopath string 616 } 617 618 // TrimPathToRoot will take import path github.comcast.com/x/tools/go/vcs 619 // and root golang.org/x/tools and create github.comcast.com/x/tools. 620 func TrimPathToRoot(importPath, root string) (string, error) { 621 pathParts := strings.Split(importPath, "/") 622 rootParts := strings.Split(root, "/") 623 624 if len(pathParts) < len(rootParts) { 625 return "", fmt.Errorf("path %s does not contain enough prefix for path %s", importPath, root) 626 } 627 return path.Join(pathParts[0:len(rootParts)]...), nil 628 } 629 630 // ResolveRepo on a default reporesolver is effectively go get wraped 631 // to use the url string. 632 func (dr *DefaultRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) { 633 // We guess our vcs based off our url path if present 634 resolvePath := getResolvePath(importPath) 635 636 LogVerbose("Attempting to use go get vcs for url: %s", resolvePath) 637 vcs.Verbose = Verbose 638 repo, err := vcs.RepoRootForImportPath(resolvePath, true) 639 if err != nil { 640 LogVerbose("Failed creating VCS for url: %s, err: %s", resolvePath, err.Error()) 641 return nil, err 642 } 643 644 // If we found something return non nil 645 repo.Root, err = TrimPathToRoot(importPath, repo.Root) 646 if err != nil { 647 LogVerbose("Failed creating VCS for url: %s, err: %s", resolvePath, err.Error()) 648 return nil, err 649 } 650 v := &PackageVCS{Repo: repo, Gopath: dr.Gopath} 651 LogVerbose("Created VCS for url: %s", resolvePath) 652 return v, nil 653 } 654 655 // RemoteRepoResolver attempts to resolve a repo using the internal 656 // guessing logic for Canticle. 657 type RemoteRepoResolver struct { 658 Gopath string 659 } 660 661 // ResolveRepo on the remoterepo resolver uses our own GuessVCS 662 // method. It mostly looks at protocol cues like svn:// and git@. 663 func (rr *RemoteRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) { 664 resolvePath := getResolvePath(importPath) 665 if dep != nil && dep.SourcePath != "" { 666 resolvePath = getResolvePath(dep.SourcePath) 667 } 668 // Attempt our internal guessing logic first 669 LogVerbose("Attempting to use default resolver for url: %s", resolvePath) 670 v := GuessVCS(resolvePath) 671 if v == nil { 672 return nil, NewResolutionFailureError(importPath, "remote") 673 } 674 675 root := dep.Root 676 if root == "" { 677 root = importPath 678 } 679 pv := &PackageVCS{ 680 Repo: &vcs.RepoRoot{ 681 VCS: v, 682 Repo: resolvePath, 683 Root: root, 684 }, 685 Gopath: rr.Gopath, 686 } 687 return pv, nil 688 } 689 690 func getResolvePath(importPath string) string { 691 if strings.Contains(importPath, "/") { 692 return importPath 693 } else { 694 return importPath + "/" 695 } 696 } 697 698 // LocalRepoResolver will attempt to find local copies of a repo in 699 // LocalPath (treating it like a gopath) and provide VCS systems for 700 // updating them in RemotePath (also treaded like a gopath). 701 type LocalRepoResolver struct { 702 LocalPath string 703 } 704 705 // ResolveRepo on a local resolver may return an error if: 706 // * The local package is not present (no directory) in LocalPath 707 // * The local "package" is a file in localpath 708 // * There was an error stating the directory for the localPkg 709 func (lr *LocalRepoResolver) ResolveRepo(pkg string, dep *CanticleDependency) (VCS, error) { 710 LogVerbose("Finding local vcs for package: %s\n", pkg) 711 fullPath := PackageSource(lr.LocalPath, getResolvePath(pkg)) 712 s, err := os.Stat(fullPath) 713 switch { 714 case err != nil: 715 LogVerbose("Error stating local copy of package: %s %s\n", fullPath, err.Error()) 716 return nil, err 717 case s != nil && s.IsDir(): 718 cmd, root, err := vcs.FromDir(fullPath, lr.LocalPath) 719 if err != nil { 720 LogVerbose("Error with local vcs: %s", err.Error()) 721 return nil, err 722 } 723 root, _ = PackageName(lr.LocalPath, path.Join(lr.LocalPath, root)) 724 v := NewLocalVCS(root, root, lr.LocalPath, cmd) 725 LogVerbose("Created vcs for local pkg: %+v", v) 726 return v, nil 727 default: 728 LogVerbose("Could not resolve local vcs for package: %s", fullPath) 729 return nil, NewResolutionFailureError(pkg, "local") 730 } 731 } 732 733 // CompositeRepoResolver calls the repos in resolvers in order, 734 // discarding errors and returning the first VCS found. 735 type CompositeRepoResolver struct { 736 Resolvers []RepoResolver 737 } 738 739 // ResolveRepo for the composite attempts its sub Resolvers in order 740 // ignoring any errors. If all resolvers fail a ResolutionFailureError 741 // will be returned. 742 func (cr *CompositeRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) { 743 for _, r := range cr.Resolvers { 744 vcs, err := r.ResolveRepo(importPath, dep) 745 if vcs != nil && err == nil { 746 return vcs, nil 747 } 748 } 749 return nil, NewResolutionFailureError(importPath, "composite") 750 } 751 752 type resolve struct { 753 v VCS 754 err error 755 } 756 757 // MemoizedRepoResolver remembers the results of previously attempted 758 // resolutions and will not attempt the same resolution twice. 759 type MemoizedRepoResolver struct { 760 sync.RWMutex 761 resolvedPaths map[string]*resolve 762 resolver RepoResolver 763 } 764 765 // NewMemoizedRepoResolver creates a memozied version of the passed in 766 // resolver. 767 func NewMemoizedRepoResolver(resolver RepoResolver) *MemoizedRepoResolver { 768 return &MemoizedRepoResolver{ 769 resolvedPaths: make(map[string]*resolve), 770 resolver: resolver, 771 } 772 } 773 774 // ResolveRepo on a MemoizedRepoResolver will cache the results of its 775 // child resolver. 776 func (mr *MemoizedRepoResolver) ResolveRepo(importPath string, dep *CanticleDependency) (VCS, error) { 777 mr.RLock() 778 r := mr.resolvedPaths[importPath] 779 mr.RUnlock() 780 if r != nil { 781 return r.v, r.err 782 } 783 784 v, err := mr.resolver.ResolveRepo(importPath, dep) 785 mr.Lock() 786 mr.resolvedPaths[importPath] = &resolve{v, err} 787 mr.Unlock() 788 return v, err 789 }