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