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