[autoroll] Support projects without Commit Queue

Bug: skia:9529
Change-Id: I922f6b3b94ae40e5c78b8eb3c7484f597727c586
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/250318
Commit-Queue: Eric Boren <borenet@google.com>
Reviewed-by: Ravi Mistry <rmistry@google.com>
diff --git a/autoroll/go/codereview/config.go b/autoroll/go/codereview/config.go
index daf30c2..f805b06 100644
--- a/autoroll/go/codereview/config.go
+++ b/autoroll/go/codereview/config.go
@@ -20,17 +20,22 @@
 	// GERRIT_CONFIG_ANGLE is a Gerrit server configuration used by ANGLE.
 	GERRIT_CONFIG_ANGLE = "angle"
 
-	// GERIT_CONFIG_CHROMIUM is a Gerrit server configuration used by
+	// GERRIT_CONFIG_CHROMIUM is a Gerrit server configuration used by
 	// Chromium and related projects.
 	GERRIT_CONFIG_CHROMIUM = "chromium"
+
+	// GERRIT_CONFIG_CHROMIUM_NO_CQ is a Gerrit server configuration used by
+	// Chromium for projects with no Commit Queue.
+	GERRIT_CONFIG_CHROMIUM_NO_CQ = "chromium-no-cq"
 )
 
 var (
 	// GERRIT_CONFIGS maps Gerrit config names to gerrit.Configs.
 	GERRIT_CONFIGS = map[string]*gerrit.Config{
-		GERRIT_CONFIG_ANDROID:  gerrit.CONFIG_ANDROID,
-		GERRIT_CONFIG_ANGLE:    gerrit.CONFIG_ANGLE,
-		GERRIT_CONFIG_CHROMIUM: gerrit.CONFIG_CHROMIUM,
+		GERRIT_CONFIG_ANDROID:        gerrit.CONFIG_ANDROID,
+		GERRIT_CONFIG_ANGLE:          gerrit.CONFIG_ANGLE,
+		GERRIT_CONFIG_CHROMIUM:       gerrit.CONFIG_CHROMIUM,
+		GERRIT_CONFIG_CHROMIUM_NO_CQ: gerrit.CONFIG_CHROMIUM_NO_CQ,
 	}
 )
 
diff --git a/autoroll/go/codereview/roll_test.go b/autoroll/go/codereview/roll_test.go
index db67843..7f32993 100644
--- a/autoroll/go/codereview/roll_test.go
+++ b/autoroll/go/codereview/roll_test.go
@@ -428,6 +428,10 @@
 		RollingTo:   "def456",
 		Subject:     "roll the deps",
 	}
+	if !cfg.HasCq {
+		expect.CqFinished = true
+		expect.Result = autoroll.ROLL_RESULT_FAILURE
+	}
 	deepequal.AssertDeepEqual(t, expect, a)
 
 	// CQ failed.
@@ -481,6 +485,11 @@
 	expect.CqFinished = false
 	expect.IsDryRun = true
 	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_IN_PROGRESS
+	if !cfg.HasCq {
+		expect.DryRunFinished = true
+		expect.DryRunSuccess = true
+		expect.Result = autoroll.ROLL_RESULT_DRY_RUN_SUCCESS
+	}
 	a.IsDryRun = true
 	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
 	deepequal.AssertDeepEqual(t, expect, a)
@@ -500,6 +509,10 @@
 			Status:   autoroll.TRYBOT_STATUS_COMPLETED,
 		},
 	}
+	if !cfg.HasCq {
+		expect.DryRunSuccess = true
+		expect.Result = autoroll.ROLL_RESULT_DRY_RUN_SUCCESS
+	}
 	a.TryResults = expect.TryResults
 	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
 	deepequal.AssertDeepEqual(t, expect, a)
@@ -510,6 +523,7 @@
 	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
 	expect.Closed = true
 	expect.DryRunFinished = true
+	expect.DryRunSuccess = false
 	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_FAILURE
 	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
 	deepequal.AssertDeepEqual(t, expect, a)
@@ -553,6 +567,10 @@
 	testUpdateFromGerritChangeInfo(t, gerrit.CONFIG_CHROMIUM)
 }
 
+func TestUpdateFromGerritChangeInfoChromiumNoCQ(t *testing.T) {
+	testUpdateFromGerritChangeInfo(t, gerrit.CONFIG_CHROMIUM_NO_CQ)
+}
+
 func TestUpdateFromGitHubPullRequest(t *testing.T) {
 	unittest.SmallTest(t)
 
diff --git a/autoroll/go/repo_manager/no_checkout_deps_repo_manager_test.go b/autoroll/go/repo_manager/no_checkout_deps_repo_manager_test.go
index 3375e34..1a0177c 100644
--- a/autoroll/go/repo_manager/no_checkout_deps_repo_manager_test.go
+++ b/autoroll/go/repo_manager/no_checkout_deps_repo_manager_test.go
@@ -23,7 +23,7 @@
 	"go.skia.org/infra/go/testutils/unittest"
 )
 
-func setupNoCheckout(t *testing.T, cfg *NoCheckoutDEPSRepoManagerConfig, strategy string) (context.Context, string, RepoManager, *git_testutils.GitBuilder, *git_testutils.GitBuilder, *gitiles_testutils.MockRepo, *gitiles_testutils.MockRepo, []string, *mockhttpclient.URLMock, func()) {
+func setupNoCheckout(t *testing.T, cfg *NoCheckoutDEPSRepoManagerConfig, strategy string, gerritCfg *gerrit.Config) (context.Context, string, RepoManager, *git_testutils.GitBuilder, *git_testutils.GitBuilder, *gitiles_testutils.MockRepo, *gitiles_testutils.MockRepo, []string, *mockhttpclient.URLMock, func()) {
 	unittest.LargeTest(t)
 
 	wd, err := ioutil.TempDir("", "")
@@ -68,7 +68,7 @@
 	require.NoError(t, err)
 	serialized = append([]byte("abcd\n"), serialized...)
 	urlmock.MockOnce(gUrl+"/a/accounts/self/detail", mockhttpclient.MockGetDialogue(serialized))
-	g, err := gerrit.NewGerrit(gUrl, gitcookies, urlmock.Client())
+	g, err := gerrit.NewGerritWithConfig(gerritCfg, gUrl, gitcookies, urlmock.Client())
 	require.NoError(t, err)
 
 	cfg.ChildRepo = child.RepoUrl()
@@ -111,7 +111,7 @@
 
 func TestNoCheckoutDEPSRepoManagerUpdate(t *testing.T) {
 	cfg := noCheckoutDEPSCfg()
-	ctx, _, rm, _, parentRepo, mockChild, mockParent, childCommits, _, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_BATCH)
+	ctx, _, rm, _, parentRepo, mockChild, mockParent, childCommits, _, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_BATCH, gerrit.CONFIG_CHROMIUM)
 	defer cleanup()
 
 	mockParent.MockGetCommit(ctx, "master")
@@ -148,7 +148,7 @@
 
 func TestNoCheckoutDEPSRepoManagerStrategies(t *testing.T) {
 	cfg := noCheckoutDEPSCfg()
-	ctx, _, rm, _, parentRepo, mockChild, mockParent, childCommits, _, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_SINGLE)
+	ctx, _, rm, _, parentRepo, mockChild, mockParent, childCommits, _, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_SINGLE, gerrit.CONFIG_CHROMIUM)
 	defer cleanup()
 
 	mockParent.MockGetCommit(ctx, "master")
@@ -179,9 +179,9 @@
 	require.Equal(t, childCommits[1], rm.NextRollRev().Id)
 }
 
-func TestNoCheckoutDEPSRepoManagerCreateNewRoll(t *testing.T) {
+func testNoCheckoutDEPSRepoManagerCreateNewRoll(t *testing.T, gerritCfg *gerrit.Config) {
 	cfg := noCheckoutDEPSCfg()
-	ctx, _, rm, childRepo, parentRepo, mockChild, mockParent, childCommits, urlmock, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_BATCH)
+	ctx, _, rm, childRepo, parentRepo, mockChild, mockParent, childCommits, urlmock, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_BATCH, gerritCfg)
 	defer cleanup()
 
 	mockParent.MockGetCommit(ctx, "master")
@@ -284,20 +284,35 @@
 	urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/ready", mockhttpclient.MockPostDialogue("application/json", reqBody, []byte("")))
 
 	// Mock the request to set the CQ.
-	reqBody = []byte(`{"labels":{"Code-Review":1,"Commit-Queue":2},"message":"","reviewers":[{"reviewer":"me@google.com"}]}`)
+	if gerritCfg.HasCq {
+		reqBody = []byte(`{"labels":{"Code-Review":1,"Commit-Queue":2},"message":"","reviewers":[{"reviewer":"me@google.com"}]}`)
+	} else {
+		reqBody = []byte(`{"labels":{"Code-Review":1},"message":"","reviewers":[{"reviewer":"me@google.com"}]}`)
+	}
 	urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/revisions/ps1/review", mockhttpclient.MockPostDialogue("application/json", reqBody, []byte("")))
+	if !gerritCfg.HasCq {
+		urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/submit", mockhttpclient.MockPostDialogue("application/json", []byte("{}"), []byte("")))
+	}
 
 	issue, err := rm.CreateNewRoll(ctx, rm.LastRollRev(), rm.NextRollRev(), []string{"me@google.com"}, "", false)
 	require.NoError(t, err)
 	require.NotEqual(t, 0, issue)
 }
 
+func TestNoCheckoutDEPSRepoManagerCreateNewRoll(t *testing.T) {
+	testNoCheckoutDEPSRepoManagerCreateNewRoll(t, gerrit.CONFIG_CHROMIUM)
+}
+
+func TestNoCheckoutDEPSRepoManagerCreateNewRollNoCQ(t *testing.T) {
+	testNoCheckoutDEPSRepoManagerCreateNewRoll(t, gerrit.CONFIG_CHROMIUM_NO_CQ)
+}
+
 func TestNoCheckoutDEPSRepoManagerCreateNewRollTransitive(t *testing.T) {
 	cfg := noCheckoutDEPSCfg()
 	cfg.TransitiveDeps = map[string]string{
 		"child/dep": "parent/dep",
 	}
-	ctx, _, rm, childRepo, parentRepo, mockChild, mockParent, childCommits, urlmock, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_BATCH)
+	ctx, _, rm, childRepo, parentRepo, mockChild, mockParent, childCommits, urlmock, cleanup := setupNoCheckout(t, cfg, strategy.ROLL_STRATEGY_BATCH, gerrit.CONFIG_CHROMIUM)
 	defer cleanup()
 
 	mockParent.MockGetCommit(ctx, "master")
diff --git a/autoroll/go/repo_manager/no_checkout_repo_manager.go b/autoroll/go/repo_manager/no_checkout_repo_manager.go
index c934988..60b151b 100644
--- a/autoroll/go/repo_manager/no_checkout_repo_manager.go
+++ b/autoroll/go/repo_manager/no_checkout_repo_manager.go
@@ -134,6 +134,14 @@
 		return 0, fmt.Errorf("Failed to set review: %s", err)
 	}
 
+	// Manually submit if necessary.
+	if !rm.g.Config().HasCq {
+		if err := rm.g.Submit(ctx, ci); err != nil {
+			// TODO(borenet): Should we try to abandon the CL?
+			return 0, fmt.Errorf("Failed to submit: %s", err)
+		}
+	}
+
 	return ci.Issue, nil
 }
 
diff --git a/go/autoroll/autoroll.go b/go/autoroll/autoroll.go
index 40b5602..5590be7 100644
--- a/go/autoroll/autoroll.go
+++ b/go/autoroll/autoroll.go
@@ -236,7 +236,9 @@
 			continue
 		}
 		if !t.Succeeded() {
-			sklog.Infof("    ...failed")
+			if t.Finished() {
+				sklog.Infof("    ...failed")
+			}
 			return false
 		}
 	}
diff --git a/go/gerrit/config.go b/go/gerrit/config.go
index c3f5b00..154b2d1 100644
--- a/go/gerrit/config.go
+++ b/go/gerrit/config.go
@@ -5,6 +5,7 @@
 		SelfApproveLabels: map[string]int{
 			CODEREVIEW_LABEL: CODEREVIEW_LABEL_SELF_APPROVE,
 		},
+		HasCq: true,
 		SetCqLabels: map[string]int{
 			AUTOSUBMIT_LABEL:      AUTOSUBMIT_LABEL_SUBMIT,
 			PRESUBMIT_READY_LABEL: PRESUBMIT_READY_LABEL_ENABLE,
@@ -44,6 +45,7 @@
 		SelfApproveLabels: map[string]int{
 			CODEREVIEW_LABEL: CODEREVIEW_LABEL_SELF_APPROVE,
 		},
+		HasCq: true,
 		SetCqLabels: map[string]int{
 			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_SUBMIT,
 		},
@@ -70,6 +72,7 @@
 		SelfApproveLabels: map[string]int{
 			CODEREVIEW_LABEL: CODEREVIEW_LABEL_APPROVE,
 		},
+		HasCq: true,
 		SetCqLabels: map[string]int{
 			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_SUBMIT,
 		},
@@ -91,6 +94,25 @@
 		DryRunFailureLabels:     map[string]int{},
 		DryRunUsesTryjobResults: true,
 	}
+
+	CONFIG_CHROMIUM_NO_CQ = &Config{
+		SelfApproveLabels: map[string]int{
+			CODEREVIEW_LABEL: CODEREVIEW_LABEL_APPROVE,
+		},
+		HasCq:           false,
+		SetCqLabels:     map[string]int{},
+		SetDryRunLabels: map[string]int{},
+		NoCqLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_NONE,
+		},
+		CqActiveLabels:          map[string]int{},
+		CqSuccessLabels:         map[string]int{},
+		CqFailureLabels:         map[string]int{},
+		DryRunActiveLabels:      map[string]int{},
+		DryRunSuccessLabels:     map[string]int{},
+		DryRunFailureLabels:     map[string]int{},
+		DryRunUsesTryjobResults: false,
+	}
 )
 
 type Config struct {
@@ -98,6 +120,9 @@
 	// same as a normal approval.
 	SelfApproveLabels map[string]int
 
+	// Whether or not this project has a Commit Queue.
+	HasCq bool
+
 	// Labels to set to run the Commit Queue.
 	SetCqLabels map[string]int
 	// Labels to set to run the Commit Queue in dry run mode.
@@ -164,6 +189,9 @@
 // CqRunning returns true if the commit queue is still running. Returns false if
 // the change is merged or abandoned.
 func (c *Config) CqRunning(ci *ChangeInfo) bool {
+	if !c.HasCq {
+		return false
+	}
 	if ci.IsClosed() {
 		return false
 	}
@@ -184,6 +212,9 @@
 // DryRunRunning returns true if the dry run is still running. Returns false if
 // the change is merged or abandoned.
 func (c *Config) DryRunRunning(ci *ChangeInfo) bool {
+	if !c.HasCq {
+		return false
+	}
 	if ci.IsClosed() {
 		return false
 	}
@@ -203,6 +234,12 @@
 	if ci.IsClosed() {
 		return ci.IsMerged()
 	}
+	if !c.HasCq {
+		// DryRunSuccess indicates that the CL has passed all of the
+		// checks required for submission; if there are no checks, then
+		// it has passed all of them by default.
+		return true
+	}
 	if len(c.DryRunSuccessLabels) > 0 && all(ci, c.DryRunSuccessLabels) {
 		return true
 	}
diff --git a/go/gerrit/config_test.go b/go/gerrit/config_test.go
index bfaff91..e562e2a 100644
--- a/go/gerrit/config_test.go
+++ b/go/gerrit/config_test.go
@@ -21,17 +21,32 @@
 	require.False(t, cfg.CqRunning(ci))
 	require.False(t, cfg.CqSuccess(ci))
 	require.False(t, cfg.DryRunRunning(ci))
-	// Have to use false here because ANGLE and Chromium configs do not use
-	// CQ success/failure labels, so we can't distinguish between "dry run
-	// finished" and "dry run never started".
-	require.False(t, cfg.DryRunSuccess(ci, false))
+	if cfg.HasCq {
+		// Have to use false here because ANGLE and Chromium configs do not use
+		// CQ success/failure labels, so we can't distinguish between "dry run
+		// finished" and "dry run never started".
+		require.False(t, cfg.DryRunSuccess(ci, false))
+	} else {
+		// DryRunSuccess is always true with no CQ.
+		require.True(t, cfg.DryRunSuccess(ci, false))
+	}
 
 	// CQ in progress.
 	SetLabels(ci, cfg.SetCqLabels)
-	require.True(t, cfg.CqRunning(ci))
-	require.False(t, cfg.CqSuccess(ci))
-	require.False(t, cfg.DryRunRunning(ci))
-	require.False(t, cfg.DryRunSuccess(ci, true))
+	if cfg.HasCq {
+		require.True(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.False(t, cfg.DryRunSuccess(ci, true))
+	} else {
+		// CQ and DryRun are never running with no CQ. CqSuccess is only
+		// true if the change is merged, and DryRunSuccess is always
+		// true.
+		require.False(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.True(t, cfg.DryRunSuccess(ci, true))
+	}
 
 	// CQ success.
 	if len(cfg.CqSuccessLabels) > 0 {
@@ -39,10 +54,20 @@
 	}
 	UnsetLabels(ci, cfg.CqActiveLabels)
 	ci.Status = CHANGE_STATUS_MERGED
-	require.False(t, cfg.CqRunning(ci))
-	require.True(t, cfg.CqSuccess(ci))
-	require.False(t, cfg.DryRunRunning(ci))
-	require.True(t, cfg.DryRunSuccess(ci, false))
+	if cfg.HasCq {
+		require.False(t, cfg.CqRunning(ci))
+		require.True(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.True(t, cfg.DryRunSuccess(ci, false))
+	} else {
+		// CQ and DryRun are never running with no CQ. CqSuccess is only
+		// true if the change is merged, and DryRunSuccess is always
+		// true.
+		require.False(t, cfg.CqRunning(ci))
+		require.True(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.True(t, cfg.DryRunSuccess(ci, true))
+	}
 
 	// CQ failed.
 	if len(cfg.CqSuccessLabels) > 0 {
@@ -52,10 +77,20 @@
 		SetLabels(ci, cfg.CqFailureLabels)
 	}
 	ci.Status = ""
-	require.False(t, cfg.CqRunning(ci))
-	require.False(t, cfg.CqSuccess(ci))
-	require.False(t, cfg.DryRunRunning(ci))
-	require.False(t, cfg.DryRunSuccess(ci, false))
+	if cfg.HasCq {
+		require.False(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.False(t, cfg.DryRunSuccess(ci, false))
+	} else {
+		// CQ and DryRun are never running with no CQ. CqSuccess is only
+		// true if the change is merged, and DryRunSuccess is always
+		// true.
+		require.False(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.True(t, cfg.DryRunSuccess(ci, true))
+	}
 
 	// Dry run in progress.
 	if len(cfg.CqFailureLabels) > 0 {
@@ -63,10 +98,20 @@
 	}
 	UnsetLabels(ci, cfg.SetCqLabels)
 	SetLabels(ci, cfg.SetDryRunLabels)
-	require.False(t, cfg.CqRunning(ci))
-	require.False(t, cfg.CqSuccess(ci))
-	require.True(t, cfg.DryRunRunning(ci))
-	require.False(t, cfg.DryRunSuccess(ci, true))
+	if cfg.HasCq {
+		require.False(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.True(t, cfg.DryRunRunning(ci))
+		require.False(t, cfg.DryRunSuccess(ci, true))
+	} else {
+		// CQ and DryRun are never running with no CQ. CqSuccess is only
+		// true if the change is merged, and DryRunSuccess is always
+		// true.
+		require.False(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.True(t, cfg.DryRunSuccess(ci, true))
+	}
 
 	// Dry run success.
 	if len(cfg.DryRunSuccessLabels) > 0 {
@@ -85,10 +130,20 @@
 	if len(cfg.DryRunFailureLabels) > 0 {
 		SetLabels(ci, cfg.DryRunFailureLabels)
 	}
-	require.False(t, cfg.CqRunning(ci))
-	require.False(t, cfg.CqSuccess(ci))
-	require.False(t, cfg.DryRunRunning(ci))
-	require.False(t, cfg.DryRunSuccess(ci, false))
+	if cfg.HasCq {
+		require.False(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.False(t, cfg.DryRunSuccess(ci, false))
+	} else {
+		// CQ and DryRun are never running with no CQ. CqSuccess is only
+		// true if the change is merged, and DryRunSuccess is always
+		// true.
+		require.False(t, cfg.CqRunning(ci))
+		require.False(t, cfg.CqSuccess(ci))
+		require.False(t, cfg.DryRunRunning(ci))
+		require.True(t, cfg.DryRunSuccess(ci, true))
+	}
 }
 
 func TestConfigAndroid(t *testing.T) {
@@ -102,3 +157,7 @@
 func TestConfigChromium(t *testing.T) {
 	testConfig(t, CONFIG_CHROMIUM)
 }
+
+func TestConfigChromiumNoCQ(t *testing.T) {
+	testConfig(t, CONFIG_CHROMIUM_NO_CQ)
+}
diff --git a/go/gerrit/gerrit.go b/go/gerrit/gerrit.go
index 9b501b1..5a28246 100644
--- a/go/gerrit/gerrit.go
+++ b/go/gerrit/gerrit.go
@@ -239,6 +239,7 @@
 	SetReadyForReview(context.Context, *ChangeInfo) error
 	SetReview(context.Context, *ChangeInfo, string, map[string]int, []string) error
 	SetTopic(context.Context, string, int64) error
+	Submit(context.Context, *ChangeInfo) error
 	TurnOnAuthenticatedGets()
 	Url(int64) string
 }
@@ -937,6 +938,11 @@
 	return false, nil
 }
 
+// Submit submits the Change.
+func (g *Gerrit) Submit(ctx context.Context, ci *ChangeInfo) error {
+	return g.post(ctx, fmt.Sprintf("/a/changes/%d/submit", ci.Issue), []byte("{}"))
+}
+
 // CodeReviewCache is an LRU cache for Gerrit Issues that polls in the background to determine if
 // issues have been updated. If so it expells them from the cache to force a reload.
 type CodeReviewCache struct {
diff --git a/go/gerrit/mocks/GerritInterface.go b/go/gerrit/mocks/GerritInterface.go
index 36a988f..c300a3b 100644
--- a/go/gerrit/mocks/GerritInterface.go
+++ b/go/gerrit/mocks/GerritInterface.go
@@ -542,6 +542,20 @@
 	return r0
 }
 
+// Submit provides a mock function with given fields: _a0, _a1
+func (_m *GerritInterface) Submit(_a0 context.Context, _a1 *gerrit.ChangeInfo) error {
+	ret := _m.Called(_a0, _a1)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *gerrit.ChangeInfo) error); ok {
+		r0 = rf(_a0, _a1)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
 // TurnOnAuthenticatedGets provides a mock function with given fields:
 func (_m *GerritInterface) TurnOnAuthenticatedGets() {
 	_m.Called()
diff --git a/go/gerrit/mocks/simple_mock_gerrit.go b/go/gerrit/mocks/simple_mock_gerrit.go
index 411eda9..f474572 100644
--- a/go/gerrit/mocks/simple_mock_gerrit.go
+++ b/go/gerrit/mocks/simple_mock_gerrit.go
@@ -126,5 +126,9 @@
 	return nil
 }
 
+func (g *SimpleGerritInterface) Submit(ctx context.Context, ci *gerrit.ChangeInfo) error {
+	return nil
+}
+
 // Make sure MockGerrit fulfills GerritInterface
 var _ gerrit.GerritInterface = (*SimpleGerritInterface)(nil)