From cb27fed361a2effcc438ccf218b110d6a5489245 Mon Sep 17 00:00:00 2001
From: Ivan Trubach <mr.trubach@icloud.com>
Date: Sun, 24 May 2020 14:15:26 +0300
Subject: [PATCH] Do not rebuild gotip if the HEAD does not change

On successful build a sentinel zero-byte file is written,
similar to .unpacked-success in internal/version. If such
file exists on next gotip download, compare HEADs before
and after FETCH_HEAD checkout, and trigger the build iff
they differ. In that case, sentinel file is removed to
make git clean happy and indicate that rebuild is needed.
---
 gotip/main.go | 40 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/gotip/main.go b/gotip/main.go
index d1c69cc7..81bf2407 100644
--- a/gotip/main.go
+++ b/gotip/main.go
@@ -16,8 +16,10 @@
 package main
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"os"
 	"os/exec"
@@ -95,6 +97,9 @@ func installTip(root, clNumber string) error {
 		cmd.Dir = root
 		return cmd.Output()
 	}
+	chomp := func(s []byte) []byte {
+		return bytes.TrimRight(s, " \t\r\n")
+	}
 
 	if _, err := os.Stat(filepath.Join(root, ".git")); err != nil {
 		if err := os.MkdirAll(root, 0755); err != nil {
@@ -105,6 +110,16 @@ func installTip(root, clNumber string) error {
 		}
 	}
 
+	// Get current HEAD if there is an existing build.
+	var oldHead []byte
+	if _, err := os.Stat(filepath.Join(root, builtOkay)); err == nil {
+		head, err := gitOutput("rev-parse", "--short", "HEAD")
+		if err != nil {
+			return fmt.Errorf("failed to parse old HEAD revision: %v", err)
+		}
+		oldHead = head
+	}
+
 	if clNumber != "" {
 		fmt.Fprintf(os.Stderr, "This will download and execute code from golang.org/cl/%s, continue? [y/n] ", clNumber)
 		var answer string
@@ -152,6 +167,23 @@ func installTip(root, clNumber string) error {
 	if err := git("-c", "advice.detachedHead=false", "checkout", "FETCH_HEAD"); err != nil {
 		return fmt.Errorf("failed to checkout git repository: %v", err)
 	}
+
+	// Compare old and new HEADs to avoid unnecessary rebuilds if there are no changes.
+	// Notice that oldHead is not nil iff the last build was successful.
+	if oldHead != nil {
+		newHead, err := gitOutput("rev-parse", "--short", "HEAD")
+		if err != nil {
+			return fmt.Errorf("failed to parse new HEAD revision: %v", err)
+		}
+		if bytes.Equal(oldHead, newHead) {
+			log.Printf("Already built %s in %v", chomp(newHead), root)
+			return nil
+		}
+		if err := os.Remove(filepath.Join(root, builtOkay)); err != nil {
+			return err
+		}
+	}
+
 	// It shouldn't be the case, but in practice sometimes binary artifacts
 	// generated by earlier Go versions interfere with the build.
 	//
@@ -181,7 +213,9 @@ func installTip(root, clNumber string) error {
 	if err := cmd.Run(); err != nil {
 		return fmt.Errorf("failed to build go: %v", err)
 	}
-
+	if err := ioutil.WriteFile(filepath.Join(root, builtOkay), nil, 0644); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -198,6 +232,10 @@ func makeScript() string {
 
 const caseInsensitiveEnv = runtime.GOOS == "windows"
 
+// builtOkay is a sentinel zero-byte file to indicate that Go
+// repository was cloned and built successfully.
+const builtOkay = ".built-success"
+
 func exe() string {
 	if runtime.GOOS == "windows" {
 		return ".exe"