| // Package gerrit_crs provides a client for Gold's interaction with |
| // the Gerrit code review system. |
| package gerrit_crs |
| |
| import ( |
| "context" |
| "errors" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/time/rate" |
| |
| "go.skia.org/infra/go/gerrit" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/vcsinfo" |
| "go.skia.org/infra/golden/go/code_review" |
| ) |
| |
| const ( |
| // These values are arbitrary guesses, roughly based on the values for gitiles. |
| maxQPS = rate.Limit(4.0) |
| maxBurst = 20 |
| |
| testContextKey = gerritCRSContextKey("gerrit_crs_is_testing") |
| ) |
| |
| type gerritCRSContextKey string |
| |
| type CRSImpl struct { |
| gClient gerrit.GerritInterface |
| rl *rate.Limiter |
| } |
| |
| func New(client gerrit.GerritInterface) *CRSImpl { |
| return &CRSImpl{ |
| gClient: client, |
| rl: rate.NewLimiter(maxQPS, maxBurst), |
| } |
| } |
| |
| var invalidID = errors.New("invalid id - must be integer") |
| |
| // LoggedInAs returns the email address of the logged in user. |
| func (c *CRSImpl) LoggedInAs(ctx context.Context) (string, error) { |
| if ctx.Value(testContextKey) != nil { |
| return "test_crs_user@example.com", nil |
| } |
| s, err := c.gClient.GetUserEmail(ctx) |
| if err != nil { |
| return "", skerr.Wrap(err) |
| } |
| return s, nil |
| } |
| |
| // GetChangelist implements the code_review.Client interface. |
| func (c *CRSImpl) GetChangelist(ctx context.Context, id string) (code_review.Changelist, error) { |
| cl, err := c.getGerritCL(ctx, id) |
| if err != nil { |
| return code_review.Changelist{}, err |
| } |
| return code_review.Changelist{ |
| SystemID: strconv.FormatInt(cl.Issue, 10), |
| Owner: cl.Owner.Email, |
| Status: statusToEnum(cl.Status), |
| Subject: cl.Subject, |
| Updated: cl.Updated, |
| }, nil |
| } |
| |
| // statusToEnum converts a gerrit status string into a CLStatus enum. |
| func statusToEnum(g string) code_review.CLStatus { |
| switch g { |
| case gerrit.ChangeStatusNew: |
| return code_review.Open |
| case gerrit.ChangeStatusAbandoned: |
| return code_review.Abandoned |
| case gerrit.ChangeStatusMerged: |
| return code_review.Landed |
| } |
| return code_review.Open |
| } |
| |
| // GetPatchset implements the code_review.Client interface. |
| func (c *CRSImpl) GetPatchset(ctx context.Context, clID, psID string, psOrder int) (code_review.Patchset, error) { |
| cl, err := c.getGerritCL(ctx, clID) |
| if err != nil { |
| return code_review.Patchset{}, err |
| } |
| for _, p := range cl.Patchsets { |
| if p.ID == psID || int(p.Number) == psOrder { |
| return code_review.Patchset{ |
| SystemID: p.ID, |
| ChangelistID: clID, |
| Order: int(p.Number), |
| GitHash: p.ID, |
| Created: p.Created, |
| }, nil |
| } |
| } |
| return code_review.Patchset{}, code_review.ErrNotFound |
| } |
| |
| // GetChangelistIDForCommit implements the code_review.Client interface. |
| func (c *CRSImpl) GetChangelistIDForCommit(_ context.Context, commit *vcsinfo.LongCommit) (string, error) { |
| if commit == nil { |
| return "", skerr.Fmt("commit cannot be nil") |
| } |
| i, err := c.gClient.ExtractIssueFromCommit(commit.Body) |
| if err != nil { |
| sklog.Debugf("Could not find gerrit issue in %q: %s", commit.Body, err) |
| return "", code_review.ErrNotFound |
| } |
| return strconv.FormatInt(i, 10), nil |
| } |
| |
| // CommentOn implements the code_review.Client interface. |
| func (c *CRSImpl) CommentOn(ctx context.Context, clID, message string) error { |
| sklog.Infof("Commenting on Gerrit CL %s with message %q", clID, message) |
| cl, err := c.getGerritCL(ctx, clID) |
| if err != nil { |
| if err == gerrit.ErrNotFound || strings.Contains(err.Error(), "404 Not Found") { |
| return code_review.ErrNotFound |
| } |
| return skerr.Wrap(err) |
| } |
| err = c.gClient.AddComment(ctx, cl, message) |
| if err != nil { |
| if err == gerrit.ErrNotFound || strings.Contains(err.Error(), "404 Not Found") { |
| return code_review.ErrNotFound |
| } |
| return skerr.Wrap(err) |
| } |
| return nil |
| } |
| |
| // System implements the code_review.Client interface. |
| func (c *CRSImpl) System() string { |
| return "gerrit" |
| } |
| |
| func (c *CRSImpl) getGerritCL(ctx context.Context, clID string) (*gerrit.ChangeInfo, error) { |
| i, err := strconv.ParseInt(clID, 10, 64) |
| if err != nil { |
| return nil, invalidID |
| } |
| // Respect the rate limit. |
| if err := c.rl.Wait(ctx); err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| cl, err := c.gClient.GetIssueProperties(ctx, i) |
| if err == gerrit.ErrNotFound { |
| return nil, code_review.ErrNotFound |
| } |
| if err != nil { |
| return nil, skerr.Wrapf(err, "fetching CL from gerrit with id %d", i) |
| } |
| return cl, nil |
| } |
| |
| // TestContext returns a context that will cause certain APIs to return stub data. At present, |
| // those APIs are LoggedInAs. |
| func TestContext(ctx context.Context) context.Context { |
| return context.WithValue(ctx, testContextKey, testContextKey) |
| } |
| |
| // Make sure CRSImpl fulfills the code_review.Client interface. |
| var _ code_review.Client = (*CRSImpl)(nil) |