package commands
import (
"fmt"
"regexp"
"strings"
"github.com/github/hub/git"
"github.com/github/hub/github"
"github.com/github/hub/utils"
"github.com/octokit/go-octokit/octokit"
)
var cmdPullRequest = &Command{
Run: pullRequest,
Usage: `
pull-request [-fo] [-b ] [-h
] [-a ] [-M ] [-l ]
pull-request -m
pull-request -F
pull-request -i
`,
Long: `Create a GitHub pull request.
## Options:
-f, --force
Skip the check for unpushed commits.
-m, --message
Use the first line of as pull request title, and the rest as pull
request description.
-F, --file
Read the pull request title and description from .
-i, --issue ,
(Deprecated) Convert to a pull request.
-o, --browse
Open the new pull request in a web browser.
-b, --base
The base branch in "[OWNER:]BRANCH" format. Defaults to the default branch
(usually "master").
-h, --head
The base branch in "[OWNER:]BRANCH" format. Defaults to the current branch.
-a, --assign
Assign GitHub to this pull request.
-M, --milestone
Add this pull request to a GitHub milestone with id .
-l, --labels
Add a comma-separated list of labels to this pull request.
`,
}
var (
flagPullRequestBase,
flagPullRequestHead,
flagPullRequestIssue,
flagPullRequestMessage,
flagPullRequestAssignee,
flagPullRequestLabels,
flagPullRequestFile string
flagPullRequestBrowse,
flagPullRequestForce bool
flagPullRequestMilestone uint64
)
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.StringVarP(&flagPullRequestMessage, "message", "m", "", "MESSAGE")
cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE")
cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE")
cmdPullRequest.Flag.StringVarP(&flagPullRequestAssignee, "assign", "a", "", "USER")
cmdPullRequest.Flag.Uint64VarP(&flagPullRequestMilestone, "milestone", "M", 0, "MILESTONE")
cmdPullRequest.Flag.StringVarP(&flagPullRequestLabels, "labels", "l", "", "LABELS")
CmdRunner.Use(cmdPullRequest)
}
func pullRequest(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
currentBranch, err := localRepo.CurrentBranch()
utils.Check(err)
baseProject, err := localRepo.MainProject()
utils.Check(err)
host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
if err != nil {
utils.Check(github.FormatError("creating pull request", err))
}
trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false)
utils.Check(err)
var (
base, head string
force bool
)
force = flagPullRequestForce
if flagPullRequestBase != "" {
baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
}
if flagPullRequestHead != "" {
headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
}
if args.ParamsSize() == 1 {
arg := args.RemoveParam(0)
flagPullRequestIssue = parsePullRequestIssueNumber(arg)
}
if base == "" {
masterBranch := localRepo.MasterBranch()
base = masterBranch.ShortName()
}
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() {
e := fmt.Errorf(`Aborted: head branch is the same as base ("%s")`, base)
e = fmt.Errorf("%s\n(use `-h ` to specify an explicit pull request head)", e)
utils.Check(e)
}
}
}
if head == "" {
if trackedBranch == nil {
head = currentBranch.ShortName()
} else {
head = trackedBranch.ShortName()
}
}
title, body, err := getTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile)
utils.Check(err)
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)
}
}
var editor *github.Editor
if title == "" && flagPullRequestIssue == "" {
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)
}
message, err := pullRequestChangesMessage(baseTracking, headTracking, fullBase, fullHead)
utils.Check(err)
editor, err = github.NewEditor("PULLREQ", "pull request", message)
utils.Check(err)
title, body, err = editor.EditTitleAndBody()
utils.Check(err)
}
if title == "" && flagPullRequestIssue == "" {
utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
}
var pullRequestURL string
if args.Noop {
args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
pullRequestURL = "PULL_REQUEST_URL"
} else {
var (
pr *octokit.PullRequest
err error
)
client := github.NewClientWithHost(host)
if title != "" {
pr, err = client.CreatePullRequest(baseProject, base, fullHead, title, body)
} else if flagPullRequestIssue != "" {
pr, err = client.CreatePullRequestForIssue(baseProject, base, fullHead, flagPullRequestIssue)
}
if err == nil && editor != nil {
defer editor.DeleteFile()
}
utils.Check(err)
pullRequestURL = pr.HTMLURL
if flagPullRequestAssignee != "" || flagPullRequestMilestone > 0 ||
flagPullRequestLabels != "" {
labels := []string{}
for _, label := range strings.Split(flagPullRequestLabels, ",") {
if label != "" {
labels = append(labels, label)
}
}
params := octokit.IssueParams{
Assignee: flagPullRequestAssignee,
Milestone: flagPullRequestMilestone,
Labels: labels,
}
err = client.UpdateIssue(baseProject, pr.Number, params)
utils.Check(err)
}
}
if flagPullRequestBrowse {
launcher, err := utils.BrowserLauncher()
utils.Check(err)
args.Replace(launcher[0], "", launcher[1:]...)
args.AppendParams(pullRequestURL)
} else {
args.Replace("echo", "", pullRequestURL)
}
if flagPullRequestIssue != "" {
args.After("echo", "Warning: Issue to pull request conversion is deprecated and might not work in the future.")
}
}
func pullRequestChangesMessage(base, head, fullBase, fullHead string) (string, error) {
var (
defaultMsg string
commitLogs string
err error
)
commits, _ := git.RefList(base, head)
if len(commits) == 1 {
defaultMsg, err = git.Show(commits[0])
if err != nil {
return "", err
}
} else if len(commits) > 1 {
commitLogs, err = git.Log(base, head)
if err != nil {
return "", err
}
}
cs := git.CommentChar()
return renderPullRequestTpl(defaultMsg, cs, fullBase, fullHead, commitLogs)
}
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 ""
}