mirror of https://gitlab.com/gitlab-org/cli.git
768 lines
24 KiB
Go
768 lines
24 KiB
Go
package create
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"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"
|
|
"gitlab.com/gitlab-org/cli/internal/config"
|
|
"gitlab.com/gitlab-org/cli/internal/glrepo"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/xanzy/go-gitlab"
|
|
"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
|
|
Description string
|
|
SourceBranch string
|
|
TargetBranch string
|
|
TargetTrackingBranch string
|
|
Labels []string
|
|
Assignees []string
|
|
Reviewers []string
|
|
MileStone int
|
|
MilestoneFlag string
|
|
MRCreateTargetProject string
|
|
|
|
RelatedIssue string
|
|
CopyIssueLabels bool
|
|
|
|
CreateSourceBranch bool
|
|
RemoveSourceBranch bool
|
|
AllowCollaboration bool
|
|
SquashBeforeMerge bool
|
|
|
|
Autofill bool
|
|
FillCommitBody bool
|
|
IsDraft bool
|
|
IsWIP bool
|
|
ShouldPush bool
|
|
NoEditor bool
|
|
IsInteractive bool
|
|
Yes bool
|
|
Web bool
|
|
|
|
IO *iostreams.IOStreams
|
|
Branch func() (string, error)
|
|
Remotes func() (glrepo.Remotes, error)
|
|
Lab func() (*gitlab.Client, error)
|
|
Config func() (config.Config, error)
|
|
BaseRepo func() (glrepo.Interface, error)
|
|
HeadRepo func() (glrepo.Interface, error)
|
|
|
|
// 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
|
|
TargetProject *gitlab.Project
|
|
}
|
|
|
|
func NewCmdCreate(f *cmdutils.Factory, runE func(opts *CreateOpts) error) *cobra.Command {
|
|
opts := &CreateOpts{
|
|
IO: f.IO,
|
|
Branch: f.Branch,
|
|
Remotes: f.Remotes,
|
|
Config: f.Config,
|
|
HeadRepo: resolvedHeadRepo(f),
|
|
}
|
|
|
|
var mrCreateCmd = &cobra.Command{
|
|
Use: "create",
|
|
Short: `Create 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 --yes --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 completely override --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 either of --draft or --wip")}
|
|
}
|
|
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 runE != nil {
|
|
return runE(opts)
|
|
}
|
|
|
|
return createRun(opts)
|
|
},
|
|
}
|
|
mrCreateCmd.Flags().BoolVarP(&opts.Autofill, "fill", "f", false, "Do not prompt for title/description and just use commit info")
|
|
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 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`")
|
|
mrCreateCmd.Flags().StringSliceVarP(&opts.Reviewers, "reviewer", "", []string{}, "Request review from users by their `usernames`")
|
|
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")
|
|
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 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")
|
|
mrCreateCmd.Flags().BoolVarP(&opts.Yes, "yes", "y", false, "Skip submission confirmation prompt, with --fill it skips all optional prompts")
|
|
mrCreateCmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "continue merge request creation on web 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 merge request for an issue. The merge request title will be created from the issue if --title is not provided.")
|
|
|
|
mrCreateCmd.Flags().StringVarP(&opts.MRCreateTargetProject, "target-project", "", "", "Add target project by id or 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{}
|
|
|
|
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())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if !opts.TargetProject.MergeRequestsEnabled {
|
|
fmt.Fprintf(opts.IO.StdErr, "Merge requests are disabled for %q\n", opts.TargetProject.PathWithNamespace)
|
|
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 = &issue.Labels
|
|
}
|
|
opts.Description = fmt.Sprintf("Closes #%d", issue.IID)
|
|
opts.Title = fmt.Sprintf("Resolve \"%s\"", issue.Title)
|
|
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 --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)
|
|
}
|
|
|
|
templateNames = append(templateNames, "Open a blank merge request")
|
|
|
|
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)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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.Bool(true)
|
|
}
|
|
|
|
if opts.RemoveSourceBranch {
|
|
mrCreateOpts.RemoveSourceBranch = gitlab.Bool(true)
|
|
}
|
|
|
|
if opts.SquashBeforeMerge {
|
|
mrCreateOpts.Squash = gitlab.Bool(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 {
|
|
var metadataActions []cmdutils.Action
|
|
|
|
metadataActions, err = cmdutils.PickMetadata()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to pick metadata to add: %w", err)
|
|
}
|
|
|
|
for _, x := range metadataActions {
|
|
if x == cmdutils.AddLabelAction {
|
|
err = cmdutils.LabelsPrompt(&opts.Labels, labClient, baseRepoRemote)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if x == cmdutils.AddMilestoneAction {
|
|
err = cmdutils.MilestonesPrompt(&opts.MileStone, labClient, baseRepoRemote, opts.IO)
|
|
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.Labels{}
|
|
}
|
|
// 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.Int(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, opts.IO.IsaTTY))
|
|
return nil
|
|
}
|
|
|
|
return errors.New("expected to cancel, preview in browser, or submit")
|
|
}
|
|
|
|
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 == "" {
|
|
var body strings.Builder
|
|
for i := len(commits) - 1; i >= 0; i-- {
|
|
// 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)
|
|
}
|
|
}
|
|
opts.Description = body.String()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handlePush(opts *CreateOpts, remote *glrepo.Remote) error {
|
|
if opts.ShouldPush {
|
|
var 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/ee/user/project/quick_actions.html
|
|
// See also https://gitlab.com/gitlab-org/gitlab-foss/-/issues/19731#note_32550046
|
|
description += "\n/label "
|
|
for _, label := range opts.Labels {
|
|
description += fmt.Sprintf("~%q", label)
|
|
}
|
|
}
|
|
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"
|
|
u.RawQuery = fmt.Sprintf(
|
|
"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"),
|
|
opts.SourceBranch,
|
|
opts.TargetBranch,
|
|
opts.SourceProject.ID,
|
|
opts.TargetProject.ID)
|
|
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.PushURL.String())
|
|
// we ignore the error since git.GetDefaultBranch returns master and an error
|
|
// if the default branch cannot be determined
|
|
return br
|
|
}
|