tools: Flesh out the gerrit package
Add more helpers for interfacing with Gerrit. Move dawn-specific constants out to a new 'dawn' package. Keeps the packages dawn-specific-free. Bug: dawn:1342 Change-Id: Iaebe0b68d877340fc848d711c848d01705ddae57 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/86524 Reviewed-by: Corentin Wallez <cwallez@chromium.org> Commit-Queue: Ben Clayton <bclayton@chromium.org> Kokoro: Kokoro <noreply+kokoro@google.com>
This commit is contained in:
parent
76b49d521c
commit
53ddabe48f
|
@ -24,6 +24,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/dawn"
|
||||||
"dawn.googlesource.com/dawn/tools/src/gerrit"
|
"dawn.googlesource.com/dawn/tools/src/gerrit"
|
||||||
"dawn.googlesource.com/dawn/tools/src/git"
|
"dawn.googlesource.com/dawn/tools/src/git"
|
||||||
)
|
)
|
||||||
|
@ -90,7 +91,9 @@ func run() error {
|
||||||
after = before.Add(-time.Hour * time.Duration(24**daysFlag))
|
after = before.Add(-time.Hour * time.Duration(24**daysFlag))
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := gerrit.New(gerrit.Config{Username: *gerritUser, Password: *gerritPass})
|
g, err := gerrit.New(dawn.GerritURL, gerrit.Credentials{
|
||||||
|
Username: *gerritUser, Password: *gerritPass,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -154,8 +157,8 @@ func run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n")
|
fmt.Printf("\n")
|
||||||
fmt.Printf("Submitted query: %vq/%v\n", gerrit.URL, url.QueryEscape(submittedQuery))
|
fmt.Printf("Submitted query: %vq/%v\n", dawn.GerritURL, url.QueryEscape(submittedQuery))
|
||||||
fmt.Printf("Review query: %vq/%v\n", gerrit.URL, url.QueryEscape(reviewQuery))
|
fmt.Printf("Review query: %vq/%v\n", dawn.GerritURL, url.QueryEscape(reviewQuery))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/dawn"
|
||||||
"dawn.googlesource.com/dawn/tools/src/gerrit"
|
"dawn.googlesource.com/dawn/tools/src/gerrit"
|
||||||
"dawn.googlesource.com/dawn/tools/src/git"
|
"dawn.googlesource.com/dawn/tools/src/git"
|
||||||
)
|
)
|
||||||
|
@ -87,7 +88,9 @@ func run() error {
|
||||||
after = before.Add(-time.Hour * time.Duration(24**daysFlag))
|
after = before.Add(-time.Hour * time.Duration(24**daysFlag))
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := gerrit.New(gerrit.Config{Username: *gerritUser, Password: *gerritPass})
|
g, err := gerrit.New(dawn.GerritURL, gerrit.Credentials{
|
||||||
|
Username: *gerritUser, Password: *gerritPass,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -103,7 +106,7 @@ func run() error {
|
||||||
|
|
||||||
changesByProject := map[string][]string{}
|
changesByProject := map[string][]string{}
|
||||||
for _, change := range submitted {
|
for _, change := range submitted {
|
||||||
str := fmt.Sprintf(`* [%s](%sc/%s/+/%d)`, change.Subject, gerrit.URL, change.Project, change.Number)
|
str := fmt.Sprintf(`* [%s](%sc/%s/+/%d)`, change.Subject, dawn.GerritURL, change.Project, change.Number)
|
||||||
changesByProject[change.Project] = append(changesByProject[change.Project], str)
|
changesByProject[change.Project] = append(changesByProject[change.Project], str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2022 The Dawn Authors.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package dawn holds constants specific to the Dawn project
|
||||||
|
package dawn
|
||||||
|
|
||||||
|
// The Dawn gerrit URL
|
||||||
|
const GerritURL = "https://dawn-review.googlesource.com/"
|
|
@ -12,78 +12,115 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// gerrit provides helpers for obtaining information from Tint's gerrit instance
|
// Package gerrit provides helpers for obtaining information from Tint's gerrit instance
|
||||||
package gerrit
|
package gerrit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/andygrunwald/go-gerrit"
|
"github.com/andygrunwald/go-gerrit"
|
||||||
)
|
)
|
||||||
|
|
||||||
const URL = "https://dawn-review.googlesource.com/"
|
// Gerrit is the interface to gerrit
|
||||||
|
type Gerrit struct {
|
||||||
// G is the interface to gerrit
|
|
||||||
type G struct {
|
|
||||||
client *gerrit.Client
|
client *gerrit.Client
|
||||||
authenticated bool
|
authenticated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
// Credentials holds the user name and password used to access Gerrit.
|
||||||
|
type Credentials struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadCredentials() (user, pass string) {
|
// Patchset refers to a single gerrit patchset
|
||||||
cookiesFile := os.Getenv("HOME") + "/.gitcookies"
|
type Patchset struct {
|
||||||
if cookies, err := ioutil.ReadFile(cookiesFile); err == nil {
|
// Gerrit host
|
||||||
re := regexp.MustCompile(`dawn-review.googlesource.com\s+(?:FALSE|TRUE)[\s/]+(?:FALSE|TRUE)\s+[0-9]+\s+.\s+(.*)=(.*)`)
|
Host string
|
||||||
match := re.FindStringSubmatch(string(cookies))
|
// Gerrit project
|
||||||
if len(match) == 3 {
|
Project string
|
||||||
return match[1], match[2]
|
// Change ID
|
||||||
}
|
Change int
|
||||||
}
|
// Patchset ID
|
||||||
return "", ""
|
Patchset int
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg Config) (*G, error) {
|
// ChangeInfo is an alias to gerrit.ChangeInfo
|
||||||
client, err := gerrit.NewClient(URL, nil)
|
type ChangeInfo = gerrit.ChangeInfo
|
||||||
|
|
||||||
|
// LatestPatchest returns the latest Patchset from the ChangeInfo
|
||||||
|
func LatestPatchest(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCredentials attempts to load the gerrit credentials for the given gerrit
|
||||||
|
// URL from the git cookies file. Returns an empty Credentials on failure.
|
||||||
|
func LoadCredentials(url string) Credentials {
|
||||||
|
cookiesFile := os.Getenv("HOME") + "/.gitcookies"
|
||||||
|
if cookies, err := ioutil.ReadFile(cookiesFile); err == nil {
|
||||||
|
url := strings.TrimPrefix(url, "https://")
|
||||||
|
re := regexp.MustCompile(url + `\s+(?:FALSE|TRUE)[\s/]+(?:FALSE|TRUE)\s+[0-9]+\s+.\s+(.*)=(.*)`)
|
||||||
|
match := re.FindStringSubmatch(string(cookies))
|
||||||
|
if len(match) == 3 {
|
||||||
|
return Credentials{match[1], match[2]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Credentials{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(url string, cred Credentials) (*Gerrit, error) {
|
||||||
|
client, err := gerrit.NewClient(url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't create gerrit client: %w", err)
|
return nil, fmt.Errorf("couldn't create gerrit client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, pass := cfg.Username, cfg.Password
|
if cred.Username == "" {
|
||||||
if user == "" {
|
cred = LoadCredentials(url)
|
||||||
user, pass = LoadCredentials()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if user != "" {
|
if cred.Username != "" {
|
||||||
client.Authentication.SetBasicAuth(user, pass)
|
client.Authentication.SetBasicAuth(cred.Username, cred.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &G{client, user != ""}, nil
|
return &Gerrit{client, cred.Username != ""}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *G) QueryChanges(queryParts ...string) (changes []gerrit.ChangeInfo, query string, err error) {
|
// 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(querys ...string) (changes []gerrit.ChangeInfo, query string, err error) {
|
||||||
changes = []gerrit.ChangeInfo{}
|
changes = []gerrit.ChangeInfo{}
|
||||||
query = strings.Join(queryParts, "+")
|
query = strings.Join(querys, "+")
|
||||||
for {
|
for {
|
||||||
batch, _, err := g.client.Changes.QueryChanges(&gerrit.QueryChangeOptions{
|
batch, _, err := g.client.Changes.QueryChanges(&gerrit.QueryChangeOptions{
|
||||||
QueryOptions: gerrit.QueryOptions{Query: []string{query}},
|
QueryOptions: gerrit.QueryOptions{Query: []string{query}},
|
||||||
Skip: len(changes),
|
Skip: len(changes),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !g.authenticated {
|
return nil, "", g.maybeWrapError(err)
|
||||||
err = fmt.Errorf(`query failed, possibly because of authentication.
|
|
||||||
See https://dawn-review.googlesource.com/new-password for obtaining a username
|
|
||||||
and password which can be provided with --gerrit-user and --gerrit-pass.
|
|
||||||
%w`, err)
|
|
||||||
}
|
|
||||||
return nil, "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changes = append(changes, *batch...)
|
changes = append(changes, *batch...)
|
||||||
|
@ -93,3 +130,96 @@ func (g *G) QueryChanges(queryParts ...string) (changes []gerrit.ChangeInfo, que
|
||||||
}
|
}
|
||||||
return changes, query, nil
|
return changes, query, 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 g.maybeWrapError(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, g.maybeWrapError(err)
|
||||||
|
}
|
||||||
|
return change, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditFiles replaces the content of the files in the given change.
|
||||||
|
// 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) (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{}, g.maybeWrapError(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{}, g.maybeWrapError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := g.client.Changes.PublishChangeEdit(changeID, "NONE")
|
||||||
|
if err != nil && resp.StatusCode != 409 { // 409 no changes were made
|
||||||
|
return Patchset{}, g.maybeWrapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.LatestPatchest(changeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestPatchest returns the latest patchset for the change.
|
||||||
|
func (g *Gerrit) LatestPatchest(changeID string) (Patchset, error) {
|
||||||
|
change, _, err := g.client.Changes.GetChange(changeID, &gerrit.ChangeOptions{
|
||||||
|
AdditionalFields: []string{"CURRENT_REVISION"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return Patchset{}, g.maybeWrapError(err)
|
||||||
|
}
|
||||||
|
ps := Patchset{
|
||||||
|
Host: g.client.BaseURL().Host,
|
||||||
|
Project: change.Project,
|
||||||
|
Change: change.Number,
|
||||||
|
Patchset: change.Revisions[change.CurrentRevision].Number,
|
||||||
|
}
|
||||||
|
return ps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment posts a review comment on the given patchset.
|
||||||
|
func (g *Gerrit) Comment(ps Patchset, msg string) error {
|
||||||
|
_, _, err := g.client.Changes.SetReview(
|
||||||
|
strconv.Itoa(ps.Change),
|
||||||
|
strconv.Itoa(ps.Patchset),
|
||||||
|
&gerrit.ReviewInput{
|
||||||
|
Message: msg,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return g.maybeWrapError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (g *Gerrit) maybeWrapError(err error) error {
|
||||||
|
if err != nil && !g.authenticated {
|
||||||
|
return fmt.Errorf(`query failed, possibly because of authentication.
|
||||||
|
See https://dawn-review.googlesource.com/new-password for obtaining a username
|
||||||
|
and password which can be provided with --gerrit-user and --gerrit-pass.
|
||||||
|
Note: This tool will scan ~/.gitcookies for credentials.
|
||||||
|
%w`, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue