pull_request.go 11.9 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
	-M, --milestone <NAME>
		The milestone name to add to this pull request. Passing the milestone number
		is deprecated.
69 70 71

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

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

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

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

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

84 85 86 87 88
var (
	flagPullRequestBase,
	flagPullRequestHead,
	flagPullRequestIssue,
	flagPullRequestMessage,
89
	flagPullRequestMilestone,
90
	flagPullRequestFile string
91

92
	flagPullRequestBrowse,
93
	flagPullRequestCopy,
94
	flagPullRequestEdit,
95
	flagPullRequestPush,
96
	flagPullRequestForce bool
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.StringVarP(&flagPullRequestMilestone, "milestone", "M", "", "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 229 230 231 232
	messageBuilder.AddCommentedSection(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))

233
	if cmd.FlagPassed("message") {
234 235
		messageBuilder.Message = flagPullRequestMessage
		messageBuilder.Edit = flagPullRequestEdit
236
	} else if cmd.FlagPassed("file") {
237
		messageBuilder.Message, err = msgFromFile(flagPullRequestFile)
238
		utils.Check(err)
239
		messageBuilder.Edit = flagPullRequestEdit
240
	} else if flagPullRequestIssue == "" {
241 242
		messageBuilder.Edit = true

243 244 245
		headForMessage := headTracking
		if flagPullRequestPush {
			headForMessage = head
246 247
		}

248 249 250 251 252 253 254
		message := ""
		commitLogs := ""

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

256
			re := regexp.MustCompile(`\nSigned-off-by:\s.*$`)
257
			message = re.ReplaceAllString(message, "")
258 259 260 261 262
		} else if len(commits) > 1 {
			commitLogs, err = git.Log(baseTracking, headForMessage)
			utils.Check(err)
		}

263 264 265 266
		if commitLogs != "" {
			messageBuilder.AddCommentedSection("\nChanges:\n\n" + strings.TrimSpace(commitLogs))
		}

267 268 269 270
		workdir, _ := git.WorkdirName()
		if workdir != "" {
			template, _ := github.ReadTemplate(github.PullRequestTemplate, workdir)
			if template != "" {
271
				message = message + "\n\n" + template
272 273 274
			}
		}

275
		messageBuilder.Message = message
M
m_nakamura145 已提交
276 277
	}

278 279 280
	title, body, err := messageBuilder.Extract()
	utils.Check(err)

J
Jingwen Owen Ou 已提交
281 282 283
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
284

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

294 295 296 297 298 299 300 301 302 303 304 305
	milestoneNumber := 0
	if flagPullRequestMilestone != "" {
		// BC: Don't try to resolve milestone name if it's an integer
		milestoneNumber, err = strconv.Atoi(flagPullRequestMilestone)
		if err != nil {
			milestones, err := client.FetchMilestones(baseProject)
			utils.Check(err)
			milestoneNumber, err = findMilestoneNumber(milestones, flagPullRequestMilestone)
			utils.Check(err)
		}
	}

306
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
307
	if args.Noop {
308
		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
309
		pullRequestURL = "PULL_REQUEST_URL"
J
Jingwen Owen Ou 已提交
310
	} else {
311 312 313 314
		params := map[string]interface{}{
			"base": base,
			"head": fullHead,
		}
J
Jingwen Owen Ou 已提交
315

J
Jingwen Owen Ou 已提交
316
		if title != "" {
317 318 319 320 321 322 323
			params["title"] = title
			if body != "" {
				params["body"] = body
			}
		} else {
			issueNum, _ := strconv.Atoi(flagPullRequestIssue)
			params["issue"] = issueNum
J
Jingwen Owen Ou 已提交
324
		}
325 326 327 328 329 330

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

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

360 361
		if err == nil {
			defer messageBuilder.Cleanup()
362 363
		}

J
Jingwen Owen Ou 已提交
364
		utils.Check(err)
365

366
		pullRequestURL = pr.HtmlUrl
367

368 369 370 371 372 373 374
		params = map[string]interface{}{}
		if len(flagPullRequestLabels) > 0 {
			params["labels"] = flagPullRequestLabels
		}
		if len(flagPullRequestAssignees) > 0 {
			params["assignees"] = flagPullRequestAssignees
		}
375 376
		if milestoneNumber > 0 {
			params["milestone"] = milestoneNumber
377
		}
378

379
		if len(params) > 0 {
380
			err = client.UpdateIssue(baseProject, pr.Number, params)
381 382
			utils.Check(err)
		}
383 384

		if len(flagPullRequestReviewers) > 0 {
385 386 387 388 389 390 391 392 393 394 395 396 397
			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,
			})
398 399
			utils.Check(err)
		}
J
Jingwen Owen Ou 已提交
400
	}
401

402 403 404 405
	if flagPullRequestIssue != "" {
		ui.Errorln("Warning: Issue to pull request conversion is deprecated and might not work in the future.")
	}

406
	args.NoForward()
407
	printBrowseOrCopy(args, pullRequestURL, flagPullRequestBrowse, flagPullRequestCopy)
J
Jingwen Owen Ou 已提交
408
}
J
Jingwen Owen Ou 已提交
409

J
Jingwen Owen Ou 已提交
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 438 439 440
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 ""
}
441 442 443 444 445 446 447 448 449 450

func findMilestoneNumber(milestones []github.Milestone, name string) (int, error) {
	for _, milestone := range milestones {
		if strings.EqualFold(milestone.Title, name) {
			return milestone.Number, nil
		}
	}

	return 0, fmt.Errorf("error: no milestone found with name '%s'", name)
}