gitlab.com/lab-cli/lab@v0.14.0/cmd/mr_create.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"regexp"
     9  	"runtime"
    10  	"strconv"
    11  	"strings"
    12  	"text/template"
    13  
    14  	"github.com/pkg/errors"
    15  	"github.com/spf13/cobra"
    16  	gitconfig "github.com/tcnksm/go-gitconfig"
    17  	gitlab "github.com/xanzy/go-gitlab"
    18  	"github.com/zaquestion/lab/internal/git"
    19  	lab "github.com/zaquestion/lab/internal/gitlab"
    20  )
    21  
    22  // mrCmd represents the mr command
    23  var mrCreateCmd = &cobra.Command{
    24  	Use:     "create [remote [branch]]",
    25  	Aliases: []string{"new"},
    26  	Short:   "Open a merge request on GitLab",
    27  	Long:    `Creates a merge request (MR created on origin master by default)`,
    28  	Args:    cobra.MaximumNArgs(2),
    29  	Run:     runMRCreate,
    30  }
    31  
    32  func init() {
    33  	mrCreateCmd.Flags().StringSliceP("message", "m", []string{}, "Use the given <msg>; multiple -m are concatenated as separate paragraphs")
    34  	mrCreateCmd.Flags().StringP("assignee", "a", "", "Set assignee by username")
    35  	mrCreateCmd.Flags().StringSliceP("label", "l", []string{}, "Add label <label>; can be specified multiple times for multiple labels")
    36  	mrCreateCmd.Flags().BoolP("remove-source-branch", "d", false, "Remove source branch from remote after merge")
    37  	mrCreateCmd.Flags().BoolP("squash", "s", false, "Squash commits when merging")
    38  	mrCreateCmd.Flags().Bool("allow-collaboration", false, "Allow commits from other members")
    39  	mrCreateCmd.Flags().Int("milestone", -1, "Set milestone by milestone ID")
    40  	mergeRequestCmd.Flags().AddFlagSet(mrCreateCmd.Flags())
    41  	mrCmd.AddCommand(mrCreateCmd)
    42  }
    43  
    44  // getAssignee returns the assigneeID for use with other GitLab API calls.
    45  // NOTE: It is also used by issue_create.go
    46  func getAssigneeID(assignee string) *int {
    47  	if assignee == "" {
    48  		return nil
    49  	}
    50  	if assignee[0] == '@' {
    51  		assignee = assignee[1:]
    52  	}
    53  	assigneeID, err := lab.UserIDFromUsername(assignee)
    54  	if err != nil {
    55  		return nil
    56  	}
    57  	if assigneeID == -1 {
    58  		return nil
    59  	}
    60  	return gitlab.Int(assigneeID)
    61  }
    62  
    63  func runMRCreate(cmd *cobra.Command, args []string) {
    64  	msgs, err := cmd.Flags().GetStringSlice("message")
    65  	if err != nil {
    66  		log.Fatal(err)
    67  	}
    68  	assignee, err := cmd.Flags().GetString("assignee")
    69  	if err != nil {
    70  		log.Fatal(err)
    71  	}
    72  	branch, err := git.CurrentBranch()
    73  	if err != nil {
    74  		log.Fatal(err)
    75  	}
    76  
    77  	sourceRemote := determineSourceRemote(branch)
    78  	sourceProjectName, err := git.PathWithNameSpace(sourceRemote)
    79  	if err != nil {
    80  		log.Fatal(err)
    81  	}
    82  
    83  	p, err := lab.FindProject(sourceProjectName)
    84  	if err != nil {
    85  		log.Fatal(err)
    86  	}
    87  	if !lab.BranchPushed(p.ID, branch) {
    88  		log.Fatalf("aborting MR, source branch %s not present on remote %s. did you forget to push?", branch, sourceRemote)
    89  	}
    90  
    91  	targetRemote := forkedFromRemote
    92  	if len(args) > 0 {
    93  		targetRemote = args[0]
    94  		ok, err := git.IsRemote(targetRemote)
    95  		if err != nil || !ok {
    96  			log.Fatal(errors.Wrapf(err, "%s is not a valid remote", targetRemote))
    97  		}
    98  	}
    99  	targetProjectName, err := git.PathWithNameSpace(targetRemote)
   100  	if err != nil {
   101  		log.Fatal(err)
   102  	}
   103  	targetProject, err := lab.FindProject(targetProjectName)
   104  	if err != nil {
   105  		log.Fatal(err)
   106  	}
   107  	targetBranch := "master"
   108  	if len(args) > 1 {
   109  		targetBranch = args[1]
   110  		if !lab.BranchPushed(targetProject.ID, targetBranch) {
   111  			log.Fatalf("aborting MR, target branch %s not present on remote %s. did you forget to push?", targetBranch, targetRemote)
   112  		}
   113  	}
   114  
   115  	var title, body string
   116  
   117  	if len(msgs) > 0 {
   118  		title, body = msgs[0], strings.Join(msgs[1:], "\n\n")
   119  	} else {
   120  		msg, err := mrText(targetBranch, branch, sourceRemote, forkedFromRemote)
   121  		if err != nil {
   122  			log.Fatal(err)
   123  		}
   124  
   125  		title, body, err = git.Edit("MERGEREQ", msg)
   126  		if err != nil {
   127  			_, f, l, _ := runtime.Caller(0)
   128  			log.Fatal(f+":"+strconv.Itoa(l)+" ", err)
   129  		}
   130  	}
   131  
   132  	removeSourceBranch, _ := cmd.Flags().GetBool("remove-source-branch")
   133  	squash, _ := cmd.Flags().GetBool("squash")
   134  	allowCollaboration, _ := cmd.Flags().GetBool("allow-collaboration")
   135  
   136  	labels, err := cmd.Flags().GetStringSlice("label")
   137  	if err != nil {
   138  		log.Fatal(err)
   139  	}
   140  
   141  	milestoneID, _ := cmd.Flags().GetInt("milestone")
   142  	var milestone *int
   143  	if milestoneID < 0 {
   144  		milestone = nil
   145  	} else {
   146  		milestone = &milestoneID
   147  	}
   148  
   149  	if title == "" {
   150  		log.Fatal("aborting MR due to empty MR msg")
   151  	}
   152  
   153  	mrURL, err := lab.MRCreate(sourceProjectName, &gitlab.CreateMergeRequestOptions{
   154  		SourceBranch:       &branch,
   155  		TargetBranch:       gitlab.String(targetBranch),
   156  		TargetProjectID:    &targetProject.ID,
   157  		Title:              &title,
   158  		Description:        &body,
   159  		AssigneeID:         getAssigneeID(assignee),
   160  		RemoveSourceBranch: &removeSourceBranch,
   161  		Squash:             &squash,
   162  		AllowCollaboration: &allowCollaboration,
   163  		Labels:             gitlab.Labels(labels),
   164  		MilestoneID:        milestone,
   165  	})
   166  	if err != nil {
   167  		// FIXME: not exiting fatal here to allow code coverage to
   168  		// generate during Test_mrCreate. In the meantime API failures
   169  		// will exit 0
   170  		fmt.Fprintln(os.Stderr, err)
   171  	}
   172  	fmt.Println(mrURL + "/diffs")
   173  }
   174  
   175  func determineSourceRemote(branch string) string {
   176  	// Check if the branch is being tracked
   177  	r, err := gitconfig.Local("branch." + branch + ".remote")
   178  	if err == nil {
   179  		return r
   180  	}
   181  
   182  	return forkRemote
   183  }
   184  
   185  func mrText(base, head, sourceRemote, forkedFromRemote string) (string, error) {
   186  	lastCommitMsg, err := git.LastCommitMessage()
   187  	if err != nil {
   188  		return "", err
   189  	}
   190  	const tmpl = `{{if .InitMsg}}{{.InitMsg}}{{end}}
   191  
   192  {{if .Tmpl}}{{.Tmpl}}{{end}}
   193  {{.CommentChar}} Requesting a merge into {{.Base}} from {{.Head}}
   194  {{.CommentChar}}
   195  {{.CommentChar}} Write a message for this merge request. The first block
   196  {{.CommentChar}} of text is the title and the rest is the description.{{if .CommitLogs}}
   197  {{.CommentChar}}
   198  {{.CommentChar}} Changes:
   199  {{.CommentChar}}
   200  {{.CommitLogs}}{{end}}`
   201  
   202  	mrTmpl := lab.LoadGitLabTmpl(lab.TmplMR)
   203  
   204  	remoteBase := fmt.Sprintf("%s/%s", forkedFromRemote, base)
   205  	commitLogs, err := git.Log(remoteBase, head)
   206  	if err != nil {
   207  		return "", err
   208  	}
   209  	startRegexp := regexp.MustCompilePOSIX("^")
   210  	commentChar := git.CommentChar()
   211  	commitLogs = strings.TrimSpace(commitLogs)
   212  	commitLogs = startRegexp.ReplaceAllString(commitLogs, fmt.Sprintf("%s ", commentChar))
   213  
   214  	t, err := template.New("tmpl").Parse(tmpl)
   215  	if err != nil {
   216  		return "", err
   217  	}
   218  
   219  	msg := &struct {
   220  		InitMsg     string
   221  		Tmpl        string
   222  		CommentChar string
   223  		Base        string
   224  		Head        string
   225  		CommitLogs  string
   226  	}{
   227  		InitMsg:     lastCommitMsg,
   228  		Tmpl:        mrTmpl,
   229  		CommentChar: commentChar,
   230  		Base:        forkedFromRemote + ":" + base,
   231  		Head:        sourceRemote + ":" + head,
   232  		CommitLogs:  commitLogs,
   233  	}
   234  
   235  	var b bytes.Buffer
   236  	err = t.Execute(&b, msg)
   237  	if err != nil {
   238  		return "", err
   239  	}
   240  
   241  	return b.String(), nil
   242  }