bzr.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. package vcs
  2. import (
  3. "fmt"
  4. "net/url"
  5. "os"
  6. "os/exec"
  7. "path/filepath"
  8. "regexp"
  9. "strings"
  10. "time"
  11. )
  12. var bzrDetectURL = regexp.MustCompile("parent branch: (?P<foo>.+)\n")
  13. // NewBzrRepo creates a new instance of BzrRepo. The remote and local directories
  14. // need to be passed in.
  15. func NewBzrRepo(remote, local string) (*BzrRepo, error) {
  16. ins := depInstalled("bzr")
  17. if !ins {
  18. return nil, NewLocalError("bzr is not installed", nil, "")
  19. }
  20. ltype, err := DetectVcsFromFS(local)
  21. // Found a VCS other than Bzr. Need to report an error.
  22. if err == nil && ltype != Bzr {
  23. return nil, ErrWrongVCS
  24. }
  25. r := &BzrRepo{}
  26. r.setRemote(remote)
  27. r.setLocalPath(local)
  28. r.Logger = Logger
  29. // With the other VCS we can check if the endpoint locally is different
  30. // from the one configured internally. But, with Bzr you can't. For example,
  31. // if you do `bzr branch https://launchpad.net/govcstestbzrrepo` and then
  32. // use `bzr info` to get the parent branch you'll find it set to
  33. // http://bazaar.launchpad.net/~mattfarina/govcstestbzrrepo/trunk/. Notice
  34. // the change from https to http and the path chance.
  35. // Here we set the remote to be the local one if none is passed in.
  36. if err == nil && r.CheckLocal() && remote == "" {
  37. c := exec.Command("bzr", "info")
  38. c.Dir = local
  39. c.Env = envForDir(c.Dir)
  40. out, err := c.CombinedOutput()
  41. if err != nil {
  42. return nil, NewLocalError("Unable to retrieve local repo information", err, string(out))
  43. }
  44. m := bzrDetectURL.FindStringSubmatch(string(out))
  45. // If no remote was passed in but one is configured for the locally
  46. // checked out Bzr repo use that one.
  47. if m[1] != "" {
  48. r.setRemote(m[1])
  49. }
  50. }
  51. return r, nil
  52. }
  53. // BzrRepo implements the Repo interface for the Bzr source control.
  54. type BzrRepo struct {
  55. base
  56. }
  57. // Vcs retrieves the underlying VCS being implemented.
  58. func (s BzrRepo) Vcs() Type {
  59. return Bzr
  60. }
  61. // Get is used to perform an initial clone of a repository.
  62. func (s *BzrRepo) Get() error {
  63. basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
  64. if _, err := os.Stat(basePath); os.IsNotExist(err) {
  65. err = os.MkdirAll(basePath, 0755)
  66. if err != nil {
  67. return NewLocalError("Unable to create directory", err, "")
  68. }
  69. }
  70. out, err := s.run("bzr", "branch", "--", s.Remote(), s.LocalPath())
  71. if err != nil {
  72. return NewRemoteError("Unable to get repository", err, string(out))
  73. }
  74. return nil
  75. }
  76. // Init initializes a bazaar repository at local location.
  77. func (s *BzrRepo) Init() error {
  78. out, err := s.run("bzr", "init", "--", s.LocalPath())
  79. // There are some windows cases where bazaar cannot create the parent
  80. // directory if it does not already exist, to the location it's trying
  81. // to create the repo. Catch that error and try to handle it.
  82. if err != nil && s.isUnableToCreateDir(err) {
  83. basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
  84. if _, err := os.Stat(basePath); os.IsNotExist(err) {
  85. err = os.MkdirAll(basePath, 0755)
  86. if err != nil {
  87. return NewLocalError("Unable to initialize repository", err, "")
  88. }
  89. out, err = s.run("bzr", "init", "--", s.LocalPath())
  90. if err != nil {
  91. return NewLocalError("Unable to initialize repository", err, string(out))
  92. }
  93. return nil
  94. }
  95. } else if err != nil {
  96. return NewLocalError("Unable to initialize repository", err, string(out))
  97. }
  98. return nil
  99. }
  100. // Update performs a Bzr pull and update to an existing checkout.
  101. func (s *BzrRepo) Update() error {
  102. out, err := s.RunFromDir("bzr", "pull")
  103. if err != nil {
  104. return NewRemoteError("Unable to update repository", err, string(out))
  105. }
  106. out, err = s.RunFromDir("bzr", "update")
  107. if err != nil {
  108. return NewRemoteError("Unable to update repository", err, string(out))
  109. }
  110. return nil
  111. }
  112. // UpdateVersion sets the version of a package currently checked out via Bzr.
  113. func (s *BzrRepo) UpdateVersion(version string) error {
  114. out, err := s.RunFromDir("bzr", "update", "-r", version)
  115. if err != nil {
  116. return NewLocalError("Unable to update checked out version", err, string(out))
  117. }
  118. return nil
  119. }
  120. // Version retrieves the current version.
  121. func (s *BzrRepo) Version() (string, error) {
  122. out, err := s.RunFromDir("bzr", "revno", "--tree")
  123. if err != nil {
  124. return "", NewLocalError("Unable to retrieve checked out version", err, string(out))
  125. }
  126. return strings.TrimSpace(string(out)), nil
  127. }
  128. // Current returns the current version-ish. This means:
  129. // * -1 if on the tip of the branch (this is the Bzr value for HEAD)
  130. // * A tag if on a tag
  131. // * Otherwise a revision
  132. func (s *BzrRepo) Current() (string, error) {
  133. tip, err := s.CommitInfo("-1")
  134. if err != nil {
  135. return "", err
  136. }
  137. curr, err := s.Version()
  138. if err != nil {
  139. return "", err
  140. }
  141. if tip.Commit == curr {
  142. return "-1", nil
  143. }
  144. ts, err := s.TagsFromCommit(curr)
  145. if err != nil {
  146. return "", err
  147. }
  148. if len(ts) > 0 {
  149. return ts[0], nil
  150. }
  151. return curr, nil
  152. }
  153. // Date retrieves the date on the latest commit.
  154. func (s *BzrRepo) Date() (time.Time, error) {
  155. out, err := s.RunFromDir("bzr", "version-info", "--custom", "--template={date}")
  156. if err != nil {
  157. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  158. }
  159. t, err := time.Parse(longForm, string(out))
  160. if err != nil {
  161. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  162. }
  163. return t, nil
  164. }
  165. // CheckLocal verifies the local location is a Bzr repo.
  166. func (s *BzrRepo) CheckLocal() bool {
  167. if _, err := os.Stat(s.LocalPath() + "/.bzr"); err == nil {
  168. return true
  169. }
  170. return false
  171. }
  172. // Branches returns a list of available branches on the repository.
  173. // In Bazaar (Bzr) clones and branches are the same. A different branch will
  174. // have a different URL location which we cannot detect from the repo. This
  175. // is a little different from other VCS.
  176. func (s *BzrRepo) Branches() ([]string, error) {
  177. var branches []string
  178. return branches, nil
  179. }
  180. // Tags returns a list of available tags on the repository.
  181. func (s *BzrRepo) Tags() ([]string, error) {
  182. out, err := s.RunFromDir("bzr", "tags")
  183. if err != nil {
  184. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  185. }
  186. tags := s.referenceList(string(out), `(?m-s)^(\S+)`)
  187. return tags, nil
  188. }
  189. // IsReference returns if a string is a reference. A reference can be a
  190. // commit id or tag.
  191. func (s *BzrRepo) IsReference(r string) bool {
  192. _, err := s.RunFromDir("bzr", "revno", "-r", r)
  193. return err == nil
  194. }
  195. // IsDirty returns if the checkout has been modified from the checked
  196. // out reference.
  197. func (s *BzrRepo) IsDirty() bool {
  198. out, err := s.RunFromDir("bzr", "diff")
  199. return err != nil || len(out) != 0
  200. }
  201. // CommitInfo retrieves metadata about a commit.
  202. func (s *BzrRepo) CommitInfo(id string) (*CommitInfo, error) {
  203. r := "-r" + id
  204. out, err := s.RunFromDir("bzr", "log", r, "--log-format=long")
  205. if err != nil {
  206. return nil, ErrRevisionUnavailable
  207. }
  208. ci := &CommitInfo{}
  209. lines := strings.Split(string(out), "\n")
  210. const format = "Mon 2006-01-02 15:04:05 -0700"
  211. var track int
  212. var trackOn bool
  213. // Note, bzr does not appear to use i18m.
  214. for i, l := range lines {
  215. if strings.HasPrefix(l, "revno:") {
  216. ci.Commit = strings.TrimSpace(strings.TrimPrefix(l, "revno:"))
  217. } else if strings.HasPrefix(l, "committer:") {
  218. ci.Author = strings.TrimSpace(strings.TrimPrefix(l, "committer:"))
  219. } else if strings.HasPrefix(l, "timestamp:") {
  220. ts := strings.TrimSpace(strings.TrimPrefix(l, "timestamp:"))
  221. ci.Date, err = time.Parse(format, ts)
  222. if err != nil {
  223. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  224. }
  225. } else if strings.TrimSpace(l) == "message:" {
  226. track = i
  227. trackOn = true
  228. } else if trackOn && i > track {
  229. ci.Message = ci.Message + l
  230. }
  231. }
  232. ci.Message = strings.TrimSpace(ci.Message)
  233. // Didn't find the revision
  234. if ci.Author == "" {
  235. return nil, ErrRevisionUnavailable
  236. }
  237. return ci, nil
  238. }
  239. // TagsFromCommit retrieves tags from a commit id.
  240. func (s *BzrRepo) TagsFromCommit(id string) ([]string, error) {
  241. out, err := s.RunFromDir("bzr", "tags", "-r", id)
  242. if err != nil {
  243. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  244. }
  245. tags := s.referenceList(string(out), `(?m-s)^(\S+)`)
  246. return tags, nil
  247. }
  248. // Ping returns if remote location is accessible.
  249. func (s *BzrRepo) Ping() bool {
  250. // Running bzr info is slow. Many of the projects are on launchpad which
  251. // has a public 1.0 API we can use.
  252. u, err := url.Parse(s.Remote())
  253. if err == nil {
  254. if u.Host == "launchpad.net" {
  255. try := strings.TrimPrefix(u.Path, "/")
  256. // get returns the body and an err. If the status code is not a 200
  257. // an error is returned. Launchpad returns a 404 for a codebase that
  258. // does not exist. Otherwise it returns a JSON object describing it.
  259. _, er := get("https://api.launchpad.net/1.0/" + try)
  260. return er == nil
  261. }
  262. }
  263. // This is the same command that Go itself uses but it's not fast (or fast
  264. // enough by my standards). A faster method would be useful.
  265. _, err = s.run("bzr", "info", "--", s.Remote())
  266. return err == nil
  267. }
  268. // ExportDir exports the current revision to the passed in directory.
  269. func (s *BzrRepo) ExportDir(dir string) error {
  270. out, err := s.RunFromDir("bzr", "export", "--", dir)
  271. s.log(out)
  272. if err != nil {
  273. return NewLocalError("Unable to export source", err, string(out))
  274. }
  275. return nil
  276. }
  277. // Multi-lingual manner check for the VCS error that it couldn't create directory.
  278. // https://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev/files/head:/po/
  279. func (s *BzrRepo) isUnableToCreateDir(err error) bool {
  280. msg := err.Error()
  281. if strings.HasPrefix(msg, fmt.Sprintf("Parent directory of %s does not exist.", s.LocalPath())) ||
  282. strings.HasPrefix(msg, fmt.Sprintf("Nadřazený adresář %s neexistuje.", s.LocalPath())) ||
  283. strings.HasPrefix(msg, fmt.Sprintf("El directorio padre de %s no existe.", s.LocalPath())) ||
  284. strings.HasPrefix(msg, fmt.Sprintf("%s の親ディレクトリがありません。", s.LocalPath())) ||
  285. strings.HasPrefix(msg, fmt.Sprintf("Родительская директория для %s не существует.", s.LocalPath())) {
  286. return true
  287. }
  288. return false
  289. }