gopkg.in/zumata/godep.v14@v14.0.0-20151008182512-99082d62f381/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  
    23  	// run in sandbox repos
    24  	ExistsCmd string
    25  }
    26  
    27  var vcsBzr = &VCS{
    28  	vcs: vcs.ByCmd("bzr"),
    29  
    30  	IdentifyCmd: "version-info --custom --template {revision_id}",
    31  	DescribeCmd: "revno", // TODO(kr): find tag names if possible
    32  	DiffCmd:     "diff -r {rev}",
    33  	ListCmd:     "ls -R",
    34  }
    35  
    36  var vcsGit = &VCS{
    37  	vcs: vcs.ByCmd("git"),
    38  
    39  	IdentifyCmd: "rev-parse HEAD",
    40  	DescribeCmd: "describe --tags",
    41  	DiffCmd:     "diff {rev}",
    42  	ListCmd:     "ls-files",
    43  
    44  	ExistsCmd: "cat-file -e {rev}",
    45  }
    46  
    47  var vcsHg = &VCS{
    48  	vcs: vcs.ByCmd("hg"),
    49  
    50  	IdentifyCmd: "identify --id --debug",
    51  	DescribeCmd: "log -r . --template {latesttag}-{latesttagdistance}",
    52  	DiffCmd:     "diff -r {rev}",
    53  	ListCmd:     "status --all --no-status",
    54  
    55  	ExistsCmd: "cat -r {rev} .",
    56  }
    57  
    58  var cmd = map[*vcs.Cmd]*VCS{
    59  	vcsBzr.vcs: vcsBzr,
    60  	vcsGit.vcs: vcsGit,
    61  	vcsHg.vcs:  vcsHg,
    62  }
    63  
    64  // VCSFromDir returns a VCS value from a directory.
    65  func VCSFromDir(dir, srcRoot string) (*VCS, string, error) {
    66  	vcscmd, reporoot, err := vcs.FromDir(dir, srcRoot)
    67  	if err != nil {
    68  		return nil, "", fmt.Errorf("error while inspecting %q: %v", dir, err)
    69  	}
    70  	vcsext := cmd[vcscmd]
    71  	if vcsext == nil {
    72  		return nil, "", fmt.Errorf("%s is unsupported: %s", vcscmd.Name, dir)
    73  	}
    74  	return vcsext, reporoot, nil
    75  }
    76  
    77  // VCSForImportPath returns a VCS value for an import path.
    78  func VCSForImportPath(importPath string) (*VCS, error) {
    79  	rr, err := vcs.RepoRootForImportPath(importPath, verbose)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	vcs := cmd[rr.VCS]
    84  	if vcs == nil {
    85  		return nil, fmt.Errorf("%s is unsupported: %s", rr.VCS.Name, importPath)
    86  	}
    87  	return vcs, nil
    88  }
    89  
    90  func (v *VCS) identify(dir string) (string, error) {
    91  	out, err := v.runOutput(dir, v.IdentifyCmd)
    92  	return string(bytes.TrimSpace(out)), err
    93  }
    94  
    95  func (v *VCS) describe(dir, rev string) string {
    96  	out, err := v.runOutputVerboseOnly(dir, v.DescribeCmd, "rev", rev)
    97  	if err != nil {
    98  		return ""
    99  	}
   100  	return string(bytes.TrimSpace(out))
   101  }
   102  
   103  func (v *VCS) isDirty(dir, rev string) bool {
   104  	out, err := v.runOutput(dir, v.DiffCmd, "rev", rev)
   105  	return err != nil || len(out) != 0
   106  }
   107  
   108  type vcsFiles map[string]bool
   109  
   110  func (vf vcsFiles) Contains(path string) bool {
   111  	return vf[path]
   112  }
   113  
   114  // listFiles tracked by the VCS in the directory dir, converted to absolute path
   115  func (v *VCS) listFiles(dir string) vcsFiles {
   116  	out, err := v.runOutput(dir, v.ListCmd)
   117  	if err != nil {
   118  		return nil
   119  	}
   120  	files := make(vcsFiles)
   121  	for _, file := range bytes.Split(out, []byte{'\n'}) {
   122  		if len(file) > 0 {
   123  			path, err := filepath.Abs(filepath.Join(dir, string(file)))
   124  			if err != nil {
   125  				panic(err) // this should not happen
   126  			}
   127  			files[path] = true
   128  		}
   129  	}
   130  	return files
   131  }
   132  
   133  func (v *VCS) exists(dir, rev string) bool {
   134  	err := v.runVerboseOnly(dir, v.ExistsCmd, "rev", rev)
   135  	return err == nil
   136  }
   137  
   138  // RevSync checks out the revision given by rev in dir.
   139  // The dir must exist and rev must be a valid revision.
   140  func (v *VCS) RevSync(dir, rev string) error {
   141  	return v.run(dir, v.vcs.TagSyncCmd, "tag", rev)
   142  }
   143  
   144  // run runs the command line cmd in the given directory.
   145  // keyval is a list of key, value pairs.  run expands
   146  // instances of {key} in cmd into value, but only after
   147  // splitting cmd into individual arguments.
   148  // If an error occurs, run prints the command line and the
   149  // command's combined stdout+stderr to standard error.
   150  // Otherwise run discards the command's output.
   151  func (v *VCS) run(dir string, cmdline string, kv ...string) error {
   152  	_, err := v.run1(dir, cmdline, kv, true)
   153  	return err
   154  }
   155  
   156  // runVerboseOnly is like run but only generates error output to standard error in verbose mode.
   157  func (v *VCS) runVerboseOnly(dir string, cmdline string, kv ...string) error {
   158  	_, err := v.run1(dir, cmdline, kv, false)
   159  	return err
   160  }
   161  
   162  // runOutput is like run but returns the output of the command.
   163  func (v *VCS) runOutput(dir string, cmdline string, kv ...string) ([]byte, error) {
   164  	return v.run1(dir, cmdline, kv, true)
   165  }
   166  
   167  // runOutputVerboseOnly is like runOutput but only generates error output to standard error in verbose mode.
   168  func (v *VCS) runOutputVerboseOnly(dir string, cmdline string, kv ...string) ([]byte, error) {
   169  	return v.run1(dir, cmdline, kv, false)
   170  }
   171  
   172  // run1 is the generalized implementation of run and runOutput.
   173  func (v *VCS) run1(dir string, cmdline string, kv []string, verbose bool) ([]byte, error) {
   174  	m := make(map[string]string)
   175  	for i := 0; i < len(kv); i += 2 {
   176  		m[kv[i]] = kv[i+1]
   177  	}
   178  	args := strings.Fields(cmdline)
   179  	for i, arg := range args {
   180  		args[i] = expand(m, arg)
   181  	}
   182  
   183  	_, err := exec.LookPath(v.vcs.Cmd)
   184  	if err != nil {
   185  		fmt.Fprintf(os.Stderr, "godep: missing %s command.\n", v.vcs.Name)
   186  		return nil, err
   187  	}
   188  
   189  	cmd := exec.Command(v.vcs.Cmd, args...)
   190  	cmd.Dir = dir
   191  	var buf bytes.Buffer
   192  	cmd.Stdout = &buf
   193  	cmd.Stderr = &buf
   194  	err = cmd.Run()
   195  	out := buf.Bytes()
   196  	if err != nil {
   197  		if verbose {
   198  			fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.vcs.Cmd, strings.Join(args, " "))
   199  			os.Stderr.Write(out)
   200  		}
   201  		return nil, err
   202  	}
   203  	return out, nil
   204  }
   205  
   206  func expand(m map[string]string, s string) string {
   207  	for k, v := range m {
   208  		s = strings.Replace(s, "{"+k+"}", v, -1)
   209  	}
   210  	return s
   211  }
   212  
   213  // Mercurial has no command equivalent to git remote add.
   214  // We handle it as a special case in process.
   215  func hgLink(dir, remote, url string) error {
   216  	hgdir := filepath.Join(dir, ".hg")
   217  	if err := os.MkdirAll(hgdir, 0777); err != nil {
   218  		return err
   219  	}
   220  	path := filepath.Join(hgdir, "hgrc")
   221  	f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	fmt.Fprintf(f, "[paths]\n%s = %s\n", remote, url)
   226  	return f.Close()
   227  }