blob: b293130c8dd4a2745391fe874a82c324dd64cb1c [file] [log] [blame]
Austin Engcc2516a2023-10-17 20:57:54 +00001// Copyright 2021 The Dawn & Tint Authors
Ben Claytonf6660aa2021-07-20 20:25:38 +00002//
Austin Engcc2516a2023-10-17 20:57:54 +00003// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are met:
Ben Claytonf6660aa2021-07-20 20:25:38 +00005//
Austin Engcc2516a2023-10-17 20:57:54 +00006// 1. Redistributions of source code must retain the above copyright notice, this
7// list of conditions and the following disclaimer.
Ben Claytonf6660aa2021-07-20 20:25:38 +00008//
Austin Engcc2516a2023-10-17 20:57:54 +00009// 2. Redistributions in binary form must reproduce the above copyright notice,
10// this list of conditions and the following disclaimer in the documentation
11// and/or other materials provided with the distribution.
12//
13// 3. Neither the name of the copyright holder nor the names of its
14// contributors may be used to endorse or promote products derived from
15// this software without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Ben Claytonf6660aa2021-07-20 20:25:38 +000027
Ben Clayton53ddabe2022-04-12 16:13:31 +000028// Package gerrit provides helpers for obtaining information from Tint's gerrit instance
Ben Claytonf6660aa2021-07-20 20:25:38 +000029package gerrit
30
31import (
Austin Eng0dae5ce2023-08-17 22:07:36 +000032 "context"
Ben Clayton53ddabe2022-04-12 16:13:31 +000033 "flag"
Ben Claytonf6660aa2021-07-20 20:25:38 +000034 "fmt"
Austin Eng260ae982023-08-21 21:22:22 +000035 "log"
Ben Clayton53ddabe2022-04-12 16:13:31 +000036 "net/url"
Ben Clayton53ddabe2022-04-12 16:13:31 +000037 "strconv"
Ben Claytonf6660aa2021-07-20 20:25:38 +000038 "strings"
39
Ben Clayton1a8d0782023-05-16 14:36:37 +000040 "dawn.googlesource.com/dawn/tools/src/container"
Ben Claytonf6660aa2021-07-20 20:25:38 +000041 "github.com/andygrunwald/go-gerrit"
Austin Eng0dae5ce2023-08-17 22:07:36 +000042 "go.chromium.org/luci/auth"
Ben Claytonf6660aa2021-07-20 20:25:38 +000043)
44
Ben Clayton53ddabe2022-04-12 16:13:31 +000045// Gerrit is the interface to gerrit
46type Gerrit struct {
Ben Claytonf6660aa2021-07-20 20:25:38 +000047 client *gerrit.Client
48 authenticated bool
49}
50
Ben Clayton53ddabe2022-04-12 16:13:31 +000051// Patchset refers to a single gerrit patchset
52type Patchset struct {
53 // Gerrit host
54 Host string
55 // Gerrit project
56 Project string
57 // Change ID
58 Change int
59 // Patchset ID
60 Patchset int
Ben Clayton5209a812021-07-30 16:20:46 +000061}
62
Ben Clayton53ddabe2022-04-12 16:13:31 +000063// ChangeInfo is an alias to gerrit.ChangeInfo
64type ChangeInfo = gerrit.ChangeInfo
65
Ben Clayton1a8d0782023-05-16 14:36:37 +000066// LatestPatchset returns the latest Patchset from the ChangeInfo
67func LatestPatchset(change *ChangeInfo) Patchset {
Ben Clayton53ddabe2022-04-12 16:13:31 +000068 u, _ := url.Parse(change.URL)
69 ps := Patchset{
70 Host: u.Host,
71 Project: change.Project,
72 Change: change.Number,
73 Patchset: change.Revisions[change.CurrentRevision].Number,
74 }
75 return ps
76}
77
78// RegisterFlags registers the command line flags to populate p
79func (p *Patchset) RegisterFlags(defaultHost, defaultProject string) {
80 flag.StringVar(&p.Host, "host", defaultHost, "gerrit host")
81 flag.StringVar(&p.Project, "project", defaultProject, "gerrit project")
82 flag.IntVar(&p.Change, "cl", 0, "gerrit change id")
83 flag.IntVar(&p.Patchset, "ps", 0, "gerrit patchset id")
84}
85
Ben Clayton526087b2022-04-29 15:15:43 +000086// RefsChanges returns the gerrit 'refs/changes/X/Y/Z' string for the patchset
87func (p Patchset) RefsChanges() string {
88 // https://gerrit-review.googlesource.com/Documentation/intro-user.html
89 // A change ref has the format refs/changes/X/Y/Z where X is the last two
90 // digits of the change number, Y is the entire change number, and Z is the
91 // patch set. For example, if the change number is 263270, the ref would be
92 // refs/changes/70/263270/2 for the second patch set.
93 shortChange := fmt.Sprintf("%.2v", p.Change)
94 shortChange = shortChange[len(shortChange)-2:]
95 return fmt.Sprintf("refs/changes/%v/%v/%v", shortChange, p.Change, p.Patchset)
96}
97
Ben Clayton53ddabe2022-04-12 16:13:31 +000098// New returns a new Gerrit instance. If credentials are not provided, then
99// New() will automatically attempt to load them from the gitcookies file.
Austin Eng0dae5ce2023-08-17 22:07:36 +0000100func New(ctx context.Context, opts auth.Options, url string) (*Gerrit, error) {
101 http, err := auth.NewAuthenticator(ctx, auth.InteractiveLogin, opts).Client()
Ben Claytonf6660aa2021-07-20 20:25:38 +0000102 if err != nil {
Ben Clayton5209a812021-07-30 16:20:46 +0000103 return nil, fmt.Errorf("couldn't create gerrit client: %w", err)
Ben Claytonf6660aa2021-07-20 20:25:38 +0000104 }
105
Austin Eng0dae5ce2023-08-17 22:07:36 +0000106 client, err := gerrit.NewClient(url, http)
107 if err != nil {
108 return nil, fmt.Errorf("couldn't create gerrit client: %w", err)
Ben Claytonf6660aa2021-07-20 20:25:38 +0000109 }
110
Austin Eng0dae5ce2023-08-17 22:07:36 +0000111 return &Gerrit{client, true}, nil
Ben Claytonf6660aa2021-07-20 20:25:38 +0000112}
113
Ben Claytondededb12023-05-18 11:30:07 +0000114// QueryExtraData holds extra data to query for with QueryChangesWith()
115type QueryExtraData struct {
116 Labels bool
117 Messages bool
118 CurrentRevision bool
119 DetailedAccounts bool
Ben Clayton4cdc6bf2023-06-01 00:40:30 +0000120 Submittable bool
Ben Claytondededb12023-05-18 11:30:07 +0000121}
122
Ben Clayton53ddabe2022-04-12 16:13:31 +0000123// QueryChanges returns the changes that match the given query strings.
124// See: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators
Ben Claytondededb12023-05-18 11:30:07 +0000125func (g *Gerrit) QueryChangesWith(extras QueryExtraData, queries ...string) (changes []gerrit.ChangeInfo, query string, err error) {
Ben Claytonf6660aa2021-07-20 20:25:38 +0000126 changes = []gerrit.ChangeInfo{}
Ben Claytondededb12023-05-18 11:30:07 +0000127 query = strings.Join(queries, "+")
128
129 changeOpts := gerrit.ChangeOptions{}
130 if extras.Labels {
131 changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "LABELS")
132 }
133 if extras.Messages {
134 changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "MESSAGES")
135 }
136 if extras.CurrentRevision {
137 changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "CURRENT_REVISION")
138 }
139 if extras.DetailedAccounts {
140 changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "DETAILED_ACCOUNTS")
141 }
Ben Clayton4cdc6bf2023-06-01 00:40:30 +0000142 if extras.Submittable {
143 changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "SUBMITTABLE")
144 }
Ben Claytondededb12023-05-18 11:30:07 +0000145
Ben Claytonf6660aa2021-07-20 20:25:38 +0000146 for {
147 batch, _, err := g.client.Changes.QueryChanges(&gerrit.QueryChangeOptions{
Ben Claytondededb12023-05-18 11:30:07 +0000148 QueryOptions: gerrit.QueryOptions{Query: []string{query}},
149 Skip: len(changes),
150 ChangeOptions: changeOpts,
Ben Claytonf6660aa2021-07-20 20:25:38 +0000151 })
152 if err != nil {
Austin Eng0dae5ce2023-08-17 22:07:36 +0000153 return nil, "", err
Ben Claytonf6660aa2021-07-20 20:25:38 +0000154 }
155
156 changes = append(changes, *batch...)
157 if len(*batch) == 0 || !(*batch)[len(*batch)-1].MoreChanges {
158 break
159 }
160 }
161 return changes, query, nil
162}
Ben Clayton53ddabe2022-04-12 16:13:31 +0000163
Ben Claytondededb12023-05-18 11:30:07 +0000164// QueryChanges returns the changes that match the given query strings.
165// See: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators
166func (g *Gerrit) QueryChanges(queries ...string) (changes []gerrit.ChangeInfo, query string, err error) {
167 return g.QueryChangesWith(QueryExtraData{}, queries...)
168}
169
Ben Clayton4cdc6bf2023-06-01 00:40:30 +0000170// ChangesSubmittedTogether returns the changes that want to be submitted together
171// See: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submitted-together
172func (g *Gerrit) ChangesSubmittedTogether(changeID string) (changes []gerrit.ChangeInfo, err error) {
173 info, _, err := g.client.Changes.ChangesSubmittedTogether(changeID)
174 if err != nil {
Austin Eng0dae5ce2023-08-17 22:07:36 +0000175 return nil, err
Ben Clayton4cdc6bf2023-06-01 00:40:30 +0000176 }
177 return *info, nil
178}
179
Ben Claytondededb12023-05-18 11:30:07 +0000180func (g *Gerrit) AddLabel(changeID, revisionID, message, label string, value int) error {
181 _, _, err := g.client.Changes.SetReview(changeID, revisionID, &gerrit.ReviewInput{
182 Message: message,
183 Labels: map[string]string{label: fmt.Sprint(value)},
184 })
185 if err != nil {
Austin Eng0dae5ce2023-08-17 22:07:36 +0000186 return err
Ben Claytondededb12023-05-18 11:30:07 +0000187 }
188 return nil
189}
190
Ben Clayton53ddabe2022-04-12 16:13:31 +0000191// Abandon abandons the change with the given changeID.
192func (g *Gerrit) Abandon(changeID string) error {
193 _, _, err := g.client.Changes.AbandonChange(changeID, &gerrit.AbandonInput{})
194 if err != nil {
Austin Eng0dae5ce2023-08-17 22:07:36 +0000195 return err
Ben Clayton53ddabe2022-04-12 16:13:31 +0000196 }
197 return nil
198}
199
200// CreateChange creates a new change in the given project and branch, with the
201// given subject. If wip is true, then the change is constructed as
202// Work-In-Progress.
203func (g *Gerrit) CreateChange(project, branch, subject string, wip bool) (*ChangeInfo, error) {
204 change, _, err := g.client.Changes.CreateChange(&gerrit.ChangeInput{
205 Project: project,
206 Branch: branch,
207 Subject: subject,
208 WorkInProgress: wip,
209 })
210 if err != nil {
Austin Eng0dae5ce2023-08-17 22:07:36 +0000211 return nil, err
Ben Clayton53ddabe2022-04-12 16:13:31 +0000212 }
Ben Clayton9cae32e2024-03-19 19:23:19 +0000213 if change.URL == "" {
214 base := g.client.BaseURL()
215 change.URL = fmt.Sprintf("%vc/%v/+/%v", base.String(), change.Project, change.Number)
216 }
Ben Clayton53ddabe2022-04-12 16:13:31 +0000217 return change, nil
218}
219
Austin Eng9d14b162022-06-10 16:25:43 +0000220// EditFiles replaces the content of the files in the given change. It deletes deletedFiles.
Ben Clayton53ddabe2022-04-12 16:13:31 +0000221// If newCommitMsg is not an empty string, then the commit message is replaced
222// with the string value.
Austin Eng9d14b162022-06-10 16:25:43 +0000223func (g *Gerrit) EditFiles(changeID, newCommitMsg string, files map[string]string, deletedFiles []string) (Patchset, error) {
Ben Clayton53ddabe2022-04-12 16:13:31 +0000224 if newCommitMsg != "" {
225 resp, err := g.client.Changes.ChangeCommitMessageInChangeEdit(changeID, &gerrit.ChangeEditMessageInput{
226 Message: newCommitMsg,
227 })
228 if err != nil && resp.StatusCode != 409 { // 409 no changes were made
Austin Eng0dae5ce2023-08-17 22:07:36 +0000229 return Patchset{}, err
Ben Clayton53ddabe2022-04-12 16:13:31 +0000230 }
231 }
232 for path, content := range files {
233 resp, err := g.client.Changes.ChangeFileContentInChangeEdit(changeID, path, content)
234 if err != nil && resp.StatusCode != 409 { // 409 no changes were made
Austin Eng0dae5ce2023-08-17 22:07:36 +0000235 return Patchset{}, err
Ben Clayton53ddabe2022-04-12 16:13:31 +0000236 }
237 }
Austin Eng9d14b162022-06-10 16:25:43 +0000238 for _, path := range deletedFiles {
239 resp, err := g.client.Changes.DeleteFileInChangeEdit(changeID, path)
240 if err != nil && resp.StatusCode != 409 { // 409 no changes were made
Austin Eng0dae5ce2023-08-17 22:07:36 +0000241 return Patchset{}, err
Austin Eng9d14b162022-06-10 16:25:43 +0000242 }
243 }
Ben Clayton53ddabe2022-04-12 16:13:31 +0000244
245 resp, err := g.client.Changes.PublishChangeEdit(changeID, "NONE")
246 if err != nil && resp.StatusCode != 409 { // 409 no changes were made
Austin Eng0dae5ce2023-08-17 22:07:36 +0000247 return Patchset{}, err
Ben Clayton53ddabe2022-04-12 16:13:31 +0000248 }
249
Ben Clayton1a8d0782023-05-16 14:36:37 +0000250 return g.LatestPatchset(changeID)
Ben Clayton53ddabe2022-04-12 16:13:31 +0000251}
252
Ben Clayton1a8d0782023-05-16 14:36:37 +0000253// LatestPatchset returns the latest patchset for the change.
254func (g *Gerrit) LatestPatchset(changeID string) (Patchset, error) {
Ben Clayton53ddabe2022-04-12 16:13:31 +0000255 change, _, err := g.client.Changes.GetChange(changeID, &gerrit.ChangeOptions{
256 AdditionalFields: []string{"CURRENT_REVISION"},
257 })
258 if err != nil {
Austin Eng0dae5ce2023-08-17 22:07:36 +0000259 return Patchset{}, err
Ben Clayton53ddabe2022-04-12 16:13:31 +0000260 }
261 ps := Patchset{
262 Host: g.client.BaseURL().Host,
263 Project: change.Project,
264 Change: change.Number,
265 Patchset: change.Revisions[change.CurrentRevision].Number,
266 }
267 return ps, nil
268}
269
Ben Clayton1a8d0782023-05-16 14:36:37 +0000270// AddHashtags adds the given hashtags to the change
271func (g *Gerrit) AddHashtags(changeID string, tags container.Set[string]) error {
272 _, resp, err := g.client.Changes.SetHashtags(changeID, &gerrit.HashtagsInput{
273 Add: tags.List(),
274 })
275 if err != nil && resp.StatusCode != 409 { // 409: already ready
Austin Eng0dae5ce2023-08-17 22:07:36 +0000276 return err
Ben Clayton1a8d0782023-05-16 14:36:37 +0000277 }
278 return nil
279}
280
Ben Clayton526087b2022-04-29 15:15:43 +0000281// CommentSide is an enumerator for specifying which side code-comments should
282// be shown.
283type CommentSide int
284
285const (
Ben Claytonea7d7fe2024-02-28 00:23:17 +0000286 // Left is used to specify that code comments should appear on the parent change
Ben Clayton526087b2022-04-29 15:15:43 +0000287 Left CommentSide = iota
Ben Claytonea7d7fe2024-02-28 00:23:17 +0000288 // Right is used to specify that code comments should appear on the new change
Ben Clayton526087b2022-04-29 15:15:43 +0000289 Right
290)
291
292// FileComment describes a single comment on a file
293type FileComment struct {
294 Path string // The file path
295 Side CommentSide // Which side the comment should appear
296 Line int // The 1-based line number for the comment
297 Message string // The comment message
298}
299
Ben Clayton53ddabe2022-04-12 16:13:31 +0000300// Comment posts a review comment on the given patchset.
Ben Clayton526087b2022-04-29 15:15:43 +0000301// If comments is an optional list of file-comments to include in the comment.
302func (g *Gerrit) Comment(ps Patchset, msg string, comments []FileComment) error {
303 input := &gerrit.ReviewInput{
304 Message: msg,
305 }
306 if len(comments) > 0 {
307 input.Comments = map[string][]gerrit.CommentInput{}
308 for _, c := range comments {
309 ci := gerrit.CommentInput{
310 Line: c.Line,
311 // Updated: &gerrit.Timestamp{Time: time.Now()},
312 Message: c.Message,
313 }
314 if c.Side == Left {
315 ci.Side = "PARENT"
316 } else {
317 ci.Side = "REVISION"
318 }
319 input.Comments[c.Path] = append(input.Comments[c.Path], ci)
320 }
321 }
322 _, _, err := g.client.Changes.SetReview(strconv.Itoa(ps.Change), strconv.Itoa(ps.Patchset), input)
Ben Clayton53ddabe2022-04-12 16:13:31 +0000323 if err != nil {
Austin Eng0dae5ce2023-08-17 22:07:36 +0000324 return err
Ben Clayton53ddabe2022-04-12 16:13:31 +0000325 }
326 return nil
327}
Ben Clayton526087b2022-04-29 15:15:43 +0000328
329// SetReadyForReview marks the change as ready for review.
Austin Eng260ae982023-08-21 21:22:22 +0000330func (g *Gerrit) SetReadyForReview(changeID, message, reviewer string) error {
Ben Clayton526087b2022-04-29 15:15:43 +0000331 resp, err := g.client.Changes.SetReadyForReview(changeID, &gerrit.ReadyForReviewInput{
332 Message: message,
333 })
334 if err != nil && resp.StatusCode != 409 { // 409: already ready
Austin Eng0dae5ce2023-08-17 22:07:36 +0000335 return err
Ben Clayton526087b2022-04-29 15:15:43 +0000336 }
Austin Eng260ae982023-08-21 21:22:22 +0000337 if reviewer != "" {
Austin Eng260ae982023-08-21 21:22:22 +0000338 log.Printf("Got reviewer %s", reviewer)
Austin Eng260ae982023-08-21 21:22:22 +0000339 _, resp, err = g.client.Changes.AddReviewer(changeID, &gerrit.ReviewerInput{
340 Reviewer: reviewer,
341 })
342 if err != nil && resp.StatusCode != 409 { // 409: already ready
343 return err
344 }
345 }
Ben Clayton526087b2022-04-29 15:15:43 +0000346 return nil
347}