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

     1  package edit
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  
     7  	"github.com/MakeNowJust/heredoc"
     8  	"github.com/ungtb10d/cli/v2/api"
     9  	"github.com/ungtb10d/cli/v2/internal/config"
    10  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    11  	shared "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared"
    12  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    13  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    14  	"github.com/shurcooL/githubv4"
    15  	"github.com/spf13/cobra"
    16  	"golang.org/x/sync/errgroup"
    17  )
    18  
    19  type EditOptions struct {
    20  	HttpClient func() (*http.Client, error)
    21  	IO         *iostreams.IOStreams
    22  
    23  	Finder          shared.PRFinder
    24  	Surveyor        Surveyor
    25  	Fetcher         EditableOptionsFetcher
    26  	EditorRetriever EditorRetriever
    27  
    28  	SelectorArg string
    29  	Interactive bool
    30  
    31  	shared.Editable
    32  }
    33  
    34  func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
    35  	opts := &EditOptions{
    36  		IO:              f.IOStreams,
    37  		HttpClient:      f.HttpClient,
    38  		Surveyor:        surveyor{},
    39  		Fetcher:         fetcher{},
    40  		EditorRetriever: editorRetriever{config: f.Config},
    41  	}
    42  
    43  	var bodyFile string
    44  
    45  	cmd := &cobra.Command{
    46  		Use:   "edit [<number> | <url> | <branch>]",
    47  		Short: "Edit a pull request",
    48  		Long: heredoc.Doc(`
    49  			Edit a pull request.
    50  
    51  			Without an argument, the pull request that belongs to the current branch
    52  			is selected.
    53  		`),
    54  		Example: heredoc.Doc(`
    55  			$ gh pr edit 23 --title "I found a bug" --body "Nothing works"
    56  			$ gh pr edit 23 --add-label "bug,help wanted" --remove-label "core"
    57  			$ gh pr edit 23 --add-reviewer monalisa,hubot  --remove-reviewer myorg/team-name
    58  			$ gh pr edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
    59  			$ gh pr edit 23 --add-project "Roadmap" --remove-project v1,v2
    60  			$ gh pr edit 23 --milestone "Version 1"
    61  		`),
    62  		Args: cobra.MaximumNArgs(1),
    63  		RunE: func(cmd *cobra.Command, args []string) error {
    64  			opts.Finder = shared.NewFinder(f)
    65  
    66  			if len(args) > 0 {
    67  				opts.SelectorArg = args[0]
    68  			}
    69  
    70  			flags := cmd.Flags()
    71  
    72  			bodyProvided := flags.Changed("body")
    73  			bodyFileProvided := bodyFile != ""
    74  
    75  			if err := cmdutil.MutuallyExclusive(
    76  				"specify only one of `--body` or `--body-file`",
    77  				bodyProvided,
    78  				bodyFileProvided,
    79  			); err != nil {
    80  				return err
    81  			}
    82  			if bodyProvided || bodyFileProvided {
    83  				opts.Editable.Body.Edited = true
    84  				if bodyFileProvided {
    85  					b, err := cmdutil.ReadFile(bodyFile, opts.IO.In)
    86  					if err != nil {
    87  						return err
    88  					}
    89  					opts.Editable.Body.Value = string(b)
    90  				}
    91  			}
    92  
    93  			if flags.Changed("title") {
    94  				opts.Editable.Title.Edited = true
    95  			}
    96  			if flags.Changed("body") {
    97  				opts.Editable.Body.Edited = true
    98  			}
    99  			if flags.Changed("base") {
   100  				opts.Editable.Base.Edited = true
   101  			}
   102  			if flags.Changed("add-reviewer") || flags.Changed("remove-reviewer") {
   103  				opts.Editable.Reviewers.Edited = true
   104  			}
   105  			if flags.Changed("add-assignee") || flags.Changed("remove-assignee") {
   106  				opts.Editable.Assignees.Edited = true
   107  			}
   108  			if flags.Changed("add-label") || flags.Changed("remove-label") {
   109  				opts.Editable.Labels.Edited = true
   110  			}
   111  			if flags.Changed("add-project") || flags.Changed("remove-project") {
   112  				opts.Editable.Projects.Edited = true
   113  			}
   114  			if flags.Changed("milestone") {
   115  				opts.Editable.Milestone.Edited = true
   116  			}
   117  
   118  			if !opts.Editable.Dirty() {
   119  				opts.Interactive = true
   120  			}
   121  
   122  			if opts.Interactive && !opts.IO.CanPrompt() {
   123  				return cmdutil.FlagErrorf("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")
   124  			}
   125  
   126  			if runF != nil {
   127  				return runF(opts)
   128  			}
   129  
   130  			return editRun(opts)
   131  		},
   132  	}
   133  
   134  	cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
   135  	cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
   136  	cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
   137  	cmd.Flags().StringVarP(&opts.Editable.Base.Value, "base", "B", "", "Change the base `branch` for this pull request")
   138  	cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Add, "add-reviewer", nil, "Add reviewers by their `login`.")
   139  	cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Remove, "remove-reviewer", nil, "Remove reviewers by their `login`.")
   140  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself.")
   141  	cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Remove, "remove-assignee", nil, "Remove assigned users by their `login`. Use \"@me\" to unassign yourself.")
   142  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Add, "add-label", nil, "Add labels by `name`")
   143  	cmd.Flags().StringSliceVar(&opts.Editable.Labels.Remove, "remove-label", nil, "Remove labels by `name`")
   144  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Add, "add-project", nil, "Add the pull request to projects by `name`")
   145  	cmd.Flags().StringSliceVar(&opts.Editable.Projects.Remove, "remove-project", nil, "Remove the pull request from projects by `name`")
   146  	cmd.Flags().StringVarP(&opts.Editable.Milestone.Value, "milestone", "m", "", "Edit the milestone the pull request belongs to by `name`")
   147  
   148  	return cmd
   149  }
   150  
   151  func editRun(opts *EditOptions) error {
   152  	findOptions := shared.FindOptions{
   153  		Selector: opts.SelectorArg,
   154  		Fields:   []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "assignees", "labels", "projectCards", "milestone"},
   155  	}
   156  	pr, repo, err := opts.Finder.Find(findOptions)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	editable := opts.Editable
   162  	editable.Reviewers.Allowed = true
   163  	editable.Title.Default = pr.Title
   164  	editable.Body.Default = pr.Body
   165  	editable.Base.Default = pr.BaseRefName
   166  	editable.Reviewers.Default = pr.ReviewRequests.Logins()
   167  	editable.Assignees.Default = pr.Assignees.Logins()
   168  	editable.Labels.Default = pr.Labels.Names()
   169  	editable.Projects.Default = pr.ProjectCards.ProjectNames()
   170  	if pr.Milestone != nil {
   171  		editable.Milestone.Default = pr.Milestone.Title
   172  	}
   173  
   174  	if opts.Interactive {
   175  		err = opts.Surveyor.FieldsToEdit(&editable)
   176  		if err != nil {
   177  			return err
   178  		}
   179  	}
   180  
   181  	httpClient, err := opts.HttpClient()
   182  	if err != nil {
   183  		return err
   184  	}
   185  	apiClient := api.NewClientFromHTTP(httpClient)
   186  
   187  	opts.IO.StartProgressIndicator()
   188  	err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable)
   189  	opts.IO.StopProgressIndicator()
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	if opts.Interactive {
   195  		editorCommand, err := opts.EditorRetriever.Retrieve()
   196  		if err != nil {
   197  			return err
   198  		}
   199  		err = opts.Surveyor.EditFields(&editable, editorCommand)
   200  		if err != nil {
   201  			return err
   202  		}
   203  	}
   204  
   205  	opts.IO.StartProgressIndicator()
   206  	err = updatePullRequest(httpClient, repo, pr.ID, editable)
   207  	opts.IO.StopProgressIndicator()
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	fmt.Fprintln(opts.IO.Out, pr.URL)
   213  
   214  	return nil
   215  }
   216  
   217  func updatePullRequest(httpClient *http.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
   218  	var wg errgroup.Group
   219  	wg.Go(func() error {
   220  		return shared.UpdateIssue(httpClient, repo, id, true, editable)
   221  	})
   222  	if editable.Reviewers.Edited {
   223  		wg.Go(func() error {
   224  			return updatePullRequestReviews(httpClient, repo, id, editable)
   225  		})
   226  	}
   227  	return wg.Wait()
   228  }
   229  
   230  func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
   231  	userIds, teamIds, err := editable.ReviewerIds()
   232  	if err != nil {
   233  		return err
   234  	}
   235  	union := githubv4.Boolean(false)
   236  	reviewsRequestParams := githubv4.RequestReviewsInput{
   237  		PullRequestID: id,
   238  		Union:         &union,
   239  		UserIDs:       ghIds(userIds),
   240  		TeamIDs:       ghIds(teamIds),
   241  	}
   242  	client := api.NewClientFromHTTP(httpClient)
   243  	return api.UpdatePullRequestReviews(client, repo, reviewsRequestParams)
   244  }
   245  
   246  type Surveyor interface {
   247  	FieldsToEdit(*shared.Editable) error
   248  	EditFields(*shared.Editable, string) error
   249  }
   250  
   251  type surveyor struct{}
   252  
   253  func (s surveyor) FieldsToEdit(editable *shared.Editable) error {
   254  	return shared.FieldsToEditSurvey(editable)
   255  }
   256  
   257  func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error {
   258  	return shared.EditFieldsSurvey(editable, editorCmd)
   259  }
   260  
   261  type EditableOptionsFetcher interface {
   262  	EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable) error
   263  }
   264  
   265  type fetcher struct{}
   266  
   267  func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error {
   268  	return shared.FetchOptions(client, repo, opts)
   269  }
   270  
   271  type EditorRetriever interface {
   272  	Retrieve() (string, error)
   273  }
   274  
   275  type editorRetriever struct {
   276  	config func() (config.Config, error)
   277  }
   278  
   279  func (e editorRetriever) Retrieve() (string, error) {
   280  	return cmdutil.DetermineEditor(e.config)
   281  }
   282  
   283  func ghIds(s *[]string) *[]githubv4.ID {
   284  	if s == nil {
   285  		return nil
   286  	}
   287  	ids := make([]githubv4.ID, len(*s))
   288  	for i, v := range *s {
   289  		ids[i] = v
   290  	}
   291  	return &ids
   292  }