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