diff --git a/commands/cmdutils/cmdutils.go b/commands/cmdutils/cmdutils.go index 86e09f36..f3b22bb0 100644 --- a/commands/cmdutils/cmdutils.go +++ b/commands/cmdutils/cmdutils.go @@ -99,7 +99,7 @@ func GetEditor(cf func() (config.Config, error)) (string, error) { return editorCommand, nil } -func DescriptionPrompt(response *string, templateContent, editorCommand string) error { +func EditorPrompt(response *string, question, templateContent, editorCommand string) error { defaultBody := *response if templateContent != "" { if defaultBody != "" { @@ -112,7 +112,7 @@ func DescriptionPrompt(response *string, templateContent, editorCommand string) qs := []*survey.Question{ { - Name: "Description", + Name: question, Prompt: &surveyext.GLabEditor{ BlankAllowed: true, EditorCommand: editorCommand, @@ -265,6 +265,7 @@ const ( PreviewAction AddMetadataAction CancelAction + EditCommitMessageAction ) func ConfirmSubmission(allowPreview bool, allowAddMetadata bool) (Action, error) { diff --git a/commands/issue/create/issue_create.go b/commands/issue/create/issue_create.go index 30dbc840..78029d82 100644 --- a/commands/issue/create/issue_create.go +++ b/commands/issue/create/issue_create.go @@ -219,7 +219,7 @@ func createRun(opts *CreateOpts) error { if err != nil { return err } - err = cmdutils.DescriptionPrompt(&opts.Description, templateContents, editor) + err = cmdutils.EditorPrompt(&opts.Description, "Description", templateContents, editor) if err != nil { return err } diff --git a/commands/mr/create/mr_create.go b/commands/mr/create/mr_create.go index 12ed8a07..4e974709 100644 --- a/commands/mr/create/mr_create.go +++ b/commands/mr/create/mr_create.go @@ -324,7 +324,7 @@ func createRun(opts *CreateOpts) error { if err != nil { return err } - err = cmdutils.DescriptionPrompt(&opts.Description, templateContents, editor) + err = cmdutils.EditorPrompt(&opts.Description, "Description", templateContents, editor) if err != nil { return err } diff --git a/commands/mr/merge/mr_merge.go b/commands/mr/merge/mr_merge.go index 2259a93d..41f25cff 100644 --- a/commands/mr/merge/mr_merge.go +++ b/commands/mr/merge/mr_merge.go @@ -1,8 +1,12 @@ package merge import ( + "errors" "fmt" + "github.com/profclems/glab/pkg/surveyext" + + "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/profclems/glab/api" "github.com/profclems/glab/commands/mr/mrutils" @@ -13,18 +17,31 @@ import ( "github.com/xanzy/go-gitlab" ) +type MRMergeMethod int + +const ( + MRMergeMethodMerge MRMergeMethod = iota + MRMergeMethodSquash + MRMergeMethodRebase +) + type MergeOpts struct { MergeWhenPipelineSucceeds bool SquashBeforeMerge bool + RebaseBeforeMerge bool RemoveSourceBranch bool SquashMessage string MergeCommitMessage string SHA string + + MergeMethod MRMergeMethod } func NewCmdMerge(f *cmdutils.Factory) *cobra.Command { - var opts = &MergeOpts{} + var opts = &MergeOpts{ + MergeMethod: MRMergeMethodMerge, + } var mrMergeCmd = &cobra.Command{ Use: "merge { | }", @@ -41,6 +58,14 @@ func NewCmdMerge(f *cmdutils.Factory) *cobra.Command { var err error c := f.IO.Color() + if opts.SquashBeforeMerge && opts.RebaseBeforeMerge { + return &cmdutils.FlagError{Err: errors.New("only one of --rebase, or --squash can be enabled")} + } + + if !opts.SquashBeforeMerge && opts.SquashMessage != "" { + return &cmdutils.FlagError{Err: errors.New("--squash-message can only be used with --squash")} + } + apiClient, err := f.HttpClient() if err != nil { return err @@ -66,6 +91,55 @@ func NewCmdMerge(f *cmdutils.Factory) *cobra.Command { _ = prompt.Confirm(&opts.MergeWhenPipelineSucceeds, "Merge when pipeline succeeds?", true) } + if f.IO.IsOutputTTY() { + if !opts.SquashBeforeMerge && !opts.RebaseBeforeMerge && opts.MergeCommitMessage == "" { + opts.MergeMethod, err = mergeMethodSurvey() + if err != nil { + return err + } + if opts.MergeMethod == MRMergeMethodSquash { + opts.SquashBeforeMerge = true + } else if opts.MergeMethod == MRMergeMethodRebase { + opts.RebaseBeforeMerge = true + } + } + + if opts.MergeCommitMessage == "" && opts.SquashMessage == "" { + action, err := confirmSurvey(opts.MergeMethod != MRMergeMethodRebase) + if err != nil { + return fmt.Errorf("unable to prompt: %w", err) + } + + if action == cmdutils.EditCommitMessageAction { + var mergeMessage string + + editor, err := cmdutils.GetEditor(f.Config) + if err != nil { + return err + } + mergeMessage, err = surveyext.Edit(editor, "*.md", mr.Title, f.IO.In, f.IO.StdOut, f.IO.StdErr, nil) + if err != nil { + return err + } + + if opts.SquashBeforeMerge { + opts.SquashMessage = mergeMessage + } else { + opts.MergeCommitMessage = mergeMessage + } + + action, err = confirmSurvey(false) + if err != nil { + return fmt.Errorf("unable to confirm: %w", err) + } + } + if action == cmdutils.CancelAction { + fmt.Fprintln(f.IO.StdErr, "Cancelled.") + return cmdutils.SilentError + } + } + } + mergeOpts := &gitlab.AcceptMergeRequestOptions{} if opts.MergeCommitMessage != "" { mergeOpts.MergeCommitMessage = gitlab.String(opts.MergeCommitMessage) @@ -80,37 +154,58 @@ func NewCmdMerge(f *cmdutils.Factory) *cobra.Command { mergeOpts.ShouldRemoveSourceBranch = gitlab.Bool(true) } if opts.MergeWhenPipelineSucceeds && mr.Pipeline != nil { + if mr.Pipeline.Status == "canceled" || mr.Pipeline.Status == "failed" { + fmt.Fprintln(f.IO.StdOut, c.FailedIcon(), "Pipeline Status:", mr.Pipeline.Status) + fmt.Fprintln(f.IO.StdOut, c.FailedIcon(), "Cannot perform merge action") + return cmdutils.SilentError + } mergeOpts.MergeWhenPipelineSucceeds = gitlab.Bool(true) } if opts.SHA != "" { mergeOpts.SHA = gitlab.String(opts.SHA) } - fmt.Fprintf(f.IO.StdOut, "- Merging merge request !%d\n", mr.IID) + if opts.RebaseBeforeMerge { + err := mrutils.RebaseMR(f.IO, apiClient, repo, mr) + if err != nil { + return err + } + } + f.IO.StartSpinner("Merging merge request !%d", mr.IID) mr, err = api.MergeMR(apiClient, repo.FullName(), mr.IID, mergeOpts) - if err != nil { return err } - + f.IO.StopSpinner("") isMerged := true if opts.MergeWhenPipelineSucceeds { if mr.Pipeline == nil { fmt.Fprintln(f.IO.StdOut, c.WarnIcon(), "No pipeline running on", mr.SourceBranch) - } else if mr.Pipeline.Status == "success" { - fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), "Pipeline Succeeded") } else { - fmt.Fprintln(f.IO.StdOut, c.WarnIcon(), "Pipeline Status:", mr.Pipeline.Status) - fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), "Will merge when pipeline succeeds") - isMerged = false + switch mr.Pipeline.Status { + case "success": + fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), "Pipeline Succeeded") + default: + fmt.Fprintln(f.IO.StdOut, c.WarnIcon(), "Pipeline Status:", mr.Pipeline.Status) + if mr.State != "merged" { + fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), "Will merge when pipeline succeeds") + isMerged = false + } + } } } if isMerged { - fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), "Merged") + action := "Merged" + switch opts.MergeMethod { + case MRMergeMethodRebase: + action = "Rebased and merged" + case MRMergeMethodSquash: + action = "Squashed and merged" + } + fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), action) } fmt.Fprintln(f.IO.StdOut, mrutils.DisplayMR(c, mr)) - return nil }, } @@ -121,6 +216,67 @@ func NewCmdMerge(f *cmdutils.Factory) *cobra.Command { mrMergeCmd.Flags().StringVarP(&opts.MergeCommitMessage, "message", "m", "", "Custom merge commit message") mrMergeCmd.Flags().StringVarP(&opts.SquashMessage, "squash-message", "", "", "Custom Squash commit message") mrMergeCmd.Flags().BoolVarP(&opts.SquashBeforeMerge, "squash", "s", false, "Squash commits on merge") + mrMergeCmd.Flags().BoolVarP(&opts.RebaseBeforeMerge, "rebase", "r", false, "Rebase the commits onto the base branch\n") return mrMergeCmd } + +func mergeMethodSurvey() (MRMergeMethod, error) { + type mergeOption struct { + title string + method MRMergeMethod + } + + var mergeOpts = []mergeOption{ + {title: "Create a merge commit", method: MRMergeMethodMerge}, + {title: "Rebase and merge", method: MRMergeMethodRebase}, + {title: "Squash and merge", method: MRMergeMethodSquash}, + } + + var surveyOpts []string + for _, v := range mergeOpts { + surveyOpts = append(surveyOpts, v.title) + } + + mergeQuestion := &survey.Select{ + Message: "What merge method would you like to use?", + Options: surveyOpts, + } + + var result int + err := prompt.AskOne(mergeQuestion, &result) + return mergeOpts[result].method, err +} + +func confirmSurvey(allowEditMsg bool) (cmdutils.Action, error) { + const ( + submitLabel = "Submit" + editCommitMsgLabel = "Edit commit message" + cancelLabel = "Cancel" + ) + + options := []string{submitLabel} + if allowEditMsg { + options = append(options, editCommitMsgLabel) + } + options = append(options, cancelLabel) + + var result string + submit := &survey.Select{ + Message: "What's next?", + Options: options, + } + err := prompt.AskOne(submit, &result) + if err != nil { + return cmdutils.CancelAction, fmt.Errorf("could not prompt: %w", err) + } + + switch result { + case submitLabel: + return cmdutils.SubmitAction, nil + case editCommitMsgLabel: + return cmdutils.EditCommitMessageAction, nil + default: + return cmdutils.CancelAction, nil + } +} diff --git a/commands/mr/mrutils/mrutils.go b/commands/mr/mrutils/mrutils.go index 0d0d3738..cc3a31e0 100644 --- a/commands/mr/mrutils/mrutils.go +++ b/commands/mr/mrutils/mrutils.go @@ -2,6 +2,7 @@ package mrutils import ( "context" + "errors" "fmt" "strconv" "strings" @@ -279,3 +280,42 @@ var getMRForBranch = func(apiClient *gitlab.Client, baseRepo glrepo.Interface, a } return mrMap[pickedMR], nil } + +func RebaseMR(ios *iostreams.IOStreams, apiClient *gitlab.Client, repo glrepo.Interface, mr *gitlab.MergeRequest) error { + ios.StartSpinner("Sending rebase request...") + err := api.RebaseMR(apiClient, repo.FullName(), mr.IID) + if err != nil { + return err + } + ios.StopSpinner("") + + opts := &gitlab.GetMergeRequestsOptions{} + opts.IncludeRebaseInProgress = gitlab.Bool(true) + ios.StartSpinner("Checking rebase status...") + errorMSG := "" + i := 0 + for { + mr, err := api.GetMR(apiClient, repo.FullName(), mr.IID, opts) + if err != nil { + errorMSG = err.Error() + break + } + if i == 0 { + ios.StopSpinner("") + ios.StartSpinner("Rebase in progress...") + } + if !mr.RebaseInProgress { + if mr.MergeError != "" && mr.MergeError != "null" { + errorMSG = mr.MergeError + } + break + } + i++ + } + ios.StopSpinner("") + if errorMSG != "" { + return errors.New(errorMSG) + } + fmt.Fprintln(ios.StdOut, ios.Color().GreenCheck(), "Rebase successful") + return nil +} diff --git a/commands/mr/rebase/mr_rebase.go b/commands/mr/rebase/mr_rebase.go index f418503a..871a894b 100644 --- a/commands/mr/rebase/mr_rebase.go +++ b/commands/mr/rebase/mr_rebase.go @@ -1,15 +1,11 @@ package rebase import ( - "fmt" - "github.com/MakeNowJust/heredoc" - "github.com/profclems/glab/api" "github.com/profclems/glab/commands/cmdutils" "github.com/profclems/glab/commands/mr/mrutils" "github.com/spf13/cobra" - "github.com/xanzy/go-gitlab" ) func NewCmdRebase(f *cmdutils.Factory) *cobra.Command { @@ -25,7 +21,6 @@ func NewCmdRebase(f *cmdutils.Factory) *cobra.Command { Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var err error - c := f.IO.Color() apiClient, err := f.HttpClient() if err != nil { @@ -37,36 +32,10 @@ func NewCmdRebase(f *cmdutils.Factory) *cobra.Command { return err } - fmt.Fprintln(f.IO.StdOut, "- Sending request...") - err = api.RebaseMR(apiClient, repo.FullName(), mr.IID) - if err != nil { + if err = mrutils.RebaseMR(f.IO, apiClient, repo, mr); err != nil { return err } - opts := &gitlab.GetMergeRequestsOptions{} - opts.IncludeRebaseInProgress = gitlab.Bool(true) - fmt.Fprintln(f.IO.StdOut, "- Checking rebase status...") - i := 0 - for { - mr, err := api.GetMR(apiClient, repo.FullName(), mr.IID, opts) - if err != nil { - return err - } - if mr.RebaseInProgress { - if i == 0 { - fmt.Fprintln(f.IO.StdOut, "- Rebase in progress...") - } - } else { - if mr.MergeError != "" && mr.MergeError != "null" { - fmt.Fprintln(f.IO.StdErr, mr.MergeError) - break - } - fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), "Rebase successful") - break - } - i++ - } - return nil }, } diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index a76de3fc..6e2de354 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -2,6 +2,7 @@ package iostreams import ( "bytes" + "fmt" "io" "io/ioutil" "os" @@ -141,20 +142,20 @@ func (s *IOStreams) StopPager() { s.pagerProcess = nil } -func (s *IOStreams) StartSpinner(loadingMSG string) { +func (s *IOStreams) StartSpinner(format string, a ...interface{}) { if s.IsOutputTTY() { s.spinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond, spinner.WithWriter(s.StdErr)) - if loadingMSG != "" { - s.spinner.Suffix = " " + loadingMSG + if format != "" { + s.spinner.Suffix = fmt.Sprintf(" "+format, a...) } s.spinner.Start() } } -func (s *IOStreams) StopSpinner(finalMSG string) { +func (s *IOStreams) StopSpinner(format string, a ...interface{}) { if s.spinner != nil { s.spinner.Suffix = "" - s.spinner.FinalMSG = finalMSG + s.spinner.FinalMSG = fmt.Sprintf(format, a...) s.spinner.Stop() s.spinner = nil }