pull_request.go 7.5 KB
Newer Older
1
package commands
J
Jingwen Owen Ou 已提交
2 3

import (
J
Jingwen Owen Ou 已提交
4
	"fmt"
5 6 7
	"github.com/jingweno/gh/git"
	"github.com/jingweno/gh/github"
	"github.com/jingweno/gh/utils"
8
	"reflect"
J
Jingwen Owen Ou 已提交
9
	"regexp"
10
	"strings"
J
Jingwen Owen Ou 已提交
11 12
)

13 14
var cmdPullRequest = &Command{
	Run:   pullRequest,
J
Jingwen Owen Ou 已提交
15
	Usage: "pull-request [-f] [-m <MESSAGE>|-F <FILE>|-i <ISSUE>|<ISSUE-URL>] [-b <BASE>] [-h <HEAD>] ",
J
Jingwen Owen Ou 已提交
16 17 18 19 20 21 22
	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
J
Jingwen Owen Ou 已提交
23
on the remote. To skip this check, use "-f".
J
Jingwen Owen Ou 已提交
24

J
Jingwen Owen Ou 已提交
25 26 27
Without <MESSAGE> or <FILE>, 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.
Pull request message can also be passed via stdin with "-F -".
J
Jingwen Owen Ou 已提交
28

J
Jingwen Owen Ou 已提交
29
If instead of normal <TITLE> an issue number is given with "-i", the pull
J
Jingwen Owen Ou 已提交
30 31 32 33 34
request will be attached to an existing GitHub issue. Alternatively, instead
of title you can paste a full URL to an issue on GitHub.
`,
}

35 36 37 38 39 40 41 42
var (
	flagPullRequestBase,
	flagPullRequestHead,
	flagPullRequestIssue,
	flagPullRequestMessage,
	flagPullRequestFile string
	flagPullRequestForce bool
)
J
Jingwen Owen Ou 已提交
43 44

func init() {
45 46
	cmdPullRequest.Flag.StringVar(&flagPullRequestBase, "b", "", "BASE")
	cmdPullRequest.Flag.StringVar(&flagPullRequestHead, "h", "", "HEAD")
47
	cmdPullRequest.Flag.StringVar(&flagPullRequestIssue, "i", "", "ISSUE")
48
	cmdPullRequest.Flag.StringVar(&flagPullRequestMessage, "m", "", "MESSAGE")
49
	cmdPullRequest.Flag.BoolVar(&flagPullRequestForce, "f", false, "FORCE")
50
	cmdPullRequest.Flag.StringVar(&flagPullRequestFile, "F", "", "FILE")
51
	cmdPullRequest.Flag.StringVar(&flagPullRequestFile, "file", "", "FILE")
J
Jingwen Owen Ou 已提交
52 53
}

54 55 56 57 58 59 60 61 62
/*
  # 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

63 64 65
  $ gh pull-request -m "title\n\nbody"
  [ create pull request with title & body  ]

66 67
  $ gh pull-request -i 123
  [ attached pull request to issue #123 ]
68 69 70

  $ gh pull-request https://github.com/jingweno/gh/pull/123
  [ attached pull request to issue #123 ]
J
Jingwen Owen Ou 已提交
71 72 73

  $ gh pull-request -F FILE
  [ create pull request with title & body from FILE ]
74
*/
75
func pullRequest(cmd *Command, args *Args) {
76 77 78
	localRepo := github.LocalRepo()

	currentBranch, err := localRepo.CurrentBranch()
J
Jingwen Owen Ou 已提交
79
	utils.Check(err)
80 81

	baseProject, err := localRepo.MainProject()
J
Jingwen Owen Ou 已提交
82
	utils.Check(err)
83

J
Jingwen Owen Ou 已提交
84 85 86
	client := github.NewClient(baseProject.Host)

	trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(client.Credentials.User)
87 88
	utils.Check(err)

89
	var (
J
Jingwen Owen Ou 已提交
90 91
		base, head string
		force      bool
92
	)
J
Jingwen Owen Ou 已提交
93

94
	force = flagPullRequestForce
J
Jingwen Owen Ou 已提交
95

96
	if flagPullRequestBase != "" {
J
Jingwen Owen Ou 已提交
97
		baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
98
	}
99

100
	if flagPullRequestHead != "" {
J
Jingwen Owen Ou 已提交
101
		headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
102
	}
103

J
Jingwen Owen Ou 已提交
104
	if args.ParamsSize() == 1 {
105
		arg := args.RemoveParam(0)
J
Jingwen Owen Ou 已提交
106
		flagPullRequestIssue = parsePullRequestIssueNumber(arg)
107 108
	}

109
	if base == "" {
J
Jingwen Owen Ou 已提交
110
		masterBranch := localRepo.MasterBranch()
111 112 113 114
		base = masterBranch.ShortName()
	}

	if head == "" {
J
Jingwen Owen Ou 已提交
115 116 117 118 119
		if !trackedBranch.IsRemote() {
			// the current branch tracking another branch
			// pretend there's no upstream at all
			trackedBranch = nil
		} else {
J
Jingwen Owen Ou 已提交
120 121 122 123
			if reflect.DeepEqual(baseProject, headProject) && base == trackedBranch.ShortName() {
				e := fmt.Errorf(`Aborted: head branch is the same as base ("%s")`, base)
				e = fmt.Errorf("%s\n(use `-h <branch>` to specify an explicit pull request head)", e)
				utils.Check(e)
124 125 126
			}
		}

J
Jingwen Owen Ou 已提交
127
		if trackedBranch == nil {
128
			head = currentBranch.ShortName()
J
Jingwen Owen Ou 已提交
129 130
		} else {
			head = trackedBranch.ShortName()
131 132 133
		}
	}

134
	title, body, err := github.GetTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile)
135
	utils.Check(err)
136

137 138 139
	fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
	fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)

140 141 142 143 144 145 146
	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)
		}
147 148
	}

J
Jingwen Owen Ou 已提交
149
	if title == "" && flagPullRequestIssue == "" {
150
		commits, _ := git.RefList(base, head)
151
		title, body, err = writePullRequestTitleAndBody(base, head, fullBase, fullHead, commits)
J
Jingwen Owen Ou 已提交
152
		utils.Check(err)
J
Jingwen Owen Ou 已提交
153
	}
154

J
Jingwen Owen Ou 已提交
155 156 157
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
158

159
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
160
	if args.Noop {
161
		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
162
		pullRequestURL = "PULL_REQUEST_URL"
J
Jingwen Owen Ou 已提交
163 164
	} else {
		if title != "" {
J
Jingwen Owen Ou 已提交
165
			pr, err := client.CreatePullRequest(baseProject, base, fullHead, title, body)
J
Jingwen Owen Ou 已提交
166 167
			utils.Check(err)
			pullRequestURL = pr.HTMLURL
J
Jingwen Owen Ou 已提交
168
		}
169

J
Jingwen Owen Ou 已提交
170
		if flagPullRequestIssue != "" {
J
Jingwen Owen Ou 已提交
171
			pr, err := client.CreatePullRequestForIssue(baseProject, base, fullHead, flagPullRequestIssue)
J
Jingwen Owen Ou 已提交
172 173
			utils.Check(err)
			pullRequestURL = pr.HTMLURL
J
Jingwen Owen Ou 已提交
174 175
		}
	}
176 177

	args.Replace("echo", "", pullRequestURL)
J
Jingwen Owen Ou 已提交
178 179 180
	if flagPullRequestIssue != "" {
		args.After("echo", "Warning: Issue to pull request conversion is deprecated and might not work in the future.")
	}
J
Jingwen Owen Ou 已提交
181
}
J
Jingwen Owen Ou 已提交
182

183
func writePullRequestTitleAndBody(base, head, fullBase, fullHead string, commits []string) (title, body string, err error) {
184 185
	message, err := pullRequestChangesMessage(base, head, fullBase, fullHead, commits)
	utils.Check(err)
J
Jingwen Owen Ou 已提交
186

187
	return github.GetTitleAndBodyFromEditor("PULLREQ", message)
188 189
}

190
func pullRequestChangesMessage(base, head, fullBase, fullHead string, commits []string) (string, error) {
191 192
	var defaultMsg, commitSummary string
	if len(commits) == 1 {
J
Jingwen Owen Ou 已提交
193
		msg, err := git.Show(commits[0])
194
		if err != nil {
195
			return "", err
196
		}
J
Jingwen Owen Ou 已提交
197
		defaultMsg = fmt.Sprintf("%s\n", msg)
198
	} else if len(commits) > 1 {
199
		commitLogs, err := git.Log(base, head)
200
		if err != nil {
201
			return "", err
202 203 204 205 206 207 208 209 210 211
		}

		if len(commitLogs) > 0 {
			startRegexp := regexp.MustCompilePOSIX("^")
			endRegexp := regexp.MustCompilePOSIX(" +$")

			commitLogs = strings.TrimSpace(commitLogs)
			commitLogs = startRegexp.ReplaceAllString(commitLogs, "# ")
			commitLogs = endRegexp.ReplaceAllString(commitLogs, "")
			commitSummary = `
J
Jingwen Owen Ou 已提交
212 213 214
#
# Changes:
#
J
Jingwen Owen Ou 已提交
215
%s`
216 217
			commitSummary = fmt.Sprintf(commitSummary, commitLogs)
		}
218
	}
J
Jingwen Owen Ou 已提交
219

220 221 222 223 224 225
	message := `%s
# Requesting a pull to %s from %s
#
# Write a message for this pull request. The first block
# of the text is the title and the rest is description.%s
`
226
	message = fmt.Sprintf(message, defaultMsg, fullBase, fullHead, commitSummary)
J
Jingwen Owen Ou 已提交
227

228
	return message, nil
229
}
J
Jingwen Owen Ou 已提交
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

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 ""
}