package commands import ( "fmt" "regexp" "strconv" "strings" "github.com/github/hub/git" "github.com/github/hub/github" "github.com/github/hub/utils" ) 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 head branch in "[OWNER:]BRANCH" format. Defaults to the current branch. -a, --assign A comma-separated list of GitHub handles to assign 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. ## See also: hub(1), hub-merge(1), hub-checkout(1) `, } var ( flagPullRequestBase, flagPullRequestHead, flagPullRequestIssue, flagPullRequestMessage, flagPullRequestFile string flagPullRequestBrowse, flagPullRequestEdit, flagPullRequestForce bool flagPullRequestMilestone uint64 flagPullRequestAssignees, flagPullRequestLabels listFlag ) 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(&flagPullRequestEdit, "edit", "e", false, "EDIT") cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE") cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE") cmdPullRequest.Flag.VarP(&flagPullRequestAssignees, "assign", "a", "USERS") cmdPullRequest.Flag.Uint64VarP(&flagPullRequestMilestone, "milestone", "M", 0, "MILESTONE") cmdPullRequest.Flag.VarP(&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)) } client := github.NewClientWithHost(host) 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) 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) } } 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 := createPullRequestMessage(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 flagPullRequestFile != "" && flagPullRequestEdit { editor, err = github.NewEditor("PULLREQ", "pull request", title + "\n\n" + body) 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 { params := map[string]interface{}{ "base": base, "head": fullHead, } if title != "" { params["title"] = title if body != "" { params["body"] = body } } else { issueNum, _ := strconv.Atoi(flagPullRequestIssue) params["issue"] = issueNum } pr, err := client.CreatePullRequest(baseProject, params) if err == nil && editor != nil { defer editor.DeleteFile() } utils.Check(err) pullRequestURL = pr.HtmlUrl if len(flagPullRequestAssignees) > 0 || flagPullRequestMilestone > 0 || len(flagPullRequestLabels) > 0 { params := map[string]interface{}{ "labels": flagPullRequestLabels, "assignees": flagPullRequestAssignees, } if flagPullRequestMilestone > 0 { params["milestone"] = flagPullRequestMilestone } 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 createPullRequestMessage(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 } } if template := github.GetPullRequestTemplate(); template != "" { if defaultMsg == "" { defaultMsg = "\n\n" + template } else { parts := strings.SplitN(defaultMsg, "\n\n", 2) defaultMsg = parts[0] + "\n\n" + template if len(parts) > 1 && parts[1] != "" { defaultMsg = defaultMsg + "\n\n" + parts[1] } } } 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 "" }