gotip: add support for downloading a specific CL

Fixes golang/go#38452

Change-Id: I44b5540b9bb2f969afbffa4746f6dfa70d9ac070
Reviewed-on: https://go-review.googlesource.com/c/dl/+/228260
Reviewed-by: Katie Hockman <katie@golang.org>
diff --git a/gotip/main.go b/gotip/main.go
index 6338234..caafe34 100644
--- a/gotip/main.go
+++ b/gotip/main.go
@@ -12,6 +12,7 @@
 // And then use the gotip command as if it were your normal go command.
 //
 // To update, run "gotip download" again.
+// To download a specific CL, run "gotip download NUMBER".
 package main
 
 import (
@@ -22,7 +23,9 @@
 	"os/exec"
 	"os/user"
 	"path/filepath"
+	"regexp"
 	"runtime"
+	"strconv"
 	"strings"
 )
 
@@ -34,9 +37,21 @@
 		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)
+	if 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)
@@ -66,7 +81,7 @@
 	os.Exit(0)
 }
 
-func installTip(root string) error {
+func installTip(root, clNumber string) error {
 	git := func(args ...string) error {
 		cmd := exec.Command("git", args...)
 		cmd.Stdin = os.Stdin
@@ -75,6 +90,11 @@
 		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 {
@@ -83,9 +103,45 @@
 		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"); err != nil {
+		if err := git("fetch", "origin", "master"); err != nil {
 			return fmt.Errorf("failed to fetch git repository updates: %v", err)
 		}
 	}
@@ -93,7 +149,7 @@
 	// 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 {
+	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