github.com/alexanderthaller/godep@v0.0.0-20141231210904-0baa7ea46402/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 type VCS struct { 15 vcs *vcs.Cmd 16 17 // run in outer GOPATH 18 IdentifyCmd string 19 DescribeCmd string 20 DiffCmd string 21 22 // run in sandbox repos 23 CreateCmd string 24 LinkCmd string 25 ExistsCmd string 26 FetchCmd string 27 CheckoutCmd string 28 29 // If nil, LinkCmd is used. 30 LinkFunc func(dir, remote, url string) error 31 } 32 33 var vcsBzr = &VCS{ 34 vcs: vcs.ByCmd("bzr"), 35 36 IdentifyCmd: "version-info --custom --template {revision_id}", 37 DescribeCmd: "revno", // TODO(kr): find tag names if possible 38 DiffCmd: "diff -r {rev}", 39 } 40 41 var vcsGit = &VCS{ 42 vcs: vcs.ByCmd("git"), 43 44 IdentifyCmd: "rev-parse HEAD", 45 DescribeCmd: "describe --tags", 46 DiffCmd: "diff {rev}", 47 48 CreateCmd: "init --bare", 49 LinkCmd: "remote add {remote} {url}", 50 ExistsCmd: "cat-file -e {rev}", 51 FetchCmd: "fetch --quiet {remote}", 52 CheckoutCmd: "--git-dir {repo} --work-tree . checkout -q --force {rev}", 53 } 54 55 var vcsHg = &VCS{ 56 vcs: vcs.ByCmd("hg"), 57 58 IdentifyCmd: "identify --id --debug", 59 DescribeCmd: "log -r . --template {latesttag}-{latesttagdistance}", 60 DiffCmd: "diff -r {rev}", 61 62 CreateCmd: "init", 63 LinkFunc: hgLink, 64 ExistsCmd: "cat -r {rev} .", 65 FetchCmd: "pull {remote}", 66 CheckoutCmd: "clone -u {rev} {repo} .", 67 } 68 69 var cmd = map[*vcs.Cmd]*VCS{ 70 vcsBzr.vcs: vcsBzr, 71 vcsGit.vcs: vcsGit, 72 vcsHg.vcs: vcsHg, 73 } 74 75 func VCSFromDir(dir, srcRoot string) (*VCS, string, error) { 76 vcscmd, reporoot, err := vcs.FromDir(dir, srcRoot) 77 if err != nil { 78 return nil, "", err 79 } 80 vcsext := cmd[vcscmd] 81 if vcsext == nil { 82 return nil, "", fmt.Errorf("%s is unsupported: %s", vcscmd.Name, dir) 83 } 84 return vcsext, reporoot, nil 85 } 86 87 func VCSForImportPath(importPath string) (*VCS, *vcs.RepoRoot, error) { 88 rr, err := vcs.RepoRootForImportPath(importPath, false) 89 if err != nil { 90 return nil, nil, err 91 } 92 vcs := cmd[rr.VCS] 93 if vcs == nil { 94 return nil, nil, fmt.Errorf("%s is unsupported: %s", rr.VCS.Name, importPath) 95 } 96 return vcs, rr, nil 97 } 98 99 func (v *VCS) identify(dir string) (string, error) { 100 out, err := v.runOutput(dir, v.IdentifyCmd) 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 func (v *VCS) create(dir string) error { 118 return v.run(dir, v.CreateCmd) 119 } 120 121 func (v *VCS) link(dir, remote, url string) error { 122 if v.LinkFunc != nil { 123 return v.LinkFunc(dir, remote, url) 124 } 125 return v.run(dir, v.LinkCmd, "remote", remote, "url", url) 126 } 127 128 func (v *VCS) exists(dir, rev string) bool { 129 err := v.runVerboseOnly(dir, v.ExistsCmd, "rev", rev) 130 return err == nil 131 } 132 133 func (v *VCS) fetch(dir, remote string) error { 134 return v.run(dir, v.FetchCmd, "remote", remote) 135 } 136 137 // RevSync checks out the revision given by rev in dir. 138 // The dir must exist and rev must be a valid revision. 139 func (v *VCS) RevSync(dir, rev string) error { 140 return v.run(dir, v.vcs.TagSyncCmd, "tag", rev) 141 } 142 143 func (v *VCS) checkout(dir, rev, repo string) error { 144 return v.run(dir, v.CheckoutCmd, "rev", rev, "repo", repo) 145 } 146 147 // run runs the command line cmd in the given directory. 148 // keyval is a list of key, value pairs. run expands 149 // instances of {key} in cmd into value, but only after 150 // splitting cmd into individual arguments. 151 // If an error occurs, run prints the command line and the 152 // command's combined stdout+stderr to standard error. 153 // Otherwise run discards the command's output. 154 func (v *VCS) run(dir string, cmdline string, kv ...string) error { 155 _, err := v.run1(dir, cmdline, kv, true) 156 return err 157 } 158 159 // runVerboseOnly is like run but only generates error output to standard error in verbose mode. 160 func (v *VCS) runVerboseOnly(dir string, cmdline string, kv ...string) error { 161 _, err := v.run1(dir, cmdline, kv, false) 162 return err 163 } 164 165 // runOutput is like run but returns the output of the command. 166 func (v *VCS) runOutput(dir string, cmdline string, kv ...string) ([]byte, error) { 167 return v.run1(dir, cmdline, kv, true) 168 } 169 170 // runOutputVerboseOnly is like runOutput but only generates error output to standard error in verbose mode. 171 func (v *VCS) runOutputVerboseOnly(dir string, cmdline string, kv ...string) ([]byte, error) { 172 return v.run1(dir, cmdline, kv, false) 173 } 174 175 // run1 is the generalized implementation of run and runOutput. 176 func (v *VCS) run1(dir string, cmdline string, kv []string, verbose bool) ([]byte, error) { 177 m := make(map[string]string) 178 for i := 0; i < len(kv); i += 2 { 179 m[kv[i]] = kv[i+1] 180 } 181 args := strings.Fields(cmdline) 182 for i, arg := range args { 183 args[i] = expand(m, arg) 184 } 185 186 _, err := exec.LookPath(v.vcs.Cmd) 187 if err != nil { 188 fmt.Fprintf(os.Stderr, "godep: missing %s command.\n", v.vcs.Name) 189 return nil, err 190 } 191 192 cmd := exec.Command(v.vcs.Cmd, args...) 193 cmd.Dir = dir 194 var buf bytes.Buffer 195 cmd.Stdout = &buf 196 cmd.Stderr = &buf 197 err = cmd.Run() 198 out := buf.Bytes() 199 if err != nil { 200 if verbose { 201 fmt.Fprintf(os.Stderr, "# cd %s; %s %s\n", dir, v.vcs.Cmd, strings.Join(args, " ")) 202 os.Stderr.Write(out) 203 } 204 return nil, err 205 } 206 return out, nil 207 } 208 209 func expand(m map[string]string, s string) string { 210 for k, v := range m { 211 s = strings.Replace(s, "{"+k+"}", v, -1) 212 } 213 return s 214 } 215 216 // Mercurial has no command equivalent to git remote add. 217 // We handle it as a special case in process. 218 func hgLink(dir, remote, url string) error { 219 hgdir := filepath.Join(dir, ".hg") 220 if err := os.MkdirAll(hgdir, 0777); err != nil { 221 return err 222 } 223 path := filepath.Join(hgdir, "hgrc") 224 f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) 225 if err != nil { 226 return err 227 } 228 fmt.Fprintf(f, "[paths]\n%s = %s\n", remote, url) 229 return f.Close() 230 }