gopkg.in/tools/godep.v42@v42.0.0-20151223001600-f221061cd941/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-toplevel", 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 (v *VCS) root(dir string) (string, error) { 100 out, err := v.runOutput(dir, v.RootCmd) 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 type vcsFiles map[string]bool 118 119 func (vf vcsFiles) Contains(path string) bool { 120 // Fast path, we have the path 121 if vf[path] { 122 return true 123 } 124 125 // Slow path for case insensitive filesystems 126 // See #310 127 for f := range vf { 128 if strings.EqualFold(f, path) { 129 return true 130 } 131 // git's root command (maybe other vcs as well) resolve symlinks, so try that too 132 // 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. 133 p, err := filepath.EvalSymlinks(path) 134 if err != nil { 135 return false 136 } 137 if strings.EqualFold(f, p) { 138 return true 139 } 140 } 141 142 // No matches by either method 143 return false 144 } 145 146 // listFiles tracked by the VCS in the repo that contains dir, converted to absolute path. 147 func (v *VCS) listFiles(dir string) vcsFiles { 148 root, err := v.root(dir) 149 if err != nil { 150 return nil 151 } 152 out, err := v.runOutput(dir, v.ListCmd) 153 if err != nil { 154 return nil 155 } 156 files := make(vcsFiles) 157 for _, file := range bytes.Split(out, []byte{'\n'}) { 158 if len(file) > 0 { 159 path, err := filepath.Abs(filepath.Join(string(root), string(file))) 160 if err != nil { 161 panic(err) // this should not happen 162 } 163 files[path] = true 164 } 165 } 166 return files 167 } 168 169 func (v *VCS) exists(dir, rev string) bool { 170 err := v.runVerboseOnly(dir, v.ExistsCmd, "rev", rev) 171 return err == nil 172 } 173 174 // RevSync checks out the revision given by rev in dir. 175 // The dir must exist and rev must be a valid revision. 176 func (v *VCS) RevSync(dir, rev string) error { 177 return v.run(dir, v.vcs.TagSyncCmd, "tag", rev) 178 } 179 180 // run runs the command line cmd in the given directory. 181 // keyval is a list of key, value pairs. run expands 182 // instances of {key} in cmd into value, but only after 183 // splitting cmd into individual arguments. 184 // If an error occurs, run prints the command line and the 185 // command's combined stdout+stderr to standard error. 186 // Otherwise run discards the command's output. 187 func (v *VCS) run(dir string, cmdline string, kv ...string) error { 188 _, err := v.run1(dir, cmdline, kv, true) 189 return err 190 } 191 192 // runVerboseOnly is like run but only generates error output to standard error in verbose mode. 193 func (v *VCS) runVerboseOnly(dir string, cmdline string, kv ...string) error { 194 _, err := v.run1(dir, cmdline, kv, false) 195 return err 196 } 197 198 // runOutput is like run but returns the output of the command. 199 func (v *VCS) runOutput(dir string, cmdline string, kv ...string) ([]byte, error) { 200 return v.run1(dir, cmdline, kv, true) 201 } 202 203 // runOutputVerboseOnly is like runOutput but only generates error output to standard error in verbose mode. 204 func (v *VCS) runOutputVerboseOnly(dir string, cmdline string, kv ...string) ([]byte, error) { 205 return v.run1(dir, cmdline, kv, false) 206 } 207 208 // run1 is the generalized implementation of run and runOutput. 209 func (v *VCS) run1(dir string, cmdline string, kv []string, verbose bool) ([]byte, error) { 210 m := make(map[string]string) 211 for i := 0; i < len(kv); i += 2 { 212 m[kv[i]] = kv[i+1] 213 } 214 args := strings.Fields(cmdline) 215 for i, arg := range args { 216 args[i] = expand(m, arg) 217 } 218 219 _, err := exec.LookPath(v.vcs.Cmd) 220 if err != nil { 221 fmt.Fprintf(os.Stderr, "godep: missing %s command.\n", v.vcs.Name) 222 return nil, err 223 } 224 225 cmd := exec.Command(v.vcs.Cmd, args...) 226 cmd.Dir = dir 227 var buf bytes.Buffer 228 cmd.Stdout = &buf 229 cmd.Stderr = &buf 230 err = cmd.Run() 231 out := buf.Bytes() 232 if err != nil { 233 if verbose { 234 fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.vcs.Cmd, strings.Join(args, " ")) 235 os.Stderr.Write(out) 236 } 237 return nil, err 238 } 239 return out, nil 240 } 241 242 func expand(m map[string]string, s string) string { 243 for k, v := range m { 244 s = strings.Replace(s, "{"+k+"}", v, -1) 245 } 246 return s 247 } 248 249 // Mercurial has no command equivalent to git remote add. 250 // We handle it as a special case in process. 251 func hgLink(dir, remote, url string) error { 252 hgdir := filepath.Join(dir, ".hg") 253 if err := os.MkdirAll(hgdir, 0777); err != nil { 254 return err 255 } 256 path := filepath.Join(hgdir, "hgrc") 257 f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) 258 if err != nil { 259 return err 260 } 261 fmt.Fprintf(f, "[paths]\n%s = %s\n", remote, url) 262 return f.Close() 263 }