X Tutup
Skip to content

Commit c4f5d6d

Browse files
committed
Preliminary gh release commands
1 parent 0cc5948 commit c4f5d6d

File tree

9 files changed

+477
-7
lines changed

9 files changed

+477
-7
lines changed

api/client.go

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ type HTTPError struct {
191191

192192
func (err HTTPError) Error() string {
193193
if err.Message != "" {
194+
if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 {
195+
return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1])
196+
}
194197
return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL)
195198
}
196199
return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL)
@@ -222,7 +225,7 @@ func (c Client) HasMinimumScopes(hostname string) error {
222225
}()
223226

224227
if res.StatusCode != 200 {
225-
return handleHTTPError(res)
228+
return HandleHTTPError(res)
226229
}
227230

228231
hasScopes := strings.Split(res.Header.Get("X-Oauth-Scopes"), ",")
@@ -298,7 +301,7 @@ func (c Client) REST(hostname string, method string, p string, body io.Reader, d
298301

299302
success := resp.StatusCode >= 200 && resp.StatusCode < 300
300303
if !success {
301-
return handleHTTPError(resp)
304+
return HandleHTTPError(resp)
302305
}
303306

304307
if resp.StatusCode == http.StatusNoContent {
@@ -322,7 +325,7 @@ func handleResponse(resp *http.Response, data interface{}) error {
322325
success := resp.StatusCode >= 200 && resp.StatusCode < 300
323326

324327
if !success {
325-
return handleHTTPError(resp)
328+
return HandleHTTPError(resp)
326329
}
327330

328331
body, err := ioutil.ReadAll(resp.Body)
@@ -342,13 +345,18 @@ func handleResponse(resp *http.Response, data interface{}) error {
342345
return nil
343346
}
344347

345-
func handleHTTPError(resp *http.Response) error {
348+
func HandleHTTPError(resp *http.Response) error {
346349
httpError := HTTPError{
347350
StatusCode: resp.StatusCode,
348351
RequestURL: resp.Request.URL,
349352
OAuthScopes: resp.Header.Get("X-Oauth-Scopes"),
350353
}
351354

355+
if !jsonTypeRE.MatchString(resp.Header.Get("Content-Type")) {
356+
httpError.Message = resp.Status
357+
return httpError
358+
}
359+
352360
body, err := ioutil.ReadAll(resp.Body)
353361
if err != nil {
354362
httpError.Message = err.Error()
@@ -357,14 +365,57 @@ func handleHTTPError(resp *http.Response) error {
357365

358366
var parsedBody struct {
359367
Message string `json:"message"`
368+
Errors []json.RawMessage
360369
}
361-
if err := json.Unmarshal(body, &parsedBody); err == nil {
362-
httpError.Message = parsedBody.Message
370+
if err := json.Unmarshal(body, &parsedBody); err != nil {
371+
return httpError
363372
}
364373

374+
type errorObject struct {
375+
Message string
376+
Resource string
377+
Field string
378+
Code string
379+
}
380+
381+
messages := []string{parsedBody.Message}
382+
for _, raw := range parsedBody.Errors {
383+
switch raw[0] {
384+
case '"':
385+
var errString string
386+
_ = json.Unmarshal(raw, &errString)
387+
messages = append(messages, errString)
388+
case '{':
389+
var errInfo errorObject
390+
_ = json.Unmarshal(raw, &errInfo)
391+
msg := errInfo.Message
392+
if errInfo.Code != "custom" {
393+
msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code))
394+
}
395+
if msg != "" {
396+
messages = append(messages, msg)
397+
}
398+
}
399+
}
400+
httpError.Message = strings.Join(messages, "\n")
401+
365402
return httpError
366403
}
367404

405+
func errorCodeToMessage(code string) string {
406+
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors
407+
switch code {
408+
case "missing", "missing_field":
409+
return "is missing"
410+
case "invalid", "unprocessable":
411+
return "is invalid"
412+
case "already_exists":
413+
return "already exists"
414+
default:
415+
return code
416+
}
417+
}
418+
368419
var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
369420

370421
func inspectableMIMEType(t string) bool {

api/queries_pr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.Rea
227227
if resp.StatusCode == 404 {
228228
return nil, &NotFoundError{errors.New("pull request not found")}
229229
} else if resp.StatusCode != 200 {
230-
return nil, handleHTTPError(resp)
230+
return nil, HandleHTTPError(resp)
231231
}
232232

233233
return resp.Body, nil

pkg/cmd/release/create/create.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package list
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
10+
"github.com/cli/cli/api"
11+
"github.com/cli/cli/internal/ghinstance"
12+
"github.com/cli/cli/internal/ghrepo"
13+
"github.com/cli/cli/pkg/cmdutil"
14+
"github.com/cli/cli/pkg/iostreams"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
type CreateOptions struct {
19+
HttpClient func() (*http.Client, error)
20+
IO *iostreams.IOStreams
21+
BaseRepo func() (ghrepo.Interface, error)
22+
23+
TagName string
24+
}
25+
26+
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
27+
opts := &CreateOptions{
28+
IO: f.IOStreams,
29+
HttpClient: f.HttpClient,
30+
}
31+
32+
cmd := &cobra.Command{
33+
Use: "create <tag>",
34+
Short: "Create a new release",
35+
Args: cobra.ExactArgs(1),
36+
RunE: func(cmd *cobra.Command, args []string) error {
37+
// support `-R, --repo` override
38+
opts.BaseRepo = f.BaseRepo
39+
40+
opts.TagName = args[0]
41+
42+
if runF != nil {
43+
return runF(opts)
44+
}
45+
return createRun(opts)
46+
},
47+
}
48+
49+
return cmd
50+
}
51+
52+
func createRun(opts *CreateOptions) error {
53+
httpClient, err := opts.HttpClient()
54+
if err != nil {
55+
return err
56+
}
57+
58+
baseRepo, err := opts.BaseRepo()
59+
if err != nil {
60+
return err
61+
}
62+
63+
params := map[string]interface{}{
64+
"tag_name": opts.TagName,
65+
}
66+
67+
bodyBytes, err := json.Marshal(params)
68+
if err != nil {
69+
return err
70+
}
71+
72+
path := fmt.Sprintf("repos/%s/%s/releases", baseRepo.RepoOwner(), baseRepo.RepoName())
73+
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
74+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
75+
if err != nil {
76+
return err
77+
}
78+
79+
req.Header.Set("Content-Type", "application/json; charset=utf-8")
80+
81+
resp, err := httpClient.Do(req)
82+
if err != nil {
83+
return err
84+
}
85+
defer resp.Body.Close()
86+
87+
success := resp.StatusCode >= 200 && resp.StatusCode < 300
88+
if !success {
89+
return api.HandleHTTPError(resp)
90+
}
91+
92+
if resp.StatusCode == http.StatusNoContent {
93+
return nil
94+
}
95+
96+
b, err := ioutil.ReadAll(resp.Body)
97+
if err != nil {
98+
return err
99+
}
100+
101+
var newRelease struct {
102+
HTMLURL string `json:"html_url"`
103+
AssetsURL string `json:"assets_url"`
104+
}
105+
106+
err = json.Unmarshal(b, &newRelease)
107+
if err != nil {
108+
return err
109+
}
110+
111+
fmt.Fprintf(opts.IO.Out, "%s\n", newRelease.HTMLURL)
112+
113+
return nil
114+
}

pkg/cmd/release/list/http.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package list
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"time"
7+
8+
"github.com/cli/cli/internal/ghinstance"
9+
"github.com/cli/cli/internal/ghrepo"
10+
"github.com/shurcooL/githubv4"
11+
"github.com/shurcooL/graphql"
12+
)
13+
14+
type Release struct {
15+
Name string
16+
TagName string
17+
IsDraft bool
18+
IsPrerelease bool
19+
PublishedAt time.Time
20+
}
21+
22+
func fetchReleases(httpClient *http.Client, repo ghrepo.Interface, limit int) ([]Release, error) {
23+
var query struct {
24+
Repository struct {
25+
Releases struct {
26+
Nodes []Release
27+
PageInfo struct {
28+
HasNextPage bool
29+
EndCursor string
30+
}
31+
} `graphql:"releases(first: $perPage, orderBy: {field: CREATED_AT, direction: DESC}, after: $endCursor)"`
32+
} `graphql:"repository(owner: $owner, name: $name)"`
33+
}
34+
35+
perPage := limit
36+
if limit > 100 {
37+
perPage = 100
38+
}
39+
40+
variables := map[string]interface{}{
41+
"owner": githubv4.String(repo.RepoOwner()),
42+
"name": githubv4.String(repo.RepoName()),
43+
"perPage": githubv4.Int(perPage),
44+
"endCursor": (*githubv4.String)(nil),
45+
}
46+
47+
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
48+
49+
var releases []Release
50+
loop:
51+
for {
52+
err := gql.QueryNamed(context.Background(), "RepositoryReleaseList", &query, variables)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
for _, r := range query.Repository.Releases.Nodes {
58+
releases = append(releases, r)
59+
if len(releases) == limit {
60+
break loop
61+
}
62+
}
63+
64+
if !query.Repository.Releases.PageInfo.HasNextPage {
65+
break
66+
}
67+
variables["endCursor"] = githubv4.String(query.Repository.Releases.PageInfo.EndCursor)
68+
}
69+
70+
return releases, nil
71+
}

pkg/cmd/release/list/list.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package list
2+
3+
import (
4+
"net/http"
5+
"time"
6+
7+
"github.com/cli/cli/internal/ghrepo"
8+
"github.com/cli/cli/pkg/cmdutil"
9+
"github.com/cli/cli/pkg/iostreams"
10+
"github.com/cli/cli/pkg/text"
11+
"github.com/cli/cli/utils"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
type ListOptions struct {
16+
HttpClient func() (*http.Client, error)
17+
IO *iostreams.IOStreams
18+
BaseRepo func() (ghrepo.Interface, error)
19+
20+
LimitResults int
21+
}
22+
23+
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
24+
opts := &ListOptions{
25+
IO: f.IOStreams,
26+
HttpClient: f.HttpClient,
27+
}
28+
29+
cmd := &cobra.Command{
30+
Use: "list",
31+
Short: "List releases in a repository",
32+
Args: cobra.NoArgs,
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
// support `-R, --repo` override
35+
opts.BaseRepo = f.BaseRepo
36+
37+
if runF != nil {
38+
return runF(opts)
39+
}
40+
return listRun(opts)
41+
},
42+
}
43+
44+
cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of items to fetch")
45+
46+
return cmd
47+
}
48+
49+
func listRun(opts *ListOptions) error {
50+
httpClient, err := opts.HttpClient()
51+
if err != nil {
52+
return err
53+
}
54+
55+
baseRepo, err := opts.BaseRepo()
56+
if err != nil {
57+
return err
58+
}
59+
60+
releases, err := fetchReleases(httpClient, baseRepo, opts.LimitResults)
61+
if err != nil {
62+
return err
63+
}
64+
65+
now := time.Now()
66+
table := utils.NewTablePrinter(opts.IO)
67+
for _, rel := range releases {
68+
table.AddField(rel.TagName, nil, nil)
69+
table.AddField(text.ReplaceExcessiveWhitespace(rel.Name), nil, nil)
70+
table.AddField(utils.FuzzyAgo(now.Sub(rel.PublishedAt)), nil, nil)
71+
table.EndRow()
72+
}
73+
err = table.Render()
74+
if err != nil {
75+
return err
76+
}
77+
78+
return nil
79+
}

0 commit comments

Comments
 (0)
X Tutup