| package vcs |
| |
| import ( |
| "encoding/xml" |
| "fmt" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "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 |
| } |
| 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\n" |
| 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 an SVN repo. |
| func (s *SvnRepo) CheckLocal() bool { |
| sep := fmt.Sprintf("%c", os.PathSeparator) |
| psplit := strings.Split(s.LocalPath(), sep) |
| for i := 0; i < len(psplit); i++ { |
| path := fmt.Sprintf("%s%s", sep, filepath.Join(psplit[0:(len(psplit)-(i))]...)) |
| if _, err := os.Stat(filepath.Join(path, ".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 |
| } |