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

import (
J
Jingwen Owen Ou 已提交
4
	"fmt"
M
Mislav Marohnić 已提交
5
	"os"
J
Jingwen Owen Ou 已提交
6
	"regexp"
7
	"strconv"
8
	"strings"
9
	"time"
J
Jingwen Owen Ou 已提交
10 11 12

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

17
var cmdPullRequest = &Command{
18 19
	Run: pullRequest,
	Usage: `
20
pull-request [-foc] [-b <BASE>] [-h <HEAD>] [-r <REVIEWERS> ] [-a <ASSIGNEES>] [-M <MILESTONE>] [-l <LABELS>]
21
pull-request -m <MESSAGE>
22
pull-request -F <FILE> [--edit]
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
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>.

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

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

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

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

50
	-p, --push
N
Natalie Weizenbaum 已提交
51
		Push the current branch to <HEAD> before creating the pull request.
52

53 54 55 56 57
	-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 已提交
58
		The head branch in "[OWNER:]BRANCH" format. Defaults to the current branch.
59

60 61 62
	-r, --reviewer <USERS>
		A comma-separated list of GitHub handles to request a review from.

63 64
	-a, --assign <USERS>
		A comma-separated list of GitHub handles to assign to this pull request.
65 66 67 68 69 70

	-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ć 已提交
71

M
Mislav Marohnić 已提交
72 73 74 75 76
## Configuration:

	HUB_RETRY_TIMEOUT=<SECONDS>
		The maximum time to keep retrying after HTTP 422 on '--push' (default: 9).

M
Mislav Marohnić 已提交
77 78 79
## See also:

hub(1), hub-merge(1), hub-checkout(1)
J
Jingwen Owen Ou 已提交
80 81 82
`,
}

83 84 85 86 87 88
var (
	flagPullRequestBase,
	flagPullRequestHead,
	flagPullRequestIssue,
	flagPullRequestMessage,
	flagPullRequestFile string
89

90
	flagPullRequestBrowse,
91
	flagPullRequestCopy,
92
	flagPullRequestEdit,
93
	flagPullRequestPush,
94
	flagPullRequestForce bool
95

96
	flagPullRequestMilestone uint64
97 98

	flagPullRequestAssignees,
99
	flagPullRequestReviewers,
100
	flagPullRequestLabels listFlag
101
)
J
Jingwen Owen Ou 已提交
102 103

func init() {
104 105 106
	cmdPullRequest.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestIssue, "issue", "i", "", "ISSUE")
107
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestBrowse, "browse", "o", false, "BROWSE")
108
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestCopy, "copy", "c", false, "COPY")
109
	cmdPullRequest.Flag.StringVarP(&flagPullRequestMessage, "message", "m", "", "MESSAGE")
110
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestEdit, "edit", "e", false, "EDIT")
111
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestPush, "push", "p", false, "PUSH")
112 113
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE")
114
	cmdPullRequest.Flag.VarP(&flagPullRequestAssignees, "assign", "a", "USERS")
115
	cmdPullRequest.Flag.VarP(&flagPullRequestReviewers, "reviewer", "r", "USERS")
116
	cmdPullRequest.Flag.Uint64VarP(&flagPullRequestMilestone, "milestone", "M", 0, "MILESTONE")
117
	cmdPullRequest.Flag.VarP(&flagPullRequestLabels, "labels", "l", "LABELS")
118 119

	CmdRunner.Use(cmdPullRequest)
J
Jingwen Owen Ou 已提交
120 121
}

122
func pullRequest(cmd *Command, args *Args) {
123 124
	localRepo, err := github.LocalRepo()
	utils.Check(err)
125 126

	currentBranch, err := localRepo.CurrentBranch()
J
Jingwen Owen Ou 已提交
127
	utils.Check(err)
128 129

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

132
	host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
133 134 135
	if err != nil {
		utils.Check(github.FormatError("creating pull request", err))
	}
136
	client := github.NewClientWithHost(host)
J
Jingwen Owen Ou 已提交
137

138
	trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false)
139 140
	utils.Check(err)

141
	var (
J
Jingwen Owen Ou 已提交
142 143
		base, head string
		force      bool
144
	)
J
Jingwen Owen Ou 已提交
145

146
	force = flagPullRequestForce
J
Jingwen Owen Ou 已提交
147

148
	if flagPullRequestBase != "" {
J
Jingwen Owen Ou 已提交
149
		baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
150
	}
151

152
	if flagPullRequestHead != "" {
J
Jingwen Owen Ou 已提交
153
		headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
154
	}
155

J
Jingwen Owen Ou 已提交
156
	if args.ParamsSize() == 1 {
157
		arg := args.RemoveParam(0)
J
Jingwen Owen Ou 已提交
158
		flagPullRequestIssue = parsePullRequestIssueNumber(arg)
159 160
	}

161
	if base == "" {
J
Jingwen Owen Ou 已提交
162
		masterBranch := localRepo.MasterBranch()
163 164 165
		base = masterBranch.ShortName()
	}

J
Jingwen Owen Ou 已提交
166
	if head == "" && trackedBranch != nil {
J
Jingwen Owen Ou 已提交
167 168 169 170 171
		if !trackedBranch.IsRemote() {
			// the current branch tracking another branch
			// pretend there's no upstream at all
			trackedBranch = nil
		} else {
172
			if baseProject.SameAs(headProject) && base == trackedBranch.ShortName() {
J
Jingwen Owen Ou 已提交
173 174 175
				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)
176 177
			}
		}
J
Jingwen Owen Ou 已提交
178 179
	}

J
Jingwen Owen Ou 已提交
180 181 182 183 184 185
	if head == "" {
		if trackedBranch == nil {
			head = currentBranch.ShortName()
		} else {
			head = trackedBranch.ShortName()
		}
186 187
	}

188 189 190 191 192
	if headRepo, err := client.Repository(headProject); err == nil {
		headProject.Owner = headRepo.Owner.Login
		headProject.Name = headRepo.Name
	}

193 194 195
	fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
	fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)

196 197 198 199 200 201 202
	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)
		}
203 204
	}

205
	var editor *github.Editor
206 207
	var title, body string

208 209 210 211 212 213 214 215 216 217 218 219 220 221
	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)
	}

N
Natalie Weizenbaum 已提交
222 223 224 225
	if flagPullRequestPush && remote == nil {
		utils.Check(fmt.Errorf("Can't find remote for %s", head))
	}

226 227 228 229 230 231
	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 == "" {
232 233 234
		headForMessage := headTracking
		if flagPullRequestPush {
			headForMessage = head
235 236
		}

237
		message, err := createPullRequestMessage(baseTracking, headForMessage, fullBase, fullHead)
238 239 240 241 242 243
		utils.Check(err)

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

		title, body, err = editor.EditTitleAndBody()
M
m_nakamura145 已提交
244 245 246
		utils.Check(err)
	}

J
Jingwen Owen Ou 已提交
247 248 249
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
250

251 252 253 254
	if flagPullRequestPush {
		if args.Noop {
			args.Before(fmt.Sprintf("Would push to %s/%s", remote.Name, head), "")
		} else {
255
			err = git.Spawn("push", "--set-upstream", remote.Name, fmt.Sprintf("HEAD:%s", head))
N
Natalie Weizenbaum 已提交
256
			utils.Check(err)
257 258 259
		}
	}

260
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
261
	if args.Noop {
262
		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
263
		pullRequestURL = "PULL_REQUEST_URL"
J
Jingwen Owen Ou 已提交
264
	} else {
265 266 267 268
		params := map[string]interface{}{
			"base": base,
			"head": fullHead,
		}
J
Jingwen Owen Ou 已提交
269

J
Jingwen Owen Ou 已提交
270
		if title != "" {
271 272 273 274 275 276 277
			params["title"] = title
			if body != "" {
				params["body"] = body
			}
		} else {
			issueNum, _ := strconv.Atoi(flagPullRequestIssue)
			params["issue"] = issueNum
J
Jingwen Owen Ou 已提交
278
		}
279 280 281 282 283 284

		startedAt := time.Now()
		numRetries := 0
		retryDelay := 2
		retryAllowance := 0
		if flagPullRequestPush {
M
Mislav Marohnić 已提交
285 286 287 288 289 290
			if allowanceFromEnv := os.Getenv("HUB_RETRY_TIMEOUT"); allowanceFromEnv != "" {
				retryAllowance, err = strconv.Atoi(allowanceFromEnv)
				utils.Check(err)
			} else {
				retryAllowance = 9
			}
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
		}

		var pr *github.PullRequest
		for {
			pr, err = client.CreatePullRequest(baseProject, params)
			if err != nil && strings.Contains(err.Error(), `Invalid value for "head"`) {
				if retryAllowance > 0 {
					retryAllowance -= retryDelay
					time.Sleep(time.Duration(retryDelay) * time.Second)
					retryDelay += 1
					numRetries += 1
				} else {
					if numRetries > 0 {
						duration := time.Now().Sub(startedAt)
						err = fmt.Errorf("%s\nGiven up after retrying for %.1f seconds.", err, duration.Seconds())
					}
					break
				}
			} else {
				break
			}
		}
313

314 315 316 317
		if err == nil && editor != nil {
			defer editor.DeleteFile()
		}

J
Jingwen Owen Ou 已提交
318
		utils.Check(err)
319

320
		pullRequestURL = pr.HtmlUrl
321

322 323 324 325 326 327 328 329 330 331
		params = map[string]interface{}{}
		if len(flagPullRequestLabels) > 0 {
			params["labels"] = flagPullRequestLabels
		}
		if len(flagPullRequestAssignees) > 0 {
			params["assignees"] = flagPullRequestAssignees
		}
		if flagPullRequestMilestone > 0 {
			params["milestone"] = flagPullRequestMilestone
		}
332

333
		if len(params) > 0 {
334
			err = client.UpdateIssue(baseProject, pr.Number, params)
335 336
			utils.Check(err)
		}
337 338

		if len(flagPullRequestReviewers) > 0 {
339 340 341 342 343 344 345 346 347 348 349 350 351
			userReviewers := []string{}
			teamReviewers := []string{}
			for _, reviewer := range flagPullRequestReviewers {
				if strings.Contains(reviewer, "/") {
					teamReviewers = append(teamReviewers, strings.SplitN(reviewer, "/", 2)[1])
				} else {
					userReviewers = append(userReviewers, reviewer)
				}
			}
			err = client.RequestReview(baseProject, pr.Number, map[string]interface{}{
				"reviewers":      userReviewers,
				"team_reviewers": teamReviewers,
			})
352 353
			utils.Check(err)
		}
J
Jingwen Owen Ou 已提交
354
	}
355

356 357 358 359
	if flagPullRequestIssue != "" {
		ui.Errorln("Warning: Issue to pull request conversion is deprecated and might not work in the future.")
	}

360
	args.NoForward()
361
	printBrowseOrCopy(args, pullRequestURL, flagPullRequestBrowse, flagPullRequestCopy)
J
Jingwen Owen Ou 已提交
362
}
J
Jingwen Owen Ou 已提交
363

G
ganmacs 已提交
364
func createPullRequestMessage(base, head, fullBase, fullHead string) (string, error) {
J
Jingwen Owen Ou 已提交
365 366 367 368 369 370 371
	var (
		defaultMsg string
		commitLogs string
		err        error
	)

	commits, _ := git.RefList(base, head)
372
	if len(commits) == 1 {
J
Jingwen Owen Ou 已提交
373
		defaultMsg, err = git.Show(commits[0])
374
		if err != nil {
375
			return "", err
376 377
		}
	} else if len(commits) > 1 {
J
Jingwen Owen Ou 已提交
378
		commitLogs, err = git.Log(base, head)
379
		if err != nil {
380
			return "", err
381
		}
J
Jingwen Owen Ou 已提交
382 383
	}

384 385 386 387 388 389 390 391 392 393 394 395 396 397
	workdir, _ := git.WorkdirName()
	if workdir != "" {
		template, err := github.ReadTemplate(github.PullRequestTemplate, workdir)
		if err != nil {
			return "", err
		} else if template != "" {
			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]
				}
398 399
			}
		}
G
ganmacs 已提交
400 401
	}

J
Jingwen Owen Ou 已提交
402
	cs := git.CommentChar()
J
Jingwen Owen Ou 已提交
403

J
Jingwen Owen Ou 已提交
404
	return renderPullRequestTpl(defaultMsg, cs, fullBase, fullHead, commitLogs)
J
Jingwen Owen Ou 已提交
405 406
}

J
Jingwen Owen Ou 已提交
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
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 ""
}