From e821d2781a95235af26f84e1bfff3de5a2285079 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 14 Jan 2020 15:09:06 -0600 Subject: [PATCH 01/13] towards extending survey.Editor behavior --- command/editor.go | 165 +++++++++++++++++++++++++++++++++++ command/pr_create.go | 15 ---- command/title_body_survey.go | 16 ++-- 3 files changed, 173 insertions(+), 23 deletions(-) create mode 100644 command/editor.go diff --git a/command/editor.go b/command/editor.go new file mode 100644 index 000000000..25cc1ac1e --- /dev/null +++ b/command/editor.go @@ -0,0 +1,165 @@ +package command + +// This file extends survey.Editor to give it more flexible behavior. For more context, read +// https://github.com/github/gh-cli/issues/70 + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "runtime" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + shellquote "github.com/kballard/go-shellquote" +) + +var ( + bom = []byte{0xef, 0xbb, 0xbf} + editor = "nano" +) + +func init() { + if runtime.GOOS == "windows" { + editor = "notepad" + } + if v := os.Getenv("VISUAL"); v != "" { + editor = v + } + if e := os.Getenv("EDITOR"); e != "" { + editor = e + } +} + +type ghEditor struct { + *survey.Editor +} + +// this is not being called in the embedding case and isn't consulted in the alias case because of +// the incomplete overriding. +func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) { + // render the template + err := e.Render( + survey.EditorQuestionTemplate, + survey.EditorTemplateData{ + Editor: *e.Editor, + Config: config, + }, + ) + if err != nil { + return "", err + } + + // start reading runes from the standard in + rr := e.NewRuneReader() + rr.SetTermMode() + defer rr.RestoreTermMode() + + cursor := e.NewCursor() + cursor.Hide() + defer cursor.Show() + + for { + r, _, err := rr.ReadRune() + if err != nil { + return "", err + } + if r == '\r' || r == '\n' { + break + } + if r == terminal.KeyInterrupt { + return "", terminal.InterruptErr + } + if r == terminal.KeyEndTransmission { + break + } + if string(r) == config.HelpInput && e.Help != "" { + err = e.Render( + survey.EditorQuestionTemplate, + survey.EditorTemplateData{ + Editor: *e.Editor, + ShowHelp: true, + Config: config, + }, + ) + if err != nil { + return "", err + } + } + continue + } + + // prepare the temp file + pattern := e.FileName + if pattern == "" { + pattern = "survey*.txt" + } + f, err := ioutil.TempFile("", pattern) + if err != nil { + return "", err + } + defer os.Remove(f.Name()) + + // write utf8 BOM header + // The reason why we do this is because notepad.exe on Windows determines the + // encoding of an "empty" text file by the locale, for example, GBK in China, + // while golang string only handles utf8 well. However, a text file with utf8 + // BOM header is not considered "empty" on Windows, and the encoding will then + // be determined utf8 by notepad.exe, instead of GBK or other encodings. + if _, err := f.Write(bom); err != nil { + return "", err + } + + // write initial value + if _, err := f.WriteString(initialValue); err != nil { + return "", err + } + + // close the fd to prevent the editor unable to save file + if err := f.Close(); err != nil { + return "", err + } + + stdio := e.Stdio() + + args, err := shellquote.Split(editor) + if err != nil { + return "", err + } + args = append(args, f.Name()) + + // open the editor + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = stdio.In + cmd.Stdout = stdio.Out + cmd.Stderr = stdio.Err + cursor.Show() + if err := cmd.Run(); err != nil { + return "", err + } + + // raw is a BOM-unstripped UTF8 byte slice + raw, err := ioutil.ReadFile(f.Name()) + if err != nil { + return "", err + } + + // strip BOM header + text := string(bytes.TrimPrefix(raw, bom)) + + // check length, return default value on empty + if len(text) == 0 && !e.AppendDefault { + return e.Default, nil + } + + return text, nil +} + +func (e *ghEditor) Prompt(config *survey.PromptConfig) (interface{}, error) { + initialValue := "" + if e.Default != "" && e.AppendDefault { + initialValue = e.Default + } + return e.prompt(initialValue, config) +} diff --git a/command/pr_create.go b/command/pr_create.go index 6cacaa451..f37063b3d 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -2,8 +2,6 @@ package command import ( "fmt" - "os" - "runtime" "github.com/github/gh-cli/api" "github.com/github/gh-cli/context" @@ -158,19 +156,6 @@ func guessRemote(ctx context.Context) (string, error) { return remote.Name, nil } -func determineEditor() string { - if runtime.GOOS == "windows" { - return "notepad" - } - if v := os.Getenv("VISUAL"); v != "" { - return v - } - if e := os.Getenv("EDITOR"); e != "" { - return e - } - return "nano" -} - var prCreateCmd = &cobra.Command{ Use: "create", Short: "Create a pull request", diff --git a/command/title_body_survey.go b/command/title_body_survey.go index 180fdd11f..a24222fae 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -86,7 +86,6 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri } confirmed := false - editor := determineEditor() for !confirmed { titleQuestion := &survey.Question{ @@ -98,13 +97,14 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri } bodyQuestion := &survey.Question{ Name: "body", - Prompt: &survey.Editor{ - Message: fmt.Sprintf("Body (%s)", editor), - FileName: "*.md", - Default: inProgress.Body, - HideDefault: true, - AppendDefault: true, - Editor: editor, + Prompt: &ghEditor{ + Editor: &survey.Editor{ + Message: fmt.Sprintf("Body (%s)", editor), + FileName: "*.md", + Default: inProgress.Body, + HideDefault: true, + AppendDefault: true, + }, }, } From 164064064b11764c0fea8d94f2b6560389e0ec79 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 14 Jan 2020 15:16:51 -0600 Subject: [PATCH 02/13] e to edit body, enter to skip --- command/editor.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/command/editor.go b/command/editor.go index 25cc1ac1e..178e3d2fa 100644 --- a/command/editor.go +++ b/command/editor.go @@ -36,12 +36,25 @@ type ghEditor struct { *survey.Editor } +// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format +var EditorQuestionTemplate = ` +{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} +{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} +{{- color "default+hb"}}{{ .Message }} {{color "reset"}} +{{- if .ShowAnswer}} + {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} +{{- else }} + {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} + {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} + {{- color "cyan"}}[e: launch editor][enter: skip for now] {{color "reset"}} +{{- end}}` + // this is not being called in the embedding case and isn't consulted in the alias case because of // the incomplete overriding. func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) { // render the template err := e.Render( - survey.EditorQuestionTemplate, + EditorQuestionTemplate, survey.EditorTemplateData{ Editor: *e.Editor, Config: config, @@ -65,9 +78,12 @@ func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (int if err != nil { return "", err } - if r == '\r' || r == '\n' { + if r == 'e' { break } + if r == '\r' || r == '\n' { + return "", nil + } if r == terminal.KeyInterrupt { return "", terminal.InterruptErr } @@ -76,7 +92,7 @@ func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (int } if string(r) == config.HelpInput && e.Help != "" { err = e.Render( - survey.EditorQuestionTemplate, + EditorQuestionTemplate, survey.EditorTemplateData{ Editor: *e.Editor, ShowHelp: true, From 062d4f2367b35c6a2b324ecc1bf3ec3d91938887 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 14 Jan 2020 15:22:50 -0600 Subject: [PATCH 03/13] note --- command/editor.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/editor.go b/command/editor.go index 178e3d2fa..27f019a00 100644 --- a/command/editor.go +++ b/command/editor.go @@ -172,6 +172,7 @@ func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (int return text, nil } +// This is straight copypasta from survey to get our overriden prompt called.; func (e *ghEditor) Prompt(config *survey.PromptConfig) (interface{}, error) { initialValue := "" if e.Default != "" && e.AppendDefault { From d8cbb6a6a742f26020936a921254aa4760ecf0d4 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 14 Jan 2020 17:03:53 -0600 Subject: [PATCH 04/13] support previewing PRs in the browser --- command/pr_create.go | 55 ++++++++++++----- command/title_body_survey.go | 113 ++++++++++++++++------------------- 2 files changed, 92 insertions(+), 76 deletions(-) diff --git a/command/pr_create.go b/command/pr_create.go index f37063b3d..ab03d9256 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "net/url" "github.com/github/gh-cli/api" "github.com/github/gh-cli/context" @@ -49,6 +50,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { return err } if target == "" { + // TODO use default branch target = "master" } @@ -77,6 +79,8 @@ func prCreate(cmd *cobra.Command, _ []string) error { return errors.Wrap(err, "could not parse body") } + var action Action + interactive := title == "" || body == "" if interactive { @@ -91,8 +95,10 @@ func prCreate(cmd *cobra.Command, _ []string) error { return errors.Wrap(err, "could not collect title and/or body") } - if tb == nil { - // editing was canceled, we can just leave + action = tb.Action + + if action == CancelAction { + // TODO print about discarding return nil } @@ -123,21 +129,42 @@ func prCreate(cmd *cobra.Command, _ []string) error { return errors.Wrap(err, "could not parse draft") } - params := map[string]interface{}{ - "title": title, - "body": body, - "draft": isDraft, - "baseRefName": base, - "headRefName": head, + if action == SubmitAction { + params := map[string]interface{}{ + "title": title, + "body": body, + "draft": isDraft, + "baseRefName": base, + "headRefName": head, + } + + pr, err := api.CreatePullRequest(client, repo, params) + if err != nil { + return errors.Wrap(err, "failed to create pull request") + } + + fmt.Fprintln(cmd.OutOrStdout(), pr.URL) + } else if action == PreviewAction { + openURL := fmt.Sprintf( + "https://github.com/%s/%s/compare/%s...%s?expand=1&title=%s&body=%s", + repo.RepoOwner(), + repo.RepoName(), + target, + head, + url.QueryEscape(title), + url.QueryEscape(body), + ) + // TODO maybe do something about -d being discarded when previewing + // TODO could exceed max url length for explorer + fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) + return utils.OpenInBrowser(openURL) + + } else { + panic("Unreachable state") } - pr, err := api.CreatePullRequest(client, repo, params) - if err != nil { - return errors.Wrap(err, "failed to create pull request") - } - - fmt.Fprintln(cmd.OutOrStdout(), pr.URL) return nil + } func guessRemote(ctx context.Context) (string, error) { diff --git a/command/title_body_survey.go b/command/title_body_survey.go index a24222fae..661236ad3 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -9,18 +9,21 @@ import ( "github.com/spf13/cobra" ) +type Action int + type titleBody struct { - Body string - Title string + Body string + Title string + Action Action } const ( - _confirmed = iota - _unconfirmed = iota - _cancel = iota + PreviewAction Action = iota + SubmitAction Action = iota + CancelAction Action = iota ) -func confirm() (int, error) { +func confirm() (Action, error) { confirmAnswers := struct { Confirmation int }{} @@ -28,10 +31,10 @@ func confirm() (int, error) { { Name: "confirmation", Prompt: &survey.Select{ - Message: "Submit?", + Message: "What's next?", Options: []string{ - "Yes", - "Edit", + "Preview in browser", + "Submit", "Cancel", }, }, @@ -43,7 +46,7 @@ func confirm() (int, error) { return -1, errors.Wrap(err, "could not prompt") } - return confirmAnswers.Confirmation, nil + return Action(confirmAnswers.Confirmation), nil } func selectTemplate(templatePaths []string) (string, error) { @@ -85,59 +88,45 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri inProgress.Body = templateContents } - confirmed := false - - for !confirmed { - titleQuestion := &survey.Question{ - Name: "title", - Prompt: &survey.Input{ - Message: "Title", - Default: inProgress.Title, - }, - } - bodyQuestion := &survey.Question{ - Name: "body", - Prompt: &ghEditor{ - Editor: &survey.Editor{ - Message: fmt.Sprintf("Body (%s)", editor), - FileName: "*.md", - Default: inProgress.Body, - HideDefault: true, - AppendDefault: true, - }, - }, - } - - qs := []*survey.Question{} - if providedTitle == "" { - qs = append(qs, titleQuestion) - } - if providedBody == "" { - qs = append(qs, bodyQuestion) - } - - err := survey.Ask(qs, &inProgress) - if err != nil { - return nil, errors.Wrap(err, "could not prompt") - } - - confirmA, err := confirm() - if err != nil { - return nil, errors.Wrap(err, "unable to confirm") - } - switch confirmA { - case _confirmed: - confirmed = true - case _unconfirmed: - continue - case _cancel: - fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.") - return nil, nil - default: - panic("reached unreachable case") - } - + titleQuestion := &survey.Question{ + Name: "title", + Prompt: &survey.Input{ + Message: "Title", + Default: inProgress.Title, + }, } + bodyQuestion := &survey.Question{ + Name: "body", + Prompt: &ghEditor{ + Editor: &survey.Editor{ + Message: fmt.Sprintf("Body (%s)", editor), + FileName: "*.md", + Default: inProgress.Body, + HideDefault: true, + AppendDefault: true, + }, + }, + } + + qs := []*survey.Question{} + if providedTitle == "" { + qs = append(qs, titleQuestion) + } + if providedBody == "" { + qs = append(qs, bodyQuestion) + } + + err := survey.Ask(qs, &inProgress) + if err != nil { + return nil, errors.Wrap(err, "could not prompt") + } + + confirmA, err := confirm() + if err != nil { + return nil, errors.Wrap(err, "unable to confirm") + } + + inProgress.Action = confirmA return &inProgress, nil } From 3468a4652144aced277772e02c24ddb6ad2d0ab4 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jan 2020 11:27:12 -0600 Subject: [PATCH 05/13] support preview in browser for issue create --- command/issue.go | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/command/issue.go b/command/issue.go index 6a5820309..6abd2714b 100644 --- a/command/issue.go +++ b/command/issue.go @@ -3,6 +3,7 @@ package command import ( "fmt" "io" + "net/url" "regexp" "strconv" "strings" @@ -308,6 +309,8 @@ func issueCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("the '%s/%s' repository has disabled issues", baseRepo.RepoOwner(), baseRepo.RepoName()) } + action := SubmitAction + title, err := cmd.Flags().GetString("title") if err != nil { return errors.Wrap(err, "could not parse title") @@ -320,13 +323,16 @@ func issueCreate(cmd *cobra.Command, args []string) error { interactive := title == "" || body == "" if interactive { + // TODO handle tb.Action tb, err := titleBodySurvey(cmd, title, body, templateFiles) if err != nil { return errors.Wrap(err, "could not collect title and/or body") } - if tb == nil { - // editing was canceled, we can just leave + action = tb.Action + + if tb.Action == CancelAction { + // TODO print something return nil } @@ -337,17 +343,34 @@ func issueCreate(cmd *cobra.Command, args []string) error { body = tb.Body } } - params := map[string]interface{}{ - "title": title, - "body": body, + + if action == PreviewAction { + openURL := fmt.Sprintf( + "https://github.com/%s/%s/issues/new/?title=%s&body=%s", + baseRepo.RepoOwner(), + baseRepo.RepoName(), + url.QueryEscape(title), + url.QueryEscape(body), + ) + // TODO could exceed max url length for explorer + fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) + return utils.OpenInBrowser(openURL) + } else if action == SubmitAction { + params := map[string]interface{}{ + "title": title, + "body": body, + } + + newIssue, err := api.IssueCreate(apiClient, repo, params) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), newIssue.URL) + } else { + panic("Unreachable state") } - newIssue, err := api.IssueCreate(apiClient, repo, params) - if err != nil { - return err - } - - fmt.Fprintln(cmd.OutOrStdout(), newIssue.URL) return nil } From 7bbd70d6b28f1b5673c8fc888c7b3eebcd09545f Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jan 2020 11:27:25 -0600 Subject: [PATCH 06/13] use default to preserve non-interactive behavior --- command/pr_create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/pr_create.go b/command/pr_create.go index ab03d9256..122314b8f 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -79,7 +79,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { return errors.Wrap(err, "could not parse body") } - var action Action + action := SubmitAction interactive := title == "" || body == "" From 32461284cc044933dfd4d471f4a679f5576f19e2 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jan 2020 12:35:28 -0600 Subject: [PATCH 07/13] fix some TODOs --- command/issue.go | 4 ++-- command/pr_create.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/issue.go b/command/issue.go index 6abd2714b..853c0dcd6 100644 --- a/command/issue.go +++ b/command/issue.go @@ -323,7 +323,6 @@ func issueCreate(cmd *cobra.Command, args []string) error { interactive := title == "" || body == "" if interactive { - // TODO handle tb.Action tb, err := titleBodySurvey(cmd, title, body, templateFiles) if err != nil { return errors.Wrap(err, "could not collect title and/or body") @@ -332,7 +331,8 @@ func issueCreate(cmd *cobra.Command, args []string) error { action = tb.Action if tb.Action == CancelAction { - // TODO print something + fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.") + return nil } diff --git a/command/pr_create.go b/command/pr_create.go index 122314b8f..b2b5caab3 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -98,7 +98,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { action = tb.Action if action == CancelAction { - // TODO print about discarding + fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.") return nil } From 1e19b9953a6e92b4a619e61fdb0e834e51461cfd Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jan 2020 12:38:54 -0600 Subject: [PATCH 08/13] use pluralize helper --- command/pr_create.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/command/pr_create.go b/command/pr_create.go index b2b5caab3..085806b2b 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -21,13 +21,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { return err } if ucc > 0 { - noun := "change" - if ucc > 1 { - // TODO: use pluralize helper - noun = noun + "s" - } - - fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %d uncommitted %s\n", ucc, noun) + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change")) } repo, err := ctx.BaseRepo() From beeb35e7e2197a7b218e5b63ae150ae1f7e4c55d Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 16 Jan 2020 14:03:58 -0600 Subject: [PATCH 09/13] clean up body prompt text --- command/editor.go | 28 ++++++++++++++++++++-------- command/title_body_survey.go | 4 +--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/command/editor.go b/command/editor.go index 27f019a00..6ab141e9a 100644 --- a/command/editor.go +++ b/command/editor.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "runtime" "github.com/AlecAivazis/survey/v2" @@ -46,18 +47,28 @@ var EditorQuestionTemplate = ` {{- else }} {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} - {{- color "cyan"}}[e: launch editor][enter: skip for now] {{color "reset"}} + {{- color "cyan"}}[e: launch {{ .EditorName }}][enter: skip for now] {{color "reset"}} {{- end}}` +type EditorTemplateData struct { + survey.Editor + EditorName string + Answer string + ShowAnswer bool + ShowHelp bool + Config *survey.PromptConfig +} + // this is not being called in the embedding case and isn't consulted in the alias case because of // the incomplete overriding. func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) { // render the template err := e.Render( EditorQuestionTemplate, - survey.EditorTemplateData{ - Editor: *e.Editor, - Config: config, + EditorTemplateData{ + Editor: *e.Editor, + EditorName: filepath.Base(editor), + Config: config, }, ) if err != nil { @@ -93,10 +104,11 @@ func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (int if string(r) == config.HelpInput && e.Help != "" { err = e.Render( EditorQuestionTemplate, - survey.EditorTemplateData{ - Editor: *e.Editor, - ShowHelp: true, - Config: config, + EditorTemplateData{ + Editor: *e.Editor, + EditorName: filepath.Base(editor), + ShowHelp: true, + Config: config, }, ) if err != nil { diff --git a/command/title_body_survey.go b/command/title_body_survey.go index 661236ad3..8a5252bd4 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -1,8 +1,6 @@ package command import ( - "fmt" - "github.com/AlecAivazis/survey/v2" "github.com/github/gh-cli/pkg/githubtemplate" "github.com/pkg/errors" @@ -99,7 +97,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri Name: "body", Prompt: &ghEditor{ Editor: &survey.Editor{ - Message: fmt.Sprintf("Body (%s)", editor), + Message: "Body", FileName: "*.md", Default: inProgress.Body, HideDefault: true, From 31001877bda166ec433a0d303d443bc174189f97 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 16 Jan 2020 14:18:40 -0600 Subject: [PATCH 10/13] hide potentially long query strings when printing urls --- command/issue.go | 6 +++++- command/pr_create.go | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/command/issue.go b/command/issue.go index 853c0dcd6..315ce6aac 100644 --- a/command/issue.go +++ b/command/issue.go @@ -353,7 +353,11 @@ func issueCreate(cmd *cobra.Command, args []string) error { url.QueryEscape(body), ) // TODO could exceed max url length for explorer - fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) + url, err := url.Parse(openURL) + if err != nil { + return err + } + fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s%s in your browser.\n", url.Host, url.Path) return utils.OpenInBrowser(openURL) } else if action == SubmitAction { params := map[string]interface{}{ diff --git a/command/pr_create.go b/command/pr_create.go index 085806b2b..a72dff7ff 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -148,11 +148,13 @@ func prCreate(cmd *cobra.Command, _ []string) error { url.QueryEscape(title), url.QueryEscape(body), ) - // TODO maybe do something about -d being discarded when previewing // TODO could exceed max url length for explorer - fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", openURL) + url, err := url.Parse(openURL) + if err != nil { + return err + } + fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s%s in your browser.\n", url.Host, url.Path) return utils.OpenInBrowser(openURL) - } else { panic("Unreachable state") } From ffb6b8e29f4a6b405664ecdf058528c6467ef954 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 16 Jan 2020 14:28:40 -0600 Subject: [PATCH 11/13] move survey extension to its own package and clarify --- command/title_body_survey.go | 3 ++- {command => pkg/surveyext}/editor.go | 24 ++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) rename {command => pkg/surveyext}/editor.go (84%) diff --git a/command/title_body_survey.go b/command/title_body_survey.go index 8a5252bd4..9db3d1a2b 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -3,6 +3,7 @@ package command import ( "github.com/AlecAivazis/survey/v2" "github.com/github/gh-cli/pkg/githubtemplate" + "github.com/github/gh-cli/pkg/surveyext" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -95,7 +96,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody stri } bodyQuestion := &survey.Question{ Name: "body", - Prompt: &ghEditor{ + Prompt: &surveyext.GhEditor{ Editor: &survey.Editor{ Message: "Body", FileName: "*.md", diff --git a/command/editor.go b/pkg/surveyext/editor.go similarity index 84% rename from command/editor.go rename to pkg/surveyext/editor.go index 6ab141e9a..6527821f7 100644 --- a/command/editor.go +++ b/pkg/surveyext/editor.go @@ -1,7 +1,8 @@ -package command +package surveyext // This file extends survey.Editor to give it more flexible behavior. For more context, read // https://github.com/github/gh-cli/issues/70 +// To see what we extended, search through for EXTENDED comments. import ( "bytes" @@ -18,7 +19,7 @@ import ( var ( bom = []byte{0xef, 0xbb, 0xbf} - editor = "nano" + editor = "nano" // EXTENDED to switch from vim as a default editor ) func init() { @@ -33,11 +34,12 @@ func init() { } } -type ghEditor struct { +// EXTENDED to enable different prompting behavior +type GhEditor struct { *survey.Editor } -// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format +// EXTENDED to change prompt text var EditorQuestionTemplate = ` {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} @@ -50,6 +52,7 @@ var EditorQuestionTemplate = ` {{- color "cyan"}}[e: launch {{ .EditorName }}][enter: skip for now] {{color "reset"}} {{- end}}` +// EXTENDED to pass editor name (to use in prompt) type EditorTemplateData struct { survey.Editor EditorName string @@ -59,12 +62,11 @@ type EditorTemplateData struct { Config *survey.PromptConfig } -// this is not being called in the embedding case and isn't consulted in the alias case because of -// the incomplete overriding. -func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) { - // render the template +// EXTENDED to augment prompt text and keypress handling +func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) { err := e.Render( EditorQuestionTemplate, + // EXTENDED to support printing editor in prompt EditorTemplateData{ Editor: *e.Editor, EditorName: filepath.Base(editor), @@ -85,6 +87,7 @@ func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (int defer cursor.Show() for { + // EXTENDED to handle the e to edit / enter to skip behavior r, _, err := rr.ReadRune() if err != nil { return "", err @@ -105,6 +108,7 @@ func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (int err = e.Render( EditorQuestionTemplate, EditorTemplateData{ + // EXTENDED to support printing editor in prompt Editor: *e.Editor, EditorName: filepath.Base(editor), ShowHelp: true, @@ -184,8 +188,8 @@ func (e *ghEditor) prompt(initialValue string, config *survey.PromptConfig) (int return text, nil } -// This is straight copypasta from survey to get our overriden prompt called.; -func (e *ghEditor) Prompt(config *survey.PromptConfig) (interface{}, error) { +// EXTENDED This is straight copypasta from survey to get our overriden prompt called.; +func (e *GhEditor) Prompt(config *survey.PromptConfig) (interface{}, error) { initialValue := "" if e.Default != "" && e.AppendDefault { initialValue = e.Default From 115cc30a8e8536d0c62b6d41885a8d281e0ea556 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 16 Jan 2020 14:28:49 -0600 Subject: [PATCH 12/13] rely on iota syntax magic --- command/title_body_survey.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/title_body_survey.go b/command/title_body_survey.go index 9db3d1a2b..2b11c326f 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -18,8 +18,8 @@ type titleBody struct { const ( PreviewAction Action = iota - SubmitAction Action = iota - CancelAction Action = iota + SubmitAction + CancelAction ) func confirm() (Action, error) { From 7aa186fe028dde30851861849ff1678fa0590412 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 16 Jan 2020 14:29:59 -0600 Subject: [PATCH 13/13] make prompt match mockup more --- pkg/surveyext/editor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/surveyext/editor.go b/pkg/surveyext/editor.go index 6527821f7..a0c3d29a3 100644 --- a/pkg/surveyext/editor.go +++ b/pkg/surveyext/editor.go @@ -49,7 +49,7 @@ var EditorQuestionTemplate = ` {{- else }} {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} - {{- color "cyan"}}[e: launch {{ .EditorName }}][enter: skip for now] {{color "reset"}} + {{- color "cyan"}}[(e) to launch {{ .EditorName }}, enter to skip] {{color "reset"}} {{- end}}` // EXTENDED to pass editor name (to use in prompt)