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 }