github.com/matthewbelisle-wf/godep@v0.0.0-20140716191328-dba190f14fc8/vcs.go (about)

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