github.com/pengwynn/gh@v1.0.1-0.20140118055701-14327ca3942e/commands/pull_request.go (about)

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