pull_request.go 8.1 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"
J
Jingwen Owen Ou 已提交
10 11
	"io/ioutil"
	"os"
12
	"reflect"
J
Jingwen Owen Ou 已提交
13
	"regexp"
14
	"strings"
J
Jingwen Owen Ou 已提交
15 16
)

17 18
var cmdPullRequest = &Command{
	Run:   pullRequest,
19
	Usage: "pull-request [-f] [-i ISSUE] [-b BASE] [-d HEAD] [-m MESSAGE] [TITLE]",
J
Jingwen Owen Ou 已提交
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
	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.
`,
}

38
var flagPullRequestBase, flagPullRequestHead, flagPullRequestIssue, flagPullRequestMessage string
J
Jingwen Owen Ou 已提交
39 40

func init() {
41 42 43
	cmdPullRequest.Flag.StringVar(&flagPullRequestBase, "b", "master", "BASE")
	cmdPullRequest.Flag.StringVar(&flagPullRequestHead, "d", "", "HEAD")
	cmdPullRequest.Flag.StringVar(&flagPullRequestIssue, "i", "", "ISSUE")
44
	cmdPullRequest.Flag.StringVar(&flagPullRequestMessage, "m", "", "MESSAGE")
J
Jingwen Owen Ou 已提交
45 46
}

47 48 49 50 51 52 53 54 55
/*
  # 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

56 57 58
  $ gh pull-request -m "title\n\nbody"
  [ create pull request with title & body  ]

59 60
  $ gh pull-request -i 123
  [ attached pull request to issue #123 ]
61 62 63

  $ gh pull-request https://github.com/jingweno/gh/pull/123
  [ attached pull request to issue #123 ]
64
*/
65
func pullRequest(cmd *Command, args *Args) {
66 67 68
	localRepo := github.LocalRepo()

	currentBranch, err := localRepo.CurrentBranch()
J
Jingwen Owen Ou 已提交
69
	utils.Check(err)
70 71

	baseProject, err := localRepo.MainProject()
J
Jingwen Owen Ou 已提交
72
	utils.Check(err)
73 74 75 76 77 78 79 80 81 82 83 84 85 86

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

	gh := github.NewWithoutProject()
	gh.Project = baseProject

	var base, head string
	if flagPullRequestBase != "" {
		base = flagPullRequestBase
	}
	if flagPullRequestHead != "" {
		head = flagPullRequestHead
	}
J
Jingwen Owen Ou 已提交
87
	if args.ParamsSize() == 1 {
88 89 90 91 92 93 94
		arg := args.RemoveParam(0)
		u, e := github.ParseURL(arg)
		r := regexp.MustCompile(`^issues\/(\d+)`)
		p := u.ProjectPath()
		if e == nil && r.MatchString(p) {
			flagPullRequestIssue = r.FindStringSubmatch(p)[1]
		}
95 96
	}

97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
	if base == "" {
		masterBranch, err := localRepo.MasterBranch()
		utils.Check(err)
		base = masterBranch.ShortName()
	}

	trackedBranch, tberr := currentBranch.Upstream()
	if head == "" {
		if err == nil {
			if 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)
				}
			} else {
				// the current branch tracking another branch
				// pretend there's no upstream at all
				tberr = fmt.Errorf("No upstream found for current branch")
			}
		}

		if tberr == nil {
			head = trackedBranch.ShortName()
		} else {
			head = currentBranch.ShortName()
		}
	}

	// when no tracking, assume remote branch is published under active user's fork
	if tberr != nil && gh.Config.User != headProject.Owner {
		headProject = github.NewProjectFromNameAndOwner(headProject.Name, "")
	}

	var title, body string
	if flagPullRequestMessage != "" {
		title, body = readMsg(flagPullRequestMessage)
	}

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

J
Jingwen Owen Ou 已提交
139
	if title == "" && flagPullRequestIssue == "" {
140
		t, b, err := writePullRequestTitleAndBody(base, head, fullBase, fullHead)
J
Jingwen Owen Ou 已提交
141 142 143
		utils.Check(err)
		title = t
		body = b
J
Jingwen Owen Ou 已提交
144
	}
145

J
Jingwen Owen Ou 已提交
146 147 148
	if title == "" && flagPullRequestIssue == "" {
		utils.Check(fmt.Errorf("Aborting due to empty pull request title"))
	}
J
Jingwen Owen Ou 已提交
149

150
	var pullRequestURL string
J
Jingwen Owen Ou 已提交
151
	if args.Noop {
152
		args.Before(fmt.Sprintf("Would request a pull request to %s from %s", fullBase, fullHead), "")
153
		pullRequestURL = "PULL_REQUEST_URL"
J
Jingwen Owen Ou 已提交
154 155
	} else {
		if title != "" {
156
			pr, err := gh.CreatePullRequest(base, fullHead, title, body)
J
Jingwen Owen Ou 已提交
157 158
			utils.Check(err)
			pullRequestURL = pr.HTMLURL
J
Jingwen Owen Ou 已提交
159
		}
160

J
Jingwen Owen Ou 已提交
161
		if flagPullRequestIssue != "" {
162
			pr, err := gh.CreatePullRequestForIssue(base, fullHead, flagPullRequestIssue)
J
Jingwen Owen Ou 已提交
163 164
			utils.Check(err)
			pullRequestURL = pr.HTMLURL
J
Jingwen Owen Ou 已提交
165 166
		}
	}
167 168

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

171
func writePullRequestTitleAndBody(base, head, fullBase, fullHead string) (title, body string, err error) {
J
Jingwen Owen Ou 已提交
172 173 174
	messageFile, err := git.PullReqMsgFile()
	if err != nil {
		return
175
	}
176
	defer os.Remove(messageFile)
177

178
	err = writePullRequestChanges(base, head, fullBase, fullHead, messageFile)
J
Jingwen Owen Ou 已提交
179 180
	if err != nil {
		return
181 182
	}

J
Jingwen Owen Ou 已提交
183
	editor, err := git.Editor()
J
Jingwen Owen Ou 已提交
184 185
	if err != nil {
		return
186
	}
J
Jingwen Owen Ou 已提交
187

J
Jingwen Owen Ou 已提交
188
	err = editTitleAndBody(editor, messageFile)
J
Jingwen Owen Ou 已提交
189
	if err != nil {
190
		err = fmt.Errorf("error using text editor for pull request message")
J
Jingwen Owen Ou 已提交
191
		return
192 193
	}

J
Jingwen Owen Ou 已提交
194 195 196 197
	title, body, err = readTitleAndBody(messageFile)
	if err != nil {
		return
	}
J
Jingwen Owen Ou 已提交
198

J
Jingwen Owen Ou 已提交
199
	return
200 201
}

202 203
func writePullRequestChanges(base, head, fullBase, fullHead string, messageFile string) error {
	commits, _ := git.RefList(base, head)
204 205 206

	var defaultMsg, commitSummary string
	if len(commits) == 1 {
207
		defaultMsg, err := git.Show(commits[0])
208 209 210 211 212
		if err != nil {
			return err
		}
		defaultMsg = fmt.Sprintf("%s\n", defaultMsg)
	} else if len(commits) > 1 {
213
		commitLogs, err := git.Log(base, head)
214 215 216 217 218 219 220 221 222 223 224 225
		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 已提交
226 227 228
#
# Changes:
#
J
Jingwen Owen Ou 已提交
229
%s`
230 231
			commitSummary = fmt.Sprintf(commitSummary, commitLogs)
		}
232
	}
J
Jingwen Owen Ou 已提交
233

234 235 236 237 238 239
	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
`
240
	message = fmt.Sprintf(message, defaultMsg, fullBase, fullHead, commitSummary)
J
Jingwen Owen Ou 已提交
241 242 243 244

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

J
Jingwen Owen Ou 已提交
245 246
func editTitleAndBody(editor, messageFile string) error {
	editCmd := cmd.New(editor)
J
Jingwen Owen Ou 已提交
247
	r := regexp.MustCompile("[mg]?vi[m]$")
J
Jingwen Owen Ou 已提交
248
	if r.MatchString(editor) {
J
Jingwen Owen Ou 已提交
249
		editCmd.WithArg("-c")
J
Jingwen Owen Ou 已提交
250
		editCmd.WithArg("set ft=gitcommit tw=0 wrap lbr")
J
Jingwen Owen Ou 已提交
251
	}
J
Jingwen Owen Ou 已提交
252
	editCmd.WithArg(messageFile)
J
Jingwen Owen Ou 已提交
253

J
Jingwen Owen Ou 已提交
254
	return editCmd.Exec()
J
Jingwen Owen Ou 已提交
255 256
}

J
Jingwen Owen Ou 已提交
257
func readTitleAndBody(messageFile string) (title, body string, err error) {
258 259
	f, err := os.Open(messageFile)
	defer f.Close()
J
Jingwen Owen Ou 已提交
260
	if err != nil {
261 262 263 264
		return "", "", err
	}

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

J
Jingwen Owen Ou 已提交
266
	return readTitleAndBodyFrom(reader)
267 268
}

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

J
Jingwen Owen Ou 已提交
273
	line, err := readLine(reader)
274 275 276 277
	for err == nil {
		if strings.HasPrefix(line, "#") {
			break
		}
J
Jingwen Owen Ou 已提交
278

279 280 281 282 283
		if len(bodyParts) == 0 && r.MatchString(line) {
			titleParts = append(titleParts, line)
		} else {
			bodyParts = append(bodyParts, line)
		}
J
Jingwen Owen Ou 已提交
284 285

		line, err = readLine(reader)
286 287 288 289 290 291 292 293 294 295 296
	}

	title = strings.Join(titleParts, " ")
	title = strings.TrimSpace(title)

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

	return title, body, nil
}

J
Jingwen Owen Ou 已提交
297
func readLine(r *bufio.Reader) (string, error) {
298
	var (
J
Jingwen Owen Ou 已提交
299 300
		isPrefix = true
		err      error
301 302
		line, ln []byte
	)
J
Jingwen Owen Ou 已提交
303

304 305 306
	for isPrefix && err == nil {
		line, isPrefix, err = r.ReadLine()
		ln = append(ln, line...)
J
Jingwen Owen Ou 已提交
307
	}
J
Jingwen Owen Ou 已提交
308

309
	return string(ln), err
J
Jingwen Owen Ou 已提交
310
}
311 312 313 314 315 316 317 318 319 320

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
}