github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/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  	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  }