Merge remote-tracking branch 'origin/master' into issue-pr-create-metadata

This commit is contained in:
Mislav Marohnić 2020-05-08 17:46:17 +02:00
commit d0f168f4c3
25 changed files with 1199 additions and 307 deletions

22
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Code Scanning
on:
push:
schedule:
- cron: "0 0 * * 0"
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View file

@ -5,6 +5,8 @@ import (
"io/ioutil"
"reflect"
"testing"
"github.com/cli/cli/pkg/httpmock"
)
func eq(t *testing.T, got interface{}, expected interface{}) {
@ -15,7 +17,7 @@ func eq(t *testing.T, got interface{}, expected interface{}) {
}
func TestGraphQL(t *testing.T) {
http := &FakeHTTP{}
http := &httpmock.Registry{}
client := NewClient(
ReplaceTripper(http),
AddHeader("Authorization", "token OTOKEN"),
@ -40,7 +42,7 @@ func TestGraphQL(t *testing.T) {
}
func TestGraphQLError(t *testing.T) {
http := &FakeHTTP{}
http := &httpmock.Registry{}
client := NewClient(ReplaceTripper(http))
response := struct{}{}
@ -52,7 +54,7 @@ func TestGraphQLError(t *testing.T) {
}
func TestRESTGetDelete(t *testing.T) {
http := &FakeHTTP{}
http := &httpmock.Registry{}
client := NewClient(
ReplaceTripper(http),

View file

@ -1,101 +0,0 @@
package api
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
)
// FakeHTTP provides a mechanism by which to stub HTTP responses through
type FakeHTTP struct {
// Requests stores references to sequential requests that RoundTrip has received
Requests []*http.Request
count int
responseStubs []*http.Response
}
// StubResponse pre-records an HTTP response
func (f *FakeHTTP) StubResponse(status int, body io.Reader) {
resp := &http.Response{
StatusCode: status,
Body: ioutil.NopCloser(body),
}
f.responseStubs = append(f.responseStubs, resp)
}
// RoundTrip satisfies http.RoundTripper
func (f *FakeHTTP) RoundTrip(req *http.Request) (*http.Response, error) {
if len(f.responseStubs) <= f.count {
return nil, fmt.Errorf("FakeHTTP: missing response stub for request %d", f.count)
}
resp := f.responseStubs[f.count]
f.count++
resp.Request = req
f.Requests = append(f.Requests, req)
return resp, nil
}
func (f *FakeHTTP) StubWithFixture(status int, fixtureFileName string) func() {
fixturePath := path.Join("../test/fixtures/", fixtureFileName)
fixtureFile, _ := os.Open(fixturePath)
f.StubResponse(status, fixtureFile)
return func() { fixtureFile.Close() }
}
func (f *FakeHTTP) StubRepoResponse(owner, repo string) {
f.StubRepoResponseWithPermission(owner, repo, "WRITE")
}
func (f *FakeHTTP) StubRepoResponseWithPermission(owner, repo, permission string) {
body := bytes.NewBufferString(fmt.Sprintf(`
{ "data": { "repo_000": {
"id": "REPOID",
"name": "%s",
"owner": {"login": "%s"},
"defaultBranchRef": {
"name": "master"
},
"viewerPermission": "%s"
} } }
`, repo, owner, permission))
resp := &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(body),
}
f.responseStubs = append(f.responseStubs, resp)
}
func (f *FakeHTTP) StubForkedRepoResponse(forkFullName, parentFullName string) {
forkRepo := strings.SplitN(forkFullName, "/", 2)
parentRepo := strings.SplitN(parentFullName, "/", 2)
body := bytes.NewBufferString(fmt.Sprintf(`
{ "data": { "repo_000": {
"id": "REPOID2",
"name": "%s",
"owner": {"login": "%s"},
"defaultBranchRef": {
"name": "master"
},
"viewerPermission": "ADMIN",
"parent": {
"id": "REPOID1",
"name": "%s",
"owner": {"login": "%s"},
"defaultBranchRef": {
"name": "master"
},
"viewerPermission": "READ"
}
} } }
`, forkRepo[1], forkRepo[0], parentRepo[1], parentRepo[0]))
resp := &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(body),
}
f.responseStubs = append(f.responseStubs, resp)
}

View file

@ -7,10 +7,11 @@ import (
"testing"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/httpmock"
)
func TestIssueList(t *testing.T) {
http := &FakeHTTP{}
http := &httpmock.Registry{}
client := NewClient(ReplaceTripper(http))
http.StubResponse(200, bytes.NewBufferString(`

View file

@ -1,13 +1,29 @@
package api
import (
"context"
"fmt"
"strings"
"time"
"github.com/shurcooL/githubv4"
"github.com/cli/cli/internal/ghrepo"
)
type PullRequestReviewState int
const (
ReviewApprove PullRequestReviewState = iota
ReviewRequestChanges
ReviewComment
)
type PullRequestReviewInput struct {
Body string
State PullRequestReviewState
}
type PullRequestsPayload struct {
ViewerCreated PullRequestAndTotalCount
ReviewRequested PullRequestAndTotalCount
@ -24,6 +40,7 @@ type PullRequest struct {
Number int
Title string
State string
Closed bool
URL string
BaseRefName string
HeadRefName string
@ -345,10 +362,12 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
query($owner: String!, $repo: String!, $pr_number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr_number) {
id
url
number
title
state
closed
body
author {
login
@ -451,6 +470,7 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
repository(owner: $owner, name: $repo) {
pullRequests(headRefName: $headRefName, states: OPEN, first: 30) {
nodes {
id
number
title
state
@ -654,6 +674,34 @@ func isBlank(v interface{}) bool {
}
}
func AddReview(client *Client, pr *PullRequest, input *PullRequestReviewInput) error {
var mutation struct {
AddPullRequestReview struct {
ClientMutationID string
} `graphql:"addPullRequestReview(input:$input)"`
}
state := githubv4.PullRequestReviewEventComment
switch input.State {
case ReviewApprove:
state = githubv4.PullRequestReviewEventApprove
case ReviewRequestChanges:
state = githubv4.PullRequestReviewEventRequestChanges
}
body := githubv4.String(input.Body)
gqlInput := githubv4.AddPullRequestReviewInput{
PullRequestID: pr.ID,
Event: &state,
Body: &body,
}
v4 := githubv4.NewClient(client.http)
return v4.Mutate(context.Background(), &mutation, gqlInput, nil)
}
func PullRequestList(client *Client, vars map[string]interface{}, limit int) (*PullRequestAndTotalCount, error) {
type prBlock struct {
Edges []struct {
@ -822,6 +870,44 @@ loop:
return &res, nil
}
func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
var mutation struct {
ClosePullRequest struct {
PullRequest struct {
ID githubv4.ID
}
} `graphql:"closePullRequest(input: $input)"`
}
input := githubv4.ClosePullRequestInput{
PullRequestID: pr.ID,
}
v4 := githubv4.NewClient(client.http)
err := v4.Mutate(context.Background(), &mutation, input, nil)
return err
}
func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
var mutation struct {
ReopenPullRequest struct {
PullRequest struct {
ID githubv4.ID
}
} `graphql:"reopenPullRequest(input: $input)"`
}
input := githubv4.ReopenPullRequestInput{
PullRequestID: pr.ID,
}
v4 := githubv4.NewClient(client.http)
err := v4.Mutate(context.Background(), &mutation, input, nil)
return err
}
func min(a, b int) int {
if a < b {
return a

View file

@ -5,10 +5,12 @@ import (
"encoding/json"
"io/ioutil"
"testing"
"github.com/cli/cli/pkg/httpmock"
)
func Test_RepoCreate(t *testing.T) {
http := &FakeHTTP{}
http := &httpmock.Registry{}
client := NewClient(ReplaceTripper(http))
http.StubResponse(200, bytes.NewBufferString(`{}`))

View file

@ -6,7 +6,7 @@ import (
)
func TestCompletion_bash(t *testing.T) {
output, err := RunCommand(completionCmd, `completion`)
output, err := RunCommand(`completion`)
if err != nil {
t.Fatal(err)
}
@ -17,7 +17,7 @@ func TestCompletion_bash(t *testing.T) {
}
func TestCompletion_zsh(t *testing.T) {
output, err := RunCommand(completionCmd, `completion -s zsh`)
output, err := RunCommand(`completion -s zsh`)
if err != nil {
t.Fatal(err)
}
@ -28,7 +28,7 @@ func TestCompletion_zsh(t *testing.T) {
}
func TestCompletion_fish(t *testing.T) {
output, err := RunCommand(completionCmd, `completion -s fish`)
output, err := RunCommand(`completion -s fish`)
if err != nil {
t.Fatal(err)
}
@ -39,7 +39,7 @@ func TestCompletion_fish(t *testing.T) {
}
func TestCompletion_powerShell(t *testing.T) {
output, err := RunCommand(completionCmd, `completion -s powershell`)
output, err := RunCommand(`completion -s powershell`)
if err != nil {
t.Fatal(err)
}
@ -50,7 +50,7 @@ func TestCompletion_powerShell(t *testing.T) {
}
func TestCompletion_unsupported(t *testing.T) {
_, err := RunCommand(completionCmd, `completion -s csh`)
_, err := RunCommand(`completion -s csh`)
if err == nil || err.Error() != `unsupported shell type "csh"` {
t.Fatal(err)
}

View file

@ -17,7 +17,7 @@ editor: ed
`
initBlankContext(cfg, "OWNER/REPO", "master")
output, err := RunCommand(configGetCmd, "config get editor")
output, err := RunCommand("config get editor")
if err != nil {
t.Fatalf("error running command `config get editor`: %v", err)
}
@ -27,7 +27,7 @@ editor: ed
func TestConfigGet_default(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
output, err := RunCommand(configGetCmd, "config get git_protocol")
output, err := RunCommand("config get git_protocol")
if err != nil {
t.Fatalf("error running command `config get git_protocol`: %v", err)
}
@ -38,7 +38,7 @@ func TestConfigGet_default(t *testing.T) {
func TestConfigGet_not_found(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
output, err := RunCommand(configGetCmd, "config get missing")
output, err := RunCommand("config get missing")
if err != nil {
t.Fatalf("error running command `config get missing`: %v", err)
}
@ -51,7 +51,7 @@ func TestConfigSet(t *testing.T) {
buf := bytes.NewBufferString("")
defer config.StubWriteConfig(buf)()
output, err := RunCommand(configSetCmd, "config set editor ed")
output, err := RunCommand("config set editor ed")
if err != nil {
t.Fatalf("error running command `config set editor ed`: %v", err)
}
@ -82,7 +82,7 @@ editor: ed
buf := bytes.NewBufferString("")
defer config.StubWriteConfig(buf)()
output, err := RunCommand(configSetCmd, "config set editor vim")
output, err := RunCommand("config set editor vim")
if err != nil {
t.Fatalf("error running command `config get editor`: %v", err)
}
@ -110,7 +110,7 @@ git_protocol: https
`
initBlankContext(cfg, "OWNER/REPO", "master")
output, err := RunCommand(configGetCmd, "config get -hgithub.com git_protocol")
output, err := RunCommand("config get -hgithub.com git_protocol")
if err != nil {
t.Fatalf("error running command `config get editor`: %v", err)
}
@ -130,7 +130,7 @@ git_protocol: ssh
`
initBlankContext(cfg, "OWNER/REPO", "master")
output, err := RunCommand(configGetCmd, "config get -hgithub.com git_protocol")
output, err := RunCommand("config get -hgithub.com git_protocol")
if err != nil {
t.Fatalf("error running command `config get -hgithub.com git_protocol`: %v", err)
}
@ -143,7 +143,7 @@ func TestConfigSetHost(t *testing.T) {
buf := bytes.NewBufferString("")
defer config.StubWriteConfig(buf)()
output, err := RunCommand(configSetCmd, "config set -hgithub.com git_protocol ssh")
output, err := RunCommand("config set -hgithub.com git_protocol ssh")
if err != nil {
t.Fatalf("error running command `config set editor ed`: %v", err)
}
@ -174,7 +174,7 @@ hosts:
buf := bytes.NewBufferString("")
defer config.StubWriteConfig(buf)()
output, err := RunCommand(configSetCmd, "config set -hgithub.com git_protocol https")
output, err := RunCommand("config set -hgithub.com git_protocol https")
if err != nil {
t.Fatalf("error running command `config get editor`: %v", err)
}

View file

@ -87,13 +87,13 @@ With '--web', open the issue in a web browser instead.`,
RunE: issueView,
}
var issueCloseCmd = &cobra.Command{
Use: "close <numberOrURL>",
Use: "close {<number> | <url>}",
Short: "close issue",
Args: cobra.ExactArgs(1),
RunE: issueClose,
}
var issueReopenCmd = &cobra.Command{
Use: "reopen <numberOrURL>",
Use: "reopen {<number> | <url>}",
Short: "reopen issue",
Args: cobra.ExactArgs(1),
RunE: issueReopen,
@ -421,7 +421,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
action := SubmitAction
interactive := title == "" || body == ""
interactive := !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body"))
if interactive {
tb, err := titleBodySurvey(cmd, title, body, defaults{}, templateFiles, !hasMetadata)
@ -443,6 +443,10 @@ func issueCreate(cmd *cobra.Command, args []string) error {
if body == "" {
body = tb.Body
}
} else {
if title == "" {
return fmt.Errorf("title can't be blank")
}
}
if action == PreviewAction {

View file

@ -24,7 +24,7 @@ func TestIssueStatus(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(issueStatusCmd, "issue status")
output, err := RunCommand("issue status")
if err != nil {
t.Errorf("error running command `issue status`: %v", err)
}
@ -58,7 +58,7 @@ func TestIssueStatus_blankSlate(t *testing.T) {
} } }
`))
output, err := RunCommand(issueStatusCmd, "issue status")
output, err := RunCommand("issue status")
if err != nil {
t.Errorf("error running command `issue status`: %v", err)
}
@ -92,7 +92,7 @@ func TestIssueStatus_disabledIssues(t *testing.T) {
} } }
`))
_, err := RunCommand(issueStatusCmd, "issue status")
_, err := RunCommand("issue status")
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Errorf("error running command `issue status`: %v", err)
}
@ -107,7 +107,7 @@ func TestIssueList(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(issueListCmd, "issue list")
output, err := RunCommand("issue list")
if err != nil {
t.Errorf("error running command `issue list`: %v", err)
}
@ -143,7 +143,7 @@ func TestIssueList_withFlags(t *testing.T) {
} } }
`))
output, err := RunCommand(issueListCmd, "issue list -a probablyCher -l web,bug -s open -A foo")
output, err := RunCommand("issue list -a probablyCher -l web,bug -s open -A foo")
if err != nil {
t.Errorf("error running command `issue list`: %v", err)
}
@ -183,7 +183,7 @@ func TestIssueList_nullAssigneeLabels(t *testing.T) {
} } }
`))
_, err := RunCommand(issueListCmd, "issue list")
_, err := RunCommand("issue list")
if err != nil {
t.Errorf("error running command `issue list`: %v", err)
}
@ -211,7 +211,7 @@ func TestIssueList_disabledIssues(t *testing.T) {
} } }
`))
_, err := RunCommand(issueListCmd, "issue list")
_, err := RunCommand("issue list")
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Errorf("error running command `issue list`: %v", err)
}
@ -236,7 +236,7 @@ func TestIssueView_web(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view -w 123")
output, err := RunCommand("issue view -w 123")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
@ -270,7 +270,7 @@ func TestIssueView_web_numberArgWithHash(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view -w \"#123\"")
output, err := RunCommand("issue view -w \"#123\"")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
@ -350,7 +350,7 @@ func TestIssueView_Preview(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(issueViewCmd, tc.command)
output, err := RunCommand(tc.command)
if err != nil {
t.Errorf("error running command `%v`: %v", tc.command, err)
}
@ -379,7 +379,7 @@ func TestIssueView_web_notFound(t *testing.T) {
})
defer restoreCmd()
_, err := RunCommand(issueViewCmd, "issue view -w 9999")
_, err := RunCommand("issue view -w 9999")
if err == nil || err.Error() != "graphql error: 'Could not resolve to an Issue with the number of 9999.'" {
t.Errorf("error running command `issue view`: %v", err)
}
@ -401,7 +401,7 @@ func TestIssueView_disabledIssues(t *testing.T) {
} } }
`))
_, err := RunCommand(issueViewCmd, `issue view 6666`)
_, err := RunCommand(`issue view 6666`)
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Errorf("error running command `issue view`: %v", err)
}
@ -426,7 +426,7 @@ func TestIssueView_web_urlArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(issueViewCmd, "issue view -w https://github.com/OWNER/REPO/issues/123")
output, err := RunCommand("issue view -w https://github.com/OWNER/REPO/issues/123")
if err != nil {
t.Errorf("error running command `issue view`: %v", err)
}
@ -457,7 +457,7 @@ func TestIssueCreate(t *testing.T) {
} } } }
`))
output, err := RunCommand(issueCreateCmd, `issue create -t hello -b "cash rules everything around me"`)
output, err := RunCommand(`issue create -t hello -b "cash rules everything around me"`)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
@ -493,7 +493,7 @@ func TestIssueCreate_disabledIssues(t *testing.T) {
} } }
`))
_, err := RunCommand(issueCreateCmd, `issue create -t heres -b johnny`)
_, err := RunCommand(`issue create -t heres -b johnny`)
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Errorf("error running command `issue create`: %v", err)
}
@ -511,7 +511,7 @@ func TestIssueCreate_web(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(issueCreateCmd, `issue create --web`)
output, err := RunCommand(`issue create --web`)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
@ -537,7 +537,7 @@ func TestIssueCreate_webTitleBody(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(issueCreateCmd, `issue create -w -t mytitle -b mybody`)
output, err := RunCommand(`issue create -w -t mytitle -b mybody`)
if err != nil {
t.Errorf("error running command `issue create`: %v", err)
}
@ -695,7 +695,7 @@ func TestIssueClose(t *testing.T) {
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand(issueCloseCmd, "issue close 13")
output, err := RunCommand("issue close 13")
if err != nil {
t.Fatalf("error running command `issue close`: %v", err)
}
@ -721,7 +721,7 @@ func TestIssueClose_alreadyClosed(t *testing.T) {
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand(issueCloseCmd, "issue close 13")
output, err := RunCommand("issue close 13")
if err != nil {
t.Fatalf("error running command `issue close`: %v", err)
}
@ -744,7 +744,7 @@ func TestIssueClose_issuesDisabled(t *testing.T) {
} } }
`))
_, err := RunCommand(issueCloseCmd, "issue close 13")
_, err := RunCommand("issue close 13")
if err == nil {
t.Fatalf("expected error when issues are disabled")
}
@ -768,7 +768,7 @@ func TestIssueReopen(t *testing.T) {
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand(issueReopenCmd, "issue reopen 2")
output, err := RunCommand("issue reopen 2")
if err != nil {
t.Fatalf("error running command `issue reopen`: %v", err)
}
@ -794,7 +794,7 @@ func TestIssueReopen_alreadyOpen(t *testing.T) {
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand(issueReopenCmd, "issue reopen 2")
output, err := RunCommand("issue reopen 2")
if err != nil {
t.Fatalf("error running command `issue reopen`: %v", err)
}
@ -817,7 +817,7 @@ func TestIssueReopen_issuesDisabled(t *testing.T) {
} } }
`))
_, err := RunCommand(issueReopenCmd, "issue reopen 2")
_, err := RunCommand("issue reopen 2")
if err == nil {
t.Fatalf("expected error when issues are disabled")
}

View file

@ -21,16 +21,18 @@ func init() {
RootCmd.AddCommand(prCmd)
prCmd.AddCommand(prCheckoutCmd)
prCmd.AddCommand(prCreateCmd)
prCmd.AddCommand(prListCmd)
prCmd.AddCommand(prStatusCmd)
prCmd.AddCommand(prViewCmd)
prCmd.AddCommand(prCloseCmd)
prCmd.AddCommand(prReopenCmd)
prCmd.AddCommand(prListCmd)
prListCmd.Flags().IntP("limit", "L", 30, "Maximum number of items to fetch")
prListCmd.Flags().StringP("state", "s", "open", "Filter by state: {open|closed|merged|all}")
prListCmd.Flags().StringP("base", "B", "", "Filter by base branch")
prListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label")
prListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
prCmd.AddCommand(prViewCmd)
prViewCmd.Flags().BoolP("web", "w", false, "Open a pull request in the browser")
}
@ -65,6 +67,18 @@ is displayed.
With '--web', open the pull request in a web browser instead.`,
RunE: prView,
}
var prCloseCmd = &cobra.Command{
Use: "close {<number> | <url> | <branch>}",
Short: "Close a pull request",
Args: cobra.ExactArgs(1),
RunE: prClose,
}
var prReopenCmd = &cobra.Command{
Use: "reopen {<number> | <url> | <branch>}",
Short: "Reopen a pull request",
Args: cobra.ExactArgs(1),
RunE: prReopen,
}
func prStatus(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
@ -101,13 +115,19 @@ func prStatus(cmd *cobra.Command, args []string) error {
fmt.Fprintln(out, "")
printHeader(out, "Current branch")
if prPayload.CurrentPR != nil {
printPrs(out, 0, *prPayload.CurrentPR)
currentPR := prPayload.CurrentPR
currentBranch, _ := ctx.Branch()
noPRMessage := fmt.Sprintf(" There is no pull request associated with %s", utils.Cyan("["+currentPRHeadRef+"]"))
if currentPR != nil {
if baseRepo.(*api.Repository).DefaultBranchRef.Name == currentBranch && currentPR.State != "OPEN" {
printMessage(out, noPRMessage)
} else {
printPrs(out, 0, *currentPR)
}
} else if currentPRHeadRef == "" {
printMessage(out, " There is no current branch")
} else {
message := fmt.Sprintf(" There is no pull request associated with %s", utils.Cyan("["+currentPRHeadRef+"]"))
printMessage(out, message)
printMessage(out, noPRMessage)
}
fmt.Fprintln(out)
@ -328,6 +348,78 @@ func prView(cmd *cobra.Command, args []string) error {
}
}
func prClose(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
pr, err := prFromArg(apiClient, baseRepo, args[0])
if err != nil {
return err
}
if pr.State == "MERGED" {
err := fmt.Errorf("%s Pull request #%d can't be closed because it was already merged", utils.Red("!"), pr.Number)
return err
} else if pr.Closed {
fmt.Fprintf(colorableErr(cmd), "%s Pull request #%d is already closed\n", utils.Yellow("!"), pr.Number)
return nil
}
err = api.PullRequestClose(apiClient, baseRepo, pr)
if err != nil {
return fmt.Errorf("API call failed: %w", err)
}
fmt.Fprintf(colorableErr(cmd), "%s Closed pull request #%d\n", utils.Red("✔"), pr.Number)
return nil
}
func prReopen(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return err
}
pr, err := prFromArg(apiClient, baseRepo, args[0])
if err != nil {
return err
}
if pr.State == "MERGED" {
err := fmt.Errorf("%s Pull request #%d can't be reopened because it was already merged", utils.Red("!"), pr.Number)
return err
}
if !pr.Closed {
fmt.Fprintf(colorableErr(cmd), "%s Pull request #%d is already open\n", utils.Yellow("!"), pr.Number)
return nil
}
err = api.PullRequestReopen(apiClient, baseRepo, pr)
if err != nil {
return fmt.Errorf("API call failed: %w", err)
}
fmt.Fprintf(colorableErr(cmd), "%s Reopened pull request #%d\n", utils.Green("✔"), pr.Number)
return nil
}
func printPrPreview(out io.Writer, pr *api.PullRequest) error {
// Header (Title and State)
fmt.Fprintln(out, utils.Bold(pr.Title))

View file

@ -55,7 +55,7 @@ func TestPRCheckout_sameRepo(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout 123`)
output, err := RunCommand(`pr checkout 123`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -107,7 +107,7 @@ func TestPRCheckout_urlArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout https://github.com/OWNER/REPO/pull/123/files`)
output, err := RunCommand(`pr checkout https://github.com/OWNER/REPO/pull/123/files`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -156,7 +156,7 @@ func TestPRCheckout_urlArg_differentBase(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout https://github.com/OTHER/POE/pull/123/files`)
output, err := RunCommand(`pr checkout https://github.com/OTHER/POE/pull/123/files`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -219,7 +219,7 @@ func TestPRCheckout_branchArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout hubot:feature`)
output, err := RunCommand(`pr checkout hubot:feature`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -269,7 +269,7 @@ func TestPRCheckout_existingBranch(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout 123`)
output, err := RunCommand(`pr checkout 123`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -322,7 +322,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout 123`)
output, err := RunCommand(`pr checkout 123`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -375,7 +375,7 @@ func TestPRCheckout_differentRepo(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout 123`)
output, err := RunCommand(`pr checkout 123`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -428,7 +428,7 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout 123`)
output, err := RunCommand(`pr checkout 123`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -479,7 +479,7 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout 123`)
output, err := RunCommand(`pr checkout 123`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -530,7 +530,7 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prCheckoutCmd, `pr checkout 123`)
output, err := RunCommand(`pr checkout 123`)
eq(t, err, nil)
eq(t, output.String(), "")

View file

@ -9,6 +9,7 @@ import (
"github.com/cli/cli/context"
"github.com/cli/cli/git"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/test"
)
@ -39,7 +40,7 @@ func TestPRCreate(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
output, err := RunCommand(prCreateCmd, `pr create -t "my title" -b "my body"`)
output, err := RunCommand(`pr create -t "my title" -b "my body"`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
@ -101,7 +102,7 @@ func TestPRCreate_withForking(t *testing.T) {
cs.Stub("") // git remote add
cs.Stub("") // git push
output, err := RunCommand(prCreateCmd, `pr create -t title -b body`)
output, err := RunCommand(`pr create -t title -b body`)
eq(t, err, nil)
eq(t, http.Requests[3].URL.Path, "/repos/OWNER/REPO/forks")
@ -132,7 +133,7 @@ func TestPRCreate_alreadyExists(t *testing.T) {
cs.Stub("") // git status
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
_, err := RunCommand(prCreateCmd, `pr create`)
_, err := RunCommand(`pr create`)
if err == nil {
t.Fatal("error expected, got nil")
}
@ -167,7 +168,7 @@ func TestPRCreate_alreadyExistsDifferentBase(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git rev-parse
_, err := RunCommand(prCreateCmd, `pr create -BanotherBase -t"cool" -b"nah"`)
_, err := RunCommand(`pr create -BanotherBase -t"cool" -b"nah"`)
if err != nil {
t.Errorf("got unexpected error %q", err)
}
@ -192,7 +193,7 @@ func TestPRCreate_web(t *testing.T) {
cs.Stub("") // git push
cs.Stub("") // browser
output, err := RunCommand(prCreateCmd, `pr create --web`)
output, err := RunCommand(`pr create --web`)
eq(t, err, nil)
eq(t, output.String(), "")
@ -232,7 +233,7 @@ func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
output, err := RunCommand(prCreateCmd, `pr create -t "my title" -b "my body"`)
output, err := RunCommand(`pr create -t "my title" -b "my body"`)
eq(t, err, nil)
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
@ -301,7 +302,7 @@ func TestPRCreate_cross_repo_same_branch(t *testing.T) {
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
cs.Stub("") // git push
output, err := RunCommand(prCreateCmd, `pr create -t "cross repo" -b "same branch"`)
output, err := RunCommand(`pr create -t "cross repo" -b "same branch"`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
@ -377,7 +378,7 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
},
})
output, err := RunCommand(prCreateCmd, `pr create`)
output, err := RunCommand(`pr create`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
@ -408,20 +409,27 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "forks": { "nodes": [
] } } } }
defer http.Verify(t)
http.Register(httpmock.GraphQL(`\bviewerPermission\b`), httpmock.StringResponse(httpmock.RepoNetworkStubResponse("OWNER", "REPO", "master", "WRITE")))
http.Register(httpmock.GraphQL(`\bforks\(`), httpmock.StringResponse(`
{ "data": { "repository": { "forks": { "nodes": [
] } } } }
`))
http.StubResponse(200, bytes.NewBufferString(`
http.Register(httpmock.GraphQL(`\bpullRequests\(`), httpmock.StringResponse(`
{ "data": { "repository": { "pullRequests": { "nodes" : [
] } } } }
`))
http.StubResponse(200, bytes.NewBufferString(`
http.Register(httpmock.GraphQL(`\bcreatePullRequest\(`), httpmock.GraphQLMutation(`
{ "data": { "createPullRequest": { "pullRequest": {
"URL": "https://github.com/OWNER/REPO/pull/12"
} } } }
`))
`, func(inputs map[string]interface{}) {
eq(t, inputs["repositoryId"], "REPOID")
eq(t, inputs["title"], "the sky above the port")
eq(t, inputs["body"], "was the color of a television, turned to a dead channel")
eq(t, inputs["baseRefName"], "master")
eq(t, inputs["headRefName"], "feature")
}))
cs, cmdTeardown := test.InitCmdStubber()
defer cmdTeardown()
@ -454,31 +462,8 @@ func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
},
})
output, err := RunCommand(prCreateCmd, `pr create`)
output, err := RunCommand(`pr create`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
reqBody := struct {
Variables struct {
Input struct {
RepositoryID string
Title string
Body string
BaseRefName string
HeadRefName string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
expectedBody := "was the color of a television, turned to a dead channel"
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
eq(t, reqBody.Variables.Input.Title, "the sky above the port")
eq(t, reqBody.Variables.Input.Body, expectedBody)
eq(t, reqBody.Variables.Input.BaseRefName, "master")
eq(t, reqBody.Variables.Input.HeadRefName, "feature")
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
}
@ -512,7 +497,7 @@ func TestPRCreate_survey_autofill(t *testing.T) {
cs.Stub("") // git push
cs.Stub("") // browser open
output, err := RunCommand(prCreateCmd, `pr create -f`)
output, err := RunCommand(`pr create -f`)
eq(t, err, nil)
bodyBytes, _ := ioutil.ReadAll(http.Requests[3].Body)
@ -553,7 +538,7 @@ func TestPRCreate_defaults_error_autofill(t *testing.T) {
cs.Stub("") // git status
cs.Stub("") // git log
_, err := RunCommand(prCreateCmd, "pr create -f")
_, err := RunCommand("pr create -f")
eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between origin/master and feature")
}
@ -571,7 +556,7 @@ func TestPRCreate_defaults_error_web(t *testing.T) {
cs.Stub("") // git status
cs.Stub("") // git log
_, err := RunCommand(prCreateCmd, "pr create -w")
_, err := RunCommand("pr create -w")
eq(t, err.Error(), "could not compute title or body defaults: could not find any commits between origin/master and feature")
}
@ -621,7 +606,7 @@ func TestPRCreate_defaults_error_interactive(t *testing.T) {
},
})
output, err := RunCommand(prCreateCmd, `pr create`)
output, err := RunCommand(`pr create`)
eq(t, err, nil)
stderr := string(output.Stderr())

134
command/pr_review.go Normal file
View file

@ -0,0 +1,134 @@
package command
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/cli/cli/api"
"github.com/spf13/cobra"
)
func init() {
prCmd.AddCommand(prReviewCmd)
prReviewCmd.Flags().BoolP("approve", "a", false, "Approve pull request")
prReviewCmd.Flags().BoolP("request-changes", "r", false, "Request changes on a pull request")
prReviewCmd.Flags().BoolP("comment", "c", false, "Comment on a pull request")
prReviewCmd.Flags().StringP("body", "b", "", "Specify the body of a review")
}
var prReviewCmd = &cobra.Command{
Use: "review [{<number> | <url> | <branch>]",
Short: "Add a review to a pull request.",
Args: cobra.MaximumNArgs(1),
Long: `Add a review to either a specified pull request or the pull request associated with the current branch.
Examples:
gh pr review -a # mark the current branch's pull request as approved
gh pr review -c -b "interesting" # comment on the current branch's pull request
gh pr review 123 -r -b "needs more ascii art" # request changes on pull request 123
`,
RunE: prReview,
}
func processReviewOpt(cmd *cobra.Command) (*api.PullRequestReviewInput, error) {
found := 0
flag := ""
var state api.PullRequestReviewState
if cmd.Flags().Changed("approve") {
found++
flag = "approve"
state = api.ReviewApprove
}
if cmd.Flags().Changed("request-changes") {
found++
flag = "request-changes"
state = api.ReviewRequestChanges
}
if cmd.Flags().Changed("comment") {
found++
flag = "comment"
state = api.ReviewComment
}
if found != 1 {
return nil, errors.New("need exactly one of --approve, --request-changes, or --comment")
}
body, err := cmd.Flags().GetString("body")
if err != nil {
return nil, err
}
if (flag == "request-changes" || flag == "comment") && body == "" {
return nil, fmt.Errorf("body cannot be blank for %s review", flag)
}
return &api.PullRequestReviewInput{
Body: body,
State: state,
}, nil
}
func prReview(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
baseRepo, err := determineBaseRepo(cmd, ctx)
if err != nil {
return fmt.Errorf("could not determine base repo: %w", err)
}
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
var prNum int
branchWithOwner := ""
if len(args) == 0 {
prNum, branchWithOwner, err = prSelectorForCurrentBranch(ctx, baseRepo)
if err != nil {
return fmt.Errorf("could not query for pull request for current branch: %w", err)
}
} else {
prArg, repo := prFromURL(args[0])
if repo != nil {
baseRepo = repo
} else {
prArg = strings.TrimPrefix(args[0], "#")
}
prNum, err = strconv.Atoi(prArg)
if err != nil {
return errors.New("could not parse pull request argument")
}
}
input, err := processReviewOpt(cmd)
if err != nil {
return fmt.Errorf("did not understand desired review action: %w", err)
}
var pr *api.PullRequest
if prNum > 0 {
pr, err = api.PullRequestByNumber(apiClient, baseRepo, prNum)
if err != nil {
return fmt.Errorf("could not find pull request: %w", err)
}
} else {
pr, err = api.PullRequestForBranch(apiClient, baseRepo, "", branchWithOwner)
if err != nil {
return fmt.Errorf("could not find pull request: %w", err)
}
}
err = api.AddReview(apiClient, pr, input)
if err != nil {
return fmt.Errorf("failed to create review: %w", err)
}
return nil
}

213
command/pr_review_test.go Normal file
View file

@ -0,0 +1,213 @@
package command
import (
"bytes"
"encoding/json"
"io/ioutil"
"testing"
)
func TestPRReview_validation(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
for _, cmd := range []string{
`pr review 123`,
`pr review --approve --comment 123`,
`pr review --approve --comment -b"hey" 123`,
} {
http.StubRepoResponse("OWNER", "REPO")
_, err := RunCommand(cmd)
eq(t, err.Error(), "did not understand desired review action: need exactly one of --approve, --request-changes, or --comment")
}
}
func TestPRReview_url_arg(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequest": {
"id": "foobar123",
"number": 123,
"headRefName": "feature",
"headRepositoryOwner": {
"login": "hubot"
},
"headRepository": {
"name": "REPO",
"defaultBranchRef": {
"name": "master"
}
},
"isCrossRepository": false,
"maintainerCanModify": false
} } } } `))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
_, err := RunCommand("pr review --approve https://github.com/OWNER/REPO/pull/123")
if err != nil {
t.Fatalf("error running pr review: %s", err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
Input struct {
PullRequestID string
Event string
Body string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.PullRequestID, "foobar123")
eq(t, reqBody.Variables.Input.Event, "APPROVE")
eq(t, reqBody.Variables.Input.Body, "")
}
func TestPRReview_number_arg(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequest": {
"id": "foobar123",
"number": 123,
"headRefName": "feature",
"headRepositoryOwner": {
"login": "hubot"
},
"headRepository": {
"name": "REPO",
"defaultBranchRef": {
"name": "master"
}
},
"isCrossRepository": false,
"maintainerCanModify": false
} } } } `))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
_, err := RunCommand("pr review --approve 123")
if err != nil {
t.Fatalf("error running pr review: %s", err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
Input struct {
PullRequestID string
Event string
Body string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.PullRequestID, "foobar123")
eq(t, reqBody.Variables.Input.Event, "APPROVE")
eq(t, reqBody.Variables.Input.Body, "")
}
func TestPRReview_no_arg(t *testing.T) {
initBlankContext("", "OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [
{ "url": "https://github.com/OWNER/REPO/pull/123",
"id": "foobar123",
"headRefName": "feature",
"baseRefName": "master" }
] } } } }`))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
_, err := RunCommand(`pr review --comment -b "cool story"`)
if err != nil {
t.Fatalf("error running pr review: %s", err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
Input struct {
PullRequestID string
Event string
Body string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.PullRequestID, "foobar123")
eq(t, reqBody.Variables.Input.Event, "COMMENT")
eq(t, reqBody.Variables.Input.Body, "cool story")
}
func TestPRReview_blank_comment(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
_, err := RunCommand(`pr review --comment 123`)
eq(t, err.Error(), "did not understand desired review action: body cannot be blank for comment review")
}
func TestPRReview_blank_request_changes(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
_, err := RunCommand(`pr review -r 123`)
eq(t, err.Error(), "did not understand desired review action: body cannot be blank for request-changes review")
}
func TestPRReview(t *testing.T) {
type c struct {
Cmd string
ExpectedEvent string
ExpectedBody string
}
cases := []c{
c{`pr review --request-changes -b"bad"`, "REQUEST_CHANGES", "bad"},
c{`pr review --approve`, "APPROVE", ""},
c{`pr review --approve -b"hot damn"`, "APPROVE", "hot damn"},
c{`pr review --comment --body "i donno"`, "COMMENT", "i donno"},
}
for _, kase := range cases {
initBlankContext("", "OWNER/REPO", "feature")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "pullRequests": { "nodes": [
{ "url": "https://github.com/OWNER/REPO/pull/123",
"id": "foobar123",
"headRefName": "feature",
"baseRefName": "master" }
] } } } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
_, err := RunCommand(kase.Cmd)
if err != nil {
t.Fatalf("got unexpected error running %s: %s", kase.Cmd, err)
}
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
reqBody := struct {
Variables struct {
Input struct {
Event string
Body string
}
}
}{}
_ = json.Unmarshal(bodyBytes, &reqBody)
eq(t, reqBody.Variables.Input.Event, kase.ExpectedEvent)
eq(t, reqBody.Variables.Input.Body, kase.ExpectedBody)
}
}

View file

@ -15,9 +15,6 @@ import (
"github.com/cli/cli/internal/run"
"github.com/cli/cli/test"
"github.com/google/go-cmp/cmp"
"github.com/google/shlex"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func eq(t *testing.T, got interface{}, expected interface{}) {
@ -27,52 +24,6 @@ func eq(t *testing.T, got interface{}, expected interface{}) {
}
}
type cmdOut struct {
outBuf, errBuf *bytes.Buffer
}
func (c cmdOut) String() string {
return c.outBuf.String()
}
func (c cmdOut) Stderr() string {
return c.errBuf.String()
}
func RunCommand(cmd *cobra.Command, args string) (*cmdOut, error) {
rootCmd := cmd.Root()
argv, err := shlex.Split(args)
if err != nil {
return nil, err
}
rootCmd.SetArgs(argv)
outBuf := bytes.Buffer{}
cmd.SetOut(&outBuf)
errBuf := bytes.Buffer{}
cmd.SetErr(&errBuf)
// Reset flag values so they don't leak between tests
// FIXME: change how we initialize Cobra commands to render this hack unnecessary
cmd.Flags().VisitAll(func(f *pflag.Flag) {
switch v := f.Value.(type) {
case pflag.SliceValue:
_ = v.Replace([]string{})
default:
switch v.Type() {
case "bool", "string", "int":
_ = v.Set(f.DefValue)
}
}
})
_, err = rootCmd.ExecuteC()
cmd.SetOut(nil)
cmd.SetErr(nil)
return &cmdOut{&outBuf, &errBuf}, err
}
func TestPRStatus(t *testing.T) {
initBlankContext("", "OWNER/REPO", "blueberries")
http := initFakeHTTP()
@ -82,7 +33,7 @@ func TestPRStatus(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prStatusCmd, "pr status")
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
@ -120,7 +71,7 @@ branch.blueberries.merge refs/heads/blueberries`)}
}
})()
output, err := RunCommand(prStatusCmd, "pr status")
output, err := RunCommand("pr status")
if err != nil {
t.Fatalf("error running command `pr status`: %v", err)
}
@ -140,7 +91,7 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prStatusCmd, "pr status")
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
@ -167,7 +118,7 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prStatusCmd, "pr status")
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
@ -190,6 +141,27 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
}
}
func TestPRStatus_currentBranch_defaultBranch(t *testing.T) {
initBlankContext("", "OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponseWithDefaultBranch("OWNER", "REPO", "blueberries")
jsonFile, _ := os.Open("../test/fixtures/prStatusCurrentBranch.json")
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
expectedLine := regexp.MustCompile(`#10 Blueberries are certainly a good fruit \[blueberries\]`)
if !expectedLine.MatchString(output.String()) {
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
return
}
}
func TestPRStatus_currentBranch_Closed(t *testing.T) {
initBlankContext("", "OWNER/REPO", "blueberries")
http := initFakeHTTP()
@ -199,7 +171,7 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prStatusCmd, "pr status")
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
@ -211,6 +183,27 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) {
}
}
func TestPRStatus_currentBranch_Closed_defaultBranch(t *testing.T) {
initBlankContext("", "OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponseWithDefaultBranch("OWNER", "REPO", "blueberries")
jsonFile, _ := os.Open("../test/fixtures/prStatusCurrentBranchClosed.json")
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
expectedLine := regexp.MustCompile(`There is no pull request associated with \[blueberries\]`)
if !expectedLine.MatchString(output.String()) {
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
return
}
}
func TestPRStatus_currentBranch_Merged(t *testing.T) {
initBlankContext("", "OWNER/REPO", "blueberries")
http := initFakeHTTP()
@ -220,7 +213,7 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prStatusCmd, "pr status")
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
@ -232,6 +225,27 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) {
}
}
func TestPRStatus_currentBranch_Merged_defaultBranch(t *testing.T) {
initBlankContext("", "OWNER/REPO", "blueberries")
http := initFakeHTTP()
http.StubRepoResponseWithDefaultBranch("OWNER", "REPO", "blueberries")
jsonFile, _ := os.Open("../test/fixtures/prStatusCurrentBranchMerged.json")
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
expectedLine := regexp.MustCompile(`There is no pull request associated with \[blueberries\]`)
if !expectedLine.MatchString(output.String()) {
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", expectedLine, output)
return
}
}
func TestPRStatus_blankSlate(t *testing.T) {
initBlankContext("", "OWNER/REPO", "blueberries")
http := initFakeHTTP()
@ -241,7 +255,7 @@ func TestPRStatus_blankSlate(t *testing.T) {
{ "data": {} }
`))
output, err := RunCommand(prStatusCmd, "pr status")
output, err := RunCommand("pr status")
if err != nil {
t.Errorf("error running command `pr status`: %v", err)
}
@ -273,7 +287,7 @@ func TestPRList(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prListCmd, "pr list")
output, err := RunCommand("pr list")
if err != nil {
t.Fatal(err)
}
@ -296,7 +310,7 @@ func TestPRList_filtering(t *testing.T) {
respBody := bytes.NewBufferString(`{ "data": {} }`)
http.StubResponse(200, respBody)
output, err := RunCommand(prListCmd, `pr list -s all -l one,two -l three`)
output, err := RunCommand(`pr list -s all -l one,two -l three`)
if err != nil {
t.Fatal(err)
}
@ -329,7 +343,7 @@ func TestPRList_filteringRemoveDuplicate(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prListCmd, "pr list -l one,two")
output, err := RunCommand("pr list -l one,two")
if err != nil {
t.Fatal(err)
}
@ -348,7 +362,7 @@ func TestPRList_filteringClosed(t *testing.T) {
respBody := bytes.NewBufferString(`{ "data": {} }`)
http.StubResponse(200, respBody)
_, err := RunCommand(prListCmd, `pr list -s closed`)
_, err := RunCommand(`pr list -s closed`)
if err != nil {
t.Fatal(err)
}
@ -372,7 +386,7 @@ func TestPRList_filteringAssignee(t *testing.T) {
respBody := bytes.NewBufferString(`{ "data": {} }`)
http.StubResponse(200, respBody)
_, err := RunCommand(prListCmd, `pr list -s merged -l "needs tests" -a hubot -B develop`)
_, err := RunCommand(`pr list -s merged -l "needs tests" -a hubot -B develop`)
if err != nil {
t.Fatal(err)
}
@ -396,7 +410,7 @@ func TestPRList_filteringAssigneeLabels(t *testing.T) {
respBody := bytes.NewBufferString(`{ "data": {} }`)
http.StubResponse(200, respBody)
_, err := RunCommand(prListCmd, `pr list -l one,two -a hubot`)
_, err := RunCommand(`pr list -l one,two -a hubot`)
if err == nil && err.Error() != "multiple labels with --assignee are not supported" {
t.Fatal(err)
}
@ -527,7 +541,7 @@ func TestPRView_Preview(t *testing.T) {
defer jsonFile.Close()
http.StubResponse(200, jsonFile)
output, err := RunCommand(prViewCmd, tc.args)
output, err := RunCommand(tc.args)
if err != nil {
t.Errorf("error running command `%v`: %v", tc.args, err)
}
@ -560,7 +574,7 @@ func TestPRView_web_currentBranch(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -w")
output, err := RunCommand("pr view -w")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -598,7 +612,7 @@ func TestPRView_web_noResultsForBranch(t *testing.T) {
})
defer restoreCmd()
_, err := RunCommand(prViewCmd, "pr view -w")
_, err := RunCommand("pr view -w")
if err == nil || err.Error() != `no open pull requests found for branch "blueberries"` {
t.Errorf("error running command `pr view`: %v", err)
}
@ -626,7 +640,7 @@ func TestPRView_web_numberArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -w 23")
output, err := RunCommand("pr view -w 23")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -658,7 +672,7 @@ func TestPRView_web_numberArgWithHash(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -w \"#23\"")
output, err := RunCommand("pr view -w \"#23\"")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -689,7 +703,7 @@ func TestPRView_web_urlArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -w https://github.com/OWNER/REPO/pull/23/files")
output, err := RunCommand("pr view -w https://github.com/OWNER/REPO/pull/23/files")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -723,7 +737,7 @@ func TestPRView_web_branchArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -w blueberries")
output, err := RunCommand("pr view -w blueberries")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -758,7 +772,7 @@ func TestPRView_web_branchWithOwnerArg(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(prViewCmd, "pr view -w hubot:blueberries")
output, err := RunCommand("pr view -w hubot:blueberries")
if err != nil {
t.Errorf("error running command `pr view`: %v", err)
}
@ -800,3 +814,129 @@ func TestPrStateTitleWithColor(t *testing.T) {
})
}
}
func TestPrClose(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
"pullRequest": { "number": 96 }
} } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand("pr close 96")
if err != nil {
t.Fatalf("error running command `pr close`: %v", err)
}
r := regexp.MustCompile(`Closed pull request #96`)
if !r.MatchString(output.Stderr()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPrClose_alreadyClosed(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
"pullRequest": { "number": 101, "closed": true }
} } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand("pr close 101")
if err != nil {
t.Fatalf("error running command `pr close`: %v", err)
}
r := regexp.MustCompile(`Pull request #101 is already closed`)
if !r.MatchString(output.Stderr()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPRReopen(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
"pullRequest": { "number": 666, "closed": true}
} } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand("pr reopen 666")
if err != nil {
t.Fatalf("error running command `pr reopen`: %v", err)
}
r := regexp.MustCompile(`Reopened pull request #666`)
if !r.MatchString(output.Stderr()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPRReopen_alreadyOpen(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
"pullRequest": { "number": 666, "closed": false}
} } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand("pr reopen 666")
if err != nil {
t.Fatalf("error running command `pr reopen`: %v", err)
}
r := regexp.MustCompile(`Pull request #666 is already open`)
if !r.MatchString(output.Stderr()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}
func TestPRReopen_alreadyMerged(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": {
"pullRequest": { "number": 666, "closed": true, "state": "MERGED"}
} } }
`))
http.StubResponse(200, bytes.NewBufferString(`{"id": "THE-ID"}`))
output, err := RunCommand("pr reopen 666")
if err == nil {
t.Fatalf("expected an error running command `pr reopen`: %v", err)
}
r := regexp.MustCompile(`Pull request #666 can't be reopened because it was already merged`)
if !r.MatchString(err.Error()) {
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
}
}

View file

@ -42,7 +42,7 @@ func TestRepoFork_already_forked(t *testing.T) {
http.StubRepoResponse("OWNER", "REPO")
defer http.StubWithFixture(200, "forkResult.json")()
output, err := RunCommand(repoForkCmd, "repo fork --remote=false")
output, err := RunCommand("repo fork --remote=false")
if err != nil {
t.Errorf("got unexpected error: %v", err)
}
@ -69,7 +69,7 @@ func TestRepoFork_reuseRemote(t *testing.T) {
http.StubRepoResponse("OWNER", "REPO")
defer http.StubWithFixture(200, "forkResult.json")()
output, err := RunCommand(repoForkCmd, "repo fork")
output, err := RunCommand("repo fork")
if err != nil {
t.Errorf("got unexpected error: %v", err)
}
@ -97,7 +97,7 @@ func TestRepoFork_in_parent(t *testing.T) {
http.StubRepoResponse("OWNER", "REPO")
defer http.StubWithFixture(200, "forkResult.json")()
output, err := RunCommand(repoForkCmd, "repo fork --remote=false")
output, err := RunCommand("repo fork --remote=false")
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -132,7 +132,7 @@ func TestRepoFork_outside(t *testing.T) {
http := initFakeHTTP()
defer http.StubWithFixture(200, "forkResult.json")()
output, err := RunCommand(repoForkCmd, tt.args)
output, err := RunCommand(tt.args)
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -162,7 +162,7 @@ func TestRepoFork_in_parent_yes(t *testing.T) {
return &test.OutputStub{}
})()
output, err := RunCommand(repoForkCmd, "repo fork --remote")
output, err := RunCommand("repo fork --remote")
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -195,7 +195,7 @@ func TestRepoFork_outside_yes(t *testing.T) {
cs.Stub("") // git clone
cs.Stub("") // git remote add
output, err := RunCommand(repoForkCmd, "repo fork --clone OWNER/REPO")
output, err := RunCommand("repo fork --clone OWNER/REPO")
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -229,7 +229,7 @@ func TestRepoFork_outside_survey_yes(t *testing.T) {
}
defer func() { Confirm = oldConfirm }()
output, err := RunCommand(repoForkCmd, "repo fork OWNER/REPO")
output, err := RunCommand("repo fork OWNER/REPO")
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -263,7 +263,7 @@ func TestRepoFork_outside_survey_no(t *testing.T) {
}
defer func() { Confirm = oldConfirm }()
output, err := RunCommand(repoForkCmd, "repo fork OWNER/REPO")
output, err := RunCommand("repo fork OWNER/REPO")
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -300,7 +300,7 @@ func TestRepoFork_in_parent_survey_yes(t *testing.T) {
}
defer func() { Confirm = oldConfirm }()
output, err := RunCommand(repoForkCmd, "repo fork")
output, err := RunCommand("repo fork")
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -343,7 +343,7 @@ func TestRepoFork_in_parent_survey_no(t *testing.T) {
}
defer func() { Confirm = oldConfirm }()
output, err := RunCommand(repoForkCmd, "repo fork")
output, err := RunCommand("repo fork")
if err != nil {
t.Errorf("error running command `repo fork`: %v", err)
}
@ -469,7 +469,7 @@ func TestRepoClone(t *testing.T) {
cs.Stub("") // git clone
output, err := RunCommand(repoCloneCmd, tt.args)
output, err := RunCommand(tt.args)
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
@ -499,7 +499,7 @@ func TestRepoClone_hasParent(t *testing.T) {
cs.Stub("") // git clone
cs.Stub("") // git remote add
_, err := RunCommand(repoCloneCmd, "repo clone OWNER/REPO")
_, err := RunCommand("repo clone OWNER/REPO")
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
@ -536,7 +536,7 @@ func TestRepoCreate(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoCreateCmd, "repo create REPO")
output, err := RunCommand("repo create REPO")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
@ -605,7 +605,7 @@ func TestRepoCreate_org(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoCreateCmd, "repo create ORG/REPO")
output, err := RunCommand("repo create ORG/REPO")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
@ -674,7 +674,7 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoCreateCmd, "repo create ORG/REPO --team monkeys")
output, err := RunCommand("repo create ORG/REPO --team monkeys")
if err != nil {
t.Errorf("error running command `repo create`: %v", err)
}
@ -725,7 +725,7 @@ func TestRepoView_web(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoViewCmd, "repo view -w")
output, err := RunCommand("repo view -w")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -758,7 +758,7 @@ func TestRepoView_web_ownerRepo(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoViewCmd, "repo view -w cli/cli")
output, err := RunCommand("repo view -w cli/cli")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -790,7 +790,7 @@ func TestRepoView_web_fullURL(t *testing.T) {
})
defer restoreCmd()
output, err := RunCommand(repoViewCmd, "repo view -w https://github.com/cli/cli")
output, err := RunCommand("repo view -w https://github.com/cli/cli")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -820,7 +820,7 @@ func TestRepoView(t *testing.T) {
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}
`))
output, err := RunCommand(repoViewCmd, "repo view")
output, err := RunCommand("repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -848,7 +848,7 @@ func TestRepoView_nonmarkdown_readme(t *testing.T) {
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}
`))
output, err := RunCommand(repoViewCmd, "repo view")
output, err := RunCommand("repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
@ -867,7 +867,7 @@ func TestRepoView_blanks(t *testing.T) {
http.StubResponse(200, bytes.NewBufferString("{}"))
http.StubResponse(200, bytes.NewBufferString("{}"))
output, err := RunCommand(repoViewCmd, "repo view")
output, err := RunCommand("repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}

View file

@ -1,16 +1,19 @@
package command
import (
"bytes"
"errors"
"fmt"
"reflect"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/core"
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/pkg/httpmock"
"github.com/google/shlex"
"github.com/spf13/pflag"
)
const defaultTestConfig = `hosts:
@ -91,14 +94,67 @@ func initBlankContext(cfg, repo, branch string) {
}
}
func initFakeHTTP() *api.FakeHTTP {
http := &api.FakeHTTP{}
func initFakeHTTP() *httpmock.Registry {
http := &httpmock.Registry{}
apiClientForContext = func(context.Context) (*api.Client, error) {
return api.NewClient(api.ReplaceTripper(http)), nil
}
return http
}
type cmdOut struct {
outBuf, errBuf *bytes.Buffer
}
func (c cmdOut) String() string {
return c.outBuf.String()
}
func (c cmdOut) Stderr() string {
return c.errBuf.String()
}
func RunCommand(args string) (*cmdOut, error) {
rootCmd := RootCmd
rootArgv, err := shlex.Split(args)
if err != nil {
return nil, err
}
cmd, _, err := rootCmd.Traverse(rootArgv)
if err != nil {
return nil, err
}
rootCmd.SetArgs(rootArgv)
outBuf := bytes.Buffer{}
cmd.SetOut(&outBuf)
errBuf := bytes.Buffer{}
cmd.SetErr(&errBuf)
// Reset flag values so they don't leak between tests
// FIXME: change how we initialize Cobra commands to render this hack unnecessary
cmd.Flags().VisitAll(func(f *pflag.Flag) {
f.Changed = false
switch v := f.Value.(type) {
case pflag.SliceValue:
_ = v.Replace([]string{})
default:
switch v.Type() {
case "bool", "string", "int":
_ = v.Set(f.DefValue)
}
}
})
_, err = rootCmd.ExecuteC()
cmd.SetOut(nil)
cmd.SetErr(nil)
return &cmdOut{&outBuf, &errBuf}, err
}
type errorStub struct {
message string
}

View file

@ -10,6 +10,7 @@ import (
"github.com/cli/cli/api"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/httpmock"
)
func eq(t *testing.T, got interface{}, expected interface{}) {
@ -70,7 +71,7 @@ func Test_translateRemotes(t *testing.T) {
}
func Test_resolvedRemotes_triangularSetup(t *testing.T) {
http := &api.FakeHTTP{}
http := &httpmock.Registry{}
apiClient := api.NewClient(api.ReplaceTripper(http))
http.StubResponse(200, bytes.NewBufferString(`
@ -137,7 +138,7 @@ func Test_resolvedRemotes_triangularSetup(t *testing.T) {
}
func Test_resolvedRemotes_forkLookup(t *testing.T) {
http := &api.FakeHTTP{}
http := &httpmock.Registry{}
apiClient := api.NewClient(api.ReplaceTripper(http))
http.StubResponse(200, bytes.NewBufferString(`

View file

@ -66,11 +66,12 @@ mainLoop:
// ExtractName returns the name of the template from YAML front-matter
func ExtractName(filePath string) string {
contents, err := ioutil.ReadFile(filePath)
if err == nil && detectFrontmatter(contents)[0] == 0 {
frontmatterBoundaries := detectFrontmatter(contents)
if err == nil && frontmatterBoundaries[0] == 0 {
templateData := struct {
Name string
}{}
if err := yaml.Unmarshal(contents, &templateData); err == nil && templateData.Name != "" {
if err := yaml.Unmarshal(contents[0:frontmatterBoundaries[1]], &templateData); err == nil && templateData.Name != "" {
return templateData.Name
}
}

View file

@ -148,9 +148,7 @@ name: Bug Report
about: This is how you report bugs
---
Template contents
---
More of template
**Template contents**
`,
args: args{
filePath: tmpfile.Name(),

89
pkg/httpmock/legacy.go Normal file
View file

@ -0,0 +1,89 @@
package httpmock
import (
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
)
// TODO: clean up methods in this file when there are no more callers
func (r *Registry) StubResponse(status int, body io.Reader) {
r.Register(MatchAny, func(*http.Request) (*http.Response, error) {
return httpResponse(status, body), nil
})
}
func (r *Registry) StubWithFixture(status int, fixtureFileName string) func() {
fixturePath := path.Join("../test/fixtures/", fixtureFileName)
fixtureFile, err := os.Open(fixturePath)
r.Register(MatchAny, func(*http.Request) (*http.Response, error) {
if err != nil {
return nil, err
}
return httpResponse(200, fixtureFile), nil
})
return func() {
if err == nil {
fixtureFile.Close()
}
}
}
func (r *Registry) StubRepoResponse(owner, repo string) {
r.StubRepoResponseWithPermission(owner, repo, "WRITE")
}
func (r *Registry) StubRepoResponseWithPermission(owner, repo, permission string) {
r.Register(MatchAny, StringResponse(RepoNetworkStubResponse(owner, repo, "master", permission)))
}
func (r *Registry) StubRepoResponseWithDefaultBranch(owner, repo, defaultBranch string) {
r.Register(MatchAny, StringResponse(RepoNetworkStubResponse(owner, repo, defaultBranch, "WRITE")))
}
func (r *Registry) StubForkedRepoResponse(ownRepo, parentRepo string) {
r.Register(MatchAny, StringResponse(RepoNetworkStubForkResponse(ownRepo, parentRepo)))
}
func RepoNetworkStubResponse(owner, repo, defaultBranch, permission string) string {
return fmt.Sprintf(`
{ "data": { "repo_000": {
"id": "REPOID",
"name": "%s",
"owner": {"login": "%s"},
"defaultBranchRef": {
"name": "%s"
},
"viewerPermission": "%s"
} } }
`, repo, owner, defaultBranch, permission)
}
func RepoNetworkStubForkResponse(forkFullName, parentFullName string) string {
forkRepo := strings.SplitN(forkFullName, "/", 2)
parentRepo := strings.SplitN(parentFullName, "/", 2)
return fmt.Sprintf(`
{ "data": { "repo_000": {
"id": "REPOID2",
"name": "%s",
"owner": {"login": "%s"},
"defaultBranchRef": {
"name": "master"
},
"viewerPermission": "ADMIN",
"parent": {
"id": "REPOID1",
"name": "%s",
"owner": {"login": "%s"},
"defaultBranchRef": {
"name": "master"
},
"viewerPermission": "READ"
}
} } }
`, forkRepo[1], forkRepo[0], parentRepo[1], parentRepo[0])
}

70
pkg/httpmock/registry.go Normal file
View file

@ -0,0 +1,70 @@
package httpmock
import (
"fmt"
"net/http"
"sync"
)
type Registry struct {
mu sync.Mutex
stubs []*Stub
Requests []*http.Request
}
func (r *Registry) Register(m Matcher, resp Responder) {
r.stubs = append(r.stubs, &Stub{
Matcher: m,
Responder: resp,
})
}
type Testing interface {
Errorf(string, ...interface{})
}
func (r *Registry) Verify(t Testing) {
n := 0
for _, s := range r.stubs {
if !s.matched {
n++
}
}
if n > 0 {
// NOTE: stubs offer no useful reflection, so we can't print details
// about dead stubs and what they were trying to match
t.Errorf("%d unmatched HTTP stubs", n)
}
}
// RoundTrip satisfies http.RoundTripper
func (r *Registry) RoundTrip(req *http.Request) (*http.Response, error) {
var stub *Stub
r.mu.Lock()
for _, s := range r.stubs {
if s.matched || !s.Matcher(req) {
continue
}
// TODO: reinstate this check once the legacy layer has been cleaned up
// if stub != nil {
// r.mu.Unlock()
// return nil, fmt.Errorf("more than 1 stub matched %v", req)
// }
stub = s
break // TODO: remove
}
if stub != nil {
stub.matched = true
}
if stub == nil {
r.mu.Unlock()
return nil, fmt.Errorf("no registered stubs matched %v", req)
}
r.Requests = append(r.Requests, req)
r.mu.Unlock()
return stub.Responder(req)
}

96
pkg/httpmock/stub.go Normal file
View file

@ -0,0 +1,96 @@
package httpmock
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"regexp"
"strings"
)
type Matcher func(req *http.Request) bool
type Responder func(req *http.Request) (*http.Response, error)
type Stub struct {
matched bool
Matcher Matcher
Responder Responder
}
func MatchAny(*http.Request) bool {
return true
}
func GraphQL(q string) Matcher {
re := regexp.MustCompile(q)
return func(req *http.Request) bool {
if !strings.EqualFold(req.Method, "POST") {
return false
}
if req.URL.Path != "/graphql" {
return false
}
var bodyData struct {
Query string
}
_ = decodeJSONBody(req, &bodyData)
return re.MatchString(bodyData.Query)
}
}
func readBody(req *http.Request) ([]byte, error) {
bodyCopy := &bytes.Buffer{}
r := io.TeeReader(req.Body, bodyCopy)
req.Body = ioutil.NopCloser(bodyCopy)
return ioutil.ReadAll(r)
}
func decodeJSONBody(req *http.Request, dest interface{}) error {
b, err := readBody(req)
if err != nil {
return err
}
return json.Unmarshal(b, dest)
}
func StringResponse(body string) Responder {
return func(*http.Request) (*http.Response, error) {
return httpResponse(200, bytes.NewBufferString(body)), nil
}
}
func JSONResponse(body interface{}) Responder {
return func(*http.Request) (*http.Response, error) {
b, _ := json.Marshal(body)
return httpResponse(200, bytes.NewBuffer(b)), nil
}
}
func GraphQLMutation(body string, cb func(map[string]interface{})) Responder {
return func(req *http.Request) (*http.Response, error) {
var bodyData struct {
Variables struct {
Input map[string]interface{}
}
}
err := decodeJSONBody(req, &bodyData)
if err != nil {
return nil, err
}
cb(bodyData.Variables.Input)
return httpResponse(200, bytes.NewBufferString(body)), nil
}
}
func httpResponse(status int, body io.Reader) *http.Response {
return &http.Response{
StatusCode: status,
Body: ioutil.NopCloser(body),
}
}

View file

@ -9,6 +9,7 @@ import (
"testing"
"github.com/cli/cli/api"
"github.com/cli/cli/pkg/httpmock"
)
func TestCheckForUpdate(t *testing.T) {
@ -51,7 +52,7 @@ func TestCheckForUpdate(t *testing.T) {
for _, s := range scenarios {
t.Run(s.Name, func(t *testing.T) {
http := &api.FakeHTTP{}
http := &httpmock.Registry{}
client := api.NewClient(api.ReplaceTripper(http))
http.StubResponse(200, bytes.NewBufferString(fmt.Sprintf(`{
"tag_name": "%s",