hub/commands/pull_request.go

477 строки
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/utils"
2013-04-11 19:33:43 +04:00
)
var cmdPullRequest = &Command{
Run: pullRequest,
Usage: `
pull-request [-focpd] [-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>
`,
2019-01-21 23:54:34 +03:00
Long: `Create a GitHub Pull Request.
## Options:
-f, --force
Skip the check for unpushed commits.
-m, --message <MESSAGE>
The text up to the first blank line in <MESSAGE> is treated as the pull
request title, and the rest is used as pull request description in Markdown
format.
If multiple <MESSAGE> options are given, their values are concatenated as
separate paragraphs.
--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>
Convert <ISSUE> (referenced by its number) to a pull request.
2018-12-28 19:54:15 +03:00
You can only convert issues authored by you or that which you have admin
rights over. In most workflows it is not necessary to convert issues to
pull requests; you can simply reference the original issue in the body of
the new 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 the "[<OWNER>:]<BRANCH>" format. Defaults to the default
branch of the upstream repository (usually "master").
See the "CONVENTIONS" section of hub(1) for more information on how hub
selects the defaults in case of multiple git remotes.
-h, --head <HEAD>
The head branch in "[<OWNER>:]<BRANCH>" format. Defaults to the currently
checked out branch.
-r, --reviewer <USERS>
A comma-separated list of GitHub handles to request a review from.
-a, --assign <USERS>
2016-08-10 12:44:38 +03:00
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. Labels will be
2018-12-28 19:54:15 +03:00
created if they do not already exist.
-d, --draft
Create the pull request as a draft.
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:
2018-12-28 19:54:15 +03:00
* 'HUB_RETRY_TIMEOUT':
2016-10-22 13:28:32 +03:00
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
`,
}
func init() {
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, currentBranchErr := localRepo.CurrentBranch()
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, _ := localRepo.RemoteBranchAndProject(host.User, false)
if headProject == nil {
utils.Check(err)
}
var (
base, head string
)
2013-12-08 12:33:48 +04:00
2019-01-16 06:22:20 +03:00
if flagPullRequestBase := args.Flag.Value("--base"); flagPullRequestBase != "" {
2013-12-08 12:33:48 +04:00
baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
}
2019-01-16 06:22:20 +03:00
if flagPullRequestHead := args.Flag.Value("--head"); flagPullRequestHead != "" {
2013-12-08 12:33:48 +04:00
headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
}
baseRemote, _ := localRepo.RemoteForProject(baseProject)
if base == "" && baseRemote != nil {
base = localRepo.DefaultBranch(baseRemote).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 {
utils.Check(currentBranchErr)
2014-03-05 20:39:23 +04:00
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)
2019-01-16 06:22:20 +03:00
force := args.Flag.Bool("--force")
if !force && trackedBranch != nil {
remoteCommits, err := git.RefList(trackedBranch.LongName(), "")
if err == nil && 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 := baseRemote
if remote != nil {
baseTracking = fmt.Sprintf("%s/%s", remote.Name, base)
}
if remote == nil || !baseProject.SameAs(headProject) {
remote, _ = localRepo.RemoteForProject(headProject)
}
if remote != nil {
headTracking = fmt.Sprintf("%s/%s", remote.Name, head)
}
2019-01-16 06:22:20 +03:00
flagPullRequestPush := args.Flag.Bool("--push")
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))
2019-01-16 06:22:20 +03:00
flagPullRequestMessage := args.Flag.AllValues("--message")
flagPullRequestEdit := args.Flag.Bool("--edit")
flagPullRequestIssue := args.Flag.Value("--issue")
if !args.Flag.HasReceived("--issue") && args.ParamsSize() > 0 {
flagPullRequestIssue = parsePullRequestIssueNumber(args.GetParam(0))
}
if len(flagPullRequestMessage) > 0 {
2019-01-16 06:22:20 +03:00
messageBuilder.Message = strings.Join(flagPullRequestMessage, "\n\n")
messageBuilder.Edit = flagPullRequestEdit
2019-01-16 06:22:20 +03:00
} else if args.Flag.HasReceived("--file") {
messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file"))
utils.Check(err)
messageBuilder.Edit = flagPullRequestEdit
2019-01-16 06:22:20 +03:00
} else if args.Flag.Bool("--no-edit") {
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 := ""
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
2019-01-16 06:22:20 +03:00
if flagPullRequestMilestone := args.Flag.Value("--milestone"); 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,
}
if args.Flag.Bool("--draft") {
params["draft"] = true
2016-08-08 01:43:48 +03:00
}
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{}{}
2019-01-16 06:22:20 +03:00
flagPullRequestLabels := commaSeparated(args.Flag.AllValues("--labels"))
if len(flagPullRequestLabels) > 0 {
params["labels"] = flagPullRequestLabels
}
2019-01-16 06:22:20 +03:00
flagPullRequestAssignees := commaSeparated(args.Flag.AllValues("--assign"))
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)
}
2019-01-16 06:22:20 +03:00
flagPullRequestReviewers := commaSeparated(args.Flag.AllValues("--reviewer"))
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
args.NoForward()
2019-01-16 06:22:20 +03:00
printBrowseOrCopy(args, pullRequestURL, args.Flag.Bool("--browse"), args.Flag.Bool("--copy"))
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)
}
2019-01-16 06:22:20 +03:00
func commaSeparated(l []string) []string {
res := []string{}
for _, i := range l {
res = append(res, strings.Split(i, ",")...)
}
return res
}