blob: f55cc104e49a6b763040b34ab646f4b1bdf6c677 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package gerrit
import (
"context"
"fmt"
"net/http"
"strings"
"go.chromium.org/luci/common/api/gerrit"
"go.chromium.org/luci/common/errors"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/common/retry/transient"
)
const commitQueueLabel = "Commit-Queue"
// gerritClientWrapper provides utilities for interacting with a Gerrit project.
type gerritClientWrapper struct {
client gerritpb.GerritClient
project string
}
// NewClient returns a gerritClientWrapper for a host and project.
func NewClient(ctx context.Context, host, project string, httpClient *http.Client) (*gerritClientWrapper, error) {
client, err := gerrit.NewRESTClient(httpClient, host, true)
if err != nil {
return nil, fmt.Errorf("failed to initialize gerrit client: %v", err)
}
return &gerritClientWrapper{client: client, project: project}, nil
}
// GetChange gets info for a Gerrit change.
func (c *gerritClientWrapper) GetChange(ctx context.Context, changeNum int64) (*gerritpb.ChangeInfo, error) {
resp, err := c.client.GetChange(ctx, &gerritpb.GetChangeRequest{
Number: changeNum,
Options: []gerritpb.QueryOption{gerritpb.QueryOption_ALL_REVISIONS},
})
if err != nil {
return nil, fmt.Errorf("failed to get change: %v", err)
}
return resp, nil
}
// CreateChange creates an empty Gerrit change.
func (c *gerritClientWrapper) CreateChange(ctx context.Context, subject, baseCommit string) (*gerritpb.ChangeInfo, error) {
resp, err := c.client.CreateChange(ctx, &gerritpb.CreateChangeRequest{
Project: c.project,
Ref: "refs/heads/master",
Subject: subject,
BaseCommit: baseCommit,
})
if err != nil {
return nil, fmt.Errorf("failed to create change: %v", err)
}
return resp, nil
}
// EditFile edits a single file for a Gerrit change.
func (c *gerritClientWrapper) EditFile(ctx context.Context, changeNum int64, filepath, content string) error {
if _, err := c.client.ChangeEditFileContent(ctx, &gerritpb.ChangeEditFileContentRequest{
Number: changeNum,
Project: c.project,
FilePath: filepath,
Content: []byte(content),
}); err != nil {
errorMsg := fmt.Sprintf("failed to edit %s: %v", filepath, err)
for _, errSubstr := range []string{"NotFound", "404", "409", "500"} {
// NotFound, 404, or 409 errors may be transient, as the change may not yet be visible.
// 500 errors is a catch-all error from Gerrit which may be transient as well.
if strings.Contains(err.Error(), errSubstr) {
return errors.New(errorMsg, transient.Tag)
}
}
return errors.New(errorMsg)
}
return nil
}
// PublishEdits publishes all pending edits on a Gerrit change.
func (c *gerritClientWrapper) PublishEdits(ctx context.Context, changeNum int64) error {
if _, err := c.client.ChangeEditPublish(ctx, &gerritpb.ChangeEditPublishRequest{
Number: changeNum,
Project: c.project,
}); err != nil {
return fmt.Errorf("failed to publish edits: %v", err)
}
return nil
}
// SetCQLabel sets the CQ+1/2 label on a Gerrit change.
func (c *gerritClientWrapper) SetCQLabel(ctx context.Context, changeNum int64, dryRun bool) error {
cqValue := int32(2)
if dryRun {
cqValue = int32(1)
}
changeRes, err := c.client.GetChange(ctx, &gerritpb.GetChangeRequest{
Number: changeNum,
Options: []gerritpb.QueryOption{gerritpb.QueryOption_CURRENT_REVISION},
})
if err != nil {
return fmt.Errorf("failed to get change: %v", err)
}
reviewRes, err := c.client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: changeNum,
Project: c.project,
RevisionId: changeRes.CurrentRevision,
Labels: map[string]int32{commitQueueLabel: cqValue},
})
if err != nil {
errorMsg := fmt.Sprintf("failed to set CQ label: %v", err)
for _, errSubstr := range []string{"NotFound", "409", "500"} {
// NotFound, 409, or 500 errors are all possibly transient when setting a label.
if strings.Contains(err.Error(), errSubstr) {
return errors.New(errorMsg, transient.Tag)
}
}
return errors.New(errorMsg)
}
setValue, ok := reviewRes.Labels[commitQueueLabel]
if !ok {
return fmt.Errorf("%s label was rejected", commitQueueLabel)
}
if setValue != cqValue {
return fmt.Errorf("%s label is %d; expected %d", commitQueueLabel, setValue, cqValue)
}
return nil
}
// CheckCQCompletion checks if a Gerrit change's CQ label is unset.
func (c *gerritClientWrapper) CheckCQCompletion(ctx context.Context, changeNum int64) error {
changeInfo, err := c.client.GetChange(ctx, &gerritpb.GetChangeRequest{
Number: changeNum,
Options: []gerritpb.QueryOption{gerritpb.QueryOption_LABELS},
})
if err != nil {
return fmt.Errorf("failed to get change: %v", err)
}
labelInfo, ok := changeInfo.Labels[commitQueueLabel]
if !ok {
return fmt.Errorf("%s label was not returned", commitQueueLabel)
}
// The CQ label will be unset eventually, so tag this error as transient.
if labelInfo.Value != 0 {
return errors.New(fmt.Sprintf("%s label is still set", commitQueueLabel), transient.Tag)
}
return nil
}
// AbandonChange abandons a Gerrit change.
func (c *gerritClientWrapper) AbandonChange(ctx context.Context, changeNum int64) error {
if _, err := c.client.AbandonChange(ctx, &gerritpb.AbandonChangeRequest{
Number: changeNum,
Project: c.project,
}); err != nil {
return fmt.Errorf("failed to abandon change: %v", err)
}
return nil
}