gotip: fold into internal/version

Essentially a rebase of CL 217764 by zikaeroh, but with separate files
so that it's easier to review.

Fixes golang/go#37037

Change-Id: I8e74671544c3af1883d6958f2357ba1923175950
Reviewed-on: https://go-review.googlesource.com/c/dl/+/301909
Run-TryBot: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/gotip/main.go b/gotip/main.go
index d1c69cc..7e636ac 100644
--- a/gotip/main.go
+++ b/gotip/main.go
@@ -16,257 +16,9 @@
 package main
 
 import (
-	"errors"
-	"fmt"
-	"log"
-	"os"
-	"os/exec"
-	"os/user"
-	"path/filepath"
-	"regexp"
-	"runtime"
-	"strconv"
-	"strings"
+	"golang.org/dl/internal/version"
 )
 
 func main() {
-	log.SetFlags(0)
-
-	root, err := goroot("gotip")
-	if err != nil {
-		log.Fatalf("gotip: %v", err)
-	}
-
-	if len(os.Args) > 1 && os.Args[1] == "download" {
-		switch len(os.Args) {
-		case 2:
-			if err := installTip(root, ""); err != nil {
-				log.Fatalf("gotip: %v", err)
-			}
-		case 3:
-			if _, err := strconv.Atoi(os.Args[2]); err != nil {
-				log.Fatalf("gotip: invalid CL number: %q", os.Args[2])
-			}
-			if err := installTip(root, os.Args[2]); err != nil {
-				log.Fatalf("gotip: %v", err)
-			}
-		default:
-			log.Fatalf("gotip: usage: gotip download [CL number]")
-		}
-		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
-	newPath := filepath.Join(root, "bin")
-	if p := os.Getenv("PATH"); p != "" {
-		newPath += string(filepath.ListSeparator) + p
-	}
-	cmd.Env = dedupEnv(caseInsensitiveEnv, append(os.Environ(), "GOROOT="+root, "PATH="+newPath))
-	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, clNumber 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()
-	}
-	gitOutput := func(args ...string) ([]byte, error) {
-		cmd := exec.Command("git", args...)
-		cmd.Dir = root
-		return cmd.Output()
-	}
-
-	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)
-		}
-	}
-
-	if clNumber != "" {
-		fmt.Fprintf(os.Stderr, "This will download and execute code from golang.org/cl/%s, continue? [y/n] ", clNumber)
-		var answer string
-		if fmt.Scanln(&answer); answer != "y" {
-			return fmt.Errorf("interrupted")
-		}
-
-		// ls-remote outputs a number of lines like:
-		// 2621ba2c60d05ec0b9ef37cd71e45047b004cead	refs/changes/37/227037/1
-		// 51f2af2be0878e1541d2769bd9d977a7e99db9ab	refs/changes/37/227037/2
-		// af1f3b008281c61c54a5d203ffb69334b7af007c	refs/changes/37/227037/3
-		// 6a10ebae05ce4b01cb93b73c47bef67c0f5c5f2a	refs/changes/37/227037/meta
-		refs, err := gitOutput("ls-remote")
-		if err != nil {
-			return fmt.Errorf("failed to list remotes: %v", err)
-		}
-		r := regexp.MustCompile(`refs/changes/\d\d/` + clNumber + `/(\d+)`)
-		match := r.FindAllStringSubmatch(string(refs), -1)
-		if match == nil {
-			return fmt.Errorf("CL %v not found", clNumber)
-		}
-		var ref string
-		var patchSet int
-		for _, m := range match {
-			ps, _ := strconv.Atoi(m[1])
-			if ps > patchSet {
-				patchSet = ps
-				ref = m[0]
-			}
-		}
-		log.Printf("Fetching CL %v, Patch Set %v...", clNumber, patchSet)
-		if err := git("fetch", "origin", ref); err != nil {
-			return fmt.Errorf("failed to fetch %s: %v", ref, err)
-		}
-	} else {
-		log.Printf("Updating the go development tree...")
-		if err := git("fetch", "origin", "master"); 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", "FETCH_HEAD"); 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", makeScript()))
-	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 makeScript() string {
-	switch runtime.GOOS {
-	case "plan9":
-		return "make.rc"
-	case "windows":
-		return "make.bat"
-	default:
-		return "make.bash"
-	}
-}
-
-const caseInsensitiveEnv = runtime.GOOS == "windows"
-
-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")
-	}
-}
-
-// dedupEnv returns a copy of env with any duplicates removed, in favor of
-// later values.
-// Items are expected to be on the normal environment "key=value" form.
-// If caseInsensitive is true, the case of keys is ignored.
-//
-// This function is unnecessary when the binary is
-// built with Go 1.9+, but keep it around for now until Go 1.8
-// is no longer seen in the wild in common distros.
-//
-// This is copied verbatim from golang.org/x/build/envutil.Dedup at CL 10301
-// (commit a91ae26).
-func dedupEnv(caseInsensitive bool, env []string) []string {
-	out := make([]string, 0, len(env))
-	saw := map[string]int{} // to index in the array
-	for _, kv := range env {
-		eq := strings.Index(kv, "=")
-		if eq < 1 {
-			out = append(out, kv)
-			continue
-		}
-		k := kv[:eq]
-		if caseInsensitive {
-			k = strings.ToLower(k)
-		}
-		if dupIdx, isDup := saw[k]; isDup {
-			out[dupIdx] = kv
-		} else {
-			saw[k] = len(out)
-			out = append(out, kv)
-		}
-	}
-	return out
+	version.RunTip()
 }
diff --git a/gotip/main_test.go b/gotip/main_test.go
deleted file mode 100644
index d15f103..0000000
--- a/gotip/main_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2019 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.
-
-package main
-
-import (
-	"reflect"
-	"testing"
-)
-
-func TestDedupEnv(t *testing.T) {
-	tests := []struct {
-		noCase bool
-		in     []string
-		want   []string
-	}{
-		{
-			noCase: true,
-			in:     []string{"k1=v1", "k2=v2", "K1=v3"},
-			want:   []string{"K1=v3", "k2=v2"},
-		},
-		{
-			noCase: false,
-			in:     []string{"k1=v1", "K1=V2", "k1=v3"},
-			want:   []string{"k1=v3", "K1=V2"},
-		},
-	}
-	for _, tt := range tests {
-		got := dedupEnv(tt.noCase, tt.in)
-		if !reflect.DeepEqual(got, tt.want) {
-			t.Errorf("Dedup(%v, %q) = %q; want %q", tt.noCase, tt.in, got, tt.want)
-		}
-	}
-}
diff --git a/internal/version/gotip.go b/internal/version/gotip.go
new file mode 100644
index 0000000..14ce7ed
--- /dev/null
+++ b/internal/version/gotip.go
@@ -0,0 +1,169 @@
+// 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.
+
+package version
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strconv"
+	"strings"
+)
+
+// RunTip runs the "go" tool from the development tree.
+func RunTip() {
+	log.SetFlags(0)
+
+	root, err := goroot("gotip")
+	if err != nil {
+		log.Fatalf("gotip: %v", err)
+	}
+
+	if len(os.Args) > 1 && os.Args[1] == "download" {
+		switch len(os.Args) {
+		case 2:
+			if err := installTip(root, ""); err != nil {
+				log.Fatalf("gotip: %v", err)
+			}
+		case 3:
+			if _, err := strconv.Atoi(os.Args[2]); err != nil {
+				log.Fatalf("gotip: invalid CL number: %q", os.Args[2])
+			}
+			if err := installTip(root, os.Args[2]); err != nil {
+				log.Fatalf("gotip: %v", err)
+			}
+		default:
+			log.Fatalf("gotip: usage: gotip download [CL number]")
+		}
+		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)
+	}
+
+	runGo(root)
+}
+
+func installTip(root, clNumber 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()
+	}
+	gitOutput := func(args ...string) ([]byte, error) {
+		cmd := exec.Command("git", args...)
+		cmd.Dir = root
+		return cmd.Output()
+	}
+
+	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)
+		}
+	}
+
+	if clNumber != "" {
+		fmt.Fprintf(os.Stderr, "This will download and execute code from golang.org/cl/%s, continue? [y/n] ", clNumber)
+		var answer string
+		if fmt.Scanln(&answer); answer != "y" {
+			return fmt.Errorf("interrupted")
+		}
+
+		// ls-remote outputs a number of lines like:
+		// 2621ba2c60d05ec0b9ef37cd71e45047b004cead	refs/changes/37/227037/1
+		// 51f2af2be0878e1541d2769bd9d977a7e99db9ab	refs/changes/37/227037/2
+		// af1f3b008281c61c54a5d203ffb69334b7af007c	refs/changes/37/227037/3
+		// 6a10ebae05ce4b01cb93b73c47bef67c0f5c5f2a	refs/changes/37/227037/meta
+		refs, err := gitOutput("ls-remote")
+		if err != nil {
+			return fmt.Errorf("failed to list remotes: %v", err)
+		}
+		r := regexp.MustCompile(`refs/changes/\d\d/` + clNumber + `/(\d+)`)
+		match := r.FindAllStringSubmatch(string(refs), -1)
+		if match == nil {
+			return fmt.Errorf("CL %v not found", clNumber)
+		}
+		var ref string
+		var patchSet int
+		for _, m := range match {
+			ps, _ := strconv.Atoi(m[1])
+			if ps > patchSet {
+				patchSet = ps
+				ref = m[0]
+			}
+		}
+		log.Printf("Fetching CL %v, Patch Set %v...", clNumber, patchSet)
+		if err := git("fetch", "origin", ref); err != nil {
+			return fmt.Errorf("failed to fetch %s: %v", ref, err)
+		}
+	} else {
+		log.Printf("Updating the go development tree...")
+		if err := git("fetch", "origin", "master"); 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", "FETCH_HEAD"); 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", makeScript()))
+	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 makeScript() string {
+	switch runtime.GOOS {
+	case "plan9":
+		return "make.rc"
+	case "windows":
+		return "make.bat"
+	default:
+		return "make.bash"
+	}
+}
diff --git a/internal/version/version.go b/internal/version/version.go
index a63c649..866e4ad 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -50,6 +50,10 @@
 		log.Fatalf("%s: not downloaded. Run '%s download' to install to %v", version, version, root)
 	}
 
+	runGo(root)
+}
+
+func runGo(root string) {
 	gobin := filepath.Join(root, "bin", "go"+exe())
 	cmd := exec.Command(gobin, os.Args[1:]...)
 	cmd.Stdin = os.Stdin
@@ -419,6 +423,10 @@
 }
 
 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 getOS() {
 	case "plan9":
 		return "", fmt.Errorf("%q not yet supported", runtime.GOOS)