| // 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 |
| } |