2020-09-29 05:55:34 +00:00
package create
import (
2020-11-29 22:04:41 +00:00
"errors"
2020-09-29 05:55:34 +00:00
"fmt"
2020-12-26 15:18:50 +00:00
"net/url"
2020-12-25 21:48:29 +00:00
"os"
2021-08-11 23:45:26 +00:00
"regexp"
2020-12-25 02:13:12 +00:00
"strings"
2020-11-30 20:49:08 +00:00
2021-11-04 01:21:05 +00:00
"github.com/AlecAivazis/survey/v2"
2022-09-19 20:23:45 +00:00
"gitlab.com/gitlab-org/cli/commands/issue/issueutils"
"gitlab.com/gitlab-org/cli/pkg/prompt"
2021-11-04 01:21:05 +00:00
2022-09-19 20:23:45 +00:00
"gitlab.com/gitlab-org/cli/pkg/iostreams"
2021-01-28 22:38:32 +00:00
2021-01-08 19:13:54 +00:00
"github.com/MakeNowJust/heredoc"
2022-09-19 20:23:45 +00:00
"gitlab.com/gitlab-org/cli/internal/config"
"gitlab.com/gitlab-org/cli/internal/glrepo"
2020-12-25 02:14:21 +00:00
2020-09-29 05:55:34 +00:00
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
2022-09-19 20:23:45 +00:00
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/mr/mrutils"
"gitlab.com/gitlab-org/cli/pkg/git"
"gitlab.com/gitlab-org/cli/pkg/utils"
2020-09-29 05:55:34 +00:00
)
2020-11-29 18:04:15 +00:00
type CreateOpts struct {
2021-01-23 03:07:26 +00:00
Title string
Description string
SourceBranch string
TargetBranch string
TargetTrackingBranch string
Labels [ ] string
Assignees [ ] string
2021-10-03 17:21:24 +00:00
Reviewers [ ] string
2021-01-23 03:07:26 +00:00
MileStone int
MilestoneFlag string
MRCreateTargetProject string
2020-11-29 22:04:41 +00:00
2021-11-04 01:21:05 +00:00
RelatedIssue string
CopyIssueLabels bool
2020-11-29 18:04:15 +00:00
CreateSourceBranch bool
RemoveSourceBranch bool
AllowCollaboration bool
2021-10-04 17:41:53 +00:00
SquashBeforeMerge bool
2020-11-29 18:04:15 +00:00
2021-08-11 23:45:26 +00:00
Autofill bool
FillCommitBody bool
IsDraft bool
IsWIP bool
ShouldPush bool
NoEditor bool
IsInteractive bool
Yes bool
Web bool
2020-11-30 20:49:08 +00:00
2021-01-28 22:38:32 +00:00
IO * iostreams . IOStreams
2020-11-30 20:49:08 +00:00
Branch func ( ) ( string , error )
Remotes func ( ) ( glrepo . Remotes , error )
Lab func ( ) ( * gitlab . Client , error )
Config func ( ) ( config . Config , error )
BaseRepo func ( ) ( glrepo . Interface , error )
2020-12-25 21:48:06 +00:00
HeadRepo func ( ) ( glrepo . Interface , error )
2020-12-26 15:18:50 +00:00
2020-12-27 01:40:35 +00:00
// SourceProject is the Project we create the merge request in and where we push our branch
// it is the project we have permission to push so most likely one's fork
SourceProject * gitlab . Project
// TargetProject is the one we query for changes between our branch and the target branch
// it is the one we merge request will appear in
2020-12-26 15:18:50 +00:00
TargetProject * gitlab . Project
2020-11-29 18:04:15 +00:00
}
2021-01-23 03:07:26 +00:00
func NewCmdCreate ( f * cmdutils . Factory , runE func ( opts * CreateOpts ) error ) * cobra . Command {
2020-11-30 20:49:08 +00:00
opts := & CreateOpts {
IO : f . IO ,
Branch : f . Branch ,
Remotes : f . Remotes ,
Config : f . Config ,
2020-12-25 21:48:06 +00:00
HeadRepo : resolvedHeadRepo ( f ) ,
2020-11-30 20:49:08 +00:00
}
2020-09-29 05:55:34 +00:00
var mrCreateCmd = & cobra . Command {
Use : "create" ,
Short : ` Create new merge request ` ,
Long : ` ` ,
Aliases : [ ] string { "new" } ,
2021-01-08 19:13:54 +00:00
Example : heredoc . Doc ( `
2022-10-21 14:38:48 +00:00
glab mr new
glab mr create - a username - t "fix annoying bug"
glab mr create - f -- draft -- label RFC
glab mr create -- fill -- yes -- web
glab mr create -- fill -- fill - commit - body -- yes
2021-01-08 19:13:54 +00:00
` ) ,
Args : cobra . ExactArgs ( 0 ) ,
2020-12-25 21:48:29 +00:00
PreRun : func ( cmd * cobra . Command , args [ ] string ) {
repoOverride , _ := cmd . Flags ( ) . GetString ( "head" )
if repoFromEnv := os . Getenv ( "GITLAB_HEAD_REPO" ) ; repoOverride == "" && repoFromEnv != "" {
repoOverride = repoFromEnv
}
if repoOverride != "" {
_ = headRepoOverride ( opts , repoOverride )
}
} ,
2020-09-29 05:55:34 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2020-12-30 21:08:34 +00:00
// support `-R, --repo` override
//
// NOTE: it is important to assign the BaseRepo and HTTPClient in RunE because
// they are overridden in a PersistentRun hook (when `-R, --repo` is specified)
// which runs before RunE is executed
opts . BaseRepo = f . BaseRepo
opts . Lab = f . HttpClient
2020-11-29 22:04:41 +00:00
hasTitle := cmd . Flags ( ) . Changed ( "title" )
hasDescription := cmd . Flags ( ) . Changed ( "description" )
// disable interactive mode if title and description are explicitly defined
opts . IsInteractive = ! ( hasTitle && hasDescription )
2021-01-17 21:26:18 +00:00
if hasTitle && hasDescription && opts . Autofill {
return & cmdutils . FlagError {
2021-07-13 07:45:05 +00:00
Err : errors . New ( "usage of --title and --description completely override --fill" ) ,
2021-01-17 21:26:18 +00:00
}
}
2020-11-30 20:49:08 +00:00
if opts . IsInteractive && ! opts . IO . PromptEnabled ( ) && ! opts . Autofill {
2020-11-29 22:04:41 +00:00
return & cmdutils . FlagError { Err : errors . New ( "--title or --fill required for non-interactive mode" ) }
}
2020-12-26 15:18:50 +00:00
if cmd . Flags ( ) . Changed ( "wip" ) && cmd . Flags ( ) . Changed ( "draft" ) {
return & cmdutils . FlagError { Err : errors . New ( "specify either of --draft or --wip" ) }
}
2021-08-11 23:45:26 +00:00
if ! opts . Autofill && opts . FillCommitBody {
return & cmdutils . FlagError { Err : errors . New ( "--fill-commit-body should be used with --fill" ) }
}
2021-01-13 19:45:55 +00:00
// Remove this once --yes does more than just skip the prompts that --web happen to skip
// by design
if opts . Yes && opts . Web {
return & cmdutils . FlagError { Err : errors . New ( "--web already skips all prompts currently skipped by --yes" ) }
}
2020-11-29 22:04:41 +00:00
2021-11-04 01:21:05 +00:00
if opts . CopyIssueLabels && opts . RelatedIssue == "" {
return & cmdutils . FlagError { Err : errors . New ( "--copy-issue-labels can only be used with --related-issue" ) }
}
2021-01-23 03:07:26 +00:00
if runE != nil {
return runE ( opts )
2020-09-29 05:55:34 +00:00
}
2021-01-23 03:07:26 +00:00
return createRun ( opts )
} ,
}
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . Autofill , "fill" , "f" , false , "Do not prompt for title/description and just use commit info" )
2021-08-11 23:45:26 +00:00
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . FillCommitBody , "fill-commit-body" , "" , false , "Fill description with each commit body when multiple commits. Can only be used with --fill" )
2021-01-23 03:07:26 +00:00
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . IsDraft , "draft" , "" , false , "Mark merge request as a draft" )
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . IsWIP , "wip" , "" , false , "Mark merge request as a work in progress. Alternative to --draft" )
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . ShouldPush , "push" , "" , false , "Push committed changes after creating merge request. Make sure you have committed changes" )
mrCreateCmd . Flags ( ) . StringVarP ( & opts . Title , "title" , "t" , "" , "Supply a title for merge request" )
mrCreateCmd . Flags ( ) . StringVarP ( & opts . Description , "description" , "d" , "" , "Supply a description for merge request" )
mrCreateCmd . Flags ( ) . StringSliceVarP ( & opts . Labels , "label" , "l" , [ ] string { } , "Add label by name. Multiple labels should be comma separated" )
mrCreateCmd . Flags ( ) . StringSliceVarP ( & opts . Assignees , "assignee" , "a" , [ ] string { } , "Assign merge request to people by their `usernames`" )
2021-10-03 17:21:24 +00:00
mrCreateCmd . Flags ( ) . StringSliceVarP ( & opts . Reviewers , "reviewer" , "" , [ ] string { } , "Request review from users by their `usernames`" )
2021-01-23 03:07:26 +00:00
mrCreateCmd . Flags ( ) . StringVarP ( & opts . SourceBranch , "source-branch" , "s" , "" , "The Branch you are creating the merge request. Default is the current branch." )
mrCreateCmd . Flags ( ) . StringVarP ( & opts . TargetBranch , "target-branch" , "b" , "" , "The target or base branch into which you want your code merged" )
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . CreateSourceBranch , "create-source-branch" , "" , false , "Create source branch if it does not exist" )
mrCreateCmd . Flags ( ) . StringVarP ( & opts . MilestoneFlag , "milestone" , "m" , "" , "The global ID or title of a milestone to assign" )
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . AllowCollaboration , "allow-collaboration" , "" , false , "Allow commits from other members" )
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . RemoveSourceBranch , "remove-source-branch" , "" , false , "Remove Source Branch on merge" )
2021-10-04 06:58:11 +00:00
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . SquashBeforeMerge , "squash-before-merge" , "" , false , "Squash commits into a single commit when merging" )
2021-01-23 03:07:26 +00:00
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . NoEditor , "no-editor" , "" , false , "Don't open editor to enter description. If set to true, uses prompt. Default is false" )
mrCreateCmd . Flags ( ) . StringP ( "head" , "H" , "" , "Select another head repository using the `OWNER/REPO` or `GROUP/NAMESPACE/REPO` format or the project ID or full URL" )
2021-03-12 04:08:54 +00:00
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . Yes , "yes" , "y" , false , "Skip submission confirmation prompt, with --fill it skips all optional prompts" )
2021-01-23 03:07:26 +00:00
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . Web , "web" , "w" , false , "continue merge request creation on web browser" )
2021-11-04 01:21:05 +00:00
mrCreateCmd . Flags ( ) . BoolVarP ( & opts . CopyIssueLabels , "copy-issue-labels" , "" , false , "Copy labels from issue to the merge request. Used with --related-issue" )
mrCreateCmd . Flags ( ) . StringVarP ( & opts . RelatedIssue , "related-issue" , "i" , "" , "Create merge request for an issue. The merge request title will be created from the issue if --title is not provided." )
2020-09-29 05:55:34 +00:00
2021-01-23 03:07:26 +00:00
mrCreateCmd . Flags ( ) . StringVarP ( & opts . MRCreateTargetProject , "target-project" , "" , "" , "Add target project by id or OWNER/REPO or GROUP/NAMESPACE/REPO" )
2021-01-29 11:23:55 +00:00
_ = mrCreateCmd . Flags ( ) . MarkHidden ( "target-project" )
_ = mrCreateCmd . Flags ( ) . MarkDeprecated ( "target-project" , "Use --repo instead" )
2021-01-23 03:07:26 +00:00
return mrCreateCmd
}
2020-12-25 21:51:32 +00:00
2021-11-04 01:21:05 +00:00
func parseIssue ( apiClient * gitlab . Client , opts * CreateOpts ) ( * gitlab . Issue , error ) {
issue , _ , err := issueutils . IssueFromArg ( apiClient , opts . BaseRepo , opts . RelatedIssue )
if err != nil {
return nil , err
}
return issue , nil
}
2021-01-23 03:07:26 +00:00
func createRun ( opts * CreateOpts ) error {
out := opts . IO . StdOut
2021-01-29 00:56:45 +00:00
c := opts . IO . Color ( )
2021-01-23 03:07:26 +00:00
mrCreateOpts := & gitlab . CreateMergeRequestOptions { }
2020-12-25 21:51:32 +00:00
2021-01-23 03:07:26 +00:00
labClient , err := opts . Lab ( )
if err != nil {
return err
}
baseRepo , err := opts . BaseRepo ( )
if err != nil {
return err
}
headRepo , err := opts . HeadRepo ( )
if err != nil {
return err
}
opts . SourceProject , err = api . GetProject ( labClient , headRepo . FullName ( ) )
if err != nil {
return err
}
// if the user set the target_project, get details of the target
if opts . MRCreateTargetProject != "" {
opts . TargetProject , err = api . GetProject ( labClient , opts . MRCreateTargetProject )
if err != nil {
return err
}
} else {
// If both the baseRepo and headRepo are the same then re-use the SourceProject
if baseRepo . FullName ( ) == headRepo . FullName ( ) {
opts . TargetProject = opts . SourceProject
} else {
// Otherwise assume the user wants to create the merge request against the
// baseRepo
opts . TargetProject , err = api . GetProject ( labClient , baseRepo . FullName ( ) )
2020-12-26 15:18:50 +00:00
if err != nil {
return err
}
2021-01-23 03:07:26 +00:00
}
}
2020-12-26 15:18:50 +00:00
2021-01-23 03:07:26 +00:00
if ! opts . TargetProject . MergeRequestsEnabled {
fmt . Fprintf ( opts . IO . StdErr , "Merge requests are disabled for %q\n" , opts . TargetProject . PathWithNamespace )
return cmdutils . SilentError
}
2020-12-26 15:18:50 +00:00
2021-11-04 01:21:05 +00:00
headRepoRemote , err := repoRemote ( opts , headRepo , opts . SourceProject , "glab-head" )
2021-01-23 03:07:26 +00:00
if err != nil {
return nil
}
2020-12-26 15:18:50 +00:00
2021-01-23 03:07:26 +00:00
var baseRepoRemote * glrepo . Remote
2020-12-27 09:05:07 +00:00
2021-01-23 03:07:26 +00:00
// check if baseRepo is the same as the headRepo and set the remote
if glrepo . IsSame ( baseRepo , headRepo ) {
baseRepoRemote = headRepoRemote
} else {
2021-11-04 01:21:05 +00:00
baseRepoRemote , err = repoRemote ( opts , baseRepo , opts . TargetProject , "glab-base" )
2021-01-23 03:07:26 +00:00
if err != nil {
return nil
}
}
2020-12-27 09:05:07 +00:00
2021-01-23 03:07:26 +00:00
if opts . MilestoneFlag != "" {
opts . MileStone , err = cmdutils . ParseMilestone ( labClient , baseRepo , opts . MilestoneFlag )
if err != nil {
return err
}
}
2020-09-29 05:55:34 +00:00
2021-01-23 03:07:26 +00:00
if opts . CreateSourceBranch && opts . SourceBranch == "" {
opts . SourceBranch = utils . ReplaceNonAlphaNumericChars ( opts . Title , "-" )
2021-11-04 01:21:05 +00:00
} else if opts . SourceBranch == "" && opts . RelatedIssue == "" {
2021-01-23 03:07:26 +00:00
opts . SourceBranch , err = opts . Branch ( )
if err != nil {
return err
}
}
2020-12-30 16:52:20 +00:00
2021-01-23 03:07:26 +00:00
if opts . TargetBranch == "" {
2021-11-04 01:21:05 +00:00
opts . TargetBranch = getTargetBranch ( baseRepoRemote )
2021-01-23 03:07:26 +00:00
}
2020-10-04 14:01:03 +00:00
2021-11-04 01:21:05 +00:00
if opts . RelatedIssue != "" {
issue , err := parseIssue ( labClient , opts )
if err != nil {
2021-01-23 03:07:26 +00:00
return err
}
2021-11-04 01:21:05 +00:00
if opts . CopyIssueLabels {
2022-10-04 19:29:02 +00:00
mrCreateOpts . Labels = & issue . Labels
2021-11-04 01:21:05 +00:00
}
opts . Description = fmt . Sprintf ( "Closes #%d" , issue . IID )
opts . Title = fmt . Sprintf ( "Resolve \"%s\"" , issue . Title )
if ! opts . IsDraft && ! opts . IsWIP {
opts . IsDraft = true
2021-01-23 03:07:26 +00:00
}
2020-12-23 21:12:14 +00:00
2021-11-04 01:21:05 +00:00
if opts . SourceBranch == "" {
sourceBranch := fmt . Sprintf ( "%d-%s" , issue . IID , utils . ReplaceNonAlphaNumericChars ( strings . ToLower ( issue . Title ) , "-" ) )
branchOpts := & gitlab . CreateBranchOptions {
Branch : & sourceBranch ,
Ref : & opts . TargetBranch ,
}
_ , err = api . CreateBranch ( labClient , baseRepo . FullName ( ) , branchOpts )
if err != nil {
for branchErr , branchCount := err , 1 ; branchErr != nil ; branchCount ++ {
sourceBranch = fmt . Sprintf ( "%d-%s-%d" , issue . IID , strings . ReplaceAll ( strings . ToLower ( issue . Title ) , " " , "-" ) , branchCount )
_ , branchErr = api . CreateBranch ( labClient , baseRepo . FullName ( ) , branchOpts )
2020-11-17 18:03:44 +00:00
}
2021-11-04 01:21:05 +00:00
}
opts . SourceBranch = sourceBranch
}
} else {
opts . TargetTrackingBranch = fmt . Sprintf ( "%s/%s" , baseRepoRemote . Name , opts . TargetBranch )
if opts . SourceBranch == opts . TargetBranch && glrepo . IsSame ( baseRepo , headRepo ) {
fmt . Fprintf ( opts . IO . StdErr , "You must be on a different branch other than %q\n" , opts . TargetBranch )
return cmdutils . SilentError
}
2020-11-17 18:03:44 +00:00
2021-11-04 01:21:05 +00:00
if opts . Autofill {
if err = mrBodyAndTitle ( opts ) ; err != nil {
return err
}
_ , err = api . GetCommit ( labClient , baseRepo . FullName ( ) , opts . TargetBranch )
if err != nil {
return fmt . Errorf ( "target branch %s does not exist on remote. Specify target branch with --target-branch flag" ,
opts . TargetBranch )
}
2021-01-23 03:07:26 +00:00
2021-11-04 01:21:05 +00:00
opts . ShouldPush = true
} else if opts . IsInteractive {
var templateName string
var templateContents string
if opts . Description == "" {
if opts . NoEditor {
err = prompt . AskMultiline ( & opts . Description , "description" , "Description:" , "" )
if err != nil {
return err
}
} else {
templateResponse := struct {
Index int
} { }
templateNames , err := cmdutils . ListGitLabTemplates ( cmdutils . MergeRequestTemplate )
if err != nil {
return fmt . Errorf ( "error getting templates: %w" , err )
}
templateNames = append ( templateNames , "Open a blank merge request" )
selectQs := [ ] * survey . Question {
{
Name : "index" ,
Prompt : & survey . Select {
Message : "Choose a template" ,
Options : templateNames ,
} ,
2021-01-23 03:07:26 +00:00
} ,
2021-11-04 01:21:05 +00:00
}
2020-11-29 18:04:15 +00:00
2021-11-04 01:21:05 +00:00
if err := prompt . Ask ( selectQs , & templateResponse ) ; err != nil {
return fmt . Errorf ( "could not prompt: %w" , err )
}
if templateResponse . Index != len ( templateNames ) {
templateName = templateNames [ templateResponse . Index ]
templateContents , err = cmdutils . LoadGitLabTemplate ( cmdutils . MergeRequestTemplate , templateName )
if err != nil {
return fmt . Errorf ( "failed to get template contents: %w" , err )
}
2020-11-29 22:04:41 +00:00
}
}
2020-12-30 00:21:29 +00:00
}
2021-11-04 01:21:05 +00:00
if opts . Title == "" {
err = prompt . AskQuestionWithInput ( & opts . Title , "title" , "Title:" , "" , true )
2021-01-23 03:07:26 +00:00
if err != nil {
return err
2020-09-29 05:55:34 +00:00
}
2021-11-04 01:21:05 +00:00
}
if opts . Description == "" {
if opts . NoEditor {
err = prompt . AskMultiline ( & opts . Description , "description" , "Description:" , "" )
if err != nil {
return err
}
} else {
editor , err := cmdutils . GetEditor ( opts . Config )
if err != nil {
return err
}
err = cmdutils . EditorPrompt ( & opts . Description , "Description" , templateContents , editor )
if err != nil {
return err
}
2020-09-29 05:55:34 +00:00
}
}
2021-01-23 03:07:26 +00:00
}
2021-11-04 01:21:05 +00:00
}
if opts . Title == "" {
2021-01-23 03:07:26 +00:00
return fmt . Errorf ( "title can't be blank" )
}
2020-09-29 05:55:34 +00:00
2021-01-23 03:07:26 +00:00
if opts . IsDraft || opts . IsWIP {
if opts . IsDraft {
opts . Title = "Draft: " + opts . Title
} else {
opts . Title = "WIP: " + opts . Title
}
}
2021-11-04 01:21:05 +00:00
mrCreateOpts . Title = & opts . Title
mrCreateOpts . Description = & opts . Description
mrCreateOpts . SourceBranch = & opts . SourceBranch
mrCreateOpts . TargetBranch = & opts . TargetBranch
2021-10-03 18:20:16 +00:00
2021-01-23 03:07:26 +00:00
if opts . AllowCollaboration {
mrCreateOpts . AllowCollaboration = gitlab . Bool ( true )
}
2021-10-03 18:20:16 +00:00
2021-01-23 03:07:26 +00:00
if opts . RemoveSourceBranch {
mrCreateOpts . RemoveSourceBranch = gitlab . Bool ( true )
}
2021-10-03 18:20:16 +00:00
2021-10-04 06:58:11 +00:00
if opts . SquashBeforeMerge {
2021-10-03 18:20:16 +00:00
mrCreateOpts . Squash = gitlab . Bool ( true )
}
2021-01-23 03:07:26 +00:00
if opts . TargetProject != nil {
2021-11-04 01:21:05 +00:00
mrCreateOpts . TargetProjectID = & opts . TargetProject . ID
2021-01-23 03:07:26 +00:00
}
2020-12-26 15:18:50 +00:00
2021-01-23 03:07:26 +00:00
if opts . CreateSourceBranch {
lb := & gitlab . CreateBranchOptions {
2021-11-04 01:21:05 +00:00
Branch : & opts . SourceBranch ,
Ref : & opts . TargetBranch ,
2021-01-23 03:07:26 +00:00
}
fmt . Fprintln ( opts . IO . StdErr , "\nCreating related branch..." )
branch , err := api . CreateBranch ( labClient , headRepo . FullName ( ) , lb )
if err == nil {
fmt . Fprintln ( opts . IO . StdErr , "Branch created: " , branch . WebURL )
} else {
fmt . Fprintln ( opts . IO . StdErr , "Error creating branch: " , err . Error ( ) )
}
}
2020-11-29 22:04:41 +00:00
2021-01-23 03:07:26 +00:00
var action cmdutils . Action
2021-01-13 19:45:55 +00:00
2021-01-23 03:07:26 +00:00
// submit without prompting for non interactive mode
if ! opts . IsInteractive || opts . Yes {
action = cmdutils . SubmitAction
}
2020-11-29 23:01:41 +00:00
2021-01-23 03:07:26 +00:00
if opts . Web {
action = cmdutils . PreviewAction
}
2020-12-31 02:44:08 +00:00
2021-01-23 03:07:26 +00:00
if action == cmdutils . NoAction {
action , err = cmdutils . ConfirmSubmission ( true , true )
if err != nil {
return fmt . Errorf ( "unable to confirm: %w" , err )
}
}
2020-12-31 02:44:08 +00:00
2021-01-23 03:07:26 +00:00
if action == cmdutils . AddMetadataAction {
var metadataActions [ ] cmdutils . Action
2020-12-31 02:44:08 +00:00
2021-01-23 03:07:26 +00:00
metadataActions , err = cmdutils . PickMetadata ( )
if err != nil {
return fmt . Errorf ( "failed to pick metadata to add: %w" , err )
}
2020-12-31 02:44:08 +00:00
2021-01-23 03:07:26 +00:00
for _ , x := range metadataActions {
if x == cmdutils . AddLabelAction {
err = cmdutils . LabelsPrompt ( & opts . Labels , labClient , baseRepoRemote )
2020-12-31 02:44:08 +00:00
if err != nil {
return err
}
}
2021-01-23 03:07:26 +00:00
if x == cmdutils . AddAssigneeAction {
// Use minimum permission level 30 (Maintainer) as it is the minimum level
// to accept a merge request
err = cmdutils . AssigneesPrompt ( & opts . Assignees , labClient , baseRepoRemote , opts . IO , 30 )
2020-12-31 02:44:08 +00:00
if err != nil {
return err
}
}
2021-01-23 03:07:26 +00:00
if x == cmdutils . AddMilestoneAction {
err = cmdutils . MilestonesPrompt ( & opts . MileStone , labClient , baseRepoRemote , opts . IO )
if err != nil {
return err
}
2020-12-31 02:44:08 +00:00
}
2021-01-23 03:07:26 +00:00
}
2020-12-31 02:44:08 +00:00
2021-01-23 03:07:26 +00:00
// Ask the user again but don't permit AddMetadata a second time
action , err = cmdutils . ConfirmSubmission ( true , false )
if err != nil {
return err
}
}
2020-11-29 23:01:41 +00:00
2022-10-04 19:29:02 +00:00
// This check protects against possibly dereferencing a nil pointer.
if mrCreateOpts . Labels == nil {
mrCreateOpts . Labels = & gitlab . Labels { }
}
2021-01-23 03:07:26 +00:00
// These actions need to be done here, after the `Add metadata` prompt because
// they are metadata that can be modified by the prompt
2022-10-04 19:29:02 +00:00
* mrCreateOpts . Labels = append ( * mrCreateOpts . Labels , opts . Labels ... )
2020-09-29 05:55:34 +00:00
2021-01-23 03:07:26 +00:00
if len ( opts . Assignees ) > 0 {
users , err := api . UsersByNames ( labClient , opts . Assignees )
if err != nil {
return err
}
mrCreateOpts . AssigneeIDs = cmdutils . IDsFromUsers ( users )
}
2020-12-26 15:18:50 +00:00
2021-10-03 17:21:24 +00:00
if len ( opts . Reviewers ) > 0 {
users , err := api . UsersByNames ( labClient , opts . Reviewers )
if err != nil {
return err
}
mrCreateOpts . ReviewerIDs = cmdutils . IDsFromUsers ( users )
}
2021-01-23 03:07:26 +00:00
if opts . MileStone != 0 {
mrCreateOpts . MilestoneID = gitlab . Int ( opts . MileStone )
}
2020-12-26 15:18:50 +00:00
2021-01-23 03:07:26 +00:00
if action == cmdutils . CancelAction {
fmt . Fprintln ( opts . IO . StdErr , "Discarded." )
return nil
}
2020-12-26 15:18:50 +00:00
2021-01-23 03:07:26 +00:00
if err := handlePush ( opts , headRepoRemote ) ; err != nil {
return err
}
2020-12-26 15:18:50 +00:00
2021-01-23 03:07:26 +00:00
if action == cmdutils . PreviewAction {
return previewMR ( opts )
}
2020-12-26 15:18:50 +00:00
2021-01-23 03:07:26 +00:00
if action == cmdutils . SubmitAction {
message := "\nCreating merge request for %s into %s in %s\n\n"
if opts . IsDraft || opts . IsWIP {
message = "\nCreating draft merge request for %s into %s in %s\n\n"
}
2020-12-26 15:18:50 +00:00
2021-01-29 00:56:45 +00:00
fmt . Fprintf ( opts . IO . StdErr , message , c . Cyan ( opts . SourceBranch ) , c . Cyan ( opts . TargetBranch ) , baseRepo . FullName ( ) )
2020-09-29 05:55:34 +00:00
2021-01-23 03:07:26 +00:00
// It is intentional that we create against the head repo, it is necessary
// for cross-repository merge requests
mr , err := api . CreateMR ( labClient , headRepo . FullName ( ) , mrCreateOpts )
if err != nil {
return err
}
2020-12-26 20:23:45 +00:00
2021-11-18 14:47:03 +00:00
fmt . Fprintln ( out , mrutils . DisplayMR ( c , mr , opts . IO . IsaTTY ) )
2021-01-23 03:07:26 +00:00
return nil
}
return errors . New ( "expected to cancel, preview in browser, or submit" )
2020-09-29 05:55:34 +00:00
}
2020-11-30 20:49:08 +00:00
func mrBodyAndTitle ( opts * CreateOpts ) error {
// TODO: detect forks
commits , err := git . Commits ( opts . TargetTrackingBranch , opts . SourceBranch )
if err != nil {
return err
}
if len ( commits ) == 1 {
2021-01-17 21:24:24 +00:00
if opts . Title == "" {
opts . Title = commits [ 0 ] . Title
}
if opts . Description == "" {
body , err := git . CommitBody ( commits [ 0 ] . Sha )
if err != nil {
return err
}
opts . Description = body
2020-11-30 20:49:08 +00:00
}
} else {
2021-01-17 21:24:24 +00:00
if opts . Title == "" {
opts . Title = utils . Humanize ( opts . SourceBranch )
}
2020-11-30 20:49:08 +00:00
2021-01-17 21:24:24 +00:00
if opts . Description == "" {
var body strings . Builder
for i := len ( commits ) - 1 ; i >= 0 ; i -- {
2021-08-11 23:45:26 +00:00
// adds 2 spaces for markdown line wrapping
fmt . Fprintf ( & body , "- %s \n" , commits [ i ] . Title )
if opts . FillCommitBody {
commitBody , err := git . CommitBody ( commits [ i ] . Sha )
if err != nil {
return err
}
re := regexp . MustCompile ( ` \r?\n\n ` )
commitBody = re . ReplaceAllString ( commitBody , " \n" )
fmt . Fprintf ( & body , "%s\n" , commitBody )
}
2021-01-17 21:24:24 +00:00
}
opts . Description = body . String ( )
2020-11-30 20:49:08 +00:00
}
}
return nil
}
2020-12-25 02:13:12 +00:00
func handlePush ( opts * CreateOpts , remote * glrepo . Remote ) error {
2020-12-26 15:18:50 +00:00
if opts . ShouldPush {
var sourceRemote = remote
2020-12-25 02:13:12 +00:00
2020-12-26 15:18:50 +00:00
sourceBranch := opts . SourceBranch
2020-12-25 02:13:12 +00:00
2020-12-26 15:18:50 +00:00
if sourceBranch != "" {
if idx := strings . IndexRune ( sourceBranch , ':' ) ; idx >= 0 {
sourceBranch = sourceBranch [ idx + 1 : ]
}
2020-12-25 02:13:12 +00:00
}
2020-12-26 15:18:50 +00:00
if c , err := git . UncommittedChangeCount ( ) ; c != 0 {
if err != nil {
return err
}
fmt . Fprintf ( opts . IO . StdErr , "\nwarning: you have %s\n" , utils . Pluralize ( c , "uncommitted change" ) )
2020-12-25 02:13:12 +00:00
}
2020-12-25 21:44:23 +00:00
err := git . Push ( sourceRemote . Name , fmt . Sprintf ( "HEAD:%s" , sourceBranch ) , opts . IO . StdOut , opts . IO . StdErr )
if err == nil {
2020-12-27 08:16:47 +00:00
branchConfig := git . ReadBranchConfig ( sourceBranch )
if branchConfig . RemoteName == "" && ( branchConfig . MergeRef == "" || branchConfig . RemoteURL == nil ) {
2020-12-25 21:44:23 +00:00
// No remote is set so set it
_ = git . SetUpstream ( sourceRemote . Name , sourceBranch , opts . IO . StdOut , opts . IO . StdErr )
2020-12-26 15:18:50 +00:00
}
2020-12-25 02:13:12 +00:00
}
2020-12-25 21:44:23 +00:00
return err
2020-12-25 02:13:12 +00:00
}
2020-12-26 15:18:50 +00:00
return nil
2020-12-25 02:13:12 +00:00
}
2020-12-26 15:18:50 +00:00
func previewMR ( opts * CreateOpts ) error {
repo , err := opts . BaseRepo ( )
if err != nil {
return err
}
cfg , err := opts . Config ( )
if err != nil {
return err
}
2020-12-27 01:40:35 +00:00
openURL , err := generateMRCompareURL ( opts )
2020-12-26 15:18:50 +00:00
if err != nil {
return err
}
if opts . IO . IsOutputTTY ( ) {
fmt . Fprintf ( opts . IO . StdErr , "Opening %s in your browser.\n" , utils . DisplayURL ( openURL ) )
}
browser , _ := cfg . Get ( repo . RepoHost ( ) , "browser" )
return utils . OpenInBrowser ( openURL , browser )
}
2020-12-27 01:40:35 +00:00
func generateMRCompareURL ( opts * CreateOpts ) ( string , error ) {
2020-12-26 15:18:50 +00:00
description := opts . Description
if len ( opts . Labels ) > 0 {
// this uses the slash commands to add labels to the description
// See https://docs.gitlab.com/ee/user/project/quick_actions.html
// See also https://gitlab.com/gitlab-org/gitlab-foss/-/issues/19731#note_32550046
2020-12-28 09:03:03 +00:00
description += "\n/label "
for _ , label := range opts . Labels {
description += fmt . Sprintf ( "~%q" , label )
}
2020-12-26 15:18:50 +00:00
}
if len ( opts . Assignees ) > 0 {
// this uses the slash commands to add assignees to the description
description += fmt . Sprintf ( "\n/assign %s" , strings . Join ( opts . Assignees , ", " ) )
}
2022-03-10 07:43:37 +00:00
if len ( opts . Reviewers ) > 0 {
// this uses the slash commands to add reviewers to the description
description += fmt . Sprintf ( "\n/reviewer %s" , strings . Join ( opts . Reviewers , ", " ) )
}
2020-12-26 15:18:50 +00:00
if opts . MileStone != 0 {
// this uses the slash commands to add milestone to the description
description += fmt . Sprintf ( "\n/milestone %%%d" , opts . MileStone )
}
2020-12-27 01:40:35 +00:00
// The merge request **must** be opened against the head repo
u , err := url . Parse ( opts . SourceProject . WebURL )
2020-12-26 16:13:08 +00:00
if err != nil {
return "" , err
}
u . Path += "/-/merge_requests/new"
2020-12-26 15:18:50 +00:00
u . RawQuery = fmt . Sprintf (
2021-11-18 14:39:45 +00:00
"merge_request[title]=%s&merge_request[description]=%s&merge_request[source_branch]=%s&merge_request[target_branch]=%s&merge_request[source_project_id]=%d&merge_request[target_project_id]=%d" ,
strings . ReplaceAll ( url . PathEscape ( opts . Title ) , "+" , "%2B" ) ,
strings . ReplaceAll ( url . PathEscape ( description ) , "+" , "%2B" ) ,
2020-12-26 15:18:50 +00:00
opts . SourceBranch ,
opts . TargetBranch ,
2020-12-27 01:40:35 +00:00
opts . SourceProject . ID ,
opts . TargetProject . ID )
2020-12-26 15:18:50 +00:00
return u . String ( ) , nil
}
2020-12-25 21:45:28 +00:00
func resolvedHeadRepo ( f * cmdutils . Factory ) func ( ) ( glrepo . Interface , error ) {
return func ( ) ( glrepo . Interface , error ) {
httpClient , err := f . HttpClient ( )
if err != nil {
return nil , err
}
remotes , err := f . Remotes ( )
if err != nil {
return nil , err
}
repoContext , err := glrepo . ResolveRemotesToRepos ( remotes , httpClient , "" )
if err != nil {
return nil , err
}
2022-10-18 01:16:30 +00:00
headRepo , err := repoContext . HeadRepo ( f . IO . PromptEnabled ( ) )
2020-12-25 21:45:28 +00:00
if err != nil {
return nil , err
}
return headRepo , nil
}
}
func headRepoOverride ( opts * CreateOpts , repo string ) error {
opts . HeadRepo = func ( ) ( glrepo . Interface , error ) {
return glrepo . FromFullName ( repo )
}
return nil
}
2020-12-27 09:05:07 +00:00
2021-11-04 01:21:05 +00:00
func repoRemote ( opts * CreateOpts , repo glrepo . Interface , project * gitlab . Project , remoteName string ) ( * glrepo . Remote , error ) {
2020-12-27 09:05:07 +00:00
remotes , err := opts . Remotes ( )
if err != nil {
return nil , err
}
repoRemote , _ := remotes . FindByRepo ( repo . RepoOwner ( ) , repo . RepoName ( ) )
if repoRemote == nil {
cfg , err := opts . Config ( )
if err != nil {
return nil , err
}
gitProtocol , _ := cfg . Get ( repo . RepoHost ( ) , "git_protocol" )
2021-05-12 04:55:49 +00:00
repoURL := glrepo . RemoteURL ( project , gitProtocol )
2020-12-27 09:05:07 +00:00
gitRemote , err := git . AddRemote ( remoteName , repoURL )
if err != nil {
return nil , fmt . Errorf ( "error adding remote: %w" , err )
}
repoRemote = & glrepo . Remote {
Remote : gitRemote ,
Repo : repo ,
}
}
return repoRemote , nil
}
2021-01-21 11:07:10 +00:00
2021-11-04 01:21:05 +00:00
func getTargetBranch ( baseRepoRemote * glrepo . Remote ) string {
2021-01-21 11:07:10 +00:00
br , _ := git . GetDefaultBranch ( baseRepoRemote . PushURL . String ( ) )
// we ignore the error since git.GetDefaultBranch returns master and an error
// if the default branch cannot be determined
return br
}