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