github.com/scorpionis/hub@v2.2.1+incompatible/commands/pull_request.go (about) 1 package commands 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 8 "github.com/github/hub/git" 9 "github.com/github/hub/github" 10 "github.com/github/hub/utils" 11 "github.com/github/hub/Godeps/_workspace/src/github.com/octokit/go-octokit/octokit" 12 ) 13 14 var cmdPullRequest = &Command{ 15 Run: pullRequest, 16 Usage: "pull-request [-f] [-m <MESSAGE>|-F <FILE>|-i <ISSUE>|<ISSUE-URL>] [-o] [-b <BASE>] [-h <HEAD>] ", 17 Short: "Open a pull request on GitHub", 18 Long: `Opens a pull request on GitHub for the project that the "origin" remote 19 points to. The default head of the pull request is the current branch. 20 Both base and head of the pull request can be explicitly given in one of 21 the following formats: "branch", "owner:branch", "owner/repo:branch". 22 This command will abort operation if it detects that the current topic 23 branch has local commits that are not yet pushed to its upstream branch 24 on the remote. To skip this check, use "-f". 25 26 Without <MESSAGE> or <FILE>, a text editor will open in which title and body 27 of the pull request can be entered in the same manner as git commit message. 28 Pull request message can also be passed via stdin with "-F -". 29 30 If instead of normal <TITLE> an issue number is given with "-i", the pull 31 request will be attached to an existing GitHub issue. Alternatively, instead 32 of title you can paste a full URL to an issue on GitHub. 33 `, 34 } 35 36 var ( 37 flagPullRequestBase, 38 flagPullRequestHead, 39 flagPullRequestIssue, 40 flagPullRequestMessage, 41 flagPullRequestFile string 42 flagPullRequestBrowse, 43 flagPullRequestForce bool 44 ) 45 46 func init() { 47 cmdPullRequest.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE") 48 cmdPullRequest.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD") 49 cmdPullRequest.Flag.StringVarP(&flagPullRequestIssue, "issue", "i", "", "ISSUE") 50 cmdPullRequest.Flag.BoolVarP(&flagPullRequestBrowse, "browse", "o", false, "BROWSE") 51 cmdPullRequest.Flag.StringVarP(&flagPullRequestMessage, "message", "m", "", "MESSAGE") 52 cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE") 53 cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE") 54 55 CmdRunner.Use(cmdPullRequest) 56 } 57 58 /* 59 # while on a topic branch called "feature": 60 $ gh pull-request 61 [ opens text editor to edit title & body for the request ] 62 [ opened pull request on GitHub for "YOUR_USER:feature" ] 63 64 # explicit pull base & head: 65 $ gh pull-request -b jingweno:master -h jingweno:feature 66 67 $ gh pull-request -m "title\n\nbody" 68 [ create pull request with title & body ] 69 70 $ gh pull-request -i 123 71 [ attached pull request to issue #123 ] 72 73 $ gh pull-request https://github.com/jingweno/gh/pull/123 74 [ attached pull request to issue #123 ] 75 76 $ gh pull-request -F FILE 77 [ create pull request with title & body from FILE ] 78 */ 79 func pullRequest(cmd *Command, args *Args) { 80 localRepo, err := github.LocalRepo() 81 utils.Check(err) 82 83 currentBranch, err := localRepo.CurrentBranch() 84 utils.Check(err) 85 86 baseProject, err := localRepo.MainProject() 87 utils.Check(err) 88 89 host, err := github.CurrentConfig().PromptForHost(baseProject.Host) 90 if err != nil { 91 utils.Check(github.FormatError("creating pull request", err)) 92 } 93 94 trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false) 95 utils.Check(err) 96 97 var ( 98 base, head string 99 force bool 100 ) 101 102 force = flagPullRequestForce 103 104 if flagPullRequestBase != "" { 105 baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase) 106 } 107 108 if flagPullRequestHead != "" { 109 headProject, head = parsePullRequestProject(headProject, flagPullRequestHead) 110 } 111 112 if args.ParamsSize() == 1 { 113 arg := args.RemoveParam(0) 114 flagPullRequestIssue = parsePullRequestIssueNumber(arg) 115 } 116 117 if base == "" { 118 masterBranch := localRepo.MasterBranch() 119 base = masterBranch.ShortName() 120 } 121 122 if head == "" && trackedBranch != nil { 123 if !trackedBranch.IsRemote() { 124 // the current branch tracking another branch 125 // pretend there's no upstream at all 126 trackedBranch = nil 127 } else { 128 if baseProject.SameAs(headProject) && base == trackedBranch.ShortName() { 129 e := fmt.Errorf(`Aborted: head branch is the same as base ("%s")`, base) 130 e = fmt.Errorf("%s\n(use `-h <branch>` to specify an explicit pull request head)", e) 131 utils.Check(e) 132 } 133 } 134 } 135 136 if head == "" { 137 if trackedBranch == nil { 138 head = currentBranch.ShortName() 139 } else { 140 head = trackedBranch.ShortName() 141 } 142 } 143 144 title, body, err := getTitleAndBodyFromFlags(flagPullRequestMessage, flagPullRequestFile) 145 utils.Check(err) 146 147 fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base) 148 fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head) 149 150 if !force && trackedBranch != nil { 151 remoteCommits, _ := git.RefList(trackedBranch.LongName(), "") 152 if len(remoteCommits) > 0 { 153 err = fmt.Errorf("Aborted: %d commits are not yet pushed to %s", len(remoteCommits), trackedBranch.LongName()) 154 err = fmt.Errorf("%s\n(use `-f` to force submit a pull request anyway)", err) 155 utils.Check(err) 156 } 157 } 158 159 var editor *github.Editor 160 if title == "" && flagPullRequestIssue == "" { 161 baseTracking := base 162 headTracking := head 163 164 remote := gitRemoteForProject(baseProject) 165 if remote != nil { 166 baseTracking = fmt.Sprintf("%s/%s", remote.Name, base) 167 } 168 if remote == nil || !baseProject.SameAs(headProject) { 169 remote = gitRemoteForProject(headProject) 170 } 171 if remote != nil { 172 headTracking = fmt.Sprintf("%s/%s", remote.Name, head) 173 } 174 175 message, err := pullRequestChangesMessage(baseTracking, headTracking, fullBase, fullHead) 176 utils.Check(err) 177 178 editor, err = github.NewEditor("PULLREQ", "pull request", message) 179 utils.Check(err) 180 181 title, body, err = editor.EditTitleAndBody() 182 utils.Check(err) 183 } 184 185 if title == "" && flagPullRequestIssue == "" { 186 utils.Check(fmt.Errorf("Aborting due to empty pull request title")) 187 } 188 189 var pullRequestURL string 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" 193 } else { 194 var ( 195 pr *octokit.PullRequest 196 err error 197 ) 198 199 client := github.NewClientWithHost(host) 200 if title != "" { 201 pr, err = client.CreatePullRequest(baseProject, base, fullHead, title, body) 202 } else if flagPullRequestIssue != "" { 203 pr, err = client.CreatePullRequestForIssue(baseProject, base, fullHead, flagPullRequestIssue) 204 } 205 206 if err == nil && editor != nil { 207 defer editor.DeleteFile() 208 } 209 210 utils.Check(err) 211 pullRequestURL = pr.HTMLURL 212 } 213 214 if flagPullRequestBrowse { 215 launcher, err := utils.BrowserLauncher() 216 utils.Check(err) 217 args.Replace(launcher[0], "", launcher[1:]...) 218 args.AppendParams(pullRequestURL) 219 } else { 220 args.Replace("echo", "", pullRequestURL) 221 } 222 223 if flagPullRequestIssue != "" { 224 args.After("echo", "Warning: Issue to pull request conversion is deprecated and might not work in the future.") 225 } 226 } 227 228 func pullRequestChangesMessage(base, head, fullBase, fullHead string) (string, error) { 229 var ( 230 defaultMsg string 231 commitLogs string 232 err error 233 ) 234 235 commits, _ := git.RefList(base, head) 236 if len(commits) == 1 { 237 defaultMsg, err = git.Show(commits[0]) 238 if err != nil { 239 return "", err 240 } 241 } else if len(commits) > 1 { 242 commitLogs, err = git.Log(base, head) 243 if err != nil { 244 return "", err 245 } 246 } 247 248 cs := git.CommentChar() 249 250 return renderPullRequestTpl(defaultMsg, cs, fullBase, fullHead, commitLogs) 251 } 252 253 func parsePullRequestProject(context *github.Project, s string) (p *github.Project, ref string) { 254 p = context 255 ref = s 256 257 if strings.Contains(s, ":") { 258 split := strings.SplitN(s, ":", 2) 259 ref = split[1] 260 var name string 261 if !strings.Contains(split[0], "/") { 262 name = context.Name 263 } 264 p = github.NewProject(split[0], name, context.Host) 265 } 266 267 return 268 } 269 270 func parsePullRequestIssueNumber(url string) string { 271 u, e := github.ParseURL(url) 272 if e != nil { 273 return "" 274 } 275 276 r := regexp.MustCompile(`^issues\/(\d+)`) 277 p := u.ProjectPath() 278 if r.MatchString(p) { 279 return r.FindStringSubmatch(p)[1] 280 } 281 282 return "" 283 }