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.
9package main
10
11import (
12	"bytes"
13	"flag"
14	"fmt"
15	"go/build"
16	"io/ioutil"
17	"log"
18	"os"
19	"os/exec"
20	"path/filepath"
21	"regexp"
22	"runtime"
23	"strings"
24)
25
26var (
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
31func 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
45func 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
66var googleSourceRx = regexp.MustCompile(`(?m)^(go|go-review)?\.googlesource.com\b`)
67
68func 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
85func 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
112func 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
126func 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
149func 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
158Current directory: %s
159GOPATH: %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
188func firstGoPath() string {
189	list := filepath.SplitList(build.Default.GOPATH)
190	if len(list) < 1 {
191		return ""
192	}
193	return list[0]
194}
195
196func 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
204func 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
219func 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
250func 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
257func 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