gopkg.in/tools/godep.v76@v76.0.0-20170110172504-88cb03c7cb62/vcs.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"golang.org/x/tools/go/vcs"
    13  )
    14  
    15  // VCS represents a version control system.
    16  type VCS struct {
    17  	vcs *vcs.Cmd
    18  
    19  	IdentifyCmd string
    20  	DescribeCmd string
    21  	DiffCmd     string
    22  	ListCmd     string
    23  	RootCmd     string
    24  
    25  	// run in sandbox repos
    26  	ExistsCmd string
    27  }
    28  
    29  var vcsBzr = &VCS{
    30  	vcs: vcs.ByCmd("bzr"),
    31  
    32  	IdentifyCmd: "version-info --custom --template {revision_id}",
    33  	DescribeCmd: "revno", // TODO(kr): find tag names if possible
    34  	DiffCmd:     "diff -r {rev}",
    35  	ListCmd:     "ls --from-root -R",
    36  	RootCmd:     "root",
    37  }
    38  
    39  var vcsGit = &VCS{
    40  	vcs: vcs.ByCmd("git"),
    41  
    42  	IdentifyCmd: "rev-parse HEAD",
    43  	DescribeCmd: "describe --tags",
    44  	DiffCmd:     "diff {rev}",
    45  	ListCmd:     "ls-files --full-name",
    46  	RootCmd:     "rev-parse --show-cdup",
    47  
    48  	ExistsCmd: "cat-file -e {rev}",
    49  }
    50  
    51  var vcsHg = &VCS{
    52  	vcs: vcs.ByCmd("hg"),
    53  
    54  	IdentifyCmd: "parents --template '{node}'",
    55  	DescribeCmd: "log -r . --template {latesttag}-{latesttagdistance}",
    56  	DiffCmd:     "diff -r {rev}",
    57  	ListCmd:     "status --all --no-status",
    58  	RootCmd:     "root",
    59  
    60  	ExistsCmd: "cat -r {rev} .",
    61  }
    62  
    63  var cmd = map[*vcs.Cmd]*VCS{
    64  	vcsBzr.vcs: vcsBzr,
    65  	vcsGit.vcs: vcsGit,
    66  	vcsHg.vcs:  vcsHg,
    67  }
    68  
    69  // VCSFromDir returns a VCS value from a directory.
    70  func VCSFromDir(dir, srcRoot string) (*VCS, string, error) {
    71  	vcscmd, reporoot, err := vcs.FromDir(dir, srcRoot)
    72  	if err != nil {
    73  		return nil, "", fmt.Errorf("error while inspecting %q: %v", dir, err)
    74  	}
    75  	vcsext := cmd[vcscmd]
    76  	if vcsext == nil {
    77  		return nil, "", fmt.Errorf("%s is unsupported: %s", vcscmd.Name, dir)
    78  	}
    79  	return vcsext, reporoot, nil
    80  }
    81  
    82  // VCSForImportPath returns a VCS value for an import path.
    83  func VCSForImportPath(importPath string) (*VCS, error) {
    84  	rr, err := vcs.RepoRootForImportPath(importPath, debug)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	vcs := cmd[rr.VCS]
    89  	if vcs == nil {
    90  		return nil, fmt.Errorf("%s is unsupported: %s", rr.VCS.Name, importPath)
    91  	}
    92  	return vcs, nil
    93  }
    94  
    95  func (v *VCS) identify(dir string) (string, error) {
    96  	out, err := v.runOutput(dir, v.IdentifyCmd)
    97  	return string(bytes.TrimSpace(out)), err
    98  }
    99  
   100  func absRoot(dir, out string) string {
   101  	if filepath.IsAbs(out) {
   102  		return filepath.Clean(out)
   103  	}
   104  	return filepath.Join(dir, out)
   105  }
   106  
   107  func (v *VCS) root(dir string) (string, error) {
   108  	out, err := v.runOutput(dir, v.RootCmd)
   109  	return absRoot(dir, string(bytes.TrimSpace(out))), err
   110  }
   111  
   112  func (v *VCS) describe(dir, rev string) string {
   113  	out, err := v.runOutputVerboseOnly(dir, v.DescribeCmd, "rev", rev)
   114  	if err != nil {
   115  		return ""
   116  	}
   117  	return string(bytes.TrimSpace(out))
   118  }
   119  
   120  func (v *VCS) isDirty(dir, rev string) bool {
   121  	out, err := v.runOutput(dir, v.DiffCmd, "rev", rev)
   122  	return err != nil || len(out) != 0
   123  }
   124  
   125  type vcsFiles map[string]bool
   126  
   127  func (vf vcsFiles) Contains(path string) bool {
   128  	// Fast path, we have the path
   129  	if vf[path] {
   130  		return true
   131  	}
   132  
   133  	// Slow path for case insensitive filesystems
   134  	// See #310
   135  	for f := range vf {
   136  		if pathEqual(f, path) {
   137  			return true
   138  		}
   139  		// git's root command (maybe other vcs as well) resolve symlinks, so try that too
   140  		// 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.
   141  		p, err := filepath.EvalSymlinks(path)
   142  		if err != nil {
   143  			return false
   144  		}
   145  		if pathEqual(f, p) {
   146  			return true
   147  		}
   148  	}
   149  
   150  	// No matches by either method
   151  	return false
   152  }
   153  
   154  // listFiles tracked by the VCS in the repo that contains dir, converted to absolute path.
   155  func (v *VCS) listFiles(dir string) vcsFiles {
   156  	root, err := v.root(dir)
   157  	debugln("vcs dir", dir)
   158  	debugln("vcs root", root)
   159  	ppln(v)
   160  	if err != nil {
   161  		return nil
   162  	}
   163  	out, err := v.runOutput(dir, v.ListCmd)
   164  	if err != nil {
   165  		return nil
   166  	}
   167  	files := make(vcsFiles)
   168  	for _, file := range bytes.Split(out, []byte{'\n'}) {
   169  		if len(file) > 0 {
   170  			path, err := filepath.Abs(filepath.Join(root, string(file)))
   171  			if err != nil {
   172  				panic(err) // this should not happen
   173  			}
   174  
   175  			if pathEqual(filepath.Dir(path), dir) {
   176  				files[path] = true
   177  			}
   178  		}
   179  	}
   180  	return files
   181  }
   182  
   183  func (v *VCS) exists(dir, rev string) bool {
   184  	err := v.runVerboseOnly(dir, v.ExistsCmd, "rev", rev)
   185  	return err == nil
   186  }
   187  
   188  // RevSync checks out the revision given by rev in dir.
   189  // The dir must exist and rev must be a valid revision.
   190  func (v *VCS) RevSync(dir, rev string) error {
   191  	return v.run(dir, v.vcs.TagSyncCmd, "tag", rev)
   192  }
   193  
   194  // run runs the command line cmd in the given directory.
   195  // keyval is a list of key, value pairs.  run expands
   196  // instances of {key} in cmd into value, but only after
   197  // splitting cmd into individual arguments.
   198  // If an error occurs, run prints the command line and the
   199  // command's combined stdout+stderr to standard error.
   200  // Otherwise run discards the command's output.
   201  func (v *VCS) run(dir string, cmdline string, kv ...string) error {
   202  	_, err := v.run1(dir, cmdline, kv, true)
   203  	return err
   204  }
   205  
   206  // runVerboseOnly is like run but only generates error output to standard error in verbose mode.
   207  func (v *VCS) runVerboseOnly(dir string, cmdline string, kv ...string) error {
   208  	_, err := v.run1(dir, cmdline, kv, false)
   209  	return err
   210  }
   211  
   212  // runOutput is like run but returns the output of the command.
   213  func (v *VCS) runOutput(dir string, cmdline string, kv ...string) ([]byte, error) {
   214  	return v.run1(dir, cmdline, kv, true)
   215  }
   216  
   217  // runOutputVerboseOnly is like runOutput but only generates error output to standard error in verbose mode.
   218  func (v *VCS) runOutputVerboseOnly(dir string, cmdline string, kv ...string) ([]byte, error) {
   219  	return v.run1(dir, cmdline, kv, false)
   220  }
   221  
   222  // run1 is the generalized implementation of run and runOutput.
   223  func (v *VCS) run1(dir string, cmdline string, kv []string, verbose bool) ([]byte, error) {
   224  	m := make(map[string]string)
   225  	for i := 0; i < len(kv); i += 2 {
   226  		m[kv[i]] = kv[i+1]
   227  	}
   228  	args := strings.Fields(cmdline)
   229  	for i, arg := range args {
   230  		args[i] = expand(m, arg)
   231  	}
   232  
   233  	_, err := exec.LookPath(v.vcs.Cmd)
   234  	if err != nil {
   235  		fmt.Fprintf(os.Stderr, "godep: missing %s command.\n", v.vcs.Name)
   236  		return nil, err
   237  	}
   238  
   239  	cmd := exec.Command(v.vcs.Cmd, args...)
   240  	cmd.Dir = dir
   241  	var buf bytes.Buffer
   242  	cmd.Stdout = &buf
   243  	cmd.Stderr = &buf
   244  	err = cmd.Run()
   245  	out := buf.Bytes()
   246  	if err != nil {
   247  		if verbose {
   248  			fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.vcs.Cmd, strings.Join(args, " "))
   249  			os.Stderr.Write(out)
   250  		}
   251  		return nil, err
   252  	}
   253  	return out, nil
   254  }
   255  
   256  func expand(m map[string]string, s string) string {
   257  	for k, v := range m {
   258  		s = strings.Replace(s, "{"+k+"}", v, -1)
   259  	}
   260  	return s
   261  }
   262  
   263  // Mercurial has no command equivalent to git remote add.
   264  // We handle it as a special case in process.
   265  func hgLink(dir, remote, url string) error {
   266  	hgdir := filepath.Join(dir, ".hg")
   267  	if err := os.MkdirAll(hgdir, 0777); err != nil {
   268  		return err
   269  	}
   270  	path := filepath.Join(hgdir, "hgrc")
   271  	f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	fmt.Fprintf(f, "[paths]\n%s = %s\n", remote, url)
   276  	return f.Close()
   277  }
   278  
   279  func gitDetached(r string) (bool, error) {
   280  	o, err := vcsGit.runOutput(r, "status")
   281  	if err != nil {
   282  		return false, errors.New("unable to determine git status " + err.Error())
   283  	}
   284  	return bytes.Contains(o, []byte("HEAD detached at")), nil
   285  }
   286  
   287  func gitDefaultBranch(r string) (string, error) {
   288  	o, err := vcsGit.runOutput(r, "remote show origin")
   289  	if err != nil {
   290  		return "", errors.New("Running git remote show origin errored with: " + err.Error())
   291  	}
   292  	return gitDetermineDefaultBranch(r, string(o))
   293  }
   294  
   295  func gitDetermineDefaultBranch(r, o string) (string, error) {
   296  	e := "Unable to determine HEAD branch: "
   297  	hb := "HEAD branch:"
   298  	lbcfgp := "Local branch configured for 'git pull':"
   299  	s := strings.Index(o, hb)
   300  	if s < 0 {
   301  		b := strings.Index(o, lbcfgp)
   302  		if b < 0 {
   303  			return "", errors.New(e + "Remote HEAD is ambiguous. Before godep can pull new commits you will need to:" + `
   304  cd ` + r + `
   305  git checkout <a HEAD branch>
   306  Here is what was reported:
   307  ` + o)
   308  		}
   309  		s = b + len(lbcfgp)
   310  	} else {
   311  		s += len(hb)
   312  	}
   313  	f := strings.Fields(o[s:])
   314  	if len(f) < 3 {
   315  		return "", errors.New(e + "git output too short")
   316  	}
   317  	return f[0], nil
   318  }
   319  
   320  func gitCheckout(r, b string) error {
   321  	return vcsGit.run(r, "checkout "+b)
   322  }