| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // The go-contrib-init command helps new Go contributors get their development |
| // environment set up for the Go contribution process. |
| // |
| // It aims to be a complement or alternative to https://golang.org/doc/contribute.html. |
| package main |
| |
| import ( |
| "bytes" |
| "flag" |
| "fmt" |
| "go/build" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strings" |
| ) |
| |
| var ( |
| 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/*") |
| dry = flag.Bool("dry-run", false, "Fail with problems instead of trying to fix things.") |
| ) |
| |
| func main() { |
| log.SetFlags(0) |
| flag.Parse() |
| |
| checkCLA() |
| checkGoroot() |
| checkWorkingDir() |
| checkGitOrigin() |
| checkGitCodeReview() |
| fmt.Print("All good. Happy hacking!\n" + |
| "Remember to squash your revised commits and preserve the magic Change-Id lines.\n" + |
| "Next steps: https://golang.org/doc/contribute.html#commit_changes\n") |
| } |
| |
| func detectrepo() string { |
| wd, err := os.Getwd() |
| if err != nil { |
| return "go" |
| } |
| |
| for _, path := range filepath.SplitList(build.Default.GOPATH) { |
| rightdir := filepath.Join(path, "src", "golang.org", "x") + string(os.PathSeparator) |
| if strings.HasPrefix(wd, rightdir) { |
| tail := wd[len(rightdir):] |
| end := strings.Index(tail, string(os.PathSeparator)) |
| if end > 0 { |
| repo := tail[:end] |
| return repo |
| } |
| } |
| } |
| |
| return "go" |
| } |
| |
| var googleSourceRx = regexp.MustCompile(`(?m)^(go|go-review)?\.googlesource.com\b`) |
| |
| func checkCLA() { |
| slurp, err := ioutil.ReadFile(cookiesFile()) |
| if err != nil && !os.IsNotExist(err) { |
| log.Fatal(err) |
| } |
| if googleSourceRx.Match(slurp) { |
| // Probably good. |
| return |
| } |
| log.Fatal("Your .gitcookies file isn't configured.\n" + |
| "Next steps:\n" + |
| " * Submit a CLA (https://golang.org/doc/contribute.html#cla) if not done\n" + |
| " * Go to https://go.googlesource.com/ and click \"Generate Password\" at the top,\n" + |
| " then follow instructions.\n" + |
| " * Run go-contrib-init again.\n") |
| } |
| |
| func expandUser(s string) string { |
| env := "HOME" |
| if runtime.GOOS == "windows" { |
| env = "USERPROFILE" |
| } else if runtime.GOOS == "plan9" { |
| env = "home" |
| } |
| home := os.Getenv(env) |
| if home == "" { |
| return s |
| } |
| |
| if len(s) >= 2 && s[0] == '~' && os.IsPathSeparator(s[1]) { |
| if runtime.GOOS == "windows" { |
| s = filepath.ToSlash(filepath.Join(home, s[2:])) |
| } else { |
| s = filepath.Join(home, s[2:]) |
| } |
| } |
| return os.Expand(s, func(env string) string { |
| if env == "HOME" { |
| return home |
| } |
| return os.Getenv(env) |
| }) |
| } |
| |
| func cookiesFile() string { |
| out, _ := exec.Command("git", "config", "http.cookiefile").Output() |
| if s := strings.TrimSpace(string(out)); s != "" { |
| if strings.HasPrefix(s, "~") { |
| s = expandUser(s) |
| } |
| return s |
| } |
| if runtime.GOOS == "windows" { |
| return filepath.Join(os.Getenv("USERPROFILE"), ".gitcookies") |
| } |
| return filepath.Join(os.Getenv("HOME"), ".gitcookies") |
| } |
| |
| func checkGoroot() { |
| v := os.Getenv("GOROOT") |
| if v == "" { |
| return |
| } |
| if *repo == "go" { |
| if strings.HasPrefix(v, "/usr/") { |
| log.Fatalf("Your GOROOT environment variable is set to %q\n"+ |
| "This is almost certainly not what you want. Either unset\n"+ |
| "your GOROOT or set it to the path of your development version\n"+ |
| "of Go.", v) |
| } |
| slurp, err := ioutil.ReadFile(filepath.Join(v, "VERSION")) |
| if err == nil { |
| slurp = bytes.TrimSpace(slurp) |
| log.Fatalf("Your GOROOT environment variable is set to %q\n"+ |
| "But that path is to a binary release of Go, with VERSION file %q.\n"+ |
| "You should hack on Go in a fresh checkout of Go. Fix or unset your GOROOT.\n", |
| v, slurp) |
| } |
| } |
| } |
| |
| func checkWorkingDir() { |
| wd, err := os.Getwd() |
| if err != nil { |
| log.Fatal(err) |
| } |
| if *repo == "go" { |
| if inGoPath(wd) { |
| log.Fatalf(`You can't work on Go from within your GOPATH. Please checkout Go outside of your GOPATH |
| |
| Current directory: %s |
| GOPATH: %s |
| `, wd, os.Getenv("GOPATH")) |
| } |
| return |
| } |
| |
| gopath := firstGoPath() |
| if gopath == "" { |
| log.Fatal("Your GOPATH is not set, please set it") |
| } |
| |
| rightdir := filepath.Join(gopath, "src", "golang.org", "x", *repo) |
| if !strings.HasPrefix(wd, rightdir) { |
| dirExists, err := exists(rightdir) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if !dirExists { |
| log.Fatalf("The repo you want to work on is currently not on your system.\n"+ |
| "Run %q to obtain this repo\n"+ |
| "then go to the directory %q\n", |
| "go get -d golang.org/x/"+*repo, rightdir) |
| } |
| log.Fatalf("Your current directory is:%q\n"+ |
| "Working on golang/x/%v requires you be in %q\n", |
| wd, *repo, rightdir) |
| } |
| } |
| |
| func firstGoPath() string { |
| list := filepath.SplitList(build.Default.GOPATH) |
| if len(list) < 1 { |
| return "" |
| } |
| return list[0] |
| } |
| |
| func exists(path string) (bool, error) { |
| _, err := os.Stat(path) |
| if os.IsNotExist(err) { |
| return false, nil |
| } |
| return true, err |
| } |
| |
| func inGoPath(wd string) bool { |
| if os.Getenv("GOPATH") == "" { |
| return false |
| } |
| |
| for _, path := range filepath.SplitList(os.Getenv("GOPATH")) { |
| if strings.HasPrefix(wd, filepath.Join(path, "src")) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| // mostly check that they didn't clone from github |
| func checkGitOrigin() { |
| if _, err := exec.LookPath("git"); err != nil { |
| log.Fatalf("You don't appear to have git installed. Do that.") |
| } |
| wantRemote := "https://go.googlesource.com/" + *repo |
| remotes, err := exec.Command("git", "remote", "-v").Output() |
| if err != nil { |
| msg := cmdErr(err) |
| if strings.Contains(msg, "Not a git repository") { |
| log.Fatalf("Your current directory is not in a git checkout of %s", wantRemote) |
| } |
| log.Fatalf("Error running git remote -v: %v", msg) |
| } |
| matches := 0 |
| for _, line := range strings.Split(string(remotes), "\n") { |
| line = strings.TrimSpace(line) |
| if !strings.HasPrefix(line, "origin") { |
| continue |
| } |
| if !strings.Contains(line, wantRemote) { |
| curRemote := strings.Fields(strings.TrimPrefix(line, "origin"))[0] |
| // TODO: if not in dryRun mode, just fix it? |
| log.Fatalf("Current directory's git was cloned from %q; origin should be %q", curRemote, wantRemote) |
| } |
| matches++ |
| } |
| if matches == 0 { |
| log.Fatalf("git remote -v output didn't contain expected %q. Got:\n%s", wantRemote, remotes) |
| } |
| } |
| |
| func cmdErr(err error) string { |
| if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { |
| return fmt.Sprintf("%s: %s", err, ee.Stderr) |
| } |
| return fmt.Sprint(err) |
| } |
| |
| func checkGitCodeReview() { |
| if _, err := exec.LookPath("git-codereview"); err != nil { |
| if *dry { |
| log.Fatalf("You don't appear to have git-codereview tool. While this is technically optional,\n" + |
| "almost all Go contributors use it. Our documentation and this tool assume it is used.\n" + |
| "To install it, run:\n\n\t$ go get golang.org/x/review/git-codereview\n\n(Then run go-contrib-init again)") |
| } |
| err := exec.Command("go", "get", "golang.org/x/review/git-codereview").Run() |
| if err != nil { |
| log.Fatalf("Error running go get golang.org/x/review/git-codereview: %v", cmdErr(err)) |
| } |
| log.Printf("Installed git-codereview (ran `go get golang.org/x/review/git-codereview`)") |
| } |
| missing := false |
| for _, cmd := range []string{"change", "gofmt", "mail", "pending", "submit", "sync"} { |
| v, _ := exec.Command("git", "config", "alias."+cmd).Output() |
| if strings.Contains(string(v), "codereview") { |
| continue |
| } |
| if *dry { |
| log.Printf("Missing alias. Run:\n\t$ git config alias.%s \"codereview %s\"", cmd, cmd) |
| missing = true |
| } else { |
| err := exec.Command("git", "config", "alias."+cmd, "codereview "+cmd).Run() |
| if err != nil { |
| log.Fatalf("Error setting alias.%s: %v", cmd, cmdErr(err)) |
| } |
| } |
| } |
| if missing { |
| log.Fatalf("Missing aliases. (While optional, this tool assumes you use them.)") |
| } |
| } |