pull_request.go 11.4 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 [-focp] [-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 206 207 208
	messageBuilder := &github.MessageBuilder{
		Filename: "PULLREQ_EDITMSG",
		Title:    "pull request",
	}
209

210 211 212 213 214 215 216 217 218 219 220 221 222 223
	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 已提交
224 225 226 227
	if flagPullRequestPush && remote == nil {
		utils.Check(fmt.Errorf("Can't find remote for %s", head))
	}

228
	if cmd.FlagPassed("message") {
229 230
		messageBuilder.Message = flagPullRequestMessage
		messageBuilder.Edit = flagPullRequestEdit
231
	} else if cmd.FlagPassed("file") {
232
		messageBuilder.Message, err = msgFromFile(flagPullRequestFile)
233
		utils.Check(err)
234
		messageBuilder.Edit = flagPullRequestEdit
235
	} else if flagPullRequestIssue == "" {
236 237
		messageBuilder.Edit = true

238 239 240
		headForMessage := headTracking
		if flagPullRequestPush {
			headForMessage = head
241 242
		}

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
		message := ""
		commitLogs := ""

		commits, _ := git.RefList(baseTracking, headForMessage)
		if len(commits) == 1 {
			message, err = git.Show(commits[0])
			utils.Check(err)
		} else if len(commits) > 1 {
			commitLogs, err = git.Log(baseTracking, headForMessage)
			utils.Check(err)
		}

		workdir, _ := git.WorkdirName()
		if workdir != "" {
			template, _ := github.ReadTemplate(github.PullRequestTemplate, workdir)
			if template != "" {
				if message == "" {
					message = "\n\n" + template
				} else {
					parts := strings.SplitN(message, "\n\n", 2)
					message = parts[0] + "\n\n" + template
					if len(parts) > 1 && parts[1] != "" {
						message = message + "\n\n" + parts[1]
					}
				}
			}
		}

		helpMessage := fmt.Sprintf(`Requesting a pull to %s from %s

Write a message for this pull request. The first block
of text is the title and the rest is the description.`, fullBase, fullHead)
		if commitLogs != "" {
			helpMessage = helpMessage + "\n\nChanges:\n\n" + strings.TrimSpace(commitLogs)
		}
278

279 280
		messageBuilder.Message = message
		messageBuilder.AddCommentedSection(helpMessage)
M
m_nakamura145 已提交
281 282
	}

283 284 285
	title, body, err := messageBuilder.Extract()
	utils.Check(err)

J
Jingwen Owen Ou 已提交
286 287 288
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
289

290 291 292 293
	if flagPullRequestPush {
		if args.Noop {
			args.Before(fmt.Sprintf("Would push to %s/%s", remote.Name, head), "")
		} else {
294
			err = git.Spawn("push", "--set-upstream", remote.Name, fmt.Sprintf("HEAD:%s", head))
N
Natalie Weizenbaum 已提交
295
			utils.Check(err)
296 297 298
		}
	}

299
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
300
	if args.Noop {
301
		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
302
		pullRequestURL = "PULL_REQUEST_URL"
J
Jingwen Owen Ou 已提交
303
	} else {
304 305 306 307
		params := map[string]interface{}{
			"base": base,
			"head": fullHead,
		}
J
Jingwen Owen Ou 已提交
308

J
Jingwen Owen Ou 已提交
309
		if title != "" {
310 311 312 313 314 315 316
			params["title"] = title
			if body != "" {
				params["body"] = body
			}
		} else {
			issueNum, _ := strconv.Atoi(flagPullRequestIssue)
			params["issue"] = issueNum
J
Jingwen Owen Ou 已提交
317
		}
318 319 320 321 322 323

		startedAt := time.Now()
		numRetries := 0
		retryDelay := 2
		retryAllowance := 0
		if flagPullRequestPush {
M
Mislav Marohnić 已提交
324 325 326 327 328 329
			if allowanceFromEnv := os.Getenv("HUB_RETRY_TIMEOUT"); allowanceFromEnv != "" {
				retryAllowance, err = strconv.Atoi(allowanceFromEnv)
				utils.Check(err)
			} else {
				retryAllowance = 9
			}
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
		}

		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
			}
		}
352

353 354
		if err == nil {
			defer messageBuilder.Cleanup()
355 356
		}

J
Jingwen Owen Ou 已提交
357
		utils.Check(err)
358

359
		pullRequestURL = pr.HtmlUrl
360

361 362 363 364 365 366 367 368 369 370
		params = map[string]interface{}{}
		if len(flagPullRequestLabels) > 0 {
			params["labels"] = flagPullRequestLabels
		}
		if len(flagPullRequestAssignees) > 0 {
			params["assignees"] = flagPullRequestAssignees
		}
		if flagPullRequestMilestone > 0 {
			params["milestone"] = flagPullRequestMilestone
		}
371

372
		if len(params) > 0 {
373
			err = client.UpdateIssue(baseProject, pr.Number, params)
374 375
			utils.Check(err)
		}
376 377

		if len(flagPullRequestReviewers) > 0 {
378 379 380 381 382 383 384 385 386 387 388 389 390
			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,
			})
391 392
			utils.Check(err)
		}
J
Jingwen Owen Ou 已提交
393
	}
394

395 396 397 398
	if flagPullRequestIssue != "" {
		ui.Errorln("Warning: Issue to pull request conversion is deprecated and might not work in the future.")
	}

399
	args.NoForward()
400
	printBrowseOrCopy(args, pullRequestURL, flagPullRequestBrowse, flagPullRequestCopy)
J
Jingwen Owen Ou 已提交
401
}
J
Jingwen Owen Ou 已提交
402

J
Jingwen Owen Ou 已提交
403 404 405 406 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
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 ""
}