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