[autoroll] Move Gerrit label application and interpretation to go/gerrit

- Add gerrit.Config struct, "constant" values for common projects.
- Use labels from gerrit.Config when setting CQ, dry run, self-approve, etc.
- Use gerrit.Config to derive CqFinished, CqSuccess, DryRunFinished, and DryRunSuccess.
- Drive-by: change labels from map[string]interface{} to map[string]int

Bug: skia:9529
Change-Id: I7d81938bbe13e9fc3d719e90200d9e0e7b04dd83
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/249984
Commit-Queue: Eric Boren <borenet@google.com>
Reviewed-by: Ravi Mistry <rmistry@google.com>
diff --git a/autoroll/go/autoroll-be/main.go b/autoroll/go/autoroll-be/main.go
index 65bdd14..64d12ed 100644
--- a/autoroll/go/autoroll-be/main.go
+++ b/autoroll/go/autoroll-be/main.go
@@ -210,7 +210,11 @@
 
 	if cfg.Gerrit != nil {
 		// Create the code review API client.
-		g, err = gerrit.NewGerrit(cfg.Gerrit.URL, gitcookiesPath, nil)
+		gc, err := cfg.Gerrit.GetConfig()
+		if err != nil {
+			sklog.Fatalf("Failed to get Gerrit config: %s", err)
+		}
+		g, err = gerrit.NewGerritWithConfig(gc, cfg.Gerrit.URL, gitcookiesPath, nil)
 		if err != nil {
 			sklog.Fatalf("Failed to create Gerrit client: %s", err)
 		}
diff --git a/autoroll/go/codereview/codereview.go b/autoroll/go/codereview/codereview.go
index 8052e52..5501fd8 100644
--- a/autoroll/go/codereview/codereview.go
+++ b/autoroll/go/codereview/codereview.go
@@ -44,7 +44,6 @@
 	fullHistoryUrl string
 	gerritClient   gerrit.GerritInterface
 	issueUrlBase   string
-	labels         map[bool]map[string]interface{}
 	userEmail      string
 	userName       string
 }
@@ -83,10 +82,7 @@
 
 // See documentation for CodeReview interface.
 func (c *gerritCodeReview) RetrieveRoll(ctx context.Context, issue *autoroll.AutoRollIssue, recent *recent_rolls.RecentRolls, rollingTo *revision.Revision, finishedCallback func(context.Context, RollImpl) error) (RollImpl, error) {
-	if c.cfg.Config == GERRIT_CONFIG_ANDROID {
-		return newGerritAndroidRoll(ctx, issue, c.gerritClient, recent, c.issueUrlBase, rollingTo, finishedCallback)
-	}
-	return newGerritRoll(ctx, issue, c.gerritClient, recent, c.issueUrlBase, rollingTo, finishedCallback)
+	return newGerritRoll(ctx, c.cfg, issue, c.gerritClient, recent, c.issueUrlBase, rollingTo, finishedCallback)
 }
 
 // See documentation for CodeReview interface.
diff --git a/autoroll/go/codereview/config.go b/autoroll/go/codereview/config.go
index 8e2fd82..daf30c2 100644
--- a/autoroll/go/codereview/config.go
+++ b/autoroll/go/codereview/config.go
@@ -3,9 +3,12 @@
 import (
 	"errors"
 	"fmt"
+	"sort"
+	"strings"
 
 	"go.skia.org/infra/go/gerrit"
 	"go.skia.org/infra/go/github"
+	"go.skia.org/infra/go/skerr"
 	"go.skia.org/infra/go/util"
 )
 
@@ -23,47 +26,11 @@
 )
 
 var (
-	// GERRIT_LABELS indicates which labels should be set on Gerrit CLs in
-	// normal and dry run modes, for each Gerrit configuration.
-	GERRIT_LABELS = map[string]map[bool]map[string]interface{}{
-		GERRIT_CONFIG_ANDROID: {
-			// Normal mode.
-			false: {
-				gerrit.CODEREVIEW_LABEL:      "2",
-				gerrit.PRESUBMIT_READY_LABEL: "1",
-				gerrit.AUTOSUBMIT_LABEL:      gerrit.AUTOSUBMIT_LABEL_SUBMIT,
-			},
-			// Dry run mode.
-			true: {
-				gerrit.CODEREVIEW_LABEL:      "2",
-				gerrit.PRESUBMIT_READY_LABEL: "1",
-				gerrit.AUTOSUBMIT_LABEL:      gerrit.AUTOSUBMIT_LABEL_NONE,
-			},
-		},
-		GERRIT_CONFIG_ANGLE: {
-			// Normal mode.
-			false: {
-				gerrit.CODEREVIEW_LABEL:  gerrit.CODEREVIEW_LABEL_SELF_APPROVE,
-				gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_SUBMIT,
-			},
-			// Dry run mode.
-			true: {
-				gerrit.CODEREVIEW_LABEL:  gerrit.CODEREVIEW_LABEL_SELF_APPROVE,
-				gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_DRY_RUN,
-			},
-		},
-		GERRIT_CONFIG_CHROMIUM: {
-			// Normal mode.
-			false: {
-				gerrit.CODEREVIEW_LABEL:  gerrit.CODEREVIEW_LABEL_APPROVE,
-				gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_SUBMIT,
-			},
-			// Dry run mode.
-			true: {
-				gerrit.CODEREVIEW_LABEL:  gerrit.CODEREVIEW_LABEL_APPROVE,
-				gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_DRY_RUN,
-			},
-		},
+	// 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,
 	}
 )
 
@@ -98,8 +65,13 @@
 	if c.Project == "" {
 		return errors.New("Project is required.")
 	}
-	if c.Config != GERRIT_CONFIG_ANDROID && c.Config != GERRIT_CONFIG_ANGLE && c.Config != GERRIT_CONFIG_CHROMIUM {
-		return fmt.Errorf("Config must be one of: [%s, %s, %s]", GERRIT_CONFIG_ANDROID, GERRIT_CONFIG_ANGLE, GERRIT_CONFIG_CHROMIUM)
+	if _, ok := GERRIT_CONFIGS[c.Config]; !ok {
+		validConfigs := make([]string, 0, len(GERRIT_CONFIGS))
+		for name := range GERRIT_CONFIGS {
+			validConfigs = append(validConfigs, name)
+		}
+		sort.Strings(validConfigs)
+		return fmt.Errorf("Config must be one of: [%s]", strings.Join(validConfigs, ", "))
 	}
 	return nil
 }
@@ -109,9 +81,18 @@
 	return newGerritCodeReview(c, gerritClient)
 }
 
-// GetLabels returns the labels needed for a given CL.
-func (c *GerritConfig) GetLabels(dryRun bool) map[string]interface{} {
-	return GERRIT_LABELS[c.Config][dryRun]
+// GetConfig returns the gerrit.Config referenced by the GerritConfig.
+func (c *GerritConfig) GetConfig() (*gerrit.Config, error) {
+	cfg, ok := GERRIT_CONFIGS[c.Config]
+	if !ok {
+		return nil, skerr.Fmt("Unknown Gerrit config %q", c.Config)
+	}
+	return cfg, nil
+}
+
+// CanQueryTrybots returns true if we can query for trybot results.
+func (c *GerritConfig) CanQueryTrybots() bool {
+	return c.Config != GERRIT_CONFIG_ANDROID
 }
 
 // GithubConfig provides configuration for Github.
diff --git a/autoroll/go/codereview/roll.go b/autoroll/go/codereview/roll.go
index eb817bb..2e64e99 100644
--- a/autoroll/go/codereview/roll.go
+++ b/autoroll/go/codereview/roll.go
@@ -28,12 +28,14 @@
 	InsertIntoDB(ctx context.Context) error
 }
 
-func updateGerritIssue(ctx context.Context, a *autoroll.AutoRollIssue, g gerrit.GerritInterface, rollIntoAndroid bool) (*gerrit.ChangeInfo, error) {
+// updateIssueFromGerrit loads details about the issue from the Gerrit API and
+// updates the AutoRollIssue accordingly.
+func updateIssueFromGerrit(ctx context.Context, cfg *GerritConfig, a *autoroll.AutoRollIssue, g gerrit.GerritInterface) (*gerrit.ChangeInfo, error) {
 	info, err := g.GetIssueProperties(ctx, a.Issue)
 	if err != nil {
 		return nil, fmt.Errorf("Failed to get issue properties: %s", err)
 	}
-	if !rollIntoAndroid {
+	if cfg.CanQueryTrybots() {
 		// Use try results from the most recent non-trivial patchset.
 		if len(info.Patchsets) == 0 {
 			return nil, fmt.Errorf("Issue %d has no patchsets!", a.Issue)
@@ -56,12 +58,42 @@
 		}
 		a.TryResults = tryResults
 	}
-	if err := a.UpdateFromGerritChangeInfo(info, rollIntoAndroid); err != nil {
+	if err := updateIssueFromGerritChangeInfo(a, info, g.Config()); err != nil {
 		return nil, fmt.Errorf("Failed to convert issue format: %s", err)
 	}
 	return info, nil
 }
 
+// updateIssueFromGerritChangeInfo updates the AutoRollIssue instance based on
+// the given gerrit.ChangeInfo.
+func updateIssueFromGerritChangeInfo(i *autoroll.AutoRollIssue, ci *gerrit.ChangeInfo, gc *gerrit.Config) error {
+	if i.Issue != ci.Issue {
+		return fmt.Errorf("CL ID %d differs from existing issue number %d!", ci.Issue, i.Issue)
+	}
+	i.CqFinished = !i.IsDryRun && !gc.CqRunning(ci)
+	i.CqSuccess = !i.IsDryRun && gc.CqSuccess(ci)
+	i.DryRunFinished = i.IsDryRun && !gc.DryRunRunning(ci)
+	i.DryRunSuccess = i.IsDryRun && gc.DryRunSuccess(ci, gc.DryRunUsesTryjobResults && i.AllTrybotsSucceeded())
+
+	ps := make([]int64, 0, len(ci.Patchsets))
+	for _, p := range ci.Patchsets {
+		ps = append(ps, p.Number)
+	}
+	i.Closed = ci.IsClosed()
+	i.Committed = ci.Committed
+	i.Created = ci.Created
+	i.Modified = ci.Updated
+	i.Patchsets = ps
+	i.Subject = ci.Subject
+	i.Result = autoroll.RollResult(i)
+	// TODO(borenet): If this validation fails, it's likely that it will
+	// continue to fail indefinitely, resulting in a stuck roller.
+	// Additionally, this AutoRollIssue instance persists in the AutoRoller
+	// for its entire lifetime; it's possible to partially fail to update
+	// it and end up in an inconsistent state.
+	return i.Validate()
+}
+
 // gerritRoll is an implementation of RollImpl.
 type gerritRoll struct {
 	ci               *gerrit.ChangeInfo
@@ -77,8 +109,8 @@
 
 // newGerritRoll obtains a gerritRoll instance from the given Gerrit issue
 // number.
-func newGerritRoll(ctx context.Context, issue *autoroll.AutoRollIssue, g gerrit.GerritInterface, recent *recent_rolls.RecentRolls, issueUrlBase string, rollingTo *revision.Revision, cb func(context.Context, RollImpl) error) (RollImpl, error) {
-	ci, err := updateGerritIssue(ctx, issue, g, false)
+func newGerritRoll(ctx context.Context, cfg *GerritConfig, issue *autoroll.AutoRollIssue, g gerrit.GerritInterface, recent *recent_rolls.RecentRolls, issueUrlBase string, rollingTo *revision.Revision, cb func(context.Context, RollImpl) error) (RollImpl, error) {
+	ci, err := updateIssueFromGerrit(ctx, cfg, issue, g)
 	if err != nil {
 		return nil, err
 	}
@@ -90,7 +122,7 @@
 		g:                g,
 		recent:           recent,
 		retrieveRoll: func(ctx context.Context) (*gerrit.ChangeInfo, error) {
-			return updateGerritIssue(ctx, issue, g, false)
+			return updateIssueFromGerrit(ctx, cfg, issue, g)
 		},
 		rollingTo: rollingTo,
 	}, nil
@@ -239,86 +271,6 @@
 	return r.issueUrl
 }
 
-// Special type for Android rolls.
-type gerritAndroidRoll struct {
-	*gerritRoll
-}
-
-// newGerritAndroidRoll obtains a gerritAndroidRoll instance from the given Gerrit issue number.
-func newGerritAndroidRoll(ctx context.Context, issue *autoroll.AutoRollIssue, g gerrit.GerritInterface, recent *recent_rolls.RecentRolls, issueUrlBase string, rollingTo *revision.Revision, cb func(context.Context, RollImpl) error) (RollImpl, error) {
-	ci, err := updateGerritIssue(ctx, issue, g, true)
-	if err != nil {
-		return nil, err
-	}
-	return &gerritAndroidRoll{&gerritRoll{
-		ci:               ci,
-		issue:            issue,
-		issueUrl:         fmt.Sprintf("%s%d", issueUrlBase, issue.Issue),
-		finishedCallback: cb,
-		g:                g,
-		recent:           recent,
-		retrieveRoll: func(ctx context.Context) (*gerrit.ChangeInfo, error) {
-			return updateGerritIssue(ctx, issue, g, true)
-		},
-		rollingTo: rollingTo,
-	}}, nil
-}
-
-// See documentation for state_machine.RollCLImpl interface.
-func (r *gerritAndroidRoll) IsDryRunFinished() bool {
-	return r.issue.DryRunFinished
-}
-
-// See documentation for state_machine.RollCLImpl interface.
-func (r *gerritAndroidRoll) IsDryRunSuccess() bool {
-	return r.issue.DryRunSuccess
-}
-
-// See documentation for state_machine.RollCLImpl interface.
-func (r *gerritAndroidRoll) SwitchToDryRun(ctx context.Context) error {
-	return r.withModify(ctx, "switch the CL to dry run", func() error {
-		if err := r.g.SetReview(ctx, r.ci, "Mode was changed to dry run", map[string]interface{}{gerrit.AUTOSUBMIT_LABEL: gerrit.AUTOSUBMIT_LABEL_NONE}, nil); err != nil {
-			return err
-		}
-		r.issue.IsDryRun = true
-		return nil
-	})
-}
-
-// See documentation for state_machine.RollCLImpl interface.
-func (r *gerritAndroidRoll) SwitchToNormal(ctx context.Context) error {
-	return r.withModify(ctx, "switch the CL out of dry run", func() error {
-		if err := r.g.SetReview(ctx, r.ci, "Mode was changed to normal", map[string]interface{}{gerrit.AUTOSUBMIT_LABEL: gerrit.AUTOSUBMIT_LABEL_SUBMIT}, nil); err != nil {
-			return err
-		}
-		r.issue.IsDryRun = false
-		return nil
-	})
-}
-
-// See documentation for state_machine.RollCLImpl interface.
-func (r *gerritAndroidRoll) RetryCQ(ctx context.Context) error {
-	return r.withModify(ctx, "retry TH", func() error {
-		if err := r.g.SetReview(ctx, r.ci, "TH failed but there are no new commits. Retrying...", map[string]interface{}{gerrit.PRESUBMIT_READY_LABEL: "1", gerrit.AUTOSUBMIT_LABEL: gerrit.AUTOSUBMIT_LABEL_SUBMIT}, nil); err != nil {
-			return err
-		}
-		r.issue.IsDryRun = false
-		return nil
-
-	})
-}
-
-// See documentation for state_machine.RollCLImpl interface.
-func (r *gerritAndroidRoll) RetryDryRun(ctx context.Context) error {
-	return r.withModify(ctx, "retry the TH (dry run)", func() error {
-		if err := r.g.SetReview(ctx, r.ci, "Dry run failed but there are no new commits. Retrying...", map[string]interface{}{gerrit.PRESUBMIT_READY_LABEL: "1"}, nil); err != nil {
-			return err
-		}
-		r.issue.IsDryRun = true
-		return nil
-	})
-}
-
 // githubRoll is an implementation of RollImpl.
 // TODO(rmistry): Add tests after a code-review abstraction later exists.
 type githubRoll struct {
@@ -337,7 +289,9 @@
 	t                *travisci.TravisCI
 }
 
-func updateGithubPullRequest(ctx context.Context, a *autoroll.AutoRollIssue, g *github.GitHub, checksNum int, checksWaitFor []string, mergeMethodURL string) (*github_api.PullRequest, error) {
+// updateIssueFromGitHub loads details about the pull request from the GitHub
+// API and updates the AutoRollIssue accordingly.
+func updateIssueFromGitHub(ctx context.Context, a *autoroll.AutoRollIssue, g *github.GitHub, checksNum int, checksWaitFor []string, mergeMethodURL string) (*github_api.PullRequest, error) {
 
 	// Retrieve the pull request from github.
 	pullRequest, err := g.GetPullRequest(int(a.Issue))
@@ -403,7 +357,7 @@
 	}
 	a.TryResults = tryResults
 
-	if err := a.UpdateFromGitHubPullRequest(pullRequest); err != nil {
+	if err := updateIssueFromGitHubPullRequest(a, pullRequest); err != nil {
 		return nil, fmt.Errorf("Failed to convert issue format: %s", err)
 	}
 
@@ -471,7 +425,7 @@
 
 	if pullRequestModified {
 		// Update Autoroll issue to show the current state of the PR.
-		if err := a.UpdateFromGitHubPullRequest(pullRequest); err != nil {
+		if err := updateIssueFromGitHubPullRequest(a, pullRequest); err != nil {
 			return nil, fmt.Errorf("Failed to convert issue format: %s", err)
 		}
 	}
@@ -479,6 +433,42 @@
 	return pullRequest, nil
 }
 
+// updateIssueFromGitHubPullRequest updates the AutoRollIssue instance based on the
+// given PullRequest.
+func updateIssueFromGitHubPullRequest(i *autoroll.AutoRollIssue, pullRequest *github_api.PullRequest) error {
+	prNum := int64(pullRequest.GetNumber())
+	if i.Issue != prNum {
+		return fmt.Errorf("Pull request number %d differs from existing issue number %d!", prNum, i.Issue)
+	}
+	i.CqFinished = pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged()
+	i.CqSuccess = pullRequest.GetMerged()
+	if i.IsDryRun {
+		i.DryRunFinished = i.AllTrybotsFinished() || pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged()
+		i.DryRunSuccess = (i.DryRunFinished && i.AllTrybotsSucceeded()) || pullRequest.GetMerged()
+	} else {
+		i.DryRunFinished = false
+		i.DryRunSuccess = false
+	}
+
+	ps := make([]int64, 0, *pullRequest.Commits)
+	for i := 1; i <= *pullRequest.Commits; i++ {
+		ps = append(ps, int64(i))
+	}
+	i.Closed = pullRequest.GetState() == github.CLOSED_STATE
+	i.Committed = pullRequest.GetMerged()
+	i.Created = pullRequest.GetCreatedAt()
+	i.Modified = pullRequest.GetUpdatedAt()
+	i.Patchsets = ps
+	i.Subject = pullRequest.GetTitle()
+	i.Result = autoroll.RollResult(i)
+	// TODO(borenet): If this validation fails, it's likely that it will
+	// continue to fail indefinitely, resulting in a stuck roller.
+	// Additionally, this AutoRollIssue instance persists in the AutoRoller
+	// for its entire lifetime; it's possible to partially fail to update
+	// it and end up in an inconsistent state.
+	return i.Validate()
+}
+
 func shouldStateBeMerged(mergeableState string) bool {
 	// Allow "clean" and "unstable" mergeable state.
 	// "unstable" is allowed (for now) because of a bug in Github where a race condition makes
@@ -490,7 +480,7 @@
 
 // newGithubRoll obtains a githubRoll instance from the given Gerrit issue number.
 func newGithubRoll(ctx context.Context, issue *autoroll.AutoRollIssue, g *github.GitHub, recent *recent_rolls.RecentRolls, issueUrlBase string, config *GithubConfig, rollingTo *revision.Revision, cb func(context.Context, RollImpl) error) (RollImpl, error) {
-	pullRequest, err := updateGithubPullRequest(ctx, issue, g, config.ChecksNum, config.ChecksWaitFor, config.MergeMethodURL)
+	pullRequest, err := updateIssueFromGitHub(ctx, issue, g, config.ChecksNum, config.ChecksWaitFor, config.MergeMethodURL)
 	if err != nil {
 		return nil, err
 	}
@@ -504,7 +494,7 @@
 		pullRequest:      pullRequest,
 		recent:           recent,
 		retrieveRoll: func(ctx context.Context) (*github_api.PullRequest, error) {
-			return updateGithubPullRequest(ctx, issue, g, config.ChecksNum, config.ChecksWaitFor, config.MergeMethodURL)
+			return updateIssueFromGitHub(ctx, issue, g, config.ChecksNum, config.ChecksWaitFor, config.MergeMethodURL)
 		},
 		rollingTo: rollingTo,
 	}, nil
diff --git a/autoroll/go/codereview/roll_test.go b/autoroll/go/codereview/roll_test.go
index 66a9598..db67843 100644
--- a/autoroll/go/codereview/roll_test.go
+++ b/autoroll/go/codereview/roll_test.go
@@ -9,21 +9,24 @@
 	"time"
 
 	"github.com/golang/protobuf/ptypes"
+	github_api "github.com/google/go-github/github"
 	"github.com/stretchr/testify/require"
 	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
 	"go.skia.org/infra/autoroll/go/recent_rolls"
 	"go.skia.org/infra/autoroll/go/revision"
 	"go.skia.org/infra/go/autoroll"
+	"go.skia.org/infra/go/deepequal"
 	"go.skia.org/infra/go/ds"
 	"go.skia.org/infra/go/ds/testutil"
 	"go.skia.org/infra/go/gerrit"
 	gerrit_testutils "go.skia.org/infra/go/gerrit/testutils"
+	"go.skia.org/infra/go/github"
 	"go.skia.org/infra/go/mockhttpclient"
 	"go.skia.org/infra/go/testutils"
 	"go.skia.org/infra/go/testutils/unittest"
 )
 
-func makeFakeRoll(issueNum int64, from, to string, dryRun, android bool) (*gerrit.ChangeInfo, *autoroll.AutoRollIssue) {
+func makeFakeRoll(t *testing.T, cfg *GerritConfig, issueNum int64, from, to string, dryRun bool) (*gerrit.ChangeInfo, *autoroll.AutoRollIssue) {
 	// Gerrit API only has millisecond precision.
 	now := time.Now().UTC().Round(time.Millisecond)
 	description := fmt.Sprintf(`Roll src/third_party/skia/ %s..%s (42 commits).
@@ -37,23 +40,13 @@
 		CreatedString: now.Format(gerrit.TIME_FORMAT),
 		Created:       now,
 	}
-	cqLabel := gerrit.COMMITQUEUE_LABEL_SUBMIT
-	if android {
-		cqLabel = gerrit.AUTOSUBMIT_LABEL_SUBMIT
-	}
-	if dryRun {
-		if android {
-			cqLabel = gerrit.AUTOSUBMIT_LABEL_NONE
-		} else {
-			cqLabel = gerrit.COMMITQUEUE_LABEL_DRY_RUN
-		}
-	}
 	roll := &gerrit.ChangeInfo{
 		Created:       now,
 		CreatedString: now.Format(gerrit.TIME_FORMAT),
 		Subject:       description,
 		ChangeId:      fmt.Sprintf("%d", issueNum),
 		Issue:         issueNum,
+		Labels:        map[string]*gerrit.LabelEntry{},
 		Owner: &gerrit.Owner{
 			Email: "fake-deps-roller@chromium.org",
 		},
@@ -65,35 +58,20 @@
 		Updated:       now,
 		UpdatedString: now.Format(gerrit.TIME_FORMAT),
 	}
-	if android {
-		roll.Labels = map[string]*gerrit.LabelEntry{
-			gerrit.PRESUBMIT_VERIFIED_LABEL: {
-				All: []*gerrit.LabelDetail{},
-			},
-			gerrit.AUTOSUBMIT_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: cqLabel,
-					},
-				},
-			},
+	gc, err := cfg.GetConfig()
+	require.NoError(t, err)
+	cqLabels := gc.SetCqLabels
+	if dryRun {
+		cqLabels = gc.SetDryRunLabels
+	}
+	for k, v := range cqLabels {
+		roll.Labels[k] = &gerrit.LabelEntry{
+			All: []*gerrit.LabelDetail{{Value: v}},
 		}
-	} else {
-		roll.Labels = map[string]*gerrit.LabelEntry{
-			gerrit.CODEREVIEW_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: gerrit.CODEREVIEW_LABEL_APPROVE,
-					},
-				},
-			},
-			gerrit.COMMITQUEUE_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: cqLabel,
-					},
-				},
-			},
+	}
+	for k, v := range gc.SelfApproveLabels {
+		roll.Labels[k] = &gerrit.LabelEntry{
+			All: []*gerrit.LabelDetail{{Value: v}},
 		}
 	}
 	return roll, &autoroll.AutoRollIssue{
@@ -104,7 +82,7 @@
 	}
 }
 
-func TestGerritRoll(t *testing.T) {
+func testGerritRoll(t *testing.T, cfg *GerritConfig) {
 	unittest.LargeTest(t)
 
 	tmp, err := ioutil.TempDir("", "")
@@ -113,7 +91,9 @@
 
 	testutil.InitDatastore(t, ds.KIND_AUTOROLL_ROLL)
 
-	g := gerrit_testutils.NewGerrit(t, tmp, false)
+	gc, err := cfg.GetConfig()
+	require.NoError(t, err)
+	g := gerrit_testutils.NewGerritWithConfig(t, gc, tmp)
 	ctx := context.Background()
 	recent, err := recent_rolls.NewRecentRolls(ctx, "test-roller")
 	require.NoError(t, err)
@@ -125,10 +105,12 @@
 		Id:          to,
 		Description: "rolling to fghi",
 	}
-	ci, issue := makeFakeRoll(123, from, to, false, false)
+	ci, issue := makeFakeRoll(t, cfg, 123, from, to, false)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
-	gr, err := newGerritRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
+	gr, err := newGerritRoll(ctx, cfg, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
 	require.NoError(t, err)
 	require.False(t, issue.IsDryRun)
 	require.False(t, gr.IsFinished())
@@ -159,16 +141,12 @@
 	require.False(t, gr.IsDryRunSuccess())
 
 	// Set dry run.
-	g.MockSetDryRun(ci, "Mode was changed to dry run")
-	ci.Labels[gerrit.COMMITQUEUE_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.COMMITQUEUE_LABEL_DRY_RUN,
-			},
-		},
-	}
+	g.MockPost(ci, "Mode was changed to dry run", gc.SetDryRunLabels)
+	gerrit.SetLabels(ci, gc.SetDryRunLabels)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
 	require.NoError(t, gr.SwitchToDryRun(ctx))
 	g.AssertEmpty()
 	require.True(t, issue.IsDryRun)
@@ -178,16 +156,12 @@
 	require.False(t, gr.IsDryRunSuccess())
 
 	// Set normal.
-	g.MockSetCQ(ci, "Mode was changed to normal")
-	ci.Labels[gerrit.COMMITQUEUE_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.COMMITQUEUE_LABEL_SUBMIT,
-			},
-		},
-	}
+	g.MockPost(ci, "Mode was changed to normal", gc.SetCqLabels)
+	gerrit.SetLabels(ci, gc.SetCqLabels)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
 	require.NoError(t, gr.SwitchToNormal(ctx))
 	g.AssertEmpty()
 	require.False(t, issue.IsDryRun)
@@ -207,7 +181,9 @@
 	ci.Revisions[fmt.Sprintf("%d", rev.Number)] = rev
 	ci.Patchsets = append(ci.Patchsets, rev)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
 	require.NoError(t, gr.Update(ctx))
 	require.False(t, issue.IsDryRun)
 	require.True(t, gr.IsFinished())
@@ -217,32 +193,35 @@
 	require.Nil(t, recent.CurrentRoll())
 
 	// Upload and retrieve another roll, dry run this time.
-	ts, err := ptypes.TimestampProto(time.Now().UTC().Round(time.Millisecond))
-	require.NoError(t, err)
-	ci, issue = makeFakeRoll(124, from, to, true, false)
+	ci, issue = makeFakeRoll(t, cfg, 124, from, to, true)
 	g.MockGetIssueProperties(ci)
-	tryjob := &buildbucketpb.Build{
-		Builder: &buildbucketpb.BuilderID{
-			Project: "skia",
-			Bucket:  "fake",
-			Builder: "fake-builder",
-		},
-		Id:         99999,
-		CreateTime: ts,
-		Status:     buildbucketpb.Status_STARTED,
-		Tags: []*buildbucketpb.StringPair{
-			{
-				Key:   "user_agent",
-				Value: "cq",
+	var tryjob *buildbucketpb.Build
+	if cfg.CanQueryTrybots() {
+		ts, err := ptypes.TimestampProto(time.Now().UTC().Round(time.Millisecond))
+		require.NoError(t, err)
+		tryjob = &buildbucketpb.Build{
+			Builder: &buildbucketpb.BuilderID{
+				Project: "skia",
+				Bucket:  "fake",
+				Builder: "fake-builder",
 			},
-			{
-				Key:   "cq_experimental",
-				Value: "false",
+			Id:         99999,
+			CreateTime: ts,
+			Status:     buildbucketpb.Status_STARTED,
+			Tags: []*buildbucketpb.StringPair{
+				{
+					Key:   "user_agent",
+					Value: "cq",
+				},
+				{
+					Key:   "cq_experimental",
+					Value: "false",
+				},
 			},
-		},
+		}
+		g.MockGetTrybotResults(ci, 1, []*buildbucketpb.Build{tryjob})
 	}
-	g.MockGetTrybotResults(ci, 1, []*buildbucketpb.Build{tryjob})
-	gr, err = newGerritRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
+	gr, err = newGerritRoll(ctx, cfg, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
 	require.NoError(t, err)
 	require.True(t, issue.IsDryRun)
 	require.False(t, gr.IsFinished())
@@ -262,16 +241,15 @@
 	g.AssertEmpty()
 
 	// Success.
-	tryjob.Status = buildbucketpb.Status_SUCCESS
-	ci.Labels[gerrit.COMMITQUEUE_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.COMMITQUEUE_LABEL_NONE,
-			},
-		},
+	if len(gc.DryRunSuccessLabels) > 0 {
+		gerrit.SetLabels(ci, gc.DryRunSuccessLabels)
 	}
+	gerrit.UnsetLabels(ci, gc.DryRunActiveLabels)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, []*buildbucketpb.Build{tryjob})
+	if cfg.CanQueryTrybots() {
+		tryjob.Status = buildbucketpb.Status_SUCCESS
+		g.MockGetTrybotResults(ci, 1, []*buildbucketpb.Build{tryjob})
+	}
 	require.NoError(t, gr.Update(ctx))
 	require.True(t, issue.IsDryRun)
 	require.False(t, gr.IsFinished())
@@ -283,51 +261,59 @@
 	// Close for cleanup.
 	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, []*buildbucketpb.Build{tryjob})
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, []*buildbucketpb.Build{tryjob})
+	}
 	require.NoError(t, gr.Update(ctx))
 
 	// Verify that all of the mutation functions handle a conflict (eg.
 	// someone closed the CL) gracefully.
 
 	// 1. SwitchToDryRun.
-	ci, issue = makeFakeRoll(125, from, to, false, false)
+	ci, issue = makeFakeRoll(t, cfg, 125, from, to, false)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
-	gr, err = newGerritRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
+	gr, err = newGerritRoll(ctx, cfg, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
 	require.NoError(t, err)
 	require.NoError(t, gr.InsertIntoDB(ctx))
-	url, reqBytes := g.MakePostRequest(ci, "Mode was changed to dry run", map[string]int{
-		gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_DRY_RUN,
-	})
+	url, reqBytes := g.MakePostRequest(ci, "Mode was changed to dry run", gc.SetDryRunLabels)
 	g.Mock.MockOnce(url, mockhttpclient.MockPostError("application/json", reqBytes, "CONFLICT", http.StatusConflict))
 	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
 	require.NoError(t, gr.SwitchToDryRun(ctx))
 	g.AssertEmpty()
 
 	// 2. SwitchToNormal
-	ci, issue = makeFakeRoll(126, from, to, false, false)
+	ci, issue = makeFakeRoll(t, cfg, 126, from, to, false)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
-	gr, err = newGerritRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
+	gr, err = newGerritRoll(ctx, cfg, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
 	require.NoError(t, err)
 	require.NoError(t, gr.InsertIntoDB(ctx))
-	url, reqBytes = g.MakePostRequest(ci, "Mode was changed to normal", map[string]int{
-		gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_SUBMIT,
-	})
+	url, reqBytes = g.MakePostRequest(ci, "Mode was changed to normal", gc.SetCqLabels)
 	g.Mock.MockOnce(url, mockhttpclient.MockPostError("application/json", reqBytes, "CONFLICT", http.StatusConflict))
 	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
 	require.NoError(t, gr.SwitchToNormal(ctx))
 	g.AssertEmpty()
 
 	// 3. Close.
-	ci, issue = makeFakeRoll(127, from, to, false, false)
+	ci, issue = makeFakeRoll(t, cfg, 127, from, to, false)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
-	gr, err = newGerritRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
+	gr, err = newGerritRoll(ctx, cfg, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
 	require.NoError(t, err)
 	require.NoError(t, gr.InsertIntoDB(ctx))
 	url = fmt.Sprintf("%s/a/changes/%d/abandon", gerrit_testutils.FAKE_GERRIT_URL, ci.Issue)
@@ -339,15 +325,19 @@
 	g.Mock.MockOnce(url, mockhttpclient.MockPostError("application/json", []byte(req), "CONFLICT", http.StatusConflict))
 	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
 	require.NoError(t, gr.Close(ctx, autoroll.ROLL_RESULT_FAILURE, "close it!"))
 	g.AssertEmpty()
 
 	// Verify that we set the correct status when abandoning a CL.
-	ci, issue = makeFakeRoll(128, from, to, false, false)
+	ci, issue = makeFakeRoll(t, cfg, 128, from, to, false)
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
-	gr, err = newGerritRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
+	gr, err = newGerritRoll(ctx, cfg, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
 	require.NoError(t, err)
 	require.NoError(t, gr.InsertIntoDB(ctx))
 	url = fmt.Sprintf("%s/a/changes/%d/abandon", gerrit_testutils.FAKE_GERRIT_URL, ci.Issue)
@@ -359,7 +349,9 @@
 	g.Mock.MockOnce(url, mockhttpclient.MockPostDialogue("application/json", []byte(req), nil))
 	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
 	g.MockGetIssueProperties(ci)
-	g.MockGetTrybotResults(ci, 1, nil)
+	if cfg.CanQueryTrybots() {
+		g.MockGetTrybotResults(ci, 1, nil)
+	}
 	require.NoError(t, gr.Close(ctx, autoroll.ROLL_RESULT_DRY_RUN_SUCCESS, "close it!"))
 	g.AssertEmpty()
 	issue, err = recent.Get(ctx, 128)
@@ -367,206 +359,325 @@
 	require.Equal(t, issue.Result, autoroll.ROLL_RESULT_DRY_RUN_SUCCESS)
 }
 
+func TestGerritRoll(t *testing.T) {
+	testGerritRoll(t, &GerritConfig{
+		URL:     "???",
+		Project: "???",
+		Config:  GERRIT_CONFIG_CHROMIUM,
+	})
+}
+
 func TestGerritAndroidRoll(t *testing.T) {
-	unittest.LargeTest(t)
+	testGerritRoll(t, &GerritConfig{
+		URL:     "???",
+		Project: "???",
+		Config:  GERRIT_CONFIG_ANDROID,
+	})
+}
 
-	tmp, err := ioutil.TempDir("", "")
-	require.NoError(t, err)
-	defer testutils.RemoveAll(t, tmp)
+func testUpdateFromGerritChangeInfo(t *testing.T, cfg *gerrit.Config) {
+	unittest.SmallTest(t)
 
-	testutil.InitDatastore(t, ds.KIND_AUTOROLL_ROLL)
+	now := time.Now()
 
-	g := gerrit_testutils.NewGerrit(t, tmp, true)
-
-	ctx := context.Background()
-	recent, err := recent_rolls.NewRecentRolls(ctx, "test-roller")
-	require.NoError(t, err)
-
-	// Upload and retrieve the roll.
-	from := "abcde12345abcde12345abcde12345abcde12345"
-	to := "fghij67890fghij67890fghij67890fghij67890"
-	toRev := &revision.Revision{
-		Id:          to,
-		Description: "rolling to fghi",
+	a := &autoroll.AutoRollIssue{
+		Issue:       123,
+		RollingFrom: "abc123",
+		RollingTo:   "def456",
 	}
-	ci, issue := makeFakeRoll(123, from, to, false, true)
-	g.MockGetIssueProperties(ci)
-	gr, err := newGerritAndroidRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
-	require.NoError(t, err)
-	require.False(t, issue.IsDryRun)
-	require.False(t, gr.IsFinished())
-	require.False(t, gr.IsSuccess())
-	require.False(t, gr.IsDryRunFinished())
-	require.False(t, gr.IsDryRunSuccess())
-	g.AssertEmpty()
-	require.Equal(t, toRev, gr.RollingTo())
 
-	// Insert into DB.
-	current := recent.CurrentRoll()
-	require.Nil(t, current)
-	require.NoError(t, gr.InsertIntoDB(ctx))
-	current = recent.CurrentRoll()
-	require.NotNil(t, current)
-	require.Equal(t, current.Issue, ci.Issue)
-	g.AssertEmpty()
+	// Ensure that we don't overwrite the issue number.
+	require.EqualError(t, updateIssueFromGerritChangeInfo(a, &gerrit.ChangeInfo{}, cfg), "CL ID 0 differs from existing issue number 123!")
 
-	// Add a comment.
-	msg := "Here's a comment"
-	g.MockAddComment(ci, msg)
-	require.NoError(t, gr.AddComment(ctx, msg))
-	g.AssertEmpty()
-	require.False(t, issue.IsDryRun)
-	require.False(t, gr.IsFinished())
-	require.False(t, gr.IsSuccess())
-	require.False(t, gr.IsDryRunFinished())
-	require.False(t, gr.IsDryRunSuccess())
+	// Normal, in-progress CL.
+	rev := &gerrit.Revision{
+		ID:            "1",
+		Number:        1,
+		Created:       now,
+		CreatedString: now.Format(gerrit.TIME_FORMAT),
+	}
+	ci := &gerrit.ChangeInfo{
+		Created:       now,
+		CreatedString: now.Format(gerrit.TIME_FORMAT),
+		Subject:       "roll the deps",
+		ChangeId:      fmt.Sprintf("%d", a.Issue),
+		Issue:         a.Issue,
+		Labels:        map[string]*gerrit.LabelEntry{},
+		Owner: &gerrit.Owner{
+			Email: "fake@chromium.org",
+		},
+		Project: "skia",
+		Revisions: map[string]*gerrit.Revision{
+			rev.ID: rev,
+		},
+		Patchsets:     []*gerrit.Revision{rev},
+		Status:        gerrit.CHANGE_STATUS_NEW,
+		Updated:       now,
+		UpdatedString: now.Format(gerrit.TIME_FORMAT),
+	}
+	gerrit.SetLabels(ci, cfg.SelfApproveLabels)
+	gerrit.SetLabels(ci, cfg.SetCqLabels)
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	expect := &autoroll.AutoRollIssue{
+		Created:     now,
+		Issue:       123,
+		Modified:    now,
+		Patchsets:   []int64{1},
+		Result:      autoroll.ROLL_RESULT_IN_PROGRESS,
+		RollingFrom: "abc123",
+		RollingTo:   "def456",
+		Subject:     "roll the deps",
+	}
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// Set dry run.
-	g.MockSetDryRun(ci, "Mode was changed to dry run")
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.SwitchToDryRun(ctx))
-	g.AssertEmpty()
-	require.True(t, issue.IsDryRun)
-	require.False(t, gr.IsFinished())
-	require.False(t, gr.IsSuccess())
-	require.False(t, gr.IsDryRunFinished())
-	require.False(t, gr.IsDryRunSuccess())
+	// CQ failed.
+	if len(cfg.CqFailureLabels) > 0 {
+		gerrit.SetLabels(ci, cfg.CqFailureLabels)
+	}
+	gerrit.UnsetLabels(ci, cfg.CqActiveLabels)
+	expect.CqFinished = true
+	expect.Result = autoroll.ROLL_RESULT_FAILURE
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// Set normal.
-	g.MockSetCQ(ci, "Mode was changed to normal")
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.SwitchToNormal(ctx))
-	g.AssertEmpty()
-	require.False(t, issue.IsDryRun)
-	require.False(t, gr.IsFinished())
-	require.False(t, gr.IsSuccess())
-	require.False(t, gr.IsDryRunFinished())
-	require.False(t, gr.IsDryRunSuccess())
-
-	// Update.
+	// CQ succeeded.
+	if len(cfg.CqFailureLabels) > 0 {
+		gerrit.UnsetLabels(ci, cfg.CqFailureLabels)
+	}
+	if len(cfg.CqSuccessLabels) > 0 {
+		gerrit.SetLabels(ci, cfg.CqSuccessLabels)
+	} else {
+		gerrit.UnsetLabels(ci, cfg.CqActiveLabels)
+	}
+	ci.Committed = true
 	ci.Status = gerrit.CHANGE_STATUS_MERGED
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.Update(ctx))
-	require.False(t, issue.IsDryRun)
-	require.True(t, gr.IsFinished())
-	require.True(t, gr.IsSuccess())
-	require.False(t, gr.IsDryRunFinished())
-	require.False(t, gr.IsDryRunSuccess())
-	require.Nil(t, recent.CurrentRoll())
+	expect.Closed = true
+	expect.Committed = true
+	expect.CqSuccess = true
+	expect.Result = autoroll.ROLL_RESULT_SUCCESS
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// Upload and retrieve another roll, dry run this time.
-	ci, issue = makeFakeRoll(124, from, to, true, true)
-	g.MockGetIssueProperties(ci)
-	gr, err = newGerritAndroidRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
-	require.NoError(t, err)
-	require.True(t, issue.IsDryRun)
-	require.False(t, gr.IsFinished())
-	require.False(t, gr.IsSuccess())
-	require.False(t, gr.IsDryRunFinished())
-	require.False(t, gr.IsDryRunSuccess())
-	g.AssertEmpty()
-	require.Equal(t, toRev, gr.RollingTo())
+	// CL was abandoned while CQ was running.
+	if len(cfg.CqSuccessLabels) > 0 {
+		gerrit.UnsetLabels(ci, cfg.CqSuccessLabels)
+	} else {
+		gerrit.SetLabels(ci, cfg.CqActiveLabels)
+	}
+	ci.Committed = false
+	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
+	expect.Committed = false
+	expect.CqFinished = true // Not really, but the CL is finished.
+	expect.CqSuccess = false
+	expect.Result = autoroll.ROLL_RESULT_FAILURE
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// Insert into DB.
-	current = recent.CurrentRoll()
-	require.Nil(t, current)
-	require.NoError(t, gr.InsertIntoDB(ctx))
-	current = recent.CurrentRoll()
-	require.NotNil(t, current)
-	require.Equal(t, current.Issue, ci.Issue)
-	g.AssertEmpty()
+	// Dry run active.
+	ci.Status = gerrit.CHANGE_STATUS_NEW
+	gerrit.UnsetLabels(ci, cfg.SetCqLabels)
+	gerrit.SetLabels(ci, cfg.SetDryRunLabels)
+	expect.Closed = false
+	expect.CqFinished = false
+	expect.IsDryRun = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_IN_PROGRESS
+	a.IsDryRun = true
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// Success.
-	ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.PRESUBMIT_VERIFIED_LABEL_ACCEPTED,
-			},
+	// Dry run failed.
+	if len(cfg.DryRunFailureLabels) > 0 {
+		gerrit.SetLabels(ci, cfg.DryRunFailureLabels)
+	}
+	gerrit.UnsetLabels(ci, cfg.DryRunActiveLabels)
+	expect.DryRunFinished = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_FAILURE
+	expect.TryResults = []*autoroll.TryResult{
+		{
+			Builder:  "fake",
+			Category: autoroll.TRYBOT_CATEGORY_CQ,
+			Result:   autoroll.TRYBOT_RESULT_FAILURE,
+			Status:   autoroll.TRYBOT_STATUS_COMPLETED,
 		},
 	}
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.Update(ctx))
-	require.True(t, issue.IsDryRun)
-	require.False(t, gr.IsFinished())
-	require.False(t, gr.IsSuccess())
-	require.True(t, gr.IsDryRunFinished())
-	require.True(t, gr.IsDryRunSuccess())
-	g.AssertEmpty()
+	a.TryResults = expect.TryResults
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// Close for cleanup.
+	// The CL was abandoned while the dry run was running.
+	expect.TryResults[0].Result = ""
+	expect.TryResults[0].Status = autoroll.TRYBOT_STATUS_SCHEDULED
 	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.Update(ctx))
+	expect.Closed = true
+	expect.DryRunFinished = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_FAILURE
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// Verify that all of the mutation functions handle a conflict (eg.
-	// someone closed the CL) gracefully.
+	// The CL was landed while the dry run was running.
+	ci.Committed = true
+	ci.Status = gerrit.CHANGE_STATUS_MERGED
+	expect.Committed = true
+	expect.DryRunSuccess = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_SUCCESS
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
 
-	// 1. SwitchToDryRun.
-	ci, issue = makeFakeRoll(125, from, to, false, true)
-	g.MockGetIssueProperties(ci)
-	gr, err = newGerritAndroidRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
-	require.NoError(t, err)
-	require.NoError(t, gr.InsertIntoDB(ctx))
-	url, reqBytes := g.MakePostRequest(ci, "Mode was changed to dry run", map[string]int{
-		gerrit.AUTOSUBMIT_LABEL: gerrit.AUTOSUBMIT_LABEL_NONE,
-	})
-	g.Mock.MockOnce(url, mockhttpclient.MockPostError("application/json", reqBytes, "CONFLICT", http.StatusConflict))
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.SwitchToDryRun(ctx))
-	g.AssertEmpty()
+	// Dry run success.
+	if len(cfg.DryRunSuccessLabels) > 0 {
+		gerrit.SetLabels(ci, cfg.DryRunSuccessLabels)
+	}
+	ci.Committed = false
+	ci.Status = gerrit.CHANGE_STATUS_NEW
+	expect.Closed = false
+	expect.Committed = false
+	expect.CqFinished = false
+	expect.CqSuccess = false
+	expect.DryRunSuccess = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_SUCCESS
+	expect.TryResults[0].Result = autoroll.TRYBOT_RESULT_SUCCESS
+	expect.TryResults[0].Status = autoroll.TRYBOT_STATUS_COMPLETED
+	require.NoError(t, updateIssueFromGerritChangeInfo(a, ci, cfg))
+	deepequal.AssertDeepEqual(t, expect, a)
+}
 
-	// 2. SwitchToNormal
-	ci, issue = makeFakeRoll(126, from, to, false, true)
-	g.MockGetIssueProperties(ci)
-	gr, err = newGerritAndroidRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
-	require.NoError(t, err)
-	require.NoError(t, gr.InsertIntoDB(ctx))
-	url, reqBytes = g.MakePostRequest(ci, "Mode was changed to normal", map[string]int{
-		gerrit.AUTOSUBMIT_LABEL: gerrit.AUTOSUBMIT_LABEL_SUBMIT,
-	})
-	g.Mock.MockOnce(url, mockhttpclient.MockPostError("application/json", reqBytes, "CONFLICT", http.StatusConflict))
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.SwitchToNormal(ctx))
-	g.AssertEmpty()
+func TestUpdateFromGerritChangeInfoAndroid(t *testing.T) {
+	testUpdateFromGerritChangeInfo(t, gerrit.CONFIG_ANDROID)
+}
 
-	// 3. Close.
-	ci, issue = makeFakeRoll(127, from, to, false, true)
-	g.MockGetIssueProperties(ci)
-	gr, err = newGerritAndroidRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
-	require.NoError(t, err)
-	require.NoError(t, gr.InsertIntoDB(ctx))
-	url = fmt.Sprintf("%s/a/changes/%d/abandon", gerrit_testutils.FAKE_GERRIT_URL, ci.Issue)
-	req := testutils.MarshalJSON(t, &struct {
-		Message string `json:"message"`
-	}{
-		Message: "close it!",
-	})
-	g.Mock.MockOnce(url, mockhttpclient.MockPostError("application/json", []byte(req), "CONFLICT", http.StatusConflict))
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.Close(ctx, autoroll.ROLL_RESULT_FAILURE, "close it!"))
-	g.AssertEmpty()
+func TestUpdateFromGerritChangeInfoANGLE(t *testing.T) {
+	testUpdateFromGerritChangeInfo(t, gerrit.CONFIG_ANGLE)
+}
 
-	// Verify that we set the correct status when abandoning a CL.
-	ci, issue = makeFakeRoll(128, from, to, false, true)
-	g.MockGetIssueProperties(ci)
-	gr, err = newGerritAndroidRoll(ctx, issue, g.Gerrit, recent, "http://issue/", toRev, nil)
-	require.NoError(t, err)
-	require.NoError(t, gr.InsertIntoDB(ctx))
-	url = fmt.Sprintf("%s/a/changes/%d/abandon", gerrit_testutils.FAKE_GERRIT_URL, ci.Issue)
-	req = testutils.MarshalJSON(t, &struct {
-		Message string `json:"message"`
-	}{
-		Message: "close it!",
-	})
-	g.Mock.MockOnce(url, mockhttpclient.MockPostDialogue("application/json", []byte(req), nil))
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	g.MockGetIssueProperties(ci)
-	require.NoError(t, gr.Close(ctx, autoroll.ROLL_RESULT_DRY_RUN_SUCCESS, "close it!"))
-	g.AssertEmpty()
-	issue, err = recent.Get(ctx, 128)
-	require.NoError(t, err)
-	require.Equal(t, issue.Result, autoroll.ROLL_RESULT_DRY_RUN_SUCCESS)
+func TestUpdateFromGerritChangeInfoChromium(t *testing.T) {
+	testUpdateFromGerritChangeInfo(t, gerrit.CONFIG_CHROMIUM)
+}
+
+func TestUpdateFromGitHubPullRequest(t *testing.T) {
+	unittest.SmallTest(t)
+
+	now := time.Now()
+
+	intPtr := func(v int) *int {
+		return &v
+	}
+	stringPtr := func(v string) *string {
+		return &v
+	}
+	boolPtr := func(v bool) *bool {
+		return &v
+	}
+
+	a := &autoroll.AutoRollIssue{
+		Issue:       123,
+		RollingFrom: "abc123",
+		RollingTo:   "def456",
+	}
+
+	// Ensure that we don't overwrite the issue number.
+	require.EqualError(t, updateIssueFromGitHubPullRequest(a, &github_api.PullRequest{}), "Pull request number 0 differs from existing issue number 123!")
+
+	// Normal, in-progress CL.
+	pr := &github_api.PullRequest{
+		Number:    intPtr(int(a.Issue)),
+		State:     stringPtr(""),
+		Commits:   intPtr(1),
+		Title:     stringPtr("roll the deps"),
+		CreatedAt: &now,
+		UpdatedAt: &now,
+	}
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	expect := &autoroll.AutoRollIssue{
+		Created:     now,
+		Issue:       123,
+		Modified:    now,
+		Patchsets:   []int64{1},
+		Result:      autoroll.ROLL_RESULT_IN_PROGRESS,
+		RollingFrom: "abc123",
+		RollingTo:   "def456",
+		Subject:     "roll the deps",
+	}
+	deepequal.AssertDeepEqual(t, expect, a)
+
+	// CQ failed.
+	pr.State = &github.CLOSED_STATE
+	expect.Closed = true // if the CQ fails, we close the PR.
+	expect.CqFinished = true
+	expect.Result = autoroll.ROLL_RESULT_FAILURE
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	deepequal.AssertDeepEqual(t, expect, a)
+
+	// CQ succeeded.
+	pr.Merged = boolPtr(true)
+	expect.Closed = true
+	expect.Committed = true
+	expect.CqSuccess = true
+	expect.Result = autoroll.ROLL_RESULT_SUCCESS
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	deepequal.AssertDeepEqual(t, expect, a)
+
+	// CL was abandoned while CQ was running.
+	// (the above includes this case)
+
+	// Dry run active.
+	pr.Merged = boolPtr(false)
+	pr.State = stringPtr("")
+	expect.TryResults = []*autoroll.TryResult{
+		{
+			Builder:  "fake",
+			Category: autoroll.TRYBOT_CATEGORY_CQ,
+			Status:   autoroll.TRYBOT_STATUS_SCHEDULED,
+		},
+	}
+	expect.Closed = false
+	expect.Committed = false
+	expect.CqFinished = false
+	expect.CqSuccess = false
+	expect.IsDryRun = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_IN_PROGRESS
+	a.IsDryRun = true
+	a.TryResults = expect.TryResults
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	deepequal.AssertDeepEqual(t, expect, a)
+
+	// Dry run failed.
+	expect.DryRunFinished = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_FAILURE
+	expect.TryResults[0].Result = autoroll.TRYBOT_RESULT_FAILURE
+	expect.TryResults[0].Status = autoroll.TRYBOT_STATUS_COMPLETED
+	a.TryResults = expect.TryResults
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	deepequal.AssertDeepEqual(t, expect, a)
+
+	// CL was abandoned while dry run was still running.
+	expect.TryResults[0].Result = ""
+	expect.TryResults[0].Status = autoroll.TRYBOT_STATUS_SCHEDULED
+	pr.State = &github.CLOSED_STATE
+	expect.Closed = true
+	expect.CqFinished = true
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	deepequal.AssertDeepEqual(t, expect, a)
+
+	// CL was landed while dry run was still running.
+	pr.Merged = boolPtr(true)
+	expect.Committed = true
+	expect.CqSuccess = true
+	expect.DryRunSuccess = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_SUCCESS
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	deepequal.AssertDeepEqual(t, expect, a)
+
+	// Dry run success.
+	pr.Merged = boolPtr(false)
+	pr.State = stringPtr("")
+	expect.Closed = false
+	expect.Committed = false
+	expect.CqFinished = false
+	expect.CqSuccess = false
+	expect.DryRunSuccess = true
+	expect.Result = autoroll.ROLL_RESULT_DRY_RUN_SUCCESS
+	expect.TryResults[0].Result = autoroll.TRYBOT_RESULT_SUCCESS
+	expect.TryResults[0].Status = autoroll.TRYBOT_STATUS_COMPLETED
+	require.NoError(t, updateIssueFromGitHubPullRequest(a, pr))
+	deepequal.AssertDeepEqual(t, expect, a)
 }
diff --git a/autoroll/go/repo_manager/android_repo_manager.go b/autoroll/go/repo_manager/android_repo_manager.go
index b9f5c18..54dc375 100644
--- a/autoroll/go/repo_manager/android_repo_manager.go
+++ b/autoroll/go/repo_manager/android_repo_manager.go
@@ -247,12 +247,6 @@
 	return r.g.SetTopic(context.TODO(), topic, changeNum)
 }
 
-// setChangeLabels sets the appropriate labels on the Gerrit change, according
-// to the Gerrit config.
-func (r *androidRepoManager) setChangeLabels(change *gerrit.ChangeInfo, dryRun bool) error {
-	return r.g.SetReview(context.TODO(), change, "Roller setting labels to auto-land change.", r.codereview.Config().(*codereview.GerritConfig).GetLabels(dryRun), nil)
-}
-
 func ExtractBugNumbers(line string) map[string]bool {
 	bugs := map[string]bool{}
 	re := regexp.MustCompile("(?m)^(BUG|Bug) *[ :=] *b/([0-9]+) *$")
@@ -483,7 +477,12 @@
 	}
 
 	// Set labels.
-	if err := r.setChangeLabels(change, dryRun); err != nil {
+	labels := r.g.Config().SetCqLabels
+	if dryRun {
+		labels = r.g.Config().SetDryRunLabels
+	}
+	labels = gerrit.MergeLabels(labels, r.g.Config().SelfApproveLabels)
+	if err = r.g.SetReview(ctx, change, "Roller setting labels to auto-land change.", labels, emails); err != nil {
 		// Only throw exception here if parentBranch is master. This is
 		// because other branches will not have permissions setup for the
 		// bot to run CR+2.
diff --git a/autoroll/go/repo_manager/android_repo_manager_test.go b/autoroll/go/repo_manager/android_repo_manager_test.go
index 2cb32d5..9bd7e5a 100644
--- a/autoroll/go/repo_manager/android_repo_manager_test.go
+++ b/autoroll/go/repo_manager/android_repo_manager_test.go
@@ -116,6 +116,7 @@
 	ctx, wd, cleanup := setupAndroid(t)
 	defer cleanup()
 	g := &gerrit_mocks.SimpleGerritInterface{IssueID: androidIssueNum}
+	g.On("Config").Return(gerrit.CONFIG_ANDROID)
 	rm, err := NewAndroidRepoManager(ctx, androidCfg(), wd, g, "fake.server.com", "fake-service-account", nil, androidGerrit(t, g), false)
 	require.NoError(t, err)
 	require.NoError(t, SetStrategy(ctx, rm, strategy.ROLL_STRATEGY_BATCH))
@@ -133,6 +134,7 @@
 	defer cleanup()
 
 	g := &gerrit_mocks.SimpleGerritInterface{IssueID: androidIssueNum}
+	g.On("Config").Return(gerrit.CONFIG_ANDROID)
 	rm, err := NewAndroidRepoManager(ctx, androidCfg(), wd, g, "fake.server.com", "fake-service-account", nil, androidGerrit(t, g), false)
 	require.NoError(t, err)
 	require.NoError(t, SetStrategy(ctx, rm, strategy.ROLL_STRATEGY_BATCH))
@@ -201,6 +203,7 @@
 	defer cleanup()
 
 	g := &gerrit_mocks.SimpleGerritInterface{IssueID: androidIssueNum}
+	g.On("Config").Return(gerrit.CONFIG_ANDROID)
 	rm, err := NewAndroidRepoManager(ctx, androidCfg(), wd, g, "fake.server.com", "fake-service-account", nil, androidGerrit(t, g), false)
 	require.NoError(t, err)
 	require.NoError(t, SetStrategy(ctx, rm, strategy.ROLL_STRATEGY_BATCH))
diff --git a/autoroll/go/repo_manager/fuchsia_sdk_android_repo_manager_test.go b/autoroll/go/repo_manager/fuchsia_sdk_android_repo_manager_test.go
index 4e69dc4..95b4642 100644
--- a/autoroll/go/repo_manager/fuchsia_sdk_android_repo_manager_test.go
+++ b/autoroll/go/repo_manager/fuchsia_sdk_android_repo_manager_test.go
@@ -107,7 +107,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(gerrit.CONFIG_ANDROID, gUrl, gitcookies, urlmock.Client())
 	require.NoError(t, err)
 
 	// Initial update, everything up-to-date.
@@ -268,7 +268,7 @@
 	urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/detail?o=ALL_REVISIONS", mockhttpclient.MockGetDialogue(respBody))
 
 	// Mock the request to set the CQ.
-	reqBody = []byte(`{"labels":{"Autosubmit":1,"Code-Review":"2","Presubmit-Ready":"1"},"message":"","reviewers":[{"reviewer":"reviewer@chromium.org"}]}`)
+	reqBody = []byte(`{"labels":{"Autosubmit":1,"Code-Review":2,"Presubmit-Ready":1},"message":"","reviewers":[{"reviewer":"reviewer@chromium.org"}]}`)
 	urlmock.MockOnce("https://fake-skia-review.googlesource.com/a/changes/123/revisions/ps1/review", mockhttpclient.MockPostDialogue("application/json", reqBody, []byte("")))
 
 	issue, err := rm.CreateNewRoll(ctx, rm.LastRollRev(), rm.NextRollRev(), emails, cqExtraTrybots, false)
diff --git a/autoroll/go/repo_manager/no_checkout_repo_manager.go b/autoroll/go/repo_manager/no_checkout_repo_manager.go
index d90773a..c934988 100644
--- a/autoroll/go/repo_manager/no_checkout_repo_manager.go
+++ b/autoroll/go/repo_manager/no_checkout_repo_manager.go
@@ -124,7 +124,12 @@
 	}
 
 	// Set the CQ bit as appropriate.
-	if err = rm.g.SetReview(ctx, ci, "", rm.gerritConfig.GetLabels(dryRun), emails); err != nil {
+	labels := rm.g.Config().SetCqLabels
+	if dryRun {
+		labels = rm.g.Config().SetDryRunLabels
+	}
+	labels = gerrit.MergeLabels(labels, rm.g.Config().SelfApproveLabels)
+	if err = rm.g.SetReview(ctx, ci, "", labels, emails); err != nil {
 		// TODO(borenet): Should we try to abandon the CL?
 		return 0, fmt.Errorf("Failed to set review: %s", err)
 	}
diff --git a/autoroll/go/roller/autoroller.go b/autoroll/go/roller/autoroller.go
index 85b7374..ce4b8db 100644
--- a/autoroll/go/roller/autoroller.go
+++ b/autoroll/go/roller/autoroller.go
@@ -56,8 +56,6 @@
 	emails          []string
 	emailsMtx       sync.RWMutex
 	failureThrottle *state_machine.Throttler
-	gerrit          *gerrit.Gerrit
-	github          *github.GitHub
 	liveness        metrics2.Liveness
 	manualRollDB    manual.DB
 	modeHistory     *modes.ModeHistory
@@ -201,8 +199,6 @@
 		codereview:      cr,
 		emails:          emails,
 		failureThrottle: failureThrottle,
-		gerrit:          g,
-		github:          githubClient,
 		liveness:        metrics2.NewLiveness("last_autoroll_landed", map[string]string{"roller": c.RollerName}),
 		manualRollDB:    manualRollDB,
 		modeHistory:     mh,
diff --git a/go/autoroll/autoroll.go b/go/autoroll/autoroll.go
index ddd22fa..40b5602 100644
--- a/go/autoroll/autoroll.go
+++ b/go/autoroll/autoroll.go
@@ -12,12 +12,9 @@
 	"time"
 
 	"github.com/golang/protobuf/ptypes"
-	github_api "github.com/google/go-github/github"
 	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
 	"go.skia.org/infra/go/buildbucket"
 	"go.skia.org/infra/go/comment"
-	"go.skia.org/infra/go/gerrit"
-	"go.skia.org/infra/go/github"
 	"go.skia.org/infra/go/skerr"
 	"go.skia.org/infra/go/sklog"
 	"go.skia.org/infra/go/util"
@@ -161,117 +158,8 @@
 	}
 }
 
-// UpdateFromGitHubPullRequest updates the AutoRollIssue instance based on the
-// given PullRequest. If an error is returned, the AutoRollIssue is not changed.
-func (i *AutoRollIssue) UpdateFromGitHubPullRequest(pullRequest *github_api.PullRequest) error {
-	prNum := int64(pullRequest.GetNumber())
-	if i.Issue == 0 {
-		i.Issue = prNum
-	} else if i.Issue != prNum {
-		return fmt.Errorf("Pull request number %d differs from existing issue number %d!", prNum, i.Issue)
-	}
-	i.CqFinished = pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged()
-	i.CqSuccess = pullRequest.GetMerged()
-	if i.IsDryRun {
-		i.DryRunFinished = i.AllTrybotsFinished() || pullRequest.GetState() == github.CLOSED_STATE || pullRequest.GetMerged()
-		i.DryRunSuccess = (i.DryRunFinished && i.AllTrybotsSucceeded()) || pullRequest.GetMerged()
-	} else {
-		i.DryRunFinished = false
-		i.DryRunSuccess = false
-	}
-
-	ps := make([]int64, 0, *pullRequest.Commits)
-	for i := 1; i <= *pullRequest.Commits; i++ {
-		ps = append(ps, int64(i))
-	}
-	i.Closed = pullRequest.GetState() == github.CLOSED_STATE
-	i.Committed = pullRequest.GetMerged()
-	i.Created = pullRequest.GetCreatedAt()
-	i.Modified = pullRequest.GetUpdatedAt()
-	i.Patchsets = ps
-	i.Subject = pullRequest.GetTitle()
-	i.Result = rollResult(i)
-	return i.Validate()
-}
-
-// UpdateFromGerritChangeInfo updates the AutoRollIssue instance based on the
-// given gerrit.ChangeInfo. If an error is returned, the AutoRollIssue is not
-// changed.
-func (i *AutoRollIssue) UpdateFromGerritChangeInfo(ci *gerrit.ChangeInfo, rollIntoAndroid bool) error {
-	if i.Issue == 0 {
-		i.Issue = ci.Issue
-	} else if i.Issue != ci.Issue {
-		return fmt.Errorf("CL ID %d differs from existing issue number %d!", ci.Issue, i.Issue)
-	}
-	cqFinished := false
-	dryRunFinished := false
-	dryRunSuccess := false
-	if rollIntoAndroid {
-		if _, ok := ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL]; ok {
-			for _, lb := range ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL].All {
-				if lb.Value == gerrit.PRESUBMIT_VERIFIED_LABEL_REJECTED {
-					cqFinished = true
-					dryRunFinished = true
-					break
-				} else if lb.Value == gerrit.PRESUBMIT_VERIFIED_LABEL_ACCEPTED {
-					// Not marking cqSuccess or cqFinished
-					// true here; those are only true if the
-					// change is merged.
-					dryRunFinished = true
-					dryRunSuccess = true
-				}
-			}
-		}
-	} else {
-		foundCqLabel := false
-		foundDryRunLabel := false
-		if _, ok := ci.Labels[gerrit.COMMITQUEUE_LABEL]; ok {
-			for _, lb := range ci.Labels[gerrit.COMMITQUEUE_LABEL].All {
-				if lb.Value == gerrit.COMMITQUEUE_LABEL_DRY_RUN {
-					foundDryRunLabel = true
-				} else if lb.Value == gerrit.COMMITQUEUE_LABEL_SUBMIT {
-					foundCqLabel = true
-				}
-			}
-		}
-		if !foundCqLabel {
-			cqFinished = true
-		}
-		if !foundDryRunLabel {
-			dryRunFinished = true
-		}
-		if i.IsDryRun && dryRunFinished {
-			dryRunSuccess = i.AllTrybotsSucceeded()
-		}
-	}
-
-	i.CqFinished = ci.IsClosed()
-	i.CqSuccess = ci.Status == gerrit.CHANGE_STATUS_MERGED
-	if i.IsDryRun {
-		i.DryRunFinished = dryRunFinished || ci.IsClosed()
-		i.DryRunSuccess = dryRunSuccess || ci.Status == gerrit.CHANGE_STATUS_MERGED
-	} else {
-		i.CqFinished = i.CqFinished || cqFinished
-		i.DryRunFinished = false
-		i.DryRunSuccess = false
-	}
-
-	ps := make([]int64, 0, len(ci.Patchsets))
-	for _, p := range ci.Patchsets {
-		ps = append(ps, p.Number)
-	}
-	i.Closed = ci.IsClosed()
-	i.Committed = ci.Committed
-	i.Created = ci.Created
-	i.Modified = ci.Updated
-	i.Patchsets = ps
-	i.Subject = ci.Subject
-	i.Result = rollResult(i)
-	return i.Validate()
-}
-
-// rollResult derives a result string for the roll.
-func rollResult(roll *AutoRollIssue) string {
+// RollResult derives a result string for the roll.
+func RollResult(roll *AutoRollIssue) string {
 	if roll.IsDryRun {
 		if roll.DryRunFinished {
 			if roll.DryRunSuccess {
diff --git a/go/autoroll/autoroll_test.go b/go/autoroll/autoroll_test.go
index fbb2950..20b7ae3 100644
--- a/go/autoroll/autoroll_test.go
+++ b/go/autoroll/autoroll_test.go
@@ -1,19 +1,15 @@
 package autoroll
 
 import (
-	"fmt"
 	"testing"
 	"time"
 
 	"github.com/golang/protobuf/ptypes"
 	"github.com/golang/protobuf/ptypes/timestamp"
-	github_api "github.com/google/go-github/github"
 	"github.com/stretchr/testify/require"
 	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
 	"go.skia.org/infra/go/comment"
 	"go.skia.org/infra/go/deepequal"
-	"go.skia.org/infra/go/gerrit"
-	"go.skia.org/infra/go/github"
 	"go.skia.org/infra/go/testutils/unittest"
 )
 
@@ -77,7 +73,7 @@
 		Patchsets: []int64{1},
 		Subject:   "Roll src/third_party/skia abc123..def456 (3 commits).",
 	}
-	roll.Result = rollResult(roll)
+	roll.Result = RollResult(roll)
 
 	trybot := &buildbucketpb.Build{
 		Builder: &buildbucketpb.BuilderID{
@@ -172,470 +168,3 @@
 	require.True(t, roll.AllTrybotsFinished())
 	require.True(t, roll.AllTrybotsSucceeded())
 }
-
-func TestUpdateFromGerritChangeInfo(t *testing.T) {
-	unittest.SmallTest(t)
-
-	now := time.Now()
-
-	a := &AutoRollIssue{
-		Issue:       123,
-		RollingFrom: "abc123",
-		RollingTo:   "def456",
-	}
-
-	// Ensure that we don't overwrite the issue number.
-	require.EqualError(t, a.UpdateFromGerritChangeInfo(&gerrit.ChangeInfo{}, false), "CL ID 0 differs from existing issue number 123!")
-
-	// Normal, in-progress CL.
-	rev := &gerrit.Revision{
-		ID:            "1",
-		Number:        1,
-		Created:       now,
-		CreatedString: now.Format(gerrit.TIME_FORMAT),
-	}
-	ci := &gerrit.ChangeInfo{
-		Created:       now,
-		CreatedString: now.Format(gerrit.TIME_FORMAT),
-		Subject:       "roll the deps",
-		ChangeId:      fmt.Sprintf("%d", a.Issue),
-		Issue:         a.Issue,
-		Labels: map[string]*gerrit.LabelEntry{
-			gerrit.CODEREVIEW_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: gerrit.CODEREVIEW_LABEL_APPROVE,
-					},
-				},
-			},
-			gerrit.COMMITQUEUE_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: gerrit.COMMITQUEUE_LABEL_SUBMIT,
-					},
-				},
-			},
-		},
-		Owner: &gerrit.Owner{
-			Email: "fake@chromium.org",
-		},
-		Project: "skia",
-		Revisions: map[string]*gerrit.Revision{
-			rev.ID: rev,
-		},
-		Patchsets:     []*gerrit.Revision{rev},
-		Status:        gerrit.CHANGE_STATUS_NEW,
-		Updated:       now,
-		UpdatedString: now.Format(gerrit.TIME_FORMAT),
-	}
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, false))
-	expect := &AutoRollIssue{
-		Created:     now,
-		Issue:       123,
-		Modified:    now,
-		Patchsets:   []int64{1},
-		Result:      ROLL_RESULT_IN_PROGRESS,
-		RollingFrom: "abc123",
-		RollingTo:   "def456",
-		Subject:     "roll the deps",
-	}
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CQ failed.
-	delete(ci.Labels, gerrit.COMMITQUEUE_LABEL)
-	expect.CqFinished = true
-	expect.Result = ROLL_RESULT_FAILURE
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, false))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CQ succeeded.
-	ci.Committed = true
-	ci.Status = gerrit.CHANGE_STATUS_MERGED
-	expect.Closed = true
-	expect.Committed = true
-	expect.CqSuccess = true
-	expect.Result = ROLL_RESULT_SUCCESS
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, false))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CL was abandoned while CQ was running.
-	ci.Labels[gerrit.COMMITQUEUE_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.COMMITQUEUE_LABEL_SUBMIT,
-			},
-		},
-	}
-	ci.Committed = false
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	expect.Committed = false
-	expect.CqFinished = true // Not really, but the CL is finished.
-	expect.CqSuccess = false
-	expect.Result = ROLL_RESULT_FAILURE
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, false))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run active.
-	ci.Status = gerrit.CHANGE_STATUS_NEW
-	ci.Labels[gerrit.COMMITQUEUE_LABEL].All[0].Value = gerrit.COMMITQUEUE_LABEL_DRY_RUN
-	expect.Closed = false
-	expect.CqFinished = false
-	expect.IsDryRun = true
-	expect.Result = ROLL_RESULT_DRY_RUN_IN_PROGRESS
-	a.IsDryRun = true
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, false))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run failed.
-	delete(ci.Labels, gerrit.COMMITQUEUE_LABEL)
-	expect.DryRunFinished = true
-	expect.Result = ROLL_RESULT_DRY_RUN_FAILURE
-	expect.TryResults = []*TryResult{
-		{
-			Builder:  "fake",
-			Category: TRYBOT_CATEGORY_CQ,
-			Result:   TRYBOT_RESULT_FAILURE,
-			Status:   TRYBOT_STATUS_COMPLETED,
-		},
-	}
-	a.TryResults = expect.TryResults
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, false))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// The CL was abandoned while the dry run was running.
-	expect.TryResults[0].Result = ""
-	expect.TryResults[0].Status = TRYBOT_STATUS_SCHEDULED
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	expect.Closed = true
-	expect.CqFinished = true
-	expect.DryRunFinished = true
-	expect.Result = ROLL_RESULT_DRY_RUN_FAILURE
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// The CL was landed while the dry run was running.
-	ci.Committed = true
-	ci.Status = gerrit.CHANGE_STATUS_MERGED
-	expect.Committed = true
-	expect.CqSuccess = true
-	expect.DryRunSuccess = true
-	expect.Result = ROLL_RESULT_DRY_RUN_SUCCESS
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run success.
-	ci.Committed = false
-	ci.Status = gerrit.CHANGE_STATUS_NEW
-	expect.Closed = false
-	expect.Committed = false
-	expect.CqFinished = false
-	expect.CqSuccess = false
-	expect.DryRunSuccess = true
-	expect.Result = ROLL_RESULT_DRY_RUN_SUCCESS
-	expect.TryResults[0].Result = TRYBOT_RESULT_SUCCESS
-	expect.TryResults[0].Status = TRYBOT_STATUS_COMPLETED
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, false))
-	deepequal.AssertDeepEqual(t, expect, a)
-}
-
-func TestUpdateFromGerritChangeInfoAndroid(t *testing.T) {
-	unittest.SmallTest(t)
-
-	now := time.Now()
-
-	a := &AutoRollIssue{
-		Issue:       123,
-		RollingFrom: "abc123",
-		RollingTo:   "def456",
-	}
-
-	// Ensure that we don't overwrite the issue number.
-	require.EqualError(t, a.UpdateFromGerritChangeInfo(&gerrit.ChangeInfo{}, true), "CL ID 0 differs from existing issue number 123!")
-
-	// Normal, in-progress CL.
-	rev := &gerrit.Revision{
-		ID:            "1",
-		Number:        1,
-		Created:       now,
-		CreatedString: now.Format(gerrit.TIME_FORMAT),
-	}
-	ci := &gerrit.ChangeInfo{
-		Created:       now,
-		CreatedString: now.Format(gerrit.TIME_FORMAT),
-		Subject:       "roll the deps",
-		ChangeId:      fmt.Sprintf("%d", a.Issue),
-		Issue:         a.Issue,
-		Labels: map[string]*gerrit.LabelEntry{
-			gerrit.CODEREVIEW_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: 2,
-					},
-				},
-			},
-			gerrit.PRESUBMIT_READY_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: 1,
-					},
-				},
-			},
-			gerrit.AUTOSUBMIT_LABEL: {
-				All: []*gerrit.LabelDetail{
-					{
-						Value: gerrit.AUTOSUBMIT_LABEL_NONE,
-					},
-				},
-			},
-		},
-		Owner: &gerrit.Owner{
-			Email: "fake@chromium.org",
-		},
-		Project: "skia",
-		Revisions: map[string]*gerrit.Revision{
-			rev.ID: rev,
-		},
-		Patchsets:     []*gerrit.Revision{rev},
-		Status:        gerrit.CHANGE_STATUS_NEW,
-		Updated:       now,
-		UpdatedString: now.Format(gerrit.TIME_FORMAT),
-	}
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	expect := &AutoRollIssue{
-		Created:     now,
-		Issue:       123,
-		Modified:    now,
-		Patchsets:   []int64{1},
-		Result:      ROLL_RESULT_IN_PROGRESS,
-		RollingFrom: "abc123",
-		RollingTo:   "def456",
-		Subject:     "roll the deps",
-	}
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CQ failed.
-	ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.PRESUBMIT_VERIFIED_LABEL_REJECTED,
-			},
-		},
-	}
-	expect.CqFinished = true
-	expect.Result = ROLL_RESULT_FAILURE
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CQ succeeded.
-	ci.Committed = true
-	ci.Status = gerrit.CHANGE_STATUS_MERGED
-	expect.Closed = true
-	expect.Committed = true
-	expect.CqSuccess = true
-	expect.Result = ROLL_RESULT_SUCCESS
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CL was abandoned while CQ was running.
-	delete(ci.Labels, gerrit.PRESUBMIT_VERIFIED_LABEL)
-	ci.Committed = false
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	expect.Committed = false
-	expect.CqFinished = true // Not really, but the CL is finished.
-	expect.CqSuccess = false
-	expect.Result = ROLL_RESULT_FAILURE
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run active.
-	ci.Status = gerrit.CHANGE_STATUS_NEW
-	ci.Labels[gerrit.AUTOSUBMIT_LABEL].All[0].Value = gerrit.AUTOSUBMIT_LABEL_NONE
-	expect.Closed = false
-	expect.CqFinished = false
-	expect.IsDryRun = true
-	expect.Result = ROLL_RESULT_DRY_RUN_IN_PROGRESS
-	a.IsDryRun = true
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run failed.
-	ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.PRESUBMIT_VERIFIED_LABEL_REJECTED,
-			},
-		},
-	}
-	expect.DryRunFinished = true
-	expect.Result = ROLL_RESULT_DRY_RUN_FAILURE
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// The CL was abandoned while the dry run was running.
-	ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL].All[0].Value = gerrit.PRESUBMIT_VERIFIED_LABEL_RUNNING
-	ci.Status = gerrit.CHANGE_STATUS_ABANDONED
-	expect.Closed = true
-	expect.CqFinished = true
-	expect.DryRunFinished = true
-	expect.Result = ROLL_RESULT_DRY_RUN_FAILURE
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// The CL was landed while the dry run was running.
-	ci.Committed = true
-	ci.Status = gerrit.CHANGE_STATUS_MERGED
-	expect.Committed = true
-	expect.CqSuccess = true
-	expect.DryRunSuccess = true
-	expect.Result = ROLL_RESULT_DRY_RUN_SUCCESS
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run success.
-	ci.Labels[gerrit.PRESUBMIT_VERIFIED_LABEL] = &gerrit.LabelEntry{
-		All: []*gerrit.LabelDetail{
-			{
-				Value: gerrit.PRESUBMIT_VERIFIED_LABEL_ACCEPTED,
-			},
-		},
-	}
-	ci.Committed = false
-	ci.Status = gerrit.CHANGE_STATUS_NEW
-	expect.Closed = false
-	expect.CqFinished = false
-	expect.CqSuccess = false
-	expect.Committed = false
-	expect.DryRunSuccess = true
-	expect.Result = ROLL_RESULT_DRY_RUN_SUCCESS
-	require.NoError(t, a.UpdateFromGerritChangeInfo(ci, true))
-	deepequal.AssertDeepEqual(t, expect, a)
-}
-
-func TestUpdateFromGitHubPullRequest(t *testing.T) {
-	unittest.SmallTest(t)
-
-	now := time.Now()
-
-	intPtr := func(v int) *int {
-		return &v
-	}
-	stringPtr := func(v string) *string {
-		return &v
-	}
-	boolPtr := func(v bool) *bool {
-		return &v
-	}
-
-	a := &AutoRollIssue{
-		Issue:       123,
-		RollingFrom: "abc123",
-		RollingTo:   "def456",
-	}
-
-	// Ensure that we don't overwrite the issue number.
-	require.EqualError(t, a.UpdateFromGitHubPullRequest(&github_api.PullRequest{}), "Pull request number 0 differs from existing issue number 123!")
-
-	// Normal, in-progress CL.
-	pr := &github_api.PullRequest{
-		Number:    intPtr(int(a.Issue)),
-		State:     stringPtr(""),
-		Commits:   intPtr(1),
-		Title:     stringPtr("roll the deps"),
-		CreatedAt: &now,
-		UpdatedAt: &now,
-	}
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	expect := &AutoRollIssue{
-		Created:     now,
-		Issue:       123,
-		Modified:    now,
-		Patchsets:   []int64{1},
-		Result:      ROLL_RESULT_IN_PROGRESS,
-		RollingFrom: "abc123",
-		RollingTo:   "def456",
-		Subject:     "roll the deps",
-	}
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CQ failed.
-	pr.State = &github.CLOSED_STATE
-	expect.Closed = true // if the CQ fails, we close the PR.
-	expect.CqFinished = true
-	expect.Result = ROLL_RESULT_FAILURE
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CQ succeeded.
-	pr.Merged = boolPtr(true)
-	expect.Closed = true
-	expect.Committed = true
-	expect.CqSuccess = true
-	expect.Result = ROLL_RESULT_SUCCESS
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CL was abandoned while CQ was running.
-	// (the above includes this case)
-
-	// Dry run active.
-	pr.Merged = boolPtr(false)
-	pr.State = stringPtr("")
-	expect.TryResults = []*TryResult{
-		{
-			Builder:  "fake",
-			Category: TRYBOT_CATEGORY_CQ,
-			Status:   TRYBOT_STATUS_SCHEDULED,
-		},
-	}
-	expect.Closed = false
-	expect.Committed = false
-	expect.CqFinished = false
-	expect.CqSuccess = false
-	expect.IsDryRun = true
-	expect.Result = ROLL_RESULT_DRY_RUN_IN_PROGRESS
-	a.IsDryRun = true
-	a.TryResults = expect.TryResults
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run failed.
-	expect.DryRunFinished = true
-	expect.Result = ROLL_RESULT_DRY_RUN_FAILURE
-	expect.TryResults[0].Result = TRYBOT_RESULT_FAILURE
-	expect.TryResults[0].Status = TRYBOT_STATUS_COMPLETED
-	a.TryResults = expect.TryResults
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CL was abandoned while dry run was still running.
-	expect.TryResults[0].Result = ""
-	expect.TryResults[0].Status = TRYBOT_STATUS_SCHEDULED
-	pr.State = &github.CLOSED_STATE
-	expect.Closed = true
-	expect.CqFinished = true
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// CL was landed while dry run was still running.
-	pr.Merged = boolPtr(true)
-	expect.Committed = true
-	expect.CqSuccess = true
-	expect.DryRunSuccess = true
-	expect.Result = ROLL_RESULT_DRY_RUN_SUCCESS
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	deepequal.AssertDeepEqual(t, expect, a)
-
-	// Dry run success.
-	pr.Merged = boolPtr(false)
-	pr.State = stringPtr("")
-	expect.Closed = false
-	expect.Committed = false
-	expect.CqFinished = false
-	expect.CqSuccess = false
-	expect.DryRunSuccess = true
-	expect.Result = ROLL_RESULT_DRY_RUN_SUCCESS
-	expect.TryResults[0].Result = TRYBOT_RESULT_SUCCESS
-	expect.TryResults[0].Status = TRYBOT_STATUS_COMPLETED
-	require.NoError(t, a.UpdateFromGitHubPullRequest(pr))
-	deepequal.AssertDeepEqual(t, expect, a)
-}
diff --git a/go/gerrit/config.go b/go/gerrit/config.go
new file mode 100644
index 0000000..c3f5b00
--- /dev/null
+++ b/go/gerrit/config.go
@@ -0,0 +1,213 @@
+package gerrit
+
+var (
+	CONFIG_ANDROID = &Config{
+		SelfApproveLabels: map[string]int{
+			CODEREVIEW_LABEL: CODEREVIEW_LABEL_SELF_APPROVE,
+		},
+		SetCqLabels: map[string]int{
+			AUTOSUBMIT_LABEL:      AUTOSUBMIT_LABEL_SUBMIT,
+			PRESUBMIT_READY_LABEL: PRESUBMIT_READY_LABEL_ENABLE,
+		},
+		SetDryRunLabels: map[string]int{
+			AUTOSUBMIT_LABEL:      AUTOSUBMIT_LABEL_NONE,
+			PRESUBMIT_READY_LABEL: PRESUBMIT_READY_LABEL_ENABLE,
+		},
+		NoCqLabels: map[string]int{
+			AUTOSUBMIT_LABEL:      AUTOSUBMIT_LABEL_NONE,
+			PRESUBMIT_READY_LABEL: PRESUBMIT_READY_LABEL_NONE,
+		},
+		CqActiveLabels: map[string]int{
+			AUTOSUBMIT_LABEL:      AUTOSUBMIT_LABEL_SUBMIT,
+			PRESUBMIT_READY_LABEL: PRESUBMIT_READY_LABEL_ENABLE,
+		},
+		CqSuccessLabels: map[string]int{
+			PRESUBMIT_VERIFIED_LABEL: PRESUBMIT_VERIFIED_LABEL_ACCEPTED,
+		},
+		CqFailureLabels: map[string]int{
+			PRESUBMIT_VERIFIED_LABEL: PRESUBMIT_VERIFIED_LABEL_REJECTED,
+		},
+		DryRunActiveLabels: map[string]int{
+			AUTOSUBMIT_LABEL:      AUTOSUBMIT_LABEL_NONE,
+			PRESUBMIT_READY_LABEL: PRESUBMIT_READY_LABEL_ENABLE,
+		},
+		DryRunSuccessLabels: map[string]int{
+			PRESUBMIT_VERIFIED_LABEL: PRESUBMIT_VERIFIED_LABEL_ACCEPTED,
+		},
+		DryRunFailureLabels: map[string]int{
+			PRESUBMIT_VERIFIED_LABEL: PRESUBMIT_VERIFIED_LABEL_REJECTED,
+		},
+		DryRunUsesTryjobResults: false,
+	}
+
+	CONFIG_ANGLE = &Config{
+		SelfApproveLabels: map[string]int{
+			CODEREVIEW_LABEL: CODEREVIEW_LABEL_SELF_APPROVE,
+		},
+		SetCqLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_SUBMIT,
+		},
+		SetDryRunLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_DRY_RUN,
+		},
+		NoCqLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_NONE,
+		},
+		CqActiveLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_SUBMIT,
+		},
+		CqSuccessLabels: map[string]int{},
+		CqFailureLabels: map[string]int{},
+		DryRunActiveLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_DRY_RUN,
+		},
+		DryRunSuccessLabels:     map[string]int{},
+		DryRunFailureLabels:     map[string]int{},
+		DryRunUsesTryjobResults: true,
+	}
+
+	CONFIG_CHROMIUM = &Config{
+		SelfApproveLabels: map[string]int{
+			CODEREVIEW_LABEL: CODEREVIEW_LABEL_APPROVE,
+		},
+		SetCqLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_SUBMIT,
+		},
+		SetDryRunLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_DRY_RUN,
+		},
+		NoCqLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_NONE,
+		},
+		CqActiveLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_SUBMIT,
+		},
+		CqSuccessLabels: map[string]int{},
+		CqFailureLabels: map[string]int{},
+		DryRunActiveLabels: map[string]int{
+			COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_DRY_RUN,
+		},
+		DryRunSuccessLabels:     map[string]int{},
+		DryRunFailureLabels:     map[string]int{},
+		DryRunUsesTryjobResults: true,
+	}
+)
+
+type Config struct {
+	// Labels to set to self-approve a change. For some projects this is the
+	// same as a normal approval.
+	SelfApproveLabels map[string]int
+
+	// Labels to set to run the Commit Queue.
+	SetCqLabels map[string]int
+	// Labels to set to run the Commit Queue in dry run mode.
+	SetDryRunLabels map[string]int
+	// Labels to set to remove from the Commit Queue.
+	NoCqLabels map[string]int
+
+	// If the issue is open and all of these labels are set, the Commit
+	// Queue is considered active.
+	CqActiveLabels map[string]int
+	// If the issue is merged or all of these labels are set, the Commit
+	// Queue is considered to have finished successfully.
+	CqSuccessLabels map[string]int
+	// If the issue is abandoned or all of these labels are set, the Commit
+	// Queue is considered to have failed.
+	CqFailureLabels map[string]int
+
+	// If the issue is open and all of these labels are set, the dry run is
+	// considered active.
+	DryRunActiveLabels map[string]int
+	// If the issue is merged or all of these labels are set, the dry run is
+	// considered to have finished successfuly.
+	DryRunSuccessLabels map[string]int
+	// If the issue is abandoned or all of these labels are set, the dry run
+	// is considered to have failed.
+	DryRunFailureLabels map[string]int
+	// DryRunUsesTryjobResults is true if tryjob results should be factored
+	// into dry run success.
+	DryRunUsesTryjobResults bool
+}
+
+// all returns true iff all of the given label keys and values are set on the
+// change. Returns true if the given map of labels is empty.
+func all(ci *ChangeInfo, labels map[string]int) bool {
+	for labelKey, wantValue := range labels {
+		found := false
+		if labelEntry, ok := ci.Labels[labelKey]; ok {
+			for _, labelDetail := range labelEntry.All {
+				if wantValue == labelDetail.Value {
+					found = true
+				}
+			}
+		}
+		if !found {
+			return false
+		}
+	}
+	return true
+}
+
+// MergeLabels returns a new map containing both sets of labels. Labels from the
+// second set overwrite matching labels from the first.
+func MergeLabels(a, b map[string]int) map[string]int {
+	rv := make(map[string]int, len(a)+len(b))
+	for k, v := range a {
+		rv[k] = v
+	}
+	for k, v := range b {
+		rv[k] = v
+	}
+	return rv
+}
+
+// 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 ci.IsClosed() {
+		return false
+	}
+	if len(c.CqActiveLabels) > 0 && all(ci, c.CqActiveLabels) {
+		return true
+	}
+	return false
+}
+
+// CqSuccess returns true if the commit queue has finished successfully. This
+// requires that the change is merged; CqSuccess returns false if the change is
+// not merged, even if the commit queue finishes (ie. the relevant label is
+// removed) and all trybots were successful or a CqSuccessLabel is applied.
+func (c *Config) CqSuccess(ci *ChangeInfo) bool {
+	return ci.IsMerged()
+}
+
+// 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 ci.IsClosed() {
+		return false
+	}
+	if len(c.DryRunActiveLabels) > 0 && all(ci, c.DryRunActiveLabels) {
+		return true
+	}
+	return false
+}
+
+// DryRunSuccess returns true if the dry run succeeded. The allTrybotsSucceeded
+// parameter indicates whether or not all of the relevant trybots for this
+// change succeeded; it is unused if Config.DryRunUsesTryjobResults is false.
+func (c *Config) DryRunSuccess(ci *ChangeInfo, allTrybotsSucceeded bool) bool {
+	if c.CqRunning(ci) || c.DryRunRunning(ci) {
+		return false
+	}
+	if ci.IsClosed() {
+		return ci.IsMerged()
+	}
+	if len(c.DryRunSuccessLabels) > 0 && all(ci, c.DryRunSuccessLabels) {
+		return true
+	}
+	if len(c.DryRunFailureLabels) > 0 && all(ci, c.DryRunFailureLabels) {
+		return false
+	}
+	return c.DryRunUsesTryjobResults && allTrybotsSucceeded
+}
diff --git a/go/gerrit/config_test.go b/go/gerrit/config_test.go
new file mode 100644
index 0000000..bfaff91
--- /dev/null
+++ b/go/gerrit/config_test.go
@@ -0,0 +1,104 @@
+package gerrit
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"go.skia.org/infra/go/testutils/unittest"
+)
+
+func makeChangeInfo() *ChangeInfo {
+	return &ChangeInfo{
+		Labels: map[string]*LabelEntry{},
+	}
+}
+
+func testConfig(t *testing.T, cfg *Config) {
+	unittest.SmallTest(t)
+	ci := makeChangeInfo()
+
+	// Initial empty change. No CQ labels at all.
+	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))
+
+	// 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))
+
+	// CQ success.
+	if len(cfg.CqSuccessLabels) > 0 {
+		SetLabels(ci, cfg.CqSuccessLabels)
+	}
+	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))
+
+	// CQ failed.
+	if len(cfg.CqSuccessLabels) > 0 {
+		UnsetLabels(ci, cfg.CqSuccessLabels)
+	}
+	if len(cfg.CqFailureLabels) > 0 {
+		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))
+
+	// Dry run in progress.
+	if len(cfg.CqFailureLabels) > 0 {
+		UnsetLabels(ci, cfg.CqFailureLabels)
+	}
+	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))
+
+	// Dry run success.
+	if len(cfg.DryRunSuccessLabels) > 0 {
+		SetLabels(ci, cfg.DryRunSuccessLabels)
+	}
+	UnsetLabels(ci, cfg.DryRunActiveLabels)
+	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 failed.
+	if len(cfg.DryRunSuccessLabels) > 0 {
+		UnsetLabels(ci, cfg.DryRunSuccessLabels)
+	}
+	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))
+}
+
+func TestConfigAndroid(t *testing.T) {
+	testConfig(t, CONFIG_ANDROID)
+}
+
+func TestConfigANGLE(t *testing.T) {
+	testConfig(t, CONFIG_ANGLE)
+}
+
+func TestConfigChromium(t *testing.T) {
+	testConfig(t, CONFIG_CHROMIUM)
+}
diff --git a/go/gerrit/gerrit.go b/go/gerrit/gerrit.go
index 04ec3fa..9b501b1 100644
--- a/go/gerrit/gerrit.go
+++ b/go/gerrit/gerrit.go
@@ -66,6 +66,8 @@
 	AUTOSUBMIT_LABEL_NONE             = 0
 	AUTOSUBMIT_LABEL_SUBMIT           = 1
 	PRESUBMIT_READY_LABEL             = "Presubmit-Ready"
+	PRESUBMIT_READY_LABEL_NONE        = 0
+	PRESUBMIT_READY_LABEL_ENABLE      = 1
 	PRESUBMIT_VERIFIED_LABEL          = "Presubmit-Verified"
 	PRESUBMIT_VERIFIED_LABEL_REJECTED = -1
 	PRESUBMIT_VERIFIED_LABEL_RUNNING  = 0
@@ -157,9 +159,15 @@
 
 // IsClosed returns true iff the issue corresponding to the ChangeInfo is
 // abandoned or merged.
-func (c ChangeInfo) IsClosed() bool {
-	return (c.Status == CHANGE_STATUS_ABANDONED ||
-		c.Status == CHANGE_STATUS_MERGED)
+func (ci *ChangeInfo) IsClosed() bool {
+	return (ci.Status == CHANGE_STATUS_ABANDONED ||
+		ci.Status == CHANGE_STATUS_MERGED)
+}
+
+// IsMerged returns true iff the issue corresponding to the ChangeInfo is
+// merged.
+func (ci *ChangeInfo) IsMerged() bool {
+	return ci.Status == CHANGE_STATUS_MERGED
 }
 
 // Owner gathers the owner information of a ChangeInfo instance. Some fields omitted.
@@ -203,6 +211,7 @@
 	Abandon(context.Context, *ChangeInfo, string) error
 	AddComment(context.Context, *ChangeInfo, string) error
 	Approve(context.Context, *ChangeInfo, string) error
+	Config() *Config
 	CreateChange(context.Context, string, string, string, string) (*ChangeInfo, error)
 	DeleteChangeEdit(context.Context, *ChangeInfo) error
 	DeleteFile(context.Context, *ChangeInfo, string) error
@@ -223,11 +232,12 @@
 	PublishChangeEdit(context.Context, *ChangeInfo) error
 	RemoveFromCQ(context.Context, *ChangeInfo, string) error
 	Search(context.Context, int, ...*SearchTerm) ([]*ChangeInfo, error)
+	SelfApprove(context.Context, *ChangeInfo, string) error
 	SendToCQ(context.Context, *ChangeInfo, string) error
 	SendToDryRun(context.Context, *ChangeInfo, string) error
 	SetCommitMessage(context.Context, *ChangeInfo, string) error
 	SetReadyForReview(context.Context, *ChangeInfo) error
-	SetReview(context.Context, *ChangeInfo, string, map[string]interface{}, []string) error
+	SetReview(context.Context, *ChangeInfo, string, map[string]int, []string) error
 	SetTopic(context.Context, string, int64) error
 	TurnOnAuthenticatedGets()
 	Url(int64) string
@@ -235,6 +245,7 @@
 
 // Gerrit is an object used for interacting with the issue tracker.
 type Gerrit struct {
+	cfg                  *Config
 	client               *http.Client
 	BuildbucketClient    *buildbucket.Client
 	gitCookiesPath       string
@@ -247,6 +258,13 @@
 // instance will be in read-only mode and only return information available to
 // anonymous users.
 func NewGerrit(gerritUrl, gitCookiesPath string, client *http.Client) (*Gerrit, error) {
+	return NewGerritWithConfig(CONFIG_CHROMIUM, gerritUrl, gitCookiesPath, client)
+}
+
+// NewGerritWithConfig returns a new Gerrit instance which uses the given
+// Config. If gitCookiesPath is empty the instance will be in read-only mode and
+// only return information available to anonymous users.
+func NewGerritWithConfig(cfg *Config, gerritUrl, gitCookiesPath string, client *http.Client) (*Gerrit, error) {
 	parsedUrl, err := url.Parse(gerritUrl)
 	if err != nil {
 		return nil, skerr.Fmt("Unable to parse gerrit URL: %s", err)
@@ -262,6 +280,7 @@
 		client = httputils.NewTimeoutClient()
 	}
 	return &Gerrit{
+		cfg:               cfg,
 		url:               gerritUrl,
 		client:            client,
 		BuildbucketClient: buildbucket.NewClient(client),
@@ -270,6 +289,11 @@
 	}, nil
 }
 
+// Config returns the Config object used by this Gerrit.
+func (g *Gerrit) Config() *Config {
+	return g.cfg
+}
+
 // DefaultGitCookiesPath returns the default cookie file. The return value
 // can be used as the input to NewGerrit. If it cannot be retrieved an
 // error will be logged and the empty string is returned.
@@ -478,7 +502,7 @@
 // setReview calls the Set Review endpoint of the Gerrit API to add messages and/or set labels for
 // the latest patchset.
 // API documentation: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review
-func (g *Gerrit) SetReview(ctx context.Context, issue *ChangeInfo, message string, labels map[string]interface{}, reviewers []string) error {
+func (g *Gerrit) SetReview(ctx context.Context, issue *ChangeInfo, message string, labels map[string]int, reviewers []string) error {
 	postData := map[string]interface{}{
 		"message": message,
 		"labels":  labels,
@@ -499,35 +523,39 @@
 
 // AddComment adds a message to the issue.
 func (g *Gerrit) AddComment(ctx context.Context, issue *ChangeInfo, message string) error {
-	return g.SetReview(ctx, issue, message, map[string]interface{}{}, nil)
+	return g.SetReview(ctx, issue, message, map[string]int{}, nil)
 }
 
 // Utility methods for interacting with the COMMITQUEUE_LABEL.
 
 func (g *Gerrit) SendToDryRun(ctx context.Context, issue *ChangeInfo, message string) error {
-	return g.SetReview(ctx, issue, message, map[string]interface{}{COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_DRY_RUN}, nil)
+	return g.SetReview(ctx, issue, message, g.cfg.SetDryRunLabels, nil)
 }
 
 func (g *Gerrit) SendToCQ(ctx context.Context, issue *ChangeInfo, message string) error {
-	return g.SetReview(ctx, issue, message, map[string]interface{}{COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_SUBMIT}, nil)
+	return g.SetReview(ctx, issue, message, g.cfg.SetCqLabels, nil)
 }
 
 func (g *Gerrit) RemoveFromCQ(ctx context.Context, issue *ChangeInfo, message string) error {
-	return g.SetReview(ctx, issue, message, map[string]interface{}{COMMITQUEUE_LABEL: COMMITQUEUE_LABEL_NONE}, nil)
+	return g.SetReview(ctx, issue, message, g.cfg.NoCqLabels, nil)
 }
 
 // Utility methods for interacting with the CODEREVIEW_LABEL.
 
 func (g *Gerrit) Approve(ctx context.Context, issue *ChangeInfo, message string) error {
-	return g.SetReview(ctx, issue, message, map[string]interface{}{CODEREVIEW_LABEL: CODEREVIEW_LABEL_APPROVE}, nil)
+	return g.SetReview(ctx, issue, message, map[string]int{CODEREVIEW_LABEL: CODEREVIEW_LABEL_APPROVE}, nil)
 }
 
 func (g *Gerrit) NoScore(ctx context.Context, issue *ChangeInfo, message string) error {
-	return g.SetReview(ctx, issue, message, map[string]interface{}{CODEREVIEW_LABEL: CODEREVIEW_LABEL_NONE}, nil)
+	return g.SetReview(ctx, issue, message, map[string]int{CODEREVIEW_LABEL: CODEREVIEW_LABEL_NONE}, nil)
 }
 
 func (g *Gerrit) DisApprove(ctx context.Context, issue *ChangeInfo, message string) error {
-	return g.SetReview(ctx, issue, message, map[string]interface{}{CODEREVIEW_LABEL: CODEREVIEW_LABEL_DISAPPROVE}, nil)
+	return g.SetReview(ctx, issue, message, map[string]int{CODEREVIEW_LABEL: CODEREVIEW_LABEL_DISAPPROVE}, nil)
+}
+
+func (g *Gerrit) SelfApprove(ctx context.Context, issue *ChangeInfo, message string) error {
+	return g.SetReview(ctx, issue, message, g.cfg.SelfApproveLabels, nil)
 }
 
 // Abandon abandons the issue with the given message.
@@ -1107,3 +1135,46 @@
 func (g *Gerrit) DeleteChangeEdit(ctx context.Context, ci *ChangeInfo) error {
 	return g.delete(ctx, fmt.Sprintf("/a/changes/%s/edit", ci.Id))
 }
+
+// Set the given label on the ChangeInfo.
+func SetLabel(ci *ChangeInfo, key string, value int) {
+	labelEntry, ok := ci.Labels[key]
+	if !ok {
+		labelEntry = &LabelEntry{
+			All: []*LabelDetail{},
+		}
+		ci.Labels[key] = labelEntry
+	}
+	labelEntry.All = append(labelEntry.All, &LabelDetail{
+		Value: value,
+	})
+}
+
+// Set the given labels on the ChangeInfo.
+func SetLabels(ci *ChangeInfo, labels map[string]int) {
+	for key, value := range labels {
+		SetLabel(ci, key, value)
+	}
+}
+
+// Unset the given label on the ChangeInfo.
+func UnsetLabel(ci *ChangeInfo, key string, value int) {
+	labelEntry, ok := ci.Labels[key]
+	if !ok {
+		return
+	}
+	newEntries := make([]*LabelDetail, 0, len(labelEntry.All))
+	for _, details := range labelEntry.All {
+		if details.Value != value {
+			newEntries = append(newEntries, details)
+		}
+	}
+	labelEntry.All = newEntries
+}
+
+// Unset the given labels on the ChangeInfo.
+func UnsetLabels(ci *ChangeInfo, labels map[string]int) {
+	for key, value := range labels {
+		UnsetLabel(ci, key, value)
+	}
+}
diff --git a/go/gerrit/gerrit_test.go b/go/gerrit/gerrit_test.go
index 28bcfae..966c444 100644
--- a/go/gerrit/gerrit_test.go
+++ b/go/gerrit/gerrit_test.go
@@ -28,7 +28,7 @@
 func TestHasOpenDependency(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	dep, err := api.HasOpenDependency(context.TODO(), 52160, 1)
@@ -43,7 +43,7 @@
 func TestGerritOwnerModifiedSearch(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	t_delta := time.Now().Add(-10 * 24 * time.Hour)
@@ -66,7 +66,7 @@
 func TestGerritCommitSearch(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 	api.TurnOnAuthenticatedGets()
 
@@ -90,7 +90,7 @@
 func GerritPollerTest(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	cache := NewCodeReviewCache(api, 10*time.Second, 3)
@@ -106,7 +106,7 @@
 func TestGetPatch(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	patch, err := api.GetPatch(context.TODO(), 2370, "current")
@@ -130,7 +130,7 @@
 func TestAddComment(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	changeInfo, err := api.GetIssueProperties(context.TODO(), 2370)
@@ -142,7 +142,7 @@
 func TestSendToDryRun(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	// Send to dry run.
@@ -173,7 +173,7 @@
 func TestSendToCQ(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	// Send to CQ.
@@ -204,7 +204,7 @@
 func TestApprove(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	// Approve.
@@ -235,7 +235,7 @@
 func TestReadOnlyFailure(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, "", nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, "", nil)
 	require.NoError(t, err)
 
 	// Approve.
@@ -248,7 +248,7 @@
 func TestDisApprove(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	// DisApprove.
@@ -279,7 +279,7 @@
 func TestAbandon(t *testing.T) {
 	skipTestIfRequired(t)
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, "", nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, "", nil)
 	require.NoError(t, err)
 	c1 := ChangeInfo{
 		ChangeId: "Idb96a747c8446126f60fdf1adca361dbc2e539d5",
@@ -324,7 +324,7 @@
 
 	defer ts.Close()
 
-	api, err := NewGerrit(ts.URL, "", nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, ts.URL, "", nil)
 	files, err := api.Files(context.TODO(), 12345678, "current")
 	require.NoError(t, err)
 	require.Len(t, files, 4)
@@ -373,7 +373,7 @@
 
 	defer ts.Close()
 
-	api, err := NewGerrit(ts.URL, "", nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, ts.URL, "", nil)
 	files, err := api.GetFileNames(context.TODO(), 12345678, "current")
 	require.NoError(t, err)
 	require.Len(t, files, 4)
@@ -398,7 +398,7 @@
 		require.NoError(t, err)
 	}))
 	defer tsNoBinary.Close()
-	api, err := NewGerrit(tsNoBinary.URL, "", nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, tsNoBinary.URL, "", nil)
 	require.NoError(t, err)
 	isBinaryPatch, err := api.IsBinaryPatch(context.TODO(), 4649, "3")
 	require.NoError(t, err)
@@ -424,7 +424,7 @@
 		require.NoError(t, err)
 	}))
 	defer tsBinary.Close()
-	api, err = NewGerrit(tsBinary.URL, "", nil)
+	api, err = NewGerritWithConfig(CONFIG_CHROMIUM, tsBinary.URL, "", nil)
 	require.NoError(t, err)
 	isBinaryPatch, err = api.IsBinaryPatch(context.TODO(), 2370, "5")
 	require.NoError(t, err)
@@ -443,7 +443,7 @@
     Reviewed-on: https://skia-review.googlesource.com/549319
     Commit-Queue: John Doe <jdoe@example.com>
 	`
-	api, err := NewGerrit(GERRIT_SKIA_URL, "", nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, "", nil)
 	require.NoError(t, err)
 	issueID, err := api.ExtractIssueFromCommit(cmtMsg)
 	require.NoError(t, err)
@@ -460,7 +460,7 @@
 	revision := "91740d74af689d53b9fa4d172544e0d5620de9bd"
 	expectedParent := "aaab3c73575d5502ae345dd71cf8748c2070ffda"
 
-	api, err := NewGerrit(GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
+	api, err := NewGerritWithConfig(CONFIG_CHROMIUM, GERRIT_SKIA_URL, DefaultGitCookiesPath(), nil)
 	require.NoError(t, err)
 
 	commitInfo, err := api.GetCommit(context.TODO(), issueID, revision)
diff --git a/go/gerrit/mocks/GerritInterface.go b/go/gerrit/mocks/GerritInterface.go
index 3fd40f7..36a988f 100644
--- a/go/gerrit/mocks/GerritInterface.go
+++ b/go/gerrit/mocks/GerritInterface.go
@@ -59,6 +59,22 @@
 	return r0
 }
 
+// Config provides a mock function with given fields:
+func (_m *GerritInterface) Config() *gerrit.Config {
+	ret := _m.Called()
+
+	var r0 *gerrit.Config
+	if rf, ok := ret.Get(0).(func() *gerrit.Config); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*gerrit.Config)
+		}
+	}
+
+	return r0
+}
+
 // CreateChange provides a mock function with given fields: _a0, _a1, _a2, _a3, _a4
 func (_m *GerritInterface) CreateChange(_a0 context.Context, _a1 string, _a2 string, _a3 string, _a4 string) (*gerrit.ChangeInfo, error) {
 	ret := _m.Called(_a0, _a1, _a2, _a3, _a4)
@@ -428,6 +444,20 @@
 	return r0, r1
 }
 
+// SelfApprove provides a mock function with given fields: _a0, _a1, _a2
+func (_m *GerritInterface) SelfApprove(_a0 context.Context, _a1 *gerrit.ChangeInfo, _a2 string) error {
+	ret := _m.Called(_a0, _a1, _a2)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *gerrit.ChangeInfo, string) error); ok {
+		r0 = rf(_a0, _a1, _a2)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
 // SendToCQ provides a mock function with given fields: _a0, _a1, _a2
 func (_m *GerritInterface) SendToCQ(_a0 context.Context, _a1 *gerrit.ChangeInfo, _a2 string) error {
 	ret := _m.Called(_a0, _a1, _a2)
@@ -485,11 +515,11 @@
 }
 
 // SetReview provides a mock function with given fields: _a0, _a1, _a2, _a3, _a4
-func (_m *GerritInterface) SetReview(_a0 context.Context, _a1 *gerrit.ChangeInfo, _a2 string, _a3 map[string]interface{}, _a4 []string) error {
+func (_m *GerritInterface) SetReview(_a0 context.Context, _a1 *gerrit.ChangeInfo, _a2 string, _a3 map[string]int, _a4 []string) error {
 	ret := _m.Called(_a0, _a1, _a2, _a3, _a4)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, *gerrit.ChangeInfo, string, map[string]interface{}, []string) error); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *gerrit.ChangeInfo, string, map[string]int, []string) error); ok {
 		r0 = rf(_a0, _a1, _a2, _a3, _a4)
 	} else {
 		r0 = ret.Error(0)
diff --git a/go/gerrit/mocks/simple_mock_gerrit.go b/go/gerrit/mocks/simple_mock_gerrit.go
index 3287920..411eda9 100644
--- a/go/gerrit/mocks/simple_mock_gerrit.go
+++ b/go/gerrit/mocks/simple_mock_gerrit.go
@@ -17,6 +17,10 @@
 func (g *SimpleGerritInterface) Initialized() bool {
 	return true
 }
+func (g *SimpleGerritInterface) Config() *gerrit.Config {
+	args := g.Called()
+	return args.Get(0).(*gerrit.Config)
+}
 func (g *SimpleGerritInterface) TurnOnAuthenticatedGets() {
 }
 func (g *SimpleGerritInterface) Url(issueID int64) string {
@@ -37,7 +41,7 @@
 func (g *SimpleGerritInterface) GetPatch(ctx context.Context, issue int64, revision string) (string, error) {
 	return "", nil
 }
-func (g *SimpleGerritInterface) SetReview(ctx context.Context, issue *gerrit.ChangeInfo, message string, labels map[string]interface{}, reviewers []string) error {
+func (g *SimpleGerritInterface) SetReview(ctx context.Context, issue *gerrit.ChangeInfo, message string, labels map[string]int, reviewers []string) error {
 	return nil
 }
 func (g *SimpleGerritInterface) AddComment(ctx context.Context, issue *gerrit.ChangeInfo, message string) error {
@@ -61,6 +65,9 @@
 func (g *SimpleGerritInterface) DisApprove(ctx context.Context, issue *gerrit.ChangeInfo, message string) error {
 	return nil
 }
+func (g *SimpleGerritInterface) SelfApprove(ctx context.Context, issue *gerrit.ChangeInfo, message string) error {
+	return nil
+}
 func (g *SimpleGerritInterface) Abandon(ctx context.Context, issue *gerrit.ChangeInfo, message string) error {
 	return nil
 }
diff --git a/go/gerrit/testutils/testutils.go b/go/gerrit/testutils/testutils.go
index 56363b7..169d771 100644
--- a/go/gerrit/testutils/testutils.go
+++ b/go/gerrit/testutils/testutils.go
@@ -22,29 +22,33 @@
 // MockGerrit is a GerritInterface implementation which mocks out requests to
 // the server.
 type MockGerrit struct {
-	bb        *bb_testutils.MockClient
-	Gerrit    *gerrit.Gerrit
-	Mock      *mockhttpclient.URLMock
-	isAndroid bool
-	t         sktest.TestingT
+	bb     *bb_testutils.MockClient
+	Gerrit *gerrit.Gerrit
+	Mock   *mockhttpclient.URLMock
+	t      sktest.TestingT
 }
 
 // NewGerrit returns a mocked Gerrit instance.
-func NewGerrit(t sktest.TestingT, workdir string, isAndroid bool) *MockGerrit {
+func NewGerrit(t sktest.TestingT, workdir string) *MockGerrit {
+	return NewGerritWithConfig(t, gerrit.CONFIG_CHROMIUM, workdir)
+}
+
+// NewGerritWithConfig returns a mocked Gerrit instance which uses the given
+// Config.
+func NewGerritWithConfig(t sktest.TestingT, cfg *gerrit.Config, workdir string) *MockGerrit {
 	gitcookies := path.Join(workdir, "gitcookies_fake")
 	testutils.WriteFile(t, gitcookies, FAKE_GITCOOKIES)
 
 	mock := mockhttpclient.NewURLMock()
-	g, err := gerrit.NewGerrit(FAKE_GERRIT_URL, gitcookies, mock.Client())
+	g, err := gerrit.NewGerritWithConfig(cfg, FAKE_GERRIT_URL, gitcookies, mock.Client())
 	require.NoError(t, err)
 	bb := bb_testutils.NewMockClient(t)
 	g.BuildbucketClient = bb.Client
 	return &MockGerrit{
-		bb:        bb,
-		Gerrit:    g,
-		Mock:      mock,
-		isAndroid: isAndroid,
-		t:         t,
+		bb:     bb,
+		Gerrit: g,
+		Mock:   mock,
+		t:      t,
 	}
 }
 
@@ -97,27 +101,11 @@
 }
 
 func (g *MockGerrit) MockSetDryRun(ci *gerrit.ChangeInfo, msg string) {
-	if g.isAndroid {
-		g.MockPost(ci, msg, map[string]int{
-			gerrit.AUTOSUBMIT_LABEL: gerrit.AUTOSUBMIT_LABEL_NONE,
-		})
-	} else {
-		g.MockPost(ci, msg, map[string]int{
-			gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_DRY_RUN,
-		})
-	}
+	g.MockPost(ci, msg, g.Gerrit.Config().SetDryRunLabels)
 }
 
 func (g *MockGerrit) MockSetCQ(ci *gerrit.ChangeInfo, msg string) {
-	if g.isAndroid {
-		g.MockPost(ci, msg, map[string]int{
-			gerrit.AUTOSUBMIT_LABEL: gerrit.AUTOSUBMIT_LABEL_SUBMIT,
-		})
-	} else {
-		g.MockPost(ci, msg, map[string]int{
-			gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_SUBMIT,
-		})
-	}
+	g.MockPost(ci, msg, g.Gerrit.Config().SetCqLabels)
 }
 
 func (g *MockGerrit) Abandon(ci *gerrit.ChangeInfo, msg string) {
diff --git a/infra/bots/task_drivers/update_go_deps/update_go_deps.go b/infra/bots/task_drivers/update_go_deps/update_go_deps.go
index a2f19f7..114c5ea 100644
--- a/infra/bots/task_drivers/update_go_deps/update_go_deps.go
+++ b/infra/bots/task_drivers/update_go_deps/update_go_deps.go
@@ -117,9 +117,9 @@
 			if err != nil {
 				return err
 			}
-			var labels map[string]interface{}
+			var labels map[string]int
 			if !*local && rs.Issue == "" {
-				labels = map[string]interface{}{
+				labels = map[string]int{
 					gerrit.CODEREVIEW_LABEL:  gerrit.CODEREVIEW_LABEL_APPROVE,
 					gerrit.COMMITQUEUE_LABEL: gerrit.COMMITQUEUE_LABEL_SUBMIT,
 				}