hg.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. package vcs
  2. import (
  3. "bytes"
  4. "encoding/xml"
  5. "errors"
  6. "os"
  7. "os/exec"
  8. "regexp"
  9. "strings"
  10. "time"
  11. )
  12. var hgDetectURL = regexp.MustCompile("default = (?P<foo>.+)\n")
  13. // NewHgRepo creates a new instance of HgRepo. The remote and local directories
  14. // need to be passed in.
  15. func NewHgRepo(remote, local string) (*HgRepo, error) {
  16. ins := depInstalled("hg")
  17. if !ins {
  18. return nil, NewLocalError("hg is not installed", nil, "")
  19. }
  20. ltype, err := DetectVcsFromFS(local)
  21. // Found a VCS other than Hg. Need to report an error.
  22. if err == nil && ltype != Hg {
  23. return nil, ErrWrongVCS
  24. }
  25. r := &HgRepo{}
  26. r.setRemote(remote)
  27. r.setLocalPath(local)
  28. r.Logger = Logger
  29. // Make sure the local Hg repo is configured the same as the remote when
  30. // A remote value was passed in.
  31. if err == nil && r.CheckLocal() {
  32. // An Hg repo was found so test that the URL there matches
  33. // the repo passed in here.
  34. c := exec.Command("hg", "paths")
  35. c.Dir = local
  36. c.Env = envForDir(c.Dir)
  37. out, err := c.CombinedOutput()
  38. if err != nil {
  39. return nil, NewLocalError("Unable to retrieve local repo information", err, string(out))
  40. }
  41. m := hgDetectURL.FindStringSubmatch(string(out))
  42. if m[1] != "" && m[1] != remote {
  43. return nil, ErrWrongRemote
  44. }
  45. // If no remote was passed in but one is configured for the locally
  46. // checked out Hg repo use that one.
  47. if remote == "" && m[1] != "" {
  48. r.setRemote(m[1])
  49. }
  50. }
  51. return r, nil
  52. }
  53. // HgRepo implements the Repo interface for the Mercurial source control.
  54. type HgRepo struct {
  55. base
  56. }
  57. // Vcs retrieves the underlying VCS being implemented.
  58. func (s HgRepo) Vcs() Type {
  59. return Hg
  60. }
  61. // Get is used to perform an initial clone of a repository.
  62. func (s *HgRepo) Get() error {
  63. out, err := s.run("hg", "clone", "--", s.Remote(), s.LocalPath())
  64. if err != nil {
  65. return NewRemoteError("Unable to get repository", err, string(out))
  66. }
  67. return nil
  68. }
  69. // Init will initialize a mercurial repository at local location.
  70. func (s *HgRepo) Init() error {
  71. out, err := s.run("hg", "init", "--", s.LocalPath())
  72. if err != nil {
  73. return NewLocalError("Unable to initialize repository", err, string(out))
  74. }
  75. return nil
  76. }
  77. // Update performs a Mercurial pull to an existing checkout.
  78. func (s *HgRepo) Update() error {
  79. return s.UpdateVersion(``)
  80. }
  81. // UpdateVersion sets the version of a package currently checked out via Hg.
  82. func (s *HgRepo) UpdateVersion(version string) error {
  83. out, err := s.RunFromDir("hg", "pull")
  84. if err != nil {
  85. return NewLocalError("Unable to update checked out version", err, string(out))
  86. }
  87. if len(strings.TrimSpace(version)) > 0 {
  88. out, err = s.RunFromDir("hg", "update", "--", version)
  89. } else {
  90. out, err = s.RunFromDir("hg", "update")
  91. }
  92. if err != nil {
  93. return NewLocalError("Unable to update checked out version", err, string(out))
  94. }
  95. return nil
  96. }
  97. // Version retrieves the current version.
  98. func (s *HgRepo) Version() (string, error) {
  99. c := s.CmdFromDir("hg", "--debug", "identify")
  100. stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
  101. c.Stdout = stdout
  102. c.Stderr = stderr
  103. if err := c.Run(); err != nil {
  104. return "", NewLocalError("Unable to retrieve checked out version", err, stderr.String())
  105. }
  106. if stderr.Len() > 0 {
  107. // "hg --debug identify" can print out errors before it actually prints
  108. // the version.
  109. // https://github.com/Masterminds/vcs/issues/90
  110. return "", NewLocalError("Unable to retrieve checked out version", errors.New("Error output printed before identify"), stderr.String())
  111. }
  112. parts := strings.SplitN(stdout.String(), " ", 2)
  113. sha := parts[0]
  114. return strings.TrimSpace(sha), nil
  115. }
  116. // Current returns the current version-ish. This means:
  117. // * Branch name if on the tip of the branch
  118. // * Tag if on a tag
  119. // * Otherwise a revision id
  120. func (s *HgRepo) Current() (string, error) {
  121. out, err := s.RunFromDir("hg", "branch")
  122. if err != nil {
  123. return "", err
  124. }
  125. branch := strings.TrimSpace(string(out))
  126. tip, err := s.CommitInfo("max(branch(" + branch + "))")
  127. if err != nil {
  128. return "", err
  129. }
  130. curr, err := s.Version()
  131. if err != nil {
  132. return "", err
  133. }
  134. if tip.Commit == curr {
  135. return branch, nil
  136. }
  137. ts, err := s.TagsFromCommit(curr)
  138. if err != nil {
  139. return "", err
  140. }
  141. if len(ts) > 0 {
  142. return ts[0], nil
  143. }
  144. return curr, nil
  145. }
  146. // Date retrieves the date on the latest commit.
  147. func (s *HgRepo) Date() (time.Time, error) {
  148. version, err := s.Version()
  149. if err != nil {
  150. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, "")
  151. }
  152. out, err := s.RunFromDir("hg", "log", "-r", version, "--template", "{date|isodatesec}")
  153. if err != nil {
  154. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  155. }
  156. t, err := time.Parse(longForm, string(out))
  157. if err != nil {
  158. return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
  159. }
  160. return t, nil
  161. }
  162. // CheckLocal verifies the local location is a Git repo.
  163. func (s *HgRepo) CheckLocal() bool {
  164. if _, err := os.Stat(s.LocalPath() + "/.hg"); err == nil {
  165. return true
  166. }
  167. return false
  168. }
  169. // Branches returns a list of available branches
  170. func (s *HgRepo) Branches() ([]string, error) {
  171. out, err := s.RunFromDir("hg", "branches")
  172. if err != nil {
  173. return []string{}, NewLocalError("Unable to retrieve branches", err, string(out))
  174. }
  175. branches := s.referenceList(string(out), `(?m-s)^(\S+)`)
  176. return branches, nil
  177. }
  178. // Tags returns a list of available tags
  179. func (s *HgRepo) Tags() ([]string, error) {
  180. out, err := s.RunFromDir("hg", "tags")
  181. if err != nil {
  182. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  183. }
  184. tags := s.referenceList(string(out), `(?m-s)^(\S+)`)
  185. return tags, nil
  186. }
  187. // IsReference returns if a string is a reference. A reference can be a
  188. // commit id, branch, or tag.
  189. func (s *HgRepo) IsReference(r string) bool {
  190. _, err := s.RunFromDir("hg", "log", "-r", r)
  191. return err == nil
  192. }
  193. // IsDirty returns if the checkout has been modified from the checked
  194. // out reference.
  195. func (s *HgRepo) IsDirty() bool {
  196. out, err := s.RunFromDir("hg", "diff")
  197. return err != nil || len(out) != 0
  198. }
  199. // CommitInfo retrieves metadata about a commit.
  200. func (s *HgRepo) CommitInfo(id string) (*CommitInfo, error) {
  201. out, err := s.RunFromDir("hg", "log", "-r", id, "--style=xml")
  202. if err != nil {
  203. return nil, ErrRevisionUnavailable
  204. }
  205. type Author struct {
  206. Name string `xml:",chardata"`
  207. Email string `xml:"email,attr"`
  208. }
  209. type Logentry struct {
  210. Node string `xml:"node,attr"`
  211. Author Author `xml:"author"`
  212. Date string `xml:"date"`
  213. Msg string `xml:"msg"`
  214. }
  215. type Log struct {
  216. XMLName xml.Name `xml:"log"`
  217. Logs []Logentry `xml:"logentry"`
  218. }
  219. logs := &Log{}
  220. err = xml.Unmarshal(out, &logs)
  221. if err != nil {
  222. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  223. }
  224. if len(logs.Logs) == 0 {
  225. return nil, ErrRevisionUnavailable
  226. }
  227. ci := &CommitInfo{
  228. Commit: logs.Logs[0].Node,
  229. Author: logs.Logs[0].Author.Name + " <" + logs.Logs[0].Author.Email + ">",
  230. Message: logs.Logs[0].Msg,
  231. }
  232. if logs.Logs[0].Date != "" {
  233. ci.Date, err = time.Parse(time.RFC3339, logs.Logs[0].Date)
  234. if err != nil {
  235. return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
  236. }
  237. }
  238. return ci, nil
  239. }
  240. // TagsFromCommit retrieves tags from a commit id.
  241. func (s *HgRepo) TagsFromCommit(id string) ([]string, error) {
  242. // Hg has a single tag per commit. If a second tag is added to a commit a
  243. // new commit is created and the tag is attached to that new commit.
  244. out, err := s.RunFromDir("hg", "log", "-r", id, "--style=xml")
  245. if err != nil {
  246. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  247. }
  248. type Logentry struct {
  249. Node string `xml:"node,attr"`
  250. Tag string `xml:"tag"`
  251. }
  252. type Log struct {
  253. XMLName xml.Name `xml:"log"`
  254. Logs []Logentry `xml:"logentry"`
  255. }
  256. logs := &Log{}
  257. err = xml.Unmarshal(out, &logs)
  258. if err != nil {
  259. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  260. }
  261. if len(logs.Logs) == 0 {
  262. return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
  263. }
  264. t := strings.TrimSpace(logs.Logs[0].Tag)
  265. if t != "" {
  266. return []string{t}, nil
  267. }
  268. return []string{}, nil
  269. }
  270. // Ping returns if remote location is accessible.
  271. func (s *HgRepo) Ping() bool {
  272. _, err := s.run("hg", "identify", "--", s.Remote())
  273. return err == nil
  274. }
  275. // ExportDir exports the current revision to the passed in directory.
  276. func (s *HgRepo) ExportDir(dir string) error {
  277. out, err := s.RunFromDir("hg", "archive", "--", dir)
  278. s.log(out)
  279. if err != nil {
  280. return NewLocalError("Unable to export source", err, string(out))
  281. }
  282. return nil
  283. }