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

import (
4
	"bufio"
J
Jingwen Owen Ou 已提交
5
	"fmt"
6 7 8 9
	"github.com/jingweno/gh/cmd"
	"github.com/jingweno/gh/git"
	"github.com/jingweno/gh/github"
	"github.com/jingweno/gh/utils"
10
	"io"
J
Jingwen Owen Ou 已提交
11 12
	"io/ioutil"
	"os"
13
	"reflect"
J
Jingwen Owen Ou 已提交
14
	"regexp"
15
	"strings"
J
Jingwen Owen Ou 已提交
16 17
)

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

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

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

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

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

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

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

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

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

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

	currentBranch, err := localRepo.CurrentBranch()
J
Jingwen Owen Ou 已提交
84
	utils.Check(err)
85 86

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

	headProject, err := localRepo.CurrentProject()
	utils.Check(err)

92
	var (
93 94
		base, head           string
		force, explicitOwner bool
95
	)
J
Jingwen Owen Ou 已提交
96

97
	force = flagPullRequestForce
J
Jingwen Owen Ou 已提交
98

99
	if flagPullRequestBase != "" {
J
Jingwen Owen Ou 已提交
100
		baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase)
101
	}
102

103
	if flagPullRequestHead != "" {
J
Jingwen Owen Ou 已提交
104 105
		headProject, head = parsePullRequestProject(headProject, flagPullRequestHead)
		explicitOwner = strings.Contains(flagPullRequestHead, ":")
106
	}
107

J
Jingwen Owen Ou 已提交
108
	if args.ParamsSize() == 1 {
109
		arg := args.RemoveParam(0)
J
Jingwen Owen Ou 已提交
110
		flagPullRequestIssue = parsePullRequestIssueNumber(arg)
111 112
	}

113 114 115 116 117 118
	if base == "" {
		masterBranch, err := localRepo.MasterBranch()
		utils.Check(err)
		base = masterBranch.ShortName()
	}

J
Jingwen Owen Ou 已提交
119
	trackedBranch, _ := currentBranch.Upstream()
120
	if head == "" {
J
Jingwen Owen Ou 已提交
121 122 123 124 125
		if trackedBranch != nil && trackedBranch.IsRemote() {
			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)
126
			}
J
Jingwen Owen Ou 已提交
127 128 129 130
		} else {
			// the current branch tracking another branch
			// pretend there's no upstream at all
			trackedBranch = nil
131 132
		}

J
Jingwen Owen Ou 已提交
133
		if trackedBranch == nil {
134
			head = currentBranch.ShortName()
J
Jingwen Owen Ou 已提交
135 136
		} else {
			head = trackedBranch.ShortName()
137 138 139
		}
	}

J
Jingwen Owen Ou 已提交
140
	client := github.NewClient(baseProject)
141

142
	// when no tracking, assume remote branch is published under active user's fork
J
Jingwen Owen Ou 已提交
143
	if trackedBranch == nil && !explicitOwner && client.Credentials.User != headProject.Owner {
J
Jingwen Owen Ou 已提交
144 145
		// disable this on gh
		//headProject = github.NewProject("", headProject.Name, headProject.Host)
146 147 148
	}

	var title, body string
J
Jingwen Owen Ou 已提交
149

150 151 152 153
	if flagPullRequestMessage != "" {
		title, body = readMsg(flagPullRequestMessage)
	}

154 155 156 157 158 159 160 161 162 163 164 165 166 167
	if flagPullRequestFile != "" {
		var (
			content []byte
			err     error
		)
		if flagPullRequestFile == "-" {
			content, err = ioutil.ReadAll(os.Stdin)
		} else {
			content, err = ioutil.ReadFile(flagPullRequestFile)
		}
		utils.Check(err)
		title, body = readMsg(string(content))
	}

168 169 170
	fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base)
	fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head)

171
	commits, _ := git.RefList(base, head)
J
Jingwen Owen Ou 已提交
172
	if !force && trackedBranch != nil && len(commits) > 0 {
173 174 175 176 177
		err = fmt.Errorf("Aborted: %d commits are not yet pushed to %s", len(commits), trackedBranch.LongName())
		err = fmt.Errorf("%s\n(use `-f` to force submit a pull request anyway)", err)
		utils.Check(err)
	}

J
Jingwen Owen Ou 已提交
178
	if title == "" && flagPullRequestIssue == "" {
179
		t, b, err := writePullRequestTitleAndBody(base, head, fullBase, fullHead, commits)
J
Jingwen Owen Ou 已提交
180 181 182
		utils.Check(err)
		title = t
		body = b
J
Jingwen Owen Ou 已提交
183
	}
184

J
Jingwen Owen Ou 已提交
185 186 187
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
188

189
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
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"
J
Jingwen Owen Ou 已提交
193 194
	} else {
		if title != "" {
J
Jingwen Owen Ou 已提交
195
			pr, err := client.CreatePullRequest(base, fullHead, title, body)
J
Jingwen Owen Ou 已提交
196 197
			utils.Check(err)
			pullRequestURL = pr.HTMLURL
J
Jingwen Owen Ou 已提交
198
		}
199

J
Jingwen Owen Ou 已提交
200
		if flagPullRequestIssue != "" {
J
Jingwen Owen Ou 已提交
201
			pr, err := client.CreatePullRequestForIssue(base, fullHead, flagPullRequestIssue)
J
Jingwen Owen Ou 已提交
202 203
			utils.Check(err)
			pullRequestURL = pr.HTMLURL
J
Jingwen Owen Ou 已提交
204 205
		}
	}
206 207

	args.Replace("echo", "", pullRequestURL)
J
Jingwen Owen Ou 已提交
208
}
J
Jingwen Owen Ou 已提交
209

210
func writePullRequestTitleAndBody(base, head, fullBase, fullHead string, commits []string) (title, body string, err error) {
J
Jingwen Owen Ou 已提交
211 212 213
	messageFile, err := git.PullReqMsgFile()
	if err != nil {
		return
214
	}
215
	defer os.Remove(messageFile)
216

217
	err = writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile)
J
Jingwen Owen Ou 已提交
218 219
	if err != nil {
		return
220 221
	}

J
Jingwen Owen Ou 已提交
222
	editor, err := git.Editor()
J
Jingwen Owen Ou 已提交
223 224
	if err != nil {
		return
225
	}
J
Jingwen Owen Ou 已提交
226

J
Jingwen Owen Ou 已提交
227
	err = editTitleAndBody(editor, messageFile)
J
Jingwen Owen Ou 已提交
228
	if err != nil {
229
		err = fmt.Errorf("error using text editor for pull request message")
J
Jingwen Owen Ou 已提交
230
		return
231 232
	}

J
Jingwen Owen Ou 已提交
233 234 235 236
	title, body, err = readTitleAndBody(messageFile)
	if err != nil {
		return
	}
J
Jingwen Owen Ou 已提交
237

J
Jingwen Owen Ou 已提交
238
	return
239 240
}

241
func writePullRequestChanges(base, head, fullBase, fullHead string, commits []string, messageFile string) error {
242 243
	var defaultMsg, commitSummary string
	if len(commits) == 1 {
244
		defaultMsg, err := git.Show(commits[0])
245 246 247 248 249
		if err != nil {
			return err
		}
		defaultMsg = fmt.Sprintf("%s\n", defaultMsg)
	} else if len(commits) > 1 {
250
		commitLogs, err := git.Log(base, head)
251 252 253 254 255 256 257 258 259 260 261 262
		if err != nil {
			return err
		}

		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 已提交
263 264 265
#
# Changes:
#
J
Jingwen Owen Ou 已提交
266
%s`
267 268
			commitSummary = fmt.Sprintf(commitSummary, commitLogs)
		}
269
	}
J
Jingwen Owen Ou 已提交
270

271 272 273 274 275 276
	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
`
277
	message = fmt.Sprintf(message, defaultMsg, fullBase, fullHead, commitSummary)
J
Jingwen Owen Ou 已提交
278 279 280 281

	return ioutil.WriteFile(messageFile, []byte(message), 0644)
}

J
Jingwen Owen Ou 已提交
282 283
func editTitleAndBody(editor, messageFile string) error {
	editCmd := cmd.New(editor)
J
Jingwen Owen Ou 已提交
284
	r := regexp.MustCompile("[mg]?vi[m]$")
J
Jingwen Owen Ou 已提交
285
	if r.MatchString(editor) {
J
Jingwen Owen Ou 已提交
286
		editCmd.WithArg("-c")
J
Jingwen Owen Ou 已提交
287
		editCmd.WithArg("set ft=gitcommit tw=0 wrap lbr")
J
Jingwen Owen Ou 已提交
288
	}
J
Jingwen Owen Ou 已提交
289
	editCmd.WithArg(messageFile)
J
Jingwen Owen Ou 已提交
290

J
Jingwen Owen Ou 已提交
291
	return editCmd.Exec()
J
Jingwen Owen Ou 已提交
292 293
}

J
Jingwen Owen Ou 已提交
294
func readTitleAndBody(messageFile string) (title, body string, err error) {
295 296
	f, err := os.Open(messageFile)
	defer f.Close()
J
Jingwen Owen Ou 已提交
297
	if err != nil {
298 299 300 301
		return "", "", err
	}

	reader := bufio.NewReader(f)
J
Jingwen Owen Ou 已提交
302

J
Jingwen Owen Ou 已提交
303
	return readTitleAndBodyFrom(reader)
304 305
}

J
Jingwen Owen Ou 已提交
306
func readTitleAndBodyFrom(reader *bufio.Reader) (title, body string, err error) {
307 308 309
	r := regexp.MustCompile("\\S")
	var titleParts, bodyParts []string

J
Jingwen Owen Ou 已提交
310
	line, err := readLine(reader)
311 312 313 314
	for err == nil {
		if strings.HasPrefix(line, "#") {
			break
		}
J
Jingwen Owen Ou 已提交
315

316 317 318 319 320
		if len(bodyParts) == 0 && r.MatchString(line) {
			titleParts = append(titleParts, line)
		} else {
			bodyParts = append(bodyParts, line)
		}
J
Jingwen Owen Ou 已提交
321 322

		line, err = readLine(reader)
323 324
	}

325 326 327 328
	if err == io.EOF {
		err = nil
	}

329 330 331 332 333 334
	title = strings.Join(titleParts, " ")
	title = strings.TrimSpace(title)

	body = strings.Join(bodyParts, "\n")
	body = strings.TrimSpace(body)

335
	return
336 337
}

J
Jingwen Owen Ou 已提交
338
func readLine(r *bufio.Reader) (string, error) {
339
	var (
J
Jingwen Owen Ou 已提交
340 341
		isPrefix = true
		err      error
342 343
		line, ln []byte
	)
J
Jingwen Owen Ou 已提交
344

345 346 347
	for isPrefix && err == nil {
		line, isPrefix, err = r.ReadLine()
		ln = append(ln, line...)
J
Jingwen Owen Ou 已提交
348
	}
J
Jingwen Owen Ou 已提交
349

350
	return string(ln), err
J
Jingwen Owen Ou 已提交
351
}
352 353 354 355 356 357 358 359 360 361

func readMsg(msg string) (title, body string) {
	split := strings.SplitN(msg, "\n\n", 2)
	title = strings.TrimSpace(split[0])
	if len(split) > 1 {
		body = strings.TrimSpace(split[1])
	}

	return
}
J
Jingwen Owen Ou 已提交
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393

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