svn.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. package vcs
  2. import (
  3. "encoding/xml"
  4. "fmt"
  5. "os"
  6. "os/exec"
  7. "path/filepath"
  8. "runtime"
  9. "strings"
  10. "time"
  11. )
  12. // NewSvnRepo creates a new instance of SvnRepo. The remote and local directories
  13. // need to be passed in. The remote location should include the branch for SVN.
  14. // For example, if the package is https://github.com/Masterminds/cookoo/ the remote
  15. // should be https://github.com/Masterminds/cookoo/trunk for the trunk branch.
  16. func NewSvnRepo(remote, local string) (*SvnRepo, error) {
  17. ins := depInstalled("svn")
  18. if !ins {
  19. return nil, NewLocalError("svn is not installed", nil, "")
  20. }
  21. ltype, err := DetectVcsFromFS(local)
  22. // Found a VCS other than Svn. Need to report an error.
  23. if err == nil && ltype != Svn {
  24. return nil, ErrWrongVCS
  25. }
  26. r := &SvnRepo{}
  27. r.setRemote(remote)
  28. r.setLocalPath(local)
  29. r.Logger = Logger
  30. // Make sure the local SVN repo is configured the same as the remote when
  31. // A remote value was passed in.
  32. if err == nil && r.CheckLocal() {
  33. // An SVN repo was found so test that the URL there matches
  34. // the repo passed in here.
  35. out, err := exec.Command("svn", "info", "--", local).CombinedOutput()
  36. if err != nil {
  37. return nil, NewLocalError("Unable to retrieve local repo information", err, string(out))
  38. }
  39. detectedRemote, err := detectRemoteFromInfoCommand(string(out))
  40. if err != nil {
  41. return nil, NewLocalError("Unable to retrieve local repo information", err, string(out))
  42. }
  43. if detectedRemote != "" && remote != "" && detectedRemote != remote {
  44. return nil, ErrWrongRemote
  45. }
  46. // If no remote was passed in but one is configured for the locally
  47. // checked out Svn repo use that one.
  48. if remote == "" && detectedRemote != "" {
  49. r.setRemote(detectedRemote)
  50. }
  51. }
  52. return r, nil
  53. }
  54. // SvnRepo implements the Repo interface for the Svn source control.
  55. type SvnRepo struct {
  56. base
  57. }
  58. // Vcs retrieves the underlying VCS being implemented.
  59. func (s SvnRepo) Vcs() Type {
  60. return Svn
  61. }
  62. // Get is used to perform an initial checkout of a repository.
  63. // Note, because SVN isn't distributed this is a checkout without
  64. // a clone.
  65. func (s *SvnRepo) Get() error {
  66. remote := s.Remote()
  67. if strings.HasPrefix(remote, "/") {
  68. remote = "file://" + remote
  69. } else if runtime.GOOS == "windows" && filepath.VolumeName(remote) != "" {
  70. remote = "file:///" + remote
  71. }
  72. out, err := s.run("svn", "checkout", "--", remote, s.LocalPath())
  73. if err != nil {
  74. return NewRemoteError("Unable to get repository", err, string(out))
  75. }
  76. return nil
  77. }
  78. // Init will create a svn repository at remote location.
  79. func (s *SvnRepo) Init() error {
  80. out, err := s.run("svnadmin", "create", s.Remote())
  81. if err != nil && s.isUnableToCreateDir(err) {
  82. basePath := filepath.Dir(filepath.FromSlash(s.Remote()))
  83. if _, err := os.Stat(basePath); os.IsNotExist(err) {
  84. err = os.MkdirAll(basePath, 0755)
  85. if err != nil {
  86. return NewLocalError("Unable to initialize repository", err, "")
  87. }
  88. out, err = s.run("svnadmin", "create", s.Remote())
  89. if err != nil {
  90. return NewLocalError("Unable to initialize repository", err, string(out))
  91. }
  92. return nil
  93. }
  94. } else if err != nil {
  95. return NewLocalError("Unable to initialize repository", err, string(out))
  96. }
  97. return nil
  98. }
  99. // Update performs an SVN update to an existing checkout.
  100. func (s *SvnRepo) Update() error {
  101. out, err := s.RunFromDir("svn", "update")
  102. if err != nil {
  103. return NewRemoteError("Unable to update repository", err, string(out))
  104. }
  105. return err
  106. }
  107. // UpdateVersion sets the version of a package currently checked out via SVN.
  108. func (s *SvnRepo) UpdateVersion(version string) error {
  109. out, err := s.RunFromDir("svn", "update", "-r", version)
  110. if err != nil {
  111. return NewRemoteError("Unable to update checked out version", err, string(out))
  112. }
  113. return nil
  114. }
  115. // Version retrieves the current version.
  116. func (s *SvnRepo) Version() (string, error) {
  117. type Commit struct {
  118. Revision string `xml:"revision,attr"`
  119. }
  120. type Info struct {
  121. Commit Commit `xml:"entry>commit"`
  122. }
  123. out, err := s.RunFromDir("svn", "info", "--xml")
  124. if err != nil {
  125. return "", NewLocalError("Unable to retrieve checked out version", err, string(out))
  126. }
  127. s.log(out)
  128. infos := &Info{}
  129. err = xml.Unmarshal(out, &infos)
  130. if err != nil {
  131. return "", NewLocalError("Unable to retrieve checked out version", err, string(out))
  132. }
  133. return infos.Commit.Revision, nil
  134. }
  135. // Current returns the current version-ish. This means:
  136. // * HEAD if on the tip.
  137. // * Otherwise a revision id
  138. func (s *SvnRepo) Current() (string, error) {
  139. tip, err := s.CommitInfo("HEAD")
  140. if err != nil {
  141. return "", err
  142. }
  143. curr, err := s.Version()
  144. if err != nil {
  145. return "", err
  146. }
  147. if tip.Commit == curr {
  148. return "HEAD", nil
  149. }
  150. return curr, nil
  151. }
  152. // Date retrieves the date on the latest commit.
  153. func (s *SvnRepo) Date() (time.Time, error) {
  154. version, err := s.Version()
  155. if err != nil {
  156. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, "")
  157. }
  158. out, err := s.RunFromDir("svn", "pget", "svn:date", "--revprop", "-r", version)
  159. if err != nil {
  160. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  161. }
  162. const longForm = "2006-01-02T15:04:05.000000Z"
  163. t, err := time.Parse(longForm, strings.TrimSpace(string(out)))
  164. if err != nil {
  165. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  166. }
  167. return t, nil
  168. }
  169. // CheckLocal verifies the local location is an SVN repo.
  170. func (s *SvnRepo) CheckLocal() bool {
  171. pth, err := filepath.Abs(s.LocalPath())
  172. if err != nil {
  173. s.log(err.Error())
  174. return false
  175. }
  176. if _, err := os.Stat(filepath.Join(pth, ".svn")); err == nil {
  177. return true
  178. }
  179. oldpth := pth
  180. for oldpth != pth {
  181. pth = filepath.Dir(pth)
  182. if _, err := os.Stat(filepath.Join(pth, ".svn")); err == nil {
  183. return true
  184. }
  185. }
  186. return false
  187. }
  188. // Tags returns []string{} as there are no formal tags in SVN. Tags are a
  189. // convention in SVN. They are typically implemented as a copy of the trunk and
  190. // placed in the /tags/[tag name] directory. Since this is a convention the
  191. // expectation is to checkout a tag the correct subdirectory will be used
  192. // as the path. For more information see:
  193. // http://svnbook.red-bean.com/en/1.7/svn.branchmerge.tags.html
  194. func (s *SvnRepo) Tags() ([]string, error) {
  195. return []string{}, nil
  196. }
  197. // Branches returns []string{} as there are no formal branches in SVN. Branches
  198. // are a convention. They are typically implemented as a copy of the trunk and
  199. // placed in the /branches/[tag name] directory. Since this is a convention the
  200. // expectation is to checkout a branch the correct subdirectory will be used
  201. // as the path. For more information see:
  202. // http://svnbook.red-bean.com/en/1.7/svn.branchmerge.using.html
  203. func (s *SvnRepo) Branches() ([]string, error) {
  204. return []string{}, nil
  205. }
  206. // IsReference returns if a string is a reference. A reference is a commit id.
  207. // Branches and tags are part of the path.
  208. func (s *SvnRepo) IsReference(r string) bool {
  209. out, err := s.RunFromDir("svn", "log", "-r", r)
  210. // This is a complete hack. There must be a better way to do this. Pull
  211. // requests welcome. When the reference isn't real you get a line of
  212. // repeated - followed by an empty line. If the reference is real there
  213. // is commit information in addition to those. So, we look for responses
  214. // over 2 lines long.
  215. lines := strings.Split(string(out), "\n")
  216. if err == nil && len(lines) > 2 {
  217. return true
  218. }
  219. return false
  220. }
  221. // IsDirty returns if the checkout has been modified from the checked
  222. // out reference.
  223. func (s *SvnRepo) IsDirty() bool {
  224. out, err := s.RunFromDir("svn", "diff")
  225. return err != nil || len(out) != 0
  226. }
  227. // CommitInfo retrieves metadata about a commit.
  228. func (s *SvnRepo) CommitInfo(id string) (*CommitInfo, error) {
  229. // There are cases where Svn log doesn't return anything for HEAD or BASE.
  230. // svn info does provide details for these but does not have elements like
  231. // the commit message.
  232. if id == "HEAD" || id == "BASE" {
  233. type Commit struct {
  234. Revision string `xml:"revision,attr"`
  235. }
  236. type Info struct {
  237. Commit Commit `xml:"entry>commit"`
  238. }
  239. out, err := s.RunFromDir("svn", "info", "-r", id, "--xml")
  240. if err != nil {
  241. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  242. }
  243. infos := &Info{}
  244. err = xml.Unmarshal(out, &infos)
  245. if err != nil {
  246. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  247. }
  248. id = infos.Commit.Revision
  249. if id == "" {
  250. return nil, ErrRevisionUnavailable
  251. }
  252. }
  253. out, err := s.RunFromDir("svn", "log", "-r", id, "--xml")
  254. if err != nil {
  255. return nil, NewRemoteError("Unable to retrieve commit information", err, string(out))
  256. }
  257. type Logentry struct {
  258. Author string `xml:"author"`
  259. Date string `xml:"date"`
  260. Msg string `xml:"msg"`
  261. }
  262. type Log struct {
  263. XMLName xml.Name `xml:"log"`
  264. Logs []Logentry `xml:"logentry"`
  265. }
  266. logs := &Log{}
  267. err = xml.Unmarshal(out, &logs)
  268. if err != nil {
  269. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  270. }
  271. if len(logs.Logs) == 0 {
  272. return nil, ErrRevisionUnavailable
  273. }
  274. ci := &CommitInfo{
  275. Commit: id,
  276. Author: logs.Logs[0].Author,
  277. Message: logs.Logs[0].Msg,
  278. }
  279. if len(logs.Logs[0].Date) > 0 {
  280. ci.Date, err = time.Parse(time.RFC3339Nano, logs.Logs[0].Date)
  281. if err != nil {
  282. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  283. }
  284. }
  285. return ci, nil
  286. }
  287. // TagsFromCommit retrieves tags from a commit id.
  288. func (s *SvnRepo) TagsFromCommit(id string) ([]string, error) {
  289. // Svn tags are a convention implemented as paths. See the details on the
  290. // Tag() method for more information.
  291. return []string{}, nil
  292. }
  293. // Ping returns if remote location is accessible.
  294. func (s *SvnRepo) Ping() bool {
  295. _, err := s.run("svn", "--non-interactive", "info", "--", s.Remote())
  296. return err == nil
  297. }
  298. // ExportDir exports the current revision to the passed in directory.
  299. func (s *SvnRepo) ExportDir(dir string) error {
  300. out, err := s.RunFromDir("svn", "export", "--", ".", dir)
  301. s.log(out)
  302. if err != nil {
  303. return NewLocalError("Unable to export source", err, string(out))
  304. }
  305. return nil
  306. }
  307. // isUnableToCreateDir checks for an error in Init() to see if an error
  308. // where the parent directory of the VCS local path doesn't exist.
  309. func (s *SvnRepo) isUnableToCreateDir(err error) bool {
  310. msg := err.Error()
  311. return strings.HasPrefix(msg, "E000002")
  312. }
  313. // detectRemoteFromInfoCommand finds the remote url from the `svn info`
  314. // command's output without using a regex. We avoid regex because URLs
  315. // are notoriously complex to accurately match with a regex and
  316. // splitting strings is less complex and often faster
  317. func detectRemoteFromInfoCommand(infoOut string) (string, error) {
  318. sBytes := []byte(infoOut)
  319. urlIndex := strings.Index(infoOut, "URL: ")
  320. if urlIndex == -1 {
  321. return "", fmt.Errorf("Remote not specified in svn info")
  322. }
  323. urlEndIndex := strings.Index(string(sBytes[urlIndex:]), "\n")
  324. if urlEndIndex == -1 {
  325. urlEndIndex = strings.Index(string(sBytes[urlIndex:]), "\r")
  326. if urlEndIndex == -1 {
  327. return "", fmt.Errorf("Unable to parse remote URL for svn info")
  328. }
  329. }
  330. return string(sBytes[(urlIndex + 5):(urlIndex + urlEndIndex)]), nil
  331. }