github.com/tools/godep@v0.0.0-20180126220526-ce0bfadeb516/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 func (v *VCS) identify(dir string) (string, error) { 83 out, err := v.runOutput(dir, v.IdentifyCmd) 84 return string(bytes.TrimSpace(out)), err 85 } 86 87 func absRoot(dir, out string) string { 88 if filepath.IsAbs(out) { 89 return filepath.Clean(out) 90 } 91 return filepath.Join(dir, out) 92 } 93 94 func (v *VCS) root(dir string) (string, error) { 95 out, err := v.runOutput(dir, v.RootCmd) 96 return absRoot(dir, string(bytes.TrimSpace(out))), err 97 } 98 99 func (v *VCS) describe(dir, rev string) string { 100 out, err := v.runOutputVerboseOnly(dir, v.DescribeCmd, "rev", rev) 101 if err != nil { 102 return "" 103 } 104 return string(bytes.TrimSpace(out)) 105 } 106 107 func (v *VCS) isDirty(dir, rev string) bool { 108 out, err := v.runOutput(dir, v.DiffCmd, "rev", rev) 109 return err != nil || len(out) != 0 110 } 111 112 type vcsFiles map[string]bool 113 114 func (vf vcsFiles) Contains(path string) bool { 115 // Fast path, we have the path 116 if vf[path] { 117 return true 118 } 119 120 // Slow path for case insensitive filesystems 121 // See #310 122 for f := range vf { 123 if pathEqual(f, path) { 124 return true 125 } 126 // git's root command (maybe other vcs as well) resolve symlinks, so try that too 127 // 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. 128 p, err := filepath.EvalSymlinks(path) 129 if err != nil { 130 return false 131 } 132 if pathEqual(f, p) { 133 return true 134 } 135 } 136 137 // No matches by either method 138 return false 139 } 140 141 // listFiles tracked by the VCS in the repo that contains dir, converted to absolute path. 142 func (v *VCS) listFiles(dir string) vcsFiles { 143 root, err := v.root(dir) 144 debugln("vcs dir", dir) 145 debugln("vcs root", root) 146 ppln(v) 147 if err != nil { 148 return nil 149 } 150 out, err := v.runOutput(dir, v.ListCmd) 151 if err != nil { 152 return nil 153 } 154 files := make(vcsFiles) 155 for _, file := range bytes.Split(out, []byte{'\n'}) { 156 if len(file) > 0 { 157 path, err := filepath.Abs(filepath.Join(root, string(file))) 158 if err != nil { 159 panic(err) // this should not happen 160 } 161 162 if pathEqual(filepath.Dir(path), dir) { 163 files[path] = true 164 } 165 } 166 } 167 return files 168 } 169 170 func (v *VCS) exists(dir, rev string) bool { 171 err := v.runVerboseOnly(dir, v.ExistsCmd, "rev", rev) 172 return err == nil 173 } 174 175 // RevSync checks out the revision given by rev in dir. 176 // The dir must exist and rev must be a valid revision. 177 func (v *VCS) RevSync(dir, rev string) error { 178 return v.run(dir, v.vcs.TagSyncCmd, "tag", rev) 179 } 180 181 // run runs the command line cmd in the given directory. 182 // keyval is a list of key, value pairs. run expands 183 // instances of {key} in cmd into value, but only after 184 // splitting cmd into individual arguments. 185 // If an error occurs, run prints the command line and the 186 // command's combined stdout+stderr to standard error. 187 // Otherwise run discards the command's output. 188 func (v *VCS) run(dir string, cmdline string, kv ...string) error { 189 _, err := v.run1(dir, cmdline, kv, true) 190 return err 191 } 192 193 // runVerboseOnly is like run but only generates error output to standard error in verbose mode. 194 func (v *VCS) runVerboseOnly(dir string, cmdline string, kv ...string) error { 195 _, err := v.run1(dir, cmdline, kv, false) 196 return err 197 } 198 199 // runOutput is like run but returns the output of the command. 200 func (v *VCS) runOutput(dir string, cmdline string, kv ...string) ([]byte, error) { 201 return v.run1(dir, cmdline, kv, true) 202 } 203 204 // runOutputVerboseOnly is like runOutput but only generates error output to standard error in verbose mode. 205 func (v *VCS) runOutputVerboseOnly(dir string, cmdline string, kv ...string) ([]byte, error) { 206 return v.run1(dir, cmdline, kv, false) 207 } 208 209 // run1 is the generalized implementation of run and runOutput. 210 func (v *VCS) run1(dir string, cmdline string, kv []string, verbose bool) ([]byte, error) { 211 m := make(map[string]string) 212 for i := 0; i < len(kv); i += 2 { 213 m[kv[i]] = kv[i+1] 214 } 215 args := strings.Fields(cmdline) 216 for i, arg := range args { 217 args[i] = expand(m, arg) 218 } 219 220 _, err := exec.LookPath(v.vcs.Cmd) 221 if err != nil { 222 fmt.Fprintf(os.Stderr, "godep: missing %s command.\n", v.vcs.Name) 223 return nil, err 224 } 225 226 cmd := exec.Command(v.vcs.Cmd, args...) 227 cmd.Dir = dir 228 var buf bytes.Buffer 229 cmd.Stdout = &buf 230 cmd.Stderr = &buf 231 err = cmd.Run() 232 out := buf.Bytes() 233 if err != nil { 234 if verbose { 235 fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.vcs.Cmd, strings.Join(args, " ")) 236 os.Stderr.Write(out) 237 } 238 return nil, err 239 } 240 return out, nil 241 } 242 243 func expand(m map[string]string, s string) string { 244 for k, v := range m { 245 s = strings.Replace(s, "{"+k+"}", v, -1) 246 } 247 return s 248 } 249 250 func gitDetached(r string) (bool, error) { 251 o, err := vcsGit.runOutput(r, "status") 252 if err != nil { 253 return false, errors.New("unable to determine git status " + err.Error()) 254 } 255 return bytes.Contains(o, []byte("HEAD detached at")), nil 256 } 257 258 func gitDefaultBranch(r string) (string, error) { 259 o, err := vcsGit.runOutput(r, "remote show origin") 260 if err != nil { 261 return "", errors.New("Running git remote show origin errored with: " + err.Error()) 262 } 263 return gitDetermineDefaultBranch(r, string(o)) 264 } 265 266 func gitDetermineDefaultBranch(r, o string) (string, error) { 267 e := "Unable to determine HEAD branch: " 268 hb := "HEAD branch:" 269 lbcfgp := "Local branch configured for 'git pull':" 270 s := strings.Index(o, hb) 271 if s < 0 { 272 b := strings.Index(o, lbcfgp) 273 if b < 0 { 274 return "", errors.New(e + "Remote HEAD is ambiguous. Before godep can pull new commits you will need to:" + ` 275 cd ` + r + ` 276 git checkout <a HEAD branch> 277 Here is what was reported: 278 ` + o) 279 } 280 s = b + len(lbcfgp) 281 } else { 282 s += len(hb) 283 } 284 f := strings.Fields(o[s:]) 285 if len(f) < 3 { 286 return "", errors.New(e + "git output too short") 287 } 288 return f[0], nil 289 } 290 291 func gitCheckout(r, b string) error { 292 return vcsGit.run(r, "checkout "+b) 293 }