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 }