hub/commands/pull_request.go

484 строки
13 KiB
Go
Исходник Обычный вид История

2013-05-29 22:58:46 +04:00
package commands
2013-04-11 19:33:43 +04:00
import (
2013-04-29 00:41:45 +04:00
"fmt"
2016-10-22 13:28:32 +03:00
"os"
2013-04-24 14:00:45 +04:00
"regexp"
2016-08-08 01:43:48 +03:00
"strconv"
2013-04-28 21:45:00 +04:00
"strings"
"time"
2014-02-24 01:34:10 +04:00
"github.com/github/hub/git"
"github.com/github/hub/github"
"github.com/github/hub/ui"
2014-02-24 01:34:10 +04:00
"github.com/github/hub/utils"
2013-04-11 19:33:43 +04:00
)
var cmdPullRequest = &Command{
Run: pullRequest,
Usage: `
pull-request [-focp] [-b <BASE>] [-h <HEAD>] [-r <REVIEWERS> ] [-a <ASSIGNEES>] [-M <MILESTONE>] [-l <LABELS>]
pull-request -m <MESSAGE> [--edit]
pull-request -F <FILE> [--edit]
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.
--no-edit
Use the message from the first commit on the branch as pull request title
and description without opening a text editor.
-F, --file <FILE>
Read the pull request title and description from <FILE>.
-e, --edit
Further edit the contents of <FILE> in a text editor before submitting.
-i, --issue <ISSUE>, <ISSUE-URL>
(Deprecated) Convert <ISSUE> to a pull request.
-o, --browse
Open the new pull request in a web browser.
-c, --copy
Put the URL of the new pull request to clipboard instead of printing it.
-p, --push
2016-10-20 23:53:27 +03:00
Push the current branch to <HEAD> before creating the pull request.
-b, --base <BASE>
The base branch in "[OWNER:]BRANCH" format. Defaults to the default branch
(usually "master").
-h, --head <HEAD>
The head branch in "[OWNER:]BRANCH" format. Defaults to the current branch.
-r, --reviewer <USERS>
A comma-separated list of GitHub handles to request a review from.
2016-08-10 12:44:38 +03:00
-a, --assign <USERS>
A comma-separated list of GitHub handles to assign to this pull request.
-M, --milestone <NAME>
The milestone name to add to this pull request. Passing the milestone number
is deprecated.
-l, --labels <LABELS>
Add a comma-separated list of labels to this pull request.
2016-01-24 18:50:01 +03:00
## Examples:
$ hub pull-request
[ opens a text editor for writing title and message ]
[ creates a pull request for the current branch ]
$ hub pull-request --base OWNER:master --head MYUSER:my-branch
[ creates a pull request with explicit base and head branches ]
$ hub pull-request --browse -m "My title"
[ creates a pull request with the given title and opens it in a browser ]
$ hub pull-request -F - --edit < path/to/message-template.md
[ further edit the title and message received on standard input ]
2016-10-22 13:28:32 +03:00
## Configuration:
HUB_RETRY_TIMEOUT=<SECONDS>
The maximum time to keep retrying after HTTP 422 on '--push' (default: 9).
2016-01-24 18:50:01 +03:00
## See also:
hub(1), hub-merge(1), hub-checkout(1)
2013-04-11 19:33:43 +04:00
`,
}
2013-12-07 19:43:55 +04:00
var (
flagPullRequestBase,
flagPullRequestHead,
flagPullRequestIssue,
flagPullRequestMessage,
flagPullRequestMilestone,
2013-12-07 19:43:55 +04:00
flagPullRequestFile string
flagPullRequestBrowse,
flagPullRequestCopy,
flagPullRequestEdit,
flagPullRequestPush,
flagPullRequestForce,
flagPullRequestNoEdit bool
flagPullRequestAssignees,
flagPullRequestReviewers,
flagPullRequestLabels listFlag
2013-12-07 19:43:55 +04:00
)
2013-04-11 19:33:43 +04:00
func init() {
cmdPullRequest.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE")
cmdPullRequest.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD")
cmdPullRequest.Flag.StringVarP(&flagPullRequestIssue, "issue", "i", "", "ISSUE")
cmdPullRequest.Flag.BoolVarP(&flagPullRequestBrowse, "browse", "o", false, "BROWSE")
cmdPullRequest.Flag.BoolVarP(&flagPullRequestCopy, "copy", "c", false, "COPY")
cmdPullRequest.Flag.StringVarP(&flagPullRequestMessage, "message", "m", "", "MESSAGE")
cmdPullRequest.Flag.BoolVarP(&flagPullRequestEdit, "edit", "e", false, "EDIT")
cmdPullRequest.Flag.BoolVarP(&flagPullRequestPush, "push", "p", false, "PUSH")
cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE")
cmdPullRequest.Flag.BoolVarP(&flagPullRequestNoEdit, "no-edit", "", false, "NO-EDIT")
cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE")
cmdPullRequest.Flag.VarP(&flagPullRequestAssignees, "assign", "a", "USERS")
cmdPullRequest.Flag.VarP(&flagPullRequestReviewers, "reviewer", "r", "USERS")
cmdPullRequest.Flag.StringVarP(&flagPullRequestMilestone, "milestone", "M", "", "MILESTONE")
cmdPullRequest.Flag.VarP(&flagPullRequestLabels, "labels", "l", "LABELS")
CmdRunner.Use(cmdPullRequest)
2013-04-11 19:33:43 +04:00
}
func pullRequest(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
currentBranch, err := localRepo.CurrentBranch()
2013-12-06 22:54:11 +04:00
utils.Check(err)
baseProject, err := localRepo.MainProject()
2013-12-06 22:54:11 +04:00
utils.Check(err)
host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
if err != nil {
utils.Check(github.FormatError("creating pull request", err))
}
client := github.NewClientWithHost(host)
trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false)
utils.Check(err)
var (
base, head string
force bool
)
2013-12-08 12:33:48 +04:00
2013-12-07 19:43:55 +04:00
force = flagPullRequestForce
2013-12-08 12:33:48 +04:00
if flagPullRequestBase != "" {
2013-12-08 12:33:48 +04:00
baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
}
if flagPullRequestHead != "" {
2013-12-08 12:33:48 +04:00
headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
}
2013-07-02 22:56:45 +04:00
if args.ParamsSize() == 1 {
arg := args.RemoveParam(0)
2013-12-08 12:33:48 +04:00
flagPullRequestIssue = parsePullRequestIssueNumber(arg)
}
if base == "" {
masterBranch := localRepo.MasterBranch()
base = masterBranch.ShortName()
}
2014-03-05 20:37:17 +04:00
if head == "" && trackedBranch != nil {
if !trackedBranch.IsRemote() {
// the current branch tracking another branch
// pretend there's no upstream at all
trackedBranch = nil
} else {
if baseProject.SameAs(headProject) && base == trackedBranch.ShortName() {
2013-12-08 12:33:48 +04:00
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)
}
}
2014-03-05 20:37:17 +04:00
}
2014-03-05 20:39:23 +04:00
if head == "" {
if trackedBranch == nil {
head = currentBranch.ShortName()
} else {
head = trackedBranch.ShortName()
}
}
if headRepo, err := client.Repository(headProject); err == nil {
headProject.Owner = headRepo.Owner.Login
headProject.Name = headRepo.Name
}
fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)
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)
}
2013-12-07 19:43:55 +04:00
}
messageBuilder := &github.MessageBuilder{
Filename: "PULLREQ_EDITMSG",
Title: "pull request",
}
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)
}
2016-10-20 23:53:27 +03:00
if flagPullRequestPush && remote == nil {
utils.Check(fmt.Errorf("Can't find remote for %s", head))
}
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))
if cmd.FlagPassed("message") {
messageBuilder.Message = flagPullRequestMessage
messageBuilder.Edit = flagPullRequestEdit
} else if cmd.FlagPassed("file") {
messageBuilder.Message, err = msgFromFile(flagPullRequestFile)
utils.Check(err)
messageBuilder.Edit = flagPullRequestEdit
} else if flagPullRequestNoEdit {
commits, _ := git.RefList(baseTracking, head)
if len(commits) == 0 {
utils.Check(fmt.Errorf("Aborted: no commits detected between %s and %s", baseTracking, head))
}
message, err := git.Show(commits[len(commits)-1])
utils.Check(err)
messageBuilder.Message = message
} else if flagPullRequestIssue == "" {
messageBuilder.Edit = true
headForMessage := headTracking
if flagPullRequestPush {
headForMessage = head
}
message := ""
commitLogs := ""
commits, _ := git.RefList(baseTracking, headForMessage)
if len(commits) == 1 {
message, err = git.Show(commits[0])
utils.Check(err)
re := regexp.MustCompile(`\nSigned-off-by:\s.*$`)
message = re.ReplaceAllString(message, "")
} else if len(commits) > 1 {
commitLogs, err = git.Log(baseTracking, headForMessage)
utils.Check(err)
}
if commitLogs != "" {
messageBuilder.AddCommentedSection("\nChanges:\n\n" + strings.TrimSpace(commitLogs))
}
workdir, _ := git.WorkdirName()
if workdir != "" {
template, _ := github.ReadTemplate(github.PullRequestTemplate, workdir)
if template != "" {
message = message + "\n\n\n" + template
}
}
messageBuilder.Message = message
2015-06-08 18:17:28 +03:00
}
title, body, err := messageBuilder.Extract()
utils.Check(err)
2013-07-05 22:10:24 +04:00
if title == "" && flagPullRequestIssue == "" {
utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
}
2013-05-28 21:13:55 +04:00
if flagPullRequestPush {
if args.Noop {
args.Before(fmt.Sprintf("Would push to %s/%s", remote.Name, head), "")
} else {
err = git.Spawn("push", "--set-upstream", remote.Name, fmt.Sprintf("HEAD:%s", head))
2016-10-20 23:53:27 +03:00
utils.Check(err)
}
}
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)
}
}
2013-07-06 00:45:22 +04:00
var pullRequestURL string
2013-07-05 22:10:24 +04:00
if args.Noop {
args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
2013-07-06 00:45:22 +04:00
pullRequestURL = "PULL_REQUEST_URL"
2013-07-05 22:10:24 +04:00
} else {
2016-08-08 01:43:48 +03:00
params := map[string]interface{}{
"base": base,
"head": fullHead,
}
2014-02-24 02:24:37 +04:00
2013-07-05 22:10:24 +04:00
if title != "" {
2016-08-08 01:43:48 +03:00
params["title"] = title
if body != "" {
params["body"] = body
}
} else {
issueNum, _ := strconv.Atoi(flagPullRequestIssue)
params["issue"] = issueNum
2013-07-05 22:10:24 +04:00
}
startedAt := time.Now()
numRetries := 0
retryDelay := 2
retryAllowance := 0
if flagPullRequestPush {
2016-10-22 13:28:32 +03:00
if allowanceFromEnv := os.Getenv("HUB_RETRY_TIMEOUT"); allowanceFromEnv != "" {
retryAllowance, err = strconv.Atoi(allowanceFromEnv)
utils.Check(err)
} else {
retryAllowance = 9
}
}
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
}
}
2013-04-28 21:45:00 +04:00
if err == nil {
defer messageBuilder.Cleanup()
2014-03-02 23:43:43 +04:00
}
2014-02-24 02:24:37 +04:00
utils.Check(err)
pullRequestURL = pr.HtmlUrl
params = map[string]interface{}{}
if len(flagPullRequestLabels) > 0 {
params["labels"] = flagPullRequestLabels
}
if len(flagPullRequestAssignees) > 0 {
params["assignees"] = flagPullRequestAssignees
}
if milestoneNumber > 0 {
params["milestone"] = milestoneNumber
}
if len(params) > 0 {
err = client.UpdateIssue(baseProject, pr.Number, params)
utils.Check(err)
}
if len(flagPullRequestReviewers) > 0 {
userReviewers := []string{}
teamReviewers := []string{}
for _, reviewer := range flagPullRequestReviewers {
if strings.Contains(reviewer, "/") {
teamName := strings.SplitN(reviewer, "/", 2)[1]
if !pr.HasRequestedTeam(teamName) {
teamReviewers = append(teamReviewers, teamName)
}
} else if !pr.HasRequestedReviewer(reviewer) {
userReviewers = append(userReviewers, reviewer)
}
}
if len(userReviewers) > 0 || len(teamReviewers) > 0 {
err = client.RequestReview(baseProject, pr.Number, map[string]interface{}{
"reviewers": userReviewers,
"team_reviewers": teamReviewers,
})
utils.Check(err)
}
}
2013-07-05 22:10:24 +04:00
}
2013-07-06 00:45:22 +04:00
if flagPullRequestIssue != "" {
ui.Errorln("Warning: Issue to pull request conversion is deprecated and might not work in the future.")
}
args.NoForward()
printBrowseOrCopy(args, pullRequestURL, flagPullRequestBrowse, flagPullRequestCopy)
2013-07-05 22:10:24 +04:00
}
2013-06-30 20:11:25 +04:00
2013-12-08 12:33:48 +04:00
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 ""
}
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)
}