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 }