cmd/fetchlogs: link each subrepo Go revision to a unique subdirectory

Prior to this change, we would link only one (arbitrary) Go revision
for each subrepo revision, causing greplogs to under-report subrepo
failures.

Change-Id: Id36360005283e85d2c0077958d5839901db921f8
Reviewed-on: https://go-review.googlesource.com/c/build/+/340170
Trust: Bryan C. Mills <bcmills@google.com>
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/cmd/fetchlogs/fetchlogs.go b/cmd/fetchlogs/fetchlogs.go
index 8191383..66209b3 100644
--- a/cmd/fetchlogs/fetchlogs.go
+++ b/cmd/fetchlogs/fetchlogs.go
@@ -27,6 +27,7 @@
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -36,9 +37,12 @@
 	"net/url"
 	"os"
 	"path/filepath"
+	"strings"
 	"sync"
 	"time"
 
+	"golang.org/x/build/maintner"
+	"golang.org/x/build/maintner/godata"
 	"golang.org/x/build/repos"
 	"golang.org/x/build/types"
 )
@@ -119,9 +123,25 @@
 			if err != nil {
 				log.Fatal("malformed revision date: ", err)
 			}
-			revDir := revToDir(rev.Revision, date)
+			var goDate time.Time
+			if rev.GoRevision != "" {
+				commit := goProject().GitCommit(rev.GoRevision)
+				goDate = commit.CommitTime
+			}
+			revDir, revDirDepth := revToDir(rev.Revision, date, rev.GoRevision, goDate)
 			ensureDir(revDir)
 
+			if rev.GoRevision != "" {
+				// In October 2021 we started creating a separate subdirectory for
+				// each Go repo commit. (Previously, we overwrote the link for each
+				// subrepo commit when downloading a new Go commit.) Remove the
+				// previous links, if any, so that greplogs won't double-count them.
+				prevRevDir, _ := revToDir(rev.Revision, date, "", time.Time{})
+				if err := os.RemoveAll(prevRevDir); err != nil {
+					log.Fatal(err)
+				}
+			}
+
 			// Save revision metadata.
 			buf := bytes.Buffer{}
 			enc := json.NewEncoder(&buf)
@@ -148,17 +168,17 @@
 				}
 
 				wg.Add(1)
-				go func(rev, builder, logURL string) {
+				go func(builder, logURL string) {
 					defer wg.Done()
 					logPath := filepath.Join("log", filepath.Base(logURL))
 					err := fetcher.getFile(logURL, logPath)
 					if err != nil {
 						log.Fatal("error fetching log: ", err)
 					}
-					if err := linkLog(revDir, builder, logPath); err != nil {
+					if err := linkLog(revDir, revDirDepth, builder, logPath); err != nil {
 						log.Fatal("error linking log: ", err)
 					}
-				}(revDir, status.Builders[i], res)
+				}(status.Builders[i], res)
 			}
 		}
 	}
@@ -249,10 +269,40 @@
 	return p.err
 }
 
+var (
+	goProjectOnce   sync.Once
+	cachedGoProject *maintner.GerritProject
+	goProjectErr    error
+)
+
+func getGoProject(ctx context.Context) (*maintner.GerritProject, error) {
+	corpus, err := godata.Get(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	gp := corpus.Gerrit().Project("go.googlesource.com", "go")
+	if gp == nil {
+		return nil, fmt.Errorf("go.googlesource.com/go Gerrit project not found")
+	}
+
+	return gp, nil
+}
+
+func goProject() *maintner.GerritProject {
+	goProjectOnce.Do(func() {
+		cachedGoProject, goProjectErr = getGoProject(context.Background())
+	})
+	if goProjectErr != nil {
+		log.Fatal(goProjectErr)
+	}
+	return cachedGoProject
+}
+
 // ensureDir creates directory name if it does not exist.
 func ensureDir(name string) {
-	err := os.Mkdir(name, 0777)
-	if err != nil && !os.IsExist(err) {
+	err := os.MkdirAll(name, 0777)
+	if err != nil {
 		log.Fatal("error creating directory ", name, ": ", err)
 	}
 }
@@ -286,9 +336,9 @@
 
 // linkLog creates a symlink for finding logPath based on its git
 // revision and builder.
-func linkLog(revDir, builder, logPath string) error {
+func linkLog(revDir string, revDirDepth int, builder, logPath string) error {
 	// Create symlink.
-	err := os.Symlink("../../"+logPath, filepath.Join(revDir, builder))
+	err := os.Symlink(strings.Repeat("../", revDirDepth)+logPath, filepath.Join(revDir, builder))
 	if err != nil && !os.IsExist(err) {
 		return err
 	}
@@ -302,6 +352,16 @@
 }
 
 // revToDir returns the path of the revision directory for revision.
-func revToDir(revision string, date time.Time) string {
-	return filepath.Join("rev", date.Format("2006-01-02T15:04:05")+"-"+revision[:7])
+func revToDir(revision string, date time.Time, goRev string, goDate time.Time) (dir string, depth int) {
+	if goDate.After(date) {
+		date = goDate
+	}
+	dateStr := date.Format("2006-01-02T15:04:05")
+
+	parts := []string{dateStr, revision[:7]}
+	if goRev != "" {
+		parts = append(parts, goRev[:7])
+	}
+
+	return filepath.Join("rev", strings.Join(parts, "-")), 2
 }