You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

289 lines
7.7 KiB

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