@@ -69,7 +69,7 @@ func (e *HTTPError) Error() string {
69
69
return fmt .Sprintf ("HTTP status %s; %s" , e .Res .Status , e .Body )
70
70
}
71
71
72
- // doArg is one of urlValues, reqBody, or wantResStatus
72
+ // doArg is an optional argument for the Client.do method.
73
73
type doArg interface {
74
74
isDoArg ()
75
75
}
@@ -78,41 +78,48 @@ type wantResStatus int
78
78
79
79
func (wantResStatus ) isDoArg () {}
80
80
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 {} }
82
84
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 () {}
84
92
85
93
type urlValues url.Values
86
94
87
95
func (urlValues ) isDoArg () {}
88
96
89
97
func (c * Client ) do (ctx context.Context , dst interface {}, method , path string , opts ... doArg ) error {
90
98
var arg url.Values
91
- var body interface {}
99
+ var body io.Reader
100
+ var contentType string
92
101
var wantStatus = http .StatusOK
93
102
for _ , opt := range opts {
94
103
switch opt := opt .(type ) {
95
104
case wantResStatus :
96
105
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"
99
116
case urlValues :
100
117
arg = url .Values (opt )
101
118
default :
102
119
panic (fmt .Sprintf ("internal error; unsupported type %T" , opt ))
103
120
}
104
121
}
105
122
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
- }
116
123
// slashA is either "/a" (for authenticated requests) or "" for unauthenticated.
117
124
// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication
118
125
slashA := "/a"
@@ -124,15 +131,15 @@ func (c *Client) do(ctx context.Context, dst interface{}, method, path string, o
124
131
if arg != nil {
125
132
u += "?" + arg .Encode ()
126
133
}
127
- req , err := http .NewRequest ( method , u , bodyr )
134
+ req , err := http .NewRequestWithContext ( ctx , method , u , body )
128
135
if err != nil {
129
136
return err
130
137
}
131
138
if contentType != "" {
132
139
req .Header .Set ("Content-Type" , contentType )
133
140
}
134
141
c .auth .setAuth (c , req )
135
- res , err := c .httpClient ().Do (req . WithContext ( ctx ) )
142
+ res , err := c .httpClient ().Do (req )
136
143
if err != nil {
137
144
return err
138
145
}
@@ -143,6 +150,14 @@ func (c *Client) do(ctx context.Context, dst interface{}, method, path string, o
143
150
return & HTTPError {res , body , err }
144
151
}
145
152
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
+ }
146
161
// The JSON response begins with an XSRF-defeating header
147
162
// like ")]}\n". Read that and skip it.
148
163
br := bufio .NewReader (res .Body )
@@ -456,15 +471,15 @@ func (c *Client) GetChange(ctx context.Context, changeID string, opts ...QueryCh
456
471
default :
457
472
return nil , errors .New ("only 1 option struct supported" )
458
473
}
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 {
461
476
"n" : condInt (opt .N ),
462
477
"o" : opt .Fields ,
463
478
})
464
479
if he , ok := err .(* HTTPError ); ok && he .Res .StatusCode == 404 {
465
480
return nil , ErrChangeNotExist
466
481
}
467
- return change , err
482
+ return & change , err
468
483
}
469
484
470
485
// GetChangeDetail retrieves a change with labels, detailed labels, detailed
@@ -570,7 +585,7 @@ type reviewInfo struct {
570
585
func (c * Client ) SetReview (ctx context.Context , changeID , revision string , review ReviewInput ) error {
571
586
var res reviewInfo
572
587
return c .do (ctx , & res , "POST" , fmt .Sprintf ("/changes/%s/revisions/%s/review" , changeID , revision ),
573
- reqBody { review })
588
+ reqBodyJSON { & review })
574
589
}
575
590
576
591
// ReviewerInfo contains information about reviewers of a change.
@@ -606,7 +621,7 @@ type HashtagsInput struct {
606
621
// See https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.1/rest-api-changes.html#set-hashtags
607
622
func (c * Client ) SetHashtags (ctx context.Context , changeID string , hashtags HashtagsInput ) ([]string , error ) {
608
623
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 })
610
625
return res , err
611
626
}
612
627
@@ -645,7 +660,7 @@ func (c *Client) AbandonChange(ctx context.Context, changeID string, message ...
645
660
Message string `json:"message,omitempty"`
646
661
}{msg }
647
662
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 })
649
664
}
650
665
651
666
// ProjectInput contains the options for creating a new project.
@@ -679,7 +694,7 @@ type ProjectInfo struct {
679
694
// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-projects
680
695
func (c * Client ) ListProjects (ctx context.Context ) ([]ProjectInfo , error ) {
681
696
var res map [string ]ProjectInfo
682
- err := c .do (ctx , & res , "GET" , fmt . Sprintf ( "/projects/" ) )
697
+ err := c .do (ctx , & res , "GET" , "/projects/" )
683
698
if err != nil {
684
699
return nil , err
685
700
}
@@ -707,10 +722,49 @@ func (c *Client) CreateProject(ctx context.Context, name string, p ...ProjectInp
707
722
pi = p [0 ]
708
723
}
709
724
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 ))
711
726
return res , err
712
727
}
713
728
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
+
714
768
// ErrProjectNotExist is returned when a project doesn't exist.
715
769
// It is not necessarily returned unless a method is documented as
716
770
// returning it.
@@ -896,7 +950,7 @@ func (ts TimeStamp) MarshalJSON() ([]byte, error) {
896
950
897
951
func (ts * TimeStamp ) UnmarshalJSON (p []byte ) error {
898
952
if len (p ) < 2 {
899
- return errors .New ("Timestamp too short" )
953
+ return errors .New ("timestamp too short" )
900
954
}
901
955
if p [0 ] != '"' || p [len (p )- 1 ] != '"' {
902
956
return errors .New ("not double-quoted" )
0 commit comments