Merge branch 'pr-merge' into master

This commit is contained in:
Mislav Marohnić 2020-08-02 20:44:43 +02:00
Родитель 705360dc0a 8af17bce76
Коммит 222d11022f
7 изменённых файлов: 385 добавлений и 11 удалений

Просмотреть файл

@ -222,7 +222,7 @@ These GitHub commands are provided by hub:
fork Make a fork of a remote repository on GitHub and add as remote
gist Make a gist
issue List or create GitHub issues
pr List or checkout GitHub pull requests
pr Manage GitHub pull requests
pull-request Open a pull request on GitHub
release List or create GitHub releases
sync Fetch git objects from upstream and update branches

Просмотреть файл

@ -19,6 +19,8 @@ change the state of the pull request. However, the pull request will get
auto-closed and marked as "merged" as soon as the newly created merge commit is
pushed to the default branch of the remote repository.
To merge a pull request remotely, use ''hub pr merge''.
## Examples:
$ hub merge https://github.com/jingweno/gh/pull/73
> git fetch origin refs/pull/73/head
@ -26,7 +28,7 @@ pushed to the default branch of the remote repository.
## See also:
hub-checkout(1), hub(1), git-merge(1)
hub-pr(1), hub-checkout(1), hub(1), git-merge(1)
`,
}

Просмотреть файл

@ -19,6 +19,7 @@ pr list [-s <STATE>] [-h <HEAD>] [-b <BASE>] [-o <SORT_KEY> [-^]] [-f <FORMAT>]
pr checkout <PR-NUMBER> [<BRANCH>]
pr show [-uc] [-f <FORMAT>] [-h <HEAD>]
pr show [-uc] [-f <FORMAT>] <PR-NUMBER>
pr merge [-d] [--squash | --rebase] <PR-NUMBER> [-m <MESSAGE> | -F <FILE>] [--head-sha <COMMIT-SHA>]
`,
Long: `Manage GitHub Pull Requests for the current repository.
@ -38,6 +39,11 @@ pr show [-uc] [-f <FORMAT>] <PR-NUMBER>
the current branch name. With ''--format'', print information about the
pull request instead of opening it.
* _merge_:
Merge a pull request in the current repository remotely. Select an
alternate merge method with ''--squash'' or ''--rebase''. Change the
commit subject and body with ''--message'' or ''--file''.
## Options:
-s, --state <STATE>
@ -146,6 +152,29 @@ pr show [-uc] [-f <FORMAT>] <PR-NUMBER>
-c, --copy
Put the pull request URL to clipboard instead of opening it.
-m, --message <MESSAGE>
The text up to the first blank line in <MESSAGE> is treated as the commit
subject for the merge commit, and the rest is used as commit body.
When multiple ''--message'' are passed, their values are concatenated with a
blank line in-between.
-F, --file <FILE>
Read the subject and body for the merge commit from <FILE>. Pass "-" to read
from standard input instead. See ''--message'' for the formatting rules.
--head-sha <COMMIT-SHA>
Ensure that the head of the pull request matches the commit SHA when merging.
--squash
Squash commits instead of creating a merge commit when merging a pull request.
--rebase
Rebase commits on top of the base branch when merging a pull request.
-d, --delete-branch
Delete the head branch after successfully merging a pull request.
## See also:
hub-issue(1), hub-pull-request(1), hub(1)
@ -173,7 +202,20 @@ hub-issue(1), hub-pull-request(1), hub(1)
-c, --copy
-f, --format FORMAT
--color
`,
`,
}
cmdMergePr = &Command{
Key: "merge",
Run: mergePr,
KnownFlags: `
-m, --message MESSAGE
-F, --file FILE
--head-sha COMMIT
--squash
--rebase
-d, --delete-branch
`,
}
)
@ -181,6 +223,7 @@ func init() {
cmdPr.Use(cmdListPulls)
cmdPr.Use(cmdCheckoutPr)
cmdPr.Use(cmdShowPr)
cmdPr.Use(cmdMergePr)
CmdRunner.Use(cmdPr)
}
@ -413,6 +456,70 @@ func deducePushTarget(branch *github.Branch, owner string) (*github.Project, err
return remote.Project()
}
func mergePr(command *Command, args *Args) {
words := args.Words()
if len(words) == 0 {
utils.Check(fmt.Errorf("Error: No pull request number given"))
}
prNumber, err := strconv.Atoi(words[0])
utils.Check(err)
params := map[string]interface{}{
"merge_method": "merge",
}
if args.Flag.Bool("--squash") {
params["merge_method"] = "squash"
}
if args.Flag.Bool("--rebase") {
params["merge_method"] = "rebase"
}
msgs := args.Flag.AllValues("--message")
if len(msgs) > 0 {
params["commit_title"] = msgs[0]
params["commit_message"] = strings.Join(msgs[1:], "\n\n")
} else if args.Flag.HasReceived("--file") {
content, err := msgFromFile(args.Flag.Value("--file"))
utils.Check(err)
params["commit_title"], params["commit_message"] = github.SplitTitleBody(content)
}
if headSHA := args.Flag.Value("--head-sha"); headSHA != "" {
params["sha"] = args.Flag.Value("--head-sha")
}
localRepo, err := github.LocalRepo()
utils.Check(err)
project, err := localRepo.MainProject()
utils.Check(err)
args.NoForward()
if args.Noop {
ui.Printf("Would merge pull request #%d for %s\n", prNumber, project)
return
}
gh := github.NewClient(project.Host)
_, err = gh.MergePullRequest(project, prNumber, params)
utils.Check(err)
if !args.Flag.Bool("--delete-branch") {
return
}
pr, err := gh.PullRequest(project, strconv.Itoa(prNumber))
utils.Check(err)
if !pr.IsSameRepo() {
return
}
branchName := pr.Head.Ref
err = gh.DeleteBranch(project, branchName)
utils.Check(err)
}
func formatPullRequest(pr github.PullRequest, format string, colorize bool) string {
placeholders := formatIssuePlaceholders(github.Issue(pr), colorize)
delete(placeholders, "NC")

215
features/pr-merge.feature Normal file
Просмотреть файл

@ -0,0 +1,215 @@
Feature: hub pr merge
Background:
Given I am in "git://github.com/friederbluemle/hub.git" git repo
And I am "friederbluemle" on github.com with OAuth token "OTOKEN"
Scenario: Default merge
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
assert :merge_method => "merge",
:commit_title => :no,
:commit_message => :no,
:sha => :no
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
"""
When I successfully run `hub pr merge 12`
Then the output should contain exactly ""
Scenario: Squash merge
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
assert :merge_method => "squash",
:commit_title => :no,
:commit_message => :no,
:sha => :no
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
"""
When I successfully run `hub pr merge --squash 12`
Then the output should contain exactly ""
Scenario: Merge with rebase
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
assert :merge_method => "rebase",
:commit_title => :no,
:commit_message => :no,
:sha => :no
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
"""
When I successfully run `hub pr merge --rebase 12`
Then the output should contain exactly ""
Scenario: Merge with title
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
assert :commit_title => "mytitle",
:commit_message => ""
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
"""
When I successfully run `hub pr merge 12 -m mytitle`
Then the output should contain exactly ""
Scenario: Merge with title and body
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
assert :commit_title => "mytitle",
:commit_message => "msg1\n\nmsg2"
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
"""
When I successfully run `hub pr merge 12 -m mytitle -m msg1 -m msg2`
Then the output should contain exactly ""
Scenario: Merge with title and body from file
Given a file named "msg.txt" with:
"""
mytitle
msg1
msg2
"""
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
assert :commit_title => "mytitle",
:commit_message => "msg1\n\nmsg2"
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
"""
When I successfully run `hub pr merge 12 -F msg.txt`
Then the output should contain exactly ""
Scenario: Merge with head SHA
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
assert :sha => "MYSHA"
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
"""
When I successfully run `hub pr merge 12 --head-sha MYSHA`
Then the output should contain exactly ""
Scenario: Delete branch
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
get('/repos/friederbluemle/hub/pulls/12'){
json \
:number => 12,
:state => "merged",
:base => {
:ref => "main",
:label => "friederbluemle:main",
:repo => { :owner => { :login => "friederbluemle" } }
},
:head => {
:ref => "patch-1",
:label => "friederbluemle:patch-1",
:repo => { :owner => { :login => "friederbluemle" } }
}
}
delete('/repos/friederbluemle/hub/git/refs/heads/patch-1'){
status 204
}
"""
When I successfully run `hub pr merge -d 12`
Then the output should contain exactly ""
Scenario: Delete already deleted branch
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
get('/repos/friederbluemle/hub/pulls/12'){
json \
:number => 12,
:state => "merged",
:base => {
:ref => "main",
:label => "friederbluemle:main",
:repo => { :owner => { :login => "friederbluemle" } }
},
:head => {
:ref => "patch-1",
:label => "friederbluemle:patch-1",
:repo => { :owner => { :login => "friederbluemle" } }
}
}
delete('/repos/friederbluemle/hub/git/refs/heads/patch-1'){
status 422
json :message => "Invalid branch name"
}
"""
When I successfully run `hub pr merge -d 12`
Then the output should contain exactly ""
Scenario: Delete branch on cross-repo PR
Given the GitHub API server:
"""
put('/repos/friederbluemle/hub/pulls/12/merge'){
json :merged => true,
:sha => "MERGESHA",
:message => "All done!"
}
get('/repos/friederbluemle/hub/pulls/12'){
json \
:number => 12,
:state => "merged",
:base => {
:ref => "main",
:label => "friederbluemle:main",
:repo => { :owner => { :login => "friederbluemle" } }
},
:head => {
:ref => "patch-1",
:label => "monalisa:patch-1",
:repo => { :owner => { :login => "monalisa" } }
}
}
"""
When I successfully run `hub pr merge -d 12`
Then the output should contain exactly ""

Просмотреть файл

@ -146,6 +146,48 @@ func (client *Client) CreatePullRequest(project *Project, params map[string]inte
return
}
type PullRequestMergeResponse struct {
SHA string
Merged bool
Message string
}
func (client *Client) MergePullRequest(project *Project, prNumber int, params map[string]interface{}) (mr PullRequestMergeResponse, err error) {
api, err := client.simpleAPI()
if err != nil {
return
}
res, err := api.PutJSON(fmt.Sprintf("repos/%s/%s/pulls/%d/merge", project.Owner, project.Name, prNumber), params)
if err = checkStatus(200, "merging pull request", res, err); err != nil {
return
}
defer res.Body.Close()
err = res.Unmarshal(&mr)
return
}
func (client *Client) DeleteBranch(project *Project, branchName string) (err error) {
api, err := client.simpleAPI()
if err != nil {
return
}
res, err := api.Delete(fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", project.Owner, project.Name, branchName))
if err == nil {
defer res.Body.Close()
if res.StatusCode == 422 {
return
}
}
if err = checkStatus(204, "deleting branch", res, err); err != nil {
return
}
return
}
func (client *Client) RequestReview(project *Project, prNumber int, params map[string]interface{}) (err error) {
api, err := client.simpleAPI()
if err != nil {

Просмотреть файл

@ -476,6 +476,10 @@ func (c *simpleClient) PostJSONPreview(path string, payload interface{}, mimeTyp
})
}
func (c *simpleClient) PutJSON(path string, payload interface{}) (*simpleResponse, error) {
return c.jsonRequest("PUT", path, payload, nil)
}
func (c *simpleClient) PatchJSON(path string, payload interface{}) (*simpleResponse, error) {
return c.jsonRequest("PATCH", path, payload, nil)
}

Просмотреть файл

@ -38,14 +38,7 @@ func (b *MessageBuilder) Extract() (title, body string, err error) {
content = nl.ReplaceAllString(content, "\n")
}
parts := strings.SplitN(content, "\n\n", 2)
if len(parts) >= 1 {
title = strings.TrimSpace(strings.Replace(parts[0], "\n", " ", -1))
}
if len(parts) >= 2 {
body = strings.TrimSpace(parts[1])
}
title, body = SplitTitleBody(content)
if title == "" {
defer b.Cleanup()
}
@ -58,3 +51,14 @@ func (b *MessageBuilder) Cleanup() {
b.editor.DeleteFile()
}
}
func SplitTitleBody(content string) (title string, body string) {
parts := strings.SplitN(content, "\n\n", 2)
if len(parts) >= 1 {
title = strings.TrimSpace(strings.Replace(parts[0], "\n", " ", -1))
}
if len(parts) >= 2 {
body = strings.TrimSpace(parts[1])
}
return
}