| // Package vcs provides the ability to work with varying version control systems |
| // (VCS), also known as source control systems (SCM) though the same interface. |
| // |
| // This package includes a function that attempts to detect the repo type from |
| // the remote URL and return the proper type. For example, |
| // |
| // remote := "https://github.com/Masterminds/vcs" |
| // local, _ := ioutil.TempDir("", "go-vcs") |
| // repo, err := NewRepo(remote, local) |
| // |
| // In this case repo will be a GitRepo instance. NewRepo can detect the VCS for |
| // numerous popular VCS and from the URL. For example, a URL ending in .git |
| // that's not from one of the popular VCS will be detected as a Git repo and |
| // the correct type will be returned. |
| // |
| // If you know the repository type and would like to create an instance of a |
| // specific type you can use one of constructors for a type. They are NewGitRepo, |
| // NewSvnRepo, NewBzrRepo, and NewHgRepo. The definition and usage is the same |
| // as NewRepo. |
| // |
| // Once you have an object implementing the Repo interface the operations are |
| // the same no matter which VCS you're using. There are some caveats. For |
| // example, each VCS has its own version formats that need to be respected and |
| // checkout out branches, if a branch is being worked with, is different in |
| // each VCS. |
| package vcs |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "regexp" |
| "strings" |
| "time" |
| ) |
| |
| // Logger is where you can provide a logger, implementing the log.Logger interface, |
| // where verbose output from each VCS will be written. The default logger does |
| // not log data. To log data supply your own logger or change the output location |
| // of the provided logger. |
| var Logger *log.Logger |
| |
| func init() { |
| // Initialize the logger to one that does not actually log anywhere. This is |
| // to be overridden by the package user by setting vcs.Logger to a different |
| // logger. |
| Logger = log.New(ioutil.Discard, "go-vcs", log.LstdFlags) |
| } |
| |
| const longForm = "2006-01-02 15:04:05 -0700" |
| |
| // Type describes the type of VCS |
| type Type string |
| |
| // VCS types |
| const ( |
| NoVCS Type = "" |
| Git Type = "git" |
| Svn Type = "svn" |
| Bzr Type = "bzr" |
| Hg Type = "hg" |
| ) |
| |
| // Repo provides an interface to work with repositories using different source |
| // control systems such as Git, Bzr, Mercurial, and SVN. For implementations |
| // of this interface see BzrRepo, GitRepo, HgRepo, and SvnRepo. |
| type Repo interface { |
| |
| // Vcs retrieves the underlying VCS being implemented. |
| Vcs() Type |
| |
| // Remote retrieves the remote location for a repo. |
| Remote() string |
| |
| // LocalPath retrieves the local file system location for a repo. |
| LocalPath() string |
| |
| // Get is used to perform an initial clone/checkout of a repository. |
| Get() error |
| |
| // Initializes a new repository locally. |
| Init() error |
| |
| // Update performs an update to an existing checkout of a repository. |
| Update() error |
| |
| // UpdateVersion sets the version of a package of a repository. |
| UpdateVersion(string) error |
| |
| // Version retrieves the current version. |
| Version() (string, error) |
| |
| // Current retrieves the current version-ish. This is different from the |
| // Version method. The output could be a branch name if on the tip of a |
| // branch (git), a tag if on a tag, a revision if on a specific revision |
| // that's not the tip of the branch. The values here vary based on the VCS. |
| Current() (string, error) |
| |
| // Date retrieves the date on the latest commit. |
| Date() (time.Time, error) |
| |
| // CheckLocal verifies the local location is of the correct VCS type |
| CheckLocal() bool |
| |
| // Branches returns a list of available branches on the repository. |
| Branches() ([]string, error) |
| |
| // Tags returns a list of available tags on the repository. |
| Tags() ([]string, error) |
| |
| // IsReference returns if a string is a reference. A reference can be a |
| // commit id, branch, or tag. |
| IsReference(string) bool |
| |
| // IsDirty returns if the checkout has been modified from the checked |
| // out reference. |
| IsDirty() bool |
| |
| // CommitInfo retrieves metadata about a commit. |
| CommitInfo(string) (*CommitInfo, error) |
| |
| // TagsFromCommit retrieves tags from a commit id. |
| TagsFromCommit(string) ([]string, error) |
| |
| // Ping returns if remote location is accessible. |
| Ping() bool |
| |
| // RunFromDir executes a command from repo's directory. |
| RunFromDir(cmd string, args ...string) ([]byte, error) |
| |
| // ExportDir exports the current revision to the passed in directory. |
| ExportDir(string) error |
| } |
| |
| // NewRepo returns a Repo based on trying to detect the source control from the |
| // remote and local locations. The appropriate implementation will be returned |
| // or an ErrCannotDetectVCS if the VCS type cannot be detected. |
| // Note, this function may make calls to the Internet to determind help determine |
| // the VCS. |
| func NewRepo(remote, local string) (Repo, error) { |
| vtype, remote, err := detectVcsFromRemote(remote) |
| |
| // From the remote URL the VCS could not be detected. See if the local |
| // repo contains enough information to figure out the VCS. The reason the |
| // local repo is not checked first is because of the potential for VCS type |
| // switches which will be detected in each of the type builders. |
| if err == ErrCannotDetectVCS { |
| vtype, err = DetectVcsFromFS(local) |
| } |
| |
| if err != nil { |
| return nil, err |
| } |
| |
| switch vtype { |
| case Git: |
| return NewGitRepo(remote, local) |
| case Svn: |
| return NewSvnRepo(remote, local) |
| case Hg: |
| return NewHgRepo(remote, local) |
| case Bzr: |
| return NewBzrRepo(remote, local) |
| } |
| |
| // Should never fall through to here but just in case. |
| return nil, ErrCannotDetectVCS |
| } |
| |
| // CommitInfo contains metadata about a commit. |
| type CommitInfo struct { |
| // The commit id |
| Commit string |
| |
| // Who authored the commit |
| Author string |
| |
| // Date of the commit |
| Date time.Time |
| |
| // Commit message |
| Message string |
| } |
| |
| type base struct { |
| remote, local string |
| Logger *log.Logger |
| } |
| |
| func (b *base) log(v interface{}) { |
| b.Logger.Printf("%s", v) |
| } |
| |
| // Remote retrieves the remote location for a repo. |
| func (b *base) Remote() string { |
| return b.remote |
| } |
| |
| // LocalPath retrieves the local file system location for a repo. |
| func (b *base) LocalPath() string { |
| return b.local |
| } |
| |
| func (b *base) setRemote(remote string) { |
| b.remote = remote |
| } |
| |
| func (b *base) setLocalPath(local string) { |
| b.local = local |
| } |
| |
| func (b base) run(cmd string, args ...string) ([]byte, error) { |
| out, err := exec.Command(cmd, args...).CombinedOutput() |
| b.log(out) |
| if err != nil { |
| err = fmt.Errorf("%s: %s", out, err) |
| } |
| return out, err |
| } |
| |
| func (b *base) RunFromDir(cmd string, args ...string) ([]byte, error) { |
| c := exec.Command(cmd, args...) |
| c.Dir = b.local |
| c.Env = envForDir(c.Dir) |
| out, err := c.CombinedOutput() |
| return out, err |
| } |
| |
| func (b *base) referenceList(c, r string) []string { |
| var out []string |
| re := regexp.MustCompile(r) |
| for _, m := range re.FindAllStringSubmatch(c, -1) { |
| out = append(out, m[1]) |
| } |
| |
| return out |
| } |
| |
| func envForDir(dir string) []string { |
| env := os.Environ() |
| return mergeEnvLists([]string{"PWD=" + dir}, env) |
| } |
| |
| func mergeEnvLists(in, out []string) []string { |
| NextVar: |
| for _, inkv := range in { |
| k := strings.SplitAfterN(inkv, "=", 2)[0] |
| for i, outkv := range out { |
| if strings.HasPrefix(outkv, k) { |
| out[i] = inkv |
| continue NextVar |
| } |
| } |
| out = append(out, inkv) |
| } |
| return out |
| } |