blob: 9f1af84ad91d8e077759fab9f733763a7a5316b6 [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"
"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"
"golang.org/x/time/rate"
)
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) {
i, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return code_review.ChangeList{}, invalidID
}
return c.getCL(ctx, i)
}
// getCL fetches a CL from gerrit and converts it into a code_review.ChangeList
func (c *CRSImpl) getCL(ctx context.Context, id int64) (code_review.ChangeList, error) {
// Respect the rate limit.
if err := c.rl.Wait(ctx); err != nil {
return code_review.ChangeList{}, skerr.Wrap(err)
}
cl, err := c.gClient.GetIssueProperties(ctx, id)
if err == gerrit.ErrNotFound {
return code_review.ChangeList{}, code_review.ErrNotFound
}
if err != nil {
return code_review.ChangeList{}, skerr.Wrapf(err, "fetching CL from gerrit with id %d", id)
}
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) {
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 patchsets for CL from gerrit with id %d", i)
}
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
}
// GetChangeListForCommit implements the code_review.Client interface.
func (c *CRSImpl) GetChangeListForCommit(ctx context.Context, commit *vcsinfo.LongCommit) (code_review.ChangeList, error) {
if commit == nil {
return code_review.ChangeList{}, 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.ChangeList{}, code_review.ErrNotFound
}
return c.getCL(ctx, i)
}
// System implements the code_review.Client interface.
func (c *CRSImpl) System() string {
return "gerrit"
}
// Make sure CRSImpl fulfills the code_review.Client interface.
var _ code_review.Client = (*CRSImpl)(nil)