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