in commands/mr/create/mr_create.go [207:644]
func createRun(opts *CreateOpts) error {
out := opts.IO.StdOut
c := opts.IO.Color()
mrCreateOpts := &gitlab.CreateMergeRequestOptions{}
glRepo, err := opts.BaseRepo()
if err != nil {
return err
}
if opts.Recover {
if err := recovery.FromFile(glRepo.FullName(), "mr.json", opts); err != nil {
// if the file to recover doesn't exist, we can just ignore the error and move on
if !errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(opts.IO.StdErr, "Failed to recover from file: %v", err)
}
} else {
fmt.Fprintln(opts.IO.StdOut, "Recovered create options from file")
}
}
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
}
// only fetch source project if it wasn't saved for recovery
if opts.SourceProject == nil {
opts.SourceProject, err = api.GetProject(labClient, headRepo.FullName())
if err != nil {
return err
}
}
// only fetch target project if it wasn't saved for recovery
if opts.TargetProject == nil {
// 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())
if err != nil {
return err
}
}
}
}
if !opts.TargetProject.MergeRequestsEnabled { //nolint:staticcheck
fmt.Fprintf(opts.IO.StdErr, "Failed to create a merge request for project %q. Please ensure:\n", opts.TargetProject.PathWithNamespace)
fmt.Fprintf(opts.IO.StdErr, " - You are authenticated with the GitLab CLI.\n")
fmt.Fprintf(opts.IO.StdErr, " - Merge requests are enabled for this project.\n")
fmt.Fprintf(opts.IO.StdErr, " - Your role in this project allows you to create merge requests.\n")
return cmdutils.SilentError
}
headRepoRemote, err := repoRemote(opts, headRepo, opts.SourceProject, "glab-head")
if err != nil {
return nil
}
var baseRepoRemote *glrepo.Remote
// check if baseRepo is the same as the headRepo and set the remote
if glrepo.IsSame(baseRepo, headRepo) {
baseRepoRemote = headRepoRemote
} else {
baseRepoRemote, err = repoRemote(opts, baseRepo, opts.TargetProject, "glab-base")
if err != nil {
return nil
}
}
if opts.MilestoneFlag != "" {
opts.Milestone, err = cmdutils.ParseMilestone(labClient, baseRepo, opts.MilestoneFlag)
if err != nil {
return err
}
}
if opts.CreateSourceBranch && opts.SourceBranch == "" {
opts.SourceBranch = utils.ReplaceNonAlphaNumericChars(opts.Title, "-")
} else if opts.SourceBranch == "" && opts.RelatedIssue == "" {
opts.SourceBranch, err = opts.Branch()
if err != nil {
return err
}
}
if opts.TargetBranch == "" {
opts.TargetBranch = getTargetBranch(baseRepoRemote)
}
if opts.RelatedIssue != "" {
issue, err := parseIssue(labClient, opts)
if err != nil {
return err
}
if opts.CopyIssueLabels {
mrCreateOpts.Labels = (*gitlab.LabelOptions)(&issue.Labels)
}
opts.Description += fmt.Sprintf("\n\nCloses #%d", issue.IID)
if opts.Title == "" {
opts.Title = fmt.Sprintf("Resolve \"%s\"", issue.Title)
}
// MRs created with a related issue will always be created as a draft, same as the UI
if !opts.IsDraft && !opts.IsWIP {
opts.IsDraft = true
}
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)
}
}
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
}
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 the --target-branch flag",
opts.TargetBranch)
}
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)
}
const mrWithCommitsTemplate = "Open a merge request with commit messages."
const mrEmptyTemplate = "Open a blank merge request."
templateNames = append(templateNames, mrWithCommitsTemplate)
templateNames = append(templateNames, mrEmptyTemplate)
selectQs := []*survey.Question{
{
Name: "index",
Prompt: &survey.Select{
Message: "Choose a template:",
Options: templateNames,
},
},
}
if err := prompt.Ask(selectQs, &templateResponse); err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
templateName = templateNames[templateResponse.Index]
if templateName == mrWithCommitsTemplate {
// templateContents should be filled from commit messages
commits, err := git.Commits(opts.TargetTrackingBranch, opts.SourceBranch)
if err != nil {
return fmt.Errorf("failed to get commits: %w", err)
}
templateContents, err = mrBody(commits, true)
if err != nil {
return err
}
if opts.Signoff {
u, _ := api.CurrentUser(labClient)
templateContents += "Signed-off-by: " + u.Name + "<" + u.Email + ">"
}
} else if templateName == mrEmptyTemplate {
// blank merge request was choosen, leave templateContents empty
if opts.Signoff {
u, _ := api.CurrentUser(labClient)
templateContents += "Signed-off-by: " + u.Name + "<" + u.Email + ">"
}
} else {
templateContents, err = cmdutils.LoadGitLabTemplate(cmdutils.MergeRequestTemplate, templateName)
if err != nil {
return fmt.Errorf("failed to get template contents: %w", err)
}
}
}
}
if opts.Title == "" {
err = prompt.AskQuestionWithInput(&opts.Title, "title", "Title:", "", true)
if err != nil {
return err
}
}
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
}
}
}
}
}
if opts.Title == "" {
return fmt.Errorf("title can't be blank.")
}
if opts.IsDraft || opts.IsWIP {
if opts.IsDraft {
opts.Title = "Draft: " + opts.Title
} else {
opts.Title = "WIP: " + opts.Title
}
}
mrCreateOpts.Title = &opts.Title
mrCreateOpts.Description = &opts.Description
mrCreateOpts.SourceBranch = &opts.SourceBranch
mrCreateOpts.TargetBranch = &opts.TargetBranch
if opts.AllowCollaboration {
mrCreateOpts.AllowCollaboration = gitlab.Ptr(true)
}
if opts.RemoveSourceBranch {
mrCreateOpts.RemoveSourceBranch = gitlab.Ptr(true)
}
if opts.SquashBeforeMerge {
mrCreateOpts.Squash = gitlab.Ptr(true)
}
if opts.TargetProject != nil {
mrCreateOpts.TargetProjectID = &opts.TargetProject.ID
}
if opts.CreateSourceBranch {
lb := &gitlab.CreateBranchOptions{
Branch: &opts.SourceBranch,
Ref: &opts.TargetBranch,
}
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())
}
}
var action cmdutils.Action
// submit without prompting for non interactive mode
if !opts.IsInteractive || opts.Yes {
action = cmdutils.SubmitAction
}
if opts.Web {
action = cmdutils.PreviewAction
}
if action == cmdutils.NoAction {
action, err = cmdutils.ConfirmSubmission(true, true)
if err != nil {
return fmt.Errorf("unable to confirm: %w", err)
}
}
if action == cmdutils.AddMetadataAction {
metadataOptions := []string{
"labels",
"assignees",
"milestones",
"reviewers",
}
var metadataActions []string
err := prompt.MultiSelect(&metadataActions, "metadata", "Which metadata types to add?", metadataOptions)
if err != nil {
return fmt.Errorf("failed to pick the metadata to add: %w", err)
}
for _, x := range metadataActions {
if x == "labels" {
err = cmdutils.LabelsPrompt(&opts.Labels, labClient, baseRepoRemote)
if err != nil {
return err
}
}
if x == "assignees" {
// Use minimum permission level 30 (Maintainer) as it is the minimum level
// to accept a merge request
err = cmdutils.UsersPrompt(&opts.Assignees, labClient, baseRepoRemote, opts.IO, 30, x)
if err != nil {
return err
}
}
if x == "milestones" {
err = cmdutils.MilestonesPrompt(&opts.Milestone, labClient, baseRepoRemote, opts.IO)
if err != nil {
return err
}
}
if x == "reviewers" {
// Use minimum permission level 30 (Maintainer) as it is the minimum level
// to accept a merge request
err = cmdutils.UsersPrompt(&opts.Reviewers, labClient, baseRepoRemote, opts.IO, 30, x)
if err != nil {
return err
}
}
}
// Ask the user again but don't permit AddMetadata a second time
action, err = cmdutils.ConfirmSubmission(true, false)
if err != nil {
return err
}
}
// This check protects against possibly dereferencing a nil pointer.
if mrCreateOpts.Labels == nil {
mrCreateOpts.Labels = &gitlab.LabelOptions{}
}
// These actions need to be done here, after the `Add metadata` prompt because
// they are metadata that can be modified by the prompt
*mrCreateOpts.Labels = append(*mrCreateOpts.Labels, opts.Labels...)
if len(opts.Assignees) > 0 {
users, err := api.UsersByNames(labClient, opts.Assignees)
if err != nil {
return err
}
mrCreateOpts.AssigneeIDs = cmdutils.IDsFromUsers(users)
}
if len(opts.Reviewers) > 0 {
users, err := api.UsersByNames(labClient, opts.Reviewers)
if err != nil {
return err
}
mrCreateOpts.ReviewerIDs = cmdutils.IDsFromUsers(users)
}
if opts.Milestone != 0 {
mrCreateOpts.MilestoneID = gitlab.Ptr(opts.Milestone)
}
if action == cmdutils.CancelAction {
fmt.Fprintln(opts.IO.StdErr, "Discarded.")
return nil
}
if err := handlePush(opts, headRepoRemote); err != nil {
return err
}
if action == cmdutils.PreviewAction {
return previewMR(opts)
}
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"
}
fmt.Fprintf(opts.IO.StdErr, message, c.Cyan(opts.SourceBranch), c.Cyan(opts.TargetBranch), baseRepo.FullName())
// 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
}
fmt.Fprintln(out, mrutils.DisplayMR(c, &mr.BasicMergeRequest, opts.IO.IsaTTY))
return nil
}
return errors.New("expected to cancel, preview in browser, or submit.")
}