[tools][cts] Add roll --repo flag

When combined with --revision you can try rolling from a private fork.

Change-Id: I7c350afba4805c1315dd3a9d7df2f829757c7dd6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/179206
Kokoro: Kokoro <noreply+kokoro@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
diff --git a/tools/src/cmd/cts/common/deps.go b/tools/src/cmd/cts/common/deps.go
index fada3ee..ad1d792 100644
--- a/tools/src/cmd/cts/common/deps.go
+++ b/tools/src/cmd/cts/common/deps.go
@@ -48,12 +48,12 @@
 	return strings.ReplaceAll(strings.ReplaceAll(s, `/`, `\/`), `.`, `\.`)
 }
 
-// UpdateCTSHashInDeps replaces the CTS hashes in 'deps' with 'newCTSHash'.
+// UpdateCTSHashInDeps replaces the CTS hashes in 'deps' with 'newGitURL@newCTSHash'.
 // Returns:
 //
 //	newDEPS    - the new DEPS content
 //	oldCTSHash - the old CTS hash in the 'deps'
-func UpdateCTSHashInDeps(deps, newCTSHash string) (newDEPS, oldCTSHash string, err error) {
+func UpdateCTSHashInDeps(deps, newGitURL, newCTSHash string) (newDEPS, oldCTSHash string, err error) {
 	// Collect old CTS hashes, and replace these with newCTSHash
 	b := strings.Builder{}
 	oldCTSHashes := []string{}
@@ -63,9 +63,12 @@
 	}
 	end := 0
 	for _, match := range matches {
+		replacement := fmt.Sprintf("%v@%v", newGitURL, newCTSHash)
+		replacement = strings.ReplaceAll(replacement, "https://chromium.googlesource.com", "{chromium_git}")
+
 		oldCTSHashes = append(oldCTSHashes, deps[match[0]+len(ctsHashPrefix):match[1]])
 		b.WriteString(deps[end:match[0]])
-		b.WriteString(ctsHashPrefix + newCTSHash)
+		b.WriteString(replacement)
 		end = match[1]
 	}
 	b.WriteString(deps[end:])
diff --git a/tools/src/cmd/cts/common/deps_test.go b/tools/src/cmd/cts/common/deps_test.go
new file mode 100644
index 0000000..07ed5fd
--- /dev/null
+++ b/tools/src/cmd/cts/common/deps_test.go
@@ -0,0 +1,99 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package common_test
+
+import (
+	"testing"
+
+	"dawn.googlesource.com/dawn/tools/src/cmd/cts/common"
+)
+
+func TestUpdateCTSHashInDeps(t *testing.T) {
+	for _, test := range []struct {
+		oldDeps          string
+		newGitURL        string
+		newCTSHash       string
+		expectedDeps     string
+		expectedRevision string
+	}{
+		{
+			oldDeps: `
+BEFORE BEFORE BEFORE
+'third_party/webgpu-cts': {
+	'url': '{chromium_git}/external/github.com/gpuweb/cts@3e45aee0b16dc724a79a0feb0490e2ddb06c9f0d',
+	'condition': 'build_with_chromium',
+},
+AFTER AFTER AFTER
+`,
+			newGitURL:  `https://chromium.googlesource.com/external/github.com/gpuweb/cts`,
+			newCTSHash: `ef28f69a837732739a1e0411cdf7d44a36bfeaa1`,
+			expectedDeps: `
+BEFORE BEFORE BEFORE
+'third_party/webgpu-cts': {
+	'url': '{chromium_git}/external/github.com/gpuweb/cts@ef28f69a837732739a1e0411cdf7d44a36bfeaa1',
+	'condition': 'build_with_chromium',
+},
+AFTER AFTER AFTER
+`,
+			expectedRevision: `3e45aee0b16dc724a79a0feb0490e2ddb06c9f0d`,
+		},
+		{
+			oldDeps: `
+BEFORE BEFORE BEFORE
+'third_party/webgpu-cts': {
+	'url': '{chromium_git}/external/github.com/gpuweb/cts@3e45aee0b16dc724a79a0feb0490e2ddb06c9f0d',
+	'condition': 'build_with_chromium',
+},
+AFTER AFTER AFTER
+`,
+			newGitURL:  `https://github.com/a_cts_contributor/cts.git`,
+			newCTSHash: `ef28f69a837732739a1e0411cdf7d44a36bfeaa1`,
+			expectedDeps: `
+BEFORE BEFORE BEFORE
+'third_party/webgpu-cts': {
+	'url': 'https://github.com/a_cts_contributor/cts.git@ef28f69a837732739a1e0411cdf7d44a36bfeaa1',
+	'condition': 'build_with_chromium',
+},
+AFTER AFTER AFTER
+`,
+			expectedRevision: `3e45aee0b16dc724a79a0feb0490e2ddb06c9f0d`,
+		},
+	} {
+		newDeps, newRevision, err := common.UpdateCTSHashInDeps(test.oldDeps, test.newGitURL, test.newCTSHash)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+		if newDeps != test.expectedDeps {
+			t.Errorf("returned DEPS was not as expected.\nexpected:\n%v\ngot:\n%v\n", test.expectedDeps, newDeps)
+		}
+		if newRevision != test.expectedRevision {
+			t.Errorf("returned revision was not as expected. expected: '%v', got: '%v'", test.expectedDeps, newDeps)
+		}
+	}
+}
diff --git a/tools/src/cmd/cts/export/export.go b/tools/src/cmd/cts/export/export.go
index c7319ac..f323109 100644
--- a/tools/src/cmd/cts/export/export.go
+++ b/tools/src/cmd/cts/export/export.go
@@ -103,7 +103,7 @@
 	if err != nil {
 		return fmt.Errorf("failed to download DEPS from %v: %w", ps.RefsChanges(), err)
 	}
-	_, ctsHash, err := common.UpdateCTSHashInDeps(deps, "<unused>")
+	_, ctsHash, err := common.UpdateCTSHashInDeps(deps, "<unused>", "<unused>")
 	if err != nil {
 		return fmt.Errorf("failed to find CTS hash in deps: %w", err)
 	}
diff --git a/tools/src/cmd/cts/roll/roll.go b/tools/src/cmd/cts/roll/roll.go
index e29c4c4..00b91a2 100644
--- a/tools/src/cmd/cts/roll/roll.go
+++ b/tools/src/cmd/cts/roll/roll.go
@@ -83,6 +83,7 @@
 	nodePath            string
 	auth                authcli.Flags
 	cacheDir            string
+	ctsGitURL           string
 	ctsRevision         string
 	force               bool // Create a new roll, even if CTS is up to date
 	rebuild             bool // Rebuild the expectations file from scratch
@@ -113,6 +114,7 @@
 	flag.StringVar(&c.flags.npmPath, "npm", npmPath, "path to npm")
 	flag.StringVar(&c.flags.nodePath, "node", fileutils.NodePath(), "path to node")
 	flag.StringVar(&c.flags.cacheDir, "cache", common.DefaultCacheDir, "path to the results cache")
+	flag.StringVar(&c.flags.ctsGitURL, "repo", cfg.Git.CTS.HttpsURL(), "the CTS source repo")
 	flag.StringVar(&c.flags.ctsRevision, "revision", refMain, "revision of the CTS to roll")
 	flag.BoolVar(&c.flags.force, "force", false, "create a new roll, even if CTS is up to date")
 	flag.BoolVar(&c.flags.rebuild, "rebuild", false, "rebuild the expectation file from scratch")
@@ -161,10 +163,6 @@
 	if err != nil {
 		return err
 	}
-	cts, err := gitiles.New(ctx, cfg.Git.CTS.Host, cfg.Git.CTS.Project)
-	if err != nil {
-		return err
-	}
 	dawn, err := gitiles.New(ctx, cfg.Git.Dawn.Host, cfg.Git.Dawn.Project)
 	if err != nil {
 		return err
@@ -188,14 +186,13 @@
 		rdb:                 rdb,
 		git:                 git,
 		gerrit:              gerrit,
-		gitiles:             gitilesRepos{cts: cts, dawn: dawn},
+		gitiles:             gitilesRepos{dawn: dawn},
 		ctsDir:              ctsDir,
 	}
 	return r.roll(ctx)
 }
 
 type gitilesRepos struct {
-	cts  *gitiles.Gitiles
 	dawn *gitiles.Gitiles
 }
 
@@ -219,8 +216,21 @@
 		return err
 	}
 
+	// Checkout the CTS at the latest revision
+	ctsRepo, err := r.checkout("cts", r.ctsDir, r.flags.ctsGitURL, r.flags.ctsRevision)
+	if err != nil {
+		return err
+	}
+
+	// Obtain the target CTS revision hash
+	ctsRevisionLog, err := ctsRepo.Log(&git.LogOptions{From: r.flags.ctsRevision + "^", To: r.flags.ctsRevision})
+	if err != nil {
+		return err
+	}
+	newCTSHash := ctsRevisionLog[0].Hash.String()
+
 	// Update the DEPS file
-	updatedDEPS, newCTSHash, oldCTSHash, err := r.updateDEPS(ctx, dawnHash)
+	updatedDEPS, oldCTSHash, err := r.updateDEPS(ctx, dawnHash, newCTSHash)
 	if err != nil {
 		return err
 	}
@@ -232,12 +242,6 @@
 
 	log.Printf("starting CTS roll from %v to %v...", oldCTSHash[:8], newCTSHash[:8])
 
-	// Checkout the CTS at the latest revision
-	ctsRepo, err := r.checkout("cts", r.ctsDir, r.cfg.Git.CTS.HttpsURL(), newCTSHash)
-	if err != nil {
-		return err
-	}
-
 	// Fetch the log of changes between last roll and now
 	ctsLog, err := ctsRepo.Log(&git.LogOptions{From: oldCTSHash, To: newCTSHash})
 	if err != nil {
@@ -765,23 +769,18 @@
 	return files, nil
 }
 
-// updateDEPS fetches and updates the Dawn DEPS file at 'dawnRef' so that all
-// CTS hashes are changed to r.ctsRevision.
-func (r *roller) updateDEPS(ctx context.Context, dawnRef string) (newDEPS, newCTSHash, oldCTSHash string, err error) {
-	newCTSHash, err = r.gitiles.cts.Hash(ctx, r.flags.ctsRevision)
-	if err != nil {
-		return "", "", "", err
-	}
+// updateDEPS fetches and updates the Dawn DEPS file at 'dawnRef' so that all CTS hashes are changed to newCTSHash
+func (r *roller) updateDEPS(ctx context.Context, dawnRef, newCTSHash string) (newDEPS, oldCTSHash string, err error) {
 	deps, err := r.gitiles.dawn.DownloadFile(ctx, dawnRef, depsRelPath)
 	if err != nil {
-		return "", "", "", err
+		return "", "", err
 	}
-	newDEPS, oldCTSHash, err = common.UpdateCTSHashInDeps(deps, newCTSHash)
+	newDEPS, oldCTSHash, err = common.UpdateCTSHashInDeps(deps, r.flags.ctsGitURL, newCTSHash)
 	if err != nil {
-		return "", "", "", err
+		return "", "", err
 	}
 
-	return newDEPS, newCTSHash, oldCTSHash, nil
+	return newDEPS, oldCTSHash, nil
 }
 
 // genTSDepList returns a list of source files, for the CTS checkout at r.ctsDir
diff --git a/tools/src/gerrit/gerrit.go b/tools/src/gerrit/gerrit.go
index 9d4d310..b293130 100644
--- a/tools/src/gerrit/gerrit.go
+++ b/tools/src/gerrit/gerrit.go
@@ -210,6 +210,10 @@
 	if err != nil {
 		return nil, err
 	}
+	if change.URL == "" {
+		base := g.client.BaseURL()
+		change.URL = fmt.Sprintf("%vc/%v/+/%v", base.String(), change.Project, change.Number)
+	}
 	return change, nil
 }