pull_request.go 8.9 KB
Newer Older
1
package commands
J
Jingwen Owen Ou 已提交
2 3

import (
J
Jingwen Owen Ou 已提交
4
	"fmt"
J
Jingwen Owen Ou 已提交
5
	"regexp"
6
	"strconv"
7
	"strings"
J
Jingwen Owen Ou 已提交
8 9 10

	"github.com/github/hub/git"
	"github.com/github/hub/github"
11
	"github.com/github/hub/ui"
J
Jingwen Owen Ou 已提交
12
	"github.com/github/hub/utils"
J
Jingwen Owen Ou 已提交
13 14
)

15
var cmdPullRequest = &Command{
16 17
	Run: pullRequest,
	Usage: `
18
pull-request [-foc] [-b <BASE>] [-h <HEAD>] [-a <USERS>] [-M <MILESTONE>] [-l <LABELS>]
19
pull-request -m <MESSAGE>
20
pull-request -F <FILE> [--edit]
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
pull-request -i <ISSUE>
`,
	Long: `Create a GitHub pull request.

## Options:
	-f, --force
		Skip the check for unpushed commits.

	-m, --message <MESSAGE>
		Use the first line of <MESSAGE> as pull request title, and the rest as pull
		request description.

	-F, --file <FILE>
		Read the pull request title and description from <FILE>.

36 37 38
	-e, --edit
		Further edit the contents of <FILE> in a text editor before submitting.

39 40 41 42 43 44
	-i, --issue <ISSUE>, <ISSUE-URL>
		(Deprecated) Convert <ISSUE> to a pull request.

	-o, --browse
		Open the new pull request in a web browser.

45 46 47
	-c, --copy
		Put the URL of the new pull request to clipboard instead of printing it.

48 49 50 51 52
	-b, --base <BASE>
		The base branch in "[OWNER:]BRANCH" format. Defaults to the default branch
		(usually "master").

	-h, --head <HEAD>
C
Carlos Martín Nieto 已提交
53
		The head branch in "[OWNER:]BRANCH" format. Defaults to the current branch.
54

55 56
	-a, --assign <USERS>
		A comma-separated list of GitHub handles to assign to this pull request.
57 58 59 60 61 62

	-M, --milestone <ID>
		Add this pull request to a GitHub milestone with id <ID>.

	-l, --labels <LABELS>
		Add a comma-separated list of labels to this pull request.
M
Mislav Marohnić 已提交
63 64 65 66

## See also:

hub(1), hub-merge(1), hub-checkout(1)
J
Jingwen Owen Ou 已提交
67 68 69
`,
}

70 71 72 73 74 75
var (
	flagPullRequestBase,
	flagPullRequestHead,
	flagPullRequestIssue,
	flagPullRequestMessage,
	flagPullRequestFile string
76

77
	flagPullRequestBrowse,
78
	flagPullRequestCopy,
79
	flagPullRequestEdit,
80
	flagPullRequestForce bool
81

82
	flagPullRequestMilestone uint64
83 84 85

	flagPullRequestAssignees,
	flagPullRequestLabels listFlag
86
)
J
Jingwen Owen Ou 已提交
87 88

func init() {
89 90 91
	cmdPullRequest.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestIssue, "issue", "i", "", "ISSUE")
92
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestBrowse, "browse", "o", false, "BROWSE")
93
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestCopy, "copy", "c", false, "COPY")
94
	cmdPullRequest.Flag.StringVarP(&flagPullRequestMessage, "message", "m", "", "MESSAGE")
95
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestEdit, "edit", "e", false, "EDIT")
96 97
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE")
98
	cmdPullRequest.Flag.VarP(&flagPullRequestAssignees, "assign", "a", "USERS")
99
	cmdPullRequest.Flag.Uint64VarP(&flagPullRequestMilestone, "milestone", "M", 0, "MILESTONE")
100
	cmdPullRequest.Flag.VarP(&flagPullRequestLabels, "labels", "l", "LABELS")
101 102

	CmdRunner.Use(cmdPullRequest)
J
Jingwen Owen Ou 已提交
103 104
}

105
func pullRequest(cmd *Command, args *Args) {
106 107
	localRepo, err := github.LocalRepo()
	utils.Check(err)
108 109

	currentBranch, err := localRepo.CurrentBranch()
J
Jingwen Owen Ou 已提交
110
	utils.Check(err)
111 112

	baseProject, err := localRepo.MainProject()
J
Jingwen Owen Ou 已提交
113
	utils.Check(err)
114

115
	host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
116 117 118
	if err != nil {
		utils.Check(github.FormatError("creating pull request", err))
	}
119
	client := github.NewClientWithHost(host)
J
Jingwen Owen Ou 已提交
120

121
	trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false)
122 123
	utils.Check(err)

124
	var (
J
Jingwen Owen Ou 已提交
125 126
		base, head string
		force      bool
127
	)
J
Jingwen Owen Ou 已提交
128

129
	force = flagPullRequestForce
J
Jingwen Owen Ou 已提交
130

131
	if flagPullRequestBase != "" {
J
Jingwen Owen Ou 已提交
132
		baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
133
	}
134

135
	if flagPullRequestHead != "" {
J
Jingwen Owen Ou 已提交
136
		headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
137
	}
138

J
Jingwen Owen Ou 已提交
139
	if args.ParamsSize() == 1 {
140
		arg := args.RemoveParam(0)
J
Jingwen Owen Ou 已提交
141
		flagPullRequestIssue = parsePullRequestIssueNumber(arg)
142 143
	}

144
	if base == "" {
J
Jingwen Owen Ou 已提交
145
		masterBranch := localRepo.MasterBranch()
146 147 148
		base = masterBranch.ShortName()
	}

J
Jingwen Owen Ou 已提交
149
	if head == "" && trackedBranch != nil {
J
Jingwen Owen Ou 已提交
150 151 152 153 154
		if !trackedBranch.IsRemote() {
			// the current branch tracking another branch
			// pretend there's no upstream at all
			trackedBranch = nil
		} else {
155
			if baseProject.SameAs(headProject) && base == trackedBranch.ShortName() {
J
Jingwen Owen Ou 已提交
156 157 158
				e := fmt.Errorf(`Aborted: head branch is the same as base ("%s")`, base)
				e = fmt.Errorf("%s\n(use `-h <branch>` to specify an explicit pull request head)", e)
				utils.Check(e)
159 160
			}
		}
J
Jingwen Owen Ou 已提交
161 162
	}

J
Jingwen Owen Ou 已提交
163 164 165 166 167 168
	if head == "" {
		if trackedBranch == nil {
			head = currentBranch.ShortName()
		} else {
			head = trackedBranch.ShortName()
		}
169 170
	}

171 172 173 174 175
	if headRepo, err := client.Repository(headProject); err == nil {
		headProject.Owner = headRepo.Owner.Login
		headProject.Name = headRepo.Name
	}

176 177 178
	fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
	fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)

179 180 181 182 183 184 185
	if !force && trackedBranch != nil {
		remoteCommits, _ := git.RefList(trackedBranch.LongName(), "")
		if len(remoteCommits) > 0 {
			err = fmt.Errorf("Aborted: %d commits are not yet pushed to %s", len(remoteCommits), trackedBranch.LongName())
			err = fmt.Errorf("%s\n(use `-f` to force submit a pull request anyway)", err)
			utils.Check(err)
		}
186 187
	}

188
	var editor *github.Editor
189 190 191 192 193 194 195 196
	var title, body string

	if cmd.FlagPassed("message") {
		title, body = readMsg(flagPullRequestMessage)
	} else if cmd.FlagPassed("file") {
		title, body, editor, err = readMsgFromFile(flagPullRequestFile, flagPullRequestEdit, "PULLREQ", "pull request")
		utils.Check(err)
	} else if flagPullRequestIssue == "" {
197 198 199 200 201 202 203 204 205 206 207 208 209 210
		baseTracking := base
		headTracking := head

		remote := gitRemoteForProject(baseProject)
		if remote != nil {
			baseTracking = fmt.Sprintf("%s/%s", remote.Name, base)
		}
		if remote == nil || !baseProject.SameAs(headProject) {
			remote = gitRemoteForProject(headProject)
		}
		if remote != nil {
			headTracking = fmt.Sprintf("%s/%s", remote.Name, head)
		}

G
ganmacs 已提交
211
		message, err := createPullRequestMessage(baseTracking, headTracking, fullBase, fullHead)
212 213 214 215 216 217
		utils.Check(err)

		editor, err = github.NewEditor("PULLREQ", "pull request", message)
		utils.Check(err)

		title, body, err = editor.EditTitleAndBody()
M
m_nakamura145 已提交
218 219 220
		utils.Check(err)
	}

J
Jingwen Owen Ou 已提交
221 222 223
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
224

225
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
226
	if args.Noop {
227
		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
228
		pullRequestURL = "PULL_REQUEST_URL"
J
Jingwen Owen Ou 已提交
229
	} else {
230 231 232 233
		params := map[string]interface{}{
			"base": base,
			"head": fullHead,
		}
J
Jingwen Owen Ou 已提交
234

J
Jingwen Owen Ou 已提交
235
		if title != "" {
236 237 238 239 240 241 242
			params["title"] = title
			if body != "" {
				params["body"] = body
			}
		} else {
			issueNum, _ := strconv.Atoi(flagPullRequestIssue)
			params["issue"] = issueNum
J
Jingwen Owen Ou 已提交
243
		}
244
		pr, err := client.CreatePullRequest(baseProject, params)
245

246 247 248 249
		if err == nil && editor != nil {
			defer editor.DeleteFile()
		}

J
Jingwen Owen Ou 已提交
250
		utils.Check(err)
251

252
		pullRequestURL = pr.HtmlUrl
253

254 255 256 257 258 259 260 261 262 263
		params = map[string]interface{}{}
		if len(flagPullRequestLabels) > 0 {
			params["labels"] = flagPullRequestLabels
		}
		if len(flagPullRequestAssignees) > 0 {
			params["assignees"] = flagPullRequestAssignees
		}
		if flagPullRequestMilestone > 0 {
			params["milestone"] = flagPullRequestMilestone
		}
264

265
		if len(params) > 0 {
266
			err = client.UpdateIssue(baseProject, pr.Number, params)
267 268
			utils.Check(err)
		}
J
Jingwen Owen Ou 已提交
269
	}
270

271 272 273 274
	if flagPullRequestIssue != "" {
		ui.Errorln("Warning: Issue to pull request conversion is deprecated and might not work in the future.")
	}

275
	args.NoForward()
276
	printBrowseOrCopy(args, pullRequestURL, flagPullRequestBrowse, flagPullRequestCopy)
J
Jingwen Owen Ou 已提交
277
}
J
Jingwen Owen Ou 已提交
278

G
ganmacs 已提交
279
func createPullRequestMessage(base, head, fullBase, fullHead string) (string, error) {
J
Jingwen Owen Ou 已提交
280 281 282 283 284 285 286
	var (
		defaultMsg string
		commitLogs string
		err        error
	)

	commits, _ := git.RefList(base, head)
287
	if len(commits) == 1 {
J
Jingwen Owen Ou 已提交
288
		defaultMsg, err = git.Show(commits[0])
289
		if err != nil {
290
			return "", err
291 292
		}
	} else if len(commits) > 1 {
J
Jingwen Owen Ou 已提交
293
		commitLogs, err = git.Log(base, head)
294
		if err != nil {
295
			return "", err
296
		}
J
Jingwen Owen Ou 已提交
297 298
	}

G
ganmacs 已提交
299
	if template := github.GetPullRequestTemplate(); template != "" {
300 301 302 303 304 305 306 307 308
		if defaultMsg == "" {
			defaultMsg = "\n\n" + template
		} else {
			parts := strings.SplitN(defaultMsg, "\n\n", 2)
			defaultMsg = parts[0] + "\n\n" + template
			if len(parts) > 1 && parts[1] != "" {
				defaultMsg = defaultMsg + "\n\n" + parts[1]
			}
		}
G
ganmacs 已提交
309 310
	}

J
Jingwen Owen Ou 已提交
311
	cs := git.CommentChar()
J
Jingwen Owen Ou 已提交
312

J
Jingwen Owen Ou 已提交
313
	return renderPullRequestTpl(defaultMsg, cs, fullBase, fullHead, commitLogs)
J
Jingwen Owen Ou 已提交
314 315
}

J
Jingwen Owen Ou 已提交
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
func parsePullRequestProject(context *github.Project, s string) (p *github.Project, ref string) {
	p = context
	ref = s

	if strings.Contains(s, ":") {
		split := strings.SplitN(s, ":", 2)
		ref = split[1]
		var name string
		if !strings.Contains(split[0], "/") {
			name = context.Name
		}
		p = github.NewProject(split[0], name, context.Host)
	}

	return
}

func parsePullRequestIssueNumber(url string) string {
	u, e := github.ParseURL(url)
	if e != nil {
		return ""
	}

	r := regexp.MustCompile(`^issues\/(\d+)`)
	p := u.ProjectPath()
	if r.MatchString(p) {
		return r.FindStringSubmatch(p)[1]
	}

	return ""
}