| // Copyright 2021 The Dawn & Tint Authors | 
 | // | 
 | // Redistribution and use in source and binary forms, with or without | 
 | // modification, are permitted provided that the following conditions are met: | 
 | // | 
 | // 1. Redistributions of source code must retain the above copyright notice, this | 
 | //    list of conditions and the following disclaimer. | 
 | // | 
 | // 2. Redistributions in binary form must reproduce the above copyright notice, | 
 | //    this list of conditions and the following disclaimer in the documentation | 
 | //    and/or other materials provided with the distribution. | 
 | // | 
 | // 3. Neither the name of the copyright holder nor the names of its | 
 | //    contributors may be used to endorse or promote products derived from | 
 | //    this software without specific prior written permission. | 
 | // | 
 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | 
 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 
 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | 
 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | 
 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | 
 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | 
 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | 
 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | 
 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
 |  | 
 | // Package gerrit provides helpers for obtaining information from Tint's gerrit instance | 
 | package gerrit | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"flag" | 
 | 	"fmt" | 
 | 	"log" | 
 | 	"net/url" | 
 | 	"strconv" | 
 | 	"strings" | 
 |  | 
 | 	"dawn.googlesource.com/dawn/tools/src/container" | 
 | 	"github.com/andygrunwald/go-gerrit" | 
 | 	"go.chromium.org/luci/auth" | 
 | ) | 
 |  | 
 | // Gerrit is the interface to gerrit | 
 | type Gerrit struct { | 
 | 	client        *gerrit.Client | 
 | 	authenticated bool | 
 | } | 
 |  | 
 | // Patchset refers to a single gerrit patchset | 
 | type Patchset struct { | 
 | 	// Gerrit host | 
 | 	Host string | 
 | 	// Gerrit project | 
 | 	Project string | 
 | 	// Change ID | 
 | 	Change int | 
 | 	// Patchset ID | 
 | 	Patchset int | 
 | } | 
 |  | 
 | // ChangeInfo is an alias to gerrit.ChangeInfo | 
 | type ChangeInfo = gerrit.ChangeInfo | 
 |  | 
 | // LatestPatchset returns the latest Patchset from the ChangeInfo | 
 | func LatestPatchset(change *ChangeInfo) Patchset { | 
 | 	u, _ := url.Parse(change.URL) | 
 | 	ps := Patchset{ | 
 | 		Host:     u.Host, | 
 | 		Project:  change.Project, | 
 | 		Change:   change.Number, | 
 | 		Patchset: change.Revisions[change.CurrentRevision].Number, | 
 | 	} | 
 | 	return ps | 
 | } | 
 |  | 
 | // RegisterFlags registers the command line flags to populate p | 
 | func (p *Patchset) RegisterFlags(defaultHost, defaultProject string) { | 
 | 	flag.StringVar(&p.Host, "host", defaultHost, "gerrit host") | 
 | 	flag.StringVar(&p.Project, "project", defaultProject, "gerrit project") | 
 | 	flag.IntVar(&p.Change, "cl", 0, "gerrit change id") | 
 | 	flag.IntVar(&p.Patchset, "ps", 0, "gerrit patchset id") | 
 | } | 
 |  | 
 | // RefsChanges returns the gerrit 'refs/changes/X/Y/Z' string for the patchset | 
 | func (p Patchset) RefsChanges() string { | 
 | 	// https://gerrit-review.googlesource.com/Documentation/intro-user.html | 
 | 	// A change ref has the format refs/changes/X/Y/Z where X is the last two | 
 | 	// digits of the change number, Y is the entire change number, and Z is the | 
 | 	// patch set. For example, if the change number is 263270, the ref would be | 
 | 	// refs/changes/70/263270/2 for the second patch set. | 
 | 	shortChange := fmt.Sprintf("%.2v", p.Change) | 
 | 	shortChange = shortChange[len(shortChange)-2:] | 
 | 	return fmt.Sprintf("refs/changes/%v/%v/%v", shortChange, p.Change, p.Patchset) | 
 | } | 
 |  | 
 | // New returns a new Gerrit instance. If credentials are not provided, then | 
 | // New() will automatically attempt to load them from the gitcookies file. | 
 | func New(ctx context.Context, opts auth.Options, url string) (*Gerrit, error) { | 
 | 	http, err := auth.NewAuthenticator(ctx, auth.InteractiveLogin, opts).Client() | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("couldn't create gerrit client: %w", err) | 
 | 	} | 
 |  | 
 | 	client, err := gerrit.NewClient(url, http) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("couldn't create gerrit client: %w", err) | 
 | 	} | 
 |  | 
 | 	return &Gerrit{client, true}, nil | 
 | } | 
 |  | 
 | // QueryExtraData holds extra data to query for with QueryChangesWith() | 
 | type QueryExtraData struct { | 
 | 	Labels           bool | 
 | 	Messages         bool | 
 | 	CurrentRevision  bool | 
 | 	DetailedAccounts bool | 
 | 	Submittable      bool | 
 | } | 
 |  | 
 | // QueryChanges returns the changes that match the given query strings. | 
 | // See: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators | 
 | func (g *Gerrit) QueryChangesWith(extras QueryExtraData, queries ...string) (changes []gerrit.ChangeInfo, query string, err error) { | 
 | 	changes = []gerrit.ChangeInfo{} | 
 | 	query = strings.Join(queries, "+") | 
 |  | 
 | 	changeOpts := gerrit.ChangeOptions{} | 
 | 	if extras.Labels { | 
 | 		changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "LABELS") | 
 | 	} | 
 | 	if extras.Messages { | 
 | 		changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "MESSAGES") | 
 | 	} | 
 | 	if extras.CurrentRevision { | 
 | 		changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "CURRENT_REVISION") | 
 | 	} | 
 | 	if extras.DetailedAccounts { | 
 | 		changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "DETAILED_ACCOUNTS") | 
 | 	} | 
 | 	if extras.Submittable { | 
 | 		changeOpts.AdditionalFields = append(changeOpts.AdditionalFields, "SUBMITTABLE") | 
 | 	} | 
 |  | 
 | 	for { | 
 | 		batch, _, err := g.client.Changes.QueryChanges(&gerrit.QueryChangeOptions{ | 
 | 			QueryOptions:  gerrit.QueryOptions{Query: []string{query}}, | 
 | 			Skip:          len(changes), | 
 | 			ChangeOptions: changeOpts, | 
 | 		}) | 
 | 		if err != nil { | 
 | 			return nil, "", err | 
 | 		} | 
 |  | 
 | 		changes = append(changes, *batch...) | 
 | 		if len(*batch) == 0 || !(*batch)[len(*batch)-1].MoreChanges { | 
 | 			break | 
 | 		} | 
 | 	} | 
 | 	return changes, query, nil | 
 | } | 
 |  | 
 | // QueryChanges returns the changes that match the given query strings. | 
 | // See: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators | 
 | func (g *Gerrit) QueryChanges(queries ...string) (changes []gerrit.ChangeInfo, query string, err error) { | 
 | 	return g.QueryChangesWith(QueryExtraData{}, queries...) | 
 | } | 
 |  | 
 | // ChangesSubmittedTogether returns the changes that want to be submitted together | 
 | // See: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submitted-together | 
 | func (g *Gerrit) ChangesSubmittedTogether(changeID string) (changes []gerrit.ChangeInfo, err error) { | 
 | 	info, _, err := g.client.Changes.ChangesSubmittedTogether(changeID) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	return *info, nil | 
 | } | 
 |  | 
 | func (g *Gerrit) AddLabel(changeID, revisionID, message, label string, value int) error { | 
 | 	_, _, err := g.client.Changes.SetReview(changeID, revisionID, &gerrit.ReviewInput{ | 
 | 		Message: message, | 
 | 		Labels:  map[string]string{label: fmt.Sprint(value)}, | 
 | 	}) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // Abandon abandons the change with the given changeID. | 
 | func (g *Gerrit) Abandon(changeID string) error { | 
 | 	_, _, err := g.client.Changes.AbandonChange(changeID, &gerrit.AbandonInput{}) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // CreateChange creates a new change in the given project and branch, with the | 
 | // given subject. If wip is true, then the change is constructed as | 
 | // Work-In-Progress. | 
 | func (g *Gerrit) CreateChange(project, branch, subject string, wip bool) (*ChangeInfo, error) { | 
 | 	change, _, err := g.client.Changes.CreateChange(&gerrit.ChangeInput{ | 
 | 		Project:        project, | 
 | 		Branch:         branch, | 
 | 		Subject:        subject, | 
 | 		WorkInProgress: wip, | 
 | 	}) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	if change.URL == "" { | 
 | 		base := g.client.BaseURL() | 
 | 		change.URL = fmt.Sprintf("%vc/%v/+/%v", base.String(), change.Project, change.Number) | 
 | 	} | 
 | 	return change, nil | 
 | } | 
 |  | 
 | // EditFiles replaces the content of the files in the given change. It deletes deletedFiles. | 
 | // If newCommitMsg is not an empty string, then the commit message is replaced | 
 | // with the string value. | 
 | func (g *Gerrit) EditFiles(changeID, newCommitMsg string, files map[string]string, deletedFiles []string) (Patchset, error) { | 
 | 	if newCommitMsg != "" { | 
 | 		resp, err := g.client.Changes.ChangeCommitMessageInChangeEdit(changeID, &gerrit.ChangeEditMessageInput{ | 
 | 			Message: newCommitMsg, | 
 | 		}) | 
 | 		if err != nil && resp.StatusCode != 409 { // 409 no changes were made | 
 | 			return Patchset{}, err | 
 | 		} | 
 | 	} | 
 | 	for path, content := range files { | 
 | 		resp, err := g.client.Changes.ChangeFileContentInChangeEdit(changeID, path, content) | 
 | 		if err != nil && resp.StatusCode != 409 { // 409 no changes were made | 
 | 			return Patchset{}, err | 
 | 		} | 
 | 	} | 
 | 	for _, path := range deletedFiles { | 
 | 		resp, err := g.client.Changes.DeleteFileInChangeEdit(changeID, path) | 
 | 		if err != nil && resp.StatusCode != 409 { // 409 no changes were made | 
 | 			return Patchset{}, err | 
 | 		} | 
 | 	} | 
 |  | 
 | 	resp, err := g.client.Changes.PublishChangeEdit(changeID, "NONE") | 
 | 	if err != nil && resp.StatusCode != 409 { // 409 no changes were made | 
 | 		return Patchset{}, err | 
 | 	} | 
 |  | 
 | 	return g.LatestPatchset(changeID) | 
 | } | 
 |  | 
 | // LatestPatchset returns the latest patchset for the change. | 
 | func (g *Gerrit) LatestPatchset(changeID string) (Patchset, error) { | 
 | 	change, _, err := g.client.Changes.GetChange(changeID, &gerrit.ChangeOptions{ | 
 | 		AdditionalFields: []string{"CURRENT_REVISION"}, | 
 | 	}) | 
 | 	if err != nil { | 
 | 		return Patchset{}, err | 
 | 	} | 
 | 	ps := Patchset{ | 
 | 		Host:     g.client.BaseURL().Host, | 
 | 		Project:  change.Project, | 
 | 		Change:   change.Number, | 
 | 		Patchset: change.Revisions[change.CurrentRevision].Number, | 
 | 	} | 
 | 	return ps, nil | 
 | } | 
 |  | 
 | // AddHashtags adds the given hashtags to the change | 
 | func (g *Gerrit) AddHashtags(changeID string, tags container.Set[string]) error { | 
 | 	_, resp, err := g.client.Changes.SetHashtags(changeID, &gerrit.HashtagsInput{ | 
 | 		Add: tags.List(), | 
 | 	}) | 
 | 	if err != nil && resp.StatusCode != 409 { // 409: already ready | 
 | 		return err | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // CommentSide is an enumerator for specifying which side code-comments should | 
 | // be shown. | 
 | type CommentSide int | 
 |  | 
 | const ( | 
 | 	// Left is used to specify that code comments should appear on the parent change | 
 | 	Left CommentSide = iota | 
 | 	// Right is used to specify that code comments should appear on the new change | 
 | 	Right | 
 | ) | 
 |  | 
 | // FileComment describes a single comment on a file | 
 | type FileComment struct { | 
 | 	Path    string      // The file path | 
 | 	Side    CommentSide // Which side the comment should appear | 
 | 	Line    int         // The 1-based line number for the comment | 
 | 	Message string      // The comment message | 
 | } | 
 |  | 
 | // Comment posts a review comment on the given patchset. | 
 | // If comments is an optional list of file-comments to include in the comment. | 
 | func (g *Gerrit) Comment(ps Patchset, msg string, comments []FileComment) error { | 
 | 	input := &gerrit.ReviewInput{ | 
 | 		Message: msg, | 
 | 	} | 
 | 	if len(comments) > 0 { | 
 | 		input.Comments = map[string][]gerrit.CommentInput{} | 
 | 		for _, c := range comments { | 
 | 			ci := gerrit.CommentInput{ | 
 | 				Line: c.Line, | 
 | 				// Updated: &gerrit.Timestamp{Time: time.Now()}, | 
 | 				Message: c.Message, | 
 | 			} | 
 | 			if c.Side == Left { | 
 | 				ci.Side = "PARENT" | 
 | 			} else { | 
 | 				ci.Side = "REVISION" | 
 | 			} | 
 | 			input.Comments[c.Path] = append(input.Comments[c.Path], ci) | 
 | 		} | 
 | 	} | 
 | 	_, _, err := g.client.Changes.SetReview(strconv.Itoa(ps.Change), strconv.Itoa(ps.Patchset), input) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // SetReadyForReview marks the change as ready for review. | 
 | func (g *Gerrit) SetReadyForReview(changeID, message, reviewer string) error { | 
 | 	resp, err := g.client.Changes.SetReadyForReview(changeID, &gerrit.ReadyForReviewInput{ | 
 | 		Message: message, | 
 | 	}) | 
 | 	if err != nil && resp.StatusCode != 409 { // 409: already ready | 
 | 		return err | 
 | 	} | 
 | 	if reviewer != "" { | 
 | 		log.Printf("Got reviewer %s", reviewer) | 
 | 		_, resp, err = g.client.Changes.AddReviewer(changeID, &gerrit.ReviewerInput{ | 
 | 			Reviewer: reviewer, | 
 | 		}) | 
 | 		if err != nil && resp.StatusCode != 409 { // 409: already ready | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // ListAccessRights retrieves the access rights for the client for the specified | 
 | // projects within the client's Gerrit instance. | 
 | func (g *Gerrit) ListAccessRights( | 
 | 	ctx context.Context, projects ...string) (*map[string]gerrit.ProjectAccessInfo, error) { | 
 |  | 
 | 	listOptions := gerrit.ListAccessRightsOptions{ | 
 | 		Project: projects, | 
 | 	} | 
 | 	accessInfo, _, err := g.client.Access.ListAccessRights(&listOptions) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 |  | 
 | 	return accessInfo, nil | 
 | } |