| 1 | // Copyright 2017 The Go Authors. All rights reserved. |
|---|---|
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | // The go-contrib-init command helps new Go contributors get their development |
| 6 | // environment set up for the Go contribution process. |
| 7 | // |
| 8 | // It aims to be a complement or alternative to https://golang.org/doc/contribute.html. |
| 9 | package main |
| 10 | |
| 11 | import ( |
| 12 | "bytes" |
| 13 | "flag" |
| 14 | "fmt" |
| 15 | "go/build" |
| 16 | exec "golang.org/x/sys/execabs" |
| 17 | "io/ioutil" |
| 18 | "log" |
| 19 | "os" |
| 20 | "path/filepath" |
| 21 | "regexp" |
| 22 | "runtime" |
| 23 | "strings" |
| 24 | ) |
| 25 | |
| 26 | var ( |
| 27 | repo = flag.String("repo", detectrepo(), "Which go repo you want to contribute to. Use \"go\" for the core, or e.g. \"net\" for golang.org/x/net/*") |
| 28 | dry = flag.Bool("dry-run", false, "Fail with problems instead of trying to fix things.") |
| 29 | ) |
| 30 | |
| 31 | func main() { |
| 32 | log.SetFlags(0) |
| 33 | flag.Parse() |
| 34 | |
| 35 | checkCLA() |
| 36 | checkGoroot() |
| 37 | checkWorkingDir() |
| 38 | checkGitOrigin() |
| 39 | checkGitCodeReview() |
| 40 | fmt.Print("All good. Happy hacking!\n" + |
| 41 | "Remember to squash your revised commits and preserve the magic Change-Id lines.\n" + |
| 42 | "Next steps: https://golang.org/doc/contribute.html#commit_changes\n") |
| 43 | } |
| 44 | |
| 45 | func detectrepo() string { |
| 46 | wd, err := os.Getwd() |
| 47 | if err != nil { |
| 48 | return "go" |
| 49 | } |
| 50 | |
| 51 | for _, path := range filepath.SplitList(build.Default.GOPATH) { |
| 52 | rightdir := filepath.Join(path, "src", "golang.org", "x") + string(os.PathSeparator) |
| 53 | if strings.HasPrefix(wd, rightdir) { |
| 54 | tail := wd[len(rightdir):] |
| 55 | end := strings.Index(tail, string(os.PathSeparator)) |
| 56 | if end > 0 { |
| 57 | repo := tail[:end] |
| 58 | return repo |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | return "go" |
| 64 | } |
| 65 | |
| 66 | var googleSourceRx = regexp.MustCompile(`(?m)^(go|go-review)?\.googlesource.com\b`) |
| 67 | |
| 68 | func checkCLA() { |
| 69 | slurp, err := ioutil.ReadFile(cookiesFile()) |
| 70 | if err != nil && !os.IsNotExist(err) { |
| 71 | log.Fatal(err) |
| 72 | } |
| 73 | if googleSourceRx.Match(slurp) { |
| 74 | // Probably good. |
| 75 | return |
| 76 | } |
| 77 | log.Fatal("Your .gitcookies file isn't configured.\n" + |
| 78 | "Next steps:\n" + |
| 79 | " * Submit a CLA (https://golang.org/doc/contribute.html#cla) if not done\n" + |
| 80 | " * Go to https://go.googlesource.com/ and click \"Generate Password\" at the top,\n" + |
| 81 | " then follow instructions.\n" + |
| 82 | " * Run go-contrib-init again.\n") |
| 83 | } |
| 84 | |
| 85 | func expandUser(s string) string { |
| 86 | env := "HOME" |
| 87 | if runtime.GOOS == "windows" { |
| 88 | env = "USERPROFILE" |
| 89 | } else if runtime.GOOS == "plan9" { |
| 90 | env = "home" |
| 91 | } |
| 92 | home := os.Getenv(env) |
| 93 | if home == "" { |
| 94 | return s |
| 95 | } |
| 96 | |
| 97 | if len(s) >= 2 && s[0] == '~' && os.IsPathSeparator(s[1]) { |
| 98 | if runtime.GOOS == "windows" { |
| 99 | s = filepath.ToSlash(filepath.Join(home, s[2:])) |
| 100 | } else { |
| 101 | s = filepath.Join(home, s[2:]) |
| 102 | } |
| 103 | } |
| 104 | return os.Expand(s, func(env string) string { |
| 105 | if env == "HOME" { |
| 106 | return home |
| 107 | } |
| 108 | return os.Getenv(env) |
| 109 | }) |
| 110 | } |
| 111 | |
| 112 | func cookiesFile() string { |
| 113 | out, _ := exec.Command("git", "config", "http.cookiefile").Output() |
| 114 | if s := strings.TrimSpace(string(out)); s != "" { |
| 115 | if strings.HasPrefix(s, "~") { |
| 116 | s = expandUser(s) |
| 117 | } |
| 118 | return s |
| 119 | } |
| 120 | if runtime.GOOS == "windows" { |
| 121 | return filepath.Join(os.Getenv("USERPROFILE"), ".gitcookies") |
| 122 | } |
| 123 | return filepath.Join(os.Getenv("HOME"), ".gitcookies") |
| 124 | } |
| 125 | |
| 126 | func checkGoroot() { |
| 127 | v := os.Getenv("GOROOT") |
| 128 | if v == "" { |
| 129 | return |
| 130 | } |
| 131 | if *repo == "go" { |
| 132 | if strings.HasPrefix(v, "/usr/") { |
| 133 | log.Fatalf("Your GOROOT environment variable is set to %q\n"+ |
| 134 | "This is almost certainly not what you want. Either unset\n"+ |
| 135 | "your GOROOT or set it to the path of your development version\n"+ |
| 136 | "of Go.", v) |
| 137 | } |
| 138 | slurp, err := ioutil.ReadFile(filepath.Join(v, "VERSION")) |
| 139 | if err == nil { |
| 140 | slurp = bytes.TrimSpace(slurp) |
| 141 | log.Fatalf("Your GOROOT environment variable is set to %q\n"+ |
| 142 | "But that path is to a binary release of Go, with VERSION file %q.\n"+ |
| 143 | "You should hack on Go in a fresh checkout of Go. Fix or unset your GOROOT.\n", |
| 144 | v, slurp) |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | func checkWorkingDir() { |
| 150 | wd, err := os.Getwd() |
| 151 | if err != nil { |
| 152 | log.Fatal(err) |
| 153 | } |
| 154 | if *repo == "go" { |
| 155 | if inGoPath(wd) { |
| 156 | log.Fatalf(`You can't work on Go from within your GOPATH. Please checkout Go outside of your GOPATH |
| 157 | |
| 158 | Current directory: %s |
| 159 | GOPATH: %s |
| 160 | `, wd, os.Getenv("GOPATH")) |
| 161 | } |
| 162 | return |
| 163 | } |
| 164 | |
| 165 | gopath := firstGoPath() |
| 166 | if gopath == "" { |
| 167 | log.Fatal("Your GOPATH is not set, please set it") |
| 168 | } |
| 169 | |
| 170 | rightdir := filepath.Join(gopath, "src", "golang.org", "x", *repo) |
| 171 | if !strings.HasPrefix(wd, rightdir) { |
| 172 | dirExists, err := exists(rightdir) |
| 173 | if err != nil { |
| 174 | log.Fatal(err) |
| 175 | } |
| 176 | if !dirExists { |
| 177 | log.Fatalf("The repo you want to work on is currently not on your system.\n"+ |
| 178 | "Run %q to obtain this repo\n"+ |
| 179 | "then go to the directory %q\n", |
| 180 | "go get -d golang.org/x/"+*repo, rightdir) |
| 181 | } |
| 182 | log.Fatalf("Your current directory is:%q\n"+ |
| 183 | "Working on golang/x/%v requires you be in %q\n", |
| 184 | wd, *repo, rightdir) |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | func firstGoPath() string { |
| 189 | list := filepath.SplitList(build.Default.GOPATH) |
| 190 | if len(list) < 1 { |
| 191 | return "" |
| 192 | } |
| 193 | return list[0] |
| 194 | } |
| 195 | |
| 196 | func exists(path string) (bool, error) { |
| 197 | _, err := os.Stat(path) |
| 198 | if os.IsNotExist(err) { |
| 199 | return false, nil |
| 200 | } |
| 201 | return true, err |
| 202 | } |
| 203 | |
| 204 | func inGoPath(wd string) bool { |
| 205 | if os.Getenv("GOPATH") == "" { |
| 206 | return false |
| 207 | } |
| 208 | |
| 209 | for _, path := range filepath.SplitList(os.Getenv("GOPATH")) { |
| 210 | if strings.HasPrefix(wd, filepath.Join(path, "src")) { |
| 211 | return true |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | return false |
| 216 | } |
| 217 | |
| 218 | // mostly check that they didn't clone from github |
| 219 | func checkGitOrigin() { |
| 220 | if _, err := exec.LookPath("git"); err != nil { |
| 221 | log.Fatalf("You don't appear to have git installed. Do that.") |
| 222 | } |
| 223 | wantRemote := "https://go.googlesource.com/" + *repo |
| 224 | remotes, err := exec.Command("git", "remote", "-v").Output() |
| 225 | if err != nil { |
| 226 | msg := cmdErr(err) |
| 227 | if strings.Contains(msg, "Not a git repository") { |
| 228 | log.Fatalf("Your current directory is not in a git checkout of %s", wantRemote) |
| 229 | } |
| 230 | log.Fatalf("Error running git remote -v: %v", msg) |
| 231 | } |
| 232 | matches := 0 |
| 233 | for _, line := range strings.Split(string(remotes), "\n") { |
| 234 | line = strings.TrimSpace(line) |
| 235 | if !strings.HasPrefix(line, "origin") { |
| 236 | continue |
| 237 | } |
| 238 | if !strings.Contains(line, wantRemote) { |
| 239 | curRemote := strings.Fields(strings.TrimPrefix(line, "origin"))[0] |
| 240 | // TODO: if not in dryRun mode, just fix it? |
| 241 | log.Fatalf("Current directory's git was cloned from %q; origin should be %q", curRemote, wantRemote) |
| 242 | } |
| 243 | matches++ |
| 244 | } |
| 245 | if matches == 0 { |
| 246 | log.Fatalf("git remote -v output didn't contain expected %q. Got:\n%s", wantRemote, remotes) |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | func cmdErr(err error) string { |
| 251 | if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { |
| 252 | return fmt.Sprintf("%s: %s", err, ee.Stderr) |
| 253 | } |
| 254 | return fmt.Sprint(err) |
| 255 | } |
| 256 | |
| 257 | func checkGitCodeReview() { |
| 258 | if _, err := exec.LookPath("git-codereview"); err != nil { |
| 259 | if *dry { |
| 260 | log.Fatalf("You don't appear to have git-codereview tool. While this is technically optional,\n" + |
| 261 | "almost all Go contributors use it. Our documentation and this tool assume it is used.\n" + |
| 262 | "To install it, run:\n\n\t$ go get golang.org/x/review/git-codereview\n\n(Then run go-contrib-init again)") |
| 263 | } |
| 264 | err := exec.Command("go", "get", "golang.org/x/review/git-codereview").Run() |
| 265 | if err != nil { |
| 266 | log.Fatalf("Error running go get golang.org/x/review/git-codereview: %v", cmdErr(err)) |
| 267 | } |
| 268 | log.Printf("Installed git-codereview (ran `go get golang.org/x/review/git-codereview`)") |
| 269 | } |
| 270 | missing := false |
| 271 | for _, cmd := range []string{"change", "gofmt", "mail", "pending", "submit", "sync"} { |
| 272 | v, _ := exec.Command("git", "config", "alias."+cmd).Output() |
| 273 | if strings.Contains(string(v), "codereview") { |
| 274 | continue |
| 275 | } |
| 276 | if *dry { |
| 277 | log.Printf("Missing alias. Run:\n\t$ git config alias.%s \"codereview %s\"", cmd, cmd) |
| 278 | missing = true |
| 279 | } else { |
| 280 | err := exec.Command("git", "config", "alias."+cmd, "codereview "+cmd).Run() |
| 281 | if err != nil { |
| 282 | log.Fatalf("Error setting alias.%s: %v", cmd, cmdErr(err)) |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | if missing { |
| 287 | log.Fatalf("Missing aliases. (While optional, this tool assumes you use them.)") |
| 288 | } |
| 289 | } |
| 290 |
Members