| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- package 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
- }
|