blob: 83034f276173775b9814033eabea657d2d36be3d [file] [log] [blame]
package main
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/tools/go/vcs"
)
// VCS represents a version control system.
type VCS struct {
vcs *vcs.Cmd
IdentifyCmd string
DescribeCmd string
DiffCmd string
ListCmd string
RootCmd string
// run in sandbox repos
ExistsCmd string
}
var vcsBzr = &VCS{
vcs: vcs.ByCmd("bzr"),
IdentifyCmd: "version-info --custom --template {revision_id}",
DescribeCmd: "revno", // TODO(kr): find tag names if possible
DiffCmd: "diff -r {rev}",
ListCmd: "ls --from-root -R",
RootCmd: "root",
}
var vcsGit = &VCS{
vcs: vcs.ByCmd("git"),
IdentifyCmd: "rev-parse HEAD",
DescribeCmd: "describe --tags --abbrev=14",
DiffCmd: "diff {rev}",
ListCmd: "ls-files --full-name",
RootCmd: "rev-parse --show-cdup",
ExistsCmd: "cat-file -e {rev}",
}
var vcsHg = &VCS{
vcs: vcs.ByCmd("hg"),
IdentifyCmd: "parents --template {node}",
DescribeCmd: "log -r . --template {latesttag}-{latesttagdistance}",
DiffCmd: "diff -r {rev}",
ListCmd: "status --all --no-status",
RootCmd: "root",
ExistsCmd: "cat -r {rev} .",
}
var cmd = map[*vcs.Cmd]*VCS{
vcsBzr.vcs: vcsBzr,
vcsGit.vcs: vcsGit,
vcsHg.vcs: vcsHg,
}
// VCSFromDir returns a VCS value from a directory.
func VCSFromDir(dir, srcRoot string) (*VCS, string, error) {
vcscmd, reporoot, err := vcs.FromDir(dir, srcRoot)
if err != nil {
return nil, "", fmt.Errorf("error while inspecting %q: %v", dir, err)
}
vcsext := cmd[vcscmd]
if vcsext == nil {
return nil, "", fmt.Errorf("%s is unsupported: %s", vcscmd.Name, dir)
}
return vcsext, reporoot, nil
}
func (v *VCS) identify(dir string) (string, error) {
out, err := v.runOutput(dir, v.IdentifyCmd)
return string(bytes.TrimSpace(out)), err
}
func absRoot(dir, out string) string {
if filepath.IsAbs(out) {
return filepath.Clean(out)
}
return filepath.Join(dir, out)
}
func (v *VCS) root(dir string) (string, error) {
out, err := v.runOutput(dir, v.RootCmd)
return absRoot(dir, string(bytes.TrimSpace(out))), err
}
func (v *VCS) describe(dir, rev string) string {
out, err := v.runOutputVerboseOnly(dir, v.DescribeCmd, "rev", rev)
if err != nil {
return ""
}
return string(bytes.TrimSpace(out))
}
func (v *VCS) isDirty(dir, rev string) bool {
out, err := v.runOutput(dir, v.DiffCmd, "rev", rev)
return err != nil || len(out) != 0
}
type vcsFiles map[string]bool
func (vf vcsFiles) Contains(path string) bool {
// Fast path, we have the path
if vf[path] {
return true
}
// Slow path for case insensitive filesystems
// See #310
for f := range vf {
if pathEqual(f, path) {
return true
}
// git's root command (maybe other vcs as well) resolve symlinks, so try that too
// FIXME: rev-parse --show-cdup + extra logic will fix this for git but also need to validate the other vcs commands. This is maybe temporary.
p, err := filepath.EvalSymlinks(path)
if err != nil {
return false
}
if pathEqual(f, p) {
return true
}
}
// No matches by either method
return false
}
// listFiles tracked by the VCS in the repo that contains dir, converted to absolute path.
func (v *VCS) listFiles(dir string) vcsFiles {
root, err := v.root(dir)
debugln("vcs dir", dir)
debugln("vcs root", root)
ppln(v)
if err != nil {
return nil
}
out, err := v.runOutput(dir, v.ListCmd)
if err != nil {
return nil
}
files := make(vcsFiles)
for _, file := range bytes.Split(out, []byte{'\n'}) {
if len(file) > 0 {
path, err := filepath.Abs(filepath.Join(root, string(file)))
if err != nil {
panic(err) // this should not happen
}
if pathEqual(filepath.Dir(path), dir) {
files[path] = true
}
}
}
return files
}
func (v *VCS) exists(dir, rev string) bool {
err := v.runVerboseOnly(dir, v.ExistsCmd, "rev", rev)
return err == nil
}
// RevSync checks out the revision given by rev in dir.
// The dir must exist and rev must be a valid revision.
func (v *VCS) RevSync(dir, rev string) error {
return v.run(dir, v.vcs.TagSyncCmd, "tag", rev)
}
// run runs the command line cmd in the given directory.
// keyval is a list of key, value pairs. run expands
// instances of {key} in cmd into value, but only after
// splitting cmd into individual arguments.
// If an error occurs, run prints the command line and the
// command's combined stdout+stderr to standard error.
// Otherwise run discards the command's output.
func (v *VCS) run(dir string, cmdline string, kv ...string) error {
_, err := v.run1(dir, cmdline, kv, true)
return err
}
// runVerboseOnly is like run but only generates error output to standard error in verbose mode.
func (v *VCS) runVerboseOnly(dir string, cmdline string, kv ...string) error {
_, err := v.run1(dir, cmdline, kv, false)
return err
}
// runOutput is like run but returns the output of the command.
func (v *VCS) runOutput(dir string, cmdline string, kv ...string) ([]byte, error) {
return v.run1(dir, cmdline, kv, true)
}
// runOutputVerboseOnly is like runOutput but only generates error output to standard error in verbose mode.
func (v *VCS) runOutputVerboseOnly(dir string, cmdline string, kv ...string) ([]byte, error) {
return v.run1(dir, cmdline, kv, false)
}
// run1 is the generalized implementation of run and runOutput.
func (v *VCS) run1(dir string, cmdline string, kv []string, verbose bool) ([]byte, error) {
m := make(map[string]string)
for i := 0; i < len(kv); i += 2 {
m[kv[i]] = kv[i+1]
}
args := strings.Fields(cmdline)
for i, arg := range args {
args[i] = expand(m, arg)
}
_, err := exec.LookPath(v.vcs.Cmd)
if err != nil {
fmt.Fprintf(os.Stderr, "godep: missing %s command.\n", v.vcs.Name)
return nil, err
}
cmd := exec.Command(v.vcs.Cmd, args...)
cmd.Dir = dir
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
err = cmd.Run()
out := buf.Bytes()
if err != nil {
if verbose {
fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.vcs.Cmd, strings.Join(args, " "))
os.Stderr.Write(out)
}
return nil, err
}
return out, nil
}
func expand(m map[string]string, s string) string {
for k, v := range m {
s = strings.Replace(s, "{"+k+"}", v, -1)
}
return s
}
func gitDetached(r string) (bool, error) {
o, err := vcsGit.runOutput(r, "status")
if err != nil {
return false, errors.New("unable to determine git status " + err.Error())
}
return bytes.Contains(o, []byte("HEAD detached at")), nil
}
func gitDefaultBranch(r string) (string, error) {
o, err := vcsGit.runOutput(r, "remote show origin")
if err != nil {
return "", errors.New("Running git remote show origin errored with: " + err.Error())
}
return gitDetermineDefaultBranch(r, string(o))
}
func gitDetermineDefaultBranch(r, o string) (string, error) {
e := "Unable to determine HEAD branch: "
hb := "HEAD branch:"
lbcfgp := "Local branch configured for 'git pull':"
s := strings.Index(o, hb)
if s < 0 {
b := strings.Index(o, lbcfgp)
if b < 0 {
return "", errors.New(e + "Remote HEAD is ambiguous. Before godep can pull new commits you will need to:" + `
cd ` + r + `
git checkout <a HEAD branch>
Here is what was reported:
` + o)
}
s = b + len(lbcfgp)
} else {
s += len(hb)
}
f := strings.Fields(o[s:])
if len(f) < 3 {
return "", errors.New(e + "git output too short")
}
return f[0], nil
}
func gitCheckout(r, b string) error {
return vcsGit.run(r, "checkout "+b)
}