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

     1  package merge
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  
     9  	"github.com/AlecAivazis/survey/v2"
    10  	"github.com/MakeNowJust/heredoc"
    11  	"github.com/ungtb10d/cli/v2/api"
    12  	ghContext "github.com/ungtb10d/cli/v2/context"
    13  	"github.com/ungtb10d/cli/v2/git"
    14  	"github.com/ungtb10d/cli/v2/internal/config"
    15  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    16  	"github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared"
    17  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    18  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    19  	"github.com/ungtb10d/cli/v2/pkg/prompt"
    20  	"github.com/ungtb10d/cli/v2/pkg/surveyext"
    21  	"github.com/spf13/cobra"
    22  )
    23  
    24  type editor interface {
    25  	Edit(string, string) (string, error)
    26  }
    27  
    28  type MergeOptions struct {
    29  	HttpClient func() (*http.Client, error)
    30  	GitClient  *git.Client
    31  	IO         *iostreams.IOStreams
    32  	Branch     func() (string, error)
    33  	Remotes    func() (ghContext.Remotes, error)
    34  
    35  	Finder shared.PRFinder
    36  
    37  	SelectorArg  string
    38  	DeleteBranch bool
    39  	MergeMethod  PullRequestMergeMethod
    40  
    41  	AutoMergeEnable  bool
    42  	AutoMergeDisable bool
    43  
    44  	AuthorEmail string
    45  
    46  	Body    string
    47  	BodySet bool
    48  	Subject string
    49  	Editor  editor
    50  
    51  	UseAdmin                bool
    52  	IsDeleteBranchIndicated bool
    53  	CanDeleteLocalBranch    bool
    54  	MergeStrategyEmpty      bool
    55  	MatchHeadCommit         string
    56  }
    57  
    58  // ErrAlreadyInMergeQueue indicates that the pull request is already in a merge queue
    59  var ErrAlreadyInMergeQueue = errors.New("already in merge queue")
    60  
    61  func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Command {
    62  	opts := &MergeOptions{
    63  		IO:         f.IOStreams,
    64  		HttpClient: f.HttpClient,
    65  		GitClient:  f.GitClient,
    66  		Branch:     f.Branch,
    67  		Remotes:    f.Remotes,
    68  	}
    69  
    70  	var (
    71  		flagMerge  bool
    72  		flagSquash bool
    73  		flagRebase bool
    74  	)
    75  
    76  	var bodyFile string
    77  
    78  	cmd := &cobra.Command{
    79  		Use:   "merge [<number> | <url> | <branch>]",
    80  		Short: "Merge a pull request",
    81  		Long: heredoc.Doc(`
    82  			Merge a pull request on GitHub.
    83  
    84  			Without an argument, the pull request that belongs to the current branch
    85  			is selected.
    86  
    87  			When targeting a branch that requires a merge queue, no merge strategy is required.
    88  			If required checks have not yet passed, AutoMerge will be enabled.
    89  			If required checks have passed, the pull request will be added to the merge queue.
    90  			To bypass a merge queue and merge directly, pass the '--admin' flag.
    91      	`),
    92  		Args: cobra.MaximumNArgs(1),
    93  		RunE: func(cmd *cobra.Command, args []string) error {
    94  			opts.Finder = shared.NewFinder(f)
    95  
    96  			if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
    97  				return cmdutil.FlagErrorf("argument required when using the --repo flag")
    98  			}
    99  
   100  			if len(args) > 0 {
   101  				opts.SelectorArg = args[0]
   102  			}
   103  
   104  			methodFlags := 0
   105  			if flagMerge {
   106  				opts.MergeMethod = PullRequestMergeMethodMerge
   107  				methodFlags++
   108  			}
   109  			if flagRebase {
   110  				opts.MergeMethod = PullRequestMergeMethodRebase
   111  				methodFlags++
   112  			}
   113  			if flagSquash {
   114  				opts.MergeMethod = PullRequestMergeMethodSquash
   115  				methodFlags++
   116  			}
   117  			if methodFlags == 0 {
   118  				opts.MergeStrategyEmpty = true
   119  			} else if methodFlags > 1 {
   120  				return cmdutil.FlagErrorf("only one of --merge, --rebase, or --squash can be enabled")
   121  			}
   122  
   123  			opts.IsDeleteBranchIndicated = cmd.Flags().Changed("delete-branch")
   124  			opts.CanDeleteLocalBranch = !cmd.Flags().Changed("repo")
   125  
   126  			bodyProvided := cmd.Flags().Changed("body")
   127  			bodyFileProvided := bodyFile != ""
   128  
   129  			if err := cmdutil.MutuallyExclusive(
   130  				"specify only one of `--auto`, `--disable-auto`, or `--admin`",
   131  				opts.AutoMergeEnable,
   132  				opts.AutoMergeDisable,
   133  				opts.UseAdmin,
   134  			); err != nil {
   135  				return err
   136  			}
   137  
   138  			if err := cmdutil.MutuallyExclusive(
   139  				"specify only one of `--body` or `--body-file`",
   140  				bodyProvided,
   141  				bodyFileProvided,
   142  			); err != nil {
   143  				return err
   144  			}
   145  
   146  			if bodyProvided || bodyFileProvided {
   147  				opts.BodySet = true
   148  				if bodyFileProvided {
   149  					b, err := cmdutil.ReadFile(bodyFile, opts.IO.In)
   150  					if err != nil {
   151  						return err
   152  					}
   153  					opts.Body = string(b)
   154  				}
   155  			}
   156  
   157  			opts.Editor = &userEditor{
   158  				io:     opts.IO,
   159  				config: f.Config,
   160  			}
   161  
   162  			if runF != nil {
   163  				return runF(opts)
   164  			}
   165  
   166  			err := mergeRun(opts)
   167  			if errors.Is(err, ErrAlreadyInMergeQueue) {
   168  				return nil
   169  			}
   170  			return err
   171  		},
   172  	}
   173  
   174  	cmd.Flags().BoolVar(&opts.UseAdmin, "admin", false, "Use administrator privileges to merge a pull request that does not meet requirements")
   175  	cmd.Flags().BoolVarP(&opts.DeleteBranch, "delete-branch", "d", false, "Delete the local and remote branch after merge")
   176  	cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Body `text` for the merge commit")
   177  	cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
   178  	cmd.Flags().StringVarP(&opts.Subject, "subject", "t", "", "Subject `text` for the merge commit")
   179  	cmd.Flags().BoolVarP(&flagMerge, "merge", "m", false, "Merge the commits with the base branch")
   180  	cmd.Flags().BoolVarP(&flagRebase, "rebase", "r", false, "Rebase the commits onto the base branch")
   181  	cmd.Flags().BoolVarP(&flagSquash, "squash", "s", false, "Squash the commits into one commit and merge it into the base branch")
   182  	cmd.Flags().BoolVar(&opts.AutoMergeEnable, "auto", false, "Automatically merge only after necessary requirements are met")
   183  	cmd.Flags().BoolVar(&opts.AutoMergeDisable, "disable-auto", false, "Disable auto-merge for this pull request")
   184  	cmd.Flags().StringVar(&opts.MatchHeadCommit, "match-head-commit", "", "Commit `SHA` that the pull request head must match to allow merge")
   185  	cmd.Flags().StringVarP(&opts.AuthorEmail, "author-email", "A", "", "Email `text` for merge commit author")
   186  	return cmd
   187  }
   188  
   189  // mergeContext contains state and dependencies to merge a pull request.
   190  type mergeContext struct {
   191  	pr                 *api.PullRequest
   192  	baseRepo           ghrepo.Interface
   193  	httpClient         *http.Client
   194  	opts               *MergeOptions
   195  	cs                 *iostreams.ColorScheme
   196  	isTerminal         bool
   197  	merged             bool
   198  	localBranchExists  bool
   199  	autoMerge          bool
   200  	crossRepoPR        bool
   201  	deleteBranch       bool
   202  	switchedToBranch   string
   203  	mergeQueueRequired bool
   204  }
   205  
   206  // Attempt to disable auto merge on the pull request.
   207  func (m *mergeContext) disableAutoMerge() error {
   208  	if err := disableAutoMerge(m.httpClient, m.baseRepo, m.pr.ID); err != nil {
   209  		return err
   210  	}
   211  	return m.infof("%s Auto-merge disabled for pull request #%d\n", m.cs.SuccessIconWithColor(m.cs.Green), m.pr.Number)
   212  }
   213  
   214  // Check if this pull request is in a merge queue
   215  func (m *mergeContext) inMergeQueue() error {
   216  	// if the pull request is in a merge queue no further action is possible
   217  	if m.pr.IsInMergeQueue {
   218  		_ = m.warnf("%s Pull request #%d is already queued to merge\n", m.cs.WarningIcon(), m.pr.Number)
   219  		return ErrAlreadyInMergeQueue
   220  	}
   221  	return nil
   222  }
   223  
   224  // Warn if the pull request and the remote branch have diverged.
   225  func (m *mergeContext) warnIfDiverged() {
   226  	if m.opts.SelectorArg != "" || len(m.pr.Commits.Nodes) == 0 {
   227  		return
   228  	}
   229  
   230  	localBranchLastCommit, err := m.opts.GitClient.LastCommit(context.Background())
   231  	if err != nil {
   232  		return
   233  	}
   234  
   235  	if localBranchLastCommit.Sha == m.pr.Commits.Nodes[len(m.pr.Commits.Nodes)-1].Commit.OID {
   236  		return
   237  	}
   238  
   239  	_ = m.warnf("%s Pull request #%d (%s) has diverged from local branch\n", m.cs.Yellow("!"), m.pr.Number, m.pr.Title)
   240  }
   241  
   242  // Check if the current state of the pull request allows for merging
   243  func (m *mergeContext) canMerge() error {
   244  	if m.mergeQueueRequired {
   245  		// a pull request can always be added to the merge queue
   246  		return nil
   247  	}
   248  
   249  	reason := blockedReason(m.pr.MergeStateStatus, m.opts.UseAdmin)
   250  
   251  	if reason == "" || m.autoMerge || m.merged {
   252  		return nil
   253  	}
   254  
   255  	_ = m.warnf("%s Pull request #%d is not mergeable: %s.\n", m.cs.FailureIcon(), m.pr.Number, reason)
   256  	_ = m.warnf("To have the pull request merged after all the requirements have been met, add the `--auto` flag.\n")
   257  	if remote := remoteForMergeConflictResolution(m.baseRepo, m.pr, m.opts); remote != nil {
   258  		mergeOrRebase := "merge"
   259  		if m.opts.MergeMethod == PullRequestMergeMethodRebase {
   260  			mergeOrRebase = "rebase"
   261  		}
   262  		fetchBranch := fmt.Sprintf("%s %s", remote.Name, m.pr.BaseRefName)
   263  		mergeBranch := fmt.Sprintf("%s %s/%s", mergeOrRebase, remote.Name, m.pr.BaseRefName)
   264  		cmd := fmt.Sprintf("gh pr checkout %d && git fetch %s && git %s", m.pr.Number, fetchBranch, mergeBranch)
   265  		_ = m.warnf("Run the following to resolve the merge conflicts locally:\n  %s\n", m.cs.Bold(cmd))
   266  	}
   267  	if !m.opts.UseAdmin && allowsAdminOverride(m.pr.MergeStateStatus) {
   268  		// TODO: show this flag only to repo admins
   269  		_ = m.warnf("To use administrator privileges to immediately merge the pull request, add the `--admin` flag.\n")
   270  	}
   271  	return cmdutil.SilentError
   272  }
   273  
   274  // Merge the pull request. May prompt the user for input parameters for the merge.
   275  func (m *mergeContext) merge() error {
   276  	if m.merged {
   277  		return nil
   278  	}
   279  
   280  	payload := mergePayload{
   281  		repo:            m.baseRepo,
   282  		pullRequestID:   m.pr.ID,
   283  		method:          m.opts.MergeMethod,
   284  		auto:            m.autoMerge,
   285  		commitSubject:   m.opts.Subject,
   286  		commitBody:      m.opts.Body,
   287  		setCommitBody:   m.opts.BodySet,
   288  		expectedHeadOid: m.opts.MatchHeadCommit,
   289  		authorEmail:     m.opts.AuthorEmail,
   290  	}
   291  
   292  	if m.shouldAddToMergeQueue() {
   293  		if !m.opts.MergeStrategyEmpty {
   294  			// only warn for now
   295  			_ = m.warnf("%s The merge strategy for %s is set by the merge queue\n", m.cs.Yellow("!"), m.pr.BaseRefName)
   296  		}
   297  		// auto merge will either enable auto merge or add to the merge queue
   298  		payload.auto = true
   299  	} else {
   300  		// get user input if not already given
   301  		if m.opts.MergeStrategyEmpty {
   302  			if !m.opts.IO.CanPrompt() {
   303  				return cmdutil.FlagErrorf("--merge, --rebase, or --squash required when not running interactively")
   304  			}
   305  
   306  			apiClient := api.NewClientFromHTTP(m.httpClient)
   307  			r, err := api.GitHubRepo(apiClient, m.baseRepo)
   308  			if err != nil {
   309  				return err
   310  			}
   311  
   312  			payload.method, err = mergeMethodSurvey(r)
   313  			if err != nil {
   314  				return err
   315  			}
   316  
   317  			m.deleteBranch, err = deleteBranchSurvey(m.opts, m.crossRepoPR, m.localBranchExists)
   318  			if err != nil {
   319  				return err
   320  			}
   321  
   322  			allowEditMsg := payload.method != PullRequestMergeMethodRebase
   323  			for {
   324  				action, err := confirmSurvey(allowEditMsg)
   325  				if err != nil {
   326  					return fmt.Errorf("unable to confirm: %w", err)
   327  				}
   328  
   329  				submit, err := confirmSubmission(m.httpClient, m.opts, action, &payload)
   330  				if err != nil {
   331  					return err
   332  				}
   333  				if submit {
   334  					break
   335  				}
   336  			}
   337  		}
   338  	}
   339  
   340  	err := mergePullRequest(m.httpClient, payload)
   341  	if err != nil {
   342  		return err
   343  	}
   344  
   345  	if m.shouldAddToMergeQueue() {
   346  		_ = m.infof("%s Pull request #%d will be added to the merge queue for %s when ready\n", m.cs.SuccessIconWithColor(m.cs.Green), m.pr.Number, m.pr.BaseRefName)
   347  		return nil
   348  	}
   349  
   350  	if payload.auto {
   351  		method := ""
   352  		switch payload.method {
   353  		case PullRequestMergeMethodRebase:
   354  			method = " via rebase"
   355  		case PullRequestMergeMethodSquash:
   356  			method = " via squash"
   357  		}
   358  		return m.infof("%s Pull request #%d will be automatically merged%s when all requirements are met\n", m.cs.SuccessIconWithColor(m.cs.Green), m.pr.Number, method)
   359  	}
   360  
   361  	action := "Merged"
   362  	switch payload.method {
   363  	case PullRequestMergeMethodRebase:
   364  		action = "Rebased and merged"
   365  	case PullRequestMergeMethodSquash:
   366  		action = "Squashed and merged"
   367  	}
   368  	return m.infof("%s %s pull request #%d (%s)\n", m.cs.SuccessIconWithColor(m.cs.Magenta), action, m.pr.Number, m.pr.Title)
   369  }
   370  
   371  // Delete local branch if requested and if allowed.
   372  func (m *mergeContext) deleteLocalBranch() error {
   373  	if m.crossRepoPR || m.autoMerge {
   374  		return nil
   375  	}
   376  
   377  	if m.merged {
   378  		// prompt for delete
   379  		if m.opts.IO.CanPrompt() && !m.opts.IsDeleteBranchIndicated {
   380  			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
   381  			err := prompt.SurveyAskOne(&survey.Confirm{
   382  				Message: fmt.Sprintf("Pull request #%d was already merged. Delete the branch locally?", m.pr.Number),
   383  				Default: false,
   384  			}, &m.deleteBranch)
   385  			if err != nil {
   386  				return fmt.Errorf("could not prompt: %w", err)
   387  			}
   388  		} else {
   389  			_ = m.warnf(fmt.Sprintf("%s Pull request #%d was already merged\n", m.cs.WarningIcon(), m.pr.Number))
   390  		}
   391  	}
   392  
   393  	if !m.deleteBranch || !m.opts.CanDeleteLocalBranch || !m.localBranchExists {
   394  		return nil
   395  	}
   396  
   397  	currentBranch, err := m.opts.Branch()
   398  	if err != nil {
   399  		return err
   400  	}
   401  
   402  	ctx := context.Background()
   403  
   404  	// branch the command was run on is the same as the pull request branch
   405  	if currentBranch == m.pr.HeadRefName {
   406  		remotes, err := m.opts.Remotes()
   407  		if err != nil {
   408  			return err
   409  		}
   410  
   411  		baseRemote, err := remotes.FindByRepo(m.baseRepo.RepoOwner(), m.baseRepo.RepoName())
   412  		if err != nil {
   413  			return err
   414  		}
   415  
   416  		targetBranch := m.pr.BaseRefName
   417  		if m.opts.GitClient.HasLocalBranch(ctx, targetBranch) {
   418  			if err := m.opts.GitClient.CheckoutBranch(ctx, targetBranch); err != nil {
   419  				return err
   420  			}
   421  		} else {
   422  			if err := m.opts.GitClient.CheckoutNewBranch(ctx, baseRemote.Name, targetBranch); err != nil {
   423  				return err
   424  			}
   425  		}
   426  
   427  		if err := m.opts.GitClient.Pull(ctx, baseRemote.Name, targetBranch); err != nil {
   428  			_ = m.warnf(fmt.Sprintf("%s warning: not possible to fast-forward to: %q\n", m.cs.WarningIcon(), targetBranch))
   429  		}
   430  
   431  		m.switchedToBranch = targetBranch
   432  	}
   433  
   434  	if err := m.opts.GitClient.DeleteLocalBranch(ctx, m.pr.HeadRefName); err != nil {
   435  		return fmt.Errorf("failed to delete local branch %s: %w", m.cs.Cyan(m.pr.HeadRefName), err)
   436  	}
   437  
   438  	return nil
   439  }
   440  
   441  // Delete the remote branch if requested and if allowed.
   442  func (m *mergeContext) deleteRemoteBranch() error {
   443  	// the user was already asked if they want to delete the branch if they didn't provide the flag
   444  	if !m.deleteBranch || m.crossRepoPR || m.autoMerge {
   445  		return nil
   446  	}
   447  
   448  	if !m.merged {
   449  		apiClient := api.NewClientFromHTTP(m.httpClient)
   450  		err := api.BranchDeleteRemote(apiClient, m.baseRepo, m.pr.HeadRefName)
   451  		var httpErr api.HTTPError
   452  		// The ref might have already been deleted by GitHub
   453  		if err != nil && (!errors.As(err, &httpErr) || httpErr.StatusCode != 422) {
   454  			return fmt.Errorf("failed to delete remote branch %s: %w", m.cs.Cyan(m.pr.HeadRefName), err)
   455  		}
   456  	}
   457  
   458  	branch := ""
   459  	if m.switchedToBranch != "" {
   460  		branch = fmt.Sprintf(" and switched to branch %s", m.cs.Cyan(m.switchedToBranch))
   461  	}
   462  	return m.infof("%s Deleted branch %s%s\n", m.cs.SuccessIconWithColor(m.cs.Red), m.cs.Cyan(m.pr.HeadRefName), branch)
   463  }
   464  
   465  // Add the Pull Request to a merge queue
   466  // Admins can bypass the queue and merge directly
   467  func (m *mergeContext) shouldAddToMergeQueue() bool {
   468  	return m.mergeQueueRequired && !m.opts.UseAdmin
   469  }
   470  
   471  func (m *mergeContext) warnf(format string, args ...interface{}) error {
   472  	_, err := fmt.Fprintf(m.opts.IO.ErrOut, format, args...)
   473  	return err
   474  }
   475  
   476  func (m *mergeContext) infof(format string, args ...interface{}) error {
   477  	if !m.isTerminal {
   478  		return nil
   479  	}
   480  	_, err := fmt.Fprintf(m.opts.IO.ErrOut, format, args...)
   481  	return err
   482  }
   483  
   484  // Creates a new MergeConext from MergeOptions.
   485  func NewMergeContext(opts *MergeOptions) (*mergeContext, error) {
   486  	findOptions := shared.FindOptions{
   487  		Selector: opts.SelectorArg,
   488  		Fields:   []string{"id", "number", "state", "title", "lastCommit", "mergeStateStatus", "headRepositoryOwner", "headRefName", "baseRefName", "headRefOid", "isInMergeQueue", "isMergeQueueEnabled"},
   489  	}
   490  	pr, baseRepo, err := opts.Finder.Find(findOptions)
   491  	if err != nil {
   492  		return nil, err
   493  	}
   494  
   495  	httpClient, err := opts.HttpClient()
   496  	if err != nil {
   497  		return nil, err
   498  	}
   499  
   500  	return &mergeContext{
   501  		opts:               opts,
   502  		pr:                 pr,
   503  		cs:                 opts.IO.ColorScheme(),
   504  		baseRepo:           baseRepo,
   505  		isTerminal:         opts.IO.IsStdoutTTY(),
   506  		httpClient:         httpClient,
   507  		merged:             pr.State == MergeStateStatusMerged,
   508  		deleteBranch:       opts.DeleteBranch,
   509  		crossRepoPR:        pr.HeadRepositoryOwner.Login != baseRepo.RepoOwner(),
   510  		autoMerge:          opts.AutoMergeEnable && !isImmediatelyMergeable(pr.MergeStateStatus),
   511  		localBranchExists:  opts.CanDeleteLocalBranch && opts.GitClient.HasLocalBranch(context.Background(), pr.HeadRefName),
   512  		mergeQueueRequired: pr.IsMergeQueueEnabled,
   513  	}, nil
   514  }
   515  
   516  // Run the merge command.
   517  func mergeRun(opts *MergeOptions) error {
   518  	ctx, err := NewMergeContext(opts)
   519  	if err != nil {
   520  		return err
   521  	}
   522  
   523  	if err := ctx.inMergeQueue(); err != nil {
   524  		return err
   525  	}
   526  
   527  	// no further action is possible when disabling auto merge
   528  	if opts.AutoMergeDisable {
   529  		return ctx.disableAutoMerge()
   530  	}
   531  
   532  	ctx.warnIfDiverged()
   533  
   534  	if err := ctx.canMerge(); err != nil {
   535  		return err
   536  	}
   537  
   538  	if err := ctx.merge(); err != nil {
   539  		return err
   540  	}
   541  
   542  	if err := ctx.deleteLocalBranch(); err != nil {
   543  		return err
   544  	}
   545  
   546  	if err := ctx.deleteRemoteBranch(); err != nil {
   547  		return err
   548  	}
   549  
   550  	return nil
   551  }
   552  
   553  func mergeMethodSurvey(baseRepo *api.Repository) (PullRequestMergeMethod, error) {
   554  	type mergeOption struct {
   555  		title  string
   556  		method PullRequestMergeMethod
   557  	}
   558  
   559  	var mergeOpts []mergeOption
   560  	if baseRepo.MergeCommitAllowed {
   561  		opt := mergeOption{title: "Create a merge commit", method: PullRequestMergeMethodMerge}
   562  		mergeOpts = append(mergeOpts, opt)
   563  	}
   564  	if baseRepo.RebaseMergeAllowed {
   565  		opt := mergeOption{title: "Rebase and merge", method: PullRequestMergeMethodRebase}
   566  		mergeOpts = append(mergeOpts, opt)
   567  	}
   568  	if baseRepo.SquashMergeAllowed {
   569  		opt := mergeOption{title: "Squash and merge", method: PullRequestMergeMethodSquash}
   570  		mergeOpts = append(mergeOpts, opt)
   571  	}
   572  
   573  	var surveyOpts []string
   574  	for _, v := range mergeOpts {
   575  		surveyOpts = append(surveyOpts, v.title)
   576  	}
   577  
   578  	mergeQuestion := &survey.Select{
   579  		Message: "What merge method would you like to use?",
   580  		Options: surveyOpts,
   581  	}
   582  
   583  	var result int
   584  	//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
   585  	err := prompt.SurveyAskOne(mergeQuestion, &result)
   586  	return mergeOpts[result].method, err
   587  }
   588  
   589  func deleteBranchSurvey(opts *MergeOptions, crossRepoPR, localBranchExists bool) (bool, error) {
   590  	if !crossRepoPR && !opts.IsDeleteBranchIndicated {
   591  		var message string
   592  		if opts.CanDeleteLocalBranch && localBranchExists {
   593  			message = "Delete the branch locally and on GitHub?"
   594  		} else {
   595  			message = "Delete the branch on GitHub?"
   596  		}
   597  
   598  		var result bool
   599  		submit := &survey.Confirm{
   600  			Message: message,
   601  			Default: false,
   602  		}
   603  		//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
   604  		err := prompt.SurveyAskOne(submit, &result)
   605  		return result, err
   606  	}
   607  
   608  	return opts.DeleteBranch, nil
   609  }
   610  
   611  func confirmSurvey(allowEditMsg bool) (shared.Action, error) {
   612  	const (
   613  		submitLabel            = "Submit"
   614  		editCommitSubjectLabel = "Edit commit subject"
   615  		editCommitMsgLabel     = "Edit commit message"
   616  		cancelLabel            = "Cancel"
   617  	)
   618  
   619  	options := []string{submitLabel}
   620  	if allowEditMsg {
   621  		options = append(options, editCommitSubjectLabel, editCommitMsgLabel)
   622  	}
   623  	options = append(options, cancelLabel)
   624  
   625  	var result string
   626  	submit := &survey.Select{
   627  		Message: "What's next?",
   628  		Options: options,
   629  	}
   630  	//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
   631  	err := prompt.SurveyAskOne(submit, &result)
   632  	if err != nil {
   633  		return shared.CancelAction, fmt.Errorf("could not prompt: %w", err)
   634  	}
   635  
   636  	switch result {
   637  	case submitLabel:
   638  		return shared.SubmitAction, nil
   639  	case editCommitSubjectLabel:
   640  		return shared.EditCommitSubjectAction, nil
   641  	case editCommitMsgLabel:
   642  		return shared.EditCommitMessageAction, nil
   643  	default:
   644  		return shared.CancelAction, nil
   645  	}
   646  }
   647  
   648  func confirmSubmission(client *http.Client, opts *MergeOptions, action shared.Action, payload *mergePayload) (bool, error) {
   649  	var err error
   650  
   651  	switch action {
   652  	case shared.EditCommitMessageAction:
   653  		if !payload.setCommitBody {
   654  			_, payload.commitBody, err = getMergeText(client, payload.repo, payload.pullRequestID, payload.method)
   655  			if err != nil {
   656  				return false, err
   657  			}
   658  		}
   659  
   660  		payload.commitBody, err = opts.Editor.Edit("*.md", payload.commitBody)
   661  		if err != nil {
   662  			return false, err
   663  		}
   664  		payload.setCommitBody = true
   665  
   666  		return false, nil
   667  
   668  	case shared.EditCommitSubjectAction:
   669  		if payload.commitSubject == "" {
   670  			payload.commitSubject, _, err = getMergeText(client, payload.repo, payload.pullRequestID, payload.method)
   671  			if err != nil {
   672  				return false, err
   673  			}
   674  		}
   675  
   676  		payload.commitSubject, err = opts.Editor.Edit("*.md", payload.commitSubject)
   677  		if err != nil {
   678  			return false, err
   679  		}
   680  
   681  		return false, nil
   682  
   683  	case shared.CancelAction:
   684  		fmt.Fprintln(opts.IO.ErrOut, "Cancelled.")
   685  		return false, cmdutil.CancelError
   686  
   687  	case shared.SubmitAction:
   688  		return true, nil
   689  
   690  	default:
   691  		return false, fmt.Errorf("unable to confirm: %w", err)
   692  	}
   693  }
   694  
   695  type userEditor struct {
   696  	io     *iostreams.IOStreams
   697  	config func() (config.Config, error)
   698  }
   699  
   700  func (e *userEditor) Edit(filename, startingText string) (string, error) {
   701  	editorCommand, err := cmdutil.DetermineEditor(e.config)
   702  	if err != nil {
   703  		return "", err
   704  	}
   705  
   706  	return surveyext.Edit(editorCommand, filename, startingText, e.io.In, e.io.Out, e.io.ErrOut)
   707  }
   708  
   709  // blockedReason translates various MergeStateStatus GraphQL values into human-readable reason
   710  func blockedReason(status string, useAdmin bool) string {
   711  	switch status {
   712  	case MergeStateStatusBlocked:
   713  		if useAdmin {
   714  			return ""
   715  		}
   716  		return "the base branch policy prohibits the merge"
   717  	case MergeStateStatusBehind:
   718  		if useAdmin {
   719  			return ""
   720  		}
   721  		return "the head branch is not up to date with the base branch"
   722  	case MergeStateStatusDirty:
   723  		return "the merge commit cannot be cleanly created"
   724  	default:
   725  		return ""
   726  	}
   727  }
   728  
   729  func allowsAdminOverride(status string) bool {
   730  	switch status {
   731  	case MergeStateStatusBlocked, MergeStateStatusBehind:
   732  		return true
   733  	default:
   734  		return false
   735  	}
   736  }
   737  
   738  func remoteForMergeConflictResolution(baseRepo ghrepo.Interface, pr *api.PullRequest, opts *MergeOptions) *ghContext.Remote {
   739  	if !mergeConflictStatus(pr.MergeStateStatus) || !opts.CanDeleteLocalBranch {
   740  		return nil
   741  	}
   742  	remotes, err := opts.Remotes()
   743  	if err != nil {
   744  		return nil
   745  	}
   746  	remote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
   747  	if err != nil {
   748  		return nil
   749  	}
   750  	return remote
   751  }
   752  
   753  func mergeConflictStatus(status string) bool {
   754  	return status == MergeStateStatusDirty
   755  }
   756  
   757  func isImmediatelyMergeable(status string) bool {
   758  	switch status {
   759  	case MergeStateStatusClean, MergeStateStatusHasHooks, MergeStateStatusUnstable:
   760  		return true
   761  	default:
   762  		return false
   763  	}
   764  }