golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/gerrit.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package task
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"strings"
    13  
    14  	"golang.org/x/build/gerrit"
    15  	wf "golang.org/x/build/internal/workflow"
    16  )
    17  
    18  type GerritClient interface {
    19  	// GitilesURL returns the URL to the Gitiles server for this Gerrit instance.
    20  	GitilesURL() string
    21  	// CreateAutoSubmitChange creates a change with the given metadata and
    22  	// contents, starts trybots with auto-submit enabled, and returns its change ID.
    23  	// If the content of a file is empty, that file will be deleted from the repository.
    24  	// If the requested contents match the state of the repository, no change
    25  	// is created and the returned change ID will be empty.
    26  	// Reviewers is the username part of a golang.org or google.com email address.
    27  	CreateAutoSubmitChange(ctx *wf.TaskContext, input gerrit.ChangeInput, reviewers []string, contents map[string]string) (string, error)
    28  	// Submitted checks if the specified change has been submitted or failed
    29  	// trybots. If the CL is submitted, returns the submitted commit hash.
    30  	// If parentCommit is non-empty, the submitted CL's parent must match it.
    31  	Submitted(ctx context.Context, changeID, parentCommit string) (string, bool, error)
    32  	// GetTag returns tag information for a specified tag.
    33  	GetTag(ctx context.Context, project, tag string) (gerrit.TagInfo, error)
    34  	// Tag creates a tag on project at the specified commit.
    35  	Tag(ctx context.Context, project, tag, commit string) error
    36  	// ListTags returns all the tags on project.
    37  	ListTags(ctx context.Context, project string) ([]string, error)
    38  	// ReadBranchHead returns the head of a branch in project.
    39  	// If the branch doesn't exist, it returns an error matching gerrit.ErrResourceNotExist.
    40  	ReadBranchHead(ctx context.Context, project, branch string) (string, error)
    41  	// ListProjects lists all the projects on the server.
    42  	ListProjects(ctx context.Context) ([]string, error)
    43  	// ReadFile reads a file from project at the specified commit.
    44  	// If the file doesn't exist, it returns an error matching gerrit.ErrResourceNotExist.
    45  	ReadFile(ctx context.Context, project, commit, file string) ([]byte, error)
    46  	// GetCommitsInRefs gets refs in which the specified commits were merged into.
    47  	GetCommitsInRefs(ctx context.Context, project string, commits, refs []string) (map[string][]string, error)
    48  	// QueryChanges gets changes which match the query.
    49  	QueryChanges(ctx context.Context, query string) ([]*gerrit.ChangeInfo, error)
    50  	// SetHashtags modifies the hashtags for a CL.
    51  	SetHashtags(ctx context.Context, changeID string, hashtags gerrit.HashtagsInput) error
    52  	// GetChange gets information about a specific change.
    53  	GetChange(ctx context.Context, changeID string, opts ...gerrit.QueryChangesOpt) (*gerrit.ChangeInfo, error)
    54  }
    55  
    56  type RealGerritClient struct {
    57  	Gitiles string
    58  	Client  *gerrit.Client
    59  }
    60  
    61  func (c *RealGerritClient) GitilesURL() string {
    62  	return c.Gitiles
    63  }
    64  
    65  func (c *RealGerritClient) CreateAutoSubmitChange(ctx *wf.TaskContext, input gerrit.ChangeInput, reviewers []string, files map[string]string) (_ string, err error) {
    66  	defer func() {
    67  		// Check if status code is known to be not retryable.
    68  		if he := (*gerrit.HTTPError)(nil); errors.As(err, &he) && he.Res.StatusCode/100 == 4 {
    69  			ctx.DisableRetries()
    70  		}
    71  	}()
    72  
    73  	reviewerEmails, err := coordinatorEmails(reviewers)
    74  	if err != nil {
    75  		return "", err
    76  	}
    77  
    78  	change, err := c.Client.CreateChange(ctx, input)
    79  	if err != nil {
    80  		return "", err
    81  	}
    82  	changeID := fmt.Sprintf("%s~%d", change.Project, change.ChangeNumber)
    83  	anyChange := false
    84  	for path, content := range files {
    85  		var err error
    86  		if content == "" {
    87  			err = c.Client.DeleteFileInChangeEdit(ctx, changeID, path)
    88  		} else {
    89  			err = c.Client.ChangeFileContentInChangeEdit(ctx, changeID, path, content)
    90  		}
    91  		if errors.Is(err, gerrit.ErrNotModified) {
    92  			continue
    93  		}
    94  		if err != nil {
    95  			return "", err
    96  		}
    97  		anyChange = true
    98  	}
    99  	if !anyChange {
   100  		if err := c.Client.AbandonChange(ctx, changeID, "no changes necessary"); err != nil {
   101  			return "", err
   102  		}
   103  		return "", nil
   104  	}
   105  
   106  	if err := c.Client.PublishChangeEdit(ctx, changeID); err != nil {
   107  		return "", err
   108  	}
   109  
   110  	var reviewerInputs []gerrit.ReviewerInput
   111  	for _, r := range reviewerEmails {
   112  		reviewerInputs = append(reviewerInputs, gerrit.ReviewerInput{Reviewer: r})
   113  	}
   114  	if err := c.Client.SetReview(ctx, changeID, "current", gerrit.ReviewInput{
   115  		Labels: map[string]int{
   116  			"Commit-Queue": 1,
   117  			"Auto-Submit":  1,
   118  		},
   119  		Reviewers: reviewerInputs,
   120  	}); err != nil {
   121  		return "", err
   122  	}
   123  	return changeID, nil
   124  }
   125  
   126  func (c *RealGerritClient) Submitted(ctx context.Context, changeID, parentCommit string) (string, bool, error) {
   127  	detail, err := c.Client.GetChangeDetail(ctx, changeID, gerrit.QueryChangesOpt{
   128  		Fields: []string{"CURRENT_REVISION", "DETAILED_LABELS", "CURRENT_COMMIT"},
   129  	})
   130  	if err != nil {
   131  		return "", false, err
   132  	}
   133  	if detail.Status == "MERGED" {
   134  		parents := detail.Revisions[detail.CurrentRevision].Commit.Parents
   135  		if parentCommit != "" && (len(parents) != 1 || parents[0].CommitID != parentCommit) {
   136  			return "", false, fmt.Errorf("expected merged CL %v to have one parent commit %v, has %v", ChangeLink(changeID), parentCommit, parents)
   137  		}
   138  		return detail.CurrentRevision, true, nil
   139  	}
   140  	for _, approver := range detail.Labels["TryBot-Result"].All {
   141  		if approver.Value < 0 {
   142  			return "", false, fmt.Errorf("trybots failed on %v", ChangeLink(changeID))
   143  		}
   144  	}
   145  	return "", false, nil
   146  }
   147  
   148  func (c *RealGerritClient) Tag(ctx context.Context, project, tag, commit string) error {
   149  	info, err := c.Client.GetTag(ctx, project, tag)
   150  	if err != nil && !errors.Is(err, gerrit.ErrResourceNotExist) {
   151  		return fmt.Errorf("checking if tag already exists: %v", err)
   152  	}
   153  	if err == nil {
   154  		if info.Revision != commit {
   155  			return fmt.Errorf("tag %q already exists on revision %q rather than our %q", tag, info.Revision, commit)
   156  		} else {
   157  			// Nothing to do.
   158  			return nil
   159  		}
   160  	}
   161  
   162  	_, err = c.Client.CreateTag(ctx, project, tag, gerrit.TagInput{
   163  		Revision: commit,
   164  	})
   165  	return err
   166  }
   167  
   168  func (c *RealGerritClient) ListTags(ctx context.Context, project string) ([]string, error) {
   169  	tags, err := c.Client.GetProjectTags(ctx, project)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	var tagNames []string
   174  	for _, tag := range tags {
   175  		tagNames = append(tagNames, strings.TrimPrefix(tag.Ref, "refs/tags/"))
   176  	}
   177  	return tagNames, nil
   178  }
   179  
   180  func (c *RealGerritClient) GetTag(ctx context.Context, project, tag string) (gerrit.TagInfo, error) {
   181  	return c.Client.GetTag(ctx, project, tag)
   182  }
   183  
   184  func (c *RealGerritClient) ReadBranchHead(ctx context.Context, project, branch string) (string, error) {
   185  	branchInfo, err := c.Client.GetBranch(ctx, project, branch)
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  	return branchInfo.Revision, nil
   190  }
   191  
   192  func (c *RealGerritClient) ListProjects(ctx context.Context) ([]string, error) {
   193  	projects, err := c.Client.ListProjects(ctx)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	var names []string
   198  	for _, p := range projects {
   199  		names = append(names, p.Name)
   200  	}
   201  	return names, nil
   202  }
   203  
   204  func (c *RealGerritClient) ReadFile(ctx context.Context, project, commit, file string) ([]byte, error) {
   205  	body, err := c.Client.GetFileContent(ctx, project, commit, file)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	defer body.Close()
   210  	return io.ReadAll(body)
   211  }
   212  
   213  func (c *RealGerritClient) GetCommitsInRefs(ctx context.Context, project string, commits, refs []string) (map[string][]string, error) {
   214  	return c.Client.GetCommitsInRefs(ctx, project, commits, refs)
   215  }
   216  
   217  // ChangeLink returns a link to the review page for the CL with the specified
   218  // change ID. The change ID must be in the project~cl# form.
   219  func ChangeLink(changeID string) string {
   220  	parts := strings.SplitN(changeID, "~", 3)
   221  	if len(parts) != 2 {
   222  		return fmt.Sprintf("(unparseable change ID %q)", changeID)
   223  	}
   224  	return "https://go.dev/cl/" + parts[1]
   225  }
   226  
   227  func (c *RealGerritClient) QueryChanges(ctx context.Context, query string) ([]*gerrit.ChangeInfo, error) {
   228  	return c.Client.QueryChanges(ctx, query)
   229  }
   230  
   231  func (c *RealGerritClient) GetChange(ctx context.Context, changeID string, opts ...gerrit.QueryChangesOpt) (*gerrit.ChangeInfo, error) {
   232  	return c.Client.GetChange(ctx, changeID, opts...)
   233  }
   234  
   235  func (c *RealGerritClient) SetHashtags(ctx context.Context, changeID string, hashtags gerrit.HashtagsInput) error {
   236  	_, err := c.Client.SetHashtags(ctx, changeID, hashtags)
   237  	return err
   238  }