Skip to content

Commit 0ca6046

Browse files
committed
gerrit: add support for creating and editing CLs
Add methods for the following endpoints: • https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-changehttps://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#put-edit-filehttps://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#publish-edit These are needed to be able to mail a Gerrit CL via the Gerrit API, which will be used as part of release automation. Add support for specifying a raw (not JSON-encoded) request body, and improve miscellaneous style inconsistencies and lint issues. For golang/go#38075. Change-Id: Ic556d6b1f0fb6f56cfc61c50b76657b9aeea443a Reviewed-on: https://go-review.googlesource.com/c/build/+/350630 Run-TryBot: Dmitri Shuralyov <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Alexander Rakoczy <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]> Trust: Dmitri Shuralyov <[email protected]>
1 parent c354d4c commit 0ca6046

File tree

1 file changed

+81
-27
lines changed

1 file changed

+81
-27
lines changed

gerrit/gerrit.go

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (e *HTTPError) Error() string {
6969
return fmt.Sprintf("HTTP status %s; %s", e.Res.Status, e.Body)
7070
}
7171

72-
// doArg is one of urlValues, reqBody, or wantResStatus
72+
// doArg is an optional argument for the Client.do method.
7373
type doArg interface {
7474
isDoArg()
7575
}
@@ -78,41 +78,48 @@ type wantResStatus int
7878

7979
func (wantResStatus) isDoArg() {}
8080

81-
type reqBody struct{ body interface{} }
81+
// reqBodyJSON sets the request body to a JSON encoding of v,
82+
// and the request's Content-Type header to "application/json".
83+
type reqBodyJSON struct{ v interface{} }
8284

83-
func (reqBody) isDoArg() {}
85+
func (reqBodyJSON) isDoArg() {}
86+
87+
// reqBodyRaw sets the request body to r,
88+
// and the request's Content-Type header to "application/octet-stream".
89+
type reqBodyRaw struct{ r io.Reader }
90+
91+
func (reqBodyRaw) isDoArg() {}
8492

8593
type urlValues url.Values
8694

8795
func (urlValues) isDoArg() {}
8896

8997
func (c *Client) do(ctx context.Context, dst interface{}, method, path string, opts ...doArg) error {
9098
var arg url.Values
91-
var body interface{}
99+
var body io.Reader
100+
var contentType string
92101
var wantStatus = http.StatusOK
93102
for _, opt := range opts {
94103
switch opt := opt.(type) {
95104
case wantResStatus:
96105
wantStatus = int(opt)
97-
case reqBody:
98-
body = opt.body
106+
case reqBodyJSON:
107+
b, err := json.MarshalIndent(opt.v, "", " ")
108+
if err != nil {
109+
return err
110+
}
111+
body = bytes.NewReader(b)
112+
contentType = "application/json"
113+
case reqBodyRaw:
114+
body = opt.r
115+
contentType = "application/octet-stream"
99116
case urlValues:
100117
arg = url.Values(opt)
101118
default:
102119
panic(fmt.Sprintf("internal error; unsupported type %T", opt))
103120
}
104121
}
105122

106-
var bodyr io.Reader
107-
var contentType string
108-
if body != nil {
109-
v, err := json.MarshalIndent(body, "", " ")
110-
if err != nil {
111-
return err
112-
}
113-
bodyr = bytes.NewReader(v)
114-
contentType = "application/json"
115-
}
116123
// slashA is either "/a" (for authenticated requests) or "" for unauthenticated.
117124
// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication
118125
slashA := "/a"
@@ -124,15 +131,15 @@ func (c *Client) do(ctx context.Context, dst interface{}, method, path string, o
124131
if arg != nil {
125132
u += "?" + arg.Encode()
126133
}
127-
req, err := http.NewRequest(method, u, bodyr)
134+
req, err := http.NewRequestWithContext(ctx, method, u, body)
128135
if err != nil {
129136
return err
130137
}
131138
if contentType != "" {
132139
req.Header.Set("Content-Type", contentType)
133140
}
134141
c.auth.setAuth(c, req)
135-
res, err := c.httpClient().Do(req.WithContext(ctx))
142+
res, err := c.httpClient().Do(req)
136143
if err != nil {
137144
return err
138145
}
@@ -143,6 +150,14 @@ func (c *Client) do(ctx context.Context, dst interface{}, method, path string, o
143150
return &HTTPError{res, body, err}
144151
}
145152

153+
if dst == nil {
154+
// Drain the response body, return an error if it's anything but empty.
155+
body, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
156+
if err != nil || len(body) != 0 {
157+
return &HTTPError{res, body, err}
158+
}
159+
return nil
160+
}
146161
// The JSON response begins with an XSRF-defeating header
147162
// like ")]}\n". Read that and skip it.
148163
br := bufio.NewReader(res.Body)
@@ -456,15 +471,15 @@ func (c *Client) GetChange(ctx context.Context, changeID string, opts ...QueryCh
456471
default:
457472
return nil, errors.New("only 1 option struct supported")
458473
}
459-
change := new(ChangeInfo)
460-
err := c.do(ctx, change, "GET", "/changes/"+changeID, urlValues{
474+
var change ChangeInfo
475+
err := c.do(ctx, &change, "GET", "/changes/"+changeID, urlValues{
461476
"n": condInt(opt.N),
462477
"o": opt.Fields,
463478
})
464479
if he, ok := err.(*HTTPError); ok && he.Res.StatusCode == 404 {
465480
return nil, ErrChangeNotExist
466481
}
467-
return change, err
482+
return &change, err
468483
}
469484

470485
// GetChangeDetail retrieves a change with labels, detailed labels, detailed
@@ -570,7 +585,7 @@ type reviewInfo struct {
570585
func (c *Client) SetReview(ctx context.Context, changeID, revision string, review ReviewInput) error {
571586
var res reviewInfo
572587
return c.do(ctx, &res, "POST", fmt.Sprintf("/changes/%s/revisions/%s/review", changeID, revision),
573-
reqBody{review})
588+
reqBodyJSON{&review})
574589
}
575590

576591
// ReviewerInfo contains information about reviewers of a change.
@@ -606,7 +621,7 @@ type HashtagsInput struct {
606621
// See https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.1/rest-api-changes.html#set-hashtags
607622
func (c *Client) SetHashtags(ctx context.Context, changeID string, hashtags HashtagsInput) ([]string, error) {
608623
var res []string
609-
err := c.do(ctx, &res, "POST", fmt.Sprintf("/changes/%s/hashtags", changeID), reqBody{hashtags})
624+
err := c.do(ctx, &res, "POST", fmt.Sprintf("/changes/%s/hashtags", changeID), reqBodyJSON{&hashtags})
610625
return res, err
611626
}
612627

@@ -645,7 +660,7 @@ func (c *Client) AbandonChange(ctx context.Context, changeID string, message ...
645660
Message string `json:"message,omitempty"`
646661
}{msg}
647662
var change ChangeInfo
648-
return c.do(ctx, &change, "POST", "/changes/"+changeID+"/abandon", reqBody{&b})
663+
return c.do(ctx, &change, "POST", "/changes/"+changeID+"/abandon", reqBodyJSON{&b})
649664
}
650665

651666
// ProjectInput contains the options for creating a new project.
@@ -679,7 +694,7 @@ type ProjectInfo struct {
679694
// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-projects
680695
func (c *Client) ListProjects(ctx context.Context) ([]ProjectInfo, error) {
681696
var res map[string]ProjectInfo
682-
err := c.do(ctx, &res, "GET", fmt.Sprintf("/projects/"))
697+
err := c.do(ctx, &res, "GET", "/projects/")
683698
if err != nil {
684699
return nil, err
685700
}
@@ -707,10 +722,49 @@ func (c *Client) CreateProject(ctx context.Context, name string, p ...ProjectInp
707722
pi = p[0]
708723
}
709724
var res ProjectInfo
710-
err := c.do(ctx, &res, "PUT", fmt.Sprintf("/projects/%s", name), reqBody{&pi}, wantResStatus(http.StatusCreated))
725+
err := c.do(ctx, &res, "PUT", fmt.Sprintf("/projects/%s", name), reqBodyJSON{&pi}, wantResStatus(http.StatusCreated))
711726
return res, err
712727
}
713728

729+
// CreateChange creates a new change.
730+
//
731+
// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-change.
732+
func (c *Client) CreateChange(ctx context.Context, ci ChangeInput) (ChangeInfo, error) {
733+
var res ChangeInfo
734+
err := c.do(ctx, &res, "POST", "/changes/", reqBodyJSON{&ci}, wantResStatus(http.StatusCreated))
735+
return res, err
736+
}
737+
738+
// ChangeInput contains the options for creating a new change.
739+
// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-input.
740+
type ChangeInput struct {
741+
Project string `json:"project"`
742+
Branch string `json:"branch"`
743+
Subject string `json:"subject"`
744+
}
745+
746+
// ChangeFileContentInChangeEdit puts content of a file to a change edit.
747+
//
748+
// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#put-edit-file.
749+
func (c *Client) ChangeFileContentInChangeEdit(ctx context.Context, changeID string, path string, content string) error {
750+
err := c.do(ctx, nil, "PUT", "/changes/"+changeID+"/edit/"+url.QueryEscape(path),
751+
reqBodyRaw{strings.NewReader(content)}, wantResStatus(http.StatusNoContent))
752+
if he, ok := err.(*HTTPError); ok && he.Res.StatusCode == http.StatusConflict {
753+
// The change edit was a no-op.
754+
// Note: If/when there's a need inside x/build to handle this differently,
755+
// maybe it'll be a good time to return something other than a *HTTPError
756+
// and document it as part of the API.
757+
}
758+
return err
759+
}
760+
761+
// PublishChangeEdit promotes the change edit to a regular patch set.
762+
//
763+
// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#publish-edit.
764+
func (c *Client) PublishChangeEdit(ctx context.Context, changeID string) error {
765+
return c.do(ctx, nil, "POST", "/changes/"+changeID+"/edit:publish", wantResStatus(http.StatusNoContent))
766+
}
767+
714768
// ErrProjectNotExist is returned when a project doesn't exist.
715769
// It is not necessarily returned unless a method is documented as
716770
// returning it.
@@ -896,7 +950,7 @@ func (ts TimeStamp) MarshalJSON() ([]byte, error) {
896950

897951
func (ts *TimeStamp) UnmarshalJSON(p []byte) error {
898952
if len(p) < 2 {
899-
return errors.New("Timestamp too short")
953+
return errors.New("timestamp too short")
900954
}
901955
if p[0] != '"' || p[len(p)-1] != '"' {
902956
return errors.New("not double-quoted")

0 commit comments

Comments
 (0)