2013-05-29 22:58:46 +04:00
|
|
|
package commands
|
2013-04-11 19:33:43 +04:00
|
|
|
|
|
|
|
import (
|
2013-04-28 21:45:00 +04:00
|
|
|
"bufio"
|
2013-04-29 00:41:45 +04:00
|
|
|
"fmt"
|
2013-05-29 22:58:46 +04:00
|
|
|
"github.com/jingweno/gh/cmd"
|
|
|
|
"github.com/jingweno/gh/git"
|
|
|
|
"github.com/jingweno/gh/github"
|
|
|
|
"github.com/jingweno/gh/utils"
|
2013-04-24 14:00:45 +04:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"regexp"
|
2013-04-28 21:45:00 +04:00
|
|
|
"strings"
|
2013-04-11 19:33:43 +04:00
|
|
|
)
|
|
|
|
|
2013-06-30 00:54:40 +04:00
|
|
|
var cmdPullRequest = &Command{
|
|
|
|
Run: pullRequest,
|
2013-06-30 20:05:26 +04:00
|
|
|
Usage: "pull-request [-f] [-i ISSUE] [-b BASE] [-d HEAD] [TITLE]",
|
2013-04-11 19:33:43 +04:00
|
|
|
Short: "Open a pull request on GitHub",
|
|
|
|
Long: `Opens a pull request on GitHub for the project that the "origin" remote
|
|
|
|
points to. The default head of the pull request is the current branch.
|
|
|
|
Both base and head of the pull request can be explicitly given in one of
|
|
|
|
the following formats: "branch", "owner:branch", "owner/repo:branch".
|
|
|
|
This command will abort operation if it detects that the current topic
|
|
|
|
branch has local commits that are not yet pushed to its upstream branch
|
|
|
|
on the remote. To skip this check, use -f.
|
|
|
|
|
|
|
|
If TITLE is omitted, a text editor will open in which title and body of
|
|
|
|
the pull request can be entered in the same manner as git commit message.
|
|
|
|
|
|
|
|
If instead of normal TITLE an issue number is given with -i, the pull
|
|
|
|
request will be attached to an existing GitHub issue. Alternatively, instead
|
|
|
|
of title you can paste a full URL to an issue on GitHub.
|
|
|
|
`,
|
|
|
|
}
|
|
|
|
|
2013-06-02 06:09:17 +04:00
|
|
|
var flagPullRequestBase, flagPullRequestHead, flagPullRequestIssue string
|
2013-04-11 19:33:43 +04:00
|
|
|
|
|
|
|
func init() {
|
2013-06-30 00:54:40 +04:00
|
|
|
cmdPullRequest.Flag.StringVar(&flagPullRequestBase, "b", "master", "BASE")
|
|
|
|
cmdPullRequest.Flag.StringVar(&flagPullRequestHead, "d", "", "HEAD")
|
|
|
|
cmdPullRequest.Flag.StringVar(&flagPullRequestIssue, "i", "", "ISSUE")
|
2013-04-11 19:33:43 +04:00
|
|
|
}
|
|
|
|
|
2013-07-06 00:45:22 +04:00
|
|
|
/*
|
|
|
|
# while on a topic branch called "feature":
|
|
|
|
$ gh pull-request
|
|
|
|
[ opens text editor to edit title & body for the request ]
|
|
|
|
[ opened pull request on GitHub for "YOUR_USER:feature" ]
|
|
|
|
|
|
|
|
# explicit pull base & head:
|
|
|
|
$ gh pull-request -b jingweno:master -h jingweno:feature
|
|
|
|
|
|
|
|
$ gh pull-request -i 123
|
|
|
|
[ attached pull request to issue #123 ]
|
|
|
|
*/
|
2013-06-30 00:54:40 +04:00
|
|
|
func pullRequest(cmd *Command, args *Args) {
|
2013-07-05 22:10:24 +04:00
|
|
|
var (
|
|
|
|
title, body string
|
|
|
|
err error
|
|
|
|
)
|
2013-07-02 22:56:45 +04:00
|
|
|
if args.ParamsSize() == 1 {
|
2013-07-05 22:10:24 +04:00
|
|
|
title = args.RemoveParam(0)
|
2013-06-02 05:16:18 +04:00
|
|
|
}
|
|
|
|
|
2013-06-02 06:40:58 +04:00
|
|
|
gh := github.New()
|
2013-06-02 17:31:03 +04:00
|
|
|
repo := gh.Project.LocalRepoWith(flagPullRequestBase, flagPullRequestHead)
|
2013-06-02 06:09:17 +04:00
|
|
|
if title == "" && flagPullRequestIssue == "" {
|
2013-07-05 22:10:24 +04:00
|
|
|
title, body, err = writePullRequestTitleAndBody(repo)
|
|
|
|
}
|
2013-04-28 21:45:00 +04:00
|
|
|
|
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
|
|
|
|
2013-07-06 00:45:22 +04:00
|
|
|
var pullRequestURL string
|
2013-07-05 22:10:24 +04:00
|
|
|
if args.Noop {
|
2013-07-06 00:45:22 +04:00
|
|
|
args.Before(fmt.Sprintf("Would request a pull request to %s from %s", repo.FullBase(), repo.FullHead()), "")
|
|
|
|
pullRequestURL = "PULL_REQUEST_URL"
|
2013-07-05 22:10:24 +04:00
|
|
|
} else {
|
|
|
|
if title != "" {
|
|
|
|
pullRequestURL, err = gh.CreatePullRequest(repo.Base, repo.Head, title, body)
|
|
|
|
}
|
2013-06-02 05:16:18 +04:00
|
|
|
utils.Check(err)
|
2013-04-28 21:45:00 +04:00
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
if flagPullRequestIssue != "" {
|
|
|
|
pullRequestURL, err = gh.CreatePullRequestForIssue(repo.Base, repo.Head, flagPullRequestIssue)
|
|
|
|
}
|
2013-06-02 05:16:18 +04:00
|
|
|
utils.Check(err)
|
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
}
|
2013-07-06 00:45:22 +04:00
|
|
|
|
|
|
|
args.Replace("echo", "", pullRequestURL)
|
2013-07-05 22:10:24 +04:00
|
|
|
}
|
2013-06-30 20:11:25 +04:00
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
func writePullRequestTitleAndBody(repo *github.Repo) (title, body string, err error) {
|
|
|
|
messageFile, err := git.PullReqMsgFile()
|
|
|
|
if err != nil {
|
|
|
|
return
|
2013-06-02 05:16:18 +04:00
|
|
|
}
|
2013-05-24 09:41:06 +04:00
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
err = writePullRequestChanges(repo, messageFile)
|
|
|
|
if err != nil {
|
|
|
|
return
|
2013-04-28 21:45:00 +04:00
|
|
|
}
|
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
editorPath, err := git.EditorPath()
|
|
|
|
if err != nil {
|
|
|
|
return
|
2013-06-09 03:30:01 +04:00
|
|
|
}
|
2013-07-05 22:10:24 +04:00
|
|
|
|
|
|
|
err = editTitleAndBody(editorPath, messageFile)
|
|
|
|
if err != nil {
|
|
|
|
return
|
2013-06-09 03:30:01 +04:00
|
|
|
}
|
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
title, body, err = readTitleAndBody(messageFile)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2013-04-29 06:21:16 +04:00
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
err = os.Remove(messageFile)
|
2013-06-29 01:21:48 +04:00
|
|
|
|
2013-07-05 22:10:24 +04:00
|
|
|
return
|
2013-04-28 21:45:00 +04:00
|
|
|
}
|
|
|
|
|
2013-06-02 06:40:58 +04:00
|
|
|
func writePullRequestChanges(repo *github.Repo, messageFile string) error {
|
2013-04-29 00:41:45 +04:00
|
|
|
message := `
|
|
|
|
# Requesting a pull to %s from %s
|
|
|
|
#
|
2013-06-13 06:25:06 +04:00
|
|
|
# Write a message for this pull request. The first block
|
2013-05-24 22:54:01 +04:00
|
|
|
# of the text is the title and the rest is description.%s
|
|
|
|
`
|
|
|
|
startRegexp := regexp.MustCompilePOSIX("^")
|
|
|
|
endRegexp := regexp.MustCompilePOSIX(" +$")
|
|
|
|
|
2013-05-28 22:05:10 +04:00
|
|
|
commitLogs, _ := git.Log(repo.Base, repo.Head)
|
2013-05-24 22:54:01 +04:00
|
|
|
var changesMsg string
|
|
|
|
if len(commitLogs) > 0 {
|
|
|
|
commitLogs = strings.TrimSpace(commitLogs)
|
|
|
|
commitLogs = startRegexp.ReplaceAllString(commitLogs, "# ")
|
|
|
|
commitLogs = endRegexp.ReplaceAllString(commitLogs, "")
|
|
|
|
changesMsg = `
|
2013-04-29 00:41:45 +04:00
|
|
|
#
|
|
|
|
# Changes:
|
|
|
|
#
|
2013-05-30 05:19:14 +04:00
|
|
|
%s`
|
2013-05-24 22:54:01 +04:00
|
|
|
changesMsg = fmt.Sprintf(changesMsg, commitLogs)
|
|
|
|
}
|
2013-04-29 00:41:45 +04:00
|
|
|
|
2013-05-24 22:54:01 +04:00
|
|
|
message = fmt.Sprintf(message, repo.FullBase(), repo.FullHead(), changesMsg)
|
2013-04-29 05:32:01 +04:00
|
|
|
|
|
|
|
return ioutil.WriteFile(messageFile, []byte(message), 0644)
|
|
|
|
}
|
|
|
|
|
2013-05-28 23:42:11 +04:00
|
|
|
func editTitleAndBody(editorPath, messageFile string) error {
|
2013-05-29 22:58:46 +04:00
|
|
|
editCmd := cmd.New(editorPath)
|
2013-05-28 21:13:55 +04:00
|
|
|
r := regexp.MustCompile("[mg]?vi[m]$")
|
|
|
|
if r.MatchString(editorPath) {
|
2013-04-29 05:47:08 +04:00
|
|
|
editCmd.WithArg("-c")
|
2013-05-28 22:07:08 +04:00
|
|
|
editCmd.WithArg("set ft=gitcommit tw=0 wrap lbr")
|
2013-04-24 14:00:45 +04:00
|
|
|
}
|
2013-04-29 05:47:08 +04:00
|
|
|
editCmd.WithArg(messageFile)
|
2013-04-24 14:00:45 +04:00
|
|
|
|
2013-05-28 23:42:11 +04:00
|
|
|
return editCmd.Exec()
|
2013-04-24 14:00:45 +04:00
|
|
|
}
|
|
|
|
|
2013-05-28 23:42:11 +04:00
|
|
|
func readTitleAndBody(messageFile string) (title, body string, err error) {
|
2013-04-28 21:45:00 +04:00
|
|
|
f, err := os.Open(messageFile)
|
|
|
|
defer f.Close()
|
2013-04-22 07:36:21 +04:00
|
|
|
if err != nil {
|
2013-04-28 21:45:00 +04:00
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
reader := bufio.NewReader(f)
|
2013-04-29 05:47:08 +04:00
|
|
|
|
2013-05-28 23:42:11 +04:00
|
|
|
return readTitleAndBodyFrom(reader)
|
2013-04-28 21:45:00 +04:00
|
|
|
}
|
|
|
|
|
2013-05-28 23:42:11 +04:00
|
|
|
func readTitleAndBodyFrom(reader *bufio.Reader) (title, body string, err error) {
|
2013-04-28 21:45:00 +04:00
|
|
|
r := regexp.MustCompile("\\S")
|
|
|
|
var titleParts, bodyParts []string
|
|
|
|
|
|
|
|
line, err := readln(reader)
|
|
|
|
for err == nil {
|
|
|
|
if strings.HasPrefix(line, "#") {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if len(bodyParts) == 0 && r.MatchString(line) {
|
|
|
|
titleParts = append(titleParts, line)
|
|
|
|
} else {
|
|
|
|
bodyParts = append(bodyParts, line)
|
|
|
|
}
|
|
|
|
line, err = readln(reader)
|
|
|
|
}
|
|
|
|
|
|
|
|
title = strings.Join(titleParts, " ")
|
|
|
|
title = strings.TrimSpace(title)
|
|
|
|
|
|
|
|
body = strings.Join(bodyParts, "\n")
|
|
|
|
body = strings.TrimSpace(body)
|
|
|
|
|
|
|
|
return title, body, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func readln(r *bufio.Reader) (string, error) {
|
|
|
|
var (
|
2013-06-11 08:49:45 +04:00
|
|
|
isPrefix = true
|
|
|
|
err error
|
2013-04-28 21:45:00 +04:00
|
|
|
line, ln []byte
|
|
|
|
)
|
|
|
|
for isPrefix && err == nil {
|
|
|
|
line, isPrefix, err = r.ReadLine()
|
|
|
|
ln = append(ln, line...)
|
2013-04-22 07:36:21 +04:00
|
|
|
}
|
2013-04-29 05:47:08 +04:00
|
|
|
|
2013-04-28 21:45:00 +04:00
|
|
|
return string(ln), err
|
2013-04-11 19:33:43 +04:00
|
|
|
}
|