pax_global_header00006660000000000000000000000064131555206130014513gustar00rootroot0000000000000052 comment=6f1c6d150500e452704e9863f68c2559f58616bf vcs-1.12.0/000077500000000000000000000000001315552061300123675ustar00rootroot00000000000000vcs-1.12.0/.gitignore000066400000000000000000000004121315552061300143540ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof vcs-1.12.0/.travis.yml000066400000000000000000000013571315552061300145060ustar00rootroot00000000000000language: go go: - 1.6.x - 1.7.x - 1.8.x - 1.9.x - master before_script: - git version - svn --version # Setting sudo access to false will let Travis CI use containers rather than # VMs to run the tests. For more details see: # - http://docs.travis-ci.com/user/workers/container-based-infrastructure/ # - http://docs.travis-ci.com/user/workers/standard-infrastructure/ sudo: false script: - make setup - make test notifications: webhooks: urls: - https://webhooks.gitter.im/e/06e3328629952dabe3e0 on_success: change # options: [always|never|change] default: always on_failure: always # options: [always|never|change] default: always on_start: never # options: [always|never|change] default: always vcs-1.12.0/CHANGELOG.md000066400000000000000000000104551315552061300142050ustar00rootroot00000000000000# Changelog ## 1.12.0 (2017-09-11) ### Changed - #79: Include the error context in the error string (thanks @guywithnose) - #80: Bump the Go versions for Travis CI testing (thanks @AlekSi) ## 1.11.1 (2017-04-28) ### Fixed - #76: Fix submodule handling for Windows (thanks @m0j0hn) ## 1.11.0 (2017-03-23) ### Added - #65: Exposed CmdFromDir function (thanks @erizocosmico) ### Changed - #69: Updated testing for Go 1.8 ### Fixed - #64: Testing fatal error if bzr not installed (thanks @kevinburke) ## 1.10.2 (2017-01-24) ### Fixed - #63: Remove extra quotes in submodule export (thanks @dt) ## 1.10.1 (2017-01-18) ### Fixed - #62: Added windows testing via appveyor and fixed issues under windows. ## 1.10.0 (2017-01-09) ### Added - #60: Handle Git submodules (thanks @sdboyer) - #61: Add gometalinter to testing ## 1.9.0 (2016-11-18) ### Added - #50: Auto-detect remotes with file:// prefix. - #59: Testing against Go 1.7 ### Changed - Removed auto-detection for Google Code as the service is deprecated - Added auto-detection of git.openstack.org ### Fixed - #53: Git not fetching tags off branch ## 1.8.0 (2016-06-29) ### Added - #43: Detect when tool (e.g., git, svn, etc) not installed - #49: Detect access denied and not found situations ### Changed - #48: Updated Go Report Gard url to new format - Refactored SVN handling to detect when not in a top level directory - Updating tagging to v[SemVer] structure for compatibility with other tools. ### Fixed - #45: Fixed hg's update method so that it pulls from remote before updates ## 1.7.0 (2016-05-05) - Adds a glide.yaml file with some limited information. - Implements #37: Ability to export source as a directory. - Implements #36: Get current version-ish with Current method. This returns a branch (if on tip) or equivalent tip, a tag if on a tag, or a revision if on an individual revision. Note, the tip of branch is VCS specific so usage may require detecting VCS type. ## 1.6.1 (2016-04-27) - Fixed #30: tags from commit should not have ^{} appended (seen in git) - Fixed #29: isDetachedHead fails with non-english locales (git) - Fixed #33: Access denied and not found http errors causing xml parsing errors ## 1.6.0 (2016-04-18) - Issue #26: Added Init method to initialize a repo at the local location (thanks tony). - Issue #19: Added method to retrieve tags for a commit. - Issue #24: Reworked errors returned from common methods. Now differing VCS implementations return the same errors. The original VCS specific error is available on the error. See the docs for more details. - Issue #25: Export the function RunFromDir which runs VCS commands from the root of the local directory. This is useful for those that want to build and extend on top of the vcs package (thanks tony). - Issue #22: Added Ping command to test if remote location is present and accessible. ## 1.5.1 (2016-03-23) - Fixing bug parsing some Git commit dates. ## 1.5.0 (2016-03-22) - Add Travis CI testing for Go 1.6. - Issue #17: Add CommitInfo method allowing for a common way to get commit metadata from all VCS. - Autodetect types that have git@ or hg@ users. - Autodetect git+ssh, bzr+ssh, git, and svn+ssh scheme urls. - On Bitbucket for ssh style URLs retrieve the type from the URL. This allows for private repo type detection. - Issue #14: Autodetect ssh/scp style urls (thanks chonthu). ## 1.4.1 (2016-03-07) - Fixes #16: some windows situations are unable to create parent directory. ## 1.4.0 (2016-02-15) - Adding support for IBM JazzHub. ## 1.3.1 (2016-01-27) - Issue #12: Failed to checkout Bzr repo when parent directory didn't exist (thanks cyrilleverrier). ## 1.3.0 (2015-11-09) - Issue #9: Added Date method to get the date/time of latest commit (thanks kamilchm). ## 1.2.0 (2015-10-29) - Adding IsDirty method to detect a checkout with uncommitted changes. ## 1.1.4 (2015-10-28) - Fixed #8: Git IsReference not detecting branches that have not been checked out yet. ## 1.1.3 (2015-10-21) - Fixing issue where there are multiple go-import statements for go redirects ## 1.1.2 (2015-10-20) - Fixes #7: hg not checking out code when Get is called ## 1.1.1 (2015-10-20) - Issue #6: Allow VCS commands to be run concurrently. ## 1.1.0 (2015-10-19) - #5: Added output of failed command to returned errors. ## 1.0.0 (2015-10-06) - Initial release. vcs-1.12.0/LICENSE.txt000066400000000000000000000021061315552061300142110ustar00rootroot00000000000000The Masterminds Copyright (C) 2014-2015, Matt Butcher and Matt Farina Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vcs-1.12.0/Makefile000066400000000000000000000017171315552061300140350ustar00rootroot00000000000000.PHONY: setup setup: go get -u gopkg.in/alecthomas/gometalinter.v1 gometalinter.v1 --install .PHONY: test test: validate lint @echo "==> Running tests" go test -v .PHONY: validate validate: # misspell finds the work adresář (used in bzr.go) as a mispelling of # address. It finds adres. An issue has been filed at # https://github.com/client9/misspell/issues/99. In the meantime adding # adres to the ignore list. @echo "==> Running static validations" @gometalinter.v1 \ --disable-all \ --linter "misspell:misspell -i adres -j 1 {path}/*.go:PATH:LINE:COL:MESSAGE" \ --enable deadcode \ --severity deadcode:error \ --enable gofmt \ --enable gosimple \ --enable ineffassign \ --enable misspell \ --enable vet \ --tests \ --vendor \ --deadline 60s \ ./... || exit_code=1 .PHONY: lint lint: @echo "==> Running linters" @gometalinter.v1 \ --disable-all \ --enable golint \ --vendor \ --deadline 60s \ ./... || : vcs-1.12.0/README.md000066400000000000000000000042611315552061300136510ustar00rootroot00000000000000# VCS Repository Management for Go Manage repos in varying version control systems with ease through a common interface. [![Build Status](https://travis-ci.org/Masterminds/vcs.svg)](https://travis-ci.org/Masterminds/vcs) [![GoDoc](https://godoc.org/github.com/Masterminds/vcs?status.png)](https://godoc.org/github.com/Masterminds/vcs) [![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/vcs)](https://goreportcard.com/report/github.com/Masterminds/vcs) [![Build status](https://ci.appveyor.com/api/projects/status/vg3cjc561q2trobm?svg=true&passingText=windows%20build%20passing&failingText=windows%20build%20failing)](https://ci.appveyor.com/project/mattfarina/vcs) ## Quick Usage Quick usage: remote := "https://github.com/Masterminds/vcs" local, _ := ioutil.TempDir("", "go-vcs") repo, err := NewRepo(remote, local) In this case `NewRepo` will detect the VCS is Git and return a `GitRepo`. All of the repos implement the `Repo` interface with a common set of features between them. ## Supported VCS Git, SVN, Bazaar (Bzr), and Mercurial (Hg) are currently supported. They each have their own type (e.g., `GitRepo`) that follow a simple naming pattern. Each type implements the `Repo` interface and has a constructor (e.g., `NewGitRepo`). The constructors have the same signature as `NewRepo`. ## Features - Clone or checkout a repository depending on the version control system. - Pull updates to a repository. - Get the currently checked out commit id. - Checkout a commit id, branch, or tag (depending on the availability in the VCS). - Get a list of tags and branches in the VCS. - Check if a string value is a valid reference within the VCS. - More... For more details see [the documentation](https://godoc.org/github.com/Masterminds/vcs). ## Motivation The package `golang.org/x/tools/go/vcs` provides some valuable functionality for working with packages in repositories in varying source control management systems. That package, while useful and well tested, is designed with a specific purpose in mind. Our uses went beyond the scope of that package. To implement our scope we built a package that went beyond the functionality and scope of `golang.org/x/tools/go/vcs`. vcs-1.12.0/appveyor.yml000066400000000000000000000005641315552061300147640ustar00rootroot00000000000000 version: build-{build}.{branch} clone_folder: C:\gopath\src\github.com\Masterminds\vcs shallow_clone: true environment: GOPATH: C:\gopath platform: - x64 install: - go version - go env - choco install -y bzr - set PATH=C:\Program Files (x86)\Bazaar;%PATH% - bzr --version build_script: - go install -v ./... test_script: - go test -v deploy: off vcs-1.12.0/bzr.go000066400000000000000000000235671315552061300135300ustar00rootroot00000000000000package vcs import ( "fmt" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" "time" ) var bzrDetectURL = regexp.MustCompile("parent branch: (?P.+)\n") // NewBzrRepo creates a new instance of BzrRepo. The remote and local directories // need to be passed in. func NewBzrRepo(remote, local string) (*BzrRepo, error) { ins := depInstalled("bzr") if !ins { return nil, NewLocalError("bzr is not installed", nil, "") } ltype, err := DetectVcsFromFS(local) // Found a VCS other than Bzr. Need to report an error. if err == nil && ltype != Bzr { return nil, ErrWrongVCS } r := &BzrRepo{} r.setRemote(remote) r.setLocalPath(local) r.Logger = Logger // With the other VCS we can check if the endpoint locally is different // from the one configured internally. But, with Bzr you can't. For example, // if you do `bzr branch https://launchpad.net/govcstestbzrrepo` and then // use `bzr info` to get the parent branch you'll find it set to // http://bazaar.launchpad.net/~mattfarina/govcstestbzrrepo/trunk/. Notice // the change from https to http and the path chance. // Here we set the remote to be the local one if none is passed in. if err == nil && r.CheckLocal() && remote == "" { c := exec.Command("bzr", "info") c.Dir = local c.Env = envForDir(c.Dir) out, err := c.CombinedOutput() if err != nil { return nil, NewLocalError("Unable to retrieve local repo information", err, string(out)) } m := bzrDetectURL.FindStringSubmatch(string(out)) // If no remote was passed in but one is configured for the locally // checked out Bzr repo use that one. if m[1] != "" { r.setRemote(m[1]) } } return r, nil } // BzrRepo implements the Repo interface for the Bzr source control. type BzrRepo struct { base } // Vcs retrieves the underlying VCS being implemented. func (s BzrRepo) Vcs() Type { return Bzr } // Get is used to perform an initial clone of a repository. func (s *BzrRepo) Get() error { basePath := filepath.Dir(filepath.FromSlash(s.LocalPath())) if _, err := os.Stat(basePath); os.IsNotExist(err) { err = os.MkdirAll(basePath, 0755) if err != nil { return NewLocalError("Unable to create directory", err, "") } } out, err := s.run("bzr", "branch", s.Remote(), s.LocalPath()) if err != nil { return NewRemoteError("Unable to get repository", err, string(out)) } return nil } // Init initializes a bazaar repository at local location. func (s *BzrRepo) Init() error { out, err := s.run("bzr", "init", s.LocalPath()) // There are some windows cases where bazaar cannot create the parent // directory if it does not already exist, to the location it's trying // to create the repo. Catch that error and try to handle it. if err != nil && s.isUnableToCreateDir(err) { basePath := filepath.Dir(filepath.FromSlash(s.LocalPath())) if _, err := os.Stat(basePath); os.IsNotExist(err) { err = os.MkdirAll(basePath, 0755) if err != nil { return NewLocalError("Unable to initialize repository", err, "") } out, err = s.run("bzr", "init", s.LocalPath()) if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } } else if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } // Update performs a Bzr pull and update to an existing checkout. func (s *BzrRepo) Update() error { out, err := s.RunFromDir("bzr", "pull") if err != nil { return NewRemoteError("Unable to update repository", err, string(out)) } out, err = s.RunFromDir("bzr", "update") if err != nil { return NewRemoteError("Unable to update repository", err, string(out)) } return nil } // UpdateVersion sets the version of a package currently checked out via Bzr. func (s *BzrRepo) UpdateVersion(version string) error { out, err := s.RunFromDir("bzr", "update", "-r", version) if err != nil { return NewLocalError("Unable to update checked out version", err, string(out)) } return nil } // Version retrieves the current version. func (s *BzrRepo) Version() (string, error) { out, err := s.RunFromDir("bzr", "revno", "--tree") if err != nil { return "", NewLocalError("Unable to retrieve checked out version", err, string(out)) } return strings.TrimSpace(string(out)), nil } // Current returns the current version-ish. This means: // * -1 if on the tip of the branch (this is the Bzr value for HEAD) // * A tag if on a tag // * Otherwise a revision func (s *BzrRepo) Current() (string, error) { tip, err := s.CommitInfo("-1") if err != nil { return "", err } curr, err := s.Version() if err != nil { return "", err } if tip.Commit == curr { return "-1", nil } ts, err := s.TagsFromCommit(curr) if err != nil { return "", err } if len(ts) > 0 { return ts[0], nil } return curr, nil } // Date retrieves the date on the latest commit. func (s *BzrRepo) Date() (time.Time, error) { out, err := s.RunFromDir("bzr", "version-info", "--custom", "--template={date}") if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } t, err := time.Parse(longForm, string(out)) if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } return t, nil } // CheckLocal verifies the local location is a Bzr repo. func (s *BzrRepo) CheckLocal() bool { if _, err := os.Stat(s.LocalPath() + "/.bzr"); err == nil { return true } return false } // Branches returns a list of available branches on the repository. // In Bazaar (Bzr) clones and branches are the same. A different branch will // have a different URL location which we cannot detect from the repo. This // is a little different from other VCS. func (s *BzrRepo) Branches() ([]string, error) { var branches []string return branches, nil } // Tags returns a list of available tags on the repository. func (s *BzrRepo) Tags() ([]string, error) { out, err := s.RunFromDir("bzr", "tags") if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } tags := s.referenceList(string(out), `(?m-s)^(\S+)`) return tags, nil } // IsReference returns if a string is a reference. A reference can be a // commit id or tag. func (s *BzrRepo) IsReference(r string) bool { _, err := s.RunFromDir("bzr", "revno", "-r", r) return err == nil } // IsDirty returns if the checkout has been modified from the checked // out reference. func (s *BzrRepo) IsDirty() bool { out, err := s.RunFromDir("bzr", "diff") return err != nil || len(out) != 0 } // CommitInfo retrieves metadata about a commit. func (s *BzrRepo) CommitInfo(id string) (*CommitInfo, error) { r := "-r" + id out, err := s.RunFromDir("bzr", "log", r, "--log-format=long") if err != nil { return nil, ErrRevisionUnavailable } ci := &CommitInfo{} lines := strings.Split(string(out), "\n") const format = "Mon 2006-01-02 15:04:05 -0700" var track int var trackOn bool // Note, bzr does not appear to use i18m. for i, l := range lines { if strings.HasPrefix(l, "revno:") { ci.Commit = strings.TrimSpace(strings.TrimPrefix(l, "revno:")) } else if strings.HasPrefix(l, "committer:") { ci.Author = strings.TrimSpace(strings.TrimPrefix(l, "committer:")) } else if strings.HasPrefix(l, "timestamp:") { ts := strings.TrimSpace(strings.TrimPrefix(l, "timestamp:")) ci.Date, err = time.Parse(format, ts) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } } else if strings.TrimSpace(l) == "message:" { track = i trackOn = true } else if trackOn && i > track { ci.Message = ci.Message + l } } ci.Message = strings.TrimSpace(ci.Message) // Didn't find the revision if ci.Author == "" { return nil, ErrRevisionUnavailable } return ci, nil } // TagsFromCommit retrieves tags from a commit id. func (s *BzrRepo) TagsFromCommit(id string) ([]string, error) { out, err := s.RunFromDir("bzr", "tags", "-r", id) if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } tags := s.referenceList(string(out), `(?m-s)^(\S+)`) return tags, nil } // Ping returns if remote location is accessible. func (s *BzrRepo) Ping() bool { // Running bzr info is slow. Many of the projects are on launchpad which // has a public 1.0 API we can use. u, err := url.Parse(s.Remote()) if err == nil { if u.Host == "launchpad.net" { try := strings.TrimPrefix(u.Path, "/") // get returns the body and an err. If the status code is not a 200 // an error is returned. Launchpad returns a 404 for a codebase that // does not exist. Otherwise it returns a JSON object describing it. _, er := get("https://api.launchpad.net/1.0/" + try) return er == nil } } // This is the same command that Go itself uses but it's not fast (or fast // enough by my standards). A faster method would be useful. _, err = s.run("bzr", "info", s.Remote()) return err == nil } // ExportDir exports the current revision to the passed in directory. func (s *BzrRepo) ExportDir(dir string) error { out, err := s.RunFromDir("bzr", "export", dir) s.log(out) if err != nil { return NewLocalError("Unable to export source", err, string(out)) } return nil } // Multi-lingual manner check for the VCS error that it couldn't create directory. // https://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev/files/head:/po/ func (s *BzrRepo) isUnableToCreateDir(err error) bool { msg := err.Error() if strings.HasPrefix(msg, fmt.Sprintf("Parent directory of %s does not exist.", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("Nadřazený adresář %s neexistuje.", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("El directorio padre de %s no existe.", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("%s の親ディレクトリがありません。", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("Родительская директория для %s не существует.", s.LocalPath())) { return true } return false } vcs-1.12.0/bzr_test.go000066400000000000000000000163611315552061300145610ustar00rootroot00000000000000package vcs import ( "io/ioutil" "path/filepath" "time" //"log" "os" "testing" ) // Canary test to ensure BzrRepo implements the Repo interface. var _ Repo = &BzrRepo{} // To verify bzr is working we perform integration testing // with a known bzr service. Due to the long time of repeatedly checking out // repos these tests are structured to work together. func TestBzr(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-bzr-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewBzrRepo("https://launchpad.net/govcstestbzrrepo", tempDir+"/govcstestbzrrepo") if err != nil { t.Fatal(err) } if repo.Vcs() != Bzr { t.Error("Bzr is detecting the wrong type") } // Check the basic getters. if repo.Remote() != "https://launchpad.net/govcstestbzrrepo" { t.Error("Remote not set properly") } if repo.LocalPath() != tempDir+"/govcstestbzrrepo" { t.Error("Local disk location not set properly") } //Logger = log.New(os.Stdout, "", log.LstdFlags) // Do an initial clone. err = repo.Get() if err != nil { t.Errorf("Unable to clone Bzr repo. Err was %s", err) } // Verify Bzr repo is a Bzr repo if !repo.CheckLocal() { t.Error("Problem checking out repo or Bzr CheckLocal is not working") } // Test internal lookup mechanism used outside of Bzr specific functionality. ltype, err := DetectVcsFromFS(tempDir + "/govcstestbzrrepo") if err != nil { t.Error("detectVcsFromFS unable to Bzr repo") } if ltype != Bzr { t.Errorf("detectVcsFromFS detected %s instead of Bzr type", ltype) } // Test NewRepo on existing checkout. This should simply provide a working // instance without error based on looking at the local directory. nrepo, nrerr := NewRepo("https://launchpad.net/govcstestbzrrepo", tempDir+"/govcstestbzrrepo") if nrerr != nil { t.Error(nrerr) } // Verify the right oject is returned. It will check the local repo type. if !nrepo.CheckLocal() { t.Error("Wrong version returned from NewRepo") } v, err := repo.Current() if err != nil { t.Errorf("Error trying Bzr Current: %s", err) } if v != "-1" { t.Errorf("Current failed to detect Bzr on tip of branch. Got version: %s", v) } err = repo.UpdateVersion("2") if err != nil { t.Errorf("Unable to update Bzr repo version. Err was %s", err) } // Use Version to verify we are on the right version. v, err = repo.Version() if v != "2" { t.Error("Error checking checked out Bzr version") } if err != nil { t.Error(err) } v, err = repo.Current() if err != nil { t.Errorf("Error trying Bzr Current: %s", err) } if v != "2" { t.Errorf("Current failed to detect Bzr on rev 2 of branch. Got version: %s", v) } // Use Date to verify we are on the right commit. d, err := repo.Date() if d.Format(longForm) != "2015-07-31 09:50:42 -0400" { t.Error("Error checking checked out Bzr commit date") } if err != nil { t.Error(err) } // Perform an update. err = repo.Update() if err != nil { t.Error(err) } v, err = repo.Version() if v != "3" { t.Error("Error checking checked out Bzr version") } if err != nil { t.Error(err) } tags, err := repo.Tags() if err != nil { t.Error(err) } if tags[0] != "1.0.0" { t.Error("Bzr tags is not reporting the correct version") } tags, err = repo.TagsFromCommit("2") if err != nil { t.Error(err) } if len(tags) != 0 { t.Error("Bzr is incorrectly returning tags for a commit") } tags, err = repo.TagsFromCommit("3") if err != nil { t.Error(err) } if len(tags) != 1 || tags[0] != "1.0.0" { t.Error("Bzr is incorrectly returning tags for a commit") } branches, err := repo.Branches() if err != nil { t.Error(err) } if len(branches) != 0 { t.Error("Bzr is incorrectly returning branches") } if !repo.IsReference("1.0.0") { t.Error("Bzr is reporting a reference is not one") } if repo.IsReference("foo") { t.Error("Bzr is reporting a non-existent reference is one") } if repo.IsDirty() { t.Error("Bzr incorrectly reporting dirty") } ci, err := repo.CommitInfo("3") if err != nil { t.Error(err) } if ci.Commit != "3" { t.Error("Bzr.CommitInfo wrong commit id") } if ci.Author != "Matt Farina " { t.Error("Bzr.CommitInfo wrong author") } if ci.Message != "Updated Readme with pointer." { t.Error("Bzr.CommitInfo wrong message") } ti, err := time.Parse(time.RFC1123Z, "Fri, 31 Jul 2015 09:51:37 -0400") if err != nil { t.Error(err) } if !ti.Equal(ci.Date) { t.Error("Bzr.CommitInfo wrong date") } _, err = repo.CommitInfo("asdfasdfasdf") if err != ErrRevisionUnavailable { t.Error("Bzr didn't return expected ErrRevisionUnavailable") } tempDir2, err := ioutil.TempDir("", "go-vcs-bzr-tests-export") if err != nil { t.Fatalf("Error creating temp directory: %s", err) } defer func() { err = os.RemoveAll(tempDir2) if err != nil { t.Error(err) } }() exportDir := filepath.Join(tempDir2, "src") err = repo.ExportDir(exportDir) if err != nil { t.Errorf("Unable to export Bzr repo. Err was %s", err) } _, err = os.Stat(filepath.Join(exportDir, "Readme.md")) if err != nil { t.Errorf("Error checking exported file in Bzr: %s", err) } _, err = os.Stat(filepath.Join(exportDir, string(repo.Vcs()))) if err != nil { if found := os.IsNotExist(err); !found { t.Errorf("Error checking exported metadata in Bzr: %s", err) } } else { t.Error("Error checking Bzr metadata. It exists.") } } func TestBzrCheckLocal(t *testing.T) { // Verify repo.CheckLocal fails for non-Bzr directories. // TestBzr is already checking on a valid repo tempDir, err := ioutil.TempDir("", "go-vcs-bzr-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, _ := NewBzrRepo("", tempDir) if repo.CheckLocal() { t.Error("Bzr CheckLocal does not identify non-Bzr location") } // Test NewRepo when there's no local. This should simply provide a working // instance without error based on looking at the remote localtion. _, nrerr := NewRepo("https://launchpad.net/govcstestbzrrepo", tempDir+"/govcstestbzrrepo") if nrerr != nil { t.Error(nrerr) } } func TestBzrPing(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-bzr-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewBzrRepo("https://launchpad.net/govcstestbzrrepo", tempDir) if err != nil { t.Error(err) } ping := repo.Ping() if !ping { t.Error("Bzr unable to ping working repo") } repo, err = NewBzrRepo("https://launchpad.net/ihopethisneverexistsbecauseitshouldnt", tempDir) if err != nil { t.Error(err) } ping = repo.Ping() if ping { t.Error("Bzr got a ping response from when it should not have") } } func TestBzrInit(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-bzr-tests") repoDir := tempDir + "/repo" if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewBzrRepo(repoDir, repoDir) if err != nil { t.Error(err) } err = repo.Init() if err != nil { t.Error(err) } v, err := repo.Version() if err != nil { t.Error(err) } if v != "0" { t.Errorf("Bzr Init returns wrong version: %s", v) } } vcs-1.12.0/errors.go000066400000000000000000000060651315552061300142410ustar00rootroot00000000000000package vcs import ( "errors" "fmt" ) // The vcs package provides ways to work with errors that hide the underlying // implementation details but make them accessible if needed. For basic errors // that do not have underlying implementation specific details or the underlying // details are not necessary there are errors for comparison. // // For example: // // ci, err := repo.CommitInfo("123") // if err == vcs.ErrRevisionUnavailable { // // The commit id was not available in the VCS. // } // // There are other times where getting the details are more useful. For example, // if you're performing a repo.Get() and an error occurs. In general you'll want // to consistently know it failed. But, you may want to know the underlying // details (opt-in) to them. For those cases there is a different form of error // handling. // // For example: // // err := repo.Get() // if err != nil { // // A RemoteError was returned. This has access to the output of the // // vcs command, original error, and has a consistent cross vcs message. // } // // The errors returned here can be used in type switches to detect the underlying // error. For example: // // switch err.(type) { // case *vcs.RemoteError: // // This an error connecting to a remote system. // } // // For more information on using type switches to detect error types you can // read the Go wiki at https://github.com/golang/go/wiki/Errors var ( // ErrWrongVCS is returned when an action is tried on the wrong VCS. ErrWrongVCS = errors.New("Wrong VCS detected") // ErrCannotDetectVCS is returned when VCS cannot be detected from URI string. ErrCannotDetectVCS = errors.New("Cannot detect VCS") // ErrWrongRemote occurs when the passed in remote does not match the VCS // configured endpoint. ErrWrongRemote = errors.New("The Remote does not match the VCS endpoint") // ErrRevisionUnavailable happens when commit revision information is // unavailable. ErrRevisionUnavailable = errors.New("Revision unavailable") ) // RemoteError is returned when an operation fails against a remote repo type RemoteError struct { vcsError } // NewRemoteError constructs a RemoteError func NewRemoteError(msg string, err error, out string) error { e := &RemoteError{} e.s = msg e.e = err e.o = out return e } // LocalError is returned when a local operation has an error type LocalError struct { vcsError } // NewLocalError constructs a LocalError func NewLocalError(msg string, err error, out string) error { e := &LocalError{} e.s = msg e.e = err e.o = out return e } type vcsError struct { s string e error // The original error o string // The output from executing the command } // Error implements the Error interface func (e *vcsError) Error() string { if e.e == nil { return e.s } return fmt.Sprintf("%s: %v", e.s, e.e) } // Original retrieves the underlying implementation specific error. func (e *vcsError) Original() error { return e.e } // Out retrieves the output of the original command that was run. func (e *vcsError) Out() string { return e.o } vcs-1.12.0/errors_test.go000066400000000000000000000012301315552061300152650ustar00rootroot00000000000000package vcs import ( "errors" "testing" ) func TestNewRemoteError(t *testing.T) { base := errors.New("Foo error") out := "This is a test" msg := "remote error msg" e := NewRemoteError(msg, base, out) switch e.(type) { case *RemoteError: // This is the right error type default: t.Error("Wrong error type returned from NewRemoteError") } } func TestNewLocalError(t *testing.T) { base := errors.New("Foo error") out := "This is a test" msg := "local error msg" e := NewLocalError(msg, base, out) switch e.(type) { case *LocalError: // This is the right error type default: t.Error("Wrong error type returned from NewLocalError") } } vcs-1.12.0/git.go000066400000000000000000000336031315552061300135060ustar00rootroot00000000000000package vcs import ( "bytes" "encoding/xml" "io/ioutil" "os" "os/exec" "path/filepath" "runtime" "strings" "time" ) // NewGitRepo creates a new instance of GitRepo. The remote and local directories // need to be passed in. func NewGitRepo(remote, local string) (*GitRepo, error) { ins := depInstalled("git") if !ins { return nil, NewLocalError("git is not installed", nil, "") } ltype, err := DetectVcsFromFS(local) // Found a VCS other than Git. Need to report an error. if err == nil && ltype != Git { return nil, ErrWrongVCS } r := &GitRepo{} r.setRemote(remote) r.setLocalPath(local) r.RemoteLocation = "origin" r.Logger = Logger // Make sure the local Git repo is configured the same as the remote when // A remote value was passed in. if err == nil && r.CheckLocal() { c := exec.Command("git", "config", "--get", "remote.origin.url") c.Dir = local c.Env = envForDir(c.Dir) out, err := c.CombinedOutput() if err != nil { return nil, NewLocalError("Unable to retrieve local repo information", err, string(out)) } localRemote := strings.TrimSpace(string(out)) if remote != "" && localRemote != remote { return nil, ErrWrongRemote } // If no remote was passed in but one is configured for the locally // checked out Git repo use that one. if remote == "" && localRemote != "" { r.setRemote(localRemote) } } return r, nil } // GitRepo implements the Repo interface for the Git source control. type GitRepo struct { base RemoteLocation string } // Vcs retrieves the underlying VCS being implemented. func (s GitRepo) Vcs() Type { return Git } // Get is used to perform an initial clone of a repository. func (s *GitRepo) Get() error { out, err := s.run("git", "clone", "--recursive", s.Remote(), s.LocalPath()) // There are some windows cases where Git cannot create the parent directory, // if it does not already exist, to the location it's trying to create the // repo. Catch that error and try to handle it. if err != nil && s.isUnableToCreateDir(err) { basePath := filepath.Dir(filepath.FromSlash(s.LocalPath())) if _, err := os.Stat(basePath); os.IsNotExist(err) { err = os.MkdirAll(basePath, 0755) if err != nil { return NewLocalError("Unable to create directory", err, "") } out, err = s.run("git", "clone", s.Remote(), s.LocalPath()) if err != nil { return NewRemoteError("Unable to get repository", err, string(out)) } return err } } else if err != nil { return NewRemoteError("Unable to get repository", err, string(out)) } return nil } // Init initializes a git repository at local location. func (s *GitRepo) Init() error { out, err := s.run("git", "init", s.LocalPath()) // There are some windows cases where Git cannot create the parent directory, // if it does not already exist, to the location it's trying to create the // repo. Catch that error and try to handle it. if err != nil && s.isUnableToCreateDir(err) { basePath := filepath.Dir(filepath.FromSlash(s.LocalPath())) if _, err := os.Stat(basePath); os.IsNotExist(err) { err = os.MkdirAll(basePath, 0755) if err != nil { return NewLocalError("Unable to initialize repository", err, "") } out, err = s.run("git", "init", s.LocalPath()) if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } } else if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } // Update performs an Git fetch and pull to an existing checkout. func (s *GitRepo) Update() error { // Perform a fetch to make sure everything is up to date. out, err := s.RunFromDir("git", "fetch", "--tags", s.RemoteLocation) if err != nil { return NewRemoteError("Unable to update repository", err, string(out)) } // When in a detached head state, such as when an individual commit is checked // out do not attempt a pull. It will cause an error. detached, err := isDetachedHead(s.LocalPath()) if err != nil { return NewLocalError("Unable to update repository", err, "") } if detached { return nil } out, err = s.RunFromDir("git", "pull") if err != nil { return NewRemoteError("Unable to update repository", err, string(out)) } return s.defendAgainstSubmodules() } // UpdateVersion sets the version of a package currently checked out via Git. func (s *GitRepo) UpdateVersion(version string) error { out, err := s.RunFromDir("git", "checkout", version) if err != nil { return NewLocalError("Unable to update checked out version", err, string(out)) } return s.defendAgainstSubmodules() } // defendAgainstSubmodules tries to keep repo state sane in the event of // submodules. Or nested submodules. What a great idea, submodules. func (s *GitRepo) defendAgainstSubmodules() error { // First, update them to whatever they should be, if there should happen to be any. out, err := s.RunFromDir("git", "submodule", "update", "--init", "--recursive") if err != nil { return NewLocalError("Unexpected error while defensively updating submodules", err, string(out)) } // Now, do a special extra-aggressive clean in case changing versions caused // one or more submodules to go away. out, err = s.RunFromDir("git", "clean", "-x", "-d", "-f", "-f") if err != nil { return NewLocalError("Unexpected error while defensively cleaning up after possible derelict submodule directories", err, string(out)) } // Then, repeat just in case there are any nested submodules that went away. out, err = s.RunFromDir("git", "submodule", "foreach", "--recursive", "git", "clean", "-x", "-d", "-f", "-f") if err != nil { return NewLocalError("Unexpected error while defensively cleaning up after possible derelict nested submodule directories", err, string(out)) } return nil } // Version retrieves the current version. func (s *GitRepo) Version() (string, error) { out, err := s.RunFromDir("git", "rev-parse", "HEAD") if err != nil { return "", NewLocalError("Unable to retrieve checked out version", err, string(out)) } return strings.TrimSpace(string(out)), nil } // Current returns the current version-ish. This means: // * Branch name if on the tip of the branch // * Tag if on a tag // * Otherwise a revision id func (s *GitRepo) Current() (string, error) { out, err := s.RunFromDir("git", "symbolic-ref", "HEAD") if err == nil { o := bytes.TrimSpace(bytes.TrimPrefix(out, []byte("refs/heads/"))) return string(o), nil } v, err := s.Version() if err != nil { return "", err } ts, err := s.TagsFromCommit(v) if err != nil { return "", err } if len(ts) > 0 { return ts[0], nil } return v, nil } // Date retrieves the date on the latest commit. func (s *GitRepo) Date() (time.Time, error) { out, err := s.RunFromDir("git", "log", "-1", "--date=iso", "--pretty=format:%cd") if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } t, err := time.Parse(longForm, string(out)) if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } return t, nil } // Branches returns a list of available branches on the RemoteLocation func (s *GitRepo) Branches() ([]string, error) { out, err := s.RunFromDir("git", "show-ref") if err != nil { return []string{}, NewLocalError("Unable to retrieve branches", err, string(out)) } branches := s.referenceList(string(out), `(?m-s)(?:`+s.RemoteLocation+`)/(\S+)$`) return branches, nil } // Tags returns a list of available tags on the RemoteLocation func (s *GitRepo) Tags() ([]string, error) { out, err := s.RunFromDir("git", "show-ref") if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } tags := s.referenceList(string(out), `(?m-s)(?:tags)/(\S+)$`) return tags, nil } // CheckLocal verifies the local location is a Git repo. func (s *GitRepo) CheckLocal() bool { if _, err := os.Stat(s.LocalPath() + "/.git"); err == nil { return true } return false } // IsReference returns if a string is a reference. A reference can be a // commit id, branch, or tag. func (s *GitRepo) IsReference(r string) bool { _, err := s.RunFromDir("git", "rev-parse", "--verify", r) if err == nil { return true } // Some refs will fail rev-parse. For example, a remote branch that has // not been checked out yet. This next step should pickup the other // possible references. _, err = s.RunFromDir("git", "show-ref", r) return err == nil } // IsDirty returns if the checkout has been modified from the checked // out reference. func (s *GitRepo) IsDirty() bool { out, err := s.RunFromDir("git", "diff") return err != nil || len(out) != 0 } // CommitInfo retrieves metadata about a commit. func (s *GitRepo) CommitInfo(id string) (*CommitInfo, error) { fm := `--pretty=format:"%H%an <%ae>%aD%s"` out, err := s.RunFromDir("git", "log", id, fm, "-1") if err != nil { return nil, ErrRevisionUnavailable } cis := struct { Commit string `xml:"commit"` Author string `xml:"author"` Date string `xml:"date"` Message string `xml:"message"` }{} err = xml.Unmarshal(out, &cis) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } t, err := time.Parse("Mon, _2 Jan 2006 15:04:05 -0700", cis.Date) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } ci := &CommitInfo{ Commit: cis.Commit, Author: cis.Author, Date: t, Message: cis.Message, } return ci, nil } // TagsFromCommit retrieves tags from a commit id. func (s *GitRepo) TagsFromCommit(id string) ([]string, error) { // This is imperfect and a better method would be great. var re []string out, err := s.RunFromDir("git", "show-ref", "-d") if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } lines := strings.Split(string(out), "\n") var list []string for _, i := range lines { if strings.HasPrefix(strings.TrimSpace(i), id) { list = append(list, i) } } tags := s.referenceList(strings.Join(list, "\n"), `(?m-s)(?:tags)/(\S+)$`) for _, t := range tags { // Dereferenced tags have ^{} appended to them. re = append(re, strings.TrimSuffix(t, "^{}")) } return re, nil } // Ping returns if remote location is accessible. func (s *GitRepo) Ping() bool { c := exec.Command("git", "ls-remote", s.Remote()) // If prompted for a username and password, which GitHub does for all things // not public, it's considered not available. To make it available the // remote needs to be different. c.Env = mergeEnvLists([]string{"GIT_TERMINAL_PROMPT=0"}, os.Environ()) _, err := c.CombinedOutput() return err == nil } // EscapePathSeparator escapes the path separator by replacing it with several. // Note: this is harmless on Unix, and needed on Windows. func EscapePathSeparator(path string) (string) { switch runtime.GOOS { case `windows`: // On Windows, triple all path separators. // Needed to escape backslash(s) preceding doublequotes, // because of how Windows strings treats backslash+doublequote combo, // and Go seems to be implicitly passing around a doublequoted string on Windows, // so we cannnot use default string instead. // See: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ // e.g., C:\foo\bar\ -> C:\\\foo\\\bar\\\ // used with --prefix, like this: --prefix=C:\foo\bar\ -> --prefix=C:\\\foo\\\bar\\\ return strings.Replace(path, string(os.PathSeparator), string(os.PathSeparator) + string(os.PathSeparator) + string(os.PathSeparator), -1) default: return path } } // ExportDir exports the current revision to the passed in directory. func (s *GitRepo) ExportDir(dir string) error { var path string // Without the trailing / there can be problems. if !strings.HasSuffix(dir, string(os.PathSeparator)) { dir = dir + string(os.PathSeparator) } // checkout-index on some systems, such as some Windows cases, does not // create the parent directory to export into if it does not exist. Explicitly // creating it. err := os.MkdirAll(dir, 0755) if err != nil { return NewLocalError("Unable to create directory", err, "") } path = EscapePathSeparator( dir ) out, err := s.RunFromDir("git", "checkout-index", "-f", "-a", "--prefix="+path) s.log(out) if err != nil { return NewLocalError("Unable to export source", err, string(out)) } // and now, the horror of submodules path = EscapePathSeparator( dir + "$path" + string(os.PathSeparator) ) out, err = s.RunFromDir("git", "submodule", "foreach", "--recursive", "git checkout-index -f -a --prefix="+path) s.log(out) if err != nil { return NewLocalError("Error while exporting submodule sources", err, string(out)) } return nil } // isDetachedHead will detect if git repo is in "detached head" state. func isDetachedHead(dir string) (bool, error) { p := filepath.Join(dir, ".git", "HEAD") contents, err := ioutil.ReadFile(p) if err != nil { return false, err } contents = bytes.TrimSpace(contents) if bytes.HasPrefix(contents, []byte("ref: ")) { return false, nil } return true, nil } // isUnableToCreateDir checks for an error in Init() to see if an error // where the parent directory of the VCS local path doesn't exist. This is // done in a multi-lingual manner. func (s *GitRepo) isUnableToCreateDir(err error) bool { msg := err.Error() if strings.HasPrefix(msg, "could not create work tree dir") || strings.HasPrefix(msg, "不能创建工作区目录") || strings.HasPrefix(msg, "no s'ha pogut crear el directori d'arbre de treball") || strings.HasPrefix(msg, "impossible de créer le répertoire de la copie de travail") || strings.HasPrefix(msg, "kunde inte skapa arbetskatalogen") || (strings.HasPrefix(msg, "Konnte Arbeitsverzeichnis") && strings.Contains(msg, "nicht erstellen")) || (strings.HasPrefix(msg, "작업 디렉터리를") && strings.Contains(msg, "만들 수 없습니다")) { return true } return false } vcs-1.12.0/git_test.go000066400000000000000000000363501315552061300145470ustar00rootroot00000000000000package vcs import ( "fmt" "io/ioutil" "path/filepath" "time" //"log" "os" "testing" ) // Canary test to ensure GitRepo implements the Repo interface. var _ Repo = &GitRepo{} // To verify git is working we perform integration testing // with a known git service. func TestGit(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-git-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewGitRepo("https://github.com/Masterminds/VCSTestRepo", tempDir+"/VCSTestRepo") if err != nil { t.Error(err) } if repo.Vcs() != Git { t.Error("Git is detecting the wrong type") } // Check the basic getters. if repo.Remote() != "https://github.com/Masterminds/VCSTestRepo" { t.Error("Remote not set properly") } if repo.LocalPath() != tempDir+"/VCSTestRepo" { t.Error("Local disk location not set properly") } //Logger = log.New(os.Stdout, "", log.LstdFlags) // Do an initial clone. err = repo.Get() if err != nil { t.Errorf("Unable to clone Git repo. Err was %s", err) } // Verify Git repo is a Git repo if !repo.CheckLocal() { t.Error("Problem checking out repo or Git CheckLocal is not working") } // Test internal lookup mechanism used outside of Git specific functionality. ltype, err := DetectVcsFromFS(tempDir + "/VCSTestRepo") if err != nil { t.Error("detectVcsFromFS unable to Git repo") } if ltype != Git { t.Errorf("detectVcsFromFS detected %s instead of Git type", ltype) } // Test NewRepo on existing checkout. This should simply provide a working // instance without error based on looking at the local directory. nrepo, nrerr := NewRepo("https://github.com/Masterminds/VCSTestRepo", tempDir+"/VCSTestRepo") if nrerr != nil { t.Error(nrerr) } // Verify the right oject is returned. It will check the local repo type. if !nrepo.CheckLocal() { t.Error("Wrong version returned from NewRepo") } // Perform an update. err = repo.Update() if err != nil { t.Error(err) } v, err := repo.Current() if err != nil { t.Errorf("Error trying Git Current: %s", err) } if v != "master" { t.Errorf("Current failed to detect Git on tip of master. Got version: %s", v) } // Set the version using the short hash. err = repo.UpdateVersion("806b07b") if err != nil { t.Errorf("Unable to update Git repo version. Err was %s", err) } // Once a ref has been checked out the repo is in a detached head state. // Trying to pull in an update in this state will cause an error. Update // should cleanly handle this. Pulling on a branch (tested elsewhere) and // skipping that here. err = repo.Update() if err != nil { t.Error(err) } // Use Version to verify we are on the right version. v, err = repo.Version() if v != "806b07b08faa21cfbdae93027904f80174679402" { t.Error("Error checking checked out Git version") } if err != nil { t.Error(err) } v, err = repo.Current() if err != nil { t.Errorf("Error trying Git Current for ref: %s", err) } if v != "806b07b08faa21cfbdae93027904f80174679402" { t.Errorf("Current failed to detect Git on ref of branch. Got version: %s", v) } // Use Date to verify we are on the right commit. d, err := repo.Date() if d.Format(longForm) != "2015-07-29 09:46:39 -0400" { t.Error("Error checking checked out Git commit date") } if err != nil { t.Error(err) } // Verify that we can set the version something other than short hash err = repo.UpdateVersion("master") if err != nil { t.Errorf("Unable to update Git repo version. Err was %s", err) } err = repo.UpdateVersion("806b07b08faa21cfbdae93027904f80174679402") if err != nil { t.Errorf("Unable to update Git repo version. Err was %s", err) } v, err = repo.Version() if v != "806b07b08faa21cfbdae93027904f80174679402" { t.Error("Error checking checked out Git version") } if err != nil { t.Error(err) } tags, err := repo.Tags() if err != nil { t.Error(err) } var hasRelTag bool var hasOffMasterTag bool for _, tv := range tags { if tv == "1.0.0" { hasRelTag = true } else if tv == "off-master-tag" { hasOffMasterTag = true } } if !hasRelTag { t.Error("Git tags unable to find release tag on master") } if !hasOffMasterTag { t.Error("Git tags did not fetch tags not on master") } tags, err = repo.TagsFromCommit("74dd547545b7df4aa285bcec1b54e2b76f726395") if err != nil { t.Error(err) } if len(tags) != 0 { t.Error("Git is incorrectly returning tags for a commit") } tags, err = repo.TagsFromCommit("30605f6ac35fcb075ad0bfa9296f90a7d891523e") if err != nil { t.Error(err) } if len(tags) != 1 || tags[0] != "1.0.0" { t.Error("Git is incorrectly returning tags for a commit") } branches, err := repo.Branches() if err != nil { t.Error(err) } // The branches should be HEAD, master, other, and test. if branches[3] != "test" { t.Error("Git is incorrectly returning branches") } if !repo.IsReference("1.0.0") { t.Error("Git is reporting a reference is not one") } if repo.IsReference("foo") { t.Error("Git is reporting a non-existent reference is one") } if repo.IsDirty() { t.Error("Git incorrectly reporting dirty") } ci, err := repo.CommitInfo("806b07b08faa21cfbdae93027904f80174679402") if err != nil { t.Error(err) } if ci.Commit != "806b07b08faa21cfbdae93027904f80174679402" { t.Error("Git.CommitInfo wrong commit id") } if ci.Author != "Matt Farina " { t.Error("Git.CommitInfo wrong author") } if ci.Message != "Update README.md" { t.Error("Git.CommitInfo wrong message") } ti, err := time.Parse(time.RFC1123Z, "Wed, 29 Jul 2015 09:46:39 -0400") if err != nil { t.Error(err) } if !ti.Equal(ci.Date) { t.Error("Git.CommitInfo wrong date") } _, err = repo.CommitInfo("asdfasdfasdf") if err != ErrRevisionUnavailable { t.Error("Git didn't return expected ErrRevisionUnavailable") } tempDir2, err := ioutil.TempDir("", "go-vcs-git-tests-export") if err != nil { t.Fatalf("Error creating temp directory: %s", err) } defer func() { err = os.RemoveAll(tempDir2) if err != nil { t.Error(err) } }() exportDir := filepath.Join(tempDir2, "src") err = repo.ExportDir(exportDir) if err != nil { t.Errorf("Unable to export Git repo. Err was %s", err) } _, err = os.Stat(filepath.Join(exportDir, "README.md")) if err != nil { t.Errorf("Error checking exported file in Git: %s", err) } _, err = os.Stat(filepath.Join(exportDir, string(repo.Vcs()))) if err != nil { if found := os.IsNotExist(err); !found { t.Errorf("Error checking exported metadata in Git: %s", err) } } else { t.Error("Error checking Git metadata. It exists.") } } func TestGitCheckLocal(t *testing.T) { // Verify repo.CheckLocal fails for non-Git directories. // TestGit is already checking on a valid repo tempDir, err := ioutil.TempDir("", "go-vcs-git-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, _ := NewGitRepo("", tempDir) if repo.CheckLocal() { t.Error("Git CheckLocal does not identify non-Git location") } // Test NewRepo when there's no local. This should simply provide a working // instance without error based on looking at the remote localtion. _, nrerr := NewRepo("https://github.com/Masterminds/VCSTestRepo", tempDir+"/VCSTestRepo") if nrerr != nil { t.Error(nrerr) } } func TestGitPing(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-git-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewGitRepo("https://github.com/Masterminds/VCSTestRepo", tempDir) if err != nil { t.Error(err) } ping := repo.Ping() if !ping { t.Error("Git unable to ping working repo") } repo, err = NewGitRepo("https://github.com/Masterminds/ihopethisneverexistsbecauseitshouldnt", tempDir) if err != nil { t.Error(err) } ping = repo.Ping() if ping { t.Error("Git got a ping response from when it should not have") } } func TestGitInit(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-git-tests") repoDir := tempDir + "/repo" if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewGitRepo(repoDir, repoDir) if err != nil { t.Error(err) } err = repo.Init() if err != nil { t.Error(err) } _, err = repo.RunFromDir("git", "status") if err != nil { t.Error(err) } } func TestGitSubmoduleHandling(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-git-submodule-tests") if err != nil { t.Fatal(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() dumplocal := func(err error) string { if terr, ok := err.(*LocalError); ok { return fmt.Sprintf("msg: %s\norig: %s\nout: %s", terr.Error(), terr.Original(), terr.Out()) } return err.Error() } subdirExists := func(dir ...string) bool { _, err := os.Stat(filepath.Join(append([]string{tempDir}, dir...)...)) return err == nil } // Initial clone should get version with two submodules, each of which have // their own submodule repo, err := NewGitRepo("https://github.com/sdboyer/subm", tempDir) if err != nil { t.Fatal(dumplocal(err)) } err = repo.Get() if err != nil { t.Fatalf("unable to clone Git repo. Err was %s", dumplocal(err)) } // Verify we are on the right version. v, err := repo.Version() if v != "18e3a5f6fc7f6d577e732e7a5ab2caf990efbf8f" { t.Fatalf("did not start from expected rev, tests could fail - bailing out (got %s)", v) } if err != nil { t.Fatal(dumplocal(err)) } if !subdirExists("subm1", ".git") { t.Fatal("subm1 submodule does not exist on initial clone/checkout") } if !subdirExists("subm1", "dep-test", ".git") { t.Fatal("dep-test submodule nested under subm1 does not exist on initial clone/checkout") } if !subdirExists("subm-again", ".git") { t.Fatal("subm-again submodule does not exist on initial clone/checkout") } if !subdirExists("subm-again", "dep-test", ".git") { t.Fatal("dep-test submodule nested under subm-again does not exist on initial clone/checkout") } // Now switch to version with no submodules, make sure they all go away err = repo.UpdateVersion("e677f82015f72ac1c8fafa66b5463163b3597af2") if err != nil { t.Fatalf("checking out needed version failed with err: %s", dumplocal(err)) } if subdirExists("subm1") { t.Fatal("checking out version without submodule did not clean up immediate submodules") } if subdirExists("subm1", "dep-test") { t.Fatal("checking out version without submodule did not clean up nested submodules") } if subdirExists("subm-again") { t.Fatal("checking out version without submodule did not clean up immediate submodules") } if subdirExists("subm-again", "dep-test") { t.Fatal("checking out version without submodule did not clean up nested submodules") } err = repo.UpdateVersion("aaf7aa1bc4c3c682cc530eca8f80417088ee8540") if err != nil { t.Fatalf("checking out needed version failed with err: %s", dumplocal(err)) } if !subdirExists("subm1", ".git") { t.Fatal("checking out version with immediate submodule did not set up git subrepo") } err = repo.UpdateVersion("6cc4669af468f3b4f16e7e96275ad01ade5b522f") if err != nil { t.Fatalf("checking out needed version failed with err: %s", dumplocal(err)) } if !subdirExists("subm1", "dep-test", ".git") { t.Fatal("checking out version with nested submodule did not set up nested git subrepo") } err = repo.UpdateVersion("aaf7aa1bc4c3c682cc530eca8f80417088ee8540") if err != nil { t.Fatalf("checking out needed version failed with err: %s", dumplocal(err)) } if subdirExists("subm1", "dep-test") { t.Fatal("rolling back to version without nested submodule did not clean up the nested submodule") } err = repo.UpdateVersion("18e3a5f6fc7f6d577e732e7a5ab2caf990efbf8f") if err != nil { t.Fatalf("checking out needed version failed with err: %s", dumplocal(err)) } if !subdirExists("subm1", ".git") { t.Fatal("subm1 submodule does not exist after switch from other commit") } if !subdirExists("subm1", "dep-test", ".git") { t.Fatal("dep-test submodule nested under subm1 does not exist after switch from other commit") } if !subdirExists("subm-again", ".git") { t.Fatal("subm-again submodule does not exist after switch from other commit") } if !subdirExists("subm-again", "dep-test", ".git") { t.Fatal("dep-test submodule nested under subm-again does not exist after switch from other commit") } } func TestGitSubmoduleHandling2(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-git-submodule-tests2") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewGitRepo("https://github.com/cloudfoundry/sonde-go", tempDir+"/VCSTestRepo2") if err != nil { t.Error(err) } if repo.Vcs() != Git { t.Error("Git is detecting the wrong type") } // Check the basic getters. if repo.Remote() != "https://github.com/cloudfoundry/sonde-go" { t.Error("Remote not set properly") } if repo.LocalPath() != tempDir+"/VCSTestRepo2" { t.Error("Local disk location not set properly") } //Logger = log.New(os.Stdout, "", log.LstdFlags) // Do an initial clone. err = repo.Get() if err != nil { t.Errorf("Unable to clone Git repo. Err was %s", err) } // Verify Git repo is a Git repo if !repo.CheckLocal() { t.Error("Problem checking out repo or Git CheckLocal is not working") } // Test internal lookup mechanism used outside of Git specific functionality. ltype, err := DetectVcsFromFS(tempDir + "/VCSTestRepo2") if err != nil { t.Error("detectVcsFromFS unable to Git repo") } if ltype != Git { t.Errorf("detectVcsFromFS detected %s instead of Git type", ltype) } // Test NewRepo on existing checkout. This should simply provide a working // instance without error based on looking at the local directory. nrepo, nrerr := NewRepo("https://github.com/cloudfoundry/sonde-go", tempDir+"/VCSTestRepo2") if nrerr != nil { t.Error(nrerr) } // Verify the right oject is returned. It will check the local repo type. if !nrepo.CheckLocal() { t.Error("Wrong version returned from NewRepo") } // Perform an update. err = repo.Update() if err != nil { t.Error(err) } v, err := repo.Current() if err != nil { t.Errorf("Error trying Git Current: %s", err) } if v != "master" { t.Errorf("Current failed to detect Git on tip of master. Got version: %s", v) } tempDir2, err := ioutil.TempDir("", "go-vcs-git-tests-export") if err != nil { t.Fatalf("Error creating temp directory: %s", err) } defer func() { err = os.RemoveAll(tempDir2) if err != nil { t.Error(err) } }() exportDir := filepath.Join(tempDir2, "src") err = repo.ExportDir(exportDir) if err != nil { t.Errorf("Unable to export Git repo. Err was %s", err) } _, err = os.Stat(filepath.Join(exportDir, "README.md")) if err != nil { t.Errorf("Error checking exported file in Git: %s", err) } _, err = os.Stat(filepath.Join( filepath.Join(exportDir, "definitions"), "README.md")) if err != nil { t.Errorf("Error checking exported file in Git: %s", err) } _, err = os.Stat(filepath.Join(exportDir, string(repo.Vcs()))) if err != nil { if found := os.IsNotExist(err); !found { t.Errorf("Error checking exported metadata in Git: %s", err) } } else { t.Error("Error checking Git metadata. It exists.") } } vcs-1.12.0/glide.yaml000066400000000000000000000003121315552061300143330ustar00rootroot00000000000000package: github.com/Masterminds/vcs homepage: https://github.com/Masterminds/vcs license: MIT owners: - name: Matt Farina email: matt@mattfarina.com homepage: https://www.mattfarina.com/ import: [] vcs-1.12.0/hg.go000066400000000000000000000200441315552061300133140ustar00rootroot00000000000000package vcs import ( "encoding/xml" "os" "os/exec" "regexp" "strings" "time" ) var hgDetectURL = regexp.MustCompile("default = (?P.+)\n") // NewHgRepo creates a new instance of HgRepo. The remote and local directories // need to be passed in. func NewHgRepo(remote, local string) (*HgRepo, error) { ins := depInstalled("hg") if !ins { return nil, NewLocalError("hg is not installed", nil, "") } ltype, err := DetectVcsFromFS(local) // Found a VCS other than Hg. Need to report an error. if err == nil && ltype != Hg { return nil, ErrWrongVCS } r := &HgRepo{} r.setRemote(remote) r.setLocalPath(local) r.Logger = Logger // Make sure the local Hg repo is configured the same as the remote when // A remote value was passed in. if err == nil && r.CheckLocal() { // An Hg repo was found so test that the URL there matches // the repo passed in here. c := exec.Command("hg", "paths") c.Dir = local c.Env = envForDir(c.Dir) out, err := c.CombinedOutput() if err != nil { return nil, NewLocalError("Unable to retrieve local repo information", err, string(out)) } m := hgDetectURL.FindStringSubmatch(string(out)) if m[1] != "" && m[1] != remote { return nil, ErrWrongRemote } // If no remote was passed in but one is configured for the locally // checked out Hg repo use that one. if remote == "" && m[1] != "" { r.setRemote(m[1]) } } return r, nil } // HgRepo implements the Repo interface for the Mercurial source control. type HgRepo struct { base } // Vcs retrieves the underlying VCS being implemented. func (s HgRepo) Vcs() Type { return Hg } // Get is used to perform an initial clone of a repository. func (s *HgRepo) Get() error { out, err := s.run("hg", "clone", s.Remote(), s.LocalPath()) if err != nil { return NewRemoteError("Unable to get repository", err, string(out)) } return nil } // Init will initialize a mercurial repository at local location. func (s *HgRepo) Init() error { out, err := s.run("hg", "init", s.LocalPath()) if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } // Update performs a Mercurial pull to an existing checkout. func (s *HgRepo) Update() error { return s.UpdateVersion(``) } // UpdateVersion sets the version of a package currently checked out via Hg. func (s *HgRepo) UpdateVersion(version string) error { out, err := s.RunFromDir("hg", "pull") if err != nil { return NewLocalError("Unable to update checked out version", err, string(out)) } if len(strings.TrimSpace(version)) > 0 { out, err = s.RunFromDir("hg", "update", version) } else { out, err = s.RunFromDir("hg", "update") } if err != nil { return NewLocalError("Unable to update checked out version", err, string(out)) } return nil } // Version retrieves the current version. func (s *HgRepo) Version() (string, error) { out, err := s.RunFromDir("hg", "--debug", "identify") if err != nil { return "", NewLocalError("Unable to retrieve checked out version", err, string(out)) } parts := strings.SplitN(string(out), " ", 2) sha := parts[0] return strings.TrimSpace(sha), nil } // Current returns the current version-ish. This means: // * Branch name if on the tip of the branch // * Tag if on a tag // * Otherwise a revision id func (s *HgRepo) Current() (string, error) { out, err := s.RunFromDir("hg", "branch") if err != nil { return "", err } branch := strings.TrimSpace(string(out)) tip, err := s.CommitInfo("max(branch(" + branch + "))") if err != nil { return "", err } curr, err := s.Version() if err != nil { return "", err } if tip.Commit == curr { return branch, nil } ts, err := s.TagsFromCommit(curr) if err != nil { return "", err } if len(ts) > 0 { return ts[0], nil } return curr, nil } // Date retrieves the date on the latest commit. func (s *HgRepo) Date() (time.Time, error) { version, err := s.Version() if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, "") } out, err := s.RunFromDir("hg", "log", "-r", version, "--template", "{date|isodatesec}") if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } t, err := time.Parse(longForm, string(out)) if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } return t, nil } // CheckLocal verifies the local location is a Git repo. func (s *HgRepo) CheckLocal() bool { if _, err := os.Stat(s.LocalPath() + "/.hg"); err == nil { return true } return false } // Branches returns a list of available branches func (s *HgRepo) Branches() ([]string, error) { out, err := s.RunFromDir("hg", "branches") if err != nil { return []string{}, NewLocalError("Unable to retrieve branches", err, string(out)) } branches := s.referenceList(string(out), `(?m-s)^(\S+)`) return branches, nil } // Tags returns a list of available tags func (s *HgRepo) Tags() ([]string, error) { out, err := s.RunFromDir("hg", "tags") if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } tags := s.referenceList(string(out), `(?m-s)^(\S+)`) return tags, nil } // IsReference returns if a string is a reference. A reference can be a // commit id, branch, or tag. func (s *HgRepo) IsReference(r string) bool { _, err := s.RunFromDir("hg", "log", "-r", r) return err == nil } // IsDirty returns if the checkout has been modified from the checked // out reference. func (s *HgRepo) IsDirty() bool { out, err := s.RunFromDir("hg", "diff") return err != nil || len(out) != 0 } // CommitInfo retrieves metadata about a commit. func (s *HgRepo) CommitInfo(id string) (*CommitInfo, error) { out, err := s.RunFromDir("hg", "log", "-r", id, "--style=xml") if err != nil { return nil, ErrRevisionUnavailable } type Author struct { Name string `xml:",chardata"` Email string `xml:"email,attr"` } type Logentry struct { Node string `xml:"node,attr"` Author Author `xml:"author"` Date string `xml:"date"` Msg string `xml:"msg"` } type Log struct { XMLName xml.Name `xml:"log"` Logs []Logentry `xml:"logentry"` } logs := &Log{} err = xml.Unmarshal(out, &logs) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } if len(logs.Logs) == 0 { return nil, ErrRevisionUnavailable } ci := &CommitInfo{ Commit: logs.Logs[0].Node, Author: logs.Logs[0].Author.Name + " <" + logs.Logs[0].Author.Email + ">", Message: logs.Logs[0].Msg, } if logs.Logs[0].Date != "" { ci.Date, err = time.Parse(time.RFC3339, logs.Logs[0].Date) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } } return ci, nil } // TagsFromCommit retrieves tags from a commit id. func (s *HgRepo) TagsFromCommit(id string) ([]string, error) { // Hg has a single tag per commit. If a second tag is added to a commit a // new commit is created and the tag is attached to that new commit. out, err := s.RunFromDir("hg", "log", "-r", id, "--style=xml") if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } type Logentry struct { Node string `xml:"node,attr"` Tag string `xml:"tag"` } type Log struct { XMLName xml.Name `xml:"log"` Logs []Logentry `xml:"logentry"` } logs := &Log{} err = xml.Unmarshal(out, &logs) if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } if len(logs.Logs) == 0 { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } t := strings.TrimSpace(logs.Logs[0].Tag) if t != "" { return []string{t}, nil } return []string{}, nil } // Ping returns if remote location is accessible. func (s *HgRepo) Ping() bool { _, err := s.run("hg", "identify", s.Remote()) return err == nil } // ExportDir exports the current revision to the passed in directory. func (s *HgRepo) ExportDir(dir string) error { out, err := s.RunFromDir("hg", "archive", dir) s.log(out) if err != nil { return NewLocalError("Unable to export source", err, string(out)) } return nil } vcs-1.12.0/hg_test.go000066400000000000000000000167231315552061300143640ustar00rootroot00000000000000package vcs import ( "io/ioutil" "path/filepath" "strings" "time" //"log" "os" "testing" ) // Canary test to ensure HgRepo implements the Repo interface. var _ Repo = &HgRepo{} // To verify hg is working we perform integration testing // with a known hg service. func TestHg(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-hg-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewHgRepo("https://bitbucket.org/mattfarina/testhgrepo", tempDir+"/testhgrepo") if err != nil { t.Error(err) } if repo.Vcs() != Hg { t.Error("Hg is detecting the wrong type") } // Check the basic getters. if repo.Remote() != "https://bitbucket.org/mattfarina/testhgrepo" { t.Error("Remote not set properly") } if repo.LocalPath() != tempDir+"/testhgrepo" { t.Error("Local disk location not set properly") } //Logger = log.New(os.Stdout, "", log.LstdFlags) // Do an initial clone. err = repo.Get() if err != nil { t.Errorf("Unable to clone Hg repo. Err was %s", err) } // Verify Hg repo is a Hg repo if !repo.CheckLocal() { t.Error("Problem checking out repo or Hg CheckLocal is not working") } // Test internal lookup mechanism used outside of Hg specific functionality. ltype, err := DetectVcsFromFS(tempDir + "/testhgrepo") if err != nil { t.Error("detectVcsFromFS unable to Hg repo") } if ltype != Hg { t.Errorf("detectVcsFromFS detected %s instead of Hg type", ltype) } // Test NewRepo on existing checkout. This should simply provide a working // instance without error based on looking at the local directory. nrepo, nrerr := NewRepo("https://bitbucket.org/mattfarina/testhgrepo", tempDir+"/testhgrepo") if nrerr != nil { t.Error(nrerr) } // Verify the right oject is returned. It will check the local repo type. if !nrepo.CheckLocal() { t.Error("Wrong version returned from NewRepo") } v, err := repo.Current() if err != nil { t.Errorf("Error trying Hg Current: %s", err) } if v != "default" { t.Errorf("Current failed to detect Hg on tip of default. Got version: %s", v) } // Set the version using the short hash. err = repo.UpdateVersion("a5494ba2177f") if err != nil { t.Errorf("Unable to update Hg repo version. Err was %s", err) } // Use Version to verify we are on the right version. v, err = repo.Version() if v != "a5494ba2177ff9ef26feb3c155dfecc350b1a8ef" { t.Errorf("Error checking checked out Hg version: %s", v) } if err != nil { t.Error(err) } v, err = repo.Current() if err != nil { t.Errorf("Error trying Hg Current for ref: %s", err) } if v != "a5494ba2177ff9ef26feb3c155dfecc350b1a8ef" { t.Errorf("Current failed to detect Hg on ref of branch. Got version: %s", v) } // Use Date to verify we are on the right commit. d, err := repo.Date() if err != nil { t.Error(err) } if d.Format(longForm) != "2015-07-30 16:14:08 -0400" { t.Error("Error checking checked out Hg commit date. Got wrong date:", d) } // Perform an update. err = repo.Update() if err != nil { t.Error(err) } v, err = repo.Version() if v != "9c6ccbca73e8a1351c834f33f57f1f7a0329ad35" { t.Errorf("Error checking checked out Hg version: %s", v) } if err != nil { t.Error(err) } tags, err := repo.Tags() if err != nil { t.Error(err) } if tags[1] != "1.0.0" { t.Error("Hg tags is not reporting the correct version") } tags, err = repo.TagsFromCommit("a5494ba2177f") if err != nil { t.Error(err) } if len(tags) != 0 { t.Error("Hg is incorrectly returning tags for a commit") } tags, err = repo.TagsFromCommit("d680e82228d2") if err != nil { t.Error(err) } if len(tags) != 1 || tags[0] != "1.0.0" { t.Error("Hg is incorrectly returning tags for a commit") } branches, err := repo.Branches() if err != nil { t.Error(err) } // The branches should be HEAD, master, and test. if branches[0] != "test" { t.Error("Hg is incorrectly returning branches") } if !repo.IsReference("1.0.0") { t.Error("Hg is reporting a reference is not one") } if !repo.IsReference("test") { t.Error("Hg is reporting a reference is not one") } if repo.IsReference("foo") { t.Error("Hg is reporting a non-existent reference is one") } if repo.IsDirty() { t.Error("Hg incorrectly reporting dirty") } ci, err := repo.CommitInfo("a5494ba2177f") if err != nil { t.Error(err) } if ci.Commit != "a5494ba2177ff9ef26feb3c155dfecc350b1a8ef" { t.Error("Hg.CommitInfo wrong commit id") } if ci.Author != "Matt Farina " { t.Error("Hg.CommitInfo wrong author") } if ci.Message != "A commit" { t.Error("Hg.CommitInfo wrong message") } ti := time.Unix(1438287248, 0) if !ti.Equal(ci.Date) { t.Error("Hg.CommitInfo wrong date") } _, err = repo.CommitInfo("asdfasdfasdf") if err != ErrRevisionUnavailable { t.Error("Hg didn't return expected ErrRevisionUnavailable") } tempDir2, err := ioutil.TempDir("", "go-vcs-hg-tests-export") if err != nil { t.Fatalf("Error creating temp directory: %s", err) } defer func() { err = os.RemoveAll(tempDir2) if err != nil { t.Error(err) } }() exportDir := filepath.Join(tempDir2, "src") err = repo.ExportDir(exportDir) if err != nil { t.Errorf("Unable to export Hg repo. Err was %s", err) } _, err = os.Stat(filepath.Join(exportDir, "Readme.md")) if err != nil { t.Errorf("Error checking exported file in Hg: %s", err) } _, err = os.Stat(filepath.Join(exportDir, string(repo.Vcs()))) if err != nil { if found := os.IsNotExist(err); !found { t.Errorf("Error checking exported metadata in Hg: %s", err) } } else { t.Error("Error checking Hg metadata. It exists.") } } func TestHgCheckLocal(t *testing.T) { // Verify repo.CheckLocal fails for non-Hg directories. // TestHg is already checking on a valid repo tempDir, err := ioutil.TempDir("", "go-vcs-hg-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, _ := NewHgRepo("", tempDir) if repo.CheckLocal() { t.Error("Hg CheckLocal does not identify non-Hg location") } // Test NewRepo when there's no local. This should simply provide a working // instance without error based on looking at the remote localtion. _, nrerr := NewRepo("https://bitbucket.org/mattfarina/testhgrepo", tempDir+"/testhgrepo") if nrerr != nil { t.Error(nrerr) } } func TestHgPing(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-hg-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewHgRepo("https://bitbucket.org/mattfarina/testhgrepo", tempDir) if err != nil { t.Error(err) } ping := repo.Ping() if !ping { t.Error("Hg unable to ping working repo") } repo, err = NewHgRepo("https://bitbucket.org/mattfarina/ihopethisneverexistsbecauseitshouldnt", tempDir) if err != nil { t.Error(err) } ping = repo.Ping() if ping { t.Error("Hg got a ping response from when it should not have") } } func TestHgInit(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-hg-tests") repoDir := tempDir + "/repo" if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewHgRepo(repoDir, repoDir) if err != nil { t.Error(err) } err = repo.Init() if err != nil { t.Error(err) } v, err := repo.Version() if err != nil { t.Error(err) } if !strings.HasPrefix(v, "000000") { t.Errorf("Hg Init reporting wrong initial version: %s", v) } } vcs-1.12.0/repo.go000066400000000000000000000167101315552061300136700ustar00rootroot00000000000000// Package vcs provides the ability to work with varying version control systems // (VCS), also known as source control systems (SCM) though the same interface. // // This package includes a function that attempts to detect the repo type from // the remote URL and return the proper type. For example, // // remote := "https://github.com/Masterminds/vcs" // local, _ := ioutil.TempDir("", "go-vcs") // repo, err := NewRepo(remote, local) // // In this case repo will be a GitRepo instance. NewRepo can detect the VCS for // numerous popular VCS and from the URL. For example, a URL ending in .git // that's not from one of the popular VCS will be detected as a Git repo and // the correct type will be returned. // // If you know the repository type and would like to create an instance of a // specific type you can use one of constructors for a type. They are NewGitRepo, // NewSvnRepo, NewBzrRepo, and NewHgRepo. The definition and usage is the same // as NewRepo. // // Once you have an object implementing the Repo interface the operations are // the same no matter which VCS you're using. There are some caveats. For // example, each VCS has its own version formats that need to be respected and // checkout out branches, if a branch is being worked with, is different in // each VCS. package vcs import ( "fmt" "io/ioutil" "log" "os" "os/exec" "regexp" "strings" "time" ) // Logger is where you can provide a logger, implementing the log.Logger interface, // where verbose output from each VCS will be written. The default logger does // not log data. To log data supply your own logger or change the output location // of the provided logger. var Logger *log.Logger func init() { // Initialize the logger to one that does not actually log anywhere. This is // to be overridden by the package user by setting vcs.Logger to a different // logger. Logger = log.New(ioutil.Discard, "go-vcs", log.LstdFlags) } const longForm = "2006-01-02 15:04:05 -0700" // Type describes the type of VCS type Type string // VCS types const ( NoVCS Type = "" Git Type = "git" Svn Type = "svn" Bzr Type = "bzr" Hg Type = "hg" ) // Repo provides an interface to work with repositories using different source // control systems such as Git, Bzr, Mercurial, and SVN. For implementations // of this interface see BzrRepo, GitRepo, HgRepo, and SvnRepo. type Repo interface { // Vcs retrieves the underlying VCS being implemented. Vcs() Type // Remote retrieves the remote location for a repo. Remote() string // LocalPath retrieves the local file system location for a repo. LocalPath() string // Get is used to perform an initial clone/checkout of a repository. Get() error // Initializes a new repository locally. Init() error // Update performs an update to an existing checkout of a repository. Update() error // UpdateVersion sets the version of a package of a repository. UpdateVersion(string) error // Version retrieves the current version. Version() (string, error) // Current retrieves the current version-ish. This is different from the // Version method. The output could be a branch name if on the tip of a // branch (git), a tag if on a tag, a revision if on a specific revision // that's not the tip of the branch. The values here vary based on the VCS. Current() (string, error) // Date retrieves the date on the latest commit. Date() (time.Time, error) // CheckLocal verifies the local location is of the correct VCS type CheckLocal() bool // Branches returns a list of available branches on the repository. Branches() ([]string, error) // Tags returns a list of available tags on the repository. Tags() ([]string, error) // IsReference returns if a string is a reference. A reference can be a // commit id, branch, or tag. IsReference(string) bool // IsDirty returns if the checkout has been modified from the checked // out reference. IsDirty() bool // CommitInfo retrieves metadata about a commit. CommitInfo(string) (*CommitInfo, error) // TagsFromCommit retrieves tags from a commit id. TagsFromCommit(string) ([]string, error) // Ping returns if remote location is accessible. Ping() bool // RunFromDir executes a command from repo's directory. RunFromDir(cmd string, args ...string) ([]byte, error) // CmdFromDir creates a new command that will be executed from repo's // directory. CmdFromDir(cmd string, args ...string) *exec.Cmd // ExportDir exports the current revision to the passed in directory. ExportDir(string) error } // NewRepo returns a Repo based on trying to detect the source control from the // remote and local locations. The appropriate implementation will be returned // or an ErrCannotDetectVCS if the VCS type cannot be detected. // Note, this function may make calls to the Internet to determind help determine // the VCS. func NewRepo(remote, local string) (Repo, error) { vtype, remote, err := detectVcsFromRemote(remote) // From the remote URL the VCS could not be detected. See if the local // repo contains enough information to figure out the VCS. The reason the // local repo is not checked first is because of the potential for VCS type // switches which will be detected in each of the type builders. if err == ErrCannotDetectVCS { vtype, err = DetectVcsFromFS(local) } if err != nil { return nil, err } switch vtype { case Git: return NewGitRepo(remote, local) case Svn: return NewSvnRepo(remote, local) case Hg: return NewHgRepo(remote, local) case Bzr: return NewBzrRepo(remote, local) } // Should never fall through to here but just in case. return nil, ErrCannotDetectVCS } // CommitInfo contains metadata about a commit. type CommitInfo struct { // The commit id Commit string // Who authored the commit Author string // Date of the commit Date time.Time // Commit message Message string } type base struct { remote, local string Logger *log.Logger } func (b *base) log(v interface{}) { b.Logger.Printf("%s", v) } // Remote retrieves the remote location for a repo. func (b *base) Remote() string { return b.remote } // LocalPath retrieves the local file system location for a repo. func (b *base) LocalPath() string { return b.local } func (b *base) setRemote(remote string) { b.remote = remote } func (b *base) setLocalPath(local string) { b.local = local } func (b base) run(cmd string, args ...string) ([]byte, error) { out, err := exec.Command(cmd, args...).CombinedOutput() b.log(out) if err != nil { err = fmt.Errorf("%s: %s", out, err) } return out, err } func (b *base) CmdFromDir(cmd string, args ...string) *exec.Cmd { c := exec.Command(cmd, args...) c.Dir = b.local c.Env = envForDir(c.Dir) return c } func (b *base) RunFromDir(cmd string, args ...string) ([]byte, error) { c := b.CmdFromDir(cmd, args...) out, err := c.CombinedOutput() return out, err } func (b *base) referenceList(c, r string) []string { var out []string re := regexp.MustCompile(r) for _, m := range re.FindAllStringSubmatch(c, -1) { out = append(out, m[1]) } return out } func envForDir(dir string) []string { env := os.Environ() return mergeEnvLists([]string{"PWD=" + dir}, env) } func mergeEnvLists(in, out []string) []string { NextVar: for _, inkv := range in { k := strings.SplitAfterN(inkv, "=", 2)[0] for i, outkv := range out { if strings.HasPrefix(outkv, k) { out[i] = inkv continue NextVar } } out = append(out, inkv) } return out } func depInstalled(name string) bool { if _, err := exec.LookPath(name); err != nil { return false } return true } vcs-1.12.0/repo_test.go000066400000000000000000000032351315552061300147250ustar00rootroot00000000000000package vcs import ( "fmt" "io/ioutil" "os" "testing" ) func ExampleNewRepo() { remote := "https://github.com/Masterminds/vcs" local, _ := ioutil.TempDir("", "go-vcs") repo, _ := NewRepo(remote, local) // Returns: instance of GitRepo repo.Vcs() // Returns Git as this is a Git repo err := repo.Get() // Pulls down a repo, or a checkout in the case of SVN, and returns an // error if that didn't happen successfully. if err != nil { fmt.Println(err) } err = repo.UpdateVersion("master") // Checkouts out a specific version. In most cases this can be a commit id, // branch, or tag. if err != nil { fmt.Println(err) } } func TestTypeSwitch(t *testing.T) { // To test repo type switching we checkout as SVN and then try to get it as // a git repo afterwards. tempDir, err := ioutil.TempDir("", "go-vcs-svn-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewSvnRepo("https://github.com/Masterminds/VCSTestRepo/trunk", tempDir+string(os.PathSeparator)+"VCSTestRepo") if err != nil { t.Error(err) } err = repo.Get() if err != nil { t.Errorf("Unable to checkout SVN repo for repo switching tests. Err was %s", err) } _, err = NewRepo("https://github.com/Masterminds/VCSTestRepo", tempDir+string(os.PathSeparator)+"VCSTestRepo") if err != ErrWrongVCS { t.Errorf("Not detecting repo switch from SVN to Git") } } func TestDepInstalled(t *testing.T) { i := depInstalled("git") if !i { t.Error("depInstalled not finding installed dep.") } i = depInstalled("thisreallyisntinstalled") if i { t.Error("depInstalled finding not installed dep.") } } vcs-1.12.0/svn.go000066400000000000000000000257511315552061300135360ustar00rootroot00000000000000package vcs import ( "encoding/xml" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "time" ) // NewSvnRepo creates a new instance of SvnRepo. The remote and local directories // need to be passed in. The remote location should include the branch for SVN. // For example, if the package is https://github.com/Masterminds/cookoo/ the remote // should be https://github.com/Masterminds/cookoo/trunk for the trunk branch. func NewSvnRepo(remote, local string) (*SvnRepo, error) { ins := depInstalled("svn") if !ins { return nil, NewLocalError("svn is not installed", nil, "") } ltype, err := DetectVcsFromFS(local) // Found a VCS other than Svn. Need to report an error. if err == nil && ltype != Svn { return nil, ErrWrongVCS } r := &SvnRepo{} r.setRemote(remote) r.setLocalPath(local) r.Logger = Logger // Make sure the local SVN repo is configured the same as the remote when // A remote value was passed in. if err == nil && r.CheckLocal() { // An SVN repo was found so test that the URL there matches // the repo passed in here. out, err := exec.Command("svn", "info", local).CombinedOutput() if err != nil { return nil, NewLocalError("Unable to retrieve local repo information", err, string(out)) } detectedRemote, err := detectRemoteFromInfoCommand(string(out)) if err != nil { return nil, NewLocalError("Unable to retrieve local repo information", err, string(out)) } if detectedRemote != "" && remote != "" && detectedRemote != remote { return nil, ErrWrongRemote } // If no remote was passed in but one is configured for the locally // checked out Svn repo use that one. if remote == "" && detectedRemote != "" { r.setRemote(detectedRemote) } } return r, nil } // SvnRepo implements the Repo interface for the Svn source control. type SvnRepo struct { base } // Vcs retrieves the underlying VCS being implemented. func (s SvnRepo) Vcs() Type { return Svn } // Get is used to perform an initial checkout of a repository. // Note, because SVN isn't distributed this is a checkout without // a clone. func (s *SvnRepo) Get() error { remote := s.Remote() if strings.HasPrefix(remote, "/") { remote = "file://" + remote } else if runtime.GOOS == "windows" && filepath.VolumeName(remote) != "" { remote = "file:///" + remote } out, err := s.run("svn", "checkout", remote, s.LocalPath()) if err != nil { return NewRemoteError("Unable to get repository", err, string(out)) } return nil } // Init will create a svn repository at remote location. func (s *SvnRepo) Init() error { out, err := s.run("svnadmin", "create", s.Remote()) if err != nil && s.isUnableToCreateDir(err) { basePath := filepath.Dir(filepath.FromSlash(s.Remote())) if _, err := os.Stat(basePath); os.IsNotExist(err) { err = os.MkdirAll(basePath, 0755) if err != nil { return NewLocalError("Unable to initialize repository", err, "") } out, err = s.run("svnadmin", "create", s.Remote()) if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } } else if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } // Update performs an SVN update to an existing checkout. func (s *SvnRepo) Update() error { out, err := s.RunFromDir("svn", "update") if err != nil { return NewRemoteError("Unable to update repository", err, string(out)) } return err } // UpdateVersion sets the version of a package currently checked out via SVN. func (s *SvnRepo) UpdateVersion(version string) error { out, err := s.RunFromDir("svn", "update", "-r", version) if err != nil { return NewRemoteError("Unable to update checked out version", err, string(out)) } return nil } // Version retrieves the current version. func (s *SvnRepo) Version() (string, error) { type Commit struct { Revision string `xml:"revision,attr"` } type Info struct { Commit Commit `xml:"entry>commit"` } out, err := s.RunFromDir("svn", "info", "--xml") if err != nil { return "", NewLocalError("Unable to retrieve checked out version", err, string(out)) } s.log(out) infos := &Info{} err = xml.Unmarshal(out, &infos) if err != nil { return "", NewLocalError("Unable to retrieve checked out version", err, string(out)) } return infos.Commit.Revision, nil } // Current returns the current version-ish. This means: // * HEAD if on the tip. // * Otherwise a revision id func (s *SvnRepo) Current() (string, error) { tip, err := s.CommitInfo("HEAD") if err != nil { return "", err } curr, err := s.Version() if err != nil { return "", err } if tip.Commit == curr { return "HEAD", nil } return curr, nil } // Date retrieves the date on the latest commit. func (s *SvnRepo) Date() (time.Time, error) { version, err := s.Version() if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, "") } out, err := s.RunFromDir("svn", "pget", "svn:date", "--revprop", "-r", version) if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } const longForm = "2006-01-02T15:04:05.000000Z" t, err := time.Parse(longForm, strings.TrimSpace(string(out))) if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } return t, nil } // CheckLocal verifies the local location is an SVN repo. func (s *SvnRepo) CheckLocal() bool { pth, err := filepath.Abs(s.LocalPath()) if err != nil { s.log(err.Error()) return false } if _, err := os.Stat(filepath.Join(pth, ".svn")); err == nil { return true } oldpth := pth for oldpth != pth { pth = filepath.Dir(pth) if _, err := os.Stat(filepath.Join(pth, ".svn")); err == nil { return true } } return false } // Tags returns []string{} as there are no formal tags in SVN. Tags are a // convention in SVN. They are typically implemented as a copy of the trunk and // placed in the /tags/[tag name] directory. Since this is a convention the // expectation is to checkout a tag the correct subdirectory will be used // as the path. For more information see: // http://svnbook.red-bean.com/en/1.7/svn.branchmerge.tags.html func (s *SvnRepo) Tags() ([]string, error) { return []string{}, nil } // Branches returns []string{} as there are no formal branches in SVN. Branches // are a convention. They are typically implemented as a copy of the trunk and // placed in the /branches/[tag name] directory. Since this is a convention the // expectation is to checkout a branch the correct subdirectory will be used // as the path. For more information see: // http://svnbook.red-bean.com/en/1.7/svn.branchmerge.using.html func (s *SvnRepo) Branches() ([]string, error) { return []string{}, nil } // IsReference returns if a string is a reference. A reference is a commit id. // Branches and tags are part of the path. func (s *SvnRepo) IsReference(r string) bool { out, err := s.RunFromDir("svn", "log", "-r", r) // This is a complete hack. There must be a better way to do this. Pull // requests welcome. When the reference isn't real you get a line of // repeated - followed by an empty line. If the reference is real there // is commit information in addition to those. So, we look for responses // over 2 lines long. lines := strings.Split(string(out), "\n") if err == nil && len(lines) > 2 { return true } return false } // IsDirty returns if the checkout has been modified from the checked // out reference. func (s *SvnRepo) IsDirty() bool { out, err := s.RunFromDir("svn", "diff") return err != nil || len(out) != 0 } // CommitInfo retrieves metadata about a commit. func (s *SvnRepo) CommitInfo(id string) (*CommitInfo, error) { // There are cases where Svn log doesn't return anything for HEAD or BASE. // svn info does provide details for these but does not have elements like // the commit message. if id == "HEAD" || id == "BASE" { type Commit struct { Revision string `xml:"revision,attr"` } type Info struct { Commit Commit `xml:"entry>commit"` } out, err := s.RunFromDir("svn", "info", "-r", id, "--xml") if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } infos := &Info{} err = xml.Unmarshal(out, &infos) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } id = infos.Commit.Revision if id == "" { return nil, ErrRevisionUnavailable } } out, err := s.RunFromDir("svn", "log", "-r", id, "--xml") if err != nil { return nil, NewRemoteError("Unable to retrieve commit information", err, string(out)) } type Logentry struct { Author string `xml:"author"` Date string `xml:"date"` Msg string `xml:"msg"` } type Log struct { XMLName xml.Name `xml:"log"` Logs []Logentry `xml:"logentry"` } logs := &Log{} err = xml.Unmarshal(out, &logs) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } if len(logs.Logs) == 0 { return nil, ErrRevisionUnavailable } ci := &CommitInfo{ Commit: id, Author: logs.Logs[0].Author, Message: logs.Logs[0].Msg, } if len(logs.Logs[0].Date) > 0 { ci.Date, err = time.Parse(time.RFC3339Nano, logs.Logs[0].Date) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } } return ci, nil } // TagsFromCommit retrieves tags from a commit id. func (s *SvnRepo) TagsFromCommit(id string) ([]string, error) { // Svn tags are a convention implemented as paths. See the details on the // Tag() method for more information. return []string{}, nil } // Ping returns if remote location is accessible. func (s *SvnRepo) Ping() bool { _, err := s.run("svn", "--non-interactive", "info", s.Remote()) return err == nil } // ExportDir exports the current revision to the passed in directory. func (s *SvnRepo) ExportDir(dir string) error { out, err := s.RunFromDir("svn", "export", ".", dir) s.log(out) if err != nil { return NewLocalError("Unable to export source", err, string(out)) } return nil } // isUnableToCreateDir checks for an error in Init() to see if an error // where the parent directory of the VCS local path doesn't exist. func (s *SvnRepo) isUnableToCreateDir(err error) bool { msg := err.Error() return strings.HasPrefix(msg, "E000002") } // detectRemoteFromInfoCommand finds the remote url from the `svn info` // command's output without using a regex. We avoid regex because URLs // are notoriously complex to accurately match with a regex and // splitting strings is less complex and often faster func detectRemoteFromInfoCommand(infoOut string) (string, error) { sBytes := []byte(infoOut) urlIndex := strings.Index(infoOut, "URL: ") if urlIndex == -1 { return "", fmt.Errorf("Remote not specified in svn info") } urlEndIndex := strings.Index(string(sBytes[urlIndex:]), "\n") if urlEndIndex == -1 { urlEndIndex = strings.Index(string(sBytes[urlIndex:]), "\r") if urlEndIndex == -1 { return "", fmt.Errorf("Unable to parse remote URL for svn info") } } return string(sBytes[(urlIndex + 5):(urlIndex + urlEndIndex)]), nil } vcs-1.12.0/svn_test.go000066400000000000000000000175131315552061300145720ustar00rootroot00000000000000package vcs import ( "io/ioutil" "path/filepath" "time" //"log" "os" "testing" ) // To verify svn is working we perform integration testing // with a known svn service. // Canary test to ensure SvnRepo implements the Repo interface. var _ Repo = &SvnRepo{} func TestSvn(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-svn-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewSvnRepo("https://github.com/Masterminds/VCSTestRepo/trunk", tempDir+string(os.PathSeparator)+"VCSTestRepo") if err != nil { t.Error(err) } if repo.Vcs() != Svn { t.Error("Svn is detecting the wrong type") } // Check the basic getters. if repo.Remote() != "https://github.com/Masterminds/VCSTestRepo/trunk" { t.Error("Remote not set properly") } if repo.LocalPath() != tempDir+string(os.PathSeparator)+"VCSTestRepo" { t.Error("Local disk location not set properly") } //Logger = log.New(os.Stdout, "", log.LstdFlags) // Do an initial checkout. err = repo.Get() if err != nil { t.Errorf("Unable to checkout SVN repo. Err was %s", err) } // Verify SVN repo is a SVN repo if !repo.CheckLocal() { t.Error("Problem checking out repo or SVN CheckLocal is not working") } // Verify an incorrect remote is caught when NewSvnRepo is used on an existing location _, nrerr := NewSvnRepo("https://github.com/Masterminds/VCSTestRepo/unknownbranch", tempDir+"/VCSTestRepo") if nrerr != ErrWrongRemote { t.Error("ErrWrongRemote was not triggered for SVN") } // Test internal lookup mechanism used outside of Hg specific functionality. ltype, err := DetectVcsFromFS(tempDir + "/VCSTestRepo") if err != nil { t.Error("detectVcsFromFS unable to Svn repo") } if ltype != Svn { t.Errorf("detectVcsFromFS detected %s instead of Svn type", ltype) } // Commenting out auto-detection tests for SVN. NewRepo automatically detects // GitHub to be a Git repo and that's an issue for this test. Need an // SVN host that can autodetect from before using this test again. // // Test NewRepo on existing checkout. This should simply provide a working // instance without error based on looking at the local directory. // nrepo, nrerr := NewRepo("https://github.com/Masterminds/VCSTestRepo/trunk", tempDir+"/VCSTestRepo") // if nrerr != nil { // t.Error(nrerr) // } // // Verify the right oject is returned. It will check the local repo type. // if nrepo.CheckLocal() == false { // t.Error("Wrong version returned from NewRepo") // } v, err := repo.Current() if err != nil { t.Errorf("Error trying Svn Current: %s", err) } if v != "HEAD" { t.Errorf("Current failed to detect Svn on HEAD. Got version: %s", v) } // Update the version to a previous version. err = repo.UpdateVersion("r2") if err != nil { t.Errorf("Unable to update SVN repo version. Err was %s", err) } // Use Version to verify we are on the right version. v, err = repo.Version() if v != "2" { t.Error("Error checking checked SVN out version") } if err != nil { t.Error(err) } v, err = repo.Current() if err != nil { t.Errorf("Error trying Svn Current for ref: %s", err) } if v != "2" { t.Errorf("Current failed to detect Svn on HEAD. Got version: %s", v) } // Perform an update which should take up back to the latest version. err = repo.Update() if err != nil { t.Error(err) } // Make sure we are on a newer version because of the update. v, err = repo.Version() if v == "2" { t.Error("Error with version. Still on old version. Update failed") } if err != nil { t.Error(err) } // Use Date to verify we are on the right commit. d, err := repo.Date() if d.Format(longForm) != "2015-07-29 13:47:03 +0000" { t.Error("Error checking checked out Svn commit date") } if err != nil { t.Error(err) } tags, err := repo.Tags() if err != nil { t.Error(err) } if len(tags) != 0 { t.Error("Svn is incorrectly returning tags") } tags, err = repo.TagsFromCommit("2") if err != nil { t.Error(err) } if len(tags) != 0 { t.Error("Svn is incorrectly returning tags for a commit") } branches, err := repo.Branches() if err != nil { t.Error(err) } if len(branches) != 0 { t.Error("Svn is incorrectly returning branches") } if !repo.IsReference("r4") { t.Error("Svn is reporting a reference is not one") } if repo.IsReference("55") { t.Error("Svn is reporting a non-existent reference is one") } if repo.IsDirty() { t.Error("Svn incorrectly reporting dirty") } ci, err := repo.CommitInfo("2") if err != nil { t.Error(err) } if ci.Commit != "2" { t.Error("Svn.CommitInfo wrong commit id") } if ci.Author != "matt.farina" { t.Error("Svn.CommitInfo wrong author") } if ci.Message != "Update README.md" { t.Error("Svn.CommitInfo wrong message") } ti, err := time.Parse(time.RFC3339Nano, "2015-07-29T13:46:20.000000Z") if err != nil { t.Error(err) } if !ti.Equal(ci.Date) { t.Error("Svn.CommitInfo wrong date") } _, err = repo.CommitInfo("555555555") if err != ErrRevisionUnavailable { t.Error("Svn didn't return expected ErrRevisionUnavailable") } tempDir2, err := ioutil.TempDir("", "go-vcs-svn-tests-export") if err != nil { t.Fatalf("Error creating temp directory: %s", err) } defer func() { err = os.RemoveAll(tempDir2) if err != nil { t.Error(err) } }() exportDir := filepath.Join(tempDir2, "src") err = repo.ExportDir(exportDir) if err != nil { t.Errorf("Unable to export Svn repo. Err was %s", err) } _, err = os.Stat(filepath.Join(exportDir, "README.md")) if err != nil { t.Errorf("Error checking exported file in Svn: %s", err) } _, err = os.Stat(filepath.Join(exportDir, string(repo.Vcs()))) if err != nil { if found := os.IsNotExist(err); !found { t.Errorf("Error checking exported metadata in Svn: %s", err) } } else { t.Error("Error checking Svn metadata. It exists.") } } func TestSvnCheckLocal(t *testing.T) { // Verify repo.CheckLocal fails for non-SVN directories. // TestSvn is already checking on a valid repo tempDir, err := ioutil.TempDir("", "go-vcs-svn-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, _ := NewSvnRepo("", tempDir) if repo.CheckLocal() { t.Error("SVN CheckLocal does not identify non-SVN location") } // Test NewRepo when there's no local. This should simply provide a working // instance without error based on looking at the remote localtion. _, nrerr := NewRepo("https://github.com/Masterminds/VCSTestRepo/trunk", tempDir+"/VCSTestRepo") if nrerr != nil { t.Error(nrerr) } } func TestSvnPing(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-svn-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewSvnRepo("https://github.com/Masterminds/VCSTestRepo/trunk", tempDir) if err != nil { t.Error(err) } ping := repo.Ping() if !ping { t.Error("Svn unable to ping working repo") } repo, err = NewSvnRepo("https://github.com/Masterminds/ihopethisneverexistsbecauseitshouldnt", tempDir) if err != nil { t.Error(err) } ping = repo.Ping() if ping { t.Error("Svn got a ping response from when it should not have") } } func TestSvnInit(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-svn-tests") remoteDir := tempDir + string(os.PathSeparator) + "remoteDir" localDir := tempDir + string(os.PathSeparator) + "localDir" if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() repo, err := NewSvnRepo(remoteDir, localDir) if err != nil { t.Error(err) } err = repo.Init() if err != nil { t.Error(err) } err = repo.Get() if err != nil { t.Error(err) } v, err := repo.Version() if err != nil { t.Error(err) } if v != "0" { t.Errorf("Svn Init returns wrong version: %s", v) } } vcs-1.12.0/vcs_local_lookup.go000066400000000000000000000024131315552061300162540ustar00rootroot00000000000000package vcs import ( "os" "runtime" "strings" ) // DetectVcsFromFS detects the type from the local path. // Is there a better way to do this? func DetectVcsFromFS(vcsPath string) (Type, error) { // There are cases under windows that a path could start with a / and it needs // to be stripped. For example, a path such as /C:\foio\bar. if runtime.GOOS == "windows" && strings.HasPrefix(vcsPath, "/") { vcsPath = strings.TrimPrefix(vcsPath, "/") } // When the local directory to the package doesn't exist // it's not yet downloaded so we can't detect the type // locally. if _, err := os.Stat(vcsPath); os.IsNotExist(err) { return "", ErrCannotDetectVCS } separator := string(os.PathSeparator) // Walk through each of the different VCS types to see if // one can be detected. Do this is order of guessed popularity. if _, err := os.Stat(vcsPath + separator + ".git"); err == nil { return Git, nil } if _, err := os.Stat(vcsPath + separator + ".svn"); err == nil { return Svn, nil } if _, err := os.Stat(vcsPath + separator + ".hg"); err == nil { return Hg, nil } if _, err := os.Stat(vcsPath + separator + ".bzr"); err == nil { return Bzr, nil } // If one was not already detected than we default to not finding it. return "", ErrCannotDetectVCS } vcs-1.12.0/vcs_remote_lookup.go000066400000000000000000000221611315552061300164570ustar00rootroot00000000000000package vcs import ( "encoding/json" "encoding/xml" "fmt" "io" "io/ioutil" "net/http" "net/url" "regexp" "strings" ) type vcsInfo struct { host string pattern string vcs Type addCheck func(m map[string]string, u *url.URL) (Type, error) regex *regexp.Regexp } // scpSyntaxRe matches the SCP-like addresses used by Git to access // repositories by SSH. var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`) var vcsList = []*vcsInfo{ { host: "github.com", vcs: Git, pattern: `^(github\.com[/|:][A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`, }, { host: "bitbucket.org", pattern: `^(bitbucket\.org/(?P[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`, addCheck: checkBitbucket, }, { host: "launchpad.net", pattern: `^(launchpad\.net/(([A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)?|~[A-Za-z0-9_.\-]+/(\+junk|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`, vcs: Bzr, }, { host: "git.launchpad.net", vcs: Git, pattern: `^(git\.launchpad\.net/(([A-Za-z0-9_.\-]+)|~[A-Za-z0-9_.\-]+/(\+git|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))$`, }, { host: "hub.jazz.net", vcs: Git, pattern: `^(hub\.jazz\.net/git/[a-z0-9]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`, }, { host: "go.googlesource.com", vcs: Git, pattern: `^(go\.googlesource\.com/[A-Za-z0-9_.\-]+/?)$`, }, { host: "git.openstack.org", vcs: Git, pattern: `^(git\.openstack\.org/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)$`, }, // If none of the previous detect the type they will fall to this looking for the type in a generic sense // by the extension to the path. { addCheck: checkURL, pattern: `\.(?Pgit|hg|svn|bzr)$`, }, } func init() { // Precompile the regular expressions used to check VCS locations. for _, v := range vcsList { v.regex = regexp.MustCompile(v.pattern) } } // This function is really a hack around Go redirects rather than around // something VCS related. Should this be moved to the glide project or a // helper function? func detectVcsFromRemote(vcsURL string) (Type, string, error) { t, e := detectVcsFromURL(vcsURL) if e == nil { return t, vcsURL, nil } else if e != ErrCannotDetectVCS { return NoVCS, "", e } // Pages like https://golang.org/x/net provide an html document with // meta tags containing a location to work with. The go tool uses // a meta tag with the name go-import which is what we use here. // godoc.org also has one call go-source that we do not need to use. // The value of go-import is in the form "prefix vcs repo". The prefix // should match the vcsURL and the repo is a location that can be // checked out. Note, to get the html document you you need to add // ?go-get=1 to the url. u, err := url.Parse(vcsURL) if err != nil { return NoVCS, "", err } if u.RawQuery == "" { u.RawQuery = "go-get=1" } else { u.RawQuery = u.RawQuery + "+go-get=1" } checkURL := u.String() resp, err := http.Get(checkURL) if err != nil { return NoVCS, "", ErrCannotDetectVCS } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode == 404 { return NoVCS, "", NewRemoteError(fmt.Sprintf("%s Not Found", vcsURL), nil, "") } else if resp.StatusCode == 401 || resp.StatusCode == 403 { return NoVCS, "", NewRemoteError(fmt.Sprintf("%s Access Denied", vcsURL), nil, "") } return NoVCS, "", ErrCannotDetectVCS } t, nu, err := parseImportFromBody(u, resp.Body) if err != nil { // TODO(mattfarina): Log the parsing error return NoVCS, "", ErrCannotDetectVCS } else if t == "" || nu == "" { return NoVCS, "", ErrCannotDetectVCS } return t, nu, nil } // From a remote vcs url attempt to detect the VCS. func detectVcsFromURL(vcsURL string) (Type, error) { var u *url.URL var err error if m := scpSyntaxRe.FindStringSubmatch(vcsURL); m != nil { // Match SCP-like syntax and convert it to a URL. // Eg, "git@github.com:user/repo" becomes // "ssh://git@github.com/user/repo". u = &url.URL{ Scheme: "ssh", User: url.User(m[1]), Host: m[2], Path: "/" + m[3], } } else { u, err = url.Parse(vcsURL) if err != nil { return "", err } } // Detect file schemes if u.Scheme == "file" { return DetectVcsFromFS(u.Path) } if u.Host == "" { return "", ErrCannotDetectVCS } // Try to detect from the scheme switch u.Scheme { case "git+ssh": return Git, nil case "git": return Git, nil case "bzr+ssh": return Bzr, nil case "svn+ssh": return Svn, nil } // Try to detect from known hosts, such as Github for _, v := range vcsList { if v.host != "" && v.host != u.Host { continue } // Make sure the pattern matches for an actual repo location. For example, // we should fail if the VCS listed is github.com/masterminds as that's // not actually a repo. uCheck := u.Host + u.Path m := v.regex.FindStringSubmatch(uCheck) if m == nil { if v.host != "" { return "", ErrCannotDetectVCS } continue } // If we are here the host matches. If the host has a singular // VCS type, such as Github, we can return the type right away. if v.vcs != "" { return v.vcs, nil } // Run additional checks to determine try and determine the repo // for the matched service. info := make(map[string]string) for i, name := range v.regex.SubexpNames() { if name != "" { info[name] = m[i] } } t, err := v.addCheck(info, u) if err != nil { switch err.(type) { case *RemoteError: return "", err } return "", ErrCannotDetectVCS } return t, nil } // Attempt to ascertain from the username passed in. if u.User != nil { un := u.User.Username() if un == "git" { return Git, nil } else if un == "hg" { return Hg, nil } } // Unable to determine the vcs from the url. return "", ErrCannotDetectVCS } // Figure out the type for Bitbucket by the passed in information // or via the public API. func checkBitbucket(i map[string]string, ul *url.URL) (Type, error) { // Fast path for ssh urls where we may not even be able to // anonymously get details from the API. if ul.User != nil { un := ul.User.Username() if un == "git" { return Git, nil } else if un == "hg" { return Hg, nil } } // The part of the response we care about. var response struct { SCM Type `json:"scm"` } u := expand(i, "https://api.bitbucket.org/1.0/repositories/{name}") data, err := get(u) if err != nil { return "", err } if err := json.Unmarshal(data, &response); err != nil { return "", fmt.Errorf("Decoding error %s: %v", u, err) } return response.SCM, nil } // Expect a type key on i with the exact type detected from the regex. func checkURL(i map[string]string, u *url.URL) (Type, error) { return Type(i["type"]), nil } func get(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { if resp.StatusCode == 404 { return nil, NewRemoteError("Not Found", err, resp.Status) } else if resp.StatusCode == 401 || resp.StatusCode == 403 { return nil, NewRemoteError("Access Denied", err, resp.Status) } return nil, fmt.Errorf("%s: %s", url, resp.Status) } b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("%s: %v", url, err) } return b, nil } func expand(match map[string]string, s string) string { for k, v := range match { s = strings.Replace(s, "{"+k+"}", v, -1) } return s } func parseImportFromBody(ur *url.URL, r io.ReadCloser) (tp Type, u string, err error) { d := xml.NewDecoder(r) d.CharsetReader = charsetReader d.Strict = false var t xml.Token for { t, err = d.Token() if err != nil { if err == io.EOF { // When the end is reached it could not detect a VCS if it // got here. err = ErrCannotDetectVCS } return } if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") { return } if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") { return } e, ok := t.(xml.StartElement) if !ok || !strings.EqualFold(e.Name.Local, "meta") { continue } if attrValue(e.Attr, "name") != "go-import" { continue } if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 { // If the prefix supplied by the remote system isn't a prefix to the // url we're fetching continue to look for other imports. // This will work for exact matches and prefixes. For example, // golang.org/x/net as a prefix will match for golang.org/x/net and // golang.org/x/net/context. vcsURL := ur.Host + ur.Path if !strings.HasPrefix(vcsURL, f[0]) { continue } else { switch Type(f[1]) { case Git: tp = Git case Svn: tp = Svn case Bzr: tp = Bzr case Hg: tp = Hg } u = f[2] return } } } } func charsetReader(charset string, input io.Reader) (io.Reader, error) { switch strings.ToLower(charset) { case "ascii": return input, nil default: return nil, fmt.Errorf("can't decode XML document using charset %q", charset) } } func attrValue(attrs []xml.Attr, name string) string { for _, a := range attrs { if strings.EqualFold(a.Name.Local, name) { return a.Value } } return "" } vcs-1.12.0/vcs_remote_lookup_test.go000066400000000000000000000123461315552061300175220ustar00rootroot00000000000000package vcs import ( "io/ioutil" "os" "os/exec" "runtime" "strings" "testing" ) func TestVCSLookup(t *testing.T) { // TODO: Expand to make sure it detected the right vcs. urlList := map[string]struct { work bool t Type }{ "https://github.com/masterminds": {work: false, t: Git}, "https://github.com/Masterminds/VCSTestRepo": {work: true, t: Git}, "https://bitbucket.org/mattfarina/testhgrepo": {work: true, t: Hg}, "https://bitbucket.org/mattfarina/repo-does-not-exist": {work: false, t: Hg}, "https://bitbucket.org/mattfarina/private-repo-for-vcs-testing": {work: false, t: Hg}, "https://launchpad.net/govcstestbzrrepo/trunk": {work: true, t: Bzr}, "https://launchpad.net/~mattfarina/+junk/mygovcstestbzrrepo": {work: true, t: Bzr}, "https://launchpad.net/~mattfarina/+junk/mygovcstestbzrrepo/trunk": {work: true, t: Bzr}, "https://git.launchpad.net/govcstestgitrepo": {work: true, t: Git}, "https://git.launchpad.net/~mattfarina/+git/mygovcstestgitrepo": {work: true, t: Git}, "https://hub.jazz.net/git/user1/pkgname": {work: true, t: Git}, "https://hub.jazz.net/git/user1/pkgname/subpkg/subpkg/subpkg": {work: true, t: Git}, "https://hubs.jazz.net/git/user1/pkgname": {work: false, t: Git}, "https://example.com/foo/bar.git": {work: true, t: Git}, "https://example.com/foo/bar.svn": {work: true, t: Svn}, "https://example.com/foo/bar/baz.bzr": {work: true, t: Bzr}, "https://example.com/foo/bar/baz.hg": {work: true, t: Hg}, "https://gopkg.in/tomb.v1": {work: true, t: Git}, "https://golang.org/x/net": {work: true, t: Git}, "https://speter.net/go/exp/math/dec/inf": {work: true, t: Git}, "https://git.openstack.org/foo/bar": {work: true, t: Git}, "git@github.com:Masterminds/vcs.git": {work: true, t: Git}, "git@example.com:foo.git": {work: true, t: Git}, "ssh://hg@bitbucket.org/mattfarina/testhgrepo": {work: true, t: Hg}, "git@bitbucket.org:mattfarina/glide-bitbucket-example.git": {work: true, t: Git}, "git+ssh://example.com/foo/bar": {work: true, t: Git}, "git://example.com/foo/bar": {work: true, t: Git}, "bzr+ssh://example.com/foo/bar": {work: true, t: Bzr}, "svn+ssh://example.com/foo/bar": {work: true, t: Svn}, "git@example.com:foo/bar": {work: true, t: Git}, "hg@example.com:foo/bar": {work: true, t: Hg}, } for u, c := range urlList { ty, _, err := detectVcsFromRemote(u) if err == nil && !c.work { t.Errorf("Error detecting VCS from URL(%s)", u) } if err == ErrCannotDetectVCS && c.work { t.Errorf("Error detecting VCS from URL(%s)", u) } if err != nil && c.work { t.Errorf("Error detecting VCS from URL(%s): %s", u, err) } if err != nil && err != ErrCannotDetectVCS && !strings.HasSuffix(err.Error(), "Not Found") && !strings.HasSuffix(err.Error(), "Access Denied") && !c.work { t.Errorf("Unexpected error returned (%s): %s", u, err) } if c.work && ty != c.t { t.Errorf("Incorrect VCS type returned(%s)", u) } } } func TestVCSFileLookup(t *testing.T) { tempDir, err := ioutil.TempDir("", "go-vcs-file-lookup-tests") if err != nil { t.Error(err) } defer func() { err = os.RemoveAll(tempDir) if err != nil { t.Error(err) } }() _, err = exec.Command("git", "init", tempDir).CombinedOutput() if err != nil { t.Error(err) } // On Windows it should be file:// followed by /C:\for\bar. That / before // the drive needs to be included in testing. var pth string if runtime.GOOS == "windows" { pth = "file:///" + tempDir } else { pth = "file://" + tempDir } ty, _, err := detectVcsFromRemote(pth) if err != nil { t.Errorf("Unable to detect file:// path: %s", err) } if ty != Git { t.Errorf("Detected wrong type from file:// path. Found type %v", ty) } } func TestNotFound(t *testing.T) { _, _, err := detectVcsFromRemote("https://mattfarina.com/notfound") if err == nil || !strings.HasSuffix(err.Error(), " Not Found") { t.Errorf("Failed to find not found repo") } _, err = NewRepo("https://mattfarina.com/notfound", "") if err == nil || !strings.HasSuffix(err.Error(), " Not Found") { t.Errorf("Failed to find not found repo") } } func TestAccessDenied(t *testing.T) { _, _, err := detectVcsFromRemote("https://bitbucket.org/mattfarina/private-repo-for-vcs-testing") if err == nil || err.Error() != "Access Denied" { t.Errorf("Failed to detect access denied") } _, err = NewRepo("https://bitbucket.org/mattfarina/private-repo-for-vcs-testing", "") if err == nil || err.Error() != "Access Denied" { t.Errorf("Failed to detect access denied") } }