blob: d76343cb44fe6be0217820b65b18b39d858166e4 [file] [log] [blame]
package cmd
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
type GitVCS struct{}
var _ VCS = &GitVCS{}
var (
WrongVCS error = errors.New("Wrong VCS detected")
CannotDetermineRepo error = errors.New("Unable to determine repository")
)
var remoteRegex = regexp.MustCompile("^origin\\s+(\\S+)\\s+\\S+$")
// returns the currently checked out remote repository
// according to the state of the working directory
func (g *GitVCS) currentRepository(directory string) (string, error) {
// Make sure to stop bubbling up the directory structures. If the
// repo is another VCS sype, such as HG, git will try to bubble
// up to a parent git repo.
location, err := exec.Command("git", "rev-parse", "--git-dir").CombinedOutput()
if err != nil {
return "", WrongVCS
}
absPath, err := filepath.Abs(string(location))
repoDir := filepath.Dir(absPath)
if repoDir != directory {
return "", WrongVCS
}
out, err := exec.Command("git", "remote", "-v").CombinedOutput()
if err != nil {
return "", WrongVCS
}
for _, line := range strings.Split(string(out), "\n") {
if m := remoteRegex.FindStringSubmatch(line); len(m) == 2 {
return m[1], nil
}
}
return "", CannotDetermineRepo
}
func (g *GitVCS) currentCheckoutMatchesTagOrRef(ref string) bool {
out, err := exec.Command("git", "log", "-n", "1", "--pretty=format:%H%d").CombinedOutput()
if err != nil {
return false
}
parts := strings.SplitN(string(out), " ", 2)
sha := parts[0]
if strings.HasPrefix(sha, ref) {
return true
}
if len(parts) > 1 && strings.Contains(parts[1], "tag: ") {
re := regexp.MustCompile("tag: ([^,)]*)")
for _, tagMatch := range re.FindAllStringSubmatch(parts[1], 10) {
if len(tagMatch) == 2 && tagMatch[1] == ref {
return true
}
}
}
return false
}
// GitGet implements the getting logic for Git.
func (g *GitVCS) Get(dep *Dependency) error {
dest := fmt.Sprintf("%s/src/%s", os.Getenv("GOPATH"), dep.Name)
//Info("Cloning %s into %s\n", dep.Repository, dest)
Info("Git: ")
out, err := exec.Command("git", "clone", dep.Repository, dest).CombinedOutput()
fmt.Print(string(out))
return err
}
func (g *GitVCS) Update(dep *Dependency) error {
dest := fmt.Sprintf("%s/src/%s", os.Getenv("GOPATH"), dep.Name)
if _, err := os.Stat(dest); err != nil {
Info("Looks like %s is a new package. Cloning.\n", dep.Name)
return g.Get(dep)
}
oldDir, err := os.Getwd()
if err != nil {
return err
}
if err := os.Chdir(dest); err != nil {
return err
}
defer os.Chdir(oldDir)
if oldRepo, err := g.currentRepository(dest); err != nil || oldRepo != dep.Repository {
switch err {
case WrongVCS:
Info("VCS type changed ('%s'). I'm doing a fresh clone.\n", err)
case nil:
Info("Repository changed from %s to %s. I'm doing a clean checkout.\n", oldRepo, dep.Repository)
default:
Info("Unable to determine currently checkout out repository ('%s'). I'm doing a fresh clone.\n", err)
}
os.Chdir(oldDir)
if err := os.RemoveAll(dest); err != nil {
return err
}
return g.Get(dep)
}
if g.currentCheckoutMatchesTagOrRef(dep.Reference) {
Info("%s is up to date.\n", dep.Name)
return nil
}
// Because we can't predict which branch we want to be on, and since
// we want to set checkouts explicitly, we should probably fetch.
//out, err := exec.Command("git", "pull", "--ff-only").CombinedOutput()
Info("Git: ")
out, err := exec.Command("git", "fetch", "--all").CombinedOutput()
fmt.Print(string(out))
return err
}
func (g *GitVCS) Version(dep *Dependency) error {
dest := fmt.Sprintf("%s/src/%s", os.Getenv("GOPATH"), dep.Name)
oldDir, err := os.Getwd()
if err != nil {
return err
}
os.Chdir(dest)
defer os.Chdir(oldDir)
//Info("Setting %s with 'git checkout'\n", dep.Name)
// Now get to the right reference.
if out, err := exec.Command("git", "checkout", dep.Reference).CombinedOutput(); err != nil {
fmt.Println(string(out))
return err
} else {
updatedTo := "the latest"
if dep.Reference != "" {
updatedTo = dep.Reference
}
Info("Setting version of %s to %s\n", dep.Name, updatedTo)
//fmt.Print(string(out))
}
// EXPERIMENTAL: This will keep the repo up to date according to the
// master branch on Git. Since 'master' is convention only, this isn't
// a 100% reliable way to do things.
if dep.Reference == "" {
Info("No Git reference set. Trying to update 'master'...\n")
dep.Reference = "master"
}
branchref := fmt.Sprintf("origin/%s", dep.Reference)
//err = exec.Command("git", "show-ref", "-q", branchref).Run()
out, err := exec.Command("git", "show-ref", branchref).CombinedOutput()
if err == nil {
//Info("Git: Found branch %s", string(out))
//Debug("Reference %s is to a branch.", dep.Reference)
// git merge --ff-only origin $VERSION
out, err := exec.Command("git", "pull", "--ff-only", "origin", dep.Reference).CombinedOutput()
Info("Git: (merge) %s", string(out))
if err != nil {
return err
}
}
// EXPERIMENTAL: Show how far behind we are.
out, err = exec.Command("git", "rev-list", "--count", "HEAD..origin").CombinedOutput()
if err == nil {
count := strings.TrimSpace(string(out))
if count != "0" {
var c string
switch len(count) {
// 0-9, not that bad
case 1:
c = Green
// 10-99, we're getting behind
case 2:
c = Yellow
// Whoa! We're falling way behind!
default:
c = Red
}
Info(Color(c, fmt.Sprintf("Git: %s is %s behind origin.\n", dep.Name, count)))
}
}
return nil
}
func (g *GitVCS) LastCommit(dep *Dependency) (string, error) {
dest := fmt.Sprintf("%s/src/%s", os.Getenv("GOPATH"), dep.Name)
oldDir, err := os.Getwd()
if err != nil {
return "", err
}
os.Chdir(dest)
defer os.Chdir(oldDir)
out, err := exec.Command("git", "log", "-n", "1", "--pretty=format:%h%d").CombinedOutput()
if err != nil {
return "", err
}
parts := strings.SplitN(string(out), " ", 2)
sha := parts[0]
// Send back a tag if a tag matches.
if len(parts) > 1 && strings.Contains(parts[1], "tag: ") {
re := regexp.MustCompile("tag: ([^,)]*)")
subs := re.FindStringSubmatch(parts[1])
if len(subs) > 1 {
return subs[1], nil
}
}
return sha, nil
}