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