1
2
3
4
5
6
7
8
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
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
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
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 }
289
View as plain text