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