commands/mr/create/mr_create.go (715 lines of code) (raw):

package create import ( "errors" "fmt" "net/url" "os" "regexp" "strconv" "strings" "github.com/AlecAivazis/survey/v2" "gitlab.com/gitlab-org/cli/commands/issue/issueutils" "gitlab.com/gitlab-org/cli/pkg/prompt" "gitlab.com/gitlab-org/cli/pkg/iostreams" "github.com/MakeNowJust/heredoc/v2" "gitlab.com/gitlab-org/cli/internal/config" "gitlab.com/gitlab-org/cli/internal/glrepo" "gitlab.com/gitlab-org/cli/internal/recovery" "github.com/spf13/cobra" gitlab "gitlab.com/gitlab-org/api/client-go" "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" ) type CreateOpts struct { Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` SourceBranch string `json:"source_branch,omitempty"` TargetBranch string `json:"target_branch,omitempty"` TargetTrackingBranch string `json:"target_tracking_branch,omitempty"` Labels []string `json:"labels,omitempty"` Assignees []string `json:"assignees,omitempty"` Reviewers []string `json:"reviewers,omitempty"` Milestone int `json:"milestone,omitempty"` MilestoneFlag string `json:"milestone_flag,omitempty"` MRCreateTargetProject string `json:"mr_create_target_project,omitempty"` RelatedIssue string `json:"related_issue,omitempty"` CopyIssueLabels bool `json:"copy_issue_labels,omitempty"` CreateSourceBranch bool `json:"create_source_branch,omitempty"` RemoveSourceBranch bool `json:"remove_source_branch,omitempty"` AllowCollaboration bool `json:"allow_collaboration,omitempty"` SquashBeforeMerge bool `json:"squash_before_merge,omitempty"` Autofill bool `json:"autofill,omitempty"` FillCommitBody bool `json:"fill_commit_body,omitempty"` IsDraft bool `json:"is_draft,omitempty"` IsWIP bool `json:"is_wip,omitempty"` ShouldPush bool `json:"should_push,omitempty"` NoEditor bool `json:"-"` IsInteractive bool `json:"-"` Yes bool `json:"-"` Web bool `json:"-"` Recover bool `json:"-"` Signoff bool `json:"-"` IO *iostreams.IOStreams `json:"-"` Branch func() (string, error) `json:"-"` Remotes func() (glrepo.Remotes, error) `json:"-"` Lab func() (*gitlab.Client, error) `json:"-"` Config func() (config.Config, error) `json:"-"` BaseRepo func() (glrepo.Interface, error) `json:"-"` HeadRepo func() (glrepo.Interface, error) `json:"-"` // 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 `json:"source_project,omitempty"` // 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 TargetProject *gitlab.Project `json:"target_project,omitempty"` } func NewCmdCreate(f *cmdutils.Factory) *cobra.Command { opts := &CreateOpts{ IO: f.IO, Branch: f.Branch, Remotes: f.Remotes, Config: f.Config, HeadRepo: ResolvedHeadRepo(f), } mrCreateCmd := &cobra.Command{ Use: "create", Short: `Create a new merge request.`, Long: ``, Aliases: []string{"new"}, Example: heredoc.Doc(` $ glab mr new $ glab mr create -a username -t "fix annoying bug" $ glab mr create -f --draft --label RFC $ glab mr create --fill --web $ glab mr create --fill --fill-commit-body --yes `), Args: cobra.ExactArgs(0), 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) } }, RunE: func(cmd *cobra.Command, args []string) error { // 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 hasTitle := cmd.Flags().Changed("title") hasDescription := cmd.Flags().Changed("description") // disable interactive mode if title and description are explicitly defined opts.IsInteractive = !(hasTitle && hasDescription) if hasTitle && hasDescription && opts.Autofill { return &cmdutils.FlagError{ Err: errors.New("usage of --title and --description overrides --fill."), } } if opts.IsInteractive && !opts.IO.PromptEnabled() && !opts.Autofill { return &cmdutils.FlagError{Err: errors.New("--title or --fill required for non-interactive mode.")} } if cmd.Flags().Changed("wip") && cmd.Flags().Changed("draft") { return &cmdutils.FlagError{Err: errors.New("specify --draft.")} } if !opts.Autofill && opts.FillCommitBody { return &cmdutils.FlagError{Err: errors.New("--fill-commit-body should be used with --fill.")} } // 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.")} } if opts.CopyIssueLabels && opts.RelatedIssue == "" { return &cmdutils.FlagError{Err: errors.New("--copy-issue-labels can only be used with --related-issue.")} } if err := createRun(opts); err != nil { // always save options to file recoverErr := createRecoverSaveFile(opts) if recoverErr != nil { fmt.Fprintf(opts.IO.StdErr, "Could not create recovery file: %v", recoverErr) } return err } return nil }, } mrCreateCmd.Flags().BoolVarP(&opts.Autofill, "fill", "f", false, "Do not prompt for title or description, and just use commit info. Sets `push` to `true`, and pushes the branch.") mrCreateCmd.Flags().BoolVarP(&opts.FillCommitBody, "fill-commit-body", "", false, "Fill description with each commit body when multiple commits. Can only be used with --fill.") 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 draft. 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 the merge request.") mrCreateCmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Supply a description for the 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`.") mrCreateCmd.Flags().StringSliceVarP(&opts.Reviewers, "reviewer", "", []string{}, "Request review from users by their `usernames`.") mrCreateCmd.Flags().StringVarP(&opts.SourceBranch, "source-branch", "s", "", "Create a merge request from this branch. 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 into.") mrCreateCmd.Flags().BoolVarP(&opts.CreateSourceBranch, "create-source-branch", "", false, "Create a 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.") mrCreateCmd.Flags().BoolVarP(&opts.SquashBeforeMerge, "squash-before-merge", "", false, "Squash commits into a single commit when merging.") mrCreateCmd.Flags().BoolVarP(&opts.NoEditor, "no-editor", "", false, "Don't open editor to enter a description. If true, uses prompt. Defaults to false.") mrCreateCmd.Flags().StringP("head", "H", "", "Select another head repository using the `OWNER/REPO` or `GROUP/NAMESPACE/REPO` format, the project ID, or the full URL.") mrCreateCmd.Flags().BoolVarP(&opts.Yes, "yes", "y", false, "Skip submission confirmation prompt. Use --fill to skip all optional prompts.") mrCreateCmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Continue merge request creation in a browser.") 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 a merge request for an issue. If --title is not provided, uses the issue title.") mrCreateCmd.Flags().BoolVar(&opts.Recover, "recover", false, "Save the options to a file if the merge request creation fails. If the file exists, the options are loaded from the recovery file. (EXPERIMENTAL.)") mrCreateCmd.Flags().BoolVar(&opts.Signoff, "signoff", false, "Append a DCO signoff to the merge request description.") mrCreateCmd.Flags().StringVarP(&opts.MRCreateTargetProject, "target-project", "", "", "Add target project by id, OWNER/REPO, or GROUP/NAMESPACE/REPO.") _ = mrCreateCmd.Flags().MarkHidden("target-project") _ = mrCreateCmd.Flags().MarkDeprecated("target-project", "Use --repo instead.") return mrCreateCmd } 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 } 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.") } func mrBody(commits []*git.Commit, fillCommitBody bool) (string, error) { var body strings.Builder re := regexp.MustCompile(`\r?\n\n`) for i := len(commits) - 1; i >= 0; i-- { // adds 2 spaces for markdown line wrapping fmt.Fprintf(&body, "- %s \n", commits[i].Title) if fillCommitBody { commitBody, err := git.CommitBody(commits[i].Sha) if err != nil { return "", fmt.Errorf("failed to get commit message for %s: %w", commits[i].Sha, err) } commitBody = re.ReplaceAllString(commitBody, " \n") fmt.Fprintf(&body, "%s\n", commitBody) } } return body.String(), nil } 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 { 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 } } else { if opts.Title == "" { opts.Title = utils.Humanize(opts.SourceBranch) } if opts.Description == "" { description, err := mrBody(commits, opts.FillCommitBody) if err != nil { return err } opts.Description = description } } return nil } func handlePush(opts *CreateOpts, remote *glrepo.Remote) error { if opts.ShouldPush { sourceRemote := remote sourceBranch := opts.SourceBranch if sourceBranch != "" { if idx := strings.IndexRune(sourceBranch, ':'); idx >= 0 { sourceBranch = sourceBranch[idx+1:] } } 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")) } err := git.Push(sourceRemote.Name, fmt.Sprintf("HEAD:%s", sourceBranch), opts.IO.StdOut, opts.IO.StdErr) if err == nil { branchConfig := git.ReadBranchConfig(sourceBranch) if branchConfig.RemoteName == "" && (branchConfig.MergeRef == "" || branchConfig.RemoteURL == nil) { // No remote is set so set it _ = git.SetUpstream(sourceRemote.Name, sourceBranch, opts.IO.StdOut, opts.IO.StdErr) } } return err } return nil } func previewMR(opts *CreateOpts) error { repo, err := opts.BaseRepo() if err != nil { return err } cfg, err := opts.Config() if err != nil { return err } openURL, err := generateMRCompareURL(opts) 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) } func generateMRCompareURL(opts *CreateOpts) (string, error) { description := opts.Description if len(opts.Labels) > 0 { // this uses the slash commands to add labels to the description // See https://docs.gitlab.com/user/project/quick_actions/ // See also https://gitlab.com/gitlab-org/gitlab-foss/-/issues/19731#note_32550046 description += fmt.Sprintf("\n/label ~%s", strings.Join(opts.Labels, ", ~")) } 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, ", ")) } 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, ", ")) } if opts.Milestone != 0 { // this uses the slash commands to add milestone to the description description += fmt.Sprintf("\n/milestone %%%d", opts.Milestone) } // The merge request **must** be opened against the head repo u, err := url.Parse(opts.SourceProject.WebURL) if err != nil { return "", err } u.Path += "/-/merge_requests/new" q := u.Query() q.Set("merge_request[title]", opts.Title) q.Add("merge_request[description]", description) q.Add("merge_request[source_branch]", opts.SourceBranch) q.Add("merge_request[target_branch]", opts.TargetBranch) q.Add("merge_request[source_project_id]", strconv.Itoa(opts.SourceProject.ID)) q.Add("merge_request[target_project_id]", strconv.Itoa(opts.TargetProject.ID)) u.RawQuery = q.Encode() return u.String(), nil } 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 } headRepo, err := repoContext.HeadRepo(f.IO.PromptEnabled()) 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 } func repoRemote(opts *CreateOpts, repo glrepo.Interface, project *gitlab.Project, remoteName string) (*glrepo.Remote, error) { 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") repoURL := glrepo.RemoteURL(project, gitProtocol) 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 } func getTargetBranch(baseRepoRemote *glrepo.Remote) string { br, _ := git.GetDefaultBranch(baseRepoRemote.Name) // we ignore the error since git.GetDefaultBranch returns master and an error // if the default branch cannot be determined return br } // createRecoverSaveFile will try save the issue create options to a file func createRecoverSaveFile(opts *CreateOpts) error { glRepo, err := opts.BaseRepo() if err != nil { return err } recoverFile, err := recovery.CreateFile(glRepo.FullName(), "mr.json", opts) if err != nil { return err } fmt.Fprintf(opts.IO.StdErr, "Failed to create merge request. Created recovery file: %s\nRun the command again with the '--recover' option to retry.\n", recoverFile) return nil }