Preliminary gh release commands
This commit is contained in:
parent
0cc59488a8
commit
c4f5d6db58
9 changed files with 477 additions and 7 deletions
|
|
@ -191,6 +191,9 @@ type HTTPError struct {
|
|||
|
||||
func (err HTTPError) Error() string {
|
||||
if err.Message != "" {
|
||||
if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 {
|
||||
return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1])
|
||||
}
|
||||
return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL)
|
||||
}
|
||||
return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL)
|
||||
|
|
@ -222,7 +225,7 @@ func (c Client) HasMinimumScopes(hostname string) error {
|
|||
}()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return handleHTTPError(res)
|
||||
return HandleHTTPError(res)
|
||||
}
|
||||
|
||||
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
|
|||
|
||||
success := resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
if !success {
|
||||
return handleHTTPError(resp)
|
||||
return HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
|
|
@ -322,7 +325,7 @@ func handleResponse(resp *http.Response, data interface{}) error {
|
|||
success := resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
|
||||
if !success {
|
||||
return handleHTTPError(resp)
|
||||
return HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
|
@ -342,13 +345,18 @@ func handleResponse(resp *http.Response, data interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleHTTPError(resp *http.Response) error {
|
||||
func HandleHTTPError(resp *http.Response) error {
|
||||
httpError := HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
RequestURL: resp.Request.URL,
|
||||
OAuthScopes: resp.Header.Get("X-Oauth-Scopes"),
|
||||
}
|
||||
|
||||
if !jsonTypeRE.MatchString(resp.Header.Get("Content-Type")) {
|
||||
httpError.Message = resp.Status
|
||||
return httpError
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
httpError.Message = err.Error()
|
||||
|
|
@ -357,14 +365,57 @@ func handleHTTPError(resp *http.Response) error {
|
|||
|
||||
var parsedBody struct {
|
||||
Message string `json:"message"`
|
||||
Errors []json.RawMessage
|
||||
}
|
||||
if err := json.Unmarshal(body, &parsedBody); err == nil {
|
||||
httpError.Message = parsedBody.Message
|
||||
if err := json.Unmarshal(body, &parsedBody); err != nil {
|
||||
return httpError
|
||||
}
|
||||
|
||||
type errorObject struct {
|
||||
Message string
|
||||
Resource string
|
||||
Field string
|
||||
Code string
|
||||
}
|
||||
|
||||
messages := []string{parsedBody.Message}
|
||||
for _, raw := range parsedBody.Errors {
|
||||
switch raw[0] {
|
||||
case '"':
|
||||
var errString string
|
||||
_ = json.Unmarshal(raw, &errString)
|
||||
messages = append(messages, errString)
|
||||
case '{':
|
||||
var errInfo errorObject
|
||||
_ = json.Unmarshal(raw, &errInfo)
|
||||
msg := errInfo.Message
|
||||
if errInfo.Code != "custom" {
|
||||
msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code))
|
||||
}
|
||||
if msg != "" {
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
httpError.Message = strings.Join(messages, "\n")
|
||||
|
||||
return httpError
|
||||
}
|
||||
|
||||
func errorCodeToMessage(code string) string {
|
||||
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors
|
||||
switch code {
|
||||
case "missing", "missing_field":
|
||||
return "is missing"
|
||||
case "invalid", "unprocessable":
|
||||
return "is invalid"
|
||||
case "already_exists":
|
||||
return "already exists"
|
||||
default:
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
|
||||
|
||||
func inspectableMIMEType(t string) bool {
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.Rea
|
|||
if resp.StatusCode == 404 {
|
||||
return nil, &NotFoundError{errors.New("pull request not found")}
|
||||
} else if resp.StatusCode != 200 {
|
||||
return nil, handleHTTPError(resp)
|
||||
return nil, HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
|
|
|
|||
114
pkg/cmd/release/create/create.go
Normal file
114
pkg/cmd/release/create/create.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CreateOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
TagName string
|
||||
}
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
||||
opts := &CreateOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <tag>",
|
||||
Short: "Create a new release",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
opts.TagName = args[0]
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return createRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createRun(opts *CreateOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"tag_name": opts.TagName,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("repos/%s/%s/releases", baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
success := resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
if !success {
|
||||
return api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var newRelease struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
AssetsURL string `json:"assets_url"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &newRelease)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s\n", newRelease.HTMLURL)
|
||||
|
||||
return nil
|
||||
}
|
||||
71
pkg/cmd/release/list/http.go
Normal file
71
pkg/cmd/release/list/http.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
Name string
|
||||
TagName string
|
||||
IsDraft bool
|
||||
IsPrerelease bool
|
||||
PublishedAt time.Time
|
||||
}
|
||||
|
||||
func fetchReleases(httpClient *http.Client, repo ghrepo.Interface, limit int) ([]Release, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Releases struct {
|
||||
Nodes []Release
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"releases(first: $perPage, orderBy: {field: CREATED_AT, direction: DESC}, after: $endCursor)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
perPage := limit
|
||||
if limit > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
|
||||
|
||||
var releases []Release
|
||||
loop:
|
||||
for {
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryReleaseList", &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range query.Repository.Releases.Nodes {
|
||||
releases = append(releases, r)
|
||||
if len(releases) == limit {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if !query.Repository.Releases.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.Releases.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
79
pkg/cmd/release/list/list.go
Normal file
79
pkg/cmd/release/list/list.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/text"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ListOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
LimitResults int
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
opts := &ListOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List releases in a repository",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return listRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of items to fetch")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releases, err := fetchReleases(httpClient, baseRepo, opts.LimitResults)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
table := utils.NewTablePrinter(opts.IO)
|
||||
for _, rel := range releases {
|
||||
table.AddField(rel.TagName, nil, nil)
|
||||
table.AddField(text.ReplaceExcessiveWhitespace(rel.Name), nil, nil)
|
||||
table.AddField(utils.FuzzyAgo(now.Sub(rel.PublishedAt)), nil, nil)
|
||||
table.EndRow()
|
||||
}
|
||||
err = table.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
27
pkg/cmd/release/release.go
Normal file
27
pkg/cmd/release/release.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package release
|
||||
|
||||
import (
|
||||
cmdCreate "github.com/cli/cli/pkg/cmd/release/create"
|
||||
cmdList "github.com/cli/cli/pkg/cmd/release/list"
|
||||
cmdView "github.com/cli/cli/pkg/cmd/release/view"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdRelease(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "release <command>",
|
||||
Short: "Manage GitHub releases",
|
||||
Annotations: map[string]string{
|
||||
"IsCore": "true",
|
||||
},
|
||||
}
|
||||
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
|
||||
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
51
pkg/cmd/release/view/http.go
Normal file
51
pkg/cmd/release/view/http.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
TagName string
|
||||
Name string
|
||||
Description string
|
||||
URL string
|
||||
IsDraft bool
|
||||
IsPrerelease bool
|
||||
PublishedAt time.Time
|
||||
|
||||
Author struct {
|
||||
Login string
|
||||
}
|
||||
|
||||
ReleaseAssets struct {
|
||||
Nodes []struct {
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
} `graphql:"releaseAssets(first: 100)"`
|
||||
}
|
||||
|
||||
func fetchRelease(httpClient *http.Client, repo ghrepo.Interface, tagName string) (*Release, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Release *Release `graphql:"release(tagName: $tagName)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"tagName": githubv4.String(tagName),
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryReleaseByTag", &query, variables)
|
||||
return query.Repository.Release, err
|
||||
}
|
||||
75
pkg/cmd/release/view/view.go
Normal file
75
pkg/cmd/release/view/view.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ViewOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
TagName string
|
||||
}
|
||||
|
||||
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
|
||||
opts := &ViewOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "view <tag>",
|
||||
Short: "View information about a release",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
opts.TagName = args[0]
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return viewRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func viewRun(opts *ViewOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
release, err := fetchRelease(httpClient, baseRepo, opts.TagName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s\n", release.TagName)
|
||||
|
||||
renderedDescription, err := utils.RenderMarkdown(release.Description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(opts.IO.Out, renderedDescription)
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s\n", release.URL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import (
|
|||
gistCmd "github.com/cli/cli/pkg/cmd/gist"
|
||||
issueCmd "github.com/cli/cli/pkg/cmd/issue"
|
||||
prCmd "github.com/cli/cli/pkg/cmd/pr"
|
||||
releaseCmd "github.com/cli/cli/pkg/cmd/release"
|
||||
repoCmd "github.com/cli/cli/pkg/cmd/repo"
|
||||
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
|
|
@ -114,6 +115,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
|
||||
cmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(issueCmd.NewCmdIssue(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(releaseCmd.NewCmdRelease(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(repoCmd.NewCmdRepo(&repoResolvingCmdFactory))
|
||||
|
||||
return cmd
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue