blob: f8a283346981be6e37bcaad3992ece1f1e8caacb [file] [log] [blame]
package codereview
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/cenkalti/backoff"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
const (
// The tag that SkCQ will apply to all comments published by it.
AutogeneratedCommentTag = "autogenerated:skcq"
// The number of changes to search in Gerrit for CQ/dry-runs.
GerritOpenChangesNum = 100
)
// gerritCodeReview implements the CodeReview interface.
type gerritCodeReview struct {
gerritClient gerrit.GerritInterface
cfg *gerrit.Config
}
// NewGerrit returns a gerritCodeReview instance.
func NewGerrit(httpClient *http.Client, cfg *gerrit.Config, gerritURL string) (CodeReview, error) {
g, err := gerrit.NewGerritWithConfig(cfg, gerritURL, httpClient)
if err != nil {
return nil, err
}
return &gerritCodeReview{
gerritClient: g,
cfg: cfg,
}, nil
}
// AddComment implements the CodeReview interface.
func (gc *gerritCodeReview) AddComment(ctx context.Context, ci *gerrit.ChangeInfo, comment string, notify NotifyOption, notifyReason string) error {
// Convert SkCQ's NotifyOption to Gerrit's NotifyOption.
var notifyOption gerrit.NotifyOption
var notifyDetails gerrit.NotifyDetails
var attentionSets []*gerrit.AttentionSetInput
switch notify {
case NotifyNone:
notifyOption = gerrit.NotifyNone
case NotifyOwnerTriggerers:
// Notify the CQ voters + CL owner.
notifyOption = gerrit.NotifyOwner
notifyAccounts := gc.GetCQVoters(ctx, ci)
if !util.In(ci.Owner.Email, notifyAccounts) {
notifyAccounts = append(notifyAccounts, ci.Owner.Email)
}
notifyDetails = gerrit.NotifyDetails{
gerrit.RecipientTo: &gerrit.NotifyInfo{
Accounts: notifyAccounts,
},
}
// Create the attention set.
for _, na := range notifyAccounts {
attentionSets = append(attentionSets, &gerrit.AttentionSetInput{User: na, Reason: notifyReason})
}
case NotifyOwnerReviewersTriggerers:
// Notify the CQ voters + CL owner + CL reviewers.
notifyOption = gerrit.NotifyOwnerReviewers
notifyAccounts := gc.GetCQVoters(ctx, ci)
if !util.In(ci.Owner.Email, notifyAccounts) {
notifyAccounts = append(notifyAccounts, ci.Owner.Email)
}
for _, r := range ci.Reviewers.Reviewer {
notifyAccounts = append(notifyAccounts, r.Email)
}
notifyDetails = gerrit.NotifyDetails{
gerrit.RecipientTo: &gerrit.NotifyInfo{
Accounts: notifyAccounts,
},
}
}
// SetReview sometimes returns 404s from Gerrit and retries seems to
// resolve it. Maybe happens when a Gerrit frontend index is stale.
//
// Example flow with the used backoff values and assuming we go over
// MaxElapsedTime on the 4th try:
// request# retry_interval randomized_interval
// 1 1 [0.5, 1.5]
// 2 1.5 [0.75, 2.25]
// 3 2.25 [1.125, 3.375]
// 4 3.375 backoff.Stop
exp := &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: 0.5,
Multiplier: 1.5,
MaxInterval: 2 * time.Second,
MaxElapsedTime: 5 * time.Second,
Clock: backoff.SystemClock,
}
addCommentFunc := func() error {
return gc.gerritClient.SetReview(ctx, ci, comment, map[string]int{}, []string{}, notifyOption, notifyDetails, AutogeneratedCommentTag, 0, attentionSets)
}
return skerr.Wrap(backoff.Retry(addCommentFunc, exp))
}
// GetChangeRef implements the CodeReview interface.
func (gc *gerritCodeReview) GetChangeRef(ci *gerrit.ChangeInfo) string {
return fmt.Sprintf("%s%02d/%d/%d", gerrit.ChangeRefPrefix, ci.Issue%100, ci.Issue, gc.GetLatestPatchSetID(ci))
}
// GetCommitAuthor implements the CodeReview interface.
func (gc *gerritCodeReview) GetCommitAuthor(ctx context.Context, issue int64, revision string) (string, error) {
commitInfo, err := gc.gerritClient.GetCommit(ctx, issue, revision)
if err != nil {
return "", err
}
if commitInfo.Author == nil {
return "", errors.New("commitInfo.Author was nil")
}
return commitInfo.Author.Email, nil
}
// GetCommitMessage implements the CodeReview interface.
func (gc *gerritCodeReview) GetCommitMessage(ctx context.Context, issue int64) (string, error) {
commitInfo, err := gc.gerritClient.GetCommit(ctx, issue, "current")
if err != nil {
return "", err
}
return commitInfo.Message, nil
}
// GetEarliestEquivalentPatchSetID implements the CodeReview interface.
func (gc *gerritCodeReview) GetEarliestEquivalentPatchSetID(ci *gerrit.ChangeInfo) int64 {
nonTrivial := ci.GetNonTrivialPatchSets()
return nonTrivial[len(nonTrivial)-1].Number
}
// GetEquivalentPatchSetIDs implements the CodeReview interface.
func (gc *gerritCodeReview) GetEquivalentPatchSetIDs(ci *gerrit.ChangeInfo, patchsetID int64) []int64 {
ret := []int64{}
startChecking := false
for i := len(ci.Patchsets) - 1; i >= 0; i-- {
patchSet := ci.Patchsets[i]
if patchSet.Number == patchsetID {
startChecking = true
}
if startChecking {
// Keep adding till we reach a CODE_CHANGE and then break out.
ret = append(ret, patchSet.Number)
if !util.In(patchSet.Kind, gerrit.TrivialPatchSetKinds) {
break
}
}
}
return ret
}
// GetFileNames implements the CodeReview interface.
func (gc *gerritCodeReview) GetFileNames(ctx context.Context, ci *gerrit.ChangeInfo) ([]string, error) {
return gc.gerritClient.GetFileNames(ctx, ci.Issue, strconv.FormatInt(gc.GetLatestPatchSetID(ci), 10))
}
// GetIssueProperties implements the CodeReview interface.
func (gc *gerritCodeReview) GetIssueProperties(ctx context.Context, issue int64) (*gerrit.ChangeInfo, error) {
return gc.gerritClient.GetIssueProperties(ctx, issue)
}
// GetLatestPatchSetID implements the CodeReview interface.
func (gc *gerritCodeReview) GetLatestPatchSetID(ci *gerrit.ChangeInfo) int64 {
patchsetIDs := ci.GetPatchsetIDs()
return patchsetIDs[len(patchsetIDs)-1]
}
// GetSubmittedTogether implements the CodeReview interface.
func (gc *gerritCodeReview) GetSubmittedTogether(ctx context.Context, ci *gerrit.ChangeInfo) ([]*gerrit.ChangeInfo, error) {
changes, nonVisibleChanges, err := gc.gerritClient.SubmittedTogether(ctx, ci)
if err != nil {
return nil, skerr.Wrapf(err, "Could not get the list of submitted together changes")
}
if nonVisibleChanges > 0 {
return nil, skerr.Fmt("The SkCQ service account does not have access to view some submitted together changes of %d", ci.Issue)
}
// Filter out the specified ChangeInfo and return fully filled-in objects.
fullFilteredChanges := []*gerrit.ChangeInfo{}
for _, c := range changes {
if c.Id != ci.Id {
fullCI, err := gc.gerritClient.GetIssueProperties(ctx, c.Issue)
if err != nil {
return nil, skerr.Fmt("Could not get full issue properties of %d", c.Issue)
}
fullFilteredChanges = append(fullFilteredChanges, fullCI)
}
}
return fullFilteredChanges, nil
}
// IsCQ implements the CodeReview interface.
func (gc *gerritCodeReview) IsCQ(ctx context.Context, ci *gerrit.ChangeInfo) bool {
return gc.cfg.CqRunning(ci)
}
// IsDryRun implements the CodeReview interface.
func (gc *gerritCodeReview) IsDryRun(ctx context.Context, ci *gerrit.ChangeInfo) bool {
return gc.cfg.DryRunRunning(ci)
}
// RemoveFromCQ implements the CodeReview interface.
func (gc *gerritCodeReview) RemoveFromCQ(ctx context.Context, ci *gerrit.ChangeInfo, comment string, notifyReason string) {
// Delete all CQ+1/CQ+2 votes.
le := ci.Labels[gerrit.LabelCommitQueue]
for _, labelDetail := range le.All {
if labelDetail.Value > 0 {
if err := gc.gerritClient.DeleteVote(ctx, ci.Issue, gerrit.LabelCommitQueue, labelDetail.AccountID, gerrit.NotifyNone, true); err != nil {
sklog.Errorf("[%d] Could not remove from CQ: %s", ci.Issue, err)
return
}
}
}
// Update the change with a comment.
if err := gc.AddComment(ctx, ci, comment, NotifyOwnerTriggerers, notifyReason); err != nil {
sklog.Errorf("[%d] Could not add a comment \"%s\": %s", ci.Issue, comment, err)
return
}
}
// Search implements the CodeReview interface.
func (gc *gerritCodeReview) Search(ctx context.Context) ([]*gerrit.ChangeInfo, error) {
// Searching for open issues term will apply for both CQ and dry-run
// searches.
openSearchTerm := gerrit.SearchStatus(gerrit.ChangeStatusOpen)
// Search for CQ runs.
searchTermsCQ := []*gerrit.SearchTerm{openSearchTerm}
for label, val := range gc.gerritClient.Config().SetCqLabels {
searchTermsCQ = append(searchTermsCQ, gerrit.SearchLabel(label, strconv.Itoa(val)))
}
changesCQ, err := gc.gerritClient.Search(ctx, GerritOpenChangesNum, true, searchTermsCQ...)
if err != nil {
return nil, skerr.Wrapf(err, "Could not search for CQ issues")
}
// Search for dry-runs.
searchTermsDryRun := []*gerrit.SearchTerm{openSearchTerm}
for label, val := range gc.gerritClient.Config().SetDryRunLabels {
searchTermsDryRun = append(searchTermsDryRun, gerrit.SearchLabel(label, strconv.Itoa(val)))
}
changesDryRun, err := gc.gerritClient.Search(ctx, GerritOpenChangesNum, true, searchTermsDryRun...)
if err != nil {
return nil, skerr.Wrapf(err, "Could not search for dry-run issues")
}
// Append them to a single slice.
matchingChanges := append(changesCQ, changesDryRun...)
// Loop through the matching changes to make sure we remove duplications.
alreadySeen := map[int64]bool{}
filteredChanges := []*gerrit.ChangeInfo{}
for _, ci := range matchingChanges {
if _, ok := alreadySeen[ci.Issue]; ok {
continue
}
alreadySeen[ci.Issue] = true
filteredChanges = append(filteredChanges, ci)
}
return filteredChanges, nil
}
// SetReadyForReview implements the CodeReview interface.
func (gc *gerritCodeReview) SetReadyForReview(ctx context.Context, ci *gerrit.ChangeInfo) error {
return gc.gerritClient.SetReadyForReview(ctx, ci)
}
// Submit implements the CodeReview interface.
func (gc *gerritCodeReview) Submit(ctx context.Context, ci *gerrit.ChangeInfo) error {
return gc.gerritClient.Submit(ctx, ci)
}
// Url implements the CodeReview interface.
func (gc *gerritCodeReview) Url(issueID int64) string {
return gc.gerritClient.Url(issueID)
}
// GetRepoUrl implements the CodeReview interface.
func (gc *gerritCodeReview) GetRepoUrl(ci *gerrit.ChangeInfo) string {
return gc.gerritClient.GetRepoUrl() + "/" + ci.Project
}
// GetCQVoters implements the CodeReview interface.
func (gc *gerritCodeReview) GetCQVoters(ctx context.Context, ci *gerrit.ChangeInfo) []string {
// Find which CQ label value we are looking for.
labelValue := gerrit.LabelCommitQueueDryRun
if gc.IsCQ(ctx, ci) {
labelValue = gerrit.LabelCommitQueueSubmit
}
// Fing all voters of the CQ label value.
voters := []string{}
if val, ok := ci.Labels[gerrit.LabelCommitQueue]; ok {
for _, ld := range val.All {
if ld.Value == labelValue {
voters = append(voters, ld.Email)
}
}
}
return voters
}