gotip: add new command to run latest master build

This command makes it even easier for anyone with a working go compiler
(and git) to test the latest master. This should be particularly handy
when asking users to verify fixes that landed on tip.

gotip manages the git repository, tolerates upstream force pushes, and
makes an attempt not to wipe away local changes in case the user did
work in it.

    $ go get golang.org/dl/gotip
    $ gotip download
    Cloning the go development tree...
    Cloning into '/Users/valsorda/sdk/gotip'...
    remote: Counting objects: 9334, done
    remote: Finding sources: 100% (9334/9334)
    remote: Total 9334 (delta 1244), reused 6116 (delta 1244)
    Receiving objects: 100% (9334/9334), 22.22 MiB | 11.00 MiB/s, done.
    Resolving deltas: 100% (1244/1244), done.
    Checking out files: 100% (8447/8447), done.
    HEAD is now at dc7808d cmd/compile/internal/syntax: remove unused field in (scanner) source
    Building Go cmd/dist using /Users/valsorda/homebrew/Cellar/go/1.11/libexec.
    Building Go toolchain1 using /Users/valsorda/homebrew/Cellar/go/1.11/libexec.
    Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
    Building Go toolchain2 using go_bootstrap and Go toolchain1.
    Building Go toolchain3 using go_bootstrap and Go toolchain2.
    Building packages and commands for darwin/amd64.
    ---
    Installed Go for darwin/amd64 in /Users/valsorda/sdk/gotip
    Installed commands in /Users/valsorda/sdk/gotip/bin
    Success. You may now run 'gotip'!
    $ gotip version
    go version devel +dc7808d Wed Dec 5 23:15:21 2018 +0000 darwin/amd64

Running "gotip download" again will fetch the latest changes and rebuild.

    $ gotip download
    Updating the go development tree...
    remote: Counting objects: 34, done
    remote: Finding sources: 100% (20/20)
    remote: Total 20 (delta 11), reused 20 (delta 11)
    Unpacking objects: 100% (20/20), done.
    From https://go.googlesource.com/go
    dc7808d..fcd6117  master     -> origin/master
    Previous HEAD position was dc7808d cmd/compile/internal/syntax: remove unused field in (scanner) source
    HEAD is now at fcd6117 cmd/internal/objfile: provide consistent output in objdump on ppc64x
    Building Go cmd/dist using /Users/valsorda/homebrew/Cellar/go/1.11/libexec.
    Building Go toolchain1 using /Users/valsorda/homebrew/Cellar/go/1.11/libexec.
    Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
    Building Go toolchain2 using go_bootstrap and Go toolchain1.
    Building Go toolchain3 using go_bootstrap and Go toolchain2.
    Building packages and commands for darwin/amd64.
    ---
    Installed Go for darwin/amd64 in /Users/valsorda/sdk/gotip
    Installed commands in /Users/valsorda/sdk/gotip/bin
    Success. You may now run 'gotip'!

Change-Id: If3999c0c949662bb6453979e5cfd757ce7de0975
Reviewed-on: https://go-review.googlesource.com/c/152857
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/README.md b/README.md
index d517e19..b2764bc 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # golang.org/dl
 
 This repository holds the Go wrapper programs that run specific versions of Go, such
-as `go get golang.org/dl/go1.10.3`.
+as `go get golang.org/dl/go1.10.3` and `go get golang.org/dl/gotip`.
 
 ## Report Issues / Send Patches
 
diff --git a/gotip/main.go b/gotip/main.go
new file mode 100644
index 0000000..0423482
--- /dev/null
+++ b/gotip/main.go
@@ -0,0 +1,175 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The gotip command compiles and runs the go command from the development tree.
+//
+// To install, run:
+//
+//     $ go get golang.org/dl/gotip
+//     $ gotip download
+//
+// And then use the gotip command as if it were your normal go command.
+//
+// To update, run "gotip download" again.
+package main
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"runtime"
+	"strings"
+)
+
+func main() {
+	log.SetFlags(0)
+
+	root, err := goroot("gotip")
+	if err != nil {
+		log.Fatalf("gotip: %v", err)
+	}
+
+	if len(os.Args) == 2 && os.Args[1] == "download" {
+		if err := installTip(root); err != nil {
+			log.Fatalf("gotip: %v", err)
+		}
+		log.Printf("Success. You may now run 'gotip'!")
+		os.Exit(0)
+	}
+
+	gobin := filepath.Join(root, "bin", "go"+exe())
+	if _, err := os.Stat(gobin); err != nil {
+		log.Fatalf("gotip: not downloaded. Run 'gotip download' to install to %v", root)
+	}
+
+	cmd := exec.Command(gobin, os.Args[1:]...)
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		if _, ok := err.(*exec.ExitError); ok {
+			// TODO: return the same exit status maybe.
+			os.Exit(1)
+		}
+		log.Fatalf("gotip: failed to execute %v: %v", gobin, err)
+	}
+	os.Exit(0)
+}
+
+func installTip(root string) error {
+	git := func(args ...string) error {
+		cmd := exec.Command("git", args...)
+		cmd.Stdin = os.Stdin
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		cmd.Dir = root
+		return cmd.Run()
+	}
+
+	if _, err := os.Stat(filepath.Join(root, ".git")); err != nil {
+		if err := os.MkdirAll(root, 0755); err != nil {
+			return fmt.Errorf("failed to create repository: %v", err)
+		}
+		if err := git("clone", "--depth=1", "https://go.googlesource.com/go", root); err != nil {
+			return fmt.Errorf("failed to clone git repository: %v", err)
+		}
+	} else {
+		log.Printf("Updating the go development tree...")
+		if err := git("fetch", "origin"); err != nil {
+			return fmt.Errorf("failed to fetch git repository updates: %v", err)
+		}
+	}
+
+	// Use checkout and a detached HEAD, because it will refuse to overwrite
+	// local changes, and warn if commits are being left behind, but will not
+	// mind if master is force-pushed upstream.
+	if err := git("-c", "advice.detachedHead=false", "checkout", "origin/master"); err != nil {
+		return fmt.Errorf("failed to checkout git repository: %v", err)
+	}
+	// It shouldn't be the case, but in practice sometimes binary artifacts
+	// generated by earlier Go versions interfere with the build.
+	//
+	// Ask the user what to do about them if they are not gitignored. They might
+	// be artifacts that used to be ignored in previous versions, or precious
+	// uncommitted source files.
+	if err := git("clean", "-i", "-d"); err != nil {
+		return fmt.Errorf("failed to cleanup git repository: %v", err)
+	}
+	// Wipe away probably boring ignored files without bothering the user.
+	if err := git("clean", "-q", "-f", "-d", "-X"); err != nil {
+		return fmt.Errorf("failed to cleanup git repository: %v", err)
+	}
+
+	cmd := exec.Command(filepath.Join(root, "src", make()))
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Dir = filepath.Join(root, "src")
+	if runtime.GOOS == "windows" {
+		// Workaround make.bat not autodetecting GOROOT_BOOTSTRAP. Issue 28641.
+		goroot, err := exec.Command("go", "env", "GOROOT").Output()
+		if err != nil {
+			return fmt.Errorf("failed to detect an existing go installation for bootstrap: %v", err)
+		}
+		cmd.Env = append(os.Environ(), "GOROOT_BOOTSTRAP="+strings.TrimSpace(string(goroot)))
+	}
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to build go: %v", err)
+	}
+
+	return nil
+}
+
+func make() string {
+	switch runtime.GOOS {
+	case "plan9":
+		return "make.rc"
+	case "windows":
+		return "make.bat"
+	default:
+		return "make.bash"
+	}
+}
+
+func exe() string {
+	if runtime.GOOS == "windows" {
+		return ".exe"
+	}
+	return ""
+}
+
+func goroot(version string) (string, error) {
+	home, err := homedir()
+	if err != nil {
+		return "", fmt.Errorf("failed to get home directory: %v", err)
+	}
+	return filepath.Join(home, "sdk", version), nil
+}
+
+func homedir() (string, error) {
+	// This could be replaced with os.UserHomeDir, but it was introduced too
+	// recently, and we want this to work with go as packaged by Linux
+	// distributions. Note that user.Current is not enough as it does not
+	// prioritize $HOME. See also Issue 26463.
+	switch runtime.GOOS {
+	case "plan9":
+		return "", fmt.Errorf("%q not yet supported", runtime.GOOS)
+	case "windows":
+		if dir := os.Getenv("USERPROFILE"); dir != "" {
+			return dir, nil
+		}
+		return "", errors.New("can't find user home directory; %USERPROFILE% is empty")
+	default:
+		if dir := os.Getenv("HOME"); dir != "" {
+			return dir, nil
+		}
+		if u, err := user.Current(); err == nil && u.HomeDir != "" {
+			return u.HomeDir, nil
+		}
+		return "", errors.New("can't find user home directory; $HOME is empty")
+	}
+}