Preliminary gh release commands

This commit is contained in:
Mislav Marohnić 2020-08-19 18:25:02 +02:00
parent 0cc59488a8
commit c4f5d6db58
9 changed files with 477 additions and 7 deletions

View file

@ -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 {

View file

@ -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

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View file

@ -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