From 401aef283f8e5fd01a064e652187e7d43b88bc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 1 Nov 2019 15:31:07 +0100 Subject: [PATCH] Prototype `issue create` --- api/queries.go | 1 + api/queries_issue.go | 70 +++++++++++++++++++++++++++++++++++++ command/issue.go | 83 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 api/queries_issue.go diff --git a/api/queries.go b/api/queries.go index 161aa9e59..1a405a858 100644 --- a/api/queries.go +++ b/api/queries.go @@ -32,6 +32,7 @@ type IssuesPayload struct { type Issue struct { Number int Title string + URL string } func Issues(client *Client, ghRepo Repo, currentUsername string) (*IssuesPayload, error) { diff --git a/api/queries_issue.go b/api/queries_issue.go new file mode 100644 index 000000000..c488a18b5 --- /dev/null +++ b/api/queries_issue.go @@ -0,0 +1,70 @@ +package api + +import "fmt" + +func IssueCreate(client *Client, ghRepo Repo, params map[string]interface{}) (*Issue, error) { + repoId, err := GitHubRepoId(client, ghRepo) + if err != nil { + return nil, err + } + + query := ` + mutation CreateIssue($input: CreateIssueInput!) { + createIssue(input: $input) { + issue { + url + } + } + }` + + inputParams := map[string]interface{}{ + "repositoryId": repoId, + } + for key, val := range params { + inputParams[key] = val + } + variables := map[string]interface{}{ + "input": inputParams, + } + + result := struct { + CreateIssue struct { + Issue Issue + } + }{} + + err = client.GraphQL(query, variables, &result) + if err != nil { + return nil, err + } + + return &result.CreateIssue.Issue, nil +} + +func GitHubRepoId(client *Client, ghRepo Repo) (string, error) { + owner := ghRepo.RepoOwner() + repo := ghRepo.RepoName() + + query := ` + query FindRepoID($owner:String!, $name:String!) { + repository(owner:$owner, name:$name) { + id + } + }` + variables := map[string]interface{}{ + "owner": owner, + "name": repo, + } + + result := struct { + Repository struct { + Id string + } + }{} + err := client.GraphQL(query, variables, &result) + if err != nil || result.Repository.Id == "" { + return "", fmt.Errorf("failed to determine GH repo ID: %s", err) + } + + return result.Repository.Id, nil +} diff --git a/command/issue.go b/command/issue.go index 0ad1c8d45..3111e28cb 100644 --- a/command/issue.go +++ b/command/issue.go @@ -2,11 +2,15 @@ package command import ( "fmt" + "io/ioutil" + "os" "strconv" + "strings" "github.com/github/gh-cli/api" "github.com/github/gh-cli/utils" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" ) func init() { @@ -24,6 +28,9 @@ func init() { RunE: issueView, }, ) + issueCmd.AddCommand(issueCreateCmd) + issueCreateCmd.Flags().StringArrayP("message", "m", nil, "set title and body") + issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue") } var issueCmd = &cobra.Command{ @@ -31,6 +38,11 @@ var issueCmd = &cobra.Command{ Short: "Work with GitHub issues", Long: `Helps you work with issues.`, } +var issueCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new issue", + RunE: issueCreate, +} func issueList(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true @@ -106,6 +118,77 @@ func issueView(cmd *cobra.Command, args []string) error { return utils.OpenInBrowser(openURL) } +func issueCreate(cmd *cobra.Command, args []string) error { + ctx := contextForCommand(cmd) + + baseRepo, err := ctx.BaseRepo() + if err != nil { + return err + } + + if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb { + // TODO: move URL generation into GitHubRepository + openURL := fmt.Sprintf("https://github.com/%s/%s/issues/new", baseRepo.RepoOwner(), baseRepo.RepoName()) + // TODO: figure out how to stub this in tests + if stat, err := os.Stat(".github/ISSUE_TEMPLATE"); err == nil && stat.IsDir() { + openURL += "/choose" + } + return utils.OpenInBrowser(openURL) + } + + var title string + var body string + + message, err := cmd.Flags().GetStringArray("message") + if err != nil { + return err + } + + apiClient, err := apiClientForContext(ctx) + if err != nil { + return err + } + + if len(message) > 0 { + title = message[0] + body = strings.Join(message[1:], "\n\n") + } else { + // TODO: open the text editor for issue title & body + input := os.Stdin + if terminal.IsTerminal(int(input.Fd())) { + cmd.Println("Enter the issue title and body; press Enter + Ctrl-D when done:") + } + inputBytes, err := ioutil.ReadAll(input) + if err != nil { + return err + } + + parts := strings.SplitN(string(inputBytes), "\n\n", 2) + if len(parts) > 0 { + title = parts[0] + } + if len(parts) > 1 { + body = parts[1] + } + } + + if title == "" { + return fmt.Errorf("aborting due to empty title") + } + params := map[string]interface{}{ + "title": title, + "body": body, + } + + newIssue, err := api.IssueCreate(apiClient, baseRepo, params) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), newIssue.URL) + return nil +} + func printIssues(issues ...api.Issue) { for _, issue := range issues { fmt.Printf(" #%d %s\n", issue.Number, truncateTitle(issue.Title, 70))