github.com/echohead/hub@v2.2.1+incompatible/commands/pull_request.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/github/hub/git"
     9  	"github.com/github/hub/github"
    10  	"github.com/github/hub/utils"
    11  	"github.com/github/hub/Godeps/_workspace/src/github.com/octokit/go-octokit/octokit"
    12  )
    13  
    14  var cmdPullRequest = &Command{
    15  	Run:   pullRequest,
    16  	Usage: "pull-request [-f] [-m <MESSAGE>|-F <FILE>|-i <ISSUE>|<ISSUE-URL>] [-o] [-b <BASE>] [-h <HEAD>] ",
    17  	Short: "Open a pull request on GitHub",
    18  	Long: `Opens a pull request on GitHub for the project that the "origin" remote
    19  points to. The default head of the pull request is the current branch.
    20  Both base and head of the pull request can be explicitly given in one of
    21  the following formats: "branch", "owner:branch", "owner/repo:branch".
    22  This command will abort operation if it detects that the current topic
    23  branch has local commits that are not yet pushed to its upstream branch
    24  on the remote. To skip this check, use "-f".
    25  
    26  Without <MESSAGE> or <FILE>, a text editor will open in which title and body
    27  of the pull request can be entered in the same manner as git commit message.
    28  Pull request message can also be passed via stdin with "-F -".
    29  
    30  If instead of normal <TITLE> an issue number is given with "-i", the pull
    31  request will be attached to an existing GitHub issue. Alternatively, instead
    32  of title you can paste a full URL to an issue on GitHub.
    33  `,
    34  }
    35  
    36  var (
    37  	flagPullRequestBase,
    38  	flagPullRequestHead,
    39  	flagPullRequestIssue,
    40  	flagPullRequestMessage,
    41  	flagPullRequestFile string
    42  	flagPullRequestBrowse,
    43  	flagPullRequestForce bool
    44  )
    45  
    46  func init() {
    47  	cmdPullRequest.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE")
    48  	cmdPullRequest.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD")
    49  	cmdPullRequest.Flag.StringVarP(&flagPullRequestIssue, "issue", "i", "", "ISSUE")
    50  	cmdPullRequest.Flag.BoolVarP(&flagPullRequestBrowse, "browse", "o", false, "BROWSE")
    51  	cmdPullRequest.Flag.StringVarP(&flagPullRequestMessage, "message", "m", "", "MESSAGE")
    52  	cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE")
    53  	cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE")
    54  
    55  	CmdRunner.Use(cmdPullRequest)
    56  }
    57  
    58  /*
    59    # while on a topic branch called "feature":
    60    $ gh pull-request
    61    [ opens text editor to edit title & body for the request ]
    62    [ opened pull request on GitHub for "YOUR_USER:feature" ]
    63  
    64    # explicit pull base & head:
    65    $ gh pull-request -b jingweno:master -h jingweno:feature
    66  
    67    $ gh pull-request -m "title\n\nbody"
    68    [ create pull request with title & body  ]
    69  
    70    $ gh pull-request -i 123
    71    [ attached pull request to issue #123 ]
    72  
    73    $ gh pull-request https://github.com/jingweno/gh/pull/123
    74    [ attached pull request to issue #123 ]
    75  
    76    $ gh pull-request -F FILE
    77    [ create pull request with title & body from FILE ]
    78  */
    79  func pullRequest(cmd *Command, args *Args) {
    80  	localRepo, err := github.LocalRepo()
    81  	utils.Check(err)
    82  
    83  	currentBranch, err := localRepo.CurrentBranch()
    84  	utils.Check(err)
    85  
    86  	baseProject, err := localRepo.MainProject()
    87  	utils.Check(err)
    88  
    89  	host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
    90  	if err != nil {
    91  		utils.Check(github.FormatError("creating pull request", err))
    92  	}
    93  
    94  	trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false)
    95  	utils.Check(err)
    96  
    97  	var (
    98  		base, head string
    99  		force      bool
   100  	)
   101  
   102  	force = flagPullRequestForce
   103  
   104  	if flagPullRequestBase != "" {
   105  		baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
   106  	}
   107  
   108  	if flagPullRequestHead != "" {
   109  		headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
   110  	}
   111  
   112  	if args.ParamsSize() == 1 {
   113  		arg := args.RemoveParam(0)
   114  		flagPullRequestIssue = parsePullRequestIssueNumber(arg)
   115  	}
   116  
   117  	if base == "" {
   118  		masterBranch := localRepo.MasterBranch()
   119  		base = masterBranch.ShortName()
   120  	}
   121  
   122  	if head == "" && trackedBranch != nil {
   123  		if !trackedBranch.IsRemote() {
   124  			// the current branch tracking another branch
   125  			// pretend there's no upstream at all
   126  			trackedBranch = nil
   127  		} else {
   128  			if baseProject.SameAs(headProject) && base == trackedBranch.ShortName() {
   129  				e := fmt.Errorf(`Aborted: head branch is the same as base ("%s")`, base)
   130  				e = fmt.Errorf("%s\n(use `-h <branch>` to specify an explicit pull request head)", e)
   131  				utils.Check(e)
   132  			}
   133  		}
   134  	}
   135  
   136  	if head == "" {
   137  		if trackedBranch == nil {
   138  			head = currentBranch.ShortName()
   139  		} else {
   140  			head = trackedBranch.ShortName()
   141  		}
   142  	}
   143  
   144  	title, body, err := getTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile)
   145  	utils.Check(err)
   146  
   147  	fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
   148  	fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)
   149  
   150  	if !force && trackedBranch != nil {
   151  		remoteCommits, _ := git.RefList(trackedBranch.LongName(), "")
   152  		if len(remoteCommits) > 0 {
   153  			err = fmt.Errorf("Aborted: %d commits are not yet pushed to %s", len(remoteCommits), trackedBranch.LongName())
   154  			err = fmt.Errorf("%s\n(use `-f` to force submit a pull request anyway)", err)
   155  			utils.Check(err)
   156  		}
   157  	}
   158  
   159  	var editor *github.Editor
   160  	if title == "" && flagPullRequestIssue == "" {
   161  		baseTracking := base
   162  		headTracking := head
   163  
   164  		remote := gitRemoteForProject(baseProject)
   165  		if remote != nil {
   166  			baseTracking = fmt.Sprintf("%s/%s", remote.Name, base)
   167  		}
   168  		if remote == nil || !baseProject.SameAs(headProject) {
   169  			remote = gitRemoteForProject(headProject)
   170  		}
   171  		if remote != nil {
   172  			headTracking = fmt.Sprintf("%s/%s", remote.Name, head)
   173  		}
   174  
   175  		message, err := pullRequestChangesMessage(baseTracking, headTracking, fullBase, fullHead)
   176  		utils.Check(err)
   177  
   178  		editor, err = github.NewEditor("PULLREQ", "pull request", message)
   179  		utils.Check(err)
   180  
   181  		title, body, err = editor.EditTitleAndBody()
   182  		utils.Check(err)
   183  	}
   184  
   185  	if title == "" && flagPullRequestIssue == "" {
   186  		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
   187  	}
   188  
   189  	var pullRequestURL string
   190  	if args.Noop {
   191  		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
   192  		pullRequestURL = "PULL_REQUEST_URL"
   193  	} else {
   194  		var (
   195  			pr  *octokit.PullRequest
   196  			err error
   197  		)
   198  
   199  		client := github.NewClientWithHost(host)
   200  		if title != "" {
   201  			pr, err = client.CreatePullRequest(baseProject, base, fullHead, title, body)
   202  		} else if flagPullRequestIssue != "" {
   203  			pr, err = client.CreatePullRequestForIssue(baseProject, base, fullHead, flagPullRequestIssue)
   204  		}
   205  
   206  		if err == nil && editor != nil {
   207  			defer editor.DeleteFile()
   208  		}
   209  
   210  		utils.Check(err)
   211  		pullRequestURL = pr.HTMLURL
   212  	}
   213  
   214  	if flagPullRequestBrowse {
   215  		launcher, err := utils.BrowserLauncher()
   216  		utils.Check(err)
   217  		args.Replace(launcher[0], "", launcher[1:]...)
   218  		args.AppendParams(pullRequestURL)
   219  	} else {
   220  		args.Replace("echo", "", pullRequestURL)
   221  	}
   222  
   223  	if flagPullRequestIssue != "" {
   224  		args.After("echo", "Warning: Issue to pull request conversion is deprecated and might not work in the future.")
   225  	}
   226  }
   227  
   228  func pullRequestChangesMessage(base, head, fullBase, fullHead string) (string, error) {
   229  	var (
   230  		defaultMsg string
   231  		commitLogs string
   232  		err        error
   233  	)
   234  
   235  	commits, _ := git.RefList(base, head)
   236  	if len(commits) == 1 {
   237  		defaultMsg, err = git.Show(commits[0])
   238  		if err != nil {
   239  			return "", err
   240  		}
   241  	} else if len(commits) > 1 {
   242  		commitLogs, err = git.Log(base, head)
   243  		if err != nil {
   244  			return "", err
   245  		}
   246  	}
   247  
   248  	cs := git.CommentChar()
   249  
   250  	return renderPullRequestTpl(defaultMsg, cs, fullBase, fullHead, commitLogs)
   251  }
   252  
   253  func parsePullRequestProject(context *github.Project, s string) (p *github.Project, ref string) {
   254  	p = context
   255  	ref = s
   256  
   257  	if strings.Contains(s, ":") {
   258  		split := strings.SplitN(s, ":", 2)
   259  		ref = split[1]
   260  		var name string
   261  		if !strings.Contains(split[0], "/") {
   262  			name = context.Name
   263  		}
   264  		p = github.NewProject(split[0], name, context.Host)
   265  	}
   266  
   267  	return
   268  }
   269  
   270  func parsePullRequestIssueNumber(url string) string {
   271  	u, e := github.ParseURL(url)
   272  	if e != nil {
   273  		return ""
   274  	}
   275  
   276  	r := regexp.MustCompile(`^issues\/(\d+)`)
   277  	p := u.ProjectPath()
   278  	if r.MatchString(p) {
   279  		return r.FindStringSubmatch(p)[1]
   280  	}
   281  
   282  	return ""
   283  }