repo.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. // Package vcs provides the ability to work with varying version control systems
  2. // (VCS), also known as source control systems (SCM) though the same interface.
  3. //
  4. // This package includes a function that attempts to detect the repo type from
  5. // the remote URL and return the proper type. For example,
  6. //
  7. // remote := "https://github.com/Masterminds/vcs"
  8. // local, _ := ioutil.TempDir("", "go-vcs")
  9. // repo, err := NewRepo(remote, local)
  10. //
  11. // In this case repo will be a GitRepo instance. NewRepo can detect the VCS for
  12. // numerous popular VCS and from the URL. For example, a URL ending in .git
  13. // that's not from one of the popular VCS will be detected as a Git repo and
  14. // the correct type will be returned.
  15. //
  16. // If you know the repository type and would like to create an instance of a
  17. // specific type you can use one of constructors for a type. They are NewGitRepo,
  18. // NewSvnRepo, NewBzrRepo, and NewHgRepo. The definition and usage is the same
  19. // as NewRepo.
  20. //
  21. // Once you have an object implementing the Repo interface the operations are
  22. // the same no matter which VCS you're using. There are some caveats. For
  23. // example, each VCS has its own version formats that need to be respected and
  24. // checkout out branches, if a branch is being worked with, is different in
  25. // each VCS.
  26. package vcs
  27. import (
  28. "fmt"
  29. "io/ioutil"
  30. "log"
  31. "os"
  32. "os/exec"
  33. "regexp"
  34. "strings"
  35. "time"
  36. )
  37. // Logger is where you can provide a logger, implementing the log.Logger interface,
  38. // where verbose output from each VCS will be written. The default logger does
  39. // not log data. To log data supply your own logger or change the output location
  40. // of the provided logger.
  41. var Logger *log.Logger
  42. func init() {
  43. // Initialize the logger to one that does not actually log anywhere. This is
  44. // to be overridden by the package user by setting vcs.Logger to a different
  45. // logger.
  46. Logger = log.New(ioutil.Discard, "go-vcs", log.LstdFlags)
  47. }
  48. const longForm = "2006-01-02 15:04:05 -0700"
  49. // Type describes the type of VCS
  50. type Type string
  51. // VCS types
  52. const (
  53. NoVCS Type = ""
  54. Git Type = "git"
  55. Svn Type = "svn"
  56. Bzr Type = "bzr"
  57. Hg Type = "hg"
  58. )
  59. // Repo provides an interface to work with repositories using different source
  60. // control systems such as Git, Bzr, Mercurial, and SVN. For implementations
  61. // of this interface see BzrRepo, GitRepo, HgRepo, and SvnRepo.
  62. type Repo interface {
  63. // Vcs retrieves the underlying VCS being implemented.
  64. Vcs() Type
  65. // Remote retrieves the remote location for a repo.
  66. Remote() string
  67. // LocalPath retrieves the local file system location for a repo.
  68. LocalPath() string
  69. // Get is used to perform an initial clone/checkout of a repository.
  70. Get() error
  71. // Initializes a new repository locally.
  72. Init() error
  73. // Update performs an update to an existing checkout of a repository.
  74. Update() error
  75. // UpdateVersion sets the version of a package of a repository.
  76. UpdateVersion(string) error
  77. // Version retrieves the current version.
  78. Version() (string, error)
  79. // Current retrieves the current version-ish. This is different from the
  80. // Version method. The output could be a branch name if on the tip of a
  81. // branch (git), a tag if on a tag, a revision if on a specific revision
  82. // that's not the tip of the branch. The values here vary based on the VCS.
  83. Current() (string, error)
  84. // Date retrieves the date on the latest commit.
  85. Date() (time.Time, error)
  86. // CheckLocal verifies the local location is of the correct VCS type
  87. CheckLocal() bool
  88. // Branches returns a list of available branches on the repository.
  89. Branches() ([]string, error)
  90. // Tags returns a list of available tags on the repository.
  91. Tags() ([]string, error)
  92. // IsReference returns if a string is a reference. A reference can be a
  93. // commit id, branch, or tag.
  94. IsReference(string) bool
  95. // IsDirty returns if the checkout has been modified from the checked
  96. // out reference.
  97. IsDirty() bool
  98. // CommitInfo retrieves metadata about a commit.
  99. CommitInfo(string) (*CommitInfo, error)
  100. // TagsFromCommit retrieves tags from a commit id.
  101. TagsFromCommit(string) ([]string, error)
  102. // Ping returns if remote location is accessible.
  103. Ping() bool
  104. // RunFromDir executes a command from repo's directory.
  105. RunFromDir(cmd string, args ...string) ([]byte, error)
  106. // CmdFromDir creates a new command that will be executed from repo's
  107. // directory.
  108. CmdFromDir(cmd string, args ...string) *exec.Cmd
  109. // ExportDir exports the current revision to the passed in directory.
  110. ExportDir(string) error
  111. }
  112. // NewRepo returns a Repo based on trying to detect the source control from the
  113. // remote and local locations. The appropriate implementation will be returned
  114. // or an ErrCannotDetectVCS if the VCS type cannot be detected.
  115. // Note, this function may make calls to the Internet to determind help determine
  116. // the VCS.
  117. func NewRepo(remote, local string) (Repo, error) {
  118. vtype, remote, err := detectVcsFromRemote(remote)
  119. // From the remote URL the VCS could not be detected. See if the local
  120. // repo contains enough information to figure out the VCS. The reason the
  121. // local repo is not checked first is because of the potential for VCS type
  122. // switches which will be detected in each of the type builders.
  123. if err == ErrCannotDetectVCS {
  124. vtype, err = DetectVcsFromFS(local)
  125. }
  126. if err != nil {
  127. return nil, err
  128. }
  129. switch vtype {
  130. case Git:
  131. return NewGitRepo(remote, local)
  132. case Svn:
  133. return NewSvnRepo(remote, local)
  134. case Hg:
  135. return NewHgRepo(remote, local)
  136. case Bzr:
  137. return NewBzrRepo(remote, local)
  138. }
  139. // Should never fall through to here but just in case.
  140. return nil, ErrCannotDetectVCS
  141. }
  142. // CommitInfo contains metadata about a commit.
  143. type CommitInfo struct {
  144. // The commit id
  145. Commit string
  146. // Who authored the commit
  147. Author string
  148. // Date of the commit
  149. Date time.Time
  150. // Commit message
  151. Message string
  152. }
  153. type base struct {
  154. remote, local string
  155. Logger *log.Logger
  156. }
  157. func (b *base) log(v interface{}) {
  158. b.Logger.Printf("%s", v)
  159. }
  160. // Remote retrieves the remote location for a repo.
  161. func (b *base) Remote() string {
  162. return b.remote
  163. }
  164. // LocalPath retrieves the local file system location for a repo.
  165. func (b *base) LocalPath() string {
  166. return b.local
  167. }
  168. func (b *base) setRemote(remote string) {
  169. b.remote = remote
  170. }
  171. func (b *base) setLocalPath(local string) {
  172. b.local = local
  173. }
  174. func (b base) run(cmd string, args ...string) ([]byte, error) {
  175. out, err := exec.Command(cmd, args...).CombinedOutput()
  176. b.log(out)
  177. if err != nil {
  178. err = fmt.Errorf("%s: %s", out, err)
  179. }
  180. return out, err
  181. }
  182. func (b *base) CmdFromDir(cmd string, args ...string) *exec.Cmd {
  183. c := exec.Command(cmd, args...)
  184. c.Dir = b.local
  185. c.Env = envForDir(c.Dir)
  186. return c
  187. }
  188. func (b *base) RunFromDir(cmd string, args ...string) ([]byte, error) {
  189. c := b.CmdFromDir(cmd, args...)
  190. out, err := c.CombinedOutput()
  191. return out, err
  192. }
  193. func (b *base) referenceList(c, r string) []string {
  194. var out []string
  195. re := regexp.MustCompile(r)
  196. for _, m := range re.FindAllStringSubmatch(c, -1) {
  197. out = append(out, m[1])
  198. }
  199. return out
  200. }
  201. func envForDir(dir string) []string {
  202. env := os.Environ()
  203. return mergeEnvLists([]string{"PWD=" + dir}, env)
  204. }
  205. func mergeEnvLists(in, out []string) []string {
  206. NextVar:
  207. for _, inkv := range in {
  208. k := strings.SplitAfterN(inkv, "=", 2)[0]
  209. for i, outkv := range out {
  210. if strings.HasPrefix(outkv, k) {
  211. out[i] = inkv
  212. continue NextVar
  213. }
  214. }
  215. out = append(out, inkv)
  216. }
  217. return out
  218. }
  219. func depInstalled(name string) bool {
  220. if _, err := exec.LookPath(name); err != nil {
  221. return false
  222. }
  223. return true
  224. }