gopkg.in/tools/godep.v60@v60.0.0-20160318205742-530caa84a45c/vcs.go (about)

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