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