go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/internal/gerrit/gerrit.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package gerrit contains logic for interacting with Gerrit
    16  package gerrit
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"net/http"
    22  	"time"
    23  
    24  	"go.chromium.org/luci/common/api/gerrit"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/logging"
    27  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    28  	"go.chromium.org/luci/server/auth"
    29  )
    30  
    31  // The options to use when querying Gerrit for changes
    32  var queryOptions = []gerritpb.QueryOption{
    33  	gerritpb.QueryOption_LABELS,
    34  	gerritpb.QueryOption_CURRENT_REVISION,
    35  	gerritpb.QueryOption_CURRENT_COMMIT,
    36  	gerritpb.QueryOption_DETAILED_ACCOUNTS,
    37  	gerritpb.QueryOption_MESSAGES,
    38  	gerritpb.QueryOption_CHANGE_ACTIONS,
    39  	gerritpb.QueryOption_SKIP_MERGEABLE,
    40  	gerritpb.QueryOption_CHECK,
    41  }
    42  
    43  // mockedGerritClientKey is the context key to indicate using mocked
    44  // Gerrit client in tests
    45  var mockedGerritClientKey = "mock Gerrit client"
    46  
    47  // Client is the client to communicate with Gerrit
    48  // It wraps a gerritpb.GerritClient
    49  type Client struct {
    50  	gerritClient gerritpb.GerritClient
    51  	host         string
    52  }
    53  
    54  func newGerritClient(ctx context.Context, host string) (gerritpb.GerritClient, error) {
    55  	if mockClient, ok := ctx.Value(&mockedGerritClientKey).(*gerritpb.MockGerritClient); ok {
    56  		// return a mock Gerrit client for tests
    57  		return mockClient, nil
    58  	}
    59  
    60  	t, err := auth.GetRPCTransport(ctx, auth.AsSelf, auth.WithScopes(gerrit.OAuthScope))
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	return gerrit.NewRESTClient(&http.Client{Transport: t}, host, true)
    66  }
    67  
    68  // NewClient creates a client to communicate with Gerrit
    69  func NewClient(ctx context.Context, host string) (*Client, error) {
    70  	client, err := newGerritClient(ctx, host)
    71  	if err != nil {
    72  		return nil, errors.Annotate(err, "error making Gerrit client for host %s", host).Err()
    73  	}
    74  
    75  	return &Client{
    76  		gerritClient: client,
    77  		host:         host,
    78  	}, nil
    79  }
    80  
    81  // Host returns the Gerrit host string
    82  func (c *Client) Host(ctx context.Context) string {
    83  	return c.host
    84  }
    85  
    86  // queryChanges gets the info for corresponding change(s) given the query string.
    87  func (c *Client) queryChanges(ctx context.Context, query string) ([]*gerritpb.ChangeInfo, error) {
    88  	req := &gerritpb.ListChangesRequest{
    89  		Query:   query,
    90  		Options: queryOptions,
    91  	}
    92  
    93  	res, err := c.gerritClient.ListChanges(ctx, req)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	return res.Changes, nil
    99  }
   100  
   101  // GetChange gets the corresponding change info given the commit ID.
   102  // This function returns an error if none or more than 1 changes are returned
   103  // by Gerrit.
   104  func (c *Client) GetChange(ctx context.Context, project string, commitID string) (*gerritpb.ChangeInfo, error) {
   105  	query := fmt.Sprintf("project:\"%s\" commit:\"%s\"", project, commitID)
   106  	changes, err := c.queryChanges(ctx, query)
   107  	if err != nil {
   108  		return nil, errors.Annotate(err, "error getting change from Gerrit host %s using query %s",
   109  			c.host, query).Err()
   110  	}
   111  
   112  	if len(changes) == 0 {
   113  		return nil, fmt.Errorf("no change found from Gerrit host %s using query %s",
   114  			c.host, query,
   115  		)
   116  	}
   117  
   118  	if len(changes) > 1 {
   119  		return nil, fmt.Errorf("multiple changes found from Gerrit host %s using query %s",
   120  			c.host, query,
   121  		)
   122  	}
   123  
   124  	return changes[0], nil
   125  }
   126  
   127  // RefetchChange queries Gerrit for the given change, and returns the latest
   128  // state of the change
   129  func (c *Client) RefetchChange(ctx context.Context, change *gerritpb.ChangeInfo) (*gerritpb.ChangeInfo, error) {
   130  	req := &gerritpb.GetChangeRequest{
   131  		Project: change.Project,
   132  		Number:  change.Number,
   133  		Options: queryOptions,
   134  	}
   135  
   136  	res, err := c.gerritClient.GetChange(ctx, req)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	return res, nil
   142  }
   143  
   144  // GetReverts gets the corresponding revert(s) for the given change.
   145  func (c *Client) GetReverts(ctx context.Context, change *gerritpb.ChangeInfo) ([]*gerritpb.ChangeInfo, error) {
   146  	query := fmt.Sprintf("project:\"%s\" revertof:%d", change.Project, change.Number)
   147  	changes, err := c.queryChanges(ctx, query)
   148  	if err != nil {
   149  		return nil, errors.Annotate(err, "error getting reverts of a change from Gerrit host %s using query %s",
   150  			c.host, query,
   151  		).Err()
   152  	}
   153  
   154  	return changes, nil
   155  }
   156  
   157  // HasDependency returns whether the change has another merged change depending on it
   158  func (c *Client) HasDependency(ctx context.Context, change *gerritpb.ChangeInfo) (bool, error) {
   159  	relatedChanges, err := c.getRelatedChanges(ctx, change)
   160  	if err != nil {
   161  		return false, errors.Annotate(err, "failed checking dependency").Err()
   162  	}
   163  
   164  	for _, relatedChange := range relatedChanges {
   165  		if relatedChange.Status == gerritpb.ChangeStatus_MERGED {
   166  			// relatedChange here is the newest merged. If relatedChange != change,
   167  			// then there is a merged dependency
   168  			return relatedChange.Project != change.Project ||
   169  				relatedChange.Number != change.Number, nil
   170  		}
   171  	}
   172  
   173  	// none of the related changes are merged, so no merged dependencies
   174  	return false, nil
   175  }
   176  
   177  // CreateRevert creates a revert change in Gerrit for the specified change.
   178  func (c *Client) CreateRevert(ctx context.Context, change *gerritpb.ChangeInfo, message string) (*gerritpb.ChangeInfo, error) {
   179  	logging.Debugf(ctx, "gerrit Client.CreateRevert message: '%s'", message)
   180  	req := &gerritpb.RevertChangeRequest{
   181  		Project: change.Project,
   182  		Number:  change.Number,
   183  		Message: message,
   184  	}
   185  
   186  	// Set timeout for creating a revert
   187  	waitCtx, cancel := context.WithTimeout(ctx, time.Minute*1)
   188  	defer cancel()
   189  
   190  	res, err := c.gerritClient.RevertChange(waitCtx, req)
   191  	if err != nil {
   192  		return nil, errors.Annotate(err, "error creating revert change on Gerrit host %s for change %s~%d",
   193  			c.host, req.Project, req.Number).Err()
   194  	}
   195  
   196  	return res, nil
   197  }
   198  
   199  // AddComment adds the given message as a review comment on a change
   200  func (c *Client) AddComment(ctx context.Context, change *gerritpb.ChangeInfo, message string) (*gerritpb.ReviewResult, error) {
   201  	req := c.createSetReviewRequest(ctx, change, message)
   202  	res, err := c.setReview(ctx, req)
   203  	if err != nil {
   204  		return nil, errors.Annotate(err, "error adding comment").Err()
   205  	}
   206  
   207  	return res, nil
   208  }
   209  
   210  // SendForReview adds the emails as reviewers for the
   211  // change, and sets the change to be ready for review
   212  func (c *Client) SendForReview(ctx context.Context, change *gerritpb.ChangeInfo, message string,
   213  	reviewerEmails []string, ccEmails []string) (*gerritpb.ReviewResult, error) {
   214  	req := c.createSetReviewRequest(ctx, change, message)
   215  
   216  	// Add reviewer and CC emails to the change
   217  	reviewerCount := len(reviewerEmails)
   218  	reviewerInputs := make([]*gerritpb.ReviewerInput, reviewerCount+len(ccEmails))
   219  	for i, email := range reviewerEmails {
   220  		reviewerInputs[i] = &gerritpb.ReviewerInput{
   221  			Reviewer: email,
   222  			State:    gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_REVIEWER,
   223  		}
   224  	}
   225  	for i, email := range ccEmails {
   226  		reviewerInputs[reviewerCount+i] = &gerritpb.ReviewerInput{
   227  			Reviewer: email,
   228  			State:    gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_CC,
   229  		}
   230  	}
   231  	req.Reviewers = reviewerInputs
   232  
   233  	res, err := c.setReview(ctx, req)
   234  	if err != nil {
   235  		return nil, errors.Annotate(err, "error sending for review").Err()
   236  	}
   237  	return res, nil
   238  }
   239  
   240  // CommitRevert bot-commits the revert change. The change must be a pure revert;
   241  // if not, this function does not attempt to commit the change and returns an error.
   242  func (c *Client) CommitRevert(ctx context.Context, change *gerritpb.ChangeInfo,
   243  	message string, ccEmails []string) (*gerritpb.ReviewResult, error) {
   244  	// Check the change is a pure revert
   245  	isRevert, err := c.isPureRevert(ctx, change)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	if !isRevert {
   251  		return nil, fmt.Errorf(
   252  			"failed to commit change on Gerrit host %s - change %s~%d is not a pure revert",
   253  			c.host, change.Project, change.Number)
   254  	}
   255  
   256  	req := c.createSetReviewRequest(ctx, change, message)
   257  
   258  	// Add CC emails to the change
   259  	reviewerInputs := make([]*gerritpb.ReviewerInput, len(ccEmails))
   260  	for i, email := range ccEmails {
   261  		reviewerInputs[i] = &gerritpb.ReviewerInput{
   262  			Reviewer: email,
   263  			State:    gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_CC,
   264  		}
   265  	}
   266  	req.Reviewers = reviewerInputs
   267  
   268  	// Specify the labels required to submit the change to CQ
   269  	req.Labels = map[string]int32{
   270  		"Owners-Override": 1,
   271  		"Bot-Commit":      1,
   272  		"Commit-Queue":    2,
   273  	}
   274  
   275  	res, err := c.setReview(ctx, req)
   276  	if err != nil {
   277  		return nil, errors.Annotate(err, "error committing").Err()
   278  	}
   279  
   280  	return res, nil
   281  }
   282  
   283  // createSetReviewRequest is a helper to create a basic SetReviewRequest
   284  func (c *Client) createSetReviewRequest(ctx context.Context, change *gerritpb.ChangeInfo,
   285  	message string) *gerritpb.SetReviewRequest {
   286  	return &gerritpb.SetReviewRequest{
   287  		Project:    change.Project,
   288  		Number:     change.Number,
   289  		RevisionId: "current",
   290  		Message:    message,
   291  	}
   292  }
   293  
   294  // getRelatedChanges is a helper to call the Gerrit client GetRelatedChanges function
   295  func (c *Client) getRelatedChanges(ctx context.Context, change *gerritpb.ChangeInfo) ([]*gerritpb.GetRelatedChangesResponse_ChangeAndCommit, error) {
   296  	req := &gerritpb.GetRelatedChangesRequest{
   297  		Project:    change.Project,
   298  		Number:     change.Number,
   299  		RevisionId: "current",
   300  	}
   301  
   302  	res, err := c.gerritClient.GetRelatedChanges(ctx, req)
   303  	if err != nil {
   304  		return nil, errors.Annotate(err, "failed getting related changes from Gerrit host %s for change %s~%d",
   305  			c.host, req.Project, req.Number,
   306  		).Err()
   307  	}
   308  
   309  	// Changes are sorted by git commit order, newest to oldest.
   310  	// Empty if there are no related changes. See:
   311  	// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-changes-info
   312  	return res.Changes, nil
   313  }
   314  
   315  // isPureRevert is a helper to call the Gerrit client GetPureRevert function,
   316  // and returns whether the change is a pure revert of the change
   317  // referenced in its "revertOf" field. See:
   318  // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-pure-revert
   319  func (c *Client) isPureRevert(ctx context.Context, change *gerritpb.ChangeInfo) (bool, error) {
   320  	req := &gerritpb.GetPureRevertRequest{
   321  		Project: change.Project,
   322  		Number:  change.Number,
   323  	}
   324  
   325  	res, err := c.gerritClient.GetPureRevert(ctx, req)
   326  	if err != nil {
   327  		return false, errors.Annotate(err,
   328  			"error querying Gerrit host %s on whether the change %s~%d is a pure revert",
   329  			c.host, req.Project, req.Number).Err()
   330  	}
   331  
   332  	return res.IsPureRevert, nil
   333  }
   334  
   335  // setReview is a helper to call the Gerrit client SetReview function
   336  func (c *Client) setReview(ctx context.Context, req *gerritpb.SetReviewRequest) (*gerritpb.ReviewResult, error) {
   337  	res, err := c.gerritClient.SetReview(ctx, req)
   338  	if err != nil {
   339  		return nil, errors.Annotate(err, "failed to set review on Gerrit host %s for change %s~%d",
   340  			c.host, req.Project, req.Number).Err()
   341  	}
   342  
   343  	return res, nil
   344  }