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

import (
J
Jingwen Owen Ou 已提交
4
	"bytes"
J
Jingwen Owen Ou 已提交
5
	"fmt"
6
	"reflect"
J
Jingwen Owen Ou 已提交
7
	"regexp"
8
	"strings"
J
Jingwen Owen Ou 已提交
9
	"text/template"
J
Jingwen Owen Ou 已提交
10 11 12 13

	"github.com/github/hub/git"
	"github.com/github/hub/github"
	"github.com/github/hub/utils"
J
Jingwen Owen Ou 已提交
14
	"github.com/octokit/go-octokit/octokit"
J
Jingwen Owen Ou 已提交
15 16
)

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

J
Jingwen Owen Ou 已提交
29 30 31
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 已提交
32

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

39 40 41 42 43 44
var (
	flagPullRequestBase,
	flagPullRequestHead,
	flagPullRequestIssue,
	flagPullRequestMessage,
	flagPullRequestFile string
45
	flagPullRequestBrowse,
46 47
	flagPullRequestForce bool
)
J
Jingwen Owen Ou 已提交
48 49

func init() {
50 51 52
	cmdPullRequest.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestIssue, "issue", "i", "", "ISSUE")
53
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestBrowse, "browse", "o", false, "BROWSE")
54 55 56
	cmdPullRequest.Flag.StringVarP(&flagPullRequestMessage, "message", "m", "", "MESSAGE")
	cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE")
	cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE")
57 58

	CmdRunner.Use(cmdPullRequest)
J
Jingwen Owen Ou 已提交
59 60
}

61 62 63 64 65 66 67 68 69
/*
  # 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

70 71 72
  $ gh pull-request -m "title\n\nbody"
  [ create pull request with title & body  ]

73 74
  $ gh pull-request -i 123
  [ attached pull request to issue #123 ]
75 76 77

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

  $ gh pull-request -F FILE
  [ create pull request with title & body from FILE ]
81
*/
82
func pullRequest(cmd *Command, args *Args) {
83 84
	localRepo, err := github.LocalRepo()
	utils.Check(err)
85 86

	currentBranch, err := localRepo.CurrentBranch()
J
Jingwen Owen Ou 已提交
87
	utils.Check(err)
88 89

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

92 93 94 95
	host, err := github.CurrentConfigs().PromptForHost(baseProject.Host)
	if err != nil {
		utils.Check(github.FormatError("creating pull request", err))
	}
J
Jingwen Owen Ou 已提交
96

97
	trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false)
98 99
	utils.Check(err)

100
	var (
J
Jingwen Owen Ou 已提交
101 102
		base, head string
		force      bool
103
	)
J
Jingwen Owen Ou 已提交
104

105
	force = flagPullRequestForce
J
Jingwen Owen Ou 已提交
106

107
	if flagPullRequestBase != "" {
J
Jingwen Owen Ou 已提交
108
		baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
109
	}
110

111
	if flagPullRequestHead != "" {
J
Jingwen Owen Ou 已提交
112
		headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
113
	}
114

J
Jingwen Owen Ou 已提交
115
	if args.ParamsSize() == 1 {
116
		arg := args.RemoveParam(0)
J
Jingwen Owen Ou 已提交
117
		flagPullRequestIssue = parsePullRequestIssueNumber(arg)
118 119
	}

120
	if base == "" {
J
Jingwen Owen Ou 已提交
121
		masterBranch := localRepo.MasterBranch()
122 123 124
		base = masterBranch.ShortName()
	}

J
Jingwen Owen Ou 已提交
125
	if head == "" && trackedBranch != nil {
J
Jingwen Owen Ou 已提交
126 127 128 129 130
		if !trackedBranch.IsRemote() {
			// the current branch tracking another branch
			// pretend there's no upstream at all
			trackedBranch = nil
		} else {
J
Jingwen Owen Ou 已提交
131 132 133 134
			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)
135 136
			}
		}
J
Jingwen Owen Ou 已提交
137 138
	}

J
Jingwen Owen Ou 已提交
139 140 141 142 143 144
	if head == "" {
		if trackedBranch == nil {
			head = currentBranch.ShortName()
		} else {
			head = trackedBranch.ShortName()
		}
145 146
	}

147
	title, body, err := getTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile)
148
	utils.Check(err)
149

150 151 152
	fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
	fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)

153 154 155 156 157 158 159
	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)
		}
160 161
	}

162
	var editor *github.Editor
J
Jingwen Owen Ou 已提交
163
	if title == "" && flagPullRequestIssue == "" {
164
		commits, _ := git.RefList(base, head)
165 166 167 168 169 170 171
		message, err := pullRequestChangesMessage(base, head, fullBase, fullHead, commits)
		utils.Check(err)

		editor, err = github.NewEditor("PULLREQ", "pull request", message)
		utils.Check(err)

		title, body, err = editor.EditTitleAndBody()
J
Jingwen Owen Ou 已提交
172
		utils.Check(err)
J
Jingwen Owen Ou 已提交
173
	}
174

J
Jingwen Owen Ou 已提交
175 176 177
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
178

179
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
180
	if args.Noop {
181
		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
182
		pullRequestURL = "PULL_REQUEST_URL"
J
Jingwen Owen Ou 已提交
183
	} else {
J
Jingwen Owen Ou 已提交
184 185 186 187 188
		var (
			pr  *octokit.PullRequest
			err error
		)

189
		client := github.NewClientWithHost(host)
J
Jingwen Owen Ou 已提交
190
		if title != "" {
J
Jingwen Owen Ou 已提交
191 192 193
			pr, err = client.CreatePullRequest(baseProject, base, fullHead, title, body)
		} else if flagPullRequestIssue != "" {
			pr, err = client.CreatePullRequestForIssue(baseProject, base, fullHead, flagPullRequestIssue)
J
Jingwen Owen Ou 已提交
194
		}
195

196 197 198 199
		if err == nil && editor != nil {
			defer editor.DeleteFile()
		}

J
Jingwen Owen Ou 已提交
200 201
		utils.Check(err)
		pullRequestURL = pr.HTMLURL
J
Jingwen Owen Ou 已提交
202
	}
203

204 205 206 207 208 209 210 211 212
	if flagPullRequestBrowse {
		launcher, err := utils.BrowserLauncher()
		utils.Check(err)
		args.Replace(launcher[0], "", launcher[1:]...)
		args.AppendParams(pullRequestURL)
	} else {
		args.Replace("echo", "", pullRequestURL)
	}

J
Jingwen Owen Ou 已提交
213 214 215
	if flagPullRequestIssue != "" {
		args.After("echo", "Warning: Issue to pull request conversion is deprecated and might not work in the future.")
	}
J
Jingwen Owen Ou 已提交
216
}
J
Jingwen Owen Ou 已提交
217

218
func pullRequestChangesMessage(base, head, fullBase, fullHead string, commits []string) (string, error) {
219 220
	var defaultMsg, commitSummary string
	if len(commits) == 1 {
J
Jingwen Owen Ou 已提交
221
		msg, err := git.Show(commits[0])
222
		if err != nil {
223
			return "", err
224
		}
J
Jingwen Owen Ou 已提交
225
		defaultMsg = fmt.Sprintf("%s\n", msg)
226
	} else if len(commits) > 1 {
J
Jingwen Owen Ou 已提交
227
		//commentChar := git.CommentChar()
228
		commitLogs, err := git.Log(base, head)
229
		if err != nil {
230
			return "", err
231 232 233 234 235 236 237 238 239 240
		}

		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 已提交
241 242 243
#
# Changes:
#
J
Jingwen Owen Ou 已提交
244
%s`
245 246
			commitSummary = fmt.Sprintf(commitSummary, commitLogs)
		}
247
	}
J
Jingwen Owen Ou 已提交
248

249 250 251 252 253 254
	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
`
255
	message = fmt.Sprintf(message, defaultMsg, fullBase, fullHead, commitSummary)
J
Jingwen Owen Ou 已提交
256

257
	return message, nil
258
}
J
Jingwen Owen Ou 已提交
259

J
Jingwen Owen Ou 已提交
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
const pullRequestTmpl = `{{if .InitMsg}}{{.InitMsg}}{{end}}

{{.CS}} Requesting a pull to {{.Base}} from {{.Head}}
{{.CS}}
{{.CS}} Write a message for this pull request. The first block
{{.CS}} of the text is the title and the rest is description.
{{if .HasChanges}}
{{.CS}}
{{.CS}} Changes:
{{.CS}}
{{range .Changes}}
{{.CS}}
{{end}}
{{end}}
`

type pullRequestMsg struct {
	InitMsg string
	CS      string
	Base    string
	Head    string
}

func pullRequestEditMsg() (string, error) {
	t, err := template.New("pullRequestTmpl").Parse(pullRequestTmpl)
	if err != nil {
		return "", err
	}

	msg := pullRequestMsg{
		InitMsg: "init",
		CS:      "#",
		Base:    "jingweno/gh",
		Head:    "jingweno/gh1",
	}

	var b bytes.Buffer
	err = t.Execute(&b, msg)

	return b.String(), err
}

J
Jingwen Owen Ou 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
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 ""
}