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