github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/shared/commentable.go (about)

     1  package shared
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  
    10  	"github.com/AlecAivazis/survey/v2"
    11  	"github.com/ungtb10d/cli/v2/api"
    12  	"github.com/ungtb10d/cli/v2/internal/config"
    13  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    14  	"github.com/ungtb10d/cli/v2/internal/text"
    15  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    16  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    17  	"github.com/ungtb10d/cli/v2/pkg/surveyext"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  type InputType int
    22  
    23  const (
    24  	InputTypeEditor InputType = iota
    25  	InputTypeInline
    26  	InputTypeWeb
    27  )
    28  
    29  type Commentable interface {
    30  	Link() string
    31  	Identifier() string
    32  	CurrentUserComments() []api.Comment
    33  }
    34  
    35  type CommentableOptions struct {
    36  	IO                    *iostreams.IOStreams
    37  	HttpClient            func() (*http.Client, error)
    38  	RetrieveCommentable   func() (Commentable, ghrepo.Interface, error)
    39  	EditSurvey            func(string) (string, error)
    40  	InteractiveEditSurvey func(string) (string, error)
    41  	ConfirmSubmitSurvey   func() (bool, error)
    42  	OpenInBrowser         func(string) error
    43  	Interactive           bool
    44  	InputType             InputType
    45  	Body                  string
    46  	EditLast              bool
    47  	Quiet                 bool
    48  	Host                  string
    49  }
    50  
    51  func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
    52  	inputFlags := 0
    53  	if cmd.Flags().Changed("body") {
    54  		opts.InputType = InputTypeInline
    55  		inputFlags++
    56  	}
    57  	if cmd.Flags().Changed("body-file") {
    58  		opts.InputType = InputTypeInline
    59  		inputFlags++
    60  	}
    61  	if web, _ := cmd.Flags().GetBool("web"); web {
    62  		opts.InputType = InputTypeWeb
    63  		inputFlags++
    64  	}
    65  	if editor, _ := cmd.Flags().GetBool("editor"); editor {
    66  		opts.InputType = InputTypeEditor
    67  		inputFlags++
    68  	}
    69  
    70  	if inputFlags == 0 {
    71  		if !opts.IO.CanPrompt() {
    72  			return cmdutil.FlagErrorf("flags required when not running interactively")
    73  		}
    74  		opts.Interactive = true
    75  	} else if inputFlags > 1 {
    76  		return cmdutil.FlagErrorf("specify only one of `--body`, `--body-file`, `--editor`, or `--web`")
    77  	}
    78  
    79  	return nil
    80  }
    81  
    82  func CommentableRun(opts *CommentableOptions) error {
    83  	commentable, repo, err := opts.RetrieveCommentable()
    84  	if err != nil {
    85  		return err
    86  	}
    87  	opts.Host = repo.RepoHost()
    88  	if opts.EditLast {
    89  		return updateComment(commentable, opts)
    90  	}
    91  	return createComment(commentable, opts)
    92  }
    93  
    94  func createComment(commentable Commentable, opts *CommentableOptions) error {
    95  	switch opts.InputType {
    96  	case InputTypeWeb:
    97  		openURL := commentable.Link() + "#issuecomment-new"
    98  		if opts.IO.IsStdoutTTY() && !opts.Quiet {
    99  			fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
   100  		}
   101  		return opts.OpenInBrowser(openURL)
   102  	case InputTypeEditor:
   103  		var body string
   104  		var err error
   105  		if opts.Interactive {
   106  			body, err = opts.InteractiveEditSurvey("")
   107  		} else {
   108  			body, err = opts.EditSurvey("")
   109  		}
   110  		if err != nil {
   111  			return err
   112  		}
   113  		opts.Body = body
   114  	}
   115  
   116  	if opts.Interactive {
   117  		cont, err := opts.ConfirmSubmitSurvey()
   118  		if err != nil {
   119  			return err
   120  		}
   121  		if !cont {
   122  			return errors.New("Discarding...")
   123  		}
   124  	}
   125  
   126  	httpClient, err := opts.HttpClient()
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	apiClient := api.NewClientFromHTTP(httpClient)
   132  	params := api.CommentCreateInput{Body: opts.Body, SubjectId: commentable.Identifier()}
   133  	url, err := api.CommentCreate(apiClient, opts.Host, params)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	if !opts.Quiet {
   139  		fmt.Fprintln(opts.IO.Out, url)
   140  	}
   141  
   142  	return nil
   143  }
   144  
   145  func updateComment(commentable Commentable, opts *CommentableOptions) error {
   146  	comments := commentable.CurrentUserComments()
   147  	if len(comments) == 0 {
   148  		return fmt.Errorf("no comments found for current user")
   149  	}
   150  
   151  	lastComment := &comments[len(comments)-1]
   152  
   153  	switch opts.InputType {
   154  	case InputTypeWeb:
   155  		openURL := lastComment.Link()
   156  		if opts.IO.IsStdoutTTY() && !opts.Quiet {
   157  			fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL))
   158  		}
   159  		return opts.OpenInBrowser(openURL)
   160  	case InputTypeEditor:
   161  		var body string
   162  		var err error
   163  		initialValue := lastComment.Content()
   164  		if opts.Interactive {
   165  			body, err = opts.InteractiveEditSurvey(initialValue)
   166  		} else {
   167  			body, err = opts.EditSurvey(initialValue)
   168  		}
   169  		if err != nil {
   170  			return err
   171  		}
   172  		opts.Body = body
   173  	}
   174  
   175  	if opts.Interactive {
   176  		cont, err := opts.ConfirmSubmitSurvey()
   177  		if err != nil {
   178  			return err
   179  		}
   180  		if !cont {
   181  			return errors.New("Discarding...")
   182  		}
   183  	}
   184  
   185  	httpClient, err := opts.HttpClient()
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	apiClient := api.NewClientFromHTTP(httpClient)
   191  	params := api.CommentUpdateInput{Body: opts.Body, CommentId: lastComment.Identifier()}
   192  	url, err := api.CommentUpdate(apiClient, opts.Host, params)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	if !opts.Quiet {
   198  		fmt.Fprintln(opts.IO.Out, url)
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  func CommentableConfirmSubmitSurvey() (bool, error) {
   205  	var confirm bool
   206  	submit := &survey.Confirm{
   207  		Message: "Submit?",
   208  		Default: true,
   209  	}
   210  	err := survey.AskOne(submit, &confirm)
   211  	return confirm, err
   212  }
   213  
   214  func CommentableInteractiveEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func(string) (string, error) {
   215  	return func(initialValue string) (string, error) {
   216  		editorCommand, err := cmdutil.DetermineEditor(cf)
   217  		if err != nil {
   218  			return "", err
   219  		}
   220  		cs := io.ColorScheme()
   221  		fmt.Fprintf(io.Out, "- %s to draft your comment in %s... ", cs.Bold("Press Enter"), cs.Bold(surveyext.EditorName(editorCommand)))
   222  		_ = waitForEnter(io.In)
   223  		return surveyext.Edit(editorCommand, "*.md", initialValue, io.In, io.Out, io.ErrOut)
   224  	}
   225  }
   226  
   227  func CommentableEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func(string) (string, error) {
   228  	return func(initialValue string) (string, error) {
   229  		editorCommand, err := cmdutil.DetermineEditor(cf)
   230  		if err != nil {
   231  			return "", err
   232  		}
   233  		return surveyext.Edit(editorCommand, "*.md", initialValue, io.In, io.Out, io.ErrOut)
   234  	}
   235  }
   236  
   237  func waitForEnter(r io.Reader) error {
   238  	scanner := bufio.NewScanner(r)
   239  	scanner.Scan()
   240  	return scanner.Err()
   241  }