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 }