blob: 95860e1dd26ad352c2e82cfe2be8eb1e447323ea [file] [log] [blame]
// Package gerrit_crs provides a client for Gold's interaction with
// the Gerrit code review system.
package gerrit_crs
import (
"context"
"errors"
"sort"
"strconv"
"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
)
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")
// 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.CHANGE_STATUS_NEW:
return code_review.Open
case gerrit.CHANGE_STATUS_ABANDONED:
return code_review.Abandoned
case gerrit.CHANGE_STATUS_MERGED:
return code_review.Landed
}
return code_review.Open
}
// GetPatchSets implements the code_review.Client interface.
func (c *CRSImpl) GetPatchSets(ctx context.Context, clID string) ([]code_review.PatchSet, error) {
cl, err := c.getGerritCL(ctx, clID)
if err != nil {
return nil, err
}
var xps []code_review.PatchSet
for _, p := range cl.Patchsets {
xps = append(xps, code_review.PatchSet{
SystemID: p.ID,
ChangeListID: clID,
Order: int(p.Number),
GitHash: p.ID,
})
}
// Gerrit probably returns them in order, but this ensures it.
sort.Slice(xps, func(i, j int) bool {
return xps[i].Order < xps[j].Order
})
return xps, nil
}
// 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 {
cl, err := c.getGerritCL(ctx, clID)
if err != nil {
return err
}
return skerr.Wrapf(c.gClient.AddComment(ctx, cl, message), "commenting on gerrit CL %s", clID)
}
// 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
}
// Make sure CRSImpl fulfills the code_review.Client interface.
var _ code_review.Client = (*CRSImpl)(nil)